Skip to content
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
4 changes: 3 additions & 1 deletion doc/source/bootstraps.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ Bootstraps

This page is about creating new bootstrap backends. For build options
of existing bootstraps (i.e. with SDL2, Webview, etc.), see
:ref:`build options <bootstrap_build_options>`.
:ref:`build options <bootstrap_build_options>`. If your bootstrap is
to run Kivy 3 apps, it must also satisfy
:ref:`Kivy's bootstrap contract <kivy_bootstrap_contract>`.

python-for-android (p4a) supports multiple *bootstraps*. These fulfill a
similar role to recipes, but instead of describing how to compile a
Expand Down
1 change: 1 addition & 0 deletions doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Contents
distutils
recipes
bootstraps
kivy_bootstrap
services
troubleshooting
docker
Expand Down
133 changes: 133 additions & 0 deletions doc/source/kivy_bootstrap.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
.. _kivy_bootstrap_contract:

Supporting Kivy 3 (the Kivy bootstrap contract)
===============================================

Kivy 3 asks the bootstrap for the current Android ``Activity`` instead of
reflecting a class name of its own. Any tool that builds Android APKs — this
project, another build tool, or a bootstrap you write yourself — makes Kivy 3
work by answering that question.

This page is the implementer's guide: what to ship, the rules it must obey.
p4a's own implementation is
``pythonforandroid/recipes/android/src/_kivy_bootstrap.py``.

Kivy 2.3.1 does not use this contract and is unaffected; see
`Supporting Kivy 2.3.1 as well`_ if you need both.

What you ship
-------------

One pure-Python module named ``_kivy_bootstrap``, importable at the **top
level** of the running app's ``sys.path``. Nothing else: no registration call,
no import order to arrange, no Kivy dependency.

The name is Kivy's, not any bootstrap's. That is the point of the arrangement —
Kivy depends on the name, so it depends on no particular bootstrap, and an
unmodified Kivy runs on an APK built by any of them.

Kivy imports the module the first time it needs the Activity, so it must be on
``sys.path`` before the application's ``main.py`` runs. In p4a it is a
``py_modules`` entry of the ``android`` recipe, so it lands beside the app's
other top-level modules in ``site-packages``.

``get_activity()`` (required)
-----------------------------

Return the current ``android.app.Activity``, or ``None`` where there is none.

.. code-block:: python

def get_activity():
return SomeActivityClass.mActivity

Kivy calls this **live on every access** and never caches the result, so your
implementation must not cache it either (see rule 2 below). ``None`` is a
legitimate answer, not a failure: a background service runs with no Activity,
and Kivy treats that as the ordinary case it is.

``get_context()`` (optional)
----------------------------

Return an ``android.content.Context``, or ``None``.

Implement this only if your bootstrap runs processes that never have an
Activity and still need a Context. When it is absent — or returns ``None`` —
Kivy derives the Application context from the current Activity, which is
equivalent for everything Kivy uses a Context for. p4a does not implement it.

``remove_presplash()`` (optional)
---------------------------------

Dismiss your boot splash. Kivy calls this once it has drawn its first frame,
which is the one thing about a splash screen that only Kivy knows.

*How* a splash is dismissed is entirely yours, and the mechanisms differ
fundamentally: p4a overlays a ``View`` and removes it, while a bootstrap using
the Android 12 system splash releases a keep-on-screen condition instead — not
a method on the Activity at all. That is why this is an optional function on
your module rather than a method Kivy calls on the Activity.

If you have no splash to dismiss, omit the function. Kivy treats its absence as
the no-op it is, with no warning.

Rules
-----

1. **Never import Kivy from this module.** Kivy reads its ``KIVY_*``
environment and builds its configuration at import time, so importing it
before the application runs would freeze that configuration before the app's
``main.py`` could set it. Kivy pulls from you precisely so that you never
have to import it.

2. **Never cache the Activity.** Android destroys and recreates the Activity on
configuration changes (rotation, dark mode, locale, multi-window) and after
process death. A stored instance goes stale, and holding one in a Python
global pins a JNI reference to a dead Activity. Read it fresh on every call
and staleness stops being your problem.

3. **Resolve reflection inside the call, not at import.** Resolve the Java
class on first call and cache *the class* if you like, but do not do it at
module import: a reflection failure would then surface from Kivy's discovery
import, where it looks like a missing module rather than the real fault.

4. **Fail at import only with an ImportError, and only if you mean it.** Kivy
treats ``ImportError`` as "this bootstrap does not implement the contract"
and moves on to raise a diagnostic. Any *other* exception escaping your
module's import is treated as a fault in your module and propagates
unchanged, so it is not mistaken for an absent bootstrap.

5. **A presplash hook must not raise, and must tolerate repeat calls.**
Kivy does not guard the call and makes no promise about calling it exactly
once. Doing nothing is a correct outcome; raising is not.

6. **Hold no state beyond the resolved class.** Kivy may call these from more
than one thread. A stateless module is thread-safe without a lock, and the
contract is designed so that statelessness costs nothing.

What Kivy does with it
----------------------

Useful when debugging your implementation:

* The module is imported lazily, on first use, and the outcome is cached —
including failure, so a build with no such module does not pay for a failed
import on every geometry read.
* If the module is missing (or exposes no callable ``get_activity``) **while an
Android runtime is present**, Kivy raises ``ActivityProviderMissing``. This
is deliberately not caught by Kivy's display-geometry getters, which would
otherwise mask a broken build as plausible-looking defaults.
* ``kivy.mobile.get_app_context()`` prefers your ``get_context()`` and
otherwise calls ``getApplicationContext()`` on the current Activity.
* ``kivy.mobile.get_activity()`` returns exactly what your function returned,
``None`` included.

Supporting Kivy 2.3.1 as well
-----------------------------

Kivy 2.3.1 predates this contract. It reaches p4a's Python ``android`` module
directly — ``android.remove_presplash()`` and friends — and hardcodes
``org.kivy.android.PythonActivity``. A bootstrap that wants to run both Kivy
2.3.1 and Kivy 3 therefore has to satisfy both: ship ``_kivy_bootstrap`` for
Kivy 3, and provide the ``android`` module (and an activity of that class name)
for 2.3.1. A bootstrap targeting Kivy 3 alone needs only this contract.
64 changes: 64 additions & 0 deletions pythonforandroid/recipes/android/src/_kivy_bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""python-for-android's implementation of Kivy's Android bootstrap contract.

Kivy 3 holds no bootstrap class name of its own: it imports ``_kivy_bootstrap``
and asks it for the current ``android.app.Activity`` instead. Kivy pulls on
first use rather than having the bootstrap register at startup, which suits p4a:
``start.c`` runs the user's ``main.py`` as the process entry point, so there is
no p4a-owned Python before the app to register from. It also means p4a never
imports Kivy, which would otherwise fix Kivy's ``KIVY_*`` environment and config
before the app had a chance to set them.

The activity class comes from the build-time generated ``android.config``, so a
custom activity set with ``--activity-class-name`` is honoured rather than
assumed to be the default.

Besides the required ``get_activity()``, this implements the contract's optional
``remove_presplash()``. ``get_context()`` is not implemented: p4a's Activity can
always supply the Application context, and Kivy falls back to deriving it.

This module deliberately holds no state beyond the resolved class: the Activity
is read fresh on every call, so it stays correct across the recreation Android
performs on rotation, configuration changes and process death.
"""

from jnius import autoclass

from android.config import ACTIVITY_CLASS_NAME

_activity_class = None


def get_activity():
"""Return the current ``android.app.Activity``, or ``None`` if there is none.

``None`` is a legitimate answer — a p4a service runs without an Activity.
"""
global _activity_class
if _activity_class is None:
# Resolved on first use rather than at import so that reflection
# failures surface from the call, not from Kivy's discovery import.
_activity_class = autoclass(ACTIVITY_CLASS_NAME)
return _activity_class.mActivity


def remove_presplash():
"""Dismiss the loading screen, if this build has one.

Kivy calls this once it has drawn its first frame — the moment only Kivy
knows. How the splash goes away is p4a's business: here it means removing
the View that ``PythonActivity`` laid over the app, which the Java method
marshals onto the UI thread itself, so there is nothing to arrange here.

Not every p4a build has a splash to remove: ``service_only`` has no
``removeLoadingScreen`` at all, and a custom ``--activity-class-name`` need
not inherit one. So the activity is asked, rather than a hardcoded list of
bootstraps consulted — that list has already drifted once, ``android``'s own
``remove_presplash`` being gated to sdl2/sdl3 though the webview activity has
the method too. Doing nothing is a valid outcome and Kivy treats it as one.
"""
activity = get_activity()
if activity is None:
return
remove = getattr(activity, "removeLoadingScreen", None)
if remove is not None:
remove()
3 changes: 3 additions & 0 deletions pythonforandroid/recipes/android/src/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,8 @@
version='1.0',
packages=['android'],
package_dir={'android': 'android'},
# Top-level, not under `android`: the name is Kivy's, and Kivy imports it
# without knowing which bootstrap built the app.
py_modules=['_kivy_bootstrap'],
ext_modules=cythonized_modules
)
Loading