community icon indicating copy to clipboard operation
community copied to clipboard

'Double' instantiation of Button crashes app on Android in spite of workaround attempt

Open HeRo002 opened this issue 1 year ago • 7 comments

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

HeRo002 avatar Aug 12 '24 20:08 HeRo002

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)

HeRo002 avatar Aug 13 '24 09:08 HeRo002

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.

HeRo002 avatar Aug 13 '24 12:08 HeRo002

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'

HeRo002 avatar Aug 14 '24 08:08 HeRo002

Wait! :-)
There are 2 or 3 logical errors in my minimal example. So wait, until I check if changing them solves the problem!

HeRo002 avatar Aug 19 '24 09:08 HeRo002

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!!

HeRo002 avatar Aug 19 '24 10:08 HeRo002

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...

HeRo002 avatar Aug 19 '24 15:08 HeRo002

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.

HeRo002 avatar Aug 25 '24 19:08 HeRo002