Kivy – Problems with on_new_intent in Apps

In an earlier post I wrote about the wonderful on_new_intent hook which allows your application to receive intents from other apps on an Android device.  When the other app sends an intent your app will process it in whatever callback you bound to on_new_intent (which I will call on_new_intent() for the sake of simplicity).  To receive an intent from another app will probably mean that the user is actively using that other app.  That means that your app has been paused.  So, when Android passes the intent to your app it’s going to want to on_resume your app.   Can you see where this is going?

The problem with using on_new_intent in an app is that on_resume() and on_new_intent() are both fired at more or less the same time.  There is no guarantee that one of them will be called first and no guarantee that the one called first will return before the second one is called (in my experiments they can fire in either order and can fire in the middle of the other).  In other contexts  this might not be a problem.  Here it is.  The purpose of on_resume is to reinitialise the App state to where it was as at the last call to on_pause().  The purpose of on_new_intent is to initialise or update the App state based on the data provided by the intent.   This is not a problem for services because services are running in the background and on_resume need not be called.

The future solution is that I’ve logged an issue.  My current solution is a hackish workaround – delay the execution of on_new_intent to give on_resume a chance to start, and use flags for on_new_intent to wait for it to finish.  If the timing is bad, this will fail, but seems to be ok in practice so far (ie last 24 hours).

Sample code (init the attributes to False when you instantiate the app too btw):

class GridderApp(App):
#[stuff deleted]
    def on_resume(self):
        if self.on_new_intenting: # let it run, don't bother about initialising to saved
            return
        self.resuming = True
        self.uiwidget.load(path=".",   filename=PAUSE_FILE_NAME,  store_save= False)
        self.resuming = False

    def on_new_intent(self, intent):
        self.on_new_intenting = True
        sleep(0.25) # give on_resume a chance to fire
        # if it fires after on_new_intent has finished, it will reinitialise to the saved state
        # and we want to avoid that!
        while self.resuming:
            sleep(0.1)

        intent_data = intent.getData()
        try:
            file_uri= intent_data.toString()
        except AttributeError:
            file_uri = None
        if file_uri is None:
            return
        else:
            if file_uri[:7]=="file://":
                self.uiwidget.load(path="",  filename=url2pathname(file_uri[7:]))
            else:
                return
        self.on_new_intenting= False

Kivy – Receiving Android Intents

In theory Kivy has a way to receive intents generated from other apps on Android – it’s the on_new_intent callback hook.  To use it you first need to write a method to handle the intent.  The method will be passed the intent, but you’re going to have to unpack data from it.  What data is to be unpacked and how you go about it will be different depending on the intent.  You’ll need to dig into the Android Java docs/stack exchange to get details.  This is a sample unpacking of a file uri which has been provided by a file manager app.  The file uri is not in a format that Kivy can use so it’s got to first be unescaped (here I use url2pathname from urllib2):

class GridderApp(App):
# [stuff deleted]
    def on_new_intent(self, intent):
        intent_data = intent.getData()
        try:
            file_uri= intent_data.toString() # isn't Java awesome?
        except AttributeError:
            file_uri = None
        if file_uri is None:
            return # give up
        else:
            if file_uri[:7]=="file://":
                self.uiwidget.load(path="",  filename=url2pathname(file_uri[7:]))
            else:
                return # again, give up

That’d all be lovely if that’s how it actually worked. Unfortunately on_new_intent seems to be designed on the assumption that it’s being run as the method of a Service not an app. This code won’t work for an app for reasons to be covered in a later post.

After you’ve defined this method you need to bind it:

if __name__ == "__main__":
    if platform=="android":
        import android.activity
        app = GridderApp(file_uri=file_uri)
        android.activity.bind(on_new_intent=app.on_new_intent)

After which, you’re good to go! Except that apps suffer from a problem with receiving intents – see my next post.

Apparently (as at July 2014) on_new_intent does not work on the application’s first start, so you can’t get (eg) a file uri from an intent if your app has not been run before or has been run, but stopped.  I’m told this is a bug so it may be fixed in the future.

 

Kivy – Android Life Cycle

If your app is a stand alone app which doesn’t interact with the rest of the Android environment, then you don’t have to worry about life cycles.  For everyone else, welcome to the Android way of doing things.  The Android life cycle reflects a design decision by the Android developers that any app needs to be able to be interrupted at any time without warning for no reason – originally I think this was in order to receive phone calls and/or account for low phone resources.  However, it still applies to devices which don’t even have a cell phone radio.

Android deals with this primarily through calling on_pause and on_resume methods on apps (actually onPause and onResume in Java).  If the app wants to do something there (eg save/restore current data!), well and good.  Android calls on_pause() when it returns Android suspends the app until someone brings the app to the foreground again, at which time on_resume() is called.   The Android lifecycle is actually more complicated than this but let’s not sweat the details.

Kivy allows you to implement the on_pause and on_resume simply by adding these methods to your app instance. Some sample code:

class GridderApp(App):
# stuff deleted

    def on_pause(self):
        self.uiwidget.save(path =".", filename=PAUSE_FILE_NAME)
        return True  # app sleeps until resume return False to stop the app 

    def on_stop(self):
        pass

    def on_resume(self):
        self.uiwidget.load(path=".",   filename=PAUSE_FILE_NAME,  store_save= False)

 

 

Kivy Android Intent Filters

Intent filters are Android’s equivalent of file associations.  When you click on a file in a file manager on Android it starts the associated application by using an intent.  Applications can support the emission of intents with different categories.  Other apps which have registered to receive intents of that category can then receive and process them.

In order to make use of these intents from Kivy/buildozer you need to add an intent-filter to your a file (by default called intent_filters.xml) in your app’s root directory.  You also need to edit the buildozer.spec file to point to the file (look for this line and uncomment):

# (str) XML file to include as an intent filters in <activity> tag
android.manifest.intent_filters=intent_filters.xml

The intent filter will differ depending on the intent.  Intents based on mimetypes are better than those based on paths.  Apparently there’s a problem with Android’s file search algorithm so you need to specify every relevant directory level if you want to filter on the file’s extension (seriously!).  Sample intent (from my art gridder app, which uses files with a .bag extension:

<intent-filter>
<action android:name=”android.intent.action.VIEW” />
<category android:name=”android.intent.category.DEFAULT” />
<data android:mimeType=”*/*” />
<data android:scheme=”file” />
<data android:host=”*” />
<data android:port=”*” />
<data android:pathPattern=”.*..*..*..*..*.bag” />
<data android:pathPattern=”.*..*..*..*.bag” />
<data android:pathPattern=”.*..*..*.bag” />
<data android:pathPattern=”.*..*.bag” />
<data android:pathPattern=”.*.bag” />
</intent-filter>

Finally, your app needs to be able to receive and extract the intent (don’t expect to find this in the docs!):

if __name__ == "__main__":
    if platform=="android":
        from jnius import cast
        from jnius import autoclass
        import android
        import android.activity

        # test for an intent passed to us
        PythonActivity = autoclass('org.renpy.android.PythonActivity')
        activity = PythonActivity.mActivity
        intent = activity.getIntent()
        intent_data = intent.getData()
        try:
            file_uri= intent_data.toString()
        except AttributeError:
            file_uri = None

Then pass the file uri to the app when you instantiate it.

Kivy – Basics

To create a Kivy application you:

  • create a file called main.py as your entry point
  • subclass from kivy.app.App
  • include a build() method
  • make sure the build method returns a widget (in this case a Label)
  • instantiate an instance of the subclass
  • invoke the run() method of that instance

Example, from the kivy site:

import kivy
kivy.require('1.0.6') # replace with your current kivy version !

from kivy.app import App
from kivy.uix.label import Label

class MyApp(App):

    def build(self):
        return Label(text='Hello world')

if __name__ == '__main__':
    MyApp().run()

Then run python main.py…