@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.
- package/android/src/main/AndroidManifest.xml +14 -0
- package/android/src/main/java/com/mentra/crust/CrustModule.kt +69 -25
- package/android/src/main/java/com/mentra/crust/navigation/NavigationManager.kt +2 -0
- package/android/src/main/java/com/mentra/crust/services/NotificationListener.kt +273 -90
- package/android/src/main/java/com/mentra/crust/services/NotificationProcessBridge.kt +222 -0
- package/build/Crust.types.d.ts +6 -1
- package/build/Crust.types.d.ts.map +1 -1
- package/build/Crust.types.js.map +1 -1
- package/build/CrustModule.d.ts +2 -1
- package/build/CrustModule.d.ts.map +1 -1
- package/build/CrustModule.js.map +1 -1
- package/build/CrustModule.web.d.ts +2 -1
- package/build/CrustModule.web.d.ts.map +1 -1
- package/build/CrustModule.web.js +4 -1
- package/build/CrustModule.web.js.map +1 -1
- package/ios/CrustModule.swift +4 -0
- package/package.json +7 -3
- package/plugin/build/index.d.ts +2 -0
- package/plugin/build/index.js +2 -0
- package/plugin/build/withAndroid.js +56 -1
- package/src/Crust.types.ts +6 -1
- package/src/CrustModule.ts +2 -1
- package/src/CrustModule.web.ts +4 -1
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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") {
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
311
|
-
|
|
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.
|
|
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
|
+
}
|
package/build/Crust.types.d.ts
CHANGED
|
@@ -119,7 +119,12 @@ export type PhoneNotificationEvent = {
|
|
|
119
119
|
app: string;
|
|
120
120
|
title: string;
|
|
121
121
|
content: string;
|
|
122
|
-
|
|
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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"}
|
package/build/Crust.types.js.map
CHANGED
|
@@ -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:
|
|
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"]}
|
package/build/CrustModule.d.ts
CHANGED
|
@@ -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(
|
|
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":"AAAA,OAAO,EAAC,YAAY,EAAsB,MAAM,MAAM,CAAA;AAEtD,OAAO,EAAC,iBAAiB,EAAE,YAAY,EAAC,MAAM,eAAe,CAAA;AAE7D,OAAO,OAAO,WAAY,SAAQ,YAAY,CAAC,iBAAiB,CAAC;IAC/D,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,IAAI,MAAM;IACf,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3C,iBAAiB,CACf,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,GACnB,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,CAAC;IAC/F,iBAAiB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAElD;;;;;;;;OAQG;IACH,yBAAyB,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAGnG,qBAAqB,CAAC,
|
|
1
|
+
{"version":3,"file":"CrustModule.d.ts","sourceRoot":"","sources":["../src/CrustModule.ts"],"names":[],"mappings":"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"}
|
package/build/CrustModule.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CrustModule.js","sourceRoot":"","sources":["../src/CrustModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,mBAAmB,EAAC,MAAM,MAAM,CAAA;
|
|
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(
|
|
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,
|
|
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"}
|
package/build/CrustModule.web.js
CHANGED
|
@@ -18,7 +18,7 @@ class CrustModule extends NativeModule {
|
|
|
18
18
|
}
|
|
19
19
|
showAVRoutePicker(_tintColor) { }
|
|
20
20
|
async setDeferredSystemGestures(_edges) { }
|
|
21
|
-
async setNotificationConfig(
|
|
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,
|
|
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"]}
|
package/ios/CrustModule.swift
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/crust",
|
|
3
|
-
"version": "
|
|
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":
|
|
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": "
|
|
38
|
+
"@mentra/jspolyfill": "3.1.0-dev.10"
|
|
35
39
|
},
|
|
36
40
|
"devDependencies": {
|
|
37
41
|
"@expo/config-plugins": ">=8.0.0",
|
package/plugin/build/index.d.ts
CHANGED
|
@@ -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;
|
package/plugin/build/index.js
CHANGED
|
@@ -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
|
-
|
|
130
|
+
config = withCrustAppGradle(config);
|
|
131
|
+
return withNotificationProcessGuard(config);
|
|
77
132
|
};
|
|
78
133
|
exports.withCrustAndroidBuildContract = withCrustAndroidBuildContract;
|
package/src/Crust.types.ts
CHANGED
|
@@ -128,7 +128,12 @@ export type PhoneNotificationEvent = {
|
|
|
128
128
|
app: string
|
|
129
129
|
title: string
|
|
130
130
|
content: string
|
|
131
|
-
|
|
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
|
}
|
package/src/CrustModule.ts
CHANGED
|
@@ -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(
|
|
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
|
package/src/CrustModule.web.ts
CHANGED
|
@@ -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(
|
|
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
|
}
|