Skip to content

android: implement Kivy 3's bootstrap contract (_kivy_bootstrap)#3356

Merged
kuzeyron merged 2 commits into
kivy:developfrom
ElliotGarbus:kivy3-bootstrap-contract
Jul 26, 2026
Merged

android: implement Kivy 3's bootstrap contract (_kivy_bootstrap)#3356
kuzeyron merged 2 commits into
kivy:developfrom
ElliotGarbus:kivy3-bootstrap-contract

Conversation

@ElliotGarbus

@ElliotGarbus ElliotGarbus commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

The problem

Kivy reaches the Android Activity by reflecting a class name it hardcodes,
org.kivy.android.PythonActivity. That makes Kivy depend on a p4a
implementation detail, and it silently breaks any build whose activity is named
something else — including p4a's own --activity-class-name.

Kivy 3 inverts it: Kivy names no activity class and asks the bootstrap that
built the APK instead. This PR is p4a's half, making p4a a bootstrap that
satisfies Kivy's contract rather than the source of truth Kivy is written
against. It is inert for every existing build — nothing imports the new module
unless Kivy 3 does, and Kivy 2.3.1 keeps using the android module exactly as
it does today.

The contract

A bootstrap ships a top-level module named _kivy_bootstrap. The name is
Kivy's, not any bootstrap's, which is the point: Kivy depends on the name
without depending on who implements it.

member required meaning
get_activity() yes the current android.app.Activity, or None where none exists
get_context() no a Context for processes that never have an Activity
remove_presplash() no dismiss the boot splash; Kivy calls it after the first frame

Kivy imports the module on first use and calls get_activity() live on every
access. Nothing is cached, so nothing goes stale when Android recreates the
Activity on rotation, configuration change or process death. None is a
legitimate answer — a service runs without an Activity — while a missing
module
on a real device raises rather than degrading to a plausible default.

Kivy pulls instead of the bootstrap pushing a provider at startup, for two
p4a-specific reasons. start.c runs the user's main.py as the process entry
point, so there is no p4a-owned Python to register from without adding a new
C-level seam to every bootstrap. And pushing would mean importing Kivy before
the app runs, which would freeze Kivy's KIVY_* environment and config before
main.py could set them. Pulling means p4a never imports Kivy at all.

What this PR adds

  • pythonforandroid/recipes/android/src/_kivy_bootstrap.py (new, 64 lines,
    mostly docstrings explaining the reasoning). The class name comes from the
    build-time generated android.config, so --activity-class-name is honoured
    rather than assumed; it is resolved on first call rather than at import, so a
    reflection failure surfaces from the call that needs the Activity instead of
    from Kivy's discovery import. remove_presplash() asks the activity for
    removeLoadingScreen rather than branching on the bootstrap name — that list
    has already drifted once, android's own remove_presplash being gated to
    sdl2/sdl3 though the webview activity has the method too.
  • recipes/android/src/setup.py — one py_modules line.
  • doc/source/kivy_bootstrap.rst (new) — the contract documented for
    bootstrap authors: what to ship, the rules it must obey, and this file as the
    worked example. Added to the toctree and cross-linked from bootstraps.rst.

No changes to start.c, to any bootstrap, or to the build. The android recipe
is the natural home: it is already a hard dependency of the kivy recipe, so
every Kivy app has it, and it is the recipe that already knows
ACTIVITY_CLASS_NAME.

Verified on device

Built with this repo's sdl3 bootstrap and Kivy 3.0.0.dev0 from a local
checkout (NDK r28c, SDL3 3.4.2, python3 3.14.2), then run on an x86_64 emulator:

P4A_kivy_DIR=~/src/kivy3-src p4a apk --private ~/src/kivy3-contract-demo \
  --package=org.kivy.contract3 --name="Kivy3 Contract" --version=0.1 \
  --bootstrap=sdl3 --requirements=kivy \
  --arch=x86_64 --android-api=35 --ndk-api=24 --save-wheel-dir=~/kivy3-wheels

p4a picked the Kivy 3 path by itself: it read 3.0.0.dev0 from the checkout,
skipped use_cython.patch, and set KIVY_CROSS_PLATFORM=android. A
purpose-built app then exercised each part of the contract:

CONTRACT: ACTIVITY_OK package=org.kivy.contract3
CONTRACT: PULLED_FROM_BOOTSTRAP_OK kivy.get_activity() is the bootstrap's Activity
CONTRACT: NO_CLASS_NAME_IN_KIVY_OK kivy asked _kivy_bootstrap; build chose org.kivy.android.PythonActivity
CONTRACT: APP_CONTEXT_OK package=org.kivy.contract3
CONTRACT: GEOMETRY_OK dpi=160.0 scale=1.00 density=1.00 fontscale=1.00 keyboard=0 ...
CONTRACT: STORAGE_OK user_data_dir=/data/user/0/org.kivy.contract3/files
CONTRACT: CLIPBOARD_OK provider=ClipboardAndroid round-trip exact
CONTRACT: PRESPLASH_OK bootstrap implements remove_presplash
CONTRACT: ALL_OK 8/8

PULLED_FROM_BOOTSTRAP asserts that kivy.get_activity() and
_kivy_bootstrap.get_activity() return the same Java object.
NO_CLASS_NAME_IN_KIVY asserts that the live Activity's Java class equals
android.config.ACTIVITY_CLASS_NAME — the build's choice, not Kivy's.
CLIPBOARD exercises the coupling that started this. Kivy's log carries no
"Could not remove android presplash" warning, which is what a bootstrap failing
this contract produces today.

As a by-product, --save-wheel-dir on that build emitted
kivy-3.0.0.dev0-cp314-cp314-android_24_x86_64.whl alongside the APK, with
_kivy_bootstrap.py riding along at the top level of the android wheel.

The Kivy side is kivy/kivy#9352, for Kivy 3.0 (kivy.mobile, app.py,
clipboard_android.py, base.py, plus contract tests); after it,
grep -rn "PythonActivity\|mActivity" kivy/ matches nothing outside the test
harness, and the only code that knows the class name is p4a's file above.

A second, Gradle- and wheel-based Android builder I
am working on implements the same contract against an unmodified Kivy, differing
only in the ways the contract allows — its own activity constant, and no
remove_presplash() because it uses the androidx core-splashscreen system
splash.

Kivy 3 no longer reflects a hardcoded org.kivy.android.PythonActivity.  It
imports a top-level `_kivy_bootstrap` module supplied by whoever built the APK
and pulls the current Activity from it, so Kivy stops depending on a p4a
implementation detail and p4a becomes one bootstrap satisfying the contract.

The `android` recipe is the natural home: it is already a hard dependency of the
`kivy` recipe, so every Kivy app has it, and it already knows
ACTIVITY_CLASS_NAME.  Reading the class from the generated `android.config`
means --activity-class-name is honoured rather than assumed, which the
hardcoded name in Kivy silently broke.

The contract's optional `remove_presplash()` is implemented too, restoring for
Kivy 3 the capability negotiation Kivy 2.x got from `try: import android`.  It
asks the activity for removeLoadingScreen instead of branching on the bootstrap
name, since not every build has one and such a list has already drifted.

Nothing imports the module unless Kivy 3 does, so this is inert for existing
builds; Kivy 2.3.1 keeps using the `android` module as before.

doc/source/kivy_bootstrap.rst documents the contract for bootstrap authors —
what to ship, the rules it must obey, and this file as the worked example.
@ElliotGarbus

ElliotGarbus commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

The Kivy side is now up as kivy/kivy#9352, so the whole thing can be read together: Kivy names no activity class and pulls the Activity from _kivy_bootstrap, and this PR is the module that answers. Both were built and run together on an emulator — 8/8 contract checks, including that the Activity Kivy hands out is the class android.config.ACTIVITY_CLASS_NAME chose, which is what makes --activity-class-name work again.

@T-Dynamos

T-Dynamos commented Jul 25, 2026

Copy link
Copy Markdown
Member

Hi @ElliotGarbus I gave it an initial read and it looks promising.

However I have few concerns regarding docs:

  1. I don't think section Minimal implementation is necessary as above doc strings already explains the functions well. Same for Worked example: python-for-android.
  2. Let's not have any references to project kivyforge yet - before it's officially announced and released.

CC: @AndreMiras for his opinions

Drop the minimal-implementation, worked-example, kivyforge, and on-device
verification sections per maintainer feedback. Point readers at p4a's
`_kivy_bootstrap.py` instead of reproducing it, and stop naming other
build tools.
@ElliotGarbus

Copy link
Copy Markdown
Contributor Author

Hi @ElliotGarbus I gave it an initial read and it looks promising.

However I have few concerns regarding docs:

  1. I don't think section Minimal implementation is necessary as above doc strings already explains the functions well. Same for Worked example: python-for-android.
  2. Let's not have any references to project kivyforge yet - before it's officially announced and released.

CC: @AndreMiras for his opinions

Deleted those sections, removed all references to kivyforge.

@AndreMiras AndreMiras left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good thanks.
Ideally we would have some tests for _kivy_bootstrap e.g. for the lazy loading of the activity class, but that could come in a follow up.
I don't see any blocker for merging this

@kuzeyron
kuzeyron merged commit f7798e6 into kivy:develop Jul 26, 2026
74 of 76 checks passed
kuzeyron pushed a commit to kivy/kivy that referenced this pull request Jul 26, 2026
…a hardcoded PythonActivity (#9352)

* kivy.mobile: pull the Android Activity from the bootstrap

The Android backend reflected a hardcoded org.kivy.android.PythonActivity.  That
is a python-for-android implementation detail, and it silently breaks any build
whose activity is named something else, including p4a's own
--activity-class-name.

Kivy now names no activity class.  It imports a top-level `_kivy_bootstrap`
module supplied by whoever built the APK and pulls the Activity from it on every
access, so nothing goes stale when Android recreates the Activity on rotation,
configuration change or process death.  A missing module while an Android
runtime is present raises ActivityProviderMissing rather than degrading to
plausible-looking defaults, since it can only mean the app was built by
something that does not support Kivy 3; off-device the getters keep returning
their documented defaults.

Kivy pulls rather than the bootstrap registering a provider at startup.  p4a
runs the app's main.py as the process entry point, so it has no Python of its
own to register from, and registering would mean importing Kivy before the app
could set its KIVY_* environment and config.

Two optional members keep bootstraps that differ from p4a working: get_context()
for processes that never have an Activity, and remove_presplash(), because
dismissing a splash belongs to the bootstrap -- p4a removes an overlaid View,
while a bootstrap using the Android 12 system splash has no such method to
offer.  base.py's remove_android_splash now goes through that hook.

app.py and clipboard_android.py move off PythonActivity onto the backend, which
gains run_on_ui_thread(), get_files_dir() and get_cache_dir() so neither needs
jnius boilerplate or an Activity of its own; the clipboard also stops stashing
state on the Activity class.  Afterwards no PythonActivity or mActivity
reference remains outside the test harness.

Verified on an x86_64 emulator against a python-for-android sdl3 build, which
implements the same contract in kivy/python-for-android#3356.

* clipboard_android: simplify paste coercion into one expression

* ci: retrigger PR checks without wheel builds
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants