Skip to content

Commit b379a1c

Browse files
authored
android/activity: Add Application.ActivityLifecycleCallbacks helpers (kivy#2669)
The `Application.ActivityLifecycleCallbacks` interface is used to register a class that can receive callbacks on Activity lifecycle changes. Add a `PythonJavaClass` implementing the interface and helper functions to register python callbacks with it. This can be used to perform actions in the python app when the activity changes lifecycle states.
1 parent 6505cfc commit b379a1c

File tree

5 files changed

+218
-0
lines changed

5 files changed

+218
-0
lines changed

doc/source/apis.rst

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,51 @@ Example::
190190
# ...
191191

192192

193+
Activity lifecycle handling
194+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
195+
196+
.. module:: android.activity
197+
198+
The Android ``Application`` class provides the `ActivityLifecycleCallbacks
199+
<https://developer.android.com/reference/android/app/Application.ActivityLifecycleCallbacks>`_
200+
interface where callbacks can be registered corresponding to `activity
201+
lifecycle
202+
<https://developer.android.com/guide/components/activities/activity-lifecycle>`_
203+
changes. These callbacks can be used to implement logic in the Python app when
204+
the activity changes lifecycle states.
205+
206+
Note that some of the callbacks are not useful in the Python app. For example,
207+
an `onActivityCreated` callback will never be run since the the activity's
208+
`onCreate` callback will complete before the Python app is running. Similarly,
209+
saving instance state in an `onActivitySaveInstanceState` callback will not be
210+
helpful since the Python app doesn't have access to the restored instance
211+
state.
212+
213+
.. function:: register_activity_lifecycle_callbacks(callbackname=callback, ...)
214+
215+
This allows you to bind a callbacks to Activity lifecycle state changes.
216+
The callback names correspond to ``ActivityLifecycleCallbacks`` method
217+
names such as ``onActivityStarted``. See the `ActivityLifecycleCallbacks
218+
<https://developer.android.com/reference/android/app/Application.ActivityLifecycleCallbacks>`_
219+
documentation for names and function signatures for the callbacks.
220+
221+
.. function:: unregister_activity_lifecycle_callbacks(instance)
222+
223+
Unregister a ``ActivityLifecycleCallbacks`` instance previously registered
224+
with :func:`register_activity_lifecycle_callbacks`.
225+
226+
Example::
227+
228+
from android.activity import register_activity_lifecycle_callbacks
229+
230+
def on_activity_stopped(activity):
231+
print('Activity is stopping')
232+
233+
register_activity_lifecycle_callbacks(
234+
onActivityStopped=on_activity_stopped,
235+
)
236+
237+
193238
Receiving Broadcast message
194239
~~~~~~~~~~~~~~~~~~~~~~~~~~~
195240

pythonforandroid/recipes/android/src/android/activity.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,154 @@ def unbind(**kwargs):
6161
_activity.unregisterNewIntentListener(listener)
6262
elif event == 'on_activity_result':
6363
_activity.unregisterActivityResultListener(listener)
64+
65+
66+
# Keep a reference to all the registered classes so that python doesn't
67+
# garbage collect them.
68+
_lifecycle_callbacks = set()
69+
70+
71+
class ActivityLifecycleCallbacks(PythonJavaClass):
72+
"""Callback class for handling PythonActivity lifecycle transitions"""
73+
74+
__javainterfaces__ = ['android/app/Application$ActivityLifecycleCallbacks']
75+
76+
def __init__(self, callbacks):
77+
super().__init__()
78+
79+
# It would be nice to use keyword arguments, but PythonJavaClass
80+
# doesn't allow that in its __cinit__ method.
81+
if not isinstance(callbacks, dict):
82+
raise ValueError('callbacks must be a dict instance')
83+
self.callbacks = callbacks
84+
85+
def _callback(self, name, *args):
86+
func = self.callbacks.get(name)
87+
if func:
88+
return func(*args)
89+
90+
@java_method('(Landroid/app/Activity;Landroid/os/Bundle;)V')
91+
def onActivityCreated(self, activity, savedInstanceState):
92+
self._callback('onActivityCreated', activity, savedInstanceState)
93+
94+
@java_method('(Landroid/app/Activity;)V')
95+
def onActivityDestroyed(self, activity):
96+
self._callback('onActivityDestroyed', activity)
97+
98+
@java_method('(Landroid/app/Activity;)V')
99+
def onActivityPaused(self, activity):
100+
self._callback('onActivityPaused', activity)
101+
102+
@java_method('(Landroid/app/Activity;Landroid/os/Bundle;)V')
103+
def onActivityPostCreated(self, activity, savedInstanceState):
104+
self._callback('onActivityPostCreated', activity, savedInstanceState)
105+
106+
@java_method('(Landroid/app/Activity;)V')
107+
def onActivityPostDestroyed(self, activity):
108+
self._callback('onActivityPostDestroyed', activity)
109+
110+
@java_method('(Landroid/app/Activity;)V')
111+
def onActivityPostPaused(self, activity):
112+
self._callback('onActivityPostPaused', activity)
113+
114+
@java_method('(Landroid/app/Activity;)V')
115+
def onActivityPostResumed(self, activity):
116+
self._callback('onActivityPostResumed', activity)
117+
118+
@java_method('(Landroid/app/Activity;Landroid/os/Bundle;)V')
119+
def onActivityPostSaveInstanceState(self, activity, outState):
120+
self._callback('onActivityPostSaveInstanceState', activity, outState)
121+
122+
@java_method('(Landroid/app/Activity;)V')
123+
def onActivityPostStarted(self, activity):
124+
self._callback('onActivityPostStarted', activity)
125+
126+
@java_method('(Landroid/app/Activity;)V')
127+
def onActivityPostStopped(self, activity):
128+
self._callback('onActivityPostStopped', activity)
129+
130+
@java_method('(Landroid/app/Activity;Landroid/os/Bundle;)V')
131+
def onActivityPreCreated(self, activity, savedInstanceState):
132+
self._callback('onActivityPreCreated', activity, savedInstanceState)
133+
134+
@java_method('(Landroid/app/Activity;)V')
135+
def onActivityPreDestroyed(self, activity):
136+
self._callback('onActivityPreDestroyed', activity)
137+
138+
@java_method('(Landroid/app/Activity;)V')
139+
def onActivityPrePaused(self, activity):
140+
self._callback('onActivityPrePaused', activity)
141+
142+
@java_method('(Landroid/app/Activity;)V')
143+
def onActivityPreResumed(self, activity):
144+
self._callback('onActivityPreResumed', activity)
145+
146+
@java_method('(Landroid/app/Activity;Landroid/os/Bundle;)V')
147+
def onActivityPreSaveInstanceState(self, activity, outState):
148+
self._callback('onActivityPreSaveInstanceState', activity, outState)
149+
150+
@java_method('(Landroid/app/Activity;)V')
151+
def onActivityPreStarted(self, activity):
152+
self._callback('onActivityPreStarted', activity)
153+
154+
@java_method('(Landroid/app/Activity;)V')
155+
def onActivityPreStopped(self, activity):
156+
self._callback('onActivityPreStopped', activity)
157+
158+
@java_method('(Landroid/app/Activity;)V')
159+
def onActivityResumed(self, activity):
160+
self._callback('onActivityResumed', activity)
161+
162+
@java_method('(Landroid/app/Activity;Landroid/os/Bundle;)V')
163+
def onActivitySaveInstanceState(self, activity, outState):
164+
self._callback('onActivitySaveInstanceState', activity, outState)
165+
166+
@java_method('(Landroid/app/Activity;)V')
167+
def onActivityStarted(self, activity):
168+
self._callback('onActivityStarted', activity)
169+
170+
@java_method('(Landroid/app/Activity;)V')
171+
def onActivityStopped(self, activity):
172+
self._callback('onActivityStopped', activity)
173+
174+
175+
def register_activity_lifecycle_callbacks(**callbacks):
176+
"""Register ActivityLifecycleCallbacks instance
177+
178+
The callbacks are supplied as keyword arguments corresponding to the
179+
Application.ActivityLifecycleCallbacks methods such as
180+
onActivityStarted. See the ActivityLifecycleCallbacks documentation
181+
for the signature of each method.
182+
183+
The ActivityLifecycleCallbacks instance is returned so it can be
184+
supplied to unregister_activity_lifecycle_callbacks if needed.
185+
"""
186+
instance = ActivityLifecycleCallbacks(callbacks)
187+
_lifecycle_callbacks.add(instance)
188+
189+
# Use the registerActivityLifecycleCallbacks method from the
190+
# Activity class if it's available (API 29) since it guarantees the
191+
# callbacks will only be run for that activity. Otherwise, fallback
192+
# to the method on the Application class (API 14). In practice there
193+
# should be no difference since p4a applications only have a single
194+
# activity.
195+
if hasattr(_activity, 'registerActivityLifecycleCallbacks'):
196+
_activity.registerActivityLifecycleCallbacks(instance)
197+
else:
198+
app = _activity.getApplication()
199+
app.registerActivityLifecycleCallbacks(instance)
200+
return instance
201+
202+
203+
def unregister_activity_lifecycle_callbacks(instance):
204+
"""Unregister ActivityLifecycleCallbacks instance"""
205+
if hasattr(_activity, 'unregisterActivityLifecycleCallbacks'):
206+
_activity.unregisterActivityLifecycleCallbacks(instance)
207+
else:
208+
app = _activity.getApplication()
209+
app.unregisterActivityLifecycleCallbacks(instance)
210+
211+
try:
212+
_lifecycle_callbacks.remove(instance)
213+
except KeyError:
214+
pass

testapps/on_device_unit_tests/test_app/app_flask.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
vibrate_with_pyjnius,
2626
get_android_python_activity,
2727
set_device_orientation,
28+
setup_lifecycle_callbacks,
2829
)
2930

3031

3132
app = Flask(__name__)
33+
setup_lifecycle_callbacks()
3234
service_running = False
3335
TESTS_TO_PERFORM = dict()
3436
NON_ANDROID_DEVICE_MSG = 'Not running from Android device'

testapps/on_device_unit_tests/test_app/app_kivy.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
load_kv_from,
2323
raise_error,
2424
run_test_suites_into_buffer,
25+
setup_lifecycle_callbacks,
2526
vibrate_with_pyjnius,
2627
)
2728
from widgets import TestImage
@@ -53,6 +54,9 @@ def build(self):
5354
self.sm = Builder.load_string(screen_manager_app)
5455
return self.sm
5556

57+
def on_start(self):
58+
setup_lifecycle_callbacks()
59+
5660
def reset_unittests_results(self, refresh_ui=False):
5761
for img in get_images_with_extension():
5862
subprocess.call(["rm", "-r", img])

testapps/on_device_unit_tests/test_app/tools.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,19 @@ def set_device_orientation(direction):
160160
else:
161161
activity.setRequestedOrientation(
162162
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
163+
164+
165+
@skip_if_not_running_from_android_device
166+
def setup_lifecycle_callbacks():
167+
"""
168+
Register example ActivityLifecycleCallbacks
169+
"""
170+
from android.activity import register_activity_lifecycle_callbacks
171+
172+
register_activity_lifecycle_callbacks(
173+
onActivityStarted=lambda activity: print('onActivityStarted'),
174+
onActivityPaused=lambda activity: print('onActivityPaused'),
175+
onActivityResumed=lambda activity: print('onActivityResumed'),
176+
onActivityStopped=lambda activity: print('onActivityStopped'),
177+
onActivityDestroyed=lambda activity: print('onActivityDestroyed'),
178+
)

0 commit comments

Comments
 (0)