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 inan array or anything Iterable,while,break,continue,return;try/except/finallyandraise.except Exception:catches anything; name a Java exception withnative.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)isnew 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 anObject-- whatlist.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 withnative.java_async(). - Classes of a library added with
native.gradle(). Onlyandroid.jaris 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.**")
gradleadds one dependency line to the generatedapp/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>.keepwrites an R8 rule. Since 1.8.0apkpy releaseshrinks 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.javablock with noreturn, or anative.java_asyncblock that never callsdone; - 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>, andnative.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.