Skip to content

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

files.write("notes.txt", text)
files.append("log.txt", line)
saved = files.read("notes.txt")

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

timer.every(1, tick)
timer.after(0.4, search)
timer.cancel(tick)

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

if network.online():
    send_waiting()

network.on_change(changed)

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

display.keep_on(True)
display.brightness(1)
player = Screen(id="player", orientation="landscape")

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

app_lock.after(5)
button("Lock now", id="lock", icon="lock", command=app_lock.now, screen=home)

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 of click, so pip install apkpy replaced the one you had, and pip uninstall apkpy took 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, and show() 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 for with named calls that did not exist. They exist now: files.read() and files.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; write on_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() and get_value() answers it, as on the phone.
  • If an earlier ApkPy replaced or removed your click, python -m pip install --force-reinstall click puts 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.