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>
6c745df parent: 9a5c2a8 modified
android/java/uk/nandi/frq/CameraCapture.java +32 -0 | @@ -63,6 +63,15 @@ public final class CameraCapture { | ||
| 63 | 63 | private static long frameCount; |
| 64 | 64 | /** Host activity — used to read live display rotation while capturing. */ |
| 65 | 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 | 75 | private static int sensorOrientationDeg = 90; |
| 67 | 76 | private static boolean frontFacing = true; |
| 68 | 77 | |
| @@ -184,6 +193,29 @@ public final class CameraCapture { | ||
| 184 | 193 | stopLocked(); |
| 185 | 194 | if (activity.checkSelfPermission(android.Manifest.permission.CAMERA) |
| 186 | 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 | 219 | Log.e(TAG, "CAMERA permission not granted"); |
| 188 | 220 | onCameraState(false, "permission denied"); |
| 189 | 221 | 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; | ||
| 3 | 3 | import android.app.NativeActivity; |
| 4 | 4 | import android.content.ContentResolver; |
| 5 | 5 | import android.content.Intent; |
| 6 | +import android.content.pm.PackageManager; | |
| 6 | 7 | import android.net.Uri; |
| 8 | +import android.os.Bundle; | |
| 7 | 9 | import android.os.Build; |
| 8 | 10 | import android.provider.MediaStore; |
| 9 | 11 | import android.util.Log; |
| @@ -12,6 +14,8 @@ import java.io.File; | ||
| 12 | 14 | import java.io.FileOutputStream; |
| 13 | 15 | import java.io.InputStream; |
| 14 | 16 | import java.io.OutputStream; |
| 17 | +import java.util.ArrayList; | |
| 18 | +import java.util.List; | |
| 15 | 19 | |
| 16 | 20 | /** |
| 17 | 21 | * The only Java in the app, and it exists for one reason: a picture chooser |
| @@ -19,17 +23,58 @@ import java.io.OutputStream; | ||
| 19 | 23 | * nowhere to deliver that. Everything else the app does — the window, the |
| 20 | 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 the | |
| 26 | + * The native side reaches the methods below over JNI, by name, on the | |
| 23 | 27 | * activity handle the glue already holds. See vidya's `vidya_pick_image` and |
| 24 | 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 | 35 | public class FrqActivity extends NativeActivity { |
| 27 | 36 | private static final String TAG = "VidyaJolt"; |
| 28 | 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 | 52 | /** Where the last pick was written, until the native side takes it. */ |
| 31 | 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 | 79 | * Open the system photo picker. Called from the UI thread or off it, so it |
| 35 | 80 | * hops to the right one itself. |
| @@ -76,6 +121,67 @@ public class FrqActivity extends NativeActivity { | ||
| 76 | 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 | 185 | @Override |
| 80 | 186 | protected void onActivityResult(int request, int result, Intent data) { |
| 81 | 187 | 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 chooser | 21 | * 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, the | 23 | * 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 the | 26 | + * 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` and | 27 | * 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 it | 79 | * 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 | @Override | 185 | @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 @@ | ||
| 1 | 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 at | |
| 3 | +# android/BUCK builds the same things, and does it better for a person at | |
| 4 | 4 | # a terminal: it is incremental, and it lets a sibling jolt-native checkout win |
| 5 | 5 | # over the pinned release so that editing the Rust rebuilds the APK. Nothing |
| 6 | 6 | # here replaces that. What this adds is the other build — from nothing, on a |
| @@ -45,24 +45,28 @@ let | ||
| 45 | 45 | ndkBin = "${ndkRoot}/toolchains/llvm/prebuilt/linux-x86_64/bin"; |
| 46 | 46 | cc = "${ndkBin}/aarch64-linux-android${apiLevel}-clang"; |
| 47 | 47 | |
| 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"; | |
| 54 | 59 | }; |
| 55 | 60 | |
| 56 | 61 | 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"; | |
| 59 | 64 | }; |
| 60 | 65 | |
| 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" { } '' | |
| 64 | 68 | mkdir -p "$out" |
| 65 | - tar -xzf ${libvidya} -C "$out" | |
| 69 | + tar -xzf ${nativeRelease} -C "$out" | |
| 66 | 70 | ''; |
| 67 | 71 | |
| 68 | 72 | glueSrc = pkgs.runCommand "jolt-android-glue" { } '' |
| @@ -70,6 +74,15 @@ let | ||
| 70 | 74 | 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 | 86 | # --- Chez's arm64 cross target ------------------------------------------ |
| 74 | 87 | # The one piece with no nixpkgs equivalent: `pkgs.chez` builds a Scheme for |
| 75 | 88 | # this machine, and what the boot image needs is Chez's `tarm64le` workarea — |
| @@ -232,7 +245,8 @@ let | ||
| 232 | 245 | # crash on the phone. |
| 233 | 246 | libjoltapp = pkgs.runCommand "libjoltapp.so" { } '' |
| 234 | 247 | 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 | |
| 236 | 250 | |
| 237 | 251 | ${cc} -shared -fPIC -O2 -o "$out" \ |
| 238 | 252 | ${glueSrc}/android/jolt_main.c \ |
| @@ -242,7 +256,7 @@ let | ||
| 242 | 256 | -Llib \ |
| 243 | 257 | ${chezAndroid}/tarm64le/boot/tarm64le/libkernel.a \ |
| 244 | 258 | ${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 | |
| 246 | 260 | ''; |
| 247 | 261 | |
| 248 | 262 | # --- the Java half -------------------------------------------------------- |
| @@ -311,7 +325,9 @@ let | ||
| 311 | 325 | nativeBuildInputs = [ pkgs.zip ]; |
| 312 | 326 | } '' |
| 313 | 327 | 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 | |
| 315 | 331 | cp ${libjoltapp} stage/lib/${abi}/libjoltapp.so |
| 316 | 332 | cp ${opensslAndroid.out}/lib/libssl.so stage/lib/${abi}/libssl.so |
| 317 | 333 | cp ${opensslAndroid.out}/lib/libcrypto.so stage/lib/${abi}/libcrypto.so |
| @@ -324,7 +340,8 @@ let | ||
| 324 | 340 | --version-code 1 --version-name ${version} |
| 325 | 341 | |
| 326 | 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 | 345 | lib/${abi}/libssl.so lib/${abi}/libcrypto.so && \ |
| 329 | 346 | zip -q "$out" classes.dex ) |
| 330 | 347 | ''; |
| @@ -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 at | 3 | +# 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 win | 4 | # 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. Nothing | 5 | # 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 a | 6 | # 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 digests | 48 | + # What comes out of jolt-native's releases, by the digests scripts/*.dotslash |
| 49 | - # scripts/*.dotslash pins. Same bytes buck fetches; DotSlash's `digest` is | 49 | + # 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 repeat | 66 | + # 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=1 | 74 | 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 for | 87 | # 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 lib | 247 | 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 | ||
| 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-undefined | 259 | + -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.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 | ||
| 315 | cp ${libjoltapp} stage/lib/${abi}/libjoltapp.so | 331 | cp ${libjoltapp} stage/lib/${abi}/libjoltapp.so |
| 316 | cp ${opensslAndroid.out}/lib/libssl.so stage/lib/${abi}/libssl.so | 332 | cp ${opensslAndroid.out}/lib/libssl.so stage/lib/${abi}/libssl.so |
| 317 | cp ${opensslAndroid.out}/lib/libcrypto.so stage/lib/${abi}/libcrypto.so | 333 | 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 | ''; |