@craft-native/android 0.0.72

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.
@@ -0,0 +1,246 @@
1
+ package {{PACKAGE_NAME}}
2
+
3
+ import android.app.PendingIntent
4
+ import android.appwidget.AppWidgetManager
5
+ import android.appwidget.AppWidgetProvider
6
+ import android.content.BroadcastReceiver
7
+ import android.content.ComponentName
8
+ import android.content.Context
9
+ import android.content.Intent
10
+ import android.content.IntentFilter
11
+ import android.os.Build
12
+ import android.widget.RemoteViews
13
+
14
+ /**
15
+ * Craft Widget Provider
16
+ *
17
+ * A customizable home screen widget that displays data from your Craft app.
18
+ * The widget can show title, subtitle, value, and an icon.
19
+ *
20
+ * To use widgets:
21
+ * 1. Add this provider to your AndroidManifest.xml
22
+ * 2. Create widget layout in res/layout/craft_widget.xml
23
+ * 3. Create widget info in res/xml/craft_widget_info.xml
24
+ * 4. Call window.craft.widget.update() from your web app
25
+ */
26
+ class CraftWidgetProvider : AppWidgetProvider() {
27
+
28
+ companion object {
29
+ const val ACTION_WIDGET_UPDATE = "{{PACKAGE_NAME}}.WIDGET_UPDATE"
30
+ const val PREFS_NAME = "craft_widget_prefs"
31
+ const val PREF_TITLE = "widget_title"
32
+ const val PREF_SUBTITLE = "widget_subtitle"
33
+ const val PREF_VALUE = "widget_value"
34
+ const val PREF_ICON = "widget_icon"
35
+
36
+ /**
37
+ * Update all widgets programmatically
38
+ */
39
+ fun updateAllWidgets(context: Context) {
40
+ val intent = Intent(context, CraftWidgetProvider::class.java).apply {
41
+ action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
42
+ val appWidgetManager = AppWidgetManager.getInstance(context)
43
+ val widgetComponent = ComponentName(context, CraftWidgetProvider::class.java)
44
+ val appWidgetIds = appWidgetManager.getAppWidgetIds(widgetComponent)
45
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)
46
+ }
47
+ context.sendBroadcast(intent)
48
+ }
49
+ }
50
+
51
+ override fun onUpdate(
52
+ context: Context,
53
+ appWidgetManager: AppWidgetManager,
54
+ appWidgetIds: IntArray
55
+ ) {
56
+ // Update each widget instance
57
+ for (appWidgetId in appWidgetIds) {
58
+ updateAppWidget(context, appWidgetManager, appWidgetId)
59
+ }
60
+ }
61
+
62
+ override fun onReceive(context: Context, intent: Intent) {
63
+ super.onReceive(context, intent)
64
+
65
+ // Handle custom update action from the app
66
+ if (intent.action == ACTION_WIDGET_UPDATE) {
67
+ val appWidgetManager = AppWidgetManager.getInstance(context)
68
+ val widgetComponent = ComponentName(context, CraftWidgetProvider::class.java)
69
+ val appWidgetIds = appWidgetManager.getAppWidgetIds(widgetComponent)
70
+
71
+ for (appWidgetId in appWidgetIds) {
72
+ updateAppWidget(context, appWidgetManager, appWidgetId)
73
+ }
74
+ }
75
+ }
76
+
77
+ override fun onEnabled(context: Context) {
78
+ // Register broadcast receiver for widget updates
79
+ val filter = IntentFilter(ACTION_WIDGET_UPDATE)
80
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
81
+ context.registerReceiver(widgetUpdateReceiver, filter, Context.RECEIVER_EXPORTED)
82
+ } else {
83
+ context.registerReceiver(widgetUpdateReceiver, filter)
84
+ }
85
+ }
86
+
87
+ override fun onDisabled(context: Context) {
88
+ // Unregister broadcast receiver
89
+ try {
90
+ context.unregisterReceiver(widgetUpdateReceiver)
91
+ } catch (_: IllegalArgumentException) {
92
+ // Receiver not registered
93
+ }
94
+ }
95
+
96
+ private val widgetUpdateReceiver = object : BroadcastReceiver() {
97
+ override fun onReceive(context: Context, intent: Intent) {
98
+ if (intent.action == ACTION_WIDGET_UPDATE) {
99
+ updateAllWidgets(context)
100
+ }
101
+ }
102
+ }
103
+
104
+ private fun updateAppWidget(
105
+ context: Context,
106
+ appWidgetManager: AppWidgetManager,
107
+ appWidgetId: Int
108
+ ) {
109
+ // Get data from shared preferences
110
+ val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
111
+ val title = prefs.getString(PREF_TITLE, "Craft Widget") ?: "Craft Widget"
112
+ val subtitle = prefs.getString(PREF_SUBTITLE, "") ?: ""
113
+ val value = prefs.getString(PREF_VALUE, "") ?: ""
114
+ val iconName = prefs.getString(PREF_ICON, null)
115
+
116
+ // Create remote views
117
+ val views = RemoteViews(context.packageName, R.layout.craft_widget)
118
+
119
+ // Set text values
120
+ views.setTextViewText(R.id.widget_title, title)
121
+ views.setTextViewText(R.id.widget_subtitle, subtitle)
122
+ views.setTextViewText(R.id.widget_value, value)
123
+
124
+ // Set visibility for subtitle
125
+ views.setViewVisibility(
126
+ R.id.widget_subtitle,
127
+ if (subtitle.isEmpty()) android.view.View.GONE else android.view.View.VISIBLE
128
+ )
129
+
130
+ // Set visibility for value
131
+ views.setViewVisibility(
132
+ R.id.widget_value,
133
+ if (value.isEmpty()) android.view.View.GONE else android.view.View.VISIBLE
134
+ )
135
+
136
+ // Set icon if provided
137
+ if (iconName != null) {
138
+ val iconResId = context.resources.getIdentifier(
139
+ iconName,
140
+ "drawable",
141
+ context.packageName
142
+ )
143
+ if (iconResId != 0) {
144
+ views.setImageViewResource(R.id.widget_icon, iconResId)
145
+ views.setViewVisibility(R.id.widget_icon, android.view.View.VISIBLE)
146
+ } else {
147
+ views.setViewVisibility(R.id.widget_icon, android.view.View.GONE)
148
+ }
149
+ } else {
150
+ views.setViewVisibility(R.id.widget_icon, android.view.View.GONE)
151
+ }
152
+
153
+ // Create click intent to launch main app
154
+ val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
155
+ if (launchIntent != null) {
156
+ val pendingIntent = PendingIntent.getActivity(
157
+ context,
158
+ 0,
159
+ launchIntent,
160
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
161
+ )
162
+ views.setOnClickPendingIntent(R.id.widget_container, pendingIntent)
163
+ }
164
+
165
+ // Update the widget
166
+ appWidgetManager.updateAppWidget(appWidgetId, views)
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Widget Layout (res/layout/craft_widget.xml):
172
+ *
173
+ * <?xml version="1.0" encoding="utf-8"?>
174
+ * <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
175
+ * android:id="@+id/widget_container"
176
+ * android:layout_width="match_parent"
177
+ * android:layout_height="match_parent"
178
+ * android:orientation="vertical"
179
+ * android:padding="16dp"
180
+ * android:background="@drawable/widget_background">
181
+ *
182
+ * <ImageView
183
+ * android:id="@+id/widget_icon"
184
+ * android:layout_width="32dp"
185
+ * android:layout_height="32dp"
186
+ * android:visibility="gone" />
187
+ *
188
+ * <TextView
189
+ * android:id="@+id/widget_title"
190
+ * android:layout_width="wrap_content"
191
+ * android:layout_height="wrap_content"
192
+ * android:textSize="16sp"
193
+ * android:textStyle="bold"
194
+ * android:textColor="@color/widget_text_primary" />
195
+ *
196
+ * <TextView
197
+ * android:id="@+id/widget_subtitle"
198
+ * android:layout_width="wrap_content"
199
+ * android:layout_height="wrap_content"
200
+ * android:textSize="12sp"
201
+ * android:textColor="@color/widget_text_secondary" />
202
+ *
203
+ * <TextView
204
+ * android:id="@+id/widget_value"
205
+ * android:layout_width="wrap_content"
206
+ * android:layout_height="wrap_content"
207
+ * android:textSize="32sp"
208
+ * android:textStyle="bold"
209
+ * android:textColor="@color/widget_accent"
210
+ * android:layout_marginTop="8dp" />
211
+ *
212
+ * </LinearLayout>
213
+ */
214
+
215
+ /**
216
+ * Widget Info (res/xml/craft_widget_info.xml):
217
+ *
218
+ * <?xml version="1.0" encoding="utf-8"?>
219
+ * <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
220
+ * android:minWidth="110dp"
221
+ * android:minHeight="110dp"
222
+ * android:targetCellWidth="2"
223
+ * android:targetCellHeight="2"
224
+ * android:updatePeriodMillis="1800000"
225
+ * android:initialLayout="@layout/craft_widget"
226
+ * android:resizeMode="horizontal|vertical"
227
+ * android:widgetCategory="home_screen"
228
+ * android:previewImage="@drawable/widget_preview"
229
+ * android:description="@string/widget_description" />
230
+ */
231
+
232
+ /**
233
+ * AndroidManifest.xml entries needed:
234
+ *
235
+ * <receiver
236
+ * android:name=".CraftWidgetProvider"
237
+ * android:exported="true">
238
+ * <intent-filter>
239
+ * <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
240
+ * <action android:name="{{PACKAGE_NAME}}.WIDGET_UPDATE" />
241
+ * </intent-filter>
242
+ * <meta-data
243
+ * android:name="android.appwidget.provider"
244
+ * android:resource="@xml/craft_widget_info" />
245
+ * </receiver>
246
+ */
@@ -0,0 +1,160 @@
1
+ package {{PACKAGE_NAME}}
2
+
3
+ import android.Manifest
4
+ import android.app.NotificationChannel
5
+ import android.app.NotificationManager
6
+ import android.app.Service
7
+ import android.content.Context
8
+ import android.content.Intent
9
+ import android.content.pm.PackageManager
10
+ import android.os.Build
11
+ import android.os.IBinder
12
+ import android.os.Looper
13
+ import androidx.core.app.NotificationCompat
14
+ import androidx.core.content.ContextCompat
15
+ import com.google.android.gms.location.LocationCallback
16
+ import com.google.android.gms.location.LocationRequest
17
+ import com.google.android.gms.location.LocationResult
18
+ import com.google.android.gms.location.LocationServices
19
+ import com.google.android.gms.location.Priority
20
+ import org.json.JSONArray
21
+ import org.json.JSONObject
22
+ import java.io.File
23
+
24
+ object CraftLocationRecordingStore {
25
+ private const val PREFS = "craft_location_recording"
26
+ private const val FILE = "craft-location-recording.jsonl"
27
+
28
+ private fun prefs(context: Context) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
29
+ private fun file(context: Context) = File(context.filesDir, FILE)
30
+
31
+ @Synchronized
32
+ fun start(context: Context, id: String, startedAt: Long) {
33
+ file(context).writeText("")
34
+ prefs(context).edit()
35
+ .putString("id", id)
36
+ .putBoolean("active", true)
37
+ .putBoolean("paused", false)
38
+ .putLong("startedAt", startedAt)
39
+ .apply()
40
+ }
41
+
42
+ @Synchronized
43
+ fun setPaused(context: Context, paused: Boolean) {
44
+ prefs(context).edit().putBoolean("paused", paused).apply()
45
+ }
46
+
47
+ @Synchronized
48
+ fun stop(context: Context) {
49
+ prefs(context).edit().putBoolean("active", false).putBoolean("paused", false).apply()
50
+ }
51
+
52
+ fun isActive(context: Context) = prefs(context).getBoolean("active", false)
53
+ fun isPaused(context: Context) = prefs(context).getBoolean("paused", false)
54
+
55
+ @Synchronized
56
+ fun append(context: Context, location: android.location.Location) {
57
+ if (!isActive(context) || isPaused(context)) return
58
+ val json = JSONObject().apply {
59
+ put("latitude", location.latitude)
60
+ put("longitude", location.longitude)
61
+ put("accuracy", location.accuracy)
62
+ put("altitude", location.altitude)
63
+ put("speed", location.speed)
64
+ put("heading", location.bearing)
65
+ put("timestamp", location.time)
66
+ }
67
+ file(context).appendText("$json\n")
68
+ }
69
+
70
+ @Synchronized
71
+ fun locations(context: Context): JSONArray {
72
+ val result = JSONArray()
73
+ val source = file(context)
74
+ if (!source.exists()) return result
75
+ source.forEachLine { line ->
76
+ if (line.isNotBlank()) runCatching { result.put(JSONObject(line)) }
77
+ }
78
+ return result
79
+ }
80
+
81
+ fun state(context: Context, includeLocations: Boolean = false): JSONObject {
82
+ val values = prefs(context)
83
+ val locations = locations(context)
84
+ return JSONObject().apply {
85
+ put("id", values.getString("id", null))
86
+ put("active", values.getBoolean("active", false))
87
+ put("paused", values.getBoolean("paused", false))
88
+ put("startedAt", values.getLong("startedAt", 0).takeIf { it > 0 } ?: JSONObject.NULL)
89
+ put("sampleCount", locations.length())
90
+ if (includeLocations) put("locations", locations)
91
+ }
92
+ }
93
+ }
94
+
95
+ class LocationRecordingService : Service() {
96
+ private val client by lazy { LocationServices.getFusedLocationProviderClient(this) }
97
+ private var callback: LocationCallback? = null
98
+
99
+ override fun onCreate() {
100
+ super.onCreate()
101
+ val manager = getSystemService(NotificationManager::class.java)
102
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
103
+ manager.createNotificationChannel(NotificationChannel(CHANNEL, "Activity recording", NotificationManager.IMPORTANCE_LOW))
104
+ }
105
+ val notification = NotificationCompat.Builder(this, CHANNEL)
106
+ .setSmallIcon(android.R.drawable.ic_menu_mylocation)
107
+ .setContentTitle("Recording your activity")
108
+ .setContentText("WildLoop is securely saving your route")
109
+ .setOngoing(true)
110
+ .setSilent(true)
111
+ .build()
112
+ startForeground(NOTIFICATION_ID, notification)
113
+ }
114
+
115
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
116
+ when (intent?.action) {
117
+ ACTION_STOP -> {
118
+ stopUpdates()
119
+ stopSelf()
120
+ return START_NOT_STICKY
121
+ }
122
+ else -> startUpdates()
123
+ }
124
+ return if (CraftLocationRecordingStore.isActive(this)) START_STICKY else START_NOT_STICKY
125
+ }
126
+
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
+ val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5_000)
131
+ .setMinUpdateIntervalMillis(2_000)
132
+ .setMaxUpdateDelayMillis(10_000)
133
+ .build()
134
+ callback = object : LocationCallback() {
135
+ override fun onLocationResult(result: LocationResult) {
136
+ for (location in result.locations) CraftLocationRecordingStore.append(this@LocationRecordingService, location)
137
+ }
138
+ }
139
+ client.requestLocationUpdates(request, callback!!, Looper.getMainLooper())
140
+ }
141
+
142
+ private fun stopUpdates() {
143
+ callback?.let { client.removeLocationUpdates(it) }
144
+ callback = null
145
+ }
146
+
147
+ override fun onDestroy() {
148
+ stopUpdates()
149
+ super.onDestroy()
150
+ }
151
+
152
+ override fun onBind(intent: Intent?): IBinder? = null
153
+
154
+ companion object {
155
+ const val ACTION_START = "craft.location.START"
156
+ const val ACTION_STOP = "craft.location.STOP"
157
+ private const val CHANNEL = "craft_location_recording"
158
+ private const val NOTIFICATION_ID = 7001
159
+ }
160
+ }
@@ -0,0 +1,230 @@
1
+ package {{PACKAGE_NAME}}
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.os.Bundle
5
+ import android.content.Intent
6
+ import android.net.Uri
7
+ import android.webkit.WebResourceRequest
8
+ import android.webkit.WebResourceError
9
+ import android.webkit.WebResourceResponse
10
+ import android.webkit.WebChromeClient
11
+ import android.webkit.WebSettings
12
+ import android.webkit.WebView
13
+ import android.webkit.WebViewClient
14
+ import androidx.appcompat.app.AppCompatActivity
15
+ import androidx.core.view.WindowCompat
16
+ import androidx.webkit.WebViewAssetLoader
17
+ import org.json.JSONObject
18
+ import java.io.FileNotFoundException
19
+ import java.net.URLConnection
20
+
21
+ class MainActivity : AppCompatActivity() {
22
+ private lateinit var webView: WebView
23
+ private lateinit var craftBridge: CraftBridge
24
+ private val devServerUrl: String? by lazy { loadDevServerUrl() }
25
+ private val trustedOrigins: Set<String> by lazy { loadTrustedOrigins() }
26
+ private val hasBundledFallback: Boolean by lazy { configJson()?.optBoolean("hasBundledFallback", false) == true }
27
+ private val bundledAssetLoader: WebViewAssetLoader by lazy {
28
+ WebViewAssetLoader.Builder()
29
+ .setDomain(BUNDLED_APP_HOST)
30
+ .addPathHandler("/", RouteAwareAssetPathHandler())
31
+ .build()
32
+ }
33
+ private var loadedBundledFallback = false
34
+
35
+ @SuppressLint("SetJavaScriptEnabled")
36
+ override fun onCreate(savedInstanceState: Bundle?) {
37
+ super.onCreate(savedInstanceState)
38
+
39
+ // Enable edge-to-edge
40
+ WindowCompat.setDecorFitsSystemWindows(window, false)
41
+
42
+ setContentView(R.layout.activity_main)
43
+
44
+ webView = findViewById(R.id.webview)
45
+ craftBridge = CraftBridge(this, webView, CraftHealthConnect(this, webView))
46
+ handleIncomingDeepLink(intent, dispatch = false)
47
+
48
+ setupWebView()
49
+ loadContent()
50
+ }
51
+
52
+ @SuppressLint("SetJavaScriptEnabled")
53
+ private fun setupWebView() {
54
+ webView.settings.apply {
55
+ javaScriptEnabled = true
56
+ domStorageEnabled = true
57
+ allowFileAccess = false
58
+ allowContentAccess = false
59
+ allowFileAccessFromFileURLs = false
60
+ allowUniversalAccessFromFileURLs = false
61
+ mediaPlaybackRequiresUserGesture = false
62
+ mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
63
+ cacheMode = WebSettings.LOAD_DEFAULT
64
+ setSupportZoom(false)
65
+ builtInZoomControls = false
66
+ displayZoomControls = false
67
+ }
68
+
69
+ webView.webViewClient = object : WebViewClient() {
70
+ override fun onPageFinished(view: WebView?, url: String?) {
71
+ super.onPageFinished(view, url)
72
+ if (url != null && isTrustedUrl(url)) craftBridge.injectBridge()
73
+ }
74
+
75
+ override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
76
+ val url = request?.url ?: return null
77
+ return bundledAssetLoader.shouldInterceptRequest(url)
78
+ ?: super.shouldInterceptRequest(view, request)
79
+ }
80
+
81
+ override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
82
+ val url = request?.url?.toString() ?: return true
83
+ if (isTrustedUrl(url)) return false
84
+ runCatching { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) }
85
+ return true
86
+ }
87
+
88
+ override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
89
+ super.onReceivedError(view, request, error)
90
+ if (request?.isForMainFrame == true && hasBundledFallback && !loadedBundledFallback) {
91
+ loadedBundledFallback = true
92
+ webView.loadUrl(BUNDLED_APP_URL)
93
+ }
94
+ }
95
+ }
96
+
97
+ webView.webChromeClient = WebChromeClient()
98
+
99
+ // Add JavaScript interface
100
+ webView.addJavascriptInterface(craftBridge, "CraftAndroid")
101
+ }
102
+
103
+ private fun loadContent() {
104
+ // Check for dev server URL in config
105
+ val remoteUrl = devServerUrl
106
+ if (remoteUrl != null) {
107
+ webView.loadUrl(remoteUrl)
108
+ } else {
109
+ // Load from assets
110
+ webView.loadUrl(BUNDLED_APP_URL)
111
+ }
112
+ }
113
+
114
+ private inner class RouteAwareAssetPathHandler : WebViewAssetLoader.PathHandler {
115
+ override fun handle(path: String): WebResourceResponse? {
116
+ val cleanPath = Uri.decode(path).substringBefore('?').trimStart('/')
117
+ if (cleanPath.split('/').any { it == ".." }) return null
118
+ val candidates = when {
119
+ cleanPath.isBlank() -> listOf("index.html")
120
+ cleanPath.endsWith('/') -> listOf("${cleanPath}index.html")
121
+ cleanPath.substringAfterLast('/').contains('.') -> listOf(cleanPath)
122
+ else -> listOf("${cleanPath}.html", "${cleanPath}/index.html")
123
+ }
124
+
125
+ for (candidate in candidates) {
126
+ try {
127
+ val stream = assets.open(candidate)
128
+ val mimeType = URLConnection.guessContentTypeFromName(candidate)
129
+ ?: when (candidate.substringAfterLast('.', "").lowercase()) {
130
+ "js", "mjs" -> "text/javascript"
131
+ "css" -> "text/css"
132
+ "json", "webmanifest" -> "application/json"
133
+ "svg" -> "image/svg+xml"
134
+ else -> "application/octet-stream"
135
+ }
136
+ val encoding = if (mimeType.startsWith("text/") || mimeType.contains("javascript") || mimeType.contains("json")) "UTF-8" else null
137
+ return WebResourceResponse(mimeType, encoding, stream)
138
+ } catch (_: FileNotFoundException) {
139
+ // Try the next clean-route candidate.
140
+ }
141
+ }
142
+ return null
143
+ }
144
+ }
145
+
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)
150
+ }
151
+
152
+ private fun configJson(): JSONObject? {
153
+ return try {
154
+ assets.open("craft.config.json").bufferedReader().use { JSONObject(it.readText()) }
155
+ } catch (_: Exception) {
156
+ null
157
+ }
158
+ }
159
+
160
+ private fun loadDevServerUrl(): String? {
161
+ return try {
162
+ configJson()?.optString("devServerURL")?.takeIf { it.isNotBlank() }
163
+ } catch (e: Exception) {
164
+ null
165
+ }
166
+ }
167
+
168
+ private fun loadTrustedOrigins(): Set<String> {
169
+ val origins = mutableSetOf<String>()
170
+ val config = configJson()
171
+ val configured = config?.optJSONArray("trustedOrigins")
172
+ if (configured != null) {
173
+ for (index in 0 until configured.length()) origins.add(configured.optString(index))
174
+ }
175
+ devServerUrl?.let { url ->
176
+ val uri = Uri.parse(url)
177
+ origins.add("${uri.scheme}://${uri.authority}")
178
+ }
179
+ return origins
180
+ }
181
+
182
+ private fun isTrustedUrl(value: String): Boolean {
183
+ val uri = Uri.parse(value)
184
+ if (uri.scheme == "https" && uri.host == BUNDLED_APP_HOST) return true
185
+ val origin = "${uri.scheme}://${uri.authority}"
186
+ val localDevelopment = uri.scheme == "http" && uri.host in setOf("localhost", "127.0.0.1", "10.0.2.2")
187
+ return (uri.scheme == "https" || localDevelopment) && origin in trustedOrigins
188
+ }
189
+
190
+ override fun onNewIntent(intent: Intent) {
191
+ super.onNewIntent(intent)
192
+ setIntent(intent)
193
+ handleIncomingDeepLink(intent, dispatch = true)
194
+ }
195
+
196
+ override fun onBackPressed() {
197
+ if (webView.canGoBack()) {
198
+ webView.goBack()
199
+ } else {
200
+ super.onBackPressed()
201
+ }
202
+ }
203
+
204
+ override fun onResume() {
205
+ super.onResume()
206
+ webView.onResume()
207
+ }
208
+
209
+ override fun onPause() {
210
+ webView.onPause()
211
+ super.onPause()
212
+ }
213
+
214
+ override fun onDestroy() {
215
+ craftBridge.close()
216
+ webView.destroy()
217
+ super.onDestroy()
218
+ }
219
+
220
+ @Deprecated("Delegates the Health Connect permission contract on Android 13 and earlier")
221
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
222
+ if (craftBridge.onActivityResult(requestCode, resultCode, data)) return
223
+ super.onActivityResult(requestCode, resultCode, data)
224
+ }
225
+
226
+ companion object {
227
+ private const val BUNDLED_APP_HOST = "appassets.androidplatform.net"
228
+ private const val BUNDLED_APP_URL = "https://appassets.androidplatform.net/"
229
+ }
230
+ }