'Double' instantiation of Button crashes app on Android in spite of workaround attempt
Hi!
I think this is a bug, but nobody on the KIvy Support Google Group had both the desire and means to try to reproduce it - https://groups.google.com/g/kivy-users/c/ywPwUnT8aZs - so I think posting it here is the best next step.
Software Versions
- Python: On Linux: 3.11.9 (and 3.8.?)
- OS: Ubuntu 20.04.6 LTS
- Kivy: 2.3.0
- Kivy installation method: (Not sure. That was 4 years ago...)
Describe the bug The app works on Linux. But when I deploy it on my (old) MOTO G5 Android OS v.8.1.0 phone the app acts as if a certain Button gets instantiated twice, and although I use a flag to prevent the code in the on_release method to be executed twice, the app crashes, because it acts as if the code was executed twice anyway...! VERY strange behavior.
Expected behavior That ONE instantiation of 'class KMLSendButton(ScalableButton):' leads to ONE button on the screen, and that ONE touch on the Button leads to ONE execution of the on_release method, and that ONE execution of the code in the on_release method leads to ONE instantiation of further widgets... None of this happened - on Android - but on Linux (and Windows, according to another user).
To Reproduce The following zip-file contains the app-folder, with main.py, kv-file and buildozer.spec:
https://www.transformation.dk/wp-content/uploads/2024/08/Double-KMLSendButton-problem-4.zip
I think I have done everything to make it a minimal runnable example of the problem. Note that the zip-file is large because it happens to include a venv and an apk-file. Also note there are 6 classes that will only be activated if the app suddenly works...! :-)
Note that you have to use the modified version of python-for-android (v.2024.01.21) in the zip-file below, where the very specific filehandling of Android has been added. Also note that the minimal runnable example doesn't actually do any filehandling any more. I think the *.pyc files were created by my Buildozer, so MAYBE you need to delete them? I don't know if pyc-files are specific in some way?:
https://www.transformation.dk/wp-content/uploads/2024/08/python-for-android-master.zip
NOTE: Remember to insert the path to the modified python-for-android in the buildozer.spec here: p4a.source_dir = /PATH-TO-MODIFIED/python-for-android-master/
Code and Logs and screenshots The python code etc. is in the zip-file above.
To run the app (in Terminal, in the app-folder) I use: $ buildozer android debug deploy run
main.py contains some print statements that makes it possible to see the problem. Here is the part of the Android log that shows the print statements that show the problem. It shows that although KMLSendButton is instantiated once, the on_release() method runs twice, and even though I 'catch' the second run with the 'self.first_touch' flag, the app continues as if the code in the on_release() method ran twice!:
python : RootLayout(BoxLayout).init (platform==Android) instantiates KMLSendButton()
python : Line 1082: KMLSendButton.on_release() second time!
python : app.root.send_KML_done() is called in TestDoneButton(ScalableButton).on_release(self) python : ChooseTarget(True) is instantiated in RootLayout(BoxLayout).send_KML_done(self) python : ChooseTarget(BoxLayout).init(self, first_run, **kwargs) python : ChooseTarget(BoxLayout).my_init(self): InputOkYesNo(BoxLayout) is instantiated now.
python : app.root.send_KML_done() is called in TestDoneButton(ScalableButton).on_release(self) python : ChooseTarget(True) is instantiated in RootLayout(BoxLayout).send_KML_done(self) python : ChooseTarget(BoxLayout).init(self, first_run, **kwargs) python : ChooseTarget(BoxLayout).my_init(self): InputOkYesNo(BoxLayout) is instantiated now.
Below is the whole log from Android, that shows the problem. It was generated using the following command in Terminal (on Linux):
$ /home/henrik/.buildozer/android/platform/android-sdk/platform-tools/adb logcat | grep -w "python" | grep -Fv "extracting"
python : Initializing Python for Android
python : Setting additional env vars from p4a_env_vars.txt
python : Changing directory to the one provided by ANDROID_ARGUMENT
python : /data/user/0/dk.transformation.geoesptraining/files/app
python : Preparing to initialize python
python : _python_bundle dir exists
python : calculated paths to be...
python : /data/user/0/dk.transformation.geoesptraining/files/app/_python_bundle/stdlib.zip:/data/user/0/dk.transformation.geoesptraining/files/app/_python_bundle/modules
python : set wchar paths...
python : Initialized python
python : AND: Init threads
python : testing python print redirection
python : Android path ['.', '/data/user/0/dk.transformation.geoesptraining/files/app/_python_bundle/stdlib.zip', '/data/user/0/dk.transformation.geoesptraining/files/app/_python_bundle/modules', '/data/user/0/dk.transformation.geoesptraining/files/app/_python_bundle/site-packages']
python : os.environ is environ({'PATH': '/sbin:/system/sbin:/system/bin:/system/xbin:/vendor/bin:/vendor/xbin', 'DOWNLOAD_CACHE': '/data/cache', 'ANDROID_BOOTLOGO': '1', 'ANDROID_ROOT': '/system', 'ANDROID_ASSETS': '/system/app', 'ANDROID_DATA': '/data', 'ANDROID_STORAGE': '/storage', 'EXTERNAL_STORAGE': '/sdcard', 'ASEC_MOUNTPOINT': '/mnt/asec', 'BOOTCLASSPATH': '/system/framework/QPerformance.jar:/system/framework/qcom.fmradio.jar:/system/framework/oem-services.jar:/system/framework/tcmiface.jar:/system/framework/telephony-ext.jar:/system/framework/core-oj.jar:/system/framework/core-libart.jar:/system/framework/conscrypt.jar:/system/framework/okhttp.jar:/system/framework/bouncycastle.jar:/system/framework/apache-xml.jar:/system/framework/legacy-test.jar:/system/framework/ext.jar:/system/framework/framework.jar:/system/framework/telephony-common.jar:/system/framework/voip-common.jar:/system/framework/ims-common.jar:/system/framework/org.apache.http.legacy.boot.jar:/system/framework/android.hidl.base-V1.0-java.jar:/system/framework/android.hidl.manager-V1.0-java.jar', 'SYSTEMSERVERCLASSPATH': '/system/framework/services.jar:/system/framework/ethernet-service.jar:/system/framework/wifi-service.jar:/system/framework/com.android.location.provider.jar', 'ANDROID_SOCKET_zygote': '9', 'ANDROID_ENTRYPOINT': 'main.pyc', 'ANDROID_ARGUMENT': '/data/user/0/dk.transformation.geoesptraining/files/app', 'ANDROID_APP_PATH': '/data/user/0/dk.transformation.geoesptraining/files/app', 'ANDROID_PRIVATE': '/data/user/0/dk.transformation.geoesptraining/files', 'ANDROID_UNPACK': '/data/user/0/dk.transformation.geoesptraining/files/app', 'PYTHONHOME': '/data/user/0/dk.transformation.geoesptraining/files/app', 'PYTHONPATH': '/data/user/0/dk.transformation.geoesptraining/files/app:/data/user/0/dk.transformation.geoesptraining/files/app/lib', 'PYTHONOPTIMIZE': '2', 'P4A_BOOTSTRAP': 'SDL2', 'PYTHON_NAME': 'python', 'P4A_IS_WINDOWED': 'True', 'KIVY_ORIENTATION': 'Portrait LandscapeLeft', 'P4A_NUMERIC_VERSION': 'None', 'P4A_MINSDK': '21', 'LC_CTYPE': 'C.UTF-8'})
python : Android kivy bootstrap done. __name__ is __main__
python : AND: Ran string
python : Run user program, change dir and execute entrypoint
python : [ERROR ] Error when copying logo directory
python : Traceback (most recent call last):
python : File "/mnt/4AF15A0435E762B4/mypython/Double-KMLSendButton-to-run/.buildozer/android/platform/build-armeabi-v7a_arm64-v8a/build/python-installs/geoesptraining/armeabi-v7a/kivy/__init__.py", line 372, in <module>
python : File "/mnt/4AF15A0435E762B4/mypython/Double-KMLSendButton-to-run/.buildozer/android/platform/build-armeabi-v7a_arm64-v8a/build/other_builds/python3/armeabi-v7a__ndk_target_21/python3/Lib/shutil.py", line 561, in copytree
python : File "/mnt/4AF15A0435E762B4/mypython/Double-KMLSendButton-to-run/.buildozer/android/platform/build-armeabi-v7a_arm64-v8a/build/other_builds/python3/armeabi-v7a__ndk_target_21/python3/Lib/shutil.py", line 515, in _copytree
python : shutil.Error: [('/data/user/0/dk.transformation.geoesptraining/files/app/_python_bundle/site-packages/kivy/data/logo/kivy-icon-128.png', '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon/kivy-icon-128.png', "[Errno 13] Permission denied: '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon/kivy-icon-128.png'"), ('/data/user/0/dk.transformation.geoesptraining/files/app/_python_bundle/site-packages/kivy/data/logo/kivy-icon-16.png', '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon/kivy-icon-16.png', "[Errno 13] Permission denied: '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon/kivy-icon-16.png'"), ('/data/user/0/dk.transformation.geoesptraining/files/app/_python_bundle/site-packages/kivy/data/logo/kivy-icon-24.png', '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon/kivy-icon-24.png', "[Errno 13] Permission denied: '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon/kivy-icon-24.png'"), ('/data/user/0/dk.transformation.geoesptraining/files/app/_python_bundle/site-packages/kivy/data/logo/kivy-icon-256.png', '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon/kivy-icon-256.png', "[Errno 13] Permission denied: '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon/kivy-icon-256.png'"), ('/data/user/0/dk.transformation.geoesptraining/files/app/_python_bundle/site-packages/kivy/data/logo/kivy-icon-32.png', '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon/kivy-icon-32.png', "[Errno 13] Permission denied: '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon/kivy-icon-32.png'"), ('/data/user/0/dk.transformation.geoesptraining/files/app/_python_bundle/site-packages/kivy/data/logo/kivy-icon-48.png', '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon/kivy-icon-48.png', "[Errno 13] Permission denied: '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon/kivy-icon-48.png'"), ('/data/user/0/dk.transformation.geoesptraining/files/app/_python_bundle/site-packages/kivy/data/logo/kivy-icon-512.png', '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon/kivy-icon-512.png', "[Errno 13] Permission denied: '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon/kivy-icon-512.png'"), ('/data/user/0/dk.transformation.geoesptraining/files/app/_python_bundle/site-packages/kivy/data/logo/kivy-icon-64.ico', '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon/kivy-icon-64.ico', "[Errno 13] Permission denied: '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon/kivy-icon-64.ico'"), ('/data/user/0/dk.transformation.geoesptraining/files/app/_python_bundle/site-packages/kivy/data/logo/kivy-icon-64.png', '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon/kivy-icon-64.png', "[Errno 13] Permission denied: '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon/kivy-icon-64.png'"), ('/data/user/0/dk.transformation.geoesptraining/files/app/_python_bundle/site-packages/kivy/data/logo', '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon', "[Errno 13] Permission denied: '/data/user/0/dk.transformation.geoesptraining/files/app/.kivy/icon'")]
python : [WARNING] [Config ] Older configuration version detected (0 instead of 27)
python : [WARNING] [Config ] Upgrading configuration in progress.
python : [DEBUG ] [Config ] Upgrading from 0 to 1
python : [INFO ] [Logger ] Record log in /data/user/0/dk.transformation.geoesptraining/files/app/.kivy/logs/kivy_24-08-11_0.txt
python : [INFO ] [Kivy ] v2.3.0
python : [INFO ] [Kivy ] Installed at "/data/user/0/dk.transformation.geoesptraining/files/app/_python_bundle/site-packages/kivy/__init__.pyc"
python : [INFO ] [Python ] v3.11.5 (main, Aug 11 2024, 09:42:39) [Clang 14.0.6 (https://android.googlesource.com/toolchain/llvm-project 4c603efb0
python : [INFO ] [Python ] Interpreter at ""
python : [INFO ] [Logger ] Purge log fired. Processing...
python : [INFO ] [Logger ] Purge finished!
python : [INFO ] [Factory ] 195 symbols loaded
python : [INFO ] [Image ] Providers: img_tex, img_dds, img_sdl2 (img_pil, img_ffpyplayer ignored)
python : [INFO ] [Window ] Provider: sdl2
python : [INFO ] [GL ] Using the "OpenGL ES 2" graphics system
python : [INFO ] [GL ] Backend used <sdl2>
python : [INFO ] [GL ] OpenGL version <b'OpenGL ES 3.2 [email protected] (GIT@bb5b86c, I77d3059488) (Date:06/07/18)'>
python : [INFO ] [GL ] OpenGL vendor <b'Qualcomm'>
python : [INFO ] [GL ] OpenGL renderer <b'Adreno (TM) 505'>
python : [INFO ] [GL ] OpenGL parsed version: 3, 2
python : [INFO ] [GL ] Texture max size <16384>
python : [INFO ] [GL ] Texture max units <16>
python : [INFO ] [Window ] auto add sdl2 input provider
python : [INFO ] [Window ] virtual keyboard not allowed, single mode, not docked
python : [INFO ] [Text ] Provider: sdl2
python : app.directory = .
python : app.user_data_dir = /data/user/0/dk.transformation.geoesptraining/files
python : platform = android
python : RootLayout(BoxLayout).__init__ (platform==Android) instantiates KMLSendButton()
python : [WARNING] [Base ] Unknown <android> provider
python : [INFO ] [Base ] Start application main loop
python : [INFO ] [GL ] NPOT texture support is available
python : Line 1082: KMLSendButton.on_release() second time!
python : app.root.send_KML_done() is called in TestDoneButton(ScalableButton).on_release(self)
python : ChooseTarget(True) is instantiated in RootLayout(BoxLayout).send_KML_done(self)
python : ChooseTarget(BoxLayout).__init__(self, first_run, **kwargs)
python : ChooseTarget(BoxLayout).my_init(self): InputOkYesNo(BoxLayout) is instantiated now.
python : app.root.send_KML_done() is called in TestDoneButton(ScalableButton).on_release(self)
python : ChooseTarget(True) is instantiated in RootLayout(BoxLayout).send_KML_done(self)
python : ChooseTarget(BoxLayout).__init__(self, first_run, **kwargs)
python : ChooseTarget(BoxLayout).my_init(self): InputOkYesNo(BoxLayout) is instantiated now.
python : [INFO ] [Base ] Leaving application in progress...
python : Traceback (most recent call last):
python : File "/mnt/4AF15A0435E762B4/mypython/Double-KMLSendButton-to-run/.buildozer/android/app/main.py", line 378, in <module>
python : File "/mnt/4AF15A0435E762B4/mypython/Double-KMLSendButton-to-run/.buildozer/android/platform/build-armeabi-v7a_arm64-v8a/build/python-installs/geoesptraining/armeabi-v7a/kivy/app.py", line 956, in run
python : File "/mnt/4AF15A0435E762B4/mypython/Double-KMLSendButton-to-run/.buildozer/android/platform/build-armeabi-v7a_arm64-v8a/build/python-installs/geoesptraining/armeabi-v7a/kivy/base.py", line 574, in runTouchApp
python : File "/mnt/4AF15A0435E762B4/mypython/Double-KMLSendButton-to-run/.buildozer/android/platform/build-armeabi-v7a_arm64-v8a/build/python-installs/geoesptraining/armeabi-v7a/kivy/base.py", line 339, in mainloop
python : File "/mnt/4AF15A0435E762B4/mypython/Double-KMLSendButton-to-run/.buildozer/android/platform/build-armeabi-v7a_arm64-v8a/build/python-installs/geoesptraining/armeabi-v7a/kivy/base.py", line 383, in idle
python : File "/mnt/4AF15A0435E762B4/mypython/Double-KMLSendButton-to-run/.buildozer/android/platform/build-armeabi-v7a_arm64-v8a/build/python-installs/geoesptraining/armeabi-v7a/kivy/base.py", line 334, in dispatch_input
python : File "/mnt/4AF15A0435E762B4/mypython/Double-KMLSendButton-to-run/.buildozer/android/platform/build-armeabi-v7a_arm64-v8a/build/python-installs/geoesptraining/armeabi-v7a/kivy/base.py", line 302, in post_dispatch_input
python : File "kivy/_event.pyx", line 731, in kivy._event.EventDispatcher.dispatch
python : File "/mnt/4AF15A0435E762B4/mypython/Double-KMLSendButton-to-run/.buildozer/android/platform/build-armeabi-v7a_arm64-v8a/build/python-installs/geoesptraining/armeabi-v7a/kivy/uix/behaviors/button.py", line 179, in on_touch_up
python : File "kivy/_event.pyx", line 727, in kivy._event.EventDispatcher.dispatch
python : File "kivy/_event.pyx", line 1307, in kivy._event.EventObservers.dispatch
python : File "kivy/_event.pyx", line 1191, in kivy._event.EventObservers._dispatch
python : File "/mnt/4AF15A0435E762B4/mypython/Double-KMLSendButton-to-run/.buildozer/android/platform/build-armeabi-v7a_arm64-v8a/build/python-installs/geoesptraining/armeabi-v7a/kivy/lang/builder.py", line 60, in custom_callback
python : File "/data/data/dk.transformation.geoesptraining/files/app/double-KMLSendButton.kv", line 85, in <module>
python : on_release: root.no_pressed()
python : ^^^^^^^^^^^^^
python : File "/mnt/4AF15A0435E762B4/mypython/Double-KMLSendButton-to-run/.buildozer/android/app/main.py", line 94, in no_pressed
python : AttributeError: 'NoneType' object has no attribute 'no_no'
python : Python for android ended.
Additional context Please feel free to ask me ANY question! :-) And excuse me if there is something I have forgotten to mention. I don't think so, but this is getting complex... :-) Many many months of work on this app will be lost if this is not solved! I look forward to hear from you. Thank you!
Henrik
I should add, that 'class KMLSendButton(ScalableButton):' worked fine 2 years ago, in the last version of the app, where it looked slightly different, because I did not have the 'self.first_touch' flag, to try to compensate for on_release being run twice:
class KMLSendButton(ScalableButton): # Specific for Android:
def on_release(self):
# self.parent.remove_widget(self.parent.KML_send_button)
self.parent.share = ShareBox() # Should be self.parent.share ...
self.parent.add_widget(self.parent.share)
self.parent.share.share_file(KML_testfilename, KML_MIMEtype)
self.button_text2 = language.format_value("kml-test-done", {"KML_file": KML_testfile})
# Should be self.parent.test_done_button ...
self.parent.test_done_button = TestDoneButton(text=self.button_text2)
self.parent.add_widget(self.parent.test_done_button)
The version mentioned above used p4a from March 2022. I also made a small update in August 2023, where I used p4a from May 2023, but I am sure if I actually tested it. Both of them just used the current Kivy.
I understand if you want to see the content of the files directly here on the screen, so here is the content of main.py:
Monday, August 19th:
NOTE! I have removed the 'main.py' code in THIS comment, because I posted a new and better version in a new comment below!
And here is the content of the kv-file double-KMLSendButton.kv:
#:kivy 1.11.1
# Useless? #:import kivy kivy
# :import Touchtracer touchtracer.Touchtracer
# But it works even without this import!
#:set my_font_size sp(15)
# Anything larger than that will be too big on Android!
# Something slightly larger might be ok on iOS - iPhones.
# Remember to also set 'base_font_size' further below.
# but sp(17) looks better on Ubuntu...
#:set large_font_size sp(30)
#:set my_padding dp(10)
#:set my_bar_color [.3, .3, .3, .9]
#:set white 1, 1, 1, 1
#:set black 0, 0, 0, 1
<ScalableLabel>: # Button, unlike Label, already HAS a background_color ...(??)
text: '(...)' # Will be set later in Python.
font_size: my_font_size
text_size: self.width, None
# size: self.texture_size
size_hint_y: None
padding: (my_padding,my_padding)
height: self.texture_size[1]
background_color: [0, 0, 0, 1] # Black
# Transparent, and thus set later. (If you place this comment at the end of the line above the app crashes...)
canvas.before:
Color:
rgba: root.background_color
Rectangle:
size: self.size
pos: self.pos
<ScalableButton>: # Button, unlike Label, already HAS a background_color ...(??)
text: '(...)' # Will be set later in Python.
font_size: my_font_size
text_size: self.width, None
# size: self.texture_size
size_hint_y: None
padding: (my_padding, my_padding)
height: self.texture_size[1]
background_color: [0, 1, 0, 1]
# DropDown is based on Scrollview. Therefore I have to create a MyDropDown which is a COPY of MyScrollView:
<MyDropDown@DropDown>:
do_scroll_x: False
do_scroll_y: True
bar_color: my_bar_color
bar_inactive_color: my_bar_color
bar_width: 0.6 * my_padding # Before June 2024: bar_width: my_padding
canvas.before:
Color:
rgba: 0.9, 0.9, 0.9, 1
Rectangle:
size: self.size
pos: self.pos
<FixedTextinput@TextInput>:
text: ''
multiline: False
size_hint_y: None
height: self.minimum_height
<ChooseTarget>:
orientation: 'vertical'
<InputOkYesNo>:
orientation: 'vertical'
size_hint_y: None
height: question.texture_size[1] + yes_button.texture_size[1] + no_button.texture_size[1]
# Elliot Garbus: "I think the core of the issue is the hint lines are evaluated at creation prior to
# the widget being added. At that time there is no parent. I fixed the issue by testing for parent:"
# size_hint_x: 0.5 if self.parent and (self.parent.orientation=='horizontal') else 1
# size_hint_y: 0.5 if self.parent and (self.parent.orientation=='vertical') else 1
ScalableLabel:
id: question
# text: 'Wait...' # 'Føles det rigtigt?'
ScalableButton:
id: yes_button
on_release: root.yes_pressed()
# text: 'Wait...' # 'Ja'
# pos_hint: {'right': 1}
ScalableButton:
id: no_button
on_release: root.no_pressed()
background_color: [1, 0, 0, 1]
# text: 'Wait...' # 'Ikke helt, jeg vil gerne lige prøve igen'
Wait! :-)
There are 2 or 3 logical errors in my minimal example. So wait, until I check if changing them solves the problem!
Now I have corrected the logical errors, which makes the minimal example more 'clean'. But it does NOT solve the problem.
Here is the new version of the main.py. Everything else is unchanged:
'''
If I ever need to know current working directory of this app etc:
https://stackoverflow.com/questions/5137497/find-current-directory-and-files-directory
'''
__version__ = '1.5.0'
# Developed in Python 3.6 (and 3.8 and 3.11)
import os
import shutil
from kivy.config import Config
from kivy.properties import OptionProperty
Config.set('kivy', 'exit_on_escape', 0) # disable exit on esc
Config.set('input', 'mouse', 'mouse,disable_multitouch') # disable the red dot, multi-touch simulation
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
# from kivy.uix.floatlayout import FloatLayout
from kivy.uix.dropdown import DropDown
from kivy.utils import platform
from kivy.clock import Clock
Builder.load_file('double-KMLSendButton.kv')
# Dec. 13, 2020:
# Here I have to import either the Android version or the iOS / Ubuntu version of ShareBox. Usage:
# Instantiate ShareBox(ScalableLabel)
# Call ShareBox.share_file(internal_filename, MIMEtype)
"""
if platform == 'android':
from sharebox_android import ShareBox
else:
from sharebox_ios_etc import ShareBox
"""
# if platform == 'ios':
# from plyer import storagepath # it's only used on iOS, and NOT on Android!
KML_testfile = 'myplaces-new.kml'
KML_MIMEtype = 'application/vnd.google-earth.kml+xml'
KML_templatefile = 'myplaces-template.kml'
from common_kivy_stuff import ScalableLabel, ScalableButton, FixedTextinput
class KMLSendButton(ScalableButton):
# Specific for Android:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.first_touch = True
def on_release(self):
# This ran twice!!
if self.first_touch:
self.first_touch = False
self.parent.test_done()
else:
print("Line 1082: KMLSendButton.on_release() second time!")
class TestDoneButton(ScalableButton):
# Both for Android and iOS:
def on_release(self):
app = App.get_running_app()
print("app.root.send_KML_done() is called in TestDoneButton(ScalableButton).on_release(self)")
app.root.send_KML_done() # .content. ??
class InputOkYesNo(BoxLayout):
def __init__(self, question, yes_text, no_text, **kwargs):
super().__init__(**kwargs)
self.ids.question.text = question
self.ids.yes_button.text = yes_text
self.ids.no_button.text = no_text
def yes_pressed(self):
self.parent.yes_yes()
def no_pressed(self):
self.parent.no_no()
class ChooseTarget(BoxLayout):
# first_run = BooleanProperty ?
def __init__(self, first_run, **kwargs):
super().__init__(**kwargs)
self.first_run = first_run
print("ChooseTarget(BoxLayout).__init__(self, first_run, **kwargs)")
self.my_init()
def my_init(self):
# Create string for the MyScrollView
self.clear_widgets()
self.targets_str = ""
for [name, x, y, z] in targets_list:
self.targets_str += name + "\n"
self.targets = ScalableLabel(text=self.targets_str, color='white') # Was MyScrollView()
self.add_widget(self.targets)
# self.targets_list_text = ScalableLabel(text=self.targets_str, color='white')
# self.targets.add_widget(self.targets_list_text)
print("ChooseTarget(BoxLayout).my_init(self): InputOkYesNo(BoxLayout) is instantiated now.")
self.create_new_target_yes_no = InputOkYesNo("Would you like to define your own target, or to choose a target from the list above? For a beginner it is recommended to use your current",
"I will define a new target.", "I will choose a target from the list.")
self.add_widget(self.create_new_target_yes_no)
def yes_yes(self):
# User will define new target
self.clear_widgets()
self.multible_widgets = PromptsNewTarget(self.first_run, orientation='vertical', size_hint_y=None)
self.multible_widgets.bind(minimum_height=self.multible_widgets.setter('height'))
self.add_widget(self.multible_widgets)
# self.prompts_scroll_block = MyMultiScrollView(self.first_run)
# self.add_widget(self.prompts_scroll_block)
def no_no(self):
# User will choose a target from a list
self.clear_widgets()
self.mydropdown = ButtonDropDown(text="Choose a target on the list:",
background_color=[0.9, 0.9, 0.9, 1])
self.add_widget(self.mydropdown)
self.mydropdown.bind(text=self.text_selected)
self.placeholder = ScalableLabel(size_hint=(1,1))
self.add_widget(self.placeholder)
def text_selected(self, obj, user_choice):
self.clear_widgets()
for [name, lati, long, decli] in targets_list:
if name == user_choice: break
if self.first_run:
app = App.get_running_app()
app.root.clear_widgets()
app.root.test_end_1 = ScalableLabel(text="End-1 double-KMLSendButton problem") # was .tabs = MyTabbedPanel()
app.root.add_widget(app.root.test_end_1)
else:
self.parent.after_choose_target()
#######################################################################
# NOTE! The BELOW classes are only instantiated if the App actually works on Android!
#######################################################################
class ButtonDropDown(ScalableButton):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.dropdown = DropDown()
Clock.schedule_once(self.add_buttons)
def add_buttons(self, _):
for [name, x, y, z] in targets_list:
button = ScalableButton(text=name)
button.bind(on_release=lambda btn: self.dropdown.select(btn.text))
self.dropdown.add_widget(button)
self.bind(on_release=self.dropdown.open)
self.dropdown.bind(on_select=lambda instance, x: setattr(self, 'text', x))
self.dropdown.open(self) # open the dropdown
class MyLatiInput(FixedTextinput):
def on_text_validate(self): # 'enter_pressed'
# validate decimal number - latitude
try:
val = float(self.text)
if val >= -90 and val <= 90:
self.parent.valid_lati()
else:
self.parent.invalid_lati()
except ValueError:
self.parent.invalid_lati()
class MyLongInput(FixedTextinput):
def on_text_validate(self): # 'enter_pressed'
# validate decimal number
try:
val = float(self.text)
if val >= -180 and val <= 180:
self.parent.valid_long()
else:
self.parent.invalid_long()
except ValueError:
self.parent.invalid_long()
class MyNameInput(FixedTextinput):
def on_text_validate(self):
if len(self.text) > 6:
self.parent.valid_name()
else:
self.parent.invalid_name()
class PromptsNewTarget(BoxLayout):
def __init__(self, first_run, **kwargs):
super().__init__(**kwargs)
self.first_run = first_run
self.my_init()
def my_init(self):
self.clear_widgets()
self.new_target_prompt = ScalableLabel(text="We need the geographical coordinates of the new target you want to use:", color='black') # Text color
self.add_widget(self.new_target_prompt)
self.lati_prompt = ScalableLabel(text="Enter the latitude (North-South position) as a decimal degree [-90, 90]:", color='black')
self.add_widget(self.lati_prompt)
self.lati_input = MyLatiInput() # text='', multiline=False, size_hint_y=None
# self.lati_input.bind(minimum_height=self.lati_input.setter('height'))
self.add_widget(self.lati_input)
self.long_prompt = ScalableLabel(text="Enter the longitude (East-West position) as a decimal degree [-180, 180]:", color='black')
self.add_widget(self.long_prompt)
self.long_input = MyLongInput() # text='', multiline=False, size_hint_y=None
# self.long_input.bind(minimum_height=self.long_input.setter('height'))
self.add_widget(self.long_input)
# Now we only need the name and mag decli of the location...
self.name_prompt = ScalableLabel(text="Now give the new target location a name (at least 7 characters):", color='black')
self.add_widget(self.name_prompt)
self.name_input = MyNameInput() # text='', multiline=False, size_hint_y=None
# self.name_input.bind(minimum_height=self.name_input.setter('height'))
self.add_widget(self.name_input)
self.lati_input.focus = True
def valid_lati(self):
self.latitude = float(self.lati_input.text)
self.long_input.focus = True
def valid_long(self):
self.longitude = float(self.long_input.text)
self.name_input.focus = True
def valid_name(self):
self.target_name = self.name_input.text
targets_list.append([self.target_name, self.latitude, self.longitude])
if self.first_run:
app = App.get_running_app()
app.root.clear_widgets()
app.root.test_end_2 = ScalableLabel(text="End-2 double-KMLSendButton problem") # was .tabs = MyTabbedPanel()
app.root.add_widget(app.root.test_end_2)
else:
self.parent.parent.after_choose_target()
def invalid_lati(self):
self.parent.parent.my_init()
def invalid_long(self):
self.lati_input.focus = True
def invalid_name(self):
self.long_input.focus = True
#######################################################################
# NOTE! The classes ABOVE are only instantiated if the App actually works on Android!
#######################################################################
send_kml_test_help = """
Before you begin the Geo-ESP training
-------------------------------------
(Long explanation...)
(Install Google Earth from the app store on this device. Afterwards, continue here:)
"""
android_help = """
Save the kml file somewhere where you can find it again and open it. (For instance the Download-folder.)
On Android phones, you have to install a File Manager, such as File Manager+.
Now press the button below to proceed:
"""
ios_help = """
(non-Android help)
Open the kml file in Google Earth
"""
class RootLayout(BoxLayout):
# Why did I think this is the way to do it...: orientation = OptionProperty('vertical')
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.orientation = 'vertical' # set the orientation here...
app = App.get_running_app()
print("app.directory = ", app.directory)
print("app.user_data_dir = ", app.user_data_dir)
global KML_testfilename, shared_files_path, app_dir_readonly
app_dir_readonly = app.directory
print("platform = ", platform)
shared_files_path = app.user_data_dir
KML_testfilename = os.path.join(app_dir_readonly, KML_testfile)
global targets_list
targets_list = [["The Brandenburg Gate, Berlin, Germany", 52.5163, 13.3777, 4.646129074844017],
["Neuschwanstein Castle, Bavaria, Germany", 47.5576, 10.7498, 3.6677740883668175],
["Stonehenge, Salisbury, England", 51.1789, -1.8262, -0.04692730206116467],
["Eiffel Tower, Paris, France", 48.8584, 2.2945, 1.435555796454664],
["Basilica de la Sagrada Familia, Barcelona, Spain", 41.4036, 2.1744, 1.5727463793333682]]
# self.KML_box = SendKMLfile("testfile", KML_testfile, orientation='vertical')
# self.add_widget(self.KML_box)
# class SendKMLfile(BoxLayout):
# def __init__(self, mode, loc_KML_file, **kwargs):
# super().__init__(**kwargs)
self.general_help_text = send_kml_test_help
# if platform == 'android':
self.OS_spec_help = android_help
# else:
# self.OS_spec_help = ios_help
self.help_text = self.general_help_text + "\n\n" + self.OS_spec_help
self.help_doc = ScalableLabel(text=self.help_text)
self.add_widget(self.help_doc)
# NOTE: Here I removed "if platform == 'android':"
# self.button_text = "Press here to send/save/share the test kml file '{0}' (e.g. to Google Earth)!".format(KML_testfile)
# The ONLY way to avoid the Double-KMLSendButton-problem that I have found is to remove the following lines and insert 'pass' instead...:
print("RootLayout(BoxLayout).__init__ (platform==Android) instantiates KMLSendButton()")
self.KML_send_button = KMLSendButton(text="Press here to send/save/share the test kml file '{0}' (e.g. to Google Earth)!".format(KML_testfile))
self.add_widget(self.KML_send_button)
def test_done(self):
self.remove_widget(self.KML_send_button)
# The following 3 lines are specific to the special Android OS file handling:
# self.share = ShareBox()
# self.add_widget(self.share)
# self.share.share_file(KML_testfilename, KML_MIMEtype)
self.button_text2 = "Press here after viewing the contents of the test kml file '{0}' (e.g. in Google Earth).".format(KML_testfile)
self.test_done_button = TestDoneButton(text=self.button_text2)
self.add_widget(self.test_done_button)
def send_KML_done(self):
# June 2024: RIGHT HERE is where the user has to choose the initial first target!:
self.clear_widgets()
print("ChooseTarget(True) is instantiated in RootLayout(BoxLayout).send_KML_done(self)")
self.choose_target = ChooseTarget(True)
self.add_widget(self.choose_target)
def after_choose_target(self):
app = App.get_running_app()
app.root.children[0].ids.at_target.set_label_text()
app.root.children[0].ids.at_ref_point.set_label_text()
app.root.children[0].ids.guide_2.set_label_text()
# call 'WhenAtRefPoint'.self.start_new_trial()
app.root.children[0].ids.at_ref_point.start_new_trial()
"""
The removed code from above:
if platform == 'android': # or platform == 'linux': # 6-8-24: TEST
# self.button_text = "Press here to send/save/share the test kml file '{0}' (e.g. to Google Earth)!".format(KML_testfile)
# The ONLY way to avoid the Double-KMLSendButton-problem that I have found is to remove the following lines and insert 'pass' instead...:
print("RootLayout(BoxLayout).__init__ (platform==Android) instantiates KMLSendButton()")
self.KML_send_button = KMLSendButton(text="Press here to send/save/share the test kml file '{0}' (e.g. to Google Earth)!".format(
KML_testfile))
self.add_widget(self.KML_send_button)
else:
shutil.copy(KML_testfilename, shared_files_path)
# self.button_text2 = "Press here after viewing the contents of the test kml file '{0}' (e.g. in Google Earth).".format(KML_testfile)
self.test_done_button = TestDoneButton(text="Press here after viewing the contents of the test kml file '{0}' (e.g. in Google Earth).".format(KML_testfile))
self.add_widget(self.test_done_button)
"""
class GeoESPTraining(App):
def build(self):
return RootLayout()
if __name__ == '__main__':
GeoESPTraining().run()
I still look forward to hear from anyone who tries running this on their own Android phone!!
I have finally isolated the problem!!
When I remove these lines, everything works:
from kivy.config import Config Config.set('kivy', 'exit_on_escape', 0) # disable exit on esc Config.set('input', 'mouse', 'mouse,disable_multitouch') # disable the red dot, multi-touch simulation
The tragic-comical thing is that these lines only make sense on a desktop...
NOTE: This is still a bug in Kivy, that should be found and solved. Although I managed to get my app to run again by deleting kivy.config.