Packages
mob_new
0.3.6
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.45
0.1.44
0.1.43
0.1.42
0.1.40
0.1.33
0.1.30
0.1.29
0.1.28
0.1.27
0.1.26
0.1.25
0.1.24
0.1.23
0.1.21
0.1.20
0.1.19
0.1.18
0.1.17
0.1.16
0.1.15
0.1.14
0.1.12
0.1.11
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Project generator for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
priv/templates/mob.new/android/app/src/main/java/MainActivity.kt.eex
package <%= java_package %>
import android.app.Activity
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.core.content.FileProvider
import java.io.File
class MainActivity : ComponentActivity() {
companion object {
private const val TAG = "<%= display_name %>"
init { System.loadLibrary("<%= app_name %>") }
}
external fun nativeSetActivity(activity: Activity)
external fun nativeStartBeam()
// ── Camera launchers ──────────────────────────────────────────────────
private var cameraPhotoUri: Uri? = null
private val cameraPhotoLauncher =
registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
MobBridge.handleCameraPhotoResult(if (success) cameraPhotoUri else null)
}
private val cameraVideoLauncher =
registerForActivityResult(ActivityResultContracts.CaptureVideo()) { success ->
MobBridge.handleCameraVideoResult(if (success) cameraPhotoUri else null)
}
fun launchCameraPhoto() {
val file = File(cacheDir, "mob_cam_${System.currentTimeMillis()}.jpg")
cameraPhotoUri = FileProvider.getUriForFile(this, "$packageName.fileprovider", file)
cameraPhotoLauncher.launch(cameraPhotoUri!!)
}
fun launchCameraVideo() {
val file = File(cacheDir, "mob_cam_${System.currentTimeMillis()}.mp4")
cameraPhotoUri = FileProvider.getUriForFile(this, "$packageName.fileprovider", file)
cameraVideoLauncher.launch(cameraPhotoUri!!)
}
// ── Photo picker launcher ─────────────────────────────────────────────
private val photosPickerLauncher =
registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { uris ->
MobBridge.handlePhotosResult(uris)
}
fun launchPhotosPicker(max: Int) {
photosPickerLauncher.launch(
androidx.activity.result.PickVisualMediaRequest(
androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageAndVideo
)
)
}
// ── File picker launcher ──────────────────────────────────────────────
private val filePickerLauncher =
registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris ->
MobBridge.handleFilesResult(uris)
}
fun launchFilePicker() {
filePickerLauncher.launch(arrayOf("*/*"))
}
// ── QR scanner launcher ───────────────────────────────────────────────
// For QR scanning we use an intent to a helper activity (MobScannerActivity)
// that uses CameraX + ML Kit. It returns the scanned value as a result string.
private val scannerLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val value = result.data?.getStringExtra("scan_value")
val type = result.data?.getStringExtra("scan_type") ?: "qr"
MobBridge.handleScanResult(value, type)
}
fun launchQrScanner() {
val intent = android.content.Intent(this, MobScannerActivity::class.java)
scannerLauncher.launch(intent)
}
// ── Permission result ─────────────────────────────────────────────────
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 9001) {
val granted = grantResults.isNotEmpty() &&
grantResults.all { it == PackageManager.PERMISSION_GRANTED }
MobBridge.onPermissionResult(granted)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
// Edge-to-edge: lets the content draw behind the (transparent) status
// and navigation bars instead of being letterboxed by opaque system
// bars. Must be called BEFORE super.onCreate() per AndroidX docs.
enableEdgeToEdge()
super.onCreate(savedInstanceState)
MobBridge.init(this)
// Forward launcher-supplied env vars into the BEAM process. Set BEFORE
// nativeStartBeam below so the BEAM (and Mob.Dist in particular) sees
// them when it reads getenv()/System.get_env/1.
//
// mob_node_suffix — appended to the configured node name
// (`<app>_android` → `<app>_android_<suffix>`).
// Lets multiple Android phones running the same
// app coexist in Mac's shared EPMD.
// mob_dist_port — Erlang dist listen port (default 9100).
intent?.extras?.getString("mob_node_suffix")?.takeIf { it.isNotEmpty() }?.let { suffix ->
android.system.Os.setenv("MOB_NODE_SUFFIX", suffix, true)
Log.i(TAG, "onCreate: MOB_NODE_SUFFIX=$suffix")
}
intent?.extras?.getInt("mob_dist_port", -1)?.takeIf { it > 0 }?.let { port ->
android.system.Os.setenv("MOB_DIST_PORT", port.toString(), true)
Log.i(TAG, "onCreate: MOB_DIST_PORT=$port")
}
// Check if launched from a notification tap
intent?.extras?.getString("mob_notification_json")?.let { json ->
MobBridge.setLaunchNotification(json)
}
setContent {
val state by MobBridge.rootState
BackHandler(enabled = state.node != null) { MobBridge.nativeHandleBack() }
AnimatedContent(
targetState = state,
contentKey = { it.navKey },
transitionSpec = {
when (targetState.transition) {
"push" ->
slideInHorizontally(animationSpec = tween(300)) { it } togetherWith
slideOutHorizontally(animationSpec = tween(300)) { -it / 3 }
"pop" ->
slideInHorizontally(animationSpec = tween(300)) { -it / 3 } togetherWith
slideOutHorizontally(animationSpec = tween(300)) { it }
"reset" ->
fadeIn(animationSpec = tween(250)) togetherWith
fadeOut(animationSpec = tween(250))
else ->
EnterTransition.None togetherWith ExitTransition.None
}
},
label = "nav"
) { s ->
s.node?.let { RenderNode(it, modifier = Modifier.fillMaxSize().safeDrawingPadding()) }
}
}
// If the project ships embedded Python (mix mob.enable pythonx), the
// APK contains assets/python/{stdlib,lib-dynload}/. Extract those to
// filesDir on first launch and tell the BEAM where they landed via
// env vars consumed by <App>.PythonPaths. Idempotent — re-launches
// skip extraction once the marker file is present.
extractPythonAssetsIfNeeded()
Log.i(TAG, "onCreate — handing off to BEAM")
nativeSetActivity(this)
Thread({ nativeStartBeam() }, "beam-main").start()
}
private fun extractPythonAssetsIfNeeded() {
val pythonRoot = File(filesDir, "python")
val marker = File(pythonRoot, ".extracted")
// libpython3.13.so is auto-extracted by the APK installer to the
// app's nativeLibraryDir — point Pythonx.init/4 at it.
val libPython = File(applicationInfo.nativeLibraryDir, "libpython3.13.so")
if (libPython.exists()) {
android.system.Os.setenv("MOB_PYTHON_DL", libPython.absolutePath, true)
}
// Skip extraction if no assets ship Python (project doesn't use Pythonx)
// or if extraction has already happened.
val assetList = try { assets.list("python") ?: emptyArray() } catch (_: Throwable) { emptyArray() }
if (assetList.isEmpty() || marker.exists()) {
if (pythonRoot.exists()) {
android.system.Os.setenv("MOB_PYTHON_HOME", pythonRoot.absolutePath, true)
}
return
}
Log.i(TAG, "extractPythonAssets: extracting assets/python → ${pythonRoot.absolutePath}")
copyAssetTree("python", pythonRoot)
flattenLibDynload(pythonRoot)
marker.createNewFile()
android.system.Os.setenv("MOB_PYTHON_HOME", pythonRoot.absolutePath, true)
Log.i(TAG, "extractPythonAssets: done")
}
// Chaquopy ships lib-dynload/<abi>/*.so per architecture; CPython expects
// them flat in lib-dynload/. Move the device's primary-abi `.so` files
// up one level and discard the rest.
private fun flattenLibDynload(pythonRoot: File) {
val libDynload = File(pythonRoot, "lib/python3.13/lib-dynload")
if (!libDynload.isDirectory) return
val deviceAbi = android.os.Build.SUPPORTED_ABIS.firstOrNull() ?: return
val abiDir = File(libDynload, deviceAbi)
if (!abiDir.isDirectory) return
abiDir.listFiles()?.forEach { src ->
val dst = File(libDynload, src.name)
if (!dst.exists()) src.renameTo(dst)
}
// Drop the other-abi dirs to free space.
libDynload.listFiles { f -> f.isDirectory }?.forEach { it.deleteRecursively() }
}
private fun copyAssetTree(srcPath: String, destDir: File) {
val children = assets.list(srcPath) ?: emptyArray()
if (children.isEmpty()) {
// Leaf: copy the file
destDir.parentFile?.mkdirs()
assets.open(srcPath).use { input ->
destDir.outputStream().use { output -> input.copyTo(output) }
}
return
}
destDir.mkdirs()
for (child in children) {
copyAssetTree("$srcPath/$child", File(destDir, child))
}
}
// Called when a notification is tapped and the activity already exists at
// the top of the stack (singleTop launch mode). If BEAM is running, deliver
// the notification directly; otherwise store it for delivery on boot.
override fun onNewIntent(intent: android.content.Intent) {
super.onNewIntent(intent)
setIntent(intent)
intent.extras?.getString("mob_notification_json")?.let { json ->
val pid = MobBridge.notifyPid
if (pid != 0L) {
MobBridge.nativeDeliverNotification(pid, json)
} else {
MobBridge.setLaunchNotification(json)
}
}
}
// Manifest declares `android:configChanges` including `uiMode`, so a
// dark/light toggle delivers here instead of recreating the activity.
// Forward to MobBridge so Mob.Device :appearance subscribers can react
// (e.g. re-resolve Mob.Theme.Adaptive without an app restart).
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val nightMode = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK
val scheme = if (nightMode == Configuration.UI_MODE_NIGHT_YES) "dark" else "light"
MobBridge.notifyColorSchemeChanged(scheme)
}
}