|
| 1 | +package bluetooth.explorer |
| 2 | + |
| 3 | +import skip.lib.* |
| 4 | +import skip.model.* |
| 5 | +import skip.foundation.* |
| 6 | +import skip.ui.* |
| 7 | + |
| 8 | +import android.Manifest |
| 9 | +import android.app.Application |
| 10 | +import android.graphics.Color as AndroidColor |
| 11 | +import androidx.activity.compose.setContent |
| 12 | +import androidx.activity.enableEdgeToEdge |
| 13 | +import androidx.activity.SystemBarStyle |
| 14 | +import androidx.activity.ComponentActivity |
| 15 | +import androidx.appcompat.app.AppCompatActivity |
| 16 | +import androidx.compose.foundation.isSystemInDarkTheme |
| 17 | +import androidx.compose.foundation.layout.fillMaxSize |
| 18 | +import androidx.compose.foundation.layout.Box |
| 19 | +import androidx.compose.runtime.Composable |
| 20 | +import androidx.compose.runtime.DisposableEffect |
| 21 | +import androidx.compose.runtime.SideEffect |
| 22 | +import androidx.compose.runtime.saveable.rememberSaveableStateHolder |
| 23 | +import androidx.compose.ui.Alignment |
| 24 | +import androidx.compose.ui.Modifier |
| 25 | +import androidx.compose.ui.graphics.luminance |
| 26 | +import androidx.compose.ui.platform.LocalContext |
| 27 | +import androidx.compose.material3.MaterialTheme |
| 28 | +import androidx.core.app.ActivityCompat |
| 29 | + |
| 30 | +internal val logger: SkipLogger = SkipLogger(subsystem = "bluetooth.explorer", category = "BluetoothExplorer") |
| 31 | + |
| 32 | +private typealias AppRootView = BluetoothExplorerRootView |
| 33 | +private typealias AppDelegate = BluetoothExplorerAppDelegate |
| 34 | + |
| 35 | +/// AndroidAppMain is the `android.app.Application` entry point, and must match `application android:name` in the AndroidMainfest.xml file. |
| 36 | +open class AndroidAppMain: Application { |
| 37 | + constructor() { |
| 38 | + } |
| 39 | + |
| 40 | + override fun onCreate() { |
| 41 | + super.onCreate() |
| 42 | + logger.info("starting app") |
| 43 | + try { |
| 44 | + // Ensure SwiftJNI JNI_OnLoad runs before Skip bridge bootstrap converts Java strings. |
| 45 | + java.lang.System.loadLibrary("SwiftJava") |
| 46 | + java.lang.System.loadLibrary("SkipBridge") |
| 47 | + java.lang.System.loadLibrary("SwiftJNI") |
| 48 | + java.lang.System.loadLibrary("BluetoothExplorer") |
| 49 | + } catch (error: Throwable) { |
| 50 | + logger.warning("SwiftJNI load skipped or failed: ${error.message ?: error::class.java.name}") |
| 51 | + } |
| 52 | + ProcessInfo.launch(applicationContext) |
| 53 | + AppDelegate.shared.onInit() |
| 54 | + } |
| 55 | + |
| 56 | + companion object { |
| 57 | + } |
| 58 | +} |
| 59 | + |
| 60 | +/// AndroidAppMain is initial `androidx.appcompat.app.AppCompatActivity`, and must match `activity android:name` in the AndroidMainfest.xml file. |
| 61 | +open class MainActivity: AppCompatActivity { |
| 62 | + constructor() { |
| 63 | + } |
| 64 | + |
| 65 | + override fun onCreate(savedInstanceState: android.os.Bundle?) { |
| 66 | + super.onCreate(savedInstanceState) |
| 67 | + logger.info("starting activity") |
| 68 | + UIApplication.launch(this) |
| 69 | + enableEdgeToEdge() |
| 70 | + |
| 71 | + setContent { |
| 72 | + val saveableStateHolder = rememberSaveableStateHolder() |
| 73 | + saveableStateHolder.SaveableStateProvider(true) { |
| 74 | + PresentationRootView(ComposeContext()) |
| 75 | + SideEffect { saveableStateHolder.removeState(true) } |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + AppDelegate.shared.onLaunch() |
| 80 | + |
| 81 | + // Request permissions on startup. |
| 82 | + val permissions = listOf( |
| 83 | + Manifest.permission.BLUETOOTH_SCAN, |
| 84 | + Manifest.permission.BLUETOOTH_CONNECT, |
| 85 | + Manifest.permission.BLUETOOTH_ADVERTISE, |
| 86 | + Manifest.permission.INTERNET |
| 87 | + ) |
| 88 | + val requestTag = 1 |
| 89 | + ActivityCompat.requestPermissions(this, permissions.toTypedArray(), requestTag) |
| 90 | + } |
| 91 | + |
| 92 | + override fun onStart() { |
| 93 | + logger.info("onStart") |
| 94 | + super.onStart() |
| 95 | + } |
| 96 | + |
| 97 | + override fun onResume() { |
| 98 | + super.onResume() |
| 99 | + AppDelegate.shared.onResume() |
| 100 | + } |
| 101 | + |
| 102 | + override fun onPause() { |
| 103 | + super.onPause() |
| 104 | + AppDelegate.shared.onPause() |
| 105 | + } |
| 106 | + |
| 107 | + override fun onStop() { |
| 108 | + super.onStop() |
| 109 | + AppDelegate.shared.onStop() |
| 110 | + } |
| 111 | + |
| 112 | + override fun onDestroy() { |
| 113 | + super.onDestroy() |
| 114 | + AppDelegate.shared.onDestroy() |
| 115 | + } |
| 116 | + |
| 117 | + override fun onLowMemory() { |
| 118 | + super.onLowMemory() |
| 119 | + AppDelegate.shared.onLowMemory() |
| 120 | + } |
| 121 | + |
| 122 | + override fun onRestart() { |
| 123 | + logger.info("onRestart") |
| 124 | + super.onRestart() |
| 125 | + } |
| 126 | + |
| 127 | + override fun onSaveInstanceState(outState: android.os.Bundle): Unit = super.onSaveInstanceState(outState) |
| 128 | + |
| 129 | + override fun onRestoreInstanceState(bundle: android.os.Bundle) { |
| 130 | + // Usually you restore your state in onCreate(). It is possible to restore it in onRestoreInstanceState() as well, but not very common. (onRestoreInstanceState() is called after onStart(), whereas onCreate() is called before onStart(). |
| 131 | + logger.info("onRestoreInstanceState") |
| 132 | + super.onRestoreInstanceState(bundle) |
| 133 | + } |
| 134 | + |
| 135 | + override fun onRequestPermissionsResult(requestCode: Int, permissions: kotlin.Array<String>, grantResults: IntArray) { |
| 136 | + super.onRequestPermissionsResult(requestCode, permissions, grantResults) |
| 137 | + logger.info("onRequestPermissionsResult: ${requestCode}") |
| 138 | + } |
| 139 | + |
| 140 | + companion object { |
| 141 | + } |
| 142 | +} |
| 143 | + |
| 144 | +@Composable |
| 145 | +internal fun SyncSystemBarsWithTheme() { |
| 146 | + val dark = MaterialTheme.colorScheme.background.luminance() < 0.5f |
| 147 | + |
| 148 | + val transparent = AndroidColor.TRANSPARENT |
| 149 | + val style = if (dark) { |
| 150 | + SystemBarStyle.dark(transparent) |
| 151 | + } else { |
| 152 | + SystemBarStyle.light(transparent, transparent) |
| 153 | + } |
| 154 | + |
| 155 | + val activity = LocalContext.current as? ComponentActivity |
| 156 | + DisposableEffect(style) { |
| 157 | + activity?.enableEdgeToEdge( |
| 158 | + statusBarStyle = style, |
| 159 | + navigationBarStyle = style |
| 160 | + ) |
| 161 | + onDispose { } |
| 162 | + } |
| 163 | +} |
| 164 | + |
| 165 | +@Composable |
| 166 | +internal fun PresentationRootView(context: ComposeContext) { |
| 167 | + val colorScheme = if (isSystemInDarkTheme()) ColorScheme.dark else ColorScheme.light |
| 168 | + PresentationRoot(defaultColorScheme = colorScheme, context = context) { ctx -> |
| 169 | + SyncSystemBarsWithTheme() |
| 170 | + val contentContext = ctx.content() |
| 171 | + Box(modifier = ctx.modifier.fillMaxSize(), contentAlignment = Alignment.Center) { |
| 172 | + AppRootView().Compose(context = contentContext) |
| 173 | + } |
| 174 | + } |
| 175 | +} |
0 commit comments