nandi/frqpublic Fork 0
6c745df
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Ask for the camera and the microphone from the app

Both are runtime permissions, so the manifest only makes them askable and
something has to ask. Nothing did: joining a call opened the mic through
cpal inside the media plane, which is Rust with no Activity to raise a
dialog on, so the grant was never requested — AudioSystem answered
EX_SECURITY, the input stream never built, and a second later the pipeline
gave up reading a source that was never there. A second of video and then
out, with `adb shell pm grant` the only way in.

So FrqActivity asks for both at onCreate, before anything can want them.
Not at the first call: by the time the media plane finds out, the call is
already up and there is nothing left to ask on. CameraCapture keeps a
second path for the permission revoked from Settings mid-run — it asks
once and retries the start, rather than reporting denied at a permission
it could have had for the asking.

The APK gains the media plane while it is here. The flake still pinned
jolt-native v0.1.1, which predates libjoltmoq entirely, so there was
nothing to package; it is v0.1.3 now, both libraries out of the one
archive since libjoltapp links both and a mismatched pair is untested.
libc++_shared.so travels with them, because openh264 is C++ and an app's
linker namespace will not hand out the platform's own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-08-31T23:49:19-07:00 Browse files
6c745df parent: 9a5c2a8
modified android/java/uk/nandi/frq/CameraCapture.java +32 -0
@@ -63,6 +63,15 @@ public final class CameraCapture {
6363 private static long frameCount;
6464 /** Host activity — used to read live display rotation while capturing. */
6565 private static volatile Activity hostActivity;
66+
67+ /**
68+ * Whether the dialog has been raised in this process. Once, because the
69+ * ask after a refusal is one the system does not show at all — it answers
70+ * denied straight away, which here would be an invisible loop between the
71+ * check below and the retry it schedules. After a refusal the grant comes
72+ * from Settings, and the next start finds it.
73+ */
74+ private static volatile boolean permissionAsked;
6675 private static int sensorOrientationDeg = 90;
6776 private static boolean frontFacing = true;
6877
@@ -184,6 +193,29 @@ public final class CameraCapture {
184193 stopLocked();
185194 if (activity.checkSelfPermission(android.Manifest.permission.CAMERA)
186195 != PackageManager.PERMISSION_GRANTED) {
196+ // Ask, once, and come back here when the reader has answered.
197+ // The retry re-checks rather than trusting the dialog, so a
198+ // refusal lands on this same branch a second time — by then
199+ // permissionAsked is set and it reports denied instead of
200+ // asking again.
201+ if (activity instanceof FrqActivity && !permissionAsked) {
202+ permissionAsked = true;
203+ final Activity host = activity;
204+ final String requested = cameraId;
205+ ((FrqActivity) activity)
206+ .ensurePermissions(
207+ new String[] {
208+ android.Manifest.permission.CAMERA,
209+ android.Manifest.permission.RECORD_AUDIO,
210+ },
211+ new Runnable() {
212+ @Override
213+ public void run() {
214+ startOnThread(host, requested);
215+ }
216+ });
217+ return;
218+ }
187219 Log.e(TAG, "CAMERA permission not granted");
188220 onCameraState(false, "permission denied");
189221 return;
@@ -63,6 +63,15 @@ public final class CameraCapture {
63 private static long frameCount;63 private static long frameCount;
64 /** Host activity — used to read live display rotation while capturing. */64 /** Host activity — used to read live display rotation while capturing. */
65 private static volatile Activity hostActivity;65 private static volatile Activity hostActivity;
66+
67+ /**
68+ * Whether the dialog has been raised in this process. Once, because the
69+ * ask after a refusal is one the system does not show at all — it answers
70+ * denied straight away, which here would be an invisible loop between the
71+ * check below and the retry it schedules. After a refusal the grant comes
72+ * from Settings, and the next start finds it.
73+ */
74+ private static volatile boolean permissionAsked;
66 private static int sensorOrientationDeg = 90;75 private static int sensorOrientationDeg = 90;
67 private static boolean frontFacing = true;76 private static boolean frontFacing = true;
68 77
@@ -184,6 +193,29 @@ public final class CameraCapture {
184 stopLocked();193 stopLocked();
185 if (activity.checkSelfPermission(android.Manifest.permission.CAMERA)194 if (activity.checkSelfPermission(android.Manifest.permission.CAMERA)
186 != PackageManager.PERMISSION_GRANTED) {195 != PackageManager.PERMISSION_GRANTED) {
196+ // Ask, once, and come back here when the reader has answered.
197+ // The retry re-checks rather than trusting the dialog, so a
198+ // refusal lands on this same branch a second time — by then
199+ // permissionAsked is set and it reports denied instead of
200+ // asking again.
201+ if (activity instanceof FrqActivity && !permissionAsked) {
202+ permissionAsked = true;
203+ final Activity host = activity;
204+ final String requested = cameraId;
205+ ((FrqActivity) activity)
206+ .ensurePermissions(
207+ new String[] {
208+ android.Manifest.permission.CAMERA,
209+ android.Manifest.permission.RECORD_AUDIO,
210+ },
211+ new Runnable() {
212+ @Override
213+ public void run() {
214+ startOnThread(host, requested);
215+ }
216+ });
217+ return;
218+ }
187 Log.e(TAG, "CAMERA permission not granted");219 Log.e(TAG, "CAMERA permission not granted");
188 onCameraState(false, "permission denied");220 onCameraState(false, "permission denied");
189 return;221 return;
modified android/java/uk/nandi/frq/FrqActivity.java +107 -1
@@ -3,7 +3,9 @@ package uk.nandi.frq;
33 import android.app.NativeActivity;
44 import android.content.ContentResolver;
55 import android.content.Intent;
6+import android.content.pm.PackageManager;
67 import android.net.Uri;
8+import android.os.Bundle;
79 import android.os.Build;
810 import android.provider.MediaStore;
911 import android.util.Log;
@@ -12,6 +14,8 @@ import java.io.File;
1214 import java.io.FileOutputStream;
1315 import java.io.InputStream;
1416 import java.io.OutputStream;
17+import java.util.ArrayList;
18+import java.util.List;
1519
1620 /**
1721 * The only Java in the app, and it exists for one reason: a picture chooser
@@ -19,17 +23,58 @@ import java.io.OutputStream;
1923 * nowhere to deliver that. Everything else the app does — the window, the
2024 * event loop, the UI — is native, and this class does not touch any of it.
2125 *
22- * The native side reaches the two methods below over JNI, by name, on the
26+ * The native side reaches the methods below over JNI, by name, on the
2327 * activity handle the glue already holds. See vidya's `vidya_pick_image` and
2428 * `vidya_picked_image`.
29+ *
30+ * The other reason it exists is the runtime permission dialog: the manifest
31+ * can only declare CAMERA and RECORD_AUDIO, and asking for them is an
32+ * Activity call. {@link CameraCapture} asks through here rather than failing
33+ * at a permission it could have had for the asking.
2534 */
2635 public class FrqActivity extends NativeActivity {
2736 private static final String TAG = "VidyaJolt";
2837 private static final int PICK_IMAGE = 0x1CE;
38+ private static final int PERMISSIONS = 0x1CF;
39+
40+ /**
41+ * What a call needs and cannot be given by the manifest: both are runtime
42+ * permissions, so the manifest only makes them askable and this activity
43+ * is what asks. The picture chooser is deliberately not here — the picker
44+ * hands back a grant for the one file that was chosen, so it needs no
45+ * standing permission at all.
46+ */
47+ private static final String[] CALL_PERMISSIONS = {
48+ android.Manifest.permission.CAMERA,
49+ android.Manifest.permission.RECORD_AUDIO,
50+ };
2951
3052 /** Where the last pick was written, until the native side takes it. */
3153 private volatile String picked;
3254
55+ /** What to run when the dialog is answered, whichever way it is answered. */
56+ private volatile Runnable afterPermissions;
57+
58+ /**
59+ * Asked for at startup rather than at the first call, because the call is
60+ * not where they can be asked from: the microphone is opened by cpal
61+ * inside the media plane, which is Rust with no Activity to raise a dialog
62+ * on, and by the time it fails the call is already up. A denied mic there
63+ * is not a prompt but a dead pipeline — the audio stream never builds, and
64+ * a second later the pipeline gives up reading a source that was never
65+ * there.
66+ *
67+ * So both are asked for once, here, before anything can want them. The
68+ * answer is not waited for: the dialog is its own window and the activity
69+ * carries on behind it, exactly as it would if the permissions were
70+ * already held.
71+ */
72+ @Override
73+ protected void onCreate(Bundle state) {
74+ super.onCreate(state);
75+ ensurePermissions(CALL_PERMISSIONS, null);
76+ }
77+
3378 /**
3479 * Open the system photo picker. Called from the UI thread or off it, so it
3580 * hops to the right one itself.
@@ -76,6 +121,67 @@ public class FrqActivity extends NativeActivity {
76121 return path;
77122 }
78123
124+ /**
125+ * Ask for the camera and the microphone, and say nothing about the answer.
126+ *
127+ * Here for the native side, which reaches it over JNI by name the same way
128+ * it reaches {@link #pickImage} — a call that is about to open the mic can
129+ * raise the dialog before it does. Granting is not waited for: the dialog
130+ * is a separate window and the caller carries on without it.
131+ */
132+ public void requestCallPermissions() {
133+ ensurePermissions(CALL_PERMISSIONS, null);
134+ }
135+
136+ /**
137+ * Ask for whichever of {@code perms} is not held yet, then run {@code after}
138+ * on the UI thread — after the reader answers, or immediately if there was
139+ * nothing to ask. {@code after} is told nothing: what it does next is a
140+ * fresh {@code checkSelfPermission}, because a dialog can be dismissed and
141+ * a permission can be revoked from Settings while the app is running.
142+ *
143+ * One dialog at a time. A second ask while the first is still up would
144+ * lose the first one's callback, so it is refused and answers late — when
145+ * the outstanding one comes back.
146+ */
147+ public void ensurePermissions(final String[] perms, final Runnable after) {
148+ runOnUiThread(new Runnable() {
149+ @Override
150+ public void run() {
151+ List<String> missing = new ArrayList<>();
152+ for (String p : perms) {
153+ if (checkSelfPermission(p) != PackageManager.PERMISSION_GRANTED) {
154+ missing.add(p);
155+ }
156+ }
157+ if (missing.isEmpty() || afterPermissions != null) {
158+ if (!missing.isEmpty()) {
159+ Log.w(TAG, "a permission dialog is already up; not asking again");
160+ }
161+ if (after != null) {
162+ after.run();
163+ }
164+ return;
165+ }
166+ afterPermissions = after;
167+ requestPermissions(missing.toArray(new String[0]), PERMISSIONS);
168+ }
169+ });
170+ }
171+
172+ @Override
173+ public void onRequestPermissionsResult(int request, String[] perms, int[] results) {
174+ super.onRequestPermissionsResult(request, perms, results);
175+ if (request != PERMISSIONS) {
176+ return;
177+ }
178+ Runnable after = afterPermissions;
179+ afterPermissions = null;
180+ if (after != null) {
181+ after.run();
182+ }
183+ }
184+
79185 @Override
80186 protected void onActivityResult(int request, int result, Intent data) {
81187 super.onActivityResult(request, result, data);
@@ -3,7 +3,9 @@ package uk.nandi.frq;
3 import android.app.NativeActivity;3 import android.app.NativeActivity;
4 import android.content.ContentResolver;4 import android.content.ContentResolver;
5 import android.content.Intent;5 import android.content.Intent;
6+import android.content.pm.PackageManager;
6 import android.net.Uri;7 import android.net.Uri;
8+import android.os.Bundle;
7 import android.os.Build;9 import android.os.Build;
8 import android.provider.MediaStore;10 import android.provider.MediaStore;
9 import android.util.Log;11 import android.util.Log;
@@ -12,6 +14,8 @@ import java.io.File;
12 import java.io.FileOutputStream;14 import java.io.FileOutputStream;
13 import java.io.InputStream;15 import java.io.InputStream;
14 import java.io.OutputStream;16 import java.io.OutputStream;
17+import java.util.ArrayList;
18+import java.util.List;
15 19
16 /**20 /**
17 * The only Java in the app, and it exists for one reason: a picture chooser21 * The only Java in the app, and it exists for one reason: a picture chooser
@@ -19,17 +23,58 @@ import java.io.OutputStream;
19 * nowhere to deliver that. Everything else the app does — the window, the23 * nowhere to deliver that. Everything else the app does — the window, the
20 * event loop, the UI — is native, and this class does not touch any of it.24 * event loop, the UI — is native, and this class does not touch any of it.
21 *25 *
22- * The native side reaches the two methods below over JNI, by name, on the26+ * The native side reaches the methods below over JNI, by name, on the
23 * activity handle the glue already holds. See vidya's `vidya_pick_image` and27 * activity handle the glue already holds. See vidya's `vidya_pick_image` and
24 * `vidya_picked_image`.28 * `vidya_picked_image`.
29+ *
30+ * The other reason it exists is the runtime permission dialog: the manifest
31+ * can only declare CAMERA and RECORD_AUDIO, and asking for them is an
32+ * Activity call. {@link CameraCapture} asks through here rather than failing
33+ * at a permission it could have had for the asking.
25 */34 */
26 public class FrqActivity extends NativeActivity {35 public class FrqActivity extends NativeActivity {
27 private static final String TAG = "VidyaJolt";36 private static final String TAG = "VidyaJolt";
28 private static final int PICK_IMAGE = 0x1CE;37 private static final int PICK_IMAGE = 0x1CE;
38+ private static final int PERMISSIONS = 0x1CF;
39+
40+ /**
41+ * What a call needs and cannot be given by the manifest: both are runtime
42+ * permissions, so the manifest only makes them askable and this activity
43+ * is what asks. The picture chooser is deliberately not here — the picker
44+ * hands back a grant for the one file that was chosen, so it needs no
45+ * standing permission at all.
46+ */
47+ private static final String[] CALL_PERMISSIONS = {
48+ android.Manifest.permission.CAMERA,
49+ android.Manifest.permission.RECORD_AUDIO,
50+ };
29 51
30 /** Where the last pick was written, until the native side takes it. */52 /** Where the last pick was written, until the native side takes it. */
31 private volatile String picked;53 private volatile String picked;
32 54
55+ /** What to run when the dialog is answered, whichever way it is answered. */
56+ private volatile Runnable afterPermissions;
57+
58+ /**
59+ * Asked for at startup rather than at the first call, because the call is
60+ * not where they can be asked from: the microphone is opened by cpal
61+ * inside the media plane, which is Rust with no Activity to raise a dialog
62+ * on, and by the time it fails the call is already up. A denied mic there
63+ * is not a prompt but a dead pipeline — the audio stream never builds, and
64+ * a second later the pipeline gives up reading a source that was never
65+ * there.
66+ *
67+ * So both are asked for once, here, before anything can want them. The
68+ * answer is not waited for: the dialog is its own window and the activity
69+ * carries on behind it, exactly as it would if the permissions were
70+ * already held.
71+ */
72+ @Override
73+ protected void onCreate(Bundle state) {
74+ super.onCreate(state);
75+ ensurePermissions(CALL_PERMISSIONS, null);
76+ }
77+
33 /**78 /**
34 * Open the system photo picker. Called from the UI thread or off it, so it79 * Open the system photo picker. Called from the UI thread or off it, so it
35 * hops to the right one itself.80 * hops to the right one itself.
@@ -76,6 +121,67 @@ public class FrqActivity extends NativeActivity {
76 return path;121 return path;
77 }122 }
78 123
124+ /**
125+ * Ask for the camera and the microphone, and say nothing about the answer.
126+ *
127+ * Here for the native side, which reaches it over JNI by name the same way
128+ * it reaches {@link #pickImage} — a call that is about to open the mic can
129+ * raise the dialog before it does. Granting is not waited for: the dialog
130+ * is a separate window and the caller carries on without it.
131+ */
132+ public void requestCallPermissions() {
133+ ensurePermissions(CALL_PERMISSIONS, null);
134+ }
135+
136+ /**
137+ * Ask for whichever of {@code perms} is not held yet, then run {@code after}
138+ * on the UI thread — after the reader answers, or immediately if there was
139+ * nothing to ask. {@code after} is told nothing: what it does next is a
140+ * fresh {@code checkSelfPermission}, because a dialog can be dismissed and
141+ * a permission can be revoked from Settings while the app is running.
142+ *
143+ * One dialog at a time. A second ask while the first is still up would
144+ * lose the first one's callback, so it is refused and answers late — when
145+ * the outstanding one comes back.
146+ */
147+ public void ensurePermissions(final String[] perms, final Runnable after) {
148+ runOnUiThread(new Runnable() {
149+ @Override
150+ public void run() {
151+ List<String> missing = new ArrayList<>();
152+ for (String p : perms) {
153+ if (checkSelfPermission(p) != PackageManager.PERMISSION_GRANTED) {
154+ missing.add(p);
155+ }
156+ }
157+ if (missing.isEmpty() || afterPermissions != null) {
158+ if (!missing.isEmpty()) {
159+ Log.w(TAG, "a permission dialog is already up; not asking again");
160+ }
161+ if (after != null) {
162+ after.run();
163+ }
164+ return;
165+ }
166+ afterPermissions = after;
167+ requestPermissions(missing.toArray(new String[0]), PERMISSIONS);
168+ }
169+ });
170+ }
171+
172+ @Override
173+ public void onRequestPermissionsResult(int request, String[] perms, int[] results) {
174+ super.onRequestPermissionsResult(request, perms, results);
175+ if (request != PERMISSIONS) {
176+ return;
177+ }
178+ Runnable after = afterPermissions;
179+ afterPermissions = null;
180+ if (after != null) {
181+ after.run();
182+ }
183+ }
184+
79 @Override185 @Override
80 protected void onActivityResult(int request, int result, Intent data) {186 protected void onActivityResult(int request, int result, Intent data) {
81 super.onActivityResult(request, result, data);187 super.onActivityResult(request, result, data);
modified nix/android.nix +34 -17
@@ -1,6 +1,6 @@
11 # The APK, as derivations rather than as a buck2 graph.
22 #
3-# android/BUCK builds the same five things, and does it better for a person at
3+# android/BUCK builds the same things, and does it better for a person at
44 # a terminal: it is incremental, and it lets a sibling jolt-native checkout win
55 # over the pinned release so that editing the Rust rebuilds the APK. Nothing
66 # here replaces that. What this adds is the other build — from nothing, on a
@@ -45,24 +45,28 @@ let
4545 ndkBin = "${ndkRoot}/toolchains/llvm/prebuilt/linux-x86_64/bin";
4646 cc = "${ndkBin}/aarch64-linux-android${apiLevel}-clang";
4747
48- # The two halves that come out of jolt-native's releases, by the digests
49- # scripts/*.dotslash pins. Same bytes buck fetches; DotSlash's `digest` is
50- # over the archive, which is what fetchurl hashes too.
51- libvidya = pkgs.fetchurl {
52- url = "https://gitlab.com/-/project/85910092/uploads/a7264f20582e6d42d45b17fe626ba471/jolt-native-android-arm64-v0.1.1.tar.gz";
53- sha256 = "bb85c57ea263b9bfe113927c6f6b0c8c6414b97056bcb0281b8444187616a233";
48+ # What comes out of jolt-native's releases, by the digests scripts/*.dotslash
49+ # pin. Same bytes buck fetches; DotSlash's `digest` is over the archive,
50+ # which is what fetchurl hashes too.
51+ #
52+ # One archive, two libraries: libvidya (the retained-tree UI) and libjoltmoq
53+ # (the AV media plane). They are built together and only make sense together
54+ # — libjoltapp links both — so there is one pin for the pair rather than two
55+ # that could drift apart.
56+ nativeRelease = pkgs.fetchurl {
57+ url = "https://gitlab.com/nandithebull/jolt-native/-/releases/v0.1.3/downloads/jolt-native-android-arm64-v0.1.3.tar.gz";
58+ sha256 = "4519745bae6db9a791a71b38a26478879e246166802b0a48dbf7d45e4d45a108";
5459 };
5560
5661 glue = pkgs.fetchurl {
57- url = "https://gitlab.com/-/project/85910092/uploads/16b1a3ea32dac6737fc21aec701d7b0c/jolt-native-android-glue-v0.1.1.tar.gz";
58- sha256 = "7f4c179d72a3660ce8e80c3cf52a788ea33d7ef97d6f624310e61e7f4f98851b";
62+ url = "https://gitlab.com/nandithebull/jolt-native/-/releases/v0.1.3/downloads/jolt-native-android-glue-v0.1.3.tar.gz";
63+ sha256 = "83313eda124f2a0cfff6827cf4473600c1f71db2f208d654b97068a85af38da5";
5964 };
6065
61- # Unpacked once, so the three consumers below name files rather than repeat
62- # the tar.
63- vidyaLib = pkgs.runCommand "libvidya-android" { } ''
66+ # Unpacked once, so the consumers below name files rather than repeat the tar.
67+ nativeLibs = pkgs.runCommand "jolt-native-android" { } ''
6468 mkdir -p "$out"
65- tar -xzf ${libvidya} -C "$out"
69+ tar -xzf ${nativeRelease} -C "$out"
6670 '';
6771
6872 glueSrc = pkgs.runCommand "jolt-android-glue" { } ''
@@ -70,6 +74,15 @@ let
7074 tar -xzf ${glue} -C "$out" --strip-components=1
7175 '';
7276
77+ # The C++ runtime, out of the same NDK the glue is compiled with.
78+ #
79+ # openh264 is C++, and its build script asks to be linked against
80+ # `libc++_shared.so` by name so libjoltmoq carries that as a DT_NEEDED. An
81+ # app's linker namespace will not hand out the platform's own copy (there is
82+ # no stable one to hand out), so the APK carries it, exactly as it carries
83+ # OpenSSL below and for the same reason.
84+ libcxx = "${ndkRoot}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so";
85+
7386 # --- Chez's arm64 cross target ------------------------------------------
7487 # The one piece with no nixpkgs equivalent: `pkgs.chez` builds a Scheme for
7588 # this machine, and what the boot image needs is Chez's `tarm64le` workarea
@@ -232,7 +245,8 @@ let
232245 # crash on the phone.
233246 libjoltapp = pkgs.runCommand "libjoltapp.so" { } ''
234247 mkdir -p lib
235- cp ${vidyaLib}/libvidya.so lib/libvidya.so
248+ cp ${nativeLibs}/libvidya.so lib/libvidya.so
249+ cp ${nativeLibs}/libjoltmoq.so lib/libjoltmoq.so
236250
237251 ${cc} -shared -fPIC -O2 -o "$out" \
238252 ${glueSrc}/android/jolt_main.c \
@@ -242,7 +256,7 @@ let
242256 -Llib \
243257 ${chezAndroid}/tarm64le/boot/tarm64le/libkernel.a \
244258 ${chezAndroid}/lz4/lib/liblz4.a \
245- -lvidya -landroid -llog -lz -ldl -lm -Wl,--no-undefined
259+ -lvidya -ljoltmoq -landroid -llog -lz -ldl -lm -Wl,--no-undefined
246260 '';
247261
248262 # --- the Java half --------------------------------------------------------
@@ -311,7 +325,9 @@ let
311325 nativeBuildInputs = [ pkgs.zip ];
312326 } ''
313327 mkdir -p stage/lib/${abi}
314- cp ${vidyaLib}/libvidya.so stage/lib/${abi}/libvidya.so
328+ cp ${nativeLibs}/libvidya.so stage/lib/${abi}/libvidya.so
329+ cp ${nativeLibs}/libjoltmoq.so stage/lib/${abi}/libjoltmoq.so
330+ cp ${libcxx} stage/lib/${abi}/libc++_shared.so
315331 cp ${libjoltapp} stage/lib/${abi}/libjoltapp.so
316332 cp ${opensslAndroid.out}/lib/libssl.so stage/lib/${abi}/libssl.so
317333 cp ${opensslAndroid.out}/lib/libcrypto.so stage/lib/${abi}/libcrypto.so
@@ -324,7 +340,8 @@ let
324340 --version-code 1 --version-name ${version}
325341
326342 ( cd stage && \
327- zip -q -0 "$out" lib/${abi}/libvidya.so lib/${abi}/libjoltapp.so \
343+ zip -q -0 "$out" lib/${abi}/libvidya.so lib/${abi}/libjoltmoq.so \
344+ lib/${abi}/libc++_shared.so lib/${abi}/libjoltapp.so \
328345 lib/${abi}/libssl.so lib/${abi}/libcrypto.so && \
329346 zip -q "$out" classes.dex )
330347 '';
@@ -1,6 +1,6 @@
1 # The APK, as derivations rather than as a buck2 graph.1 # The APK, as derivations rather than as a buck2 graph.
2 #2 #
3-# android/BUCK builds the same five things, and does it better for a person at3+# android/BUCK builds the same things, and does it better for a person at
4 # a terminal: it is incremental, and it lets a sibling jolt-native checkout win4 # a terminal: it is incremental, and it lets a sibling jolt-native checkout win
5 # over the pinned release so that editing the Rust rebuilds the APK. Nothing5 # over the pinned release so that editing the Rust rebuilds the APK. Nothing
6 # here replaces that. What this adds is the other build — from nothing, on a6 # here replaces that. What this adds is the other build — from nothing, on a
@@ -45,24 +45,28 @@ let
45 ndkBin = "${ndkRoot}/toolchains/llvm/prebuilt/linux-x86_64/bin";45 ndkBin = "${ndkRoot}/toolchains/llvm/prebuilt/linux-x86_64/bin";
46 cc = "${ndkBin}/aarch64-linux-android${apiLevel}-clang";46 cc = "${ndkBin}/aarch64-linux-android${apiLevel}-clang";
47 47
48- # The two halves that come out of jolt-native's releases, by the digests48+ # What comes out of jolt-native's releases, by the digests scripts/*.dotslash
49- # scripts/*.dotslash pins. Same bytes buck fetches; DotSlash's `digest` is49+ # pin. Same bytes buck fetches; DotSlash's `digest` is over the archive,
50- # over the archive, which is what fetchurl hashes too.50+ # which is what fetchurl hashes too.
51- libvidya = pkgs.fetchurl {51+ #
52- url = "https://gitlab.com/-/project/85910092/uploads/a7264f20582e6d42d45b17fe626ba471/jolt-native-android-arm64-v0.1.1.tar.gz";52+ # One archive, two libraries: libvidya (the retained-tree UI) and libjoltmoq
53- sha256 = "bb85c57ea263b9bfe113927c6f6b0c8c6414b97056bcb0281b8444187616a233";53+ # (the AV media plane). They are built together and only make sense together
54+ # — libjoltapp links both — so there is one pin for the pair rather than two
55+ # that could drift apart.
56+ nativeRelease = pkgs.fetchurl {
57+ url = "https://gitlab.com/nandithebull/jolt-native/-/releases/v0.1.3/downloads/jolt-native-android-arm64-v0.1.3.tar.gz";
58+ sha256 = "4519745bae6db9a791a71b38a26478879e246166802b0a48dbf7d45e4d45a108";
54 };59 };
55 60
56 glue = pkgs.fetchurl {61 glue = pkgs.fetchurl {
57- url = "https://gitlab.com/-/project/85910092/uploads/16b1a3ea32dac6737fc21aec701d7b0c/jolt-native-android-glue-v0.1.1.tar.gz";62+ url = "https://gitlab.com/nandithebull/jolt-native/-/releases/v0.1.3/downloads/jolt-native-android-glue-v0.1.3.tar.gz";
58- sha256 = "7f4c179d72a3660ce8e80c3cf52a788ea33d7ef97d6f624310e61e7f4f98851b";63+ sha256 = "83313eda124f2a0cfff6827cf4473600c1f71db2f208d654b97068a85af38da5";
59 };64 };
60 65
61- # Unpacked once, so the three consumers below name files rather than repeat66+ # Unpacked once, so the consumers below name files rather than repeat the tar.
62- # the tar.67+ nativeLibs = pkgs.runCommand "jolt-native-android" { } ''
63- vidyaLib = pkgs.runCommand "libvidya-android" { } ''
64 mkdir -p "$out"68 mkdir -p "$out"
65- tar -xzf ${libvidya} -C "$out"69+ tar -xzf ${nativeRelease} -C "$out"
66 '';70 '';
67 71
68 glueSrc = pkgs.runCommand "jolt-android-glue" { } ''72 glueSrc = pkgs.runCommand "jolt-android-glue" { } ''
@@ -70,6 +74,15 @@ let
70 tar -xzf ${glue} -C "$out" --strip-components=174 tar -xzf ${glue} -C "$out" --strip-components=1
71 '';75 '';
72 76
77+ # The C++ runtime, out of the same NDK the glue is compiled with.
78+ #
79+ # openh264 is C++, and its build script asks to be linked against
80+ # `libc++_shared.so` by name so libjoltmoq carries that as a DT_NEEDED. An
81+ # app's linker namespace will not hand out the platform's own copy (there is
82+ # no stable one to hand out), so the APK carries it, exactly as it carries
83+ # OpenSSL below and for the same reason.
84+ libcxx = "${ndkRoot}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so";
85+
73 # --- Chez's arm64 cross target ------------------------------------------86 # --- Chez's arm64 cross target ------------------------------------------
74 # The one piece with no nixpkgs equivalent: `pkgs.chez` builds a Scheme for87 # The one piece with no nixpkgs equivalent: `pkgs.chez` builds a Scheme for
75 # this machine, and what the boot image needs is Chez's `tarm64le` workarea 88 # this machine, and what the boot image needs is Chez's `tarm64le` workarea
@@ -232,7 +245,8 @@ let
232 # crash on the phone.245 # crash on the phone.
233 libjoltapp = pkgs.runCommand "libjoltapp.so" { } ''246 libjoltapp = pkgs.runCommand "libjoltapp.so" { } ''
234 mkdir -p lib247 mkdir -p lib
235- cp ${vidyaLib}/libvidya.so lib/libvidya.so248+ cp ${nativeLibs}/libvidya.so lib/libvidya.so
249+ cp ${nativeLibs}/libjoltmoq.so lib/libjoltmoq.so
236 250
237 ${cc} -shared -fPIC -O2 -o "$out" \251 ${cc} -shared -fPIC -O2 -o "$out" \
238 ${glueSrc}/android/jolt_main.c \252 ${glueSrc}/android/jolt_main.c \
@@ -242,7 +256,7 @@ let
242 -Llib \256 -Llib \
243 ${chezAndroid}/tarm64le/boot/tarm64le/libkernel.a \257 ${chezAndroid}/tarm64le/boot/tarm64le/libkernel.a \
244 ${chezAndroid}/lz4/lib/liblz4.a \258 ${chezAndroid}/lz4/lib/liblz4.a \
245- -lvidya -landroid -llog -lz -ldl -lm -Wl,--no-undefined259+ -lvidya -ljoltmoq -landroid -llog -lz -ldl -lm -Wl,--no-undefined
246 '';260 '';
247 261
248 # --- the Java half --------------------------------------------------------262 # --- the Java half --------------------------------------------------------
@@ -311,7 +325,9 @@ let
311 nativeBuildInputs = [ pkgs.zip ];325 nativeBuildInputs = [ pkgs.zip ];
312 } ''326 } ''
313 mkdir -p stage/lib/${abi}327 mkdir -p stage/lib/${abi}
314- cp ${vidyaLib}/libvidya.so stage/lib/${abi}/libvidya.so328+ cp ${nativeLibs}/libvidya.so stage/lib/${abi}/libvidya.so
329+ cp ${nativeLibs}/libjoltmoq.so stage/lib/${abi}/libjoltmoq.so
330+ cp ${libcxx} stage/lib/${abi}/libc++_shared.so
315 cp ${libjoltapp} stage/lib/${abi}/libjoltapp.so331 cp ${libjoltapp} stage/lib/${abi}/libjoltapp.so
316 cp ${opensslAndroid.out}/lib/libssl.so stage/lib/${abi}/libssl.so332 cp ${opensslAndroid.out}/lib/libssl.so stage/lib/${abi}/libssl.so
317 cp ${opensslAndroid.out}/lib/libcrypto.so stage/lib/${abi}/libcrypto.so333 cp ${opensslAndroid.out}/lib/libcrypto.so stage/lib/${abi}/libcrypto.so
@@ -324,7 +340,8 @@ let
324 --version-code 1 --version-name ${version}340 --version-code 1 --version-name ${version}
325 341
326 ( cd stage && \342 ( cd stage && \
327- zip -q -0 "$out" lib/${abi}/libvidya.so lib/${abi}/libjoltapp.so \343+ zip -q -0 "$out" lib/${abi}/libvidya.so lib/${abi}/libjoltmoq.so \
344+ lib/${abi}/libc++_shared.so lib/${abi}/libjoltapp.so \
328 lib/${abi}/libssl.so lib/${abi}/libcrypto.so && \345 lib/${abi}/libssl.so lib/${abi}/libcrypto.so && \
329 zip -q "$out" classes.dex )346 zip -q "$out" classes.dex )
330 '';347 '';