@mentra/crust 0.1.0-dev.1 → 3.1.0-dev.10

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,9 +1,16 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <permission
3
+ android:name="${applicationId}.permission.CRUST_NOTIFICATION_BRIDGE"
4
+ android:protectionLevel="signature" />
5
+ <uses-permission android:name="${applicationId}.permission.CRUST_NOTIFICATION_BRIDGE" />
6
+
2
7
  <application>
3
8
  <service
4
9
  android:name="com.mentra.crust.services.NotificationListenerServiceImpl"
5
10
  android:label="@string/mentra_crust_notification_listener_label"
11
+ android:enabled="false"
6
12
  android:exported="false"
13
+ android:process=":notif"
7
14
  android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
8
15
  <intent-filter>
9
16
  <action android:name="android.service.notification.NotificationListenerService" />
@@ -12,6 +19,13 @@
12
19
  <meta-data android:name="android.service.notification.disabled_filter_types" android:value="ongoing|silent" />
13
20
  </service>
14
21
 
22
+ <receiver
23
+ android:name="com.mentra.crust.services.NotificationConfigReceiver"
24
+ android:enabled="true"
25
+ android:exported="false"
26
+ android:permission="${applicationId}.permission.CRUST_NOTIFICATION_BRIDGE"
27
+ android:process=":notif" />
28
+
15
29
  <!-- The Mapbox Navigation SDK delivers turn-by-turn steps inline via
16
30
  RouteProgress (no Messenger-IPC service), so the Google-era
17
31
  NavInfoReceiverService is gone. Nothing to register here for nav. -->
@@ -1,7 +1,9 @@
1
1
  package com.mentra.crust
2
2
 
3
+ import android.content.BroadcastReceiver
3
4
  import android.util.Log
4
5
  import com.mentra.crust.services.NotificationListener
6
+ import com.mentra.crust.services.NotificationProcessBridge
5
7
  import expo.modules.kotlin.modules.Module
6
8
  import expo.modules.kotlin.modules.ModuleDefinition
7
9
  import java.net.URL
@@ -19,34 +21,33 @@ class CrustModule : Module() {
19
21
  @Volatile private var eventEmitter: ((String, Map<String, Any>) -> Unit)? = null
20
22
 
21
23
  fun emitPhoneNotification(
24
+ context: android.content.Context,
22
25
  notificationKey: String,
23
26
  packageName: String,
24
27
  appName: String,
25
28
  title: String,
26
29
  text: String,
27
30
  timestamp: Long,
31
+ priority: Int,
28
32
  ) {
29
- val data =
30
- mapOf(
31
- "notificationId" to "$packageName-$notificationKey",
32
- "app" to appName,
33
- "title" to title.ifEmpty { appName },
34
- "content" to text,
35
- "priority" to "normal",
36
- "timestamp" to timestamp,
37
- "packageName" to packageName,
38
- )
39
- emitEvent("phone_notification", data)
33
+ NotificationProcessBridge.emitPosted(
34
+ context,
35
+ notificationKey,
36
+ packageName,
37
+ appName,
38
+ title,
39
+ text,
40
+ timestamp,
41
+ priority,
42
+ )
40
43
  }
41
44
 
42
- fun emitPhoneNotificationDismissed(notificationKey: String, packageName: String) {
43
- val data =
44
- mapOf(
45
- "notificationId" to "$packageName-$notificationKey",
46
- "notificationKey" to notificationKey,
47
- "packageName" to packageName,
48
- )
49
- emitEvent("phone_notification_dismissed", data)
45
+ fun emitPhoneNotificationDismissed(
46
+ context: android.content.Context,
47
+ notificationKey: String,
48
+ packageName: String,
49
+ ) {
50
+ NotificationProcessBridge.emitDismissed(context, notificationKey, packageName)
50
51
  }
51
52
 
52
53
  fun emitCaptionsTesterIncident(data: Map<String, Any>) {
@@ -75,6 +76,23 @@ class CrustModule : Module() {
75
76
  // __dispatch from a per-miniapp QuickJS context (SUBSCRIBE, mic, location,
76
77
  // display, send, etc.) would be silently dropped on Android.
77
78
  @Volatile private var runtimeInstalled: Boolean = false
79
+ private var notificationEventReceiver: BroadcastReceiver? = null
80
+ private var notificationBridgeContext: android.content.Context? = null
81
+
82
+ private fun registerNotificationBridgeIfPossible(): Boolean {
83
+ if (notificationEventReceiver != null) return true
84
+ val context = appContext.reactContext ?: appContext.currentActivity ?: return false
85
+ val applicationContext = context.applicationContext
86
+ notificationEventReceiver =
87
+ NotificationProcessBridge.register(applicationContext) { eventName, data ->
88
+ emitEvent(eventName, data)
89
+ }
90
+ // Keep the exact long-lived context used to register the receiver. Expo may
91
+ // clear reactContext/currentActivity before OnDestroy, but Android still
92
+ // requires this receiver to be unregistered when the module is recreated.
93
+ notificationBridgeContext = applicationContext
94
+ return true
95
+ }
78
96
 
79
97
  private fun installRuntimeIfPossible(reason: String): Boolean {
80
98
  if (runtimeInstalled) return true
@@ -118,9 +136,21 @@ class CrustModule : Module() {
118
136
 
119
137
  OnCreate {
120
138
  eventEmitter = { eventName, data -> sendEvent(eventName, data) }
139
+ registerNotificationBridgeIfPossible()
121
140
  installRuntimeIfPossible("OnCreate")
122
141
  }
123
142
 
143
+ OnDestroy {
144
+ val context = notificationBridgeContext
145
+ val receiver = notificationEventReceiver
146
+ if (context != null && receiver != null) {
147
+ NotificationProcessBridge.unregister(context, receiver)
148
+ }
149
+ notificationEventReceiver = null
150
+ notificationBridgeContext = null
151
+ eventEmitter = null
152
+ }
153
+
124
154
  Function("hello") {
125
155
  "Hello world! 👋"
126
156
  }
@@ -152,12 +182,18 @@ class CrustModule : Module() {
152
182
 
153
183
  // MARK: - MentraOS Notification Commands
154
184
 
155
- AsyncFunction("setNotificationConfig") { enabled: Boolean, blocklist: List<String> ->
185
+ AsyncFunction("setNotificationConfig") {
186
+ listenerEnabled: Boolean, blocklist: List<String> ->
187
+ registerNotificationBridgeIfPossible()
156
188
  val context =
157
189
  appContext.reactContext
158
190
  ?: appContext.currentActivity
159
191
  ?: throw IllegalStateException("No context available")
160
- NotificationListener.getInstance(context).setNotificationConfig(enabled, blocklist)
192
+ NotificationListener.setNotificationConfig(
193
+ context,
194
+ listenerEnabled,
195
+ blocklist,
196
+ )
161
197
  }
162
198
 
163
199
  AsyncFunction("getInstalledApps") {
@@ -165,7 +201,7 @@ class CrustModule : Module() {
165
201
  appContext.reactContext
166
202
  ?: appContext.currentActivity
167
203
  ?: throw IllegalStateException("No context available")
168
- NotificationListener.getInstance(context).getInstalledApps()
204
+ NotificationListener.getInstalledApps(context)
169
205
  }
170
206
 
171
207
  AsyncFunction("getInstalledAppsForNotifications") {
@@ -173,7 +209,7 @@ class CrustModule : Module() {
173
209
  appContext.reactContext
174
210
  ?: appContext.currentActivity
175
211
  ?: throw IllegalStateException("No context available")
176
- NotificationListener.getInstance(context).getInstalledApps()
212
+ NotificationListener.getInstalledApps(context)
177
213
  }
178
214
 
179
215
  AsyncFunction("hasNotificationListenerPermission") {
@@ -181,7 +217,15 @@ class CrustModule : Module() {
181
217
  appContext.reactContext
182
218
  ?: appContext.currentActivity
183
219
  ?: throw IllegalStateException("No context available")
184
- NotificationListener.getInstance(context).hasNotificationListenerPermission()
220
+ NotificationListener.hasNotificationListenerPermission(context)
221
+ }
222
+
223
+ AsyncFunction("refreshNotificationListener") {
224
+ val context =
225
+ appContext.reactContext
226
+ ?: appContext.currentActivity
227
+ ?: throw IllegalStateException("No context available")
228
+ NotificationListener.refreshComponentForPermission(context)
185
229
  }
186
230
 
187
231
  AsyncFunction("openNotificationListenerSettings") {
@@ -189,7 +233,7 @@ class CrustModule : Module() {
189
233
  appContext.reactContext
190
234
  ?: appContext.currentActivity
191
235
  ?: throw IllegalStateException("No context available")
192
- NotificationListener.getInstance(context).openNotificationListenerSettings()
236
+ NotificationListener.openNotificationListenerSettings(context)
193
237
  true
194
238
  }
195
239
 
@@ -436,6 +436,8 @@ object NavigationManager {
436
436
  .alternatives(false)
437
437
  .steps(true)
438
438
  .bannerInstructions(true)
439
+ // The onboard router rejects voice units when voice instructions are disabled.
440
+ .voiceUnits(null)
439
441
  .voiceInstructions(false)
440
442
  .exclude(buildExclude(options))
441
443
  .build()
@@ -14,12 +14,13 @@ import android.text.TextUtils
14
14
  import android.util.Base64
15
15
  import android.util.Log
16
16
  import com.mentra.crust.CrustModule
17
+ import java.util.concurrent.ConcurrentHashMap
17
18
 
18
19
  class NotificationListener private constructor(private val context: Context) {
19
20
  companion object {
20
21
  private const val TAG = "CrustNotificationListener"
21
22
  private const val PREFS_NAME = "mentra_crust_notification_prefs"
22
- private const val PREF_NOTIFICATIONS_ENABLED = "notifications_enabled"
23
+ private const val PREF_LISTENER_ENABLED = "notification_listener_enabled"
23
24
  private const val PREF_NOTIFICATIONS_BLOCKLIST = "notifications_blocklist"
24
25
 
25
26
  @Volatile private var instance: NotificationListener? = null
@@ -33,9 +34,207 @@ class NotificationListener private constructor(private val context: Context) {
33
34
  }
34
35
  }
35
36
  }
37
+
38
+ /**
39
+ * Persist desired notification state without constructing the listener.
40
+ * The component stays enabled so Android can show it in notification-access
41
+ * Settings, but its service process only starts after access is granted.
42
+ */
43
+ fun setNotificationConfig(
44
+ context: Context,
45
+ listenerEnabled: Boolean,
46
+ blocklist: List<String>,
47
+ ) {
48
+ val applicationContext = context.applicationContext
49
+ val blocklistSet = blocklist.toSet()
50
+ persistConfig(applicationContext, listenerEnabled, blocklistSet)
51
+
52
+ val permissionGranted = hasNotificationListenerPermission(applicationContext)
53
+ val shouldRun = listenerEnabled && permissionGranted
54
+ val componentChanged = updateComponentState(applicationContext, enabled = listenerEnabled)
55
+
56
+ // Keep the isolated process's own SharedPreferences cache and live
57
+ // singleton in sync. A rebind is only needed when enabling the component;
58
+ // ordinary blocklist updates must not restart a healthy listener.
59
+ if (permissionGranted) {
60
+ NotificationProcessBridge.sendConfig(
61
+ applicationContext,
62
+ listenerEnabled,
63
+ blocklistSet,
64
+ requestRebind = shouldRun && componentChanged,
65
+ )
66
+ }
67
+
68
+ if (!shouldRun) {
69
+ Log.d(TAG, "Notification listener prerequisites not met; service will not start")
70
+ return
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Reconcile the component after an OS notification-access check.
76
+ *
77
+ * The permission flow calls this when the app returns from Settings, so a
78
+ * newly granted listener starts immediately. Without permission the service
79
+ * is not rebound and the isolated process is never started.
80
+ */
81
+ fun refreshComponentForPermission(context: Context): Boolean {
82
+ val applicationContext = context.applicationContext
83
+ val permissionGranted = hasNotificationListenerPermission(applicationContext)
84
+ val preferences = applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
85
+ val listenerEnabled = preferences.getBoolean(PREF_LISTENER_ENABLED, false)
86
+ val blocklist =
87
+ preferences.getStringSet(PREF_NOTIFICATIONS_BLOCKLIST, emptySet())?.toSet() ?: emptySet()
88
+
89
+ val shouldRun = listenerEnabled && permissionGranted
90
+ updateComponentState(applicationContext, enabled = listenerEnabled)
91
+ if (permissionGranted) {
92
+ NotificationProcessBridge.sendConfig(
93
+ applicationContext,
94
+ listenerEnabled,
95
+ blocklist,
96
+ // This method is reserved for the confirmed permission-grant path.
97
+ // The :notif receiver persists the config before requesting rebind.
98
+ requestRebind = shouldRun,
99
+ )
100
+ }
101
+ return permissionGranted
102
+ }
103
+
104
+ fun openNotificationListenerSettings(context: Context) {
105
+ val intent = Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)
106
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
107
+ context.applicationContext.startActivity(intent)
108
+ }
109
+
110
+ fun hasNotificationListenerPermission(context: Context): Boolean {
111
+ val packageName = context.packageName
112
+ val flat = Settings.Secure.getString(context.contentResolver, "enabled_notification_listeners")
113
+ if (TextUtils.isEmpty(flat)) return false
114
+
115
+ return flat.split(":").any { name ->
116
+ ComponentName.unflattenFromString(name)?.packageName == packageName
117
+ }
118
+ }
119
+
120
+ private fun updateComponentState(
121
+ context: Context,
122
+ enabled: Boolean,
123
+ ): Boolean {
124
+ val component = ComponentName(context, NotificationListenerServiceImpl::class.java)
125
+ val newState =
126
+ if (enabled) {
127
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED
128
+ } else {
129
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED
130
+ }
131
+ val packageManager = context.packageManager
132
+ if (packageManager.getComponentEnabledSetting(component) != newState) {
133
+ packageManager.setComponentEnabledSetting(
134
+ component,
135
+ newState,
136
+ PackageManager.DONT_KILL_APP,
137
+ )
138
+ return true
139
+ }
140
+ return false
141
+ }
142
+
143
+ internal fun persistConfig(
144
+ context: Context,
145
+ listenerEnabled: Boolean,
146
+ blocklist: Set<String>,
147
+ ) {
148
+ val committed =
149
+ context
150
+ .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
151
+ .edit()
152
+ .putBoolean(PREF_LISTENER_ENABLED, listenerEnabled)
153
+ .putStringSet(PREF_NOTIFICATIONS_BLOCKLIST, blocklist)
154
+ .commit()
155
+ if (!committed) {
156
+ Log.w(TAG, "Could not persist notification-listener config")
157
+ }
158
+ }
159
+
160
+ internal fun requestListenerRebind(context: Context) {
161
+ val component = ComponentName(context, NotificationListenerServiceImpl::class.java)
162
+ runCatching { NotificationListenerService.requestRebind(component) }
163
+ .onFailure { Log.w(TAG, "Could not request notification-listener rebind", it) }
164
+ }
165
+
166
+ internal fun applyConfigToExisting(
167
+ listenerEnabled: Boolean,
168
+ blocklist: Set<String>,
169
+ ) {
170
+ synchronized(this) {
171
+ instance?.applyConfig(listenerEnabled, blocklist)
172
+ }
173
+ }
174
+
175
+ fun isListenerEnabled(context: Context): Boolean {
176
+ val preferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
177
+ return preferences.getBoolean(PREF_LISTENER_ENABLED, false) &&
178
+ hasNotificationListenerPermission(context)
179
+ }
180
+
181
+ /**
182
+ * Read installed-app metadata without constructing the service singleton.
183
+ *
184
+ * These APIs run in the React Native process. The notification singleton
185
+ * belongs exclusively to :notif; constructing it here would create an
186
+ * orphaned HandlerThread that no service lifecycle ever destroys.
187
+ */
188
+ fun getInstalledApps(context: Context): List<Map<String, Any?>> {
189
+ val applicationContext = context.applicationContext
190
+ val packageManager = applicationContext.packageManager
191
+ val packages = packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
192
+ val blocklist =
193
+ applicationContext
194
+ .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
195
+ .getStringSet(PREF_NOTIFICATIONS_BLOCKLIST, emptySet())
196
+ ?.toSet() ?: emptySet()
197
+
198
+ return packages
199
+ .filter { it.flags and ApplicationInfo.FLAG_SYSTEM == 0 }
200
+ .map { appInfo ->
201
+ val icon =
202
+ try {
203
+ val drawable = packageManager.getApplicationIcon(appInfo.packageName)
204
+ val bitmap = (drawable as? android.graphics.drawable.BitmapDrawable)?.bitmap
205
+ if (bitmap != null) {
206
+ val outputStream = java.io.ByteArrayOutputStream()
207
+ bitmap.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, outputStream)
208
+ val byteArray = outputStream.toByteArray()
209
+ "data:image/png;base64," + Base64.encodeToString(byteArray, Base64.NO_WRAP)
210
+ } else {
211
+ null
212
+ }
213
+ } catch (_: Exception) {
214
+ null
215
+ }
216
+
217
+ mapOf(
218
+ "packageName" to appInfo.packageName,
219
+ "appName" to packageManager.getApplicationLabel(appInfo).toString(),
220
+ "isBlocked" to blocklist.contains(appInfo.packageName),
221
+ "icon" to icon,
222
+ )
223
+ }
224
+ .sortedBy { it["appName"] as String }
225
+ }
226
+
227
+ /** Dispose the process singleton so a service rebind gets a live HandlerThread. */
228
+ fun destroyInstance() {
229
+ synchronized(this) {
230
+ instance?.cleanup()
231
+ instance = null
232
+ }
233
+ }
36
234
  }
37
235
 
38
236
  private val listeners = mutableListOf<OnNotificationReceivedListener>()
237
+ private val appNameCache = ConcurrentHashMap<String, String>()
39
238
 
40
239
  // Deduplication tracking with dedicated background thread. Using HandlerThread
41
240
  // keeps the service independent from the app lifecycle on newer Android versions.
@@ -46,45 +245,17 @@ class NotificationListener private constructor(private val context: Context) {
46
245
 
47
246
  private val preferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
48
247
 
49
- @Volatile private var notificationsEnabled = preferences.getBoolean(PREF_NOTIFICATIONS_ENABLED, false)
248
+ @Volatile private var listenerEnabled = preferences.getBoolean(PREF_LISTENER_ENABLED, false)
50
249
  @Volatile
51
250
  private var notificationsBlocklist =
52
251
  preferences.getStringSet(PREF_NOTIFICATIONS_BLOCKLIST, emptySet())?.toSet() ?: emptySet()
53
252
 
54
- /** Keep MentraOS notification settings in Crust instead of Bluetooth SDK state. */
55
- fun setNotificationConfig(enabled: Boolean, blocklist: List<String>) {
56
- val blocklistSet = blocklist.toSet()
57
- notificationsEnabled = enabled
58
- notificationsBlocklist = blocklistSet
59
- preferences
60
- .edit()
61
- .putBoolean(PREF_NOTIFICATIONS_ENABLED, enabled)
62
- .putStringSet(PREF_NOTIFICATIONS_BLOCKLIST, blocklistSet)
63
- .apply()
64
- }
65
-
66
- /** Check if notification listener permission is granted. */
67
- fun hasNotificationListenerPermission(): Boolean {
68
- val packageName = context.packageName
69
- val flat = Settings.Secure.getString(context.contentResolver, "enabled_notification_listeners")
70
-
71
- if (!TextUtils.isEmpty(flat)) {
72
- val names = flat.split(":")
73
- for (name in names) {
74
- val componentName = ComponentName.unflattenFromString(name)
75
- if (componentName != null && TextUtils.equals(packageName, componentName.packageName)) {
76
- return true
77
- }
78
- }
79
- }
80
- return false
81
- }
82
-
83
- /** Open notification listener settings. */
84
- fun openNotificationListenerSettings() {
85
- val intent = Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)
86
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
87
- context.startActivity(intent)
253
+ private fun applyConfig(
254
+ listenerEnabled: Boolean,
255
+ blocklist: Set<String>,
256
+ ) {
257
+ this.listenerEnabled = listenerEnabled
258
+ notificationsBlocklist = blocklist
88
259
  }
89
260
 
90
261
  /** Add a listener for notifications. */
@@ -120,7 +291,17 @@ class NotificationListener private constructor(private val context: Context) {
120
291
  }
121
292
 
122
293
  /** Called internally by the service when a notification is posted. */
123
- internal fun onNotificationPosted(sbn: StatusBarNotification) {
294
+ /**
295
+ * @param importance NotificationManager.IMPORTANCE_* from the ranking, or null
296
+ * when unavailable. Preferred over the deprecated Notification.priority,
297
+ * which reads 0 for channel-based notifications.
298
+ */
299
+ internal fun onNotificationPosted(sbn: StatusBarNotification, importance: Int? = null) {
300
+ if (!listenerEnabled) {
301
+ Log.d(TAG, "Notification listener disabled")
302
+ return
303
+ }
304
+
124
305
  val packageName = sbn.packageName
125
306
  Log.d(TAG, "Received notification from $packageName (key: ${sbn.key})")
126
307
 
@@ -134,11 +315,6 @@ class NotificationListener private constructor(private val context: Context) {
134
315
  return
135
316
  }
136
317
 
137
- if (!notificationsEnabled) {
138
- Log.d(TAG, "Notifications disabled globally")
139
- return
140
- }
141
-
142
318
  val notification = sbn.notification
143
319
  val extras = notification.extras
144
320
  val title = extras.getCharSequence("android.title")?.toString() ?: ""
@@ -154,15 +330,6 @@ class NotificationListener private constructor(private val context: Context) {
154
330
  return
155
331
  }
156
332
 
157
- val packageManager = context.packageManager
158
- val appName =
159
- try {
160
- val appInfo = packageManager.getApplicationInfo(packageName, 0)
161
- packageManager.getApplicationLabel(appInfo).toString()
162
- } catch (_: Exception) {
163
- packageName
164
- }
165
-
166
333
  val notificationKey = "$packageName|$title|$text"
167
334
 
168
335
  synchronized(notificationBuffer) {
@@ -174,14 +341,39 @@ class NotificationListener private constructor(private val context: Context) {
174
341
  val task =
175
342
  Runnable {
176
343
  try {
344
+ // Config can change during the debounce window. Recheck here so a
345
+ // kill-switch or blocklist update cannot leak an already-buffered
346
+ // notification after it takes effect.
347
+ if (!listenerEnabled || notificationsBlocklist.contains(packageName)) {
348
+ return@Runnable
349
+ }
350
+
351
+ // PackageManager label resolution can take tens of milliseconds on
352
+ // low-end devices. It belongs on this handler thread, not the
353
+ // NotificationListenerService main callback, and only once per app.
354
+ val appName =
355
+ appNameCache.getOrPut(packageName) {
356
+ try {
357
+ val packageManager = context.packageManager
358
+ val appInfo = packageManager.getApplicationInfo(packageName, 0)
359
+ packageManager.getApplicationLabel(appInfo).toString()
360
+ } catch (_: Exception) {
361
+ packageName
362
+ }
363
+ }
177
364
  Log.d(TAG, "Processing buffered notification from $appName")
178
365
  CrustModule.emitPhoneNotification(
366
+ context = context,
179
367
  notificationKey = sbn.key,
180
368
  packageName = packageName,
181
369
  appName = appName,
182
370
  title = title,
183
371
  text = text,
184
372
  timestamp = sbn.postTime,
373
+ // Map channel importance onto the priority scale consumers
374
+ // already understand (LOW/MIN are negative). Fall back to the
375
+ // deprecated field only when no ranking was available.
376
+ priority = importance?.let { it - 3 } ?: notification.priority,
185
377
  )
186
378
 
187
379
  val notificationData =
@@ -213,12 +405,17 @@ class NotificationListener private constructor(private val context: Context) {
213
405
 
214
406
  /** Called internally by the service when a notification is removed. */
215
407
  internal fun onNotificationRemoved(sbn: StatusBarNotification) {
408
+ if (!listenerEnabled) {
409
+ return
410
+ }
411
+
216
412
  val packageName = sbn.packageName
217
413
  val notificationKey = sbn.key
218
414
 
219
415
  Log.d(TAG, "Notification removed - package: $packageName, key: $notificationKey")
220
416
 
221
417
  CrustModule.emitPhoneNotificationDismissed(
418
+ context = context,
222
419
  notificationKey = notificationKey,
223
420
  packageName = packageName,
224
421
  )
@@ -242,41 +439,6 @@ class NotificationListener private constructor(private val context: Context) {
242
439
  val tag: String?,
243
440
  )
244
441
 
245
- /** Get all installed apps with details. */
246
- fun getInstalledApps(): List<Map<String, Any?>> {
247
- val packageManager = context.packageManager
248
- val packages = packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
249
- val blocklist = notificationsBlocklist
250
-
251
- return packages
252
- .filter { it.flags and ApplicationInfo.FLAG_SYSTEM == 0 }
253
- .map { appInfo ->
254
- val icon =
255
- try {
256
- val drawable = packageManager.getApplicationIcon(appInfo.packageName)
257
- val bitmap = (drawable as? android.graphics.drawable.BitmapDrawable)?.bitmap
258
- if (bitmap != null) {
259
- val outputStream = java.io.ByteArrayOutputStream()
260
- bitmap.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, outputStream)
261
- val byteArray = outputStream.toByteArray()
262
- "data:image/png;base64," + Base64.encodeToString(byteArray, Base64.NO_WRAP)
263
- } else {
264
- null
265
- }
266
- } catch (_: Exception) {
267
- null
268
- }
269
-
270
- mapOf(
271
- "packageName" to appInfo.packageName,
272
- "appName" to packageManager.getApplicationLabel(appInfo).toString(),
273
- "isBlocked" to blocklist.contains(appInfo.packageName),
274
- "icon" to icon,
275
- )
276
- }
277
- .sortedBy { it["appName"] as String }
278
- }
279
-
280
442
  /** Clean up resources when the service is destroyed. */
281
443
  fun cleanup() {
282
444
  Log.d(TAG, "Cleaning up notification handler thread")
@@ -292,7 +454,22 @@ class NotificationListener private constructor(private val context: Context) {
292
454
  class NotificationListenerServiceImpl : NotificationListenerService() {
293
455
  override fun onNotificationPosted(sbn: StatusBarNotification) {
294
456
  super.onNotificationPosted(sbn)
295
- NotificationListener.getInstance(applicationContext).onNotificationPosted(sbn)
457
+ // Channel importance, not Notification.priority: the latter was deprecated
458
+ // at API 26 and reads 0 for apps that express intent through a channel
459
+ // instead, which would make every consumer treat them as ordinary. Only the
460
+ // service can see the ranking, so it is resolved here and passed down.
461
+ NotificationListener.getInstance(applicationContext).onNotificationPosted(sbn, importanceOf(sbn))
462
+ }
463
+
464
+ /** IMPORTANCE_* for this notification, or null when the ranking is unavailable. */
465
+ private fun importanceOf(sbn: StatusBarNotification): Int? {
466
+ return try {
467
+ val ranking = NotificationListenerService.Ranking()
468
+ if (currentRanking?.getRanking(sbn.key, ranking) == true) ranking.importance else null
469
+ } catch (e: Exception) {
470
+ Log.w("CrustNotificationListener", "could not read ranking importance", e)
471
+ null
472
+ }
296
473
  }
297
474
 
298
475
  override fun onNotificationRemoved(sbn: StatusBarNotification) {
@@ -307,13 +484,19 @@ class NotificationListenerServiceImpl : NotificationListenerService() {
307
484
 
308
485
  override fun onListenerDisconnected() {
309
486
  super.onListenerDisconnected()
310
- Log.d("CrustNotificationListener", "NotificationListenerService disconnected, requesting rebind")
311
- requestRebind(ComponentName(this, NotificationListenerServiceImpl::class.java))
487
+ Log.d("CrustNotificationListener", "NotificationListenerService disconnected")
488
+ if (NotificationListener.isListenerEnabled(applicationContext)) {
489
+ runCatching {
490
+ requestRebind(ComponentName(this, NotificationListenerServiceImpl::class.java))
491
+ }.onFailure {
492
+ Log.w("CrustNotificationListener", "Could not request notification-listener rebind", it)
493
+ }
494
+ }
312
495
  }
313
496
 
314
497
  override fun onDestroy() {
315
498
  super.onDestroy()
316
499
  Log.d("CrustNotificationListener", "NotificationListenerService being destroyed")
317
- NotificationListener.getInstance(applicationContext).cleanup()
500
+ NotificationListener.destroyInstance()
318
501
  }
319
502
  }
@@ -0,0 +1,222 @@
1
+ package com.mentra.crust.services
2
+
3
+ import android.app.ActivityManager
4
+ import android.app.Application
5
+ import android.content.BroadcastReceiver
6
+ import android.content.ComponentName
7
+ import android.content.Context
8
+ import android.content.Intent
9
+ import android.content.IntentFilter
10
+ import android.os.Build
11
+ import android.os.Process
12
+ import android.util.Log
13
+ import java.io.File
14
+
15
+ /**
16
+ * Identifies the lightweight process used by NotificationListenerService.
17
+ *
18
+ * The host app's generated MainApplication calls this before initializing
19
+ * React Native. Keep this object free of Expo/React dependencies so loading it
20
+ * cannot recreate the cold-start work that the process split avoids.
21
+ */
22
+ object NotificationProcess {
23
+ const val SUFFIX = ":notif"
24
+
25
+ @JvmStatic
26
+ fun isCurrent(context: Context): Boolean = currentName(context).endsWith(SUFFIX)
27
+
28
+ private fun currentName(context: Context): String {
29
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
30
+ return Application.getProcessName()
31
+ }
32
+
33
+ val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
34
+ activityManager
35
+ ?.runningAppProcesses
36
+ ?.firstOrNull { it.pid == Process.myPid() }
37
+ ?.processName
38
+ ?.let { return it }
39
+
40
+ return runCatching {
41
+ File("/proc/self/cmdline").inputStream().bufferedReader().use { it.readText().trimEnd('\u0000') }
42
+ }.getOrDefault(context.packageName)
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Signature-protected, process-safe handoff from the notification process to
48
+ * the React Native process. The receiver is dynamic, so sending while the app
49
+ * runtime is dead does not start the expensive main process.
50
+ */
51
+ object NotificationProcessBridge {
52
+ private const val TAG = "CrustNotificationBridge"
53
+ private const val POSTED_SUFFIX = ".crust.PHONE_NOTIFICATION"
54
+ private const val DISMISSED_SUFFIX = ".crust.PHONE_NOTIFICATION_DISMISSED"
55
+ private const val CONFIG_SUFFIX = ".crust.NOTIFICATION_CONFIG"
56
+ private const val PERMISSION_SUFFIX = ".permission.CRUST_NOTIFICATION_BRIDGE"
57
+
58
+ private const val EXTRA_NOTIFICATION_ID = "notificationId"
59
+ private const val EXTRA_NOTIFICATION_KEY = "notificationKey"
60
+ private const val EXTRA_PACKAGE_NAME = "packageName"
61
+ private const val EXTRA_APP = "app"
62
+ private const val EXTRA_TITLE = "title"
63
+ private const val EXTRA_CONTENT = "content"
64
+ private const val EXTRA_PRIORITY = "priority"
65
+ private const val EXTRA_TIMESTAMP = "timestamp"
66
+ private const val EXTRA_LISTENER_ENABLED = "listenerEnabled"
67
+ private const val EXTRA_BLOCKLIST = "blocklist"
68
+ private const val EXTRA_REQUEST_REBIND = "requestRebind"
69
+
70
+ fun register(
71
+ context: Context,
72
+ onEvent: (String, Map<String, Any>) -> Unit,
73
+ ): BroadcastReceiver {
74
+ val applicationContext = context.applicationContext
75
+ val receiver =
76
+ object : BroadcastReceiver() {
77
+ override fun onReceive(receiveContext: Context, intent: Intent) {
78
+ when (intent.action) {
79
+ postedAction(receiveContext) -> {
80
+ onEvent(
81
+ "phone_notification",
82
+ mapOf(
83
+ EXTRA_NOTIFICATION_ID to intent.getStringExtra(EXTRA_NOTIFICATION_ID).orEmpty(),
84
+ EXTRA_APP to intent.getStringExtra(EXTRA_APP).orEmpty(),
85
+ EXTRA_TITLE to intent.getStringExtra(EXTRA_TITLE).orEmpty(),
86
+ EXTRA_CONTENT to intent.getStringExtra(EXTRA_CONTENT).orEmpty(),
87
+ EXTRA_PRIORITY to intent.getIntExtra(EXTRA_PRIORITY, 0),
88
+ EXTRA_TIMESTAMP to intent.getLongExtra(EXTRA_TIMESTAMP, 0L),
89
+ EXTRA_PACKAGE_NAME to intent.getStringExtra(EXTRA_PACKAGE_NAME).orEmpty(),
90
+ ),
91
+ )
92
+ }
93
+ dismissedAction(receiveContext) -> {
94
+ onEvent(
95
+ "phone_notification_dismissed",
96
+ mapOf(
97
+ EXTRA_NOTIFICATION_ID to intent.getStringExtra(EXTRA_NOTIFICATION_ID).orEmpty(),
98
+ EXTRA_NOTIFICATION_KEY to intent.getStringExtra(EXTRA_NOTIFICATION_KEY).orEmpty(),
99
+ EXTRA_PACKAGE_NAME to intent.getStringExtra(EXTRA_PACKAGE_NAME).orEmpty(),
100
+ ),
101
+ )
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ val filter =
108
+ IntentFilter().apply {
109
+ addAction(postedAction(applicationContext))
110
+ addAction(dismissedAction(applicationContext))
111
+ }
112
+ val permission = bridgePermission(applicationContext)
113
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
114
+ applicationContext.registerReceiver(
115
+ receiver,
116
+ filter,
117
+ permission,
118
+ null,
119
+ Context.RECEIVER_NOT_EXPORTED,
120
+ )
121
+ } else {
122
+ @Suppress("UnspecifiedRegisterReceiverFlag")
123
+ applicationContext.registerReceiver(receiver, filter, permission, null)
124
+ }
125
+ return receiver
126
+ }
127
+
128
+ fun unregister(context: Context, receiver: BroadcastReceiver) {
129
+ runCatching { context.applicationContext.unregisterReceiver(receiver) }
130
+ .onFailure { Log.w(TAG, "Could not unregister notification bridge", it) }
131
+ }
132
+
133
+ fun emitPosted(
134
+ context: Context,
135
+ notificationKey: String,
136
+ packageName: String,
137
+ appName: String,
138
+ title: String,
139
+ text: String,
140
+ timestamp: Long,
141
+ priority: Int,
142
+ ) {
143
+ val intent =
144
+ Intent(postedAction(context))
145
+ .setPackage(context.packageName)
146
+ .putExtra(EXTRA_NOTIFICATION_ID, "$packageName-$notificationKey")
147
+ .putExtra(EXTRA_APP, appName)
148
+ .putExtra(EXTRA_TITLE, title.ifEmpty { appName })
149
+ .putExtra(EXTRA_CONTENT, text)
150
+ .putExtra(EXTRA_PRIORITY, priority)
151
+ .putExtra(EXTRA_TIMESTAMP, timestamp)
152
+ .putExtra(EXTRA_PACKAGE_NAME, packageName)
153
+ context.sendBroadcast(intent, bridgePermission(context))
154
+ }
155
+
156
+ fun emitDismissed(context: Context, notificationKey: String, packageName: String) {
157
+ val intent =
158
+ Intent(dismissedAction(context))
159
+ .setPackage(context.packageName)
160
+ .putExtra(EXTRA_NOTIFICATION_ID, "$packageName-$notificationKey")
161
+ .putExtra(EXTRA_NOTIFICATION_KEY, notificationKey)
162
+ .putExtra(EXTRA_PACKAGE_NAME, packageName)
163
+ context.sendBroadcast(intent, bridgePermission(context))
164
+ }
165
+
166
+ fun sendConfig(
167
+ context: Context,
168
+ listenerEnabled: Boolean,
169
+ blocklist: Set<String>,
170
+ requestRebind: Boolean,
171
+ ) {
172
+ val intent =
173
+ Intent(configAction(context))
174
+ .setComponent(ComponentName(context, NotificationConfigReceiver::class.java))
175
+ .putExtra(EXTRA_LISTENER_ENABLED, listenerEnabled)
176
+ .putStringArrayListExtra(EXTRA_BLOCKLIST, ArrayList(blocklist))
177
+ .putExtra(EXTRA_REQUEST_REBIND, requestRebind)
178
+ context.sendBroadcast(intent, bridgePermission(context))
179
+ }
180
+
181
+ internal fun readConfig(intent: Intent): NotificationConfigUpdate {
182
+ return NotificationConfigUpdate(
183
+ listenerEnabled = intent.getBooleanExtra(EXTRA_LISTENER_ENABLED, false),
184
+ blocklist = intent.getStringArrayListExtra(EXTRA_BLOCKLIST)?.toSet() ?: emptySet(),
185
+ requestRebind = intent.getBooleanExtra(EXTRA_REQUEST_REBIND, false),
186
+ )
187
+ }
188
+
189
+ internal fun isConfigAction(context: Context, intent: Intent): Boolean = intent.action == configAction(context)
190
+
191
+ private fun postedAction(context: Context) = context.packageName + POSTED_SUFFIX
192
+
193
+ private fun dismissedAction(context: Context) = context.packageName + DISMISSED_SUFFIX
194
+
195
+ private fun configAction(context: Context) = context.packageName + CONFIG_SUFFIX
196
+
197
+ private fun bridgePermission(context: Context) = context.packageName + PERMISSION_SUFFIX
198
+ }
199
+
200
+ internal data class NotificationConfigUpdate(
201
+ val listenerEnabled: Boolean,
202
+ val blocklist: Set<String>,
203
+ val requestRebind: Boolean,
204
+ )
205
+
206
+ /** Persists and applies config inside the isolated notification process. */
207
+ class NotificationConfigReceiver : BroadcastReceiver() {
208
+ override fun onReceive(context: Context, intent: Intent) {
209
+ if (!NotificationProcessBridge.isConfigAction(context, intent)) return
210
+ val config = NotificationProcessBridge.readConfig(intent)
211
+
212
+ // SharedPreferences caches are per-process. Persist the payload here before
213
+ // touching the live instance so a listener recreated by requestRebind reads
214
+ // the same config rather than this process's stale cache.
215
+ NotificationListener.persistConfig(context, config.listenerEnabled, config.blocklist)
216
+ NotificationListener.applyConfigToExisting(config.listenerEnabled, config.blocklist)
217
+
218
+ if (config.listenerEnabled && config.requestRebind) {
219
+ NotificationListener.requestListenerRebind(context)
220
+ }
221
+ }
222
+ }
@@ -119,7 +119,12 @@ export type PhoneNotificationEvent = {
119
119
  app: string;
120
120
  title: string;
121
121
  content: string;
122
- priority: string;
122
+ /**
123
+ * Android notification priority: PRIORITY_MIN (-2) through PRIORITY_MAX (2).
124
+ * Derived from the channel importance when the ranking is available, since
125
+ * Notification.priority reads 0 for channel-based notifications.
126
+ */
127
+ priority: number;
123
128
  timestamp: number;
124
129
  packageName: string;
125
130
  };
@@ -1 +1 @@
1
- {"version":3,"file":"Crust.types.d.ts","sourceRoot":"","sources":["../src/Crust.types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Crust.types.d.ts","sourceRoot":"","sources":["../src/Crust.types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"Crust.types.js","sourceRoot":"","sources":["../src/Crust.types.ts"],"names":[],"mappings":"","sourcesContent":["import type {StyleProp, ViewStyle} from \"react-native\"\n\nexport type OnLoadEventPayload = {\n url: string\n}\n\nexport type CrustModuleEvents = {\n onChange: (params: ChangeEventPayload) => void\n onNavManeuver: (params: NavManeuverPayload) => void\n onNavRerouting: (params: Record<string, never>) => void\n onNavArrived: (params: Record<string, never>) => void\n onNavError: (params: NavErrorPayload) => void\n onNavLocation: (params: NavLocationPayload) => void\n onNavRoute: (params: NavRoutePayload) => void\n onNavOffRoute: (params: NavOffRoutePayload) => void\n onHeading: (params: HeadingPayload) => void\n}\n\nexport type NavOffRoutePayload = {\n /** Approximate perpendicular distance in meters from the route. */\n offRouteDistanceMeters: number\n}\n\nexport type HeadingPayload = {\n /** Compass heading in degrees, 0 = north, 90 = east. */\n degrees: number\n}\n\nexport type NavRoutePayload = {\n points: Array<{lat: number; lng: number}>\n /**\n * Ordered step list along the route, when supplied by the host. Each\n * step is one navigable segment — typically one straight stretch of\n * road ending in a maneuver. Optional for back-compat with older\n * hosts that only ship the polyline. The SDK's pivot module consumes\n * this to enrich pivots with `fromRoad` / `toRoad` metadata.\n */\n steps?: NavRouteStepPayload[]\n}\n\nexport type NavRouteStepPayload = {\n /** Coordinate where this step begins (== end of the previous step). */\n lat: number\n lng: number\n /**\n * Index into `NavRoutePayload.points[]` where this step starts.\n * Lets consumers correlate step boundaries with polyline geometry.\n */\n routeIndex: number\n /** Name of the road traversed during this step. Null when the engine has no name. */\n road?: string | null\n /**\n * Categorical maneuver performed at the END of this step (i.e. how\n * the user exits this step). Same vocabulary as `NavManeuverPayload.maneuverType`.\n */\n maneuver: string\n /** Length of this step in meters. */\n distanceMeters: number\n}\n\nexport type NavLocationPayload = {\n lat: number\n lng: number\n /** Horizontal accuracy in meters, if reported by the platform. */\n accuracy: number | null\n /** Unix ms timestamp of the fix. */\n timestamp: number\n phone_notification: (event: PhoneNotificationEvent) => void\n phone_notification_dismissed: (event: PhoneNotificationDismissedEvent) => void\n captions_tester_incident: (event: CaptionsTesterIncidentEvent) => void\n}\n\nexport type ChangeEventPayload = {\n value: string\n}\n\nexport type NavManeuverPayload = {\n /**\n * Categorical type of the upcoming maneuver. One of: STRAIGHT,\n * SLIGHT_LEFT, SLIGHT_RIGHT, TURN_LEFT, TURN_RIGHT, SHARP_LEFT,\n * SHARP_RIGHT, U_TURN, ARRIVE.\n */\n maneuverType: string\n /** Distance in meters from the user's current position to that maneuver. -1 if unknown. */\n distanceMeters: number\n /** Road the user is currently on, per the Nav SDK. Null if unavailable. */\n fromRoad?: string | null\n /**\n * Legacy \"next road\" field — historically populated from the same\n * source as `fromRoad` (the current step's road, NOT the road\n * after the upcoming turn). Kept for back-compat. New consumers\n * should read `nextStepRoad` instead.\n */\n toRoad?: string | null\n /**\n * Road the user will be on AFTER the upcoming maneuver, sourced\n * from the Nav SDK's `remainingSteps[0]`. This is the value to\n * show as the \"next street\" headline on the maneuver card. Null\n * when the SDK hasn't surfaced a remaining step yet (final leg,\n * pre-first-NavInfo, etc.).\n */\n nextStepRoad?: string | null\n /** Total remaining distance to final destination, meters. -1 if unknown. */\n distanceToDestinationMeters?: number\n /** Total remaining travel time, seconds. -1 if unknown. */\n timeToDestinationSeconds?: number\n /** Current speed in m/s. Null if unavailable. */\n currentSpeedMps?: number | null\n /** Speed limit on the current road segment in m/s. Null if unknown / not regulated. */\n speedLimitMps?: number | null\n /** Bearing along the route at the user's current position, 0–360. Null if unknown. */\n routeHeadingDeg?: number | null\n}\n\nexport type NavErrorPayload = {\n message: string\n}\n\nexport type InstalledApp = {\n packageName: string\n appName: string\n isBlocked: boolean\n icon: string | null\n}\n\nexport type PhoneNotificationEvent = {\n notificationId: string\n app: string\n title: string\n content: string\n priority: string\n timestamp: number\n packageName: string\n}\n\nexport type PhoneNotificationDismissedEvent = {\n notificationKey: string\n packageName: string\n notificationId: string\n}\n\nexport type CaptionsTesterIncidentEvent = {\n action?: string\n timestamp?: number\n failure_code?: string\n failure_message?: string\n test_run_id?: string\n scenario_name?: string\n source?: string\n [key: string]: unknown\n}\n\nexport type CrustViewProps = {\n url: string\n onLoad: (event: {nativeEvent: OnLoadEventPayload}) => void\n style?: StyleProp<ViewStyle>\n}\n"]}
1
+ {"version":3,"file":"Crust.types.js","sourceRoot":"","sources":["../src/Crust.types.ts"],"names":[],"mappings":"","sourcesContent":["import type {StyleProp, ViewStyle} from \"react-native\"\n\nexport type OnLoadEventPayload = {\n url: string\n}\n\nexport type CrustModuleEvents = {\n onChange: (params: ChangeEventPayload) => void\n onNavManeuver: (params: NavManeuverPayload) => void\n onNavRerouting: (params: Record<string, never>) => void\n onNavArrived: (params: Record<string, never>) => void\n onNavError: (params: NavErrorPayload) => void\n onNavLocation: (params: NavLocationPayload) => void\n onNavRoute: (params: NavRoutePayload) => void\n onNavOffRoute: (params: NavOffRoutePayload) => void\n onHeading: (params: HeadingPayload) => void\n}\n\nexport type NavOffRoutePayload = {\n /** Approximate perpendicular distance in meters from the route. */\n offRouteDistanceMeters: number\n}\n\nexport type HeadingPayload = {\n /** Compass heading in degrees, 0 = north, 90 = east. */\n degrees: number\n}\n\nexport type NavRoutePayload = {\n points: Array<{lat: number; lng: number}>\n /**\n * Ordered step list along the route, when supplied by the host. Each\n * step is one navigable segment — typically one straight stretch of\n * road ending in a maneuver. Optional for back-compat with older\n * hosts that only ship the polyline. The SDK's pivot module consumes\n * this to enrich pivots with `fromRoad` / `toRoad` metadata.\n */\n steps?: NavRouteStepPayload[]\n}\n\nexport type NavRouteStepPayload = {\n /** Coordinate where this step begins (== end of the previous step). */\n lat: number\n lng: number\n /**\n * Index into `NavRoutePayload.points[]` where this step starts.\n * Lets consumers correlate step boundaries with polyline geometry.\n */\n routeIndex: number\n /** Name of the road traversed during this step. Null when the engine has no name. */\n road?: string | null\n /**\n * Categorical maneuver performed at the END of this step (i.e. how\n * the user exits this step). Same vocabulary as `NavManeuverPayload.maneuverType`.\n */\n maneuver: string\n /** Length of this step in meters. */\n distanceMeters: number\n}\n\nexport type NavLocationPayload = {\n lat: number\n lng: number\n /** Horizontal accuracy in meters, if reported by the platform. */\n accuracy: number | null\n /** Unix ms timestamp of the fix. */\n timestamp: number\n phone_notification: (event: PhoneNotificationEvent) => void\n phone_notification_dismissed: (event: PhoneNotificationDismissedEvent) => void\n captions_tester_incident: (event: CaptionsTesterIncidentEvent) => void\n}\n\nexport type ChangeEventPayload = {\n value: string\n}\n\nexport type NavManeuverPayload = {\n /**\n * Categorical type of the upcoming maneuver. One of: STRAIGHT,\n * SLIGHT_LEFT, SLIGHT_RIGHT, TURN_LEFT, TURN_RIGHT, SHARP_LEFT,\n * SHARP_RIGHT, U_TURN, ARRIVE.\n */\n maneuverType: string\n /** Distance in meters from the user's current position to that maneuver. -1 if unknown. */\n distanceMeters: number\n /** Road the user is currently on, per the Nav SDK. Null if unavailable. */\n fromRoad?: string | null\n /**\n * Legacy \"next road\" field — historically populated from the same\n * source as `fromRoad` (the current step's road, NOT the road\n * after the upcoming turn). Kept for back-compat. New consumers\n * should read `nextStepRoad` instead.\n */\n toRoad?: string | null\n /**\n * Road the user will be on AFTER the upcoming maneuver, sourced\n * from the Nav SDK's `remainingSteps[0]`. This is the value to\n * show as the \"next street\" headline on the maneuver card. Null\n * when the SDK hasn't surfaced a remaining step yet (final leg,\n * pre-first-NavInfo, etc.).\n */\n nextStepRoad?: string | null\n /** Total remaining distance to final destination, meters. -1 if unknown. */\n distanceToDestinationMeters?: number\n /** Total remaining travel time, seconds. -1 if unknown. */\n timeToDestinationSeconds?: number\n /** Current speed in m/s. Null if unavailable. */\n currentSpeedMps?: number | null\n /** Speed limit on the current road segment in m/s. Null if unknown / not regulated. */\n speedLimitMps?: number | null\n /** Bearing along the route at the user's current position, 0–360. Null if unknown. */\n routeHeadingDeg?: number | null\n}\n\nexport type NavErrorPayload = {\n message: string\n}\n\nexport type InstalledApp = {\n packageName: string\n appName: string\n isBlocked: boolean\n icon: string | null\n}\n\nexport type PhoneNotificationEvent = {\n notificationId: string\n app: string\n title: string\n content: string\n /**\n * Android notification priority: PRIORITY_MIN (-2) through PRIORITY_MAX (2).\n * Derived from the channel importance when the ranking is available, since\n * Notification.priority reads 0 for channel-based notifications.\n */\n priority: number\n timestamp: number\n packageName: string\n}\n\nexport type PhoneNotificationDismissedEvent = {\n notificationKey: string\n packageName: string\n notificationId: string\n}\n\nexport type CaptionsTesterIncidentEvent = {\n action?: string\n timestamp?: number\n failure_code?: string\n failure_message?: string\n test_run_id?: string\n scenario_name?: string\n source?: string\n [key: string]: unknown\n}\n\nexport type CrustViewProps = {\n url: string\n onLoad: (event: {nativeEvent: OnLoadEventPayload}) => void\n style?: StyleProp<ViewStyle>\n}\n"]}
@@ -21,10 +21,11 @@ declare class CrustModule extends NativeModule<CrustModuleEvents> {
21
21
  * per-app equivalent; system gestures are configured at the OS level).
22
22
  */
23
23
  setDeferredSystemGestures(edges: Array<"top" | "bottom" | "left" | "right" | "all">): Promise<void>;
24
- setNotificationConfig(enabled: boolean, blocklist: string[]): Promise<void>;
24
+ setNotificationConfig(listenerEnabled: boolean, blocklist: string[]): Promise<void>;
25
25
  getInstalledApps(): Promise<InstalledApp[]>;
26
26
  getInstalledAppsForNotifications(): Promise<InstalledApp[]>;
27
27
  hasNotificationListenerPermission(): Promise<boolean>;
28
+ refreshNotificationListener(): Promise<boolean>;
28
29
  openNotificationListenerSettings(): Promise<boolean>;
29
30
  isBetaBuild(): Promise<boolean>;
30
31
  showLocationServicesDialog(): Promise<boolean>;
@@ -1 +1 @@
1
- {"version":3,"file":"CrustModule.d.ts","sourceRoot":"","sources":["../src/CrustModule.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CrustModule.d.ts","sourceRoot":"","sources":["../src/CrustModule.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CrustModule.js","sourceRoot":"","sources":["../src/CrustModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,mBAAmB,EAAC,MAAM,MAAM,CAAA;AA6LtD,yDAAyD;AACzD,eAAe,mBAAmB,CAAc,OAAO,CAAC,CAAA","sourcesContent":["import {NativeModule, requireNativeModule} from \"expo\"\n\nimport {CrustModuleEvents, InstalledApp} from \"./Crust.types\"\n\ndeclare class CrustModule extends NativeModule<CrustModuleEvents> {\n PI: number\n hello(): string\n setValueAsync(value: string): Promise<void>\n nativeHttpRequest(\n method: string,\n url: string,\n headers: Record<string, string>,\n body?: string | null,\n ): Promise<{status: number; statusText: string; headers: Record<string, string>; body: string}>\n showAVRoutePicker(tintColor?: string | null): void\n\n /**\n * iOS: configure `preferredScreenEdgesDeferringSystemGestures`. When an\n * edge is deferred, the first swipe across that edge is consumed by the\n * app and the system gesture (Control Center, Notification Center, Home\n * indicator) only fires on a second swipe — i.e. a two-swipe-to-exit UX.\n *\n * Pass `[]` to restore default behavior. Android: no-op (Android has no\n * per-app equivalent; system gestures are configured at the OS level).\n */\n setDeferredSystemGestures(edges: Array<\"top\" | \"bottom\" | \"left\" | \"right\" | \"all\">): Promise<void>\n\n // MentraOS Notification Commands\n setNotificationConfig(enabled: boolean, blocklist: string[]): Promise<void>\n getInstalledApps(): Promise<InstalledApp[]>\n getInstalledAppsForNotifications(): Promise<InstalledApp[]>\n hasNotificationListenerPermission(): Promise<boolean>\n openNotificationListenerSettings(): Promise<boolean>\n isBetaBuild(): Promise<boolean>\n // location services commands\n showLocationServicesDialog(): Promise<boolean>\n isLocationServicesEnabled(): Promise<boolean>\n openLocationSettings(): Promise<boolean>\n openAppSettings(): Promise<boolean>\n openBluetoothSettings(): Promise<boolean>\n\n // Image Processing Commands\n processGalleryImage(\n inputPath: string,\n outputPath: string,\n options: {\n lensCorrection?: boolean\n colorCorrection?: boolean\n },\n ): Promise<{\n success: boolean\n outputPath?: string\n processingTimeMs?: number\n error?: string\n }>\n\n mergeHdrBrackets(\n underPath: string,\n normalPath: string,\n overPath: string,\n outputPath: string,\n ): Promise<{\n success: boolean\n outputPath?: string\n processingTimeMs?: number\n error?: string\n }>\n\n stabilizeVideo(\n inputPath: string,\n imuPath: string,\n outputPath: string,\n ): Promise<{\n success: boolean\n outputPath?: string\n processingTimeMs?: number\n error?: string\n }>\n\n // Media Library Commands\n saveToGalleryWithDate(\n filePath: string,\n captureTimeMillis?: number,\n displayName?: string,\n ): Promise<{\n success: boolean\n uri?: string\n identifier?: string\n existing?: boolean\n error?: string\n }>\n\n // Navigation (Android only — iOS stubs return error)\n startNavigation(\n lat: number,\n lng: number,\n options?: {\n simulate?: boolean\n speedMultiplier?: number\n /** Optional multi-stop list. When present takes precedence over lat/lng. Last entry is the final destination. */\n stops?: Array<{lat: number; lng: number}>\n /** \"walking\" | \"driving\" | \"cycling\" | \"two_wheeler\". Defaults driving. */\n mode?: string\n avoid?: {highways?: boolean; tolls?: boolean; ferries?: boolean}\n /** Force a reroute when the user is more than N meters past a pivot they didn't take. */\n missedTurnRerouteMeters?: number\n },\n ): Promise<{ok: boolean; error?: string}>\n stopNavigation(): Promise<{ok: boolean; error?: string}>\n\n /**\n * Show the Google Nav SDK Terms & Conditions dialog if not already\n * accepted. Idempotent — resolves immediately with `{accepted: true}`\n * when the user has already accepted (cached in-process / on-disk /\n * inside the SDK).\n */\n requestNavigationPermission(): Promise<{ok: boolean; accepted: boolean; error?: string}>\n\n /**\n * Dev-only: clear cached T&C acceptance (SDK flag + on-disk pref +\n * in-process flag) so the next requestNavigationPermission() re-shows\n * the dialog. Android only; iOS returns {ok: false, error: \"not\n * supported on iOS\"}.\n */\n resetNavigationPermission(): Promise<{ok: boolean; error?: string}>\n\n /**\n * Dev-only: nudge the simulated user position ~offsetMeters perpendicular\n * to the route so the Nav SDK reroutes. Default 20m. Android only.\n */\n simulateDeviation(offsetMeters?: number): Promise<{ok: boolean; error?: string}>\n\n /**\n * Dev toggle. When enabled, the native NavigationManager shifts every\n * reported location ~8m perpendicular to the route bearing, simulating\n * a pedestrian walking on the wrong sidewalk. Only meaningful in\n * simulate mode; lets us verify the SDK's along-path pivot trigger\n * fires even when the user never crosses the 7m pivot point radius.\n * Android-only today; iOS is a no-op stub.\n */\n setWrongSidewalkOffset(enabled: boolean): Promise<{ok: boolean; error?: string}>\n\n /**\n * Dev toggle. When enabled, the native NavigationManager takes over\n * from the Google simulator and walks the user along a modified\n * polyline that omits crossing micro-steps — reproducing the\n * wrong-sidewalk-then-missed-the-turn scenario. Android-only today;\n * iOS is a no-op stub.\n */\n setSkipCrossings(enabled: boolean): Promise<{ok: boolean; error?: string}>\n\n // Heading / compass (Android only)\n startHeading(): Promise<{ok: boolean; error?: string}>\n stopHeading(): Promise<{ok: boolean; error?: string}>\n\n // MentraJS Runtime — per-miniapp JSContext lifecycle.\n /**\n * Spawn a per-miniapp JS context. Re-spawn is allowed: a live context\n * with the same packageName is killed first. Returns true if the\n * polyfill bundle + miniapp source evaluated without throwing.\n *\n * The polyfill bundle is `mobile/modules/mentrajs-runtime/dist/startup.js`\n * (shipped inside the host binary). It installs window-style globals\n * (console, timers, fetch, localStorage, crypto) atop the JSC runtime.\n */\n mentraJsSpawn(packageName: string, polyfillBundle: string, miniappJs: string): Promise<boolean>\n mentraJsEvaluate(packageName: string, source: string): Promise<unknown>\n mentraJsKill(packageName: string): Promise<void>\n /**\n * Push a `{kind: \"event\"|\"response\", …}` envelope into the named\n * context's globalThis.__deliver. Returns when the underlying\n * evaluateScript completes (the JS handler runs synchronously on the\n * context's queue).\n */\n mentraJsDispatchToJs(packageName: string, envelope: Record<string, unknown>): Promise<void>\n mentraJsSetManifest(packageName: string, permissions: string[]): Promise<void>\n /** Diagnostic — returns the packageNames of every live JSContext. */\n mentraJsAlivePackages(): string[]\n /** Diagnostic — force a GC cycle on the named context. */\n mentraJsDebugForceGC(packageName: string): Promise<boolean>\n /**\n * Read the bundled MentraJS polyfill (startup.js) shipped inside the\n * host binary. Synchronous — host RN code calls this once on app boot\n * and caches the string, then passes it to every mentraJsSpawn so\n * every JSContext starts with the same polyfill ABI.\n */\n mentraJsLoadPolyfillBundle(): string\n}\n\n// This call loads the native module object from the JSI.\nexport default requireNativeModule<CrustModule>(\"Crust\")\n"]}
1
+ {"version":3,"file":"CrustModule.js","sourceRoot":"","sources":["../src/CrustModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,mBAAmB,EAAC,MAAM,MAAM,CAAA;AA8LtD,yDAAyD;AACzD,eAAe,mBAAmB,CAAc,OAAO,CAAC,CAAA","sourcesContent":["import {NativeModule, requireNativeModule} from \"expo\"\n\nimport {CrustModuleEvents, InstalledApp} from \"./Crust.types\"\n\ndeclare class CrustModule extends NativeModule<CrustModuleEvents> {\n PI: number\n hello(): string\n setValueAsync(value: string): Promise<void>\n nativeHttpRequest(\n method: string,\n url: string,\n headers: Record<string, string>,\n body?: string | null,\n ): Promise<{status: number; statusText: string; headers: Record<string, string>; body: string}>\n showAVRoutePicker(tintColor?: string | null): void\n\n /**\n * iOS: configure `preferredScreenEdgesDeferringSystemGestures`. When an\n * edge is deferred, the first swipe across that edge is consumed by the\n * app and the system gesture (Control Center, Notification Center, Home\n * indicator) only fires on a second swipe — i.e. a two-swipe-to-exit UX.\n *\n * Pass `[]` to restore default behavior. Android: no-op (Android has no\n * per-app equivalent; system gestures are configured at the OS level).\n */\n setDeferredSystemGestures(edges: Array<\"top\" | \"bottom\" | \"left\" | \"right\" | \"all\">): Promise<void>\n\n // MentraOS Notification Commands\n setNotificationConfig(listenerEnabled: boolean, blocklist: string[]): Promise<void>\n getInstalledApps(): Promise<InstalledApp[]>\n getInstalledAppsForNotifications(): Promise<InstalledApp[]>\n hasNotificationListenerPermission(): Promise<boolean>\n refreshNotificationListener(): Promise<boolean>\n openNotificationListenerSettings(): Promise<boolean>\n isBetaBuild(): Promise<boolean>\n // location services commands\n showLocationServicesDialog(): Promise<boolean>\n isLocationServicesEnabled(): Promise<boolean>\n openLocationSettings(): Promise<boolean>\n openAppSettings(): Promise<boolean>\n openBluetoothSettings(): Promise<boolean>\n\n // Image Processing Commands\n processGalleryImage(\n inputPath: string,\n outputPath: string,\n options: {\n lensCorrection?: boolean\n colorCorrection?: boolean\n },\n ): Promise<{\n success: boolean\n outputPath?: string\n processingTimeMs?: number\n error?: string\n }>\n\n mergeHdrBrackets(\n underPath: string,\n normalPath: string,\n overPath: string,\n outputPath: string,\n ): Promise<{\n success: boolean\n outputPath?: string\n processingTimeMs?: number\n error?: string\n }>\n\n stabilizeVideo(\n inputPath: string,\n imuPath: string,\n outputPath: string,\n ): Promise<{\n success: boolean\n outputPath?: string\n processingTimeMs?: number\n error?: string\n }>\n\n // Media Library Commands\n saveToGalleryWithDate(\n filePath: string,\n captureTimeMillis?: number,\n displayName?: string,\n ): Promise<{\n success: boolean\n uri?: string\n identifier?: string\n existing?: boolean\n error?: string\n }>\n\n // Navigation (Android only — iOS stubs return error)\n startNavigation(\n lat: number,\n lng: number,\n options?: {\n simulate?: boolean\n speedMultiplier?: number\n /** Optional multi-stop list. When present takes precedence over lat/lng. Last entry is the final destination. */\n stops?: Array<{lat: number; lng: number}>\n /** \"walking\" | \"driving\" | \"cycling\" | \"two_wheeler\". Defaults driving. */\n mode?: string\n avoid?: {highways?: boolean; tolls?: boolean; ferries?: boolean}\n /** Force a reroute when the user is more than N meters past a pivot they didn't take. */\n missedTurnRerouteMeters?: number\n },\n ): Promise<{ok: boolean; error?: string}>\n stopNavigation(): Promise<{ok: boolean; error?: string}>\n\n /**\n * Show the Google Nav SDK Terms & Conditions dialog if not already\n * accepted. Idempotent — resolves immediately with `{accepted: true}`\n * when the user has already accepted (cached in-process / on-disk /\n * inside the SDK).\n */\n requestNavigationPermission(): Promise<{ok: boolean; accepted: boolean; error?: string}>\n\n /**\n * Dev-only: clear cached T&C acceptance (SDK flag + on-disk pref +\n * in-process flag) so the next requestNavigationPermission() re-shows\n * the dialog. Android only; iOS returns {ok: false, error: \"not\n * supported on iOS\"}.\n */\n resetNavigationPermission(): Promise<{ok: boolean; error?: string}>\n\n /**\n * Dev-only: nudge the simulated user position ~offsetMeters perpendicular\n * to the route so the Nav SDK reroutes. Default 20m. Android only.\n */\n simulateDeviation(offsetMeters?: number): Promise<{ok: boolean; error?: string}>\n\n /**\n * Dev toggle. When enabled, the native NavigationManager shifts every\n * reported location ~8m perpendicular to the route bearing, simulating\n * a pedestrian walking on the wrong sidewalk. Only meaningful in\n * simulate mode; lets us verify the SDK's along-path pivot trigger\n * fires even when the user never crosses the 7m pivot point radius.\n * Android-only today; iOS is a no-op stub.\n */\n setWrongSidewalkOffset(enabled: boolean): Promise<{ok: boolean; error?: string}>\n\n /**\n * Dev toggle. When enabled, the native NavigationManager takes over\n * from the Google simulator and walks the user along a modified\n * polyline that omits crossing micro-steps — reproducing the\n * wrong-sidewalk-then-missed-the-turn scenario. Android-only today;\n * iOS is a no-op stub.\n */\n setSkipCrossings(enabled: boolean): Promise<{ok: boolean; error?: string}>\n\n // Heading / compass (Android only)\n startHeading(): Promise<{ok: boolean; error?: string}>\n stopHeading(): Promise<{ok: boolean; error?: string}>\n\n // MentraJS Runtime — per-miniapp JSContext lifecycle.\n /**\n * Spawn a per-miniapp JS context. Re-spawn is allowed: a live context\n * with the same packageName is killed first. Returns true if the\n * polyfill bundle + miniapp source evaluated without throwing.\n *\n * The polyfill bundle is `mobile/modules/mentrajs-runtime/dist/startup.js`\n * (shipped inside the host binary). It installs window-style globals\n * (console, timers, fetch, localStorage, crypto) atop the JSC runtime.\n */\n mentraJsSpawn(packageName: string, polyfillBundle: string, miniappJs: string): Promise<boolean>\n mentraJsEvaluate(packageName: string, source: string): Promise<unknown>\n mentraJsKill(packageName: string): Promise<void>\n /**\n * Push a `{kind: \"event\"|\"response\", …}` envelope into the named\n * context's globalThis.__deliver. Returns when the underlying\n * evaluateScript completes (the JS handler runs synchronously on the\n * context's queue).\n */\n mentraJsDispatchToJs(packageName: string, envelope: Record<string, unknown>): Promise<void>\n mentraJsSetManifest(packageName: string, permissions: string[]): Promise<void>\n /** Diagnostic — returns the packageNames of every live JSContext. */\n mentraJsAlivePackages(): string[]\n /** Diagnostic — force a GC cycle on the named context. */\n mentraJsDebugForceGC(packageName: string): Promise<boolean>\n /**\n * Read the bundled MentraJS polyfill (startup.js) shipped inside the\n * host binary. Synchronous — host RN code calls this once on app boot\n * and caches the string, then passes it to every mentraJsSpawn so\n * every JSContext starts with the same polyfill ABI.\n */\n mentraJsLoadPolyfillBundle(): string\n}\n\n// This call loads the native module object from the JSI.\nexport default requireNativeModule<CrustModule>(\"Crust\")\n"]}
@@ -14,10 +14,11 @@ declare class CrustModule extends NativeModule<CrustModuleEvents> {
14
14
  hello(): string;
15
15
  showAVRoutePicker(_tintColor?: string | null): void;
16
16
  setDeferredSystemGestures(_edges: string[]): Promise<void>;
17
- setNotificationConfig(_enabled: boolean, _blocklist: string[]): Promise<void>;
17
+ setNotificationConfig(_listenerEnabled: boolean, _blocklist: string[]): Promise<void>;
18
18
  getInstalledApps(): Promise<never[]>;
19
19
  getInstalledAppsForNotifications(): Promise<never[]>;
20
20
  hasNotificationListenerPermission(): Promise<boolean>;
21
+ refreshNotificationListener(): Promise<boolean>;
21
22
  openNotificationListenerSettings(): Promise<boolean>;
22
23
  isBetaBuild(): Promise<boolean>;
23
24
  mentraJsSpawn(_pkg: string, _polyfill: string, _miniappJs: string): Promise<boolean>;
@@ -1 +1 @@
1
- {"version":3,"file":"CrustModule.web.d.ts","sourceRoot":"","sources":["../src/CrustModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,YAAY,EAAC,MAAM,MAAM,CAAA;AAEpD,OAAO,EAAC,iBAAiB,EAAC,MAAM,eAAe,CAAA;AAE/C,cAAM,WAAY,SAAQ,YAAY,CAAC,iBAAiB,CAAC;IACvD,EAAE,SAAU;IACN,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAG3C,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;;;;;;;;IAS1G,KAAK;IAGL,iBAAiB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI;IACtC,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAC1D,qBAAqB,CAAC,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAC7E,gBAAgB;IAGhB,gCAAgC;IAGhC,iCAAiC;IAGjC,gCAAgC;IAGhC,WAAW;IAGX,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;IAGjE,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;IAG3C,YAAY,CAAC,IAAI,EAAE,MAAM;IAGzB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAGhE,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE;IAGxD,qBAAqB;IAGf,oBAAoB,CAAC,IAAI,EAAE,MAAM;IAGvC,0BAA0B;CAG3B;;AAED,wBAA4D"}
1
+ {"version":3,"file":"CrustModule.web.d.ts","sourceRoot":"","sources":["../src/CrustModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,YAAY,EAAC,MAAM,MAAM,CAAA;AAEpD,OAAO,EAAC,iBAAiB,EAAC,MAAM,eAAe,CAAA;AAE/C,cAAM,WAAY,SAAQ,YAAY,CAAC,iBAAiB,CAAC;IACvD,EAAE,SAAU;IACN,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAG3C,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;;;;;;;;IAS1G,KAAK;IAGL,iBAAiB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI;IACtC,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAC1D,qBAAqB,CAAC,gBAAgB,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IACrF,gBAAgB;IAGhB,gCAAgC;IAGhC,iCAAiC;IAGjC,2BAA2B;IAG3B,gCAAgC;IAGhC,WAAW;IAGX,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;IAGjE,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;IAG3C,YAAY,CAAC,IAAI,EAAE,MAAM;IAGzB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAGhE,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE;IAGxD,qBAAqB;IAGf,oBAAoB,CAAC,IAAI,EAAE,MAAM;IAGvC,0BAA0B;CAG3B;;AAED,wBAA4D"}
@@ -18,7 +18,7 @@ class CrustModule extends NativeModule {
18
18
  }
19
19
  showAVRoutePicker(_tintColor) { }
20
20
  async setDeferredSystemGestures(_edges) { }
21
- async setNotificationConfig(_enabled, _blocklist) { }
21
+ async setNotificationConfig(_listenerEnabled, _blocklist) { }
22
22
  async getInstalledApps() {
23
23
  return [];
24
24
  }
@@ -28,6 +28,9 @@ class CrustModule extends NativeModule {
28
28
  async hasNotificationListenerPermission() {
29
29
  return false;
30
30
  }
31
+ async refreshNotificationListener() {
32
+ return false;
33
+ }
31
34
  async openNotificationListenerSettings() {
32
35
  return false;
33
36
  }
@@ -1 +1 @@
1
- {"version":3,"file":"CrustModule.web.js","sourceRoot":"","sources":["../src/CrustModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,iBAAiB,EAAE,YAAY,EAAC,MAAM,MAAM,CAAA;AAIpD,MAAM,WAAY,SAAQ,YAA+B;IACvD,EAAE,GAAG,IAAI,CAAC,EAAE,CAAA;IACZ,KAAK,CAAC,aAAa,CAAC,KAAa;QAC/B,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAC,KAAK,EAAC,CAAC,CAAA;IAChC,CAAC;IACD,KAAK,CAAC,iBAAiB,CAAC,MAAc,EAAE,GAAW,EAAE,OAA+B,EAAE,IAAoB;QACxG,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAC,CAAC,CAAA;QAC1D,OAAO;YACL,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;YAC/B,OAAO,EAAE,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YACvD,IAAI,EAAE,MAAM,QAAQ,CAAC,IAAI,EAAE;SAC5B,CAAA;IACH,CAAC;IACD,KAAK;QACH,OAAO,iBAAiB,CAAA;IAC1B,CAAC;IACD,iBAAiB,CAAC,UAA0B,IAAG,CAAC;IAChD,KAAK,CAAC,yBAAyB,CAAC,MAAgB,IAAkB,CAAC;IACnE,KAAK,CAAC,qBAAqB,CAAC,QAAiB,EAAE,UAAoB,IAAkB,CAAC;IACtF,KAAK,CAAC,gBAAgB;QACpB,OAAO,EAAE,CAAA;IACX,CAAC;IACD,KAAK,CAAC,gCAAgC;QACpC,OAAO,EAAE,CAAA;IACX,CAAC;IACD,KAAK,CAAC,iCAAiC;QACrC,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,CAAC,gCAAgC;QACpC,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,CAAC,WAAW;QACf,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,CAAC,aAAa,CAAC,IAAY,EAAE,SAAiB,EAAE,UAAkB;QACrE,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,CAAC,gBAAgB,CAAC,IAAY,EAAE,IAAY;QAC/C,OAAO,IAAI,CAAA;IACb,CAAC;IACD,KAAK,CAAC,YAAY,CAAC,IAAY;QAC7B,OAAM;IACR,CAAC;IACD,KAAK,CAAC,oBAAoB,CAAC,IAAY,EAAE,IAA6B;QACpE,OAAM;IACR,CAAC;IACD,KAAK,CAAC,mBAAmB,CAAC,IAAY,EAAE,MAAgB;QACtD,OAAM;IACR,CAAC;IACD,qBAAqB;QACnB,OAAO,EAAE,CAAA;IACX,CAAC;IACD,KAAK,CAAC,oBAAoB,CAAC,IAAY;QACrC,OAAO,KAAK,CAAA;IACd,CAAC;IACD,0BAA0B;QACxB,OAAO,EAAE,CAAA;IACX,CAAC;CACF;AAED,eAAe,iBAAiB,CAAC,WAAW,EAAE,aAAa,CAAC,CAAA","sourcesContent":["import {registerWebModule, NativeModule} from \"expo\"\n\nimport {CrustModuleEvents} from \"./Crust.types\"\n\nclass CrustModule extends NativeModule<CrustModuleEvents> {\n PI = Math.PI\n async setValueAsync(value: string): Promise<void> {\n this.emit(\"onChange\", {value})\n }\n async nativeHttpRequest(method: string, url: string, headers: Record<string, string>, body?: string | null) {\n const response = await fetch(url, {method, headers, body})\n return {\n status: response.status,\n statusText: response.statusText,\n headers: Object.fromEntries(response.headers.entries()),\n body: await response.text(),\n }\n }\n hello() {\n return \"Hello world! 👋\"\n }\n showAVRoutePicker(_tintColor?: string | null) {}\n async setDeferredSystemGestures(_edges: string[]): Promise<void> {}\n async setNotificationConfig(_enabled: boolean, _blocklist: string[]): Promise<void> {}\n async getInstalledApps() {\n return []\n }\n async getInstalledAppsForNotifications() {\n return []\n }\n async hasNotificationListenerPermission() {\n return false\n }\n async openNotificationListenerSettings() {\n return false\n }\n async isBetaBuild() {\n return false\n }\n async mentraJsSpawn(_pkg: string, _polyfill: string, _miniappJs: string) {\n return false\n }\n async mentraJsEvaluate(_pkg: string, _src: string) {\n return null\n }\n async mentraJsKill(_pkg: string) {\n return\n }\n async mentraJsDispatchToJs(_pkg: string, _env: Record<string, unknown>) {\n return\n }\n async mentraJsSetManifest(_pkg: string, _perms: string[]) {\n return\n }\n mentraJsAlivePackages() {\n return []\n }\n async mentraJsDebugForceGC(_pkg: string) {\n return false\n }\n mentraJsLoadPolyfillBundle() {\n return \"\"\n }\n}\n\nexport default registerWebModule(CrustModule, \"CrustModule\")\n"]}
1
+ {"version":3,"file":"CrustModule.web.js","sourceRoot":"","sources":["../src/CrustModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,iBAAiB,EAAE,YAAY,EAAC,MAAM,MAAM,CAAA;AAIpD,MAAM,WAAY,SAAQ,YAA+B;IACvD,EAAE,GAAG,IAAI,CAAC,EAAE,CAAA;IACZ,KAAK,CAAC,aAAa,CAAC,KAAa;QAC/B,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAC,KAAK,EAAC,CAAC,CAAA;IAChC,CAAC;IACD,KAAK,CAAC,iBAAiB,CAAC,MAAc,EAAE,GAAW,EAAE,OAA+B,EAAE,IAAoB;QACxG,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAC,CAAC,CAAA;QAC1D,OAAO;YACL,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;YAC/B,OAAO,EAAE,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YACvD,IAAI,EAAE,MAAM,QAAQ,CAAC,IAAI,EAAE;SAC5B,CAAA;IACH,CAAC;IACD,KAAK;QACH,OAAO,iBAAiB,CAAA;IAC1B,CAAC;IACD,iBAAiB,CAAC,UAA0B,IAAG,CAAC;IAChD,KAAK,CAAC,yBAAyB,CAAC,MAAgB,IAAkB,CAAC;IACnE,KAAK,CAAC,qBAAqB,CAAC,gBAAyB,EAAE,UAAoB,IAAkB,CAAC;IAC9F,KAAK,CAAC,gBAAgB;QACpB,OAAO,EAAE,CAAA;IACX,CAAC;IACD,KAAK,CAAC,gCAAgC;QACpC,OAAO,EAAE,CAAA;IACX,CAAC;IACD,KAAK,CAAC,iCAAiC;QACrC,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,CAAC,2BAA2B;QAC/B,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,CAAC,gCAAgC;QACpC,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,CAAC,WAAW;QACf,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,CAAC,aAAa,CAAC,IAAY,EAAE,SAAiB,EAAE,UAAkB;QACrE,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,CAAC,gBAAgB,CAAC,IAAY,EAAE,IAAY;QAC/C,OAAO,IAAI,CAAA;IACb,CAAC;IACD,KAAK,CAAC,YAAY,CAAC,IAAY;QAC7B,OAAM;IACR,CAAC;IACD,KAAK,CAAC,oBAAoB,CAAC,IAAY,EAAE,IAA6B;QACpE,OAAM;IACR,CAAC;IACD,KAAK,CAAC,mBAAmB,CAAC,IAAY,EAAE,MAAgB;QACtD,OAAM;IACR,CAAC;IACD,qBAAqB;QACnB,OAAO,EAAE,CAAA;IACX,CAAC;IACD,KAAK,CAAC,oBAAoB,CAAC,IAAY;QACrC,OAAO,KAAK,CAAA;IACd,CAAC;IACD,0BAA0B;QACxB,OAAO,EAAE,CAAA;IACX,CAAC;CACF;AAED,eAAe,iBAAiB,CAAC,WAAW,EAAE,aAAa,CAAC,CAAA","sourcesContent":["import {registerWebModule, NativeModule} from \"expo\"\n\nimport {CrustModuleEvents} from \"./Crust.types\"\n\nclass CrustModule extends NativeModule<CrustModuleEvents> {\n PI = Math.PI\n async setValueAsync(value: string): Promise<void> {\n this.emit(\"onChange\", {value})\n }\n async nativeHttpRequest(method: string, url: string, headers: Record<string, string>, body?: string | null) {\n const response = await fetch(url, {method, headers, body})\n return {\n status: response.status,\n statusText: response.statusText,\n headers: Object.fromEntries(response.headers.entries()),\n body: await response.text(),\n }\n }\n hello() {\n return \"Hello world! 👋\"\n }\n showAVRoutePicker(_tintColor?: string | null) {}\n async setDeferredSystemGestures(_edges: string[]): Promise<void> {}\n async setNotificationConfig(_listenerEnabled: boolean, _blocklist: string[]): Promise<void> {}\n async getInstalledApps() {\n return []\n }\n async getInstalledAppsForNotifications() {\n return []\n }\n async hasNotificationListenerPermission() {\n return false\n }\n async refreshNotificationListener() {\n return false\n }\n async openNotificationListenerSettings() {\n return false\n }\n async isBetaBuild() {\n return false\n }\n async mentraJsSpawn(_pkg: string, _polyfill: string, _miniappJs: string) {\n return false\n }\n async mentraJsEvaluate(_pkg: string, _src: string) {\n return null\n }\n async mentraJsKill(_pkg: string) {\n return\n }\n async mentraJsDispatchToJs(_pkg: string, _env: Record<string, unknown>) {\n return\n }\n async mentraJsSetManifest(_pkg: string, _perms: string[]) {\n return\n }\n mentraJsAlivePackages() {\n return []\n }\n async mentraJsDebugForceGC(_pkg: string) {\n return false\n }\n mentraJsLoadPolyfillBundle() {\n return \"\"\n }\n}\n\nexport default registerWebModule(CrustModule, \"CrustModule\")\n"]}
@@ -259,6 +259,10 @@ public class CrustModule: Module {
259
259
  return false
260
260
  }
261
261
 
262
+ AsyncFunction("refreshNotificationListener") { () -> Bool in
263
+ return false
264
+ }
265
+
262
266
  AsyncFunction("openNotificationListenerSettings") { () -> Bool in
263
267
  return false
264
268
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/crust",
3
- "version": "0.1.0-dev.1",
3
+ "version": "3.1.0-dev.10",
4
4
  "description": "Mentra Native Module",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -23,7 +23,11 @@
23
23
  "crust",
24
24
  "Crust"
25
25
  ],
26
- "repository": "https://github.com/fossephate/crust",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/Mentra-Community/MentraOS.git",
29
+ "directory": "mobile/modules/crust"
30
+ },
27
31
  "bugs": {
28
32
  "url": "https://github.com/fossephate/crust/issues"
29
33
  },
@@ -31,7 +35,7 @@
31
35
  "license": "MIT",
32
36
  "homepage": "https://github.com/fossephate/crust#readme",
33
37
  "dependencies": {
34
- "@mentra/jspolyfill": "^0.1.0-dev.0"
38
+ "@mentra/jspolyfill": "3.1.0-dev.10"
35
39
  },
36
40
  "devDependencies": {
37
41
  "@expo/config-plugins": ">=8.0.0",
@@ -14,6 +14,8 @@ import { type ConfigPlugin } from "expo/config-plugins";
14
14
  * both fails the release build with duplicate classes)
15
15
  * - core-library desugaring (crust's AAR metadata requires it of embedding
16
16
  * apps — the Nav SDK uses newer core libs)
17
+ * - a generated MainApplication process guard so the notification-listener
18
+ * process never initializes React Native
17
19
  */
18
20
  declare const withCrust: ConfigPlugin;
19
21
  export default withCrust;
@@ -16,6 +16,8 @@ const withAndroid_1 = require("./withAndroid");
16
16
  * both fails the release build with duplicate classes)
17
17
  * - core-library desugaring (crust's AAR metadata requires it of embedding
18
18
  * apps — the Nav SDK uses newer core libs)
19
+ * - a generated MainApplication process guard so the notification-listener
20
+ * process never initializes React Native
19
21
  */
20
22
  const withCrust = (config) => {
21
23
  return (0, withAndroid_1.withCrustAndroidBuildContract)(config);
@@ -18,6 +18,8 @@ const MAPBOX_REPO = [
18
18
  " }",
19
19
  ].join("\n");
20
20
  const PROTOBUF_EXCLUDE = "exclude group: 'com.google.protobuf', module: 'protobuf-javalite'";
21
+ const NOTIFICATION_PROCESS_GUARD = "crust: skip React Native in notification process";
22
+ const NOTIFICATION_CONFIG_GUARD = "crust: skip Expo lifecycle in notification process";
21
23
  function withCrustProjectGradle(config) {
22
24
  return (0, config_plugins_1.withProjectBuildGradle)(config, (cfg) => {
23
25
  let gradle = cfg.modResults.contents;
@@ -71,8 +73,61 @@ function withCrustAppGradle(config) {
71
73
  return cfg;
72
74
  });
73
75
  }
76
+ function withNotificationProcessGuard(config) {
77
+ return (0, config_plugins_1.withMainApplication)(config, (cfg) => {
78
+ let source = cfg.modResults.contents;
79
+ const isJava = cfg.modResults.language === "java";
80
+ if (!source.includes(NOTIFICATION_PROCESS_GUARD)) {
81
+ const pattern = isJava
82
+ ? /public void onCreate\(\)\s*\{\s*super\.onCreate\(\);/
83
+ : /override fun onCreate\(\)\s*\{\s*super\.onCreate\(\)/;
84
+ const replacement = isJava
85
+ ? `public void onCreate() {
86
+ super.onCreate();
87
+ // ${NOTIFICATION_PROCESS_GUARD}
88
+ if (com.mentra.crust.services.NotificationProcess.isCurrent(this)) {
89
+ return;
90
+ }`
91
+ : `override fun onCreate() {
92
+ super.onCreate()
93
+ // ${NOTIFICATION_PROCESS_GUARD}
94
+ if (com.mentra.crust.services.NotificationProcess.isCurrent(this)) {
95
+ return
96
+ }`;
97
+ if (!pattern.test(source)) {
98
+ throw new Error("@mentra/crust could not add the notification-process guard to MainApplication");
99
+ }
100
+ source = source.replace(pattern, replacement);
101
+ }
102
+ if (!source.includes(NOTIFICATION_CONFIG_GUARD)) {
103
+ const pattern = isJava
104
+ ? /public void onConfigurationChanged\(Configuration newConfig\)\s*\{\s*super\.onConfigurationChanged\(newConfig\);/
105
+ : /override fun onConfigurationChanged\(newConfig: Configuration\)\s*\{\s*super\.onConfigurationChanged\(newConfig\)/;
106
+ const replacement = isJava
107
+ ? `public void onConfigurationChanged(Configuration newConfig) {
108
+ super.onConfigurationChanged(newConfig);
109
+ // ${NOTIFICATION_CONFIG_GUARD}
110
+ if (com.mentra.crust.services.NotificationProcess.isCurrent(this)) {
111
+ return;
112
+ }`
113
+ : `override fun onConfigurationChanged(newConfig: Configuration) {
114
+ super.onConfigurationChanged(newConfig)
115
+ // ${NOTIFICATION_CONFIG_GUARD}
116
+ if (com.mentra.crust.services.NotificationProcess.isCurrent(this)) {
117
+ return
118
+ }`;
119
+ // Some custom hosts do not override onConfigurationChanged. In that case
120
+ // there is no Expo lifecycle callback to guard.
121
+ if (pattern.test(source))
122
+ source = source.replace(pattern, replacement);
123
+ }
124
+ cfg.modResults.contents = source;
125
+ return cfg;
126
+ });
127
+ }
74
128
  const withCrustAndroidBuildContract = (config) => {
75
129
  config = withCrustProjectGradle(config);
76
- return withCrustAppGradle(config);
130
+ config = withCrustAppGradle(config);
131
+ return withNotificationProcessGuard(config);
77
132
  };
78
133
  exports.withCrustAndroidBuildContract = withCrustAndroidBuildContract;
@@ -128,7 +128,12 @@ export type PhoneNotificationEvent = {
128
128
  app: string
129
129
  title: string
130
130
  content: string
131
- priority: string
131
+ /**
132
+ * Android notification priority: PRIORITY_MIN (-2) through PRIORITY_MAX (2).
133
+ * Derived from the channel importance when the ranking is available, since
134
+ * Notification.priority reads 0 for channel-based notifications.
135
+ */
136
+ priority: number
132
137
  timestamp: number
133
138
  packageName: string
134
139
  }
@@ -26,10 +26,11 @@ declare class CrustModule extends NativeModule<CrustModuleEvents> {
26
26
  setDeferredSystemGestures(edges: Array<"top" | "bottom" | "left" | "right" | "all">): Promise<void>
27
27
 
28
28
  // MentraOS Notification Commands
29
- setNotificationConfig(enabled: boolean, blocklist: string[]): Promise<void>
29
+ setNotificationConfig(listenerEnabled: boolean, blocklist: string[]): Promise<void>
30
30
  getInstalledApps(): Promise<InstalledApp[]>
31
31
  getInstalledAppsForNotifications(): Promise<InstalledApp[]>
32
32
  hasNotificationListenerPermission(): Promise<boolean>
33
+ refreshNotificationListener(): Promise<boolean>
33
34
  openNotificationListenerSettings(): Promise<boolean>
34
35
  isBetaBuild(): Promise<boolean>
35
36
  // location services commands
@@ -21,7 +21,7 @@ class CrustModule extends NativeModule<CrustModuleEvents> {
21
21
  }
22
22
  showAVRoutePicker(_tintColor?: string | null) {}
23
23
  async setDeferredSystemGestures(_edges: string[]): Promise<void> {}
24
- async setNotificationConfig(_enabled: boolean, _blocklist: string[]): Promise<void> {}
24
+ async setNotificationConfig(_listenerEnabled: boolean, _blocklist: string[]): Promise<void> {}
25
25
  async getInstalledApps() {
26
26
  return []
27
27
  }
@@ -31,6 +31,9 @@ class CrustModule extends NativeModule<CrustModuleEvents> {
31
31
  async hasNotificationListenerPermission() {
32
32
  return false
33
33
  }
34
+ async refreshNotificationListener() {
35
+ return false
36
+ }
34
37
  async openNotificationListenerSettings() {
35
38
  return false
36
39
  }