Skip to content

Your own Java

Looking for one that is already written? Native recipes has six, compiled before they were published.

ApkPy wraps a large part of Android in calls of its own. When the thing you need is not among them, the answer used to be "wait for the next release" -- and editing the generated project in Android Studio is not an answer, because apkpy build rewrites that project every time.

native gives the gap a shape. You do not paste Java into the middle of an app; you declare a function with a name, arguments and one answer, and you say what the Previewer answers instead.

from apkpy_lib import Screen, button, label, native, run

home = Screen(id="home")
reading = label("", id="reading", screen=home)

battery = native.java(
    "batteryLevel",
    imports=["android.os.BatteryManager", "android.content.Context"],
    code="""
        BatteryManager bm = (BatteryManager)
                context.getSystemService(Context.BATTERY_SERVICE);
        return String.valueOf(bm.getIntProperty(
                BatteryManager.BATTERY_PROPERTY_CAPACITY));
    """,
    preview=lambda: "87",
)


def show():
    reading.set_value("Battery: " + battery() + "%")


button("Read", id="read", command=show, screen=home)
run(start_screen=home)

Why preview= is not optional

The Previewer cannot run Java. A block without a desktop answer would work on the phone and do nothing on your desk -- the exact divergence that made 1.6.0 and 1.8.0 what they were. So preview= is required, and the build stops without it.

preview= is a promise you make. ApkPy cannot check that your Java and your Python agree; it can only make sure both exist.

Written in Python

New in 1.13.0. The block can be Python instead of Java. Name the classes at the top of the file, and write a function:

from apkpy_lib import Screen, button, label, native, run

Build = native.use("android.os.Build")
Vibrator = native.use("android.os.Vibrator")
VibrationEffect = native.use("android.os.VibrationEffect")

native.manifest(permission="android.permission.VIBRATE")


@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"


def tapped():
    status.set_value("Vibration: " + buzz(150))

There is still no Python on the phone, and nothing there looks Vibrator up by name -- that is how Pyjnius and Chaquopy do it, and it needs an interpreter. The body is read once, when the app is built: every class, field and method it names is found in android.jar, the file the app is compiled against, each line is given its Java type, and the function is written out as the Java you would have typed:

/** @native def buzz -- writehere.py:11 */
private String _apkpyNative_buzz(String apkpyArg_ms) {
    final android.content.Context context = this;
    int ms = _apkpyNativeInt(apkpyArg_ms, "ms");
    android.os.Vibrator vibrator = context.getSystemService(android.os.Vibrator.class);
    if (!vibrator.hasVibrator()) {
        return "this phone has no vibrator";
    }
    if (android.os.Build.VERSION.SDK_INT >= 26) {
        vibrator.vibrate(android.os.VibrationEffect.createOneShot(ms, android.os.VibrationEffect.DEFAULT_AMPLITUDE));
    } else {
        vibrator.vibrate(ms);
    }
    return "buzzed";
}

preview= is as required as in a Java block, and for the same reason: on the desktop the body never runs, and preview= answers.

What the build checks

Because the body is checked against the real classes, a mistake is found while the app is built -- with the line -- and not on somebody's phone:

You wrote The build says
vibrator.vibrat(200) Vibrator has no method vibrat() -- did you mean vibrate()?
vibrator.vibrate("long") vibrate() does not take (the text "long"), and the forms it does take
native.use("android.os.Vibrater") there is no such class -- did you mean android.os.Vibrator?
VibrationEffect.createOneShot(...) with no test of the version it arrived in Android 8.0 (API 26); the app also runs on Android 7.0, where the line would crash

The last one is worth a paragraph. An ApkPy app runs from Android 7.0 (API 24). A class or method newer than that is there on your phone and missing on an older one, and calling it crashes the app there. The build reads which Android each member arrived in and stops unless the line is under a test of Build.VERSION.SDK_INT:

if Build.VERSION.SDK_INT >= 26:
    vibrator.vibrate(VibrationEffect.createOneShot(ms, VibrationEffect.DEFAULT_AMPLITUDE))
else:
    vibrator.vibrate(ms)

>=, >, < and <= against a number or a Build.VERSION_CODES constant are understood, in an if, in x if ... else y, on the left of and, and as an early return. Every one of these is U2047.

What a function is given, and what it answers

Inside the body
its arguments what the annotation says: str (the default), int, float, bool, or "long" for a Java long
context the screen, as an Android Context
activity the same screen as an Activity: activity.getWindow()
the classes named with native.use() to call, to make objects of, to read constants from

Arguments reach a block as text, like every value in an ApkPy app, and are read as what the function declared: "12" and "12.0" are both the int twelve, and "abc" is an error that names the argument. The phone and the Previewer accept the same texts and refuse the same ones. A function answers text, a number or a boolean ("true"/"false" once it is back in the app), or nothing. A Java object cannot leave the function that made it: the rest of the app is not Java.

The Python a body can use

Java objects and their own methods, and around them a small Python:

  • values: text, numbers, True/False/None, f-strings;
  • if/elif/else, for ... in range(...), for item in an array or anything Iterable, while, break, continue, return;
  • try/except/finally and raise. except Exception: catches anything; name a Java exception with native.use("java.io.IOException") to catch that one. A checked exception nobody catches leaves the function, as it would leave a Python one;
  • + - * / // % **, | & ^ << >> ~ for flags, comparisons, and/or/not, in, is None, a if c else b;
  • str(), int(), float(), bool(), len(), abs(), min(), max(), round(), isinstance(), print() (to Logcat).

Three things are written the Python way and come out as Java wants them:

  • A class is made by calling it: Intent(Intent.ACTION_VIEW, uri) is new Intent(...).
  • A list goes where an array is expected: vibrator.vibrate([0, 200, 100, 200], -1).
  • / is Python's division, // and % round the way Python rounds, and == on two texts compares what they say.

And two things Python has no word for:

  • A class inside a class is reached with a dot, as Java writes it: WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, Build.VERSION.SDK_INT.
  • native.cast(Vibrator, value) says what a value is when all Java knows is that it is an Object -- what list.get(i) answers, for instance. For a system service, pass the class and it comes typed: context.getSystemService(Vibrator).

What it does not do yet

  • Listeners. An Android interface implemented in Python -- a callback, a TextToSpeech.OnInitListener -- is not translated yet. Write that block in Java with native.java_async().
  • Classes of a library added with native.gradle(). Only android.jar is read: the Android SDK and the Java it ships. For another SDK, write the block in Java.
  • The app. A body cannot call toast() or set a label: hand it what it needs as arguments and use what it returns.
  • It runs on a screen, like a Java block, so not in the body of a background_job.

lambda, comprehensions, dicts, with, classes and imports are not available inside a body; each stops the build saying so. When a block needs all of Java, native.java() is still there, and the two kinds sit side by side in one app.

An answer that arrives later

Most Android APIs call you back. native.java_async() hands your block a done, which you may call from any thread: the generated wrapper moves it to the UI thread before your Python callback runs.

buzz = native.java_async(
    "buzz",
    args=("millis",),
    imports=["android.os.Vibrator", "android.os.VibrationEffect"],
    code="""
        Vibrator vibrator = (Vibrator) context.getSystemService(
                android.content.Context.VIBRATOR_SERVICE);
        if (vibrator == null || !vibrator.hasVibrator()) {
            done.run(false, "no vibrator");
            return;
        }
        vibrator.vibrate(VibrationEffect.createOneShot(
                Long.parseLong(millis.trim()),
                VibrationEffect.DEFAULT_AMPLITUDE));
        done.run(true, "ok");
    """,
    preview=lambda millis, done: done(True, "ok"),
)


def buzzed(ok, reason):
    status.set_value("Buzzed" if ok else "No buzz: " + reason)


buzz("120", on_result=buzzed)

on_result has to be a function you defined with def: the generated Java calls it by name, so a lambda has nothing to call. It is answered (ok, value), like every other callback in ApkPy.

What a block is given, and what it is not

Inside the block
the arguments you declared in args= all String, like everything else the generator writes
context the Activity, as a Context
done java_async only: done.run(true, "value")

A block does not see the inside of the generated code. Those fields are named by the generator and change between versions; an app reaching into them would break on an upgrade with nothing to warn you.

The build around the code

An SDK is rarely just code. These three go with it:

native.gradle("implementation 'com.example:sdk:2.1.0'")
native.manifest(permission="android.permission.VIBRATE")
native.manifest(feature="android.hardware.nfc")
native.manifest(xml='<service android:name="com.example.sdk.SyncService" />')
native.keep("com.example.sdk.**")
  • gradle adds one dependency line to the generated app/build.gradle.
  • manifest(permission=) asks for it like any permission ApkPy asks for; manifest(feature=) writes <uses-feature ... required="false">, so the Play Store still shows your app to phones without that hardware; manifest(xml=) adds your own element inside <application>.
  • keep writes an R8 rule. Since 1.8.0 apkpy release shrinks the app, and a class only reached by reflection disappears without one.

What the generator writes

Each block becomes a private method of the screen's Activity:

/** native.java("batteryLevel") -- writehere.py:8 */
private String _apkpyNative_batteryLevel() {
    final android.content.Context context = this;
    BatteryManager bm = (BatteryManager)
            context.getSystemService(Context.BATTERY_SERVICE);
    return String.valueOf(bm.getIntProperty(
            BatteryManager.BATTERY_PROPERTY_CAPACITY));
}

Run apkpy build and read it. The Java inside is yours, unchanged; a mistake in it is a javac error about your own lines.

What stops the build

Every one of these is U2035, and each names the line:

  • preview= missing, or not something callable;
  • code= that is an f-string, or any argument computed while the app runs -- the build reads your module, it never runs it;
  • a native.java block with no return, or a native.java_async block that never calls done;
  • a call with the wrong number of arguments, a keyword the block does not take, or an async block used where a value is expected;
  • on_result= that is not a function you defined;
  • a block called from the body of a background_job: a block is a method of a screen's Activity, and a job runs in a Worker with no screen;
  • native.manifest(xml=) that rewrites <application>, and native.gradle() that is not a dependency line.

The boundary of the guarantee

Outside a native block, ApkPy promises the Previewer and the phone agree, and that promise is tested. Inside a native block that promise is yours: your Java and your preview= are two pieces of code that only you can keep in step.

That is the trade. It is worth making when the alternative is waiting -- and worth telling us about, because what people put in these blocks is the list of what ApkPy should translate next.