@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.
- package/README.md +117 -93
- package/package.json +4 -2
- package/templates/CraftBridge.kt.template +2167 -1177
- package/templates/CraftHealthConnect.kt.template +45 -6
- package/templates/CraftHealthConnectStub.kt.template +7 -1
- package/templates/CraftNative.kt.template +2250 -0
- package/templates/LocationRecordingService.kt.template +34 -9
- package/templates/MainActivity.kt.template +42 -8
- package/templates/proguard-rules.pro.template +4 -1
- package/templates/test-bridges.html +10 -33
- package/templates/CraftBridgeExtensions.kt.template +0 -383
- package/templates/CraftWidgetProvider.kt.template +0 -246
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
package
|
|
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("
|
|
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
|
|
127
|
+
return START_STICKY
|
|
125
128
|
}
|
|
126
129
|
|
|
127
|
-
private fun startUpdates() {
|
|
128
|
-
if (callback != null
|
|
129
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
147
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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>
|
|
311
|
-
<
|
|
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
|
-
//
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
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
|
-
}
|