Skip to content

android/activity: Add Application.ActivityLifecycleCallbacks helpers #2669

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions doc/source/apis.rst
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,51 @@ Example::
# ...


Activity lifecycle handling
~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. module:: android.activity

The Android ``Application`` class provides the `ActivityLifecycleCallbacks
<https://developer.android.com/reference/android/app/Application.ActivityLifecycleCallbacks>`_
interface where callbacks can be registered corresponding to `activity
lifecycle
<https://developer.android.com/guide/components/activities/activity-lifecycle>`_
changes. These callbacks can be used to implement logic in the Python app when
the activity changes lifecycle states.

Note that some of the callbacks are not useful in the Python app. For example,
an `onActivityCreated` callback will never be run since the the activity's
`onCreate` callback will complete before the Python app is running. Similarly,
saving instance state in an `onActivitySaveInstanceState` callback will not be
helpful since the Python app doesn't have access to the restored instance
state.

.. function:: register_activity_lifecycle_callbacks(callbackname=callback, ...)

This allows you to bind a callbacks to Activity lifecycle state changes.
The callback names correspond to ``ActivityLifecycleCallbacks`` method
names such as ``onActivityStarted``. See the `ActivityLifecycleCallbacks
<https://developer.android.com/reference/android/app/Application.ActivityLifecycleCallbacks>`_
documentation for names and function signatures for the callbacks.

.. function:: unregister_activity_lifecycle_callbacks(instance)

Unregister a ``ActivityLifecycleCallbacks`` instance previously registered
with :func:`register_activity_lifecycle_callbacks`.

Example::

from android.activity import register_activity_lifecycle_callbacks

def on_activity_stopped(activity):
print('Activity is stopping')

register_activity_lifecycle_callbacks(
onActivityStopped=on_activity_stopped,
)


Receiving Broadcast message
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
151 changes: 151 additions & 0 deletions pythonforandroid/recipes/android/src/android/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,154 @@ def unbind(**kwargs):
_activity.unregisterNewIntentListener(listener)
elif event == 'on_activity_result':
_activity.unregisterActivityResultListener(listener)


# Keep a reference to all the registered classes so that python doesn't
# garbage collect them.
_lifecycle_callbacks = set()


class ActivityLifecycleCallbacks(PythonJavaClass):
"""Callback class for handling PythonActivity lifecycle transitions"""

__javainterfaces__ = ['android/app/Application$ActivityLifecycleCallbacks']

def __init__(self, callbacks):
super().__init__()

# It would be nice to use keyword arguments, but PythonJavaClass
# doesn't allow that in its __cinit__ method.
if not isinstance(callbacks, dict):
raise ValueError('callbacks must be a dict instance')
self.callbacks = callbacks

def _callback(self, name, *args):
func = self.callbacks.get(name)
if func:
return func(*args)

@java_method('(Landroid/app/Activity;Landroid/os/Bundle;)V')
def onActivityCreated(self, activity, savedInstanceState):
self._callback('onActivityCreated', activity, savedInstanceState)

@java_method('(Landroid/app/Activity;)V')
def onActivityDestroyed(self, activity):
self._callback('onActivityDestroyed', activity)

@java_method('(Landroid/app/Activity;)V')
def onActivityPaused(self, activity):
self._callback('onActivityPaused', activity)

@java_method('(Landroid/app/Activity;Landroid/os/Bundle;)V')
def onActivityPostCreated(self, activity, savedInstanceState):
self._callback('onActivityPostCreated', activity, savedInstanceState)

@java_method('(Landroid/app/Activity;)V')
def onActivityPostDestroyed(self, activity):
self._callback('onActivityPostDestroyed', activity)

@java_method('(Landroid/app/Activity;)V')
def onActivityPostPaused(self, activity):
self._callback('onActivityPostPaused', activity)

@java_method('(Landroid/app/Activity;)V')
def onActivityPostResumed(self, activity):
self._callback('onActivityPostResumed', activity)

@java_method('(Landroid/app/Activity;Landroid/os/Bundle;)V')
def onActivityPostSaveInstanceState(self, activity, outState):
self._callback('onActivityPostSaveInstanceState', activity, outState)

@java_method('(Landroid/app/Activity;)V')
def onActivityPostStarted(self, activity):
self._callback('onActivityPostStarted', activity)

@java_method('(Landroid/app/Activity;)V')
def onActivityPostStopped(self, activity):
self._callback('onActivityPostStopped', activity)

@java_method('(Landroid/app/Activity;Landroid/os/Bundle;)V')
def onActivityPreCreated(self, activity, savedInstanceState):
self._callback('onActivityPreCreated', activity, savedInstanceState)

@java_method('(Landroid/app/Activity;)V')
def onActivityPreDestroyed(self, activity):
self._callback('onActivityPreDestroyed', activity)

@java_method('(Landroid/app/Activity;)V')
def onActivityPrePaused(self, activity):
self._callback('onActivityPrePaused', activity)

@java_method('(Landroid/app/Activity;)V')
def onActivityPreResumed(self, activity):
self._callback('onActivityPreResumed', activity)

@java_method('(Landroid/app/Activity;Landroid/os/Bundle;)V')
def onActivityPreSaveInstanceState(self, activity, outState):
self._callback('onActivityPreSaveInstanceState', activity, outState)

@java_method('(Landroid/app/Activity;)V')
def onActivityPreStarted(self, activity):
self._callback('onActivityPreStarted', activity)

@java_method('(Landroid/app/Activity;)V')
def onActivityPreStopped(self, activity):
self._callback('onActivityPreStopped', activity)

@java_method('(Landroid/app/Activity;)V')
def onActivityResumed(self, activity):
self._callback('onActivityResumed', activity)

@java_method('(Landroid/app/Activity;Landroid/os/Bundle;)V')
def onActivitySaveInstanceState(self, activity, outState):
self._callback('onActivitySaveInstanceState', activity, outState)

@java_method('(Landroid/app/Activity;)V')
def onActivityStarted(self, activity):
self._callback('onActivityStarted', activity)

@java_method('(Landroid/app/Activity;)V')
def onActivityStopped(self, activity):
self._callback('onActivityStopped', activity)


def register_activity_lifecycle_callbacks(**callbacks):
"""Register ActivityLifecycleCallbacks instance

The callbacks are supplied as keyword arguments corresponding to the
Application.ActivityLifecycleCallbacks methods such as
onActivityStarted. See the ActivityLifecycleCallbacks documentation
for the signature of each method.

The ActivityLifecycleCallbacks instance is returned so it can be
supplied to unregister_activity_lifecycle_callbacks if needed.
"""
instance = ActivityLifecycleCallbacks(callbacks)
_lifecycle_callbacks.add(instance)

# Use the registerActivityLifecycleCallbacks method from the
# Activity class if it's available (API 29) since it guarantees the
# callbacks will only be run for that activity. Otherwise, fallback
# to the method on the Application class (API 14). In practice there
# should be no difference since p4a applications only have a single
# activity.
if hasattr(_activity, 'registerActivityLifecycleCallbacks'):
_activity.registerActivityLifecycleCallbacks(instance)
else:
app = _activity.getApplication()
app.registerActivityLifecycleCallbacks(instance)
return instance


def unregister_activity_lifecycle_callbacks(instance):
"""Unregister ActivityLifecycleCallbacks instance"""
if hasattr(_activity, 'unregisterActivityLifecycleCallbacks'):
_activity.unregisterActivityLifecycleCallbacks(instance)
else:
app = _activity.getApplication()
app.unregisterActivityLifecycleCallbacks(instance)

try:
_lifecycle_callbacks.remove(instance)
except KeyError:
pass
2 changes: 2 additions & 0 deletions testapps/on_device_unit_tests/test_app/app_flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
vibrate_with_pyjnius,
get_android_python_activity,
set_device_orientation,
setup_lifecycle_callbacks,
)


app = Flask(__name__)
setup_lifecycle_callbacks()
service_running = False
TESTS_TO_PERFORM = dict()
NON_ANDROID_DEVICE_MSG = 'Not running from Android device'
Expand Down
4 changes: 4 additions & 0 deletions testapps/on_device_unit_tests/test_app/app_kivy.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
load_kv_from,
raise_error,
run_test_suites_into_buffer,
setup_lifecycle_callbacks,
vibrate_with_pyjnius,
)
from widgets import TestImage
Expand Down Expand Up @@ -53,6 +54,9 @@ def build(self):
self.sm = Builder.load_string(screen_manager_app)
return self.sm

def on_start(self):
setup_lifecycle_callbacks()

def reset_unittests_results(self, refresh_ui=False):
for img in get_images_with_extension():
subprocess.call(["rm", "-r", img])
Expand Down
16 changes: 16 additions & 0 deletions testapps/on_device_unit_tests/test_app/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,19 @@ def set_device_orientation(direction):
else:
activity.setRequestedOrientation(
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)


@skip_if_not_running_from_android_device
def setup_lifecycle_callbacks():
"""
Register example ActivityLifecycleCallbacks
"""
from android.activity import register_activity_lifecycle_callbacks

register_activity_lifecycle_callbacks(
onActivityStarted=lambda activity: print('onActivityStarted'),
onActivityPaused=lambda activity: print('onActivityPaused'),
onActivityResumed=lambda activity: print('onActivityResumed'),
onActivityStopped=lambda activity: print('onActivityStopped'),
onActivityDestroyed=lambda activity: print('onActivityDestroyed'),
)