Using Permissions
A shared Android permission and media-capture helper that other native modules call from Kotlin. There is no JavaScript API to import.
Basic usage
@sigx/lynx-permissions ships only Android Kotlin source. Its JavaScript entry exports nothing:
import * as Permissions from '@sigx/lynx-permissions';
// Permissions === {} — there is no runtime JS API here.
If you are building an app, you almost never touch this module directly. Reach for a higher-level module that depends on it instead, and call its JS API:
- Camera —
requestPermission(), capture photos. - Image picker — system photo picker.
- Location — location permission and updates.
This module is for native-module authors writing Kotlin that needs Android runtime permissions, the system photo picker, the camera, or the Storage Access Framework. The rest of this page is Kotlin.
This is an Android-only package. signalx-module.json declares platforms: ["android"]; iOS needs no equivalent because the OS pickers (UIImagePickerController, PHPickerViewController, CLLocationManager) drive their own permission flows.
Checking and requesting a permission
PermissionHelper is a Kotlin object (singleton) in package com.sigx.permissions. Use a sigx permission key — "camera", "microphone", "location", "photo_library", "notifications", "bluetooth", and so on — and the helper maps it to the right Android Manifest string(s) with API-level branching.
import android.content.Context
import com.sigx.permissions.PermissionHelper
fun ensureCamera(context: Context, onReady: () -> Unit) {
// Check without prompting first.
val status = PermissionHelper.checkPermission(context, "camera")
when (status.getString("status")) {
"granted" -> onReady()
"blocked" -> PermissionHelper.openSettings(context) // user must grant manually
else -> {
// "undetermined" or "denied" with canAskAgain — prompt.
PermissionHelper.requestPermission(context, "camera") { result ->
if (result.getString("status") == "granted") {
onReady()
} else if (!result.getBoolean("canAskAgain")) {
PermissionHelper.openSettings(context)
}
}
}
}
}
Both checkPermission and requestPermission return a JavaOnlyMap shaped like { status, canAskAgain }, where status is one of "granted" | "denied" | "blocked" | "undetermined". checkPermission uses a SharedPreferences store ("sigx_permissions") to tell "undetermined" (never asked) apart from "blocked".
requestPermission short-circuits to granted when no runtime permission is required (or it is already granted). If no foreground Activity is available it resolves to denied with canAskAgain = true. The foreground Activity is read from @sigx/lynx-core's shared SigxActivityHolder.current() — this package does not hold its own Activity for permission requests.
Activity wiring (auto-linked)
The Activity-result delivery is wired automatically through PermissionsActivityHook, declared in signalx-module.json under android.activityHook. The auto-linked Activity forwards two lifecycle callbacks:
onCreate→MediaCapture.register(activity)— this must run before the Activity reaches theSTARTEDstate, becauseregisterForActivityResultthrows otherwise.onRequestPermissionsResult→PermissionHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)— this resolves the pending callback registered byrequestPermission.
If you wire an Activity by hand instead of relying on the auto-linker, forward both callbacks the same way:
import androidx.activity.ComponentActivity
import com.sigx.permissions.MediaCapture
import com.sigx.permissions.PermissionHelper
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: android.os.Bundle?) {
super.onCreate(savedInstanceState)
MediaCapture.register(this) // before STARTED
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray,
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
PermissionHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onDestroy() {
MediaCapture.unregister()
super.onDestroy()
}
}
Capturing and picking media
MediaCapture is a Kotlin object that owns the Android Activity-Result launchers, so a single Activity hook covers the camera, photo-picker, and file-picker modules. Each operation takes a callback that receives a JavaOnlyMap result.
import android.content.Context
import com.sigx.permissions.MediaCapture
// Camera — requires CAMERA permission AND a FileProvider with authority
// "${packageName}.fileprovider". Captured photos land at
// cacheDir/sigx-camera-<timestamp>.jpg as content:// URIs.
fun capture(context: Context) {
MediaCapture.takePicture(context) { result ->
if (result.getBoolean("cancelled")) {
// result.getString("error") is set on the error path.
return@takePicture
}
val uri = result.getString("uri") // content://...
}
}
// System photo picker (Android 13+) — needs NO storage/media permission.
fun pickPhotos() {
MediaCapture.pickImages(maxItems = 5) { result ->
if (result.getBoolean("cancelled")) return@pickImages
val assets = result.getArray("assets") // [{ uri, type: "image" }, ...]
}
}
// Storage Access Framework single-file picker — no runtime permission;
// SAF grants per-pick read access. Empty types defaults to "*/*".
fun pickPdf() {
MediaCapture.pickFile(arrayOf("application/pdf")) { result ->
if (result.getBoolean("cancelled")) return@pickFile
val assets = result.getArray("assets") // [{ uri }]
}
}
Use takePicture / pickImage / pickImages / pickFile / pickFiles depending on the need. Picker results carry an assets array; the camera result carries a single uri. On any error path the result has cancelled: true and an error string.
Concurrency
Only one pending operation is allowed per launcher kind (takePicture, pickImage, pickImages, pickFile, pickFiles). A re-entrant call pre-empts the prior pending callback, invoking it with error = "cancelled by new <op>". Calling unregister() resolves every pending callback with a cancelled / "activity destroyed" result, so JS Promises waiting on these never hang. Activity-result callbacks run on the main thread.
Notes
pickImagesis backed by a launcher registered once with a hard cap of 50 items (MULTI_PICK_HARD_CAP). A smaller per-callmaxItemsis enforced by trimming the returned URI list, not by re-registering.pickFileshas no cap.takePicturereturns an error result if the${packageName}.fileproviderauthority is missing. The MainActivity AndroidManifest template seeds it for you.requestPermissionuses an incrementing request code starting at 1000.
See also
- API reference — every exported helper and result shape.
- Overview — what this module is and how it links.
