@craft-native/android 0.0.90 → 0.0.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- package {{PACKAGE_NAME}}
1
+ package com.craft.runtime
2
2
 
3
3
  import android.Manifest
4
4
  import android.app.NotificationChannel
@@ -105,7 +105,7 @@ class LocationRecordingService : Service() {
105
105
  val notification = NotificationCompat.Builder(this, CHANNEL)
106
106
  .setSmallIcon(android.R.drawable.ic_menu_mylocation)
107
107
  .setContentTitle("Recording your activity")
108
- .setContentText("WildLoop is securely saving your route")
108
+ .setContentText("${applicationInfo.loadLabel(packageManager)} is securely saving your route")
109
109
  .setOngoing(true)
110
110
  .setSilent(true)
111
111
  .build()
@@ -119,24 +119,49 @@ class LocationRecordingService : Service() {
119
119
  stopSelf()
120
120
  return START_NOT_STICKY
121
121
  }
122
- else -> startUpdates()
122
+ else -> if (!startUpdates()) {
123
+ stopSelf()
124
+ return START_NOT_STICKY
125
+ }
123
126
  }
124
- return if (CraftLocationRecordingStore.isActive(this)) START_STICKY else START_NOT_STICKY
127
+ return START_STICKY
125
128
  }
126
129
 
127
- private fun startUpdates() {
128
- if (callback != null || !CraftLocationRecordingStore.isActive(this)) return
129
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) return
130
+ private fun startUpdates(): Boolean {
131
+ if (callback != null) return true
132
+ if (!CraftLocationRecordingStore.isActive(this)) return false
133
+ val hasForegroundLocation =
134
+ ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
135
+ ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
136
+ if (!hasForegroundLocation) {
137
+ CraftLocationRecordingStore.stop(this)
138
+ return false
139
+ }
130
140
  val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5_000)
131
141
  .setMinUpdateIntervalMillis(2_000)
132
142
  .setMaxUpdateDelayMillis(10_000)
133
143
  .build()
134
- callback = object : LocationCallback() {
144
+ val nextCallback = object : LocationCallback() {
135
145
  override fun onLocationResult(result: LocationResult) {
136
146
  for (location in result.locations) CraftLocationRecordingStore.append(this@LocationRecordingService, location)
137
147
  }
138
148
  }
139
- client.requestLocationUpdates(request, callback!!, Looper.getMainLooper())
149
+ callback = nextCallback
150
+ return try {
151
+ client.requestLocationUpdates(request, nextCallback, Looper.getMainLooper())
152
+ .addOnFailureListener {
153
+ if (callback === nextCallback) {
154
+ CraftLocationRecordingStore.stop(this)
155
+ stopUpdates()
156
+ stopSelf()
157
+ }
158
+ }
159
+ true
160
+ } catch (error: Exception) {
161
+ callback = null
162
+ CraftLocationRecordingStore.stop(this)
163
+ false
164
+ }
140
165
  }
141
166
 
142
167
  private fun stopUpdates() {
@@ -3,6 +3,7 @@ package {{PACKAGE_NAME}}
3
3
  import android.annotation.SuppressLint
4
4
  import android.os.Bundle
5
5
  import android.content.Intent
6
+ import android.graphics.Bitmap
6
7
  import android.net.Uri
7
8
  import android.webkit.WebResourceRequest
8
9
  import android.webkit.WebResourceError
@@ -43,7 +44,20 @@ class MainActivity : AppCompatActivity() {
43
44
 
44
45
  webView = findViewById(R.id.webview)
45
46
  craftBridge = CraftBridge(this, webView, CraftHealthConnect(this, webView))
46
- handleIncomingDeepLink(intent, dispatch = false)
47
+ // The link that launched the app (#215). Queued until the page's bridge
48
+ // is injected, then dispatched once with `initial: true`, so a page that
49
+ // subscribes rather than calling getInitialURL still hears it. A
50
+ // relaunch from Recents carries the old intent, and was not opened by it.
51
+ // A recreated activity restores what the bridge saved: its intent may
52
+ // be a later warm link that onNewIntent set.
53
+ if (savedInstanceState == null) {
54
+ val launchLink = intent.data?.toString()
55
+ val launchedFromHistory = intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0
56
+ if (launchLink != null && !launchedFromHistory) craftBridge.receiveDeepLink(launchLink)
57
+ } else {
58
+ craftBridge.restoreDeepLinks(savedInstanceState)
59
+ }
60
+ if (savedInstanceState == null) handleIncomingShortcut(intent)
47
61
 
48
62
  setupWebView()
49
63
  loadContent()
@@ -67,6 +81,11 @@ class MainActivity : AppCompatActivity() {
67
81
  }
68
82
 
69
83
  webView.webViewClient = object : WebViewClient() {
84
+ override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
85
+ super.onPageStarted(view, url, favicon)
86
+ craftBridge.markBridgeLoading()
87
+ }
88
+
70
89
  override fun onPageFinished(view: WebView?, url: String?) {
71
90
  super.onPageFinished(view, url)
72
91
  if (url != null && isTrustedUrl(url)) craftBridge.injectBridge()
@@ -143,10 +162,8 @@ class MainActivity : AppCompatActivity() {
143
162
  }
144
163
  }
145
164
 
146
- private fun handleIncomingDeepLink(source: Intent?, dispatch: Boolean) {
147
- val url = source?.data?.toString() ?: return
148
- craftBridge.setInitialURL(url)
149
- if (dispatch) craftBridge.dispatchDeepLink(url)
165
+ private fun handleIncomingShortcut(source: Intent?) {
166
+ source?.getStringExtra(SHORTCUT_TYPE_EXTRA)?.let(craftBridge::dispatchShortcut)
150
167
  }
151
168
 
152
169
  private fun configJson(): JSONObject? {
@@ -181,16 +198,22 @@ class MainActivity : AppCompatActivity() {
181
198
 
182
199
  private fun isTrustedUrl(value: String): Boolean {
183
200
  val uri = Uri.parse(value)
184
- if (uri.scheme == "https" && uri.host == BUNDLED_APP_HOST) return true
185
201
  val origin = "${uri.scheme}://${uri.authority}"
202
+ if (origin == BUNDLED_APP_ORIGIN) return true
186
203
  val localDevelopment = uri.scheme == "http" && uri.host in setOf("localhost", "127.0.0.1", "10.0.2.2")
187
204
  return (uri.scheme == "https" || localDevelopment) && origin in trustedOrigins
188
205
  }
189
206
 
207
+ override fun onSaveInstanceState(outState: Bundle) {
208
+ super.onSaveInstanceState(outState)
209
+ craftBridge.saveDeepLinks(outState)
210
+ }
211
+
190
212
  override fun onNewIntent(intent: Intent) {
191
213
  super.onNewIntent(intent)
192
214
  setIntent(intent)
193
- handleIncomingDeepLink(intent, dispatch = true)
215
+ intent.data?.toString()?.let(craftBridge::receiveDeepLink)
216
+ handleIncomingShortcut(intent)
194
217
  }
195
218
 
196
219
  override fun onBackPressed() {
@@ -223,8 +246,19 @@ class MainActivity : AppCompatActivity() {
223
246
  super.onActivityResult(requestCode, resultCode, data)
224
247
  }
225
248
 
249
+ override fun onRequestPermissionsResult(
250
+ requestCode: Int,
251
+ permissions: Array<out String>,
252
+ grantResults: IntArray
253
+ ) {
254
+ if (craftBridge.onRequestPermissionsResult(requestCode)) return
255
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
256
+ }
257
+
226
258
  companion object {
227
259
  private const val BUNDLED_APP_HOST = "appassets.androidplatform.net"
228
- private const val BUNDLED_APP_URL = "https://appassets.androidplatform.net/"
260
+ private const val BUNDLED_APP_ORIGIN = "https://appassets.androidplatform.net"
261
+ private const val BUNDLED_APP_URL = "$BUNDLED_APP_ORIGIN/"
262
+ private const val SHORTCUT_TYPE_EXTRA = "shortcut_type"
229
263
  }
230
264
  }
@@ -6,4 +6,7 @@
6
6
  }
7
7
  -keep class {{PACKAGE_NAME}}.CraftBridge { *; }
8
8
  -keep class {{PACKAGE_NAME}}.CraftHealthConnect { *; }
9
- -keep class {{PACKAGE_NAME}}.LocationRecordingService { *; }
9
+ # libcraft.so finds this exact class and registers its private external methods
10
+ # by their source names during JNI_OnLoad.
11
+ -keep class com.craft.runtime.CraftNative { *; }
12
+ -keep class com.craft.runtime.LocationRecordingService { *; }
@@ -307,9 +307,8 @@
307
307
 
308
308
  <!-- Voice Assistant -->
309
309
  <div class="section">
310
- <h2>Siri / Google Assistant</h2>
311
- <button class="btn" onclick="testRegisterSiri()">Register Shortcut</button>
312
- <button class="btn" onclick="testRemoveSiri()">Remove Shortcut</button>
310
+ <h2>Google Assistant</h2>
311
+ <p>App Actions are declared statically in <code>shortcuts.xml</code>.</p>
313
312
  <div id="siri-result" class="result">Results will appear here...</div>
314
313
  </div>
315
314
 
@@ -766,6 +765,9 @@
766
765
 
767
766
  function testBadge() {
768
767
  if (!window.craft) return showResult('system-result', 'Craft not ready');
768
+ if (!window.craft.capabilities.appBadge) {
769
+ return showResult('system-result', 'App badges are unavailable on Android');
770
+ }
769
771
  window.craft.setBadge(5);
770
772
  setTimeout(() => window.craft.clearBadge(), 3000);
771
773
  showResult('system-result', 'Badge set to 5 (will clear in 3s)');
@@ -1255,36 +1257,11 @@
1255
1257
  }
1256
1258
  }
1257
1259
 
1258
- // Siri / Voice Assistant tests
1259
- async function testRegisterSiri() {
1260
- if (!window.craft?.siri) return showResult('siri-result', 'Siri API not available');
1261
- try {
1262
- const result = await window.craft.siri.register('Open my app', 'open_app');
1263
- showResult('siri-result', 'Siri shortcut registered: ' + JSON.stringify(result));
1264
- log('Siri shortcut registered');
1265
- } catch (e) {
1266
- showResult('siri-result', 'Error: ' + e.message);
1267
- }
1268
- }
1269
-
1270
- async function testRemoveSiri() {
1271
- if (!window.craft?.siri) return showResult('siri-result', 'Siri API not available');
1272
- try {
1273
- const result = await window.craft.siri.remove('open_app');
1274
- showResult('siri-result', 'Siri shortcut removed: ' + JSON.stringify(result));
1275
- log('Siri shortcut removed');
1276
- } catch (e) {
1277
- showResult('siri-result', 'Error: ' + e.message);
1278
- }
1279
- }
1280
-
1281
- // Listen for Siri invocations
1282
- if (window.craft?.siri) {
1283
- window.craft.siri.onInvoke((detail) => {
1284
- log('Siri shortcut invoked: ' + JSON.stringify(detail));
1285
- showResult('siri-result', 'Siri invoked: ' + JSON.stringify(detail));
1286
- });
1287
- }
1260
+ // Listen for Google Assistant invocations declared in shortcuts.xml.
1261
+ window.addEventListener('craftVoiceAction', (event) => {
1262
+ log('Voice action invoked: ' + JSON.stringify(event.detail));
1263
+ showResult('siri-result', 'Voice action invoked: ' + JSON.stringify(event.detail));
1264
+ });
1288
1265
 
1289
1266
  // Watch Connectivity tests
1290
1267
  async function testWatchReachable() {
@@ -1,383 +0,0 @@
1
- package {{PACKAGE_NAME}}
2
-
3
- import android.app.Activity
4
- import android.content.Context
5
- import androidx.compose.foundation.layout.*
6
- import androidx.compose.material3.*
7
- import androidx.compose.runtime.*
8
- import androidx.compose.ui.Modifier
9
- import androidx.work.*
10
- import com.google.android.material.color.DynamicColors
11
- import com.google.firebase.analytics.FirebaseAnalytics
12
- import com.google.firebase.crashlytics.FirebaseCrashlytics
13
- import com.google.firebase.messaging.FirebaseMessaging
14
- import com.google.firebase.remoteconfig.FirebaseRemoteConfig
15
- import java.util.concurrent.TimeUnit
16
-
17
- /// Android 13+ Advanced Features
18
-
19
- // MARK: - Jetpack Compose Integration
20
-
21
- @Composable
22
- fun CraftComposeApp(content: @Composable () -> Unit) {
23
- MaterialTheme(
24
- colorScheme = dynamicColorScheme(),
25
- typography = Typography(),
26
- content = content
27
- )
28
- }
29
-
30
- @Composable
31
- fun dynamicColorScheme(): ColorScheme {
32
- return if (DynamicColors.isDynamicColorAvailable()) {
33
- dynamicDarkColorScheme()
34
- } else {
35
- darkColorScheme()
36
- }
37
- }
38
-
39
- @Composable
40
- fun CraftWebViewComposable(
41
- url: String,
42
- modifier: Modifier = Modifier
43
- ) {
44
- AndroidView(
45
- factory = { context ->
46
- android.webkit.WebView(context).apply {
47
- settings.javaScriptEnabled = true
48
- loadUrl(url)
49
- }
50
- },
51
- modifier = modifier.fillMaxSize()
52
- )
53
- }
54
-
55
- // MARK: - Material You Dynamic Colors
56
-
57
- class DynamicColorManager(private val activity: Activity) {
58
-
59
- fun applyDynamicColors() {
60
- if (DynamicColors.isDynamicColorAvailable()) {
61
- DynamicColors.applyToActivityIfAvailable(activity)
62
- }
63
- }
64
-
65
- fun isDynamicColorAvailable(): Boolean {
66
- return DynamicColors.isDynamicColorAvailable()
67
- }
68
- }
69
-
70
- // MARK: - Predictive Back Gesture (Android 13+)
71
-
72
- class PredictiveBackHandler(private val activity: Activity) {
73
-
74
- fun enablePredictiveBack() {
75
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
76
- activity.onBackPressedDispatcher.addCallback(activity, object : androidx.activity.OnBackPressedCallback(true) {
77
- override fun handleOnBackPressed() {
78
- // Handle predictive back gesture
79
- // Can animate the exit transition
80
- isEnabled = false
81
- activity.onBackPressedDispatcher.onBackPressed()
82
- }
83
- })
84
- }
85
- }
86
- }
87
-
88
- // MARK: - Per-App Language Preferences (Android 13+)
89
-
90
- class LanguageManager(private val context: Context) {
91
-
92
- fun setAppLanguage(languageCode: String) {
93
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
94
- val localeList = android.os.LocaleList(java.util.Locale.forLanguageTag(languageCode))
95
- context.getSystemService(android.app.LocaleManager::class.java)
96
- ?.applicationLocales = localeList
97
- }
98
- }
99
-
100
- fun getAppLanguage(): String? {
101
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
102
- val locales = context.getSystemService(android.app.LocaleManager::class.java)
103
- ?.applicationLocales
104
- return locales?.get(0)?.toLanguageTag()
105
- }
106
- return null
107
- }
108
- }
109
-
110
- // MARK: - Photo Picker (Android 13+)
111
-
112
- class PhotoPickerManager(private val activity: Activity) {
113
-
114
- fun launchPhotoPicker(maxItems: Int = 1, callback: (List<android.net.Uri>) -> Unit) {
115
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
116
- val intent = android.content.Intent(android.provider.MediaStore.ACTION_PICK_IMAGES).apply {
117
- putExtra(android.provider.MediaStore.EXTRA_PICK_IMAGES_MAX, maxItems)
118
- }
119
- // Launch intent and handle result
120
- }
121
- }
122
- }
123
-
124
- // MARK: - Notification Permission (Android 13+)
125
-
126
- class NotificationPermissionManager(private val activity: Activity) {
127
-
128
- fun requestNotificationPermission(callback: (Boolean) -> Unit) {
129
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
130
- if (androidx.core.app.ActivityCompat.checkSelfPermission(
131
- activity,
132
- android.Manifest.permission.POST_NOTIFICATIONS
133
- ) != android.content.pm.PackageManager.PERMISSION_GRANTED
134
- ) {
135
- androidx.core.app.ActivityCompat.requestPermissions(
136
- activity,
137
- arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
138
- 100
139
- )
140
- } else {
141
- callback(true)
142
- }
143
- } else {
144
- callback(true)
145
- }
146
- }
147
-
148
- fun hasNotificationPermission(): Boolean {
149
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
150
- return androidx.core.app.ActivityCompat.checkSelfPermission(
151
- activity,
152
- android.Manifest.permission.POST_NOTIFICATIONS
153
- ) == android.content.pm.PackageManager.PERMISSION_GRANTED
154
- }
155
- return true
156
- }
157
- }
158
-
159
- // MARK: - Foreground Services
160
-
161
- class ForegroundServiceManager(private val context: Context) {
162
-
163
- fun startForegroundService(
164
- serviceClass: Class<*>,
165
- notificationTitle: String,
166
- notificationText: String
167
- ) {
168
- val intent = android.content.Intent(context, serviceClass)
169
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
170
- context.startForegroundService(intent)
171
- } else {
172
- context.startService(intent)
173
- }
174
- }
175
-
176
- fun stopForegroundService(serviceClass: Class<*>) {
177
- val intent = android.content.Intent(context, serviceClass)
178
- context.stopService(intent)
179
- }
180
- }
181
-
182
- // MARK: - WorkManager for Background Tasks
183
-
184
- class BackgroundTaskManager(private val context: Context) {
185
-
186
- fun scheduleOneTimeWork(
187
- tag: String,
188
- initialDelay: Long = 0,
189
- requiresCharging: Boolean = false,
190
- requiresWifi: Boolean = false
191
- ) {
192
- val constraints = Constraints.Builder()
193
- .setRequiresCharging(requiresCharging)
194
- .setRequiredNetworkType(if (requiresWifi) NetworkType.UNMETERED else NetworkType.CONNECTED)
195
- .build()
196
-
197
- val workRequest = OneTimeWorkRequestBuilder<CraftWorker>()
198
- .setConstraints(constraints)
199
- .setInitialDelay(initialDelay, TimeUnit.MILLISECONDS)
200
- .addTag(tag)
201
- .build()
202
-
203
- WorkManager.getInstance(context).enqueue(workRequest)
204
- }
205
-
206
- fun schedulePeriodicWork(
207
- tag: String,
208
- intervalMinutes: Long,
209
- requiresCharging: Boolean = false
210
- ) {
211
- val constraints = Constraints.Builder()
212
- .setRequiresCharging(requiresCharging)
213
- .build()
214
-
215
- val workRequest = PeriodicWorkRequestBuilder<CraftWorker>(
216
- intervalMinutes, TimeUnit.MINUTES
217
- )
218
- .setConstraints(constraints)
219
- .addTag(tag)
220
- .build()
221
-
222
- WorkManager.getInstance(context).enqueue(workRequest)
223
- }
224
-
225
- fun cancelWork(tag: String) {
226
- WorkManager.getInstance(context).cancelAllWorkByTag(tag)
227
- }
228
-
229
- class CraftWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
230
- override fun doWork(): Result {
231
- // Perform background task
232
- return Result.success()
233
- }
234
- }
235
- }
236
-
237
- // MARK: - Firebase Integration
238
-
239
- class FirebaseManager(private val context: Context) {
240
-
241
- private val analytics: FirebaseAnalytics by lazy {
242
- FirebaseAnalytics.getInstance(context)
243
- }
244
-
245
- private val crashlytics: FirebaseCrashlytics by lazy {
246
- FirebaseCrashlytics.getInstance()
247
- }
248
-
249
- private val remoteConfig: FirebaseRemoteConfig by lazy {
250
- FirebaseRemoteConfig.getInstance()
251
- }
252
-
253
- // Analytics
254
- fun logEvent(eventName: String, params: Map<String, Any>) {
255
- val bundle = android.os.Bundle().apply {
256
- params.forEach { (key, value) ->
257
- when (value) {
258
- is String -> putString(key, value)
259
- is Int -> putInt(key, value)
260
- is Long -> putLong(key, value)
261
- is Double -> putDouble(key, value)
262
- is Boolean -> putBoolean(key, value)
263
- }
264
- }
265
- }
266
- analytics.logEvent(eventName, bundle)
267
- }
268
-
269
- fun setUserProperty(name: String, value: String) {
270
- analytics.setUserProperty(name, value)
271
- }
272
-
273
- // Crashlytics
274
- fun logCrash(message: String) {
275
- crashlytics.log(message)
276
- }
277
-
278
- fun recordException(throwable: Throwable) {
279
- crashlytics.recordException(throwable)
280
- }
281
-
282
- fun setUserId(userId: String) {
283
- crashlytics.setUserId(userId)
284
- }
285
-
286
- // Remote Config
287
- fun fetchRemoteConfig(callback: (Boolean) -> Unit) {
288
- remoteConfig.fetchAndActivate()
289
- .addOnCompleteListener { task ->
290
- callback(task.isSuccessful)
291
- }
292
- }
293
-
294
- fun getString(key: String): String {
295
- return remoteConfig.getString(key)
296
- }
297
-
298
- fun getBoolean(key: String): Boolean {
299
- return remoteConfig.getBoolean(key)
300
- }
301
-
302
- fun getLong(key: String): Long {
303
- return remoteConfig.getLong(key)
304
- }
305
-
306
- // FCM
307
- fun getFirebaseToken(callback: (String?) -> Unit) {
308
- FirebaseMessaging.getInstance().token
309
- .addOnCompleteListener { task ->
310
- if (task.isSuccessful) {
311
- callback(task.result)
312
- } else {
313
- callback(null)
314
- }
315
- }
316
- }
317
-
318
- fun subscribeToTopic(topic: String) {
319
- FirebaseMessaging.getInstance().subscribeToTopic(topic)
320
- }
321
-
322
- fun unsubscribeFromTopic(topic: String) {
323
- FirebaseMessaging.getInstance().unsubscribeFromTopic(topic)
324
- }
325
- }
326
-
327
- // MARK: - Bottom Sheet
328
-
329
- @OptIn(ExperimentalMaterial3Api::class)
330
- @Composable
331
- fun CraftBottomSheet(
332
- sheetContent: @Composable ColumnScope.() -> Unit,
333
- content: @Composable () -> Unit
334
- ) {
335
- val sheetState = rememberModalBottomSheetState()
336
- var showBottomSheet by remember { mutableStateOf(false) }
337
-
338
- if (showBottomSheet) {
339
- ModalBottomSheet(
340
- onDismissRequest = { showBottomSheet = false },
341
- sheetState = sheetState
342
- ) {
343
- sheetContent()
344
- }
345
- }
346
-
347
- content()
348
- }
349
-
350
- // MARK: - Navigation Drawer
351
-
352
- @Composable
353
- fun CraftNavigationDrawer(
354
- drawerContent: @Composable ColumnScope.() -> Unit,
355
- content: @Composable () -> Unit
356
- ) {
357
- val drawerState = rememberDrawerState(DrawerValue.Closed)
358
- val scope = rememberCoroutineScope()
359
-
360
- ModalNavigationDrawer(
361
- drawerState = drawerState,
362
- drawerContent = {
363
- ModalDrawerSheet {
364
- drawerContent()
365
- }
366
- }
367
- ) {
368
- content()
369
- }
370
- }
371
-
372
- // MARK: - Bridge Integration
373
-
374
- fun CraftBridge.initializeAdvancedFeatures() {
375
- // Initialize all advanced Android features
376
- DynamicColorManager(activity).applyDynamicColors()
377
- PredictiveBackHandler(activity).enablePredictiveBack()
378
- }
379
-
380
- fun CraftBridge.setupFirebase() {
381
- // Setup Firebase services
382
- // Called from MainActivity onCreate
383
- }