Skip to content

Commit ee1393a

Browse files
committed
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. (cherry picked from commit b379a1c)
1 parent c1ec69c commit ee1393a

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)