Skip to content
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

WorkManager support #2613

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
2 changes: 1 addition & 1 deletion ci/makefiles/android.mk
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ ANDROID_NDK_VERSION_LEGACY ?= 21e
ANDROID_SDK_TOOLS_VERSION ?= 6514223
ANDROID_SDK_BUILD_TOOLS_VERSION ?= 29.0.3
ANDROID_HOME ?= $(HOME)/.android
ANDROID_API_LEVEL ?= 27
ANDROID_API_LEVEL ?= 30

# per OS dictionary-like
UNAME_S := $(shell uname -s)
Expand Down
6 changes: 6 additions & 0 deletions doc/source/buildoptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ options (this list may not be exhaustive):
included in AndroidManifest.xml.
- ``--service``: A service name and the Python script it should
run. See :ref:`arbitrary_scripts_services`.
- ``--worker``: A worker name and the Python script it should run. See
:ref:`workers` for details.
- ``--add-source``: Add a source directory to the app's Java code.
- ``--no-compile-pyo``: Do not optimise .py files to .pyo.
- ``--enable-androidx``: Enable AndroidX support library.
Expand Down Expand Up @@ -149,6 +151,8 @@ ready.
included in AndroidManifest.xml.
- ``--service``: A service name and the Python script it should
run. See :ref:`arbitrary_scripts_services`.
- ``--worker``: A worker name and the Python script it should run. See
:ref:`workers` for details.
- ``add-source``: Add a source directory to the app's Java code.
- ``--port``: The port on localhost that the WebView will
access. Defaults to 5000.
Expand All @@ -170,6 +174,8 @@ systems and frameworks.
- ``--version``: The version number.
- ``--service``: A service name and the Python script it should
run. See :ref:`arbitrary_scripts_services`.
- ``--worker``: A worker name and the Python script it should run. See
:ref:`workers` for details.
- ``--blacklist``: The path to a file containing blacklisted patterns
that will be excluded from the final AAR. Defaults to ``./blacklist.txt``.
- ``--whitelist``: The path to a file containing whitelisted patterns
Expand Down
1 change: 1 addition & 0 deletions doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Contents
recipes
bootstraps
services
workers
troubleshooting
docker
contribute
Expand Down
129 changes: 129 additions & 0 deletions doc/source/workers.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
Workers
=======

python-for-android supports worker tasks using `WorkManager
<https://developer.android.com/topic/libraries/architecture/workmanager>`_.
``WorkManager`` tasks are the recommended way to perform both one-time
and recurring work with current Android. Starting with Android 12,
worker tasks will also be required to replace foreground services in
some cases.

Each worker runs tasks in a service declared in the
``AndroidManifest.xml`` file. This is managed by python-for-android with
a service generated from the application package. The worker service is
specified to run in a separate process since python-for-android does not
support running multiple Python interpreters in the same process.

Worker creation
---------------

To create the worker, create a python script with your worker code and
add a ``--worker=myworker:PATH_TO_WORKER_PY`` argument when calling
python-for-android.

The ``myworker`` name before the colon is in the names of the worker and
worker service classes, via which you will interact with it later.

The ``PATH_TO_WORKER_PY`` is the relative path to the worker entry point
(like ``workers/myworker.py``)

You can add multiple ``--worker`` arguments to include multiple workers,
all of which you will later be able to stop and start from your app.

Running workers
---------------

To run the workers (i.e. starting them from within your main app code),
you must use PyJNIus to interact with the Java class python-for-android
creates for each one. First, you need to create a work request using the
``buildInputData`` helper function which configures the work to run in
the appropriate service class::

from jnius import autoclass

worker = autoclass('your.package.domain.package.name.MyworkerWorker')
OneTimeWorkRequestBuilder = autoclass('androidx.work.OneTimeWorkRequest$Builder')
argument = ''
data = worker.buildInputData(argument)
request = OneTimeWorkRequestBuilder(worker._class).setInputData(data).build()

Here, ``your.package.domain.package.name`` refers to the package
identifier of your APK. The identifier is set by the ``--package``
argument to python-for-android. The name of the worker is
``MyworkerWorker``, where ``Myworker`` is the identifier that was
previously passed to the ``--worker`` argument, but with the first
letter upper case. You must also pass the ``argument`` parameter even if
(as here) it is an empty string or `None`. If you do pass it, the
service can make use of this argument.

The argument is made available to your worker via the
'PYTHON_SERVICE_ARGUMENT' environment variable. It is exposed as a
simple string, so if you want to pass in multiple values, we would
recommend using the json module to encode and decode more complex data.
::

from os import environ
argument = environ.get('PYTHON_SERVICE_ARGUMENT', '')

Now the work request needs to be enqueued in the application's
`WorkManager
<https://developer.android.com/reference/androidx/work/WorkManager>`_
instance::

mActivity = autoclass('org.kivy.android.PythonActivity').mActivity
WorkManager = autoclass('androidx.work.WorkManager')
work_manager = WorkManager.getInstance(mActivity)
work_manager.enqueue(request)

Enqueuing a work request is asynchronous and returns an `Operation
<https://developer.android.com/reference/androidx/work/Operation>`_. To
block until the request has been queued, wait for the state to resolve::

operation = work_manager.enqueue(request)
operation.getResult().get()

Once the work request has been queued, information about the request
such as its current state can be requested from ``WorkManager``::

request_id = request.getId()
work_info = work_manager.getWorkInfoById(request_id).get()
state = work_info.getState()
print('Work request state:', state.toString())
if state.isFinished():
print('Work request has completed')

.. note::

The app root directory for Python imports will be in the app root
folder even if the worker file is in a subfolder. If the worker is
in the ``worker/`` folder, it must be imported with ``import
worker.module`` rather than ``import module``.

Worker progress
~~~~~~~~~~~~~~~

A worker can send intermediate progress data for the work request that
can be retrieved in the activity. From the worker script, use the
``setProgressAsync`` method from the worker class instance::

from jnius import autoclass

mWorker = autoclass('your.package.domain.package.name.MyworkerWorker').mWorker
DataBuilder = autoclass('androidx.work.Data$Builder')

data = DataBuilder().putInt('PROGRESS', 50).build()
mWorker.setProgressAsync(data)

The progress can be retrieved in the activity from the work request
information::

request_id = request.getId()
work_info = work_manager.getWorkInfoById(request_id).get()
progress = work_info.getProgress().getInt('PROGRESS', 0)
print('Work request {}% complete'.format(progress))

.. note::

At present, there is no method to return output data for the work
request. The work is either succeeded or failed based on the exit
status of the worker script.
52 changes: 52 additions & 0 deletions pythonforandroid/bootstraps/common/build/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,39 @@ def make_package(args):
base_service_class=base_service_class,
)

worker_names = []
for spec in args.workers:
spec = spec.split(':')
name = spec[0]
entrypoint = spec[1]

worker_names.append(name)
worker_target_path = \
'src/main/java/{}/{}Worker.java'.format(
args.package.replace(".", "/"),
name.capitalize()
)
render(
'Worker.tmpl.java',
worker_target_path,
name=name,
entrypoint=entrypoint,
args=args,
)

worker_service_target_path = \
'src/main/java/{}/{}WorkerService.java'.format(
args.package.replace(".", "/"),
name.capitalize()
)
render(
'WorkerService.tmpl.java',
worker_service_target_path,
name=name,
entrypoint=entrypoint,
args=args,
)

# Find the SDK directory and target API
with open('project.properties', 'r') as fileh:
target = fileh.read().strip()
Expand All @@ -497,6 +530,15 @@ def make_package(args):
sdk_dir = fileh.read().strip()
sdk_dir = sdk_dir[8:]

# Specific WorkManager versions require newer SDK versions.
#
# See https://developer.android.com/jetpack/androidx/releases/work
# for details.
if int(android_api) >= 31:
work_manager_version = '2.7.1'
else:
work_manager_version = '2.6.0'

# Try to build with the newest available build tools
ignored = {".DS_Store", ".ds_store"}
build_tools_versions = [x for x in listdir(join(sdk_dir, 'build-tools')) if x not in ignored]
Expand Down Expand Up @@ -528,6 +570,7 @@ def make_package(args):
"args": args,
"service": service,
"service_names": service_names,
"worker_names": worker_names,
"android_api": android_api,
"debug": "debug" in args.build_mode,
"native_services": args.native_services
Expand Down Expand Up @@ -556,6 +599,7 @@ def make_package(args):
build_tools_version=build_tools_version,
debug_build="debug" in args.build_mode,
is_library=(get_bootstrap_name() == 'service_library'),
work_manager_version=work_manager_version,
)

# gradle properties
Expand Down Expand Up @@ -710,6 +754,9 @@ def parse_args_and_make_package(args=None):
ap.add_argument('--service', dest='services', action='append', default=[],
help='Declare a new service entrypoint: '
'NAME:PATH_TO_PY[:foreground]')
ap.add_argument('--worker', dest='workers', action='append', default=[],
help='Declare a new worker entrypoint: '
'NAME:PATH_TO_PY')
ap.add_argument('--native-service', dest='native_services', action='append', default=[],
help='Declare a new native service: '
'package.name.service')
Expand Down Expand Up @@ -947,6 +994,11 @@ def _read_configuration():
'--launcher (SDL2 bootstrap only)' +
'to have something to launch inside the .apk!')
sys.exit(1)

if args.workers and not args.enable_androidx:
print('WARNING: Enabling androidx for worker support')
args.enable_androidx = True

make_package(args)

return args
Expand Down
Loading