ApkPy 1.13.0¶
Released 2026-10-16. python -m pip install --upgrade apkpy==1.13.0.
Install 1.13.1 instead
On Python 3.8 to 3.11 the apkpy command of 1.13.0 does not start.
1.13.1 is this version with that fixed:
python -m pip install --upgrade apkpy==1.13.1.
ApkPy is open source from this release, under the Apache License 2.0, and the apps you build with it stay yours.
The rest of the release is the phone. What ApkPy reaches
lists sixty capabilities an app uses; 1.12 covered 43 of them. 1.13.0 covers
all sixty -- a few more narrowly than the words suggest, and the page says
which -- and an Android API ApkPy has no call for can be reached from
Python, checked against android.jar when the app is built.
Apps are built for Android 16, which Google Play asks for since 31 August.
Each part below was used on a phone before it was written down here, and the end of the page says what was not. Eight new examples, 36 to 43, are the apps that did it.
Open source¶
The translator from Python to Android, the Previewer, the apkpy command and
the tests are in the
repository under the Apache
License 2.0. An exception at the end of the LICENSE leaves the apps you
build free of the licence's conditions: what ApkPy writes into them, and an
example you started from, are yours to ship under your own terms. Pull
requests are welcome
(CONTRIBUTING.md).
Releases up to 1.12.1 stay under the licence they were published with. See
what the licence says.
Built for Android 16, so Google Play takes it¶
Since 31 August 2026 Google Play accepts new apps and updates only when they target Android 16 (API 36). ApkPy 1.12 built for API 35: an app made with it could no longer be sent to the store. 1.13.0 builds for 36.
There is nothing to change in your Python. Build again and upload.
What you need. The Android 16 platform of the SDK. apkpy setup installs
it. If your SDK came with Android Studio, the first build downloads it by
itself when the SDK licences are accepted; otherwise tick Android 16
("Baklava"), API 36 in the SDK Manager. Gradle and the JDK are the ones you
already have.
What Android 16 does differently to an app that targets it, and what ApkPy did about each:
| Android 16 | In an ApkPy app |
|---|---|
The new Back is on. An Activity is no longer handed onBackPressed(), and a window is no longer sent the Back key. |
Four places counted on one or the other, and all four were moved to the forms Android still calls: the drawer, the camera, the page of browser.show() and a zoomed picture. One would have broken: built the old way, Back on the second page of browser.show() closed the page instead of going a page back. |
| On a screen 600dp wide or more (tablets, foldables opened), an app can no longer lock its orientation or its shape. | New in 1.13.0, a screen can say which way up it is -- Screen(orientation=), run(orientation=), display.orientation() -- and on those displays it is ignored, as Android says; the Previewer does the same with device("Tablet"). Nothing locks the shape. |
| An app can no longer ask to stay out from under the system bars. | Every screen has been drawn edge to edge since 1.6.1. |
The oldest Android an app runs on is unchanged: 7.0 (API 24).
Android from Python¶
An Android API ApkPy has no call for was reached with native.java(): a
block of Java, in a string. It can be Python now. Name the classes at the
top of the file, and write a function under @native:
Build = native.use("android.os.Build")
Vibrator = native.use("android.os.Vibrator")
VibrationEffect = native.use("android.os.VibrationEffect")
@native(preview=lambda ms: "buzzed")
def buzz(ms: int):
vibrator = context.getSystemService(Vibrator)
if not vibrator.hasVibrator():
return "this phone has no vibrator"
if Build.VERSION.SDK_INT >= 26:
vibrator.vibrate(VibrationEffect.createOneShot(ms, VibrationEffect.DEFAULT_AMPLITUDE))
else:
vibrator.vibrate(ms)
return "buzzed"
There is still no Python on the phone. When the app is built, every class,
field and method the body names is looked up in android.jar, each line is
given its Java type, and the function is written out as the Java you would
have typed. So a method that does not exist, an argument of the wrong kind,
or a class newer than Android 7.0 used outside a test of
Build.VERSION.SDK_INT stops the build with the line (U2047) -- where a
runtime bridge finds out on somebody's phone. In the Previewer, preview=
answers.
Not yet: listeners (an interface implemented in Python) and the classes of a
library added with native.gradle(); native.java() is still there for
those. See Written in Python and
36_android_from_python.py.
The rest of the phone¶
Twelve things an app could not do without writing Java, each one call:
| A touch you can feel | haptics.tap(), tick(), heavy(), success(), error(), vibrate(ms) |
| Reading aloud | speech.say(text, language=, rate=, pitch=, on_result=), stop(), is_speaking() |
| Dictation | speech.listen(prompt=, language=, on_result=), can_listen() |
| A web page | browser.open(url) over the app, browser.show(url=, html=, title=) inside it |
| A file for the person | files.save_as(name, text, on_result=), files.save_copy(path) |
| A download by the system | downloads.start(url, name=, title=, on_result=) |
| The calendar | calendar.add_event(title, start, end=, location=, notes=, all_day=) |
| The dialler and messages | phone.dial(number), phone.sms(number, text) |
| The microphone | recorder.start(on_result=), stop(), is_recording(), seconds() |
| Print, or a PDF | printing.html(html, name=), printing.text(text, name=) |
| A link that opens the app | incoming.links("notes"), incoming.link() |
| Something shared into it | incoming.shares("text"), incoming.text(), incoming.file() |
They answer in three ways, the same everywhere. A call that hands over to
another app answers at once with a word: "ok", or why not. A call that
needs the person or takes time answers later, in on_result(ok, value). A
question answers yes or no: if recorder.is_recording():.
What it costs the app is what it uses. The dialler, the messages app and the
calendar open with everything filled in and the person presses the last
button, so no permission is asked for; files.save_as() goes through the
system's picker; only recorder asks the person for anything. In the
Previewer a call that hands over draws the other app arriving, with what it
was given. See The rest of the phone and
37_the_rest_of_the_phone.py.
Outside the app¶
A widget on the home screen, a tile in quick settings, ads, updates through Google Play and health data. Each is something the app has, so four of the five begin with one line at the top of the file:
home_widget.define("Steps today", "0", caption="steps", icon="directions_walk")
quick_tile.define("Walk", icon="directions_walk")
ads.setup("test")
health.policy("https://example.com/privacy")
home_widget.set("6 482", caption="steps") changes what the widget shows,
and it keeps showing it with the app closed. ads.banner("test") puts
Google's sample ad at the bottom of the screen, room taken before it
arrives, and Google's consent form comes first where the law asks for it.
updates.check() asks Play for a newer version, and health.read() adds up
a stretch of time in Health Connect. Google's ad and update
libraries go into an app that calls them and into no other. See
Outside the app and
38_outside_the_app.py.
Text of the app's own¶
The text is kept in the app's own folder, exactly as given, and a file is
replaced whole or not at all, so an app closed in the middle of a save keeps
the file it had. Nobody is asked for a permission. See
Text of the app's own and
39_text_files.py.
Timers¶
A function of the app run later, or again and again, while its screen is on
show. A timer is known by the function it calls, so there is nothing to keep,
and starting it again starts it over: timer.after(0.4, search) in a field's
on_change searches once, when the typing stops. Away from the screen
nothing runs, and what fell due runs once when it is back. See
Timers and
40_timers.py.
The connection¶
network.online() says whether the phone reaches the internet right now --
a Wi-Fi that wants a login page first is not online -- network.kind() what
the connection is, and network.metered() whether the person pays for it.
on_change() calls the function when one of the three changes. In the
Previewer, a click on the connection in the status bar takes it away. See
The connection and
41_network.py.
The display¶
A recipe that keeps the phone awake, a ticket at full brightness, a player
with no bars around it, a screen that stays upright. What a screen asks is
its own: a screen opened over it has none of it, and turning the phone keeps
it. The phone's own brightness setting is never touched. See
The display and
42_display.py.
The app lock¶
The app locks itself after five minutes away or untouched, and opens with
the phone's own fingerprint, face or screen lock. The rule is kept on the
phone and starts with the app's process, so an app closed from the recent
apps comes back locked. Locked, every screen is covered and the phone's own
prompt asks; Back leaves the app, TalkBack does not read what is covered,
and the recent apps show no picture of it. See
The app lock and
43_app_lock.py.
Fixed¶
- Installing ApkPy no longer writes over
click. The package carried its own copy ofclick, sopip install apkpyreplaced the one you had, andpip uninstall apkpytook it away. Every release up to 1.12.1 has this. on_change=lambda ...on a text field, a switch, a checkbox or a slider did nothing on a phone. The lambda was dropped while the app was built, and the Previewer ran it, so it was only ever found on a phone. It works.native.manifest(xml=...)wrote the element beside<application>, where Android reads a<service>or a<receiver>as nothing. It is written inside;<queries>stays beside it.- A permission asked for under its whole name could stop the build when the app also declared it some other way. One permission is asked for once.
- In the Previewer,
button.set_value()did not change what the button said;hide()did not hide a container with rounded corners, andshow()then broke it; a label given more lines was drawn over what came next. Each does what the phone always did. - The error for
open()and forwithnamed calls that did not exist. They exist now:files.read()andfiles.write().
Upgrading¶
- Install the Android 16 platform before the first build:
apkpy setup, or the SDK Manager. Then build again. on_change=lambda: ...that does not take the value stops the build (U2033). It never did anything on the phone; writeon_change=lambda value: ....- New names ApkPy knows:
haptics,speech,browser,downloads,calendar,phone,recorder,printing,incoming,home_widget,quick_tile,ads,updates,health,timer,network,display,app_lock. A name your app made its own still wins:phone = inputs("Phone")is still your field. - In the Previewer, a button changes its caption with
set_value()andget_value()answers it, as on the phone. - If an earlier ApkPy replaced or removed your
click,python -m pip install --force-reinstall clickputs it back. - Regenerate the Android project.
Seen on a phone, and not¶
Everything above was used on a Xiaomi running Android 16, driven through
adb: the six buttons of example 36, the calls of example 37, the
widget's calls and the tile's offer, Health Connect's screen, the consent form,
a banner, an interstitial and a rewarded ad, text that survived the app
being closed, timers away from the screen and through a rebuilt screen, the
connection going and coming back, each call of the display through two
rebuilds, and the app lock -- away, untouched, closed while locked, the
cover, Back on it, and what TalkBack is given -- opened with a finger. The
four places Android 16 changed for Back were seen doing the right thing.
Not seen: the widget on a home screen and the tile in quick settings, added by hand and used; a health reading with permission given; an update through Play, which needs a copy installed from Play; a banner with the keyboard up; touch effects felt in the hand (the test phone has touch feedback off); timers with the phone locked; Wi-Fi handing over to mobile data; a tablet; the app lock's path for Android 7 to 10, and face unlock.
The complete list is in the changelog.