@mentra/crust 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/android/build.gradle +146 -0
- package/android/src/internal/AndroidManifest.xml +9 -0
- package/android/src/internal/java/com/mentra/crust/receivers/CaptionsTesterIncidentReceiver.kt +46 -0
- package/android/src/main/AndroidManifest.xml +19 -0
- package/android/src/main/java/com/mentra/crust/CrustModule.kt +1042 -0
- package/android/src/main/java/com/mentra/crust/CrustView.kt +30 -0
- package/android/src/main/java/com/mentra/crust/heading/HeadingManager.kt +150 -0
- package/android/src/main/java/com/mentra/crust/jsc/JSCDispatcher.kt +189 -0
- package/android/src/main/java/com/mentra/crust/jsc/JSCPolyfillBridge.kt +287 -0
- package/android/src/main/java/com/mentra/crust/jsc/JSCRuntime.kt +593 -0
- package/android/src/main/java/com/mentra/crust/navigation/NavigationManager.kt +1445 -0
- package/android/src/main/java/com/mentra/crust/services/NotificationListener.kt +319 -0
- package/android/src/main/java/com/mentra/crust/utils/ImageProcessor.java +452 -0
- package/android/src/main/java/com/mentra/crust/utils/VideoStabilizer.kt +556 -0
- package/android/src/main/res/values/strings.xml +3 -0
- package/app.plugin.js +3 -0
- package/build/Crust.types.d.ts +148 -0
- package/build/Crust.types.d.ts.map +1 -0
- package/build/Crust.types.js +2 -0
- package/build/Crust.types.js.map +1 -0
- package/build/CrustModule.d.ts +182 -0
- package/build/CrustModule.d.ts.map +1 -0
- package/build/CrustModule.js +4 -0
- package/build/CrustModule.js.map +1 -0
- package/build/CrustModule.web.d.ts +34 -0
- package/build/CrustModule.web.d.ts.map +1 -0
- package/build/CrustModule.web.js +63 -0
- package/build/CrustModule.web.js.map +1 -0
- package/build/CrustView.d.ts +4 -0
- package/build/CrustView.d.ts.map +1 -0
- package/build/CrustView.js +7 -0
- package/build/CrustView.js.map +1 -0
- package/build/CrustView.web.d.ts +4 -0
- package/build/CrustView.web.d.ts.map +1 -0
- package/build/CrustView.web.js +7 -0
- package/build/CrustView.web.js.map +1 -0
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +6 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Crust.podspec +65 -0
- package/ios/CrustModule.swift +781 -0
- package/ios/CrustView.swift +38 -0
- package/ios/Resources/startup.js +814 -0
- package/ios/Source/JSCDispatcher.swift +226 -0
- package/ios/Source/JSCPolyfillBridge.swift +378 -0
- package/ios/Source/JSCRuntime.swift +673 -0
- package/ios/Source/utils/ImageProcessor.swift +392 -0
- package/ios/Source/utils/SystemGestures.swift +53 -0
- package/ios/Source/utils/VideoStabilizer.swift +374 -0
- package/ios/heading/HeadingManager.swift +74 -0
- package/ios/navigation/NavPayloads.swift +62 -0
- package/ios/navigation/NavigationManager.swift +720 -0
- package/package.json +69 -0
- package/plugin/build/index.d.ts +19 -0
- package/plugin/build/index.js +23 -0
- package/plugin/build/withAndroid.d.ts +2 -0
- package/plugin/build/withAndroid.js +78 -0
- package/src/Crust.types.ts +157 -0
- package/src/CrustModule.ts +191 -0
- package/src/CrustModule.web.ts +66 -0
- package/src/CrustView.tsx +10 -0
- package/src/CrustView.web.tsx +11 -0
- package/src/index.ts +5 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
package com.mentra.crust.services
|
|
2
|
+
|
|
3
|
+
import android.content.ComponentName
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.content.pm.ApplicationInfo
|
|
7
|
+
import android.content.pm.PackageManager
|
|
8
|
+
import android.os.Handler
|
|
9
|
+
import android.os.HandlerThread
|
|
10
|
+
import android.provider.Settings
|
|
11
|
+
import android.service.notification.NotificationListenerService
|
|
12
|
+
import android.service.notification.StatusBarNotification
|
|
13
|
+
import android.text.TextUtils
|
|
14
|
+
import android.util.Base64
|
|
15
|
+
import android.util.Log
|
|
16
|
+
import com.mentra.crust.CrustModule
|
|
17
|
+
|
|
18
|
+
class NotificationListener private constructor(private val context: Context) {
|
|
19
|
+
companion object {
|
|
20
|
+
private const val TAG = "CrustNotificationListener"
|
|
21
|
+
private const val PREFS_NAME = "mentra_crust_notification_prefs"
|
|
22
|
+
private const val PREF_NOTIFICATIONS_ENABLED = "notifications_enabled"
|
|
23
|
+
private const val PREF_NOTIFICATIONS_BLOCKLIST = "notifications_blocklist"
|
|
24
|
+
|
|
25
|
+
@Volatile private var instance: NotificationListener? = null
|
|
26
|
+
|
|
27
|
+
fun getInstance(context: Context): NotificationListener {
|
|
28
|
+
return instance
|
|
29
|
+
?: synchronized(this) {
|
|
30
|
+
instance
|
|
31
|
+
?: NotificationListener(context.applicationContext).also {
|
|
32
|
+
instance = it
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private val listeners = mutableListOf<OnNotificationReceivedListener>()
|
|
39
|
+
|
|
40
|
+
// Deduplication tracking with dedicated background thread. Using HandlerThread
|
|
41
|
+
// keeps the service independent from the app lifecycle on newer Android versions.
|
|
42
|
+
private val notificationBuffer = mutableMapOf<String, Runnable>()
|
|
43
|
+
private val notificationThread = HandlerThread("NotificationHandler").apply { start() }
|
|
44
|
+
private val notificationHandler = Handler(notificationThread.looper)
|
|
45
|
+
private val duplicateThresholdMs = 200L
|
|
46
|
+
|
|
47
|
+
private val preferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
48
|
+
|
|
49
|
+
@Volatile private var notificationsEnabled = preferences.getBoolean(PREF_NOTIFICATIONS_ENABLED, false)
|
|
50
|
+
@Volatile
|
|
51
|
+
private var notificationsBlocklist =
|
|
52
|
+
preferences.getStringSet(PREF_NOTIFICATIONS_BLOCKLIST, emptySet())?.toSet() ?: emptySet()
|
|
53
|
+
|
|
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)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Add a listener for notifications. */
|
|
91
|
+
fun addListener(listener: OnNotificationReceivedListener) {
|
|
92
|
+
if (!listeners.contains(listener)) {
|
|
93
|
+
listeners.add(listener)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Remove a listener. */
|
|
98
|
+
fun removeListener(listener: OnNotificationReceivedListener) {
|
|
99
|
+
listeners.remove(listener)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Allowlist for messaging apps that should not be blocked even if they match system patterns. */
|
|
103
|
+
private val messagingAppAllowlist =
|
|
104
|
+
setOf(
|
|
105
|
+
"com.google.android.apps.messaging",
|
|
106
|
+
"com.samsung.android.messaging",
|
|
107
|
+
"com.android.mms",
|
|
108
|
+
"com.google.android.gm",
|
|
109
|
+
"com.samsung.android.email.provider",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
/** Check if this is a system package that should be blocked. */
|
|
113
|
+
private fun isSystemPackageToBlock(packageName: String): Boolean {
|
|
114
|
+
if (messagingAppAllowlist.contains(packageName)) {
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
val pkg = packageName.lowercase()
|
|
119
|
+
return pkg.contains("google") || pkg.contains("samsung") || pkg.contains(".sec.")
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Called internally by the service when a notification is posted. */
|
|
123
|
+
internal fun onNotificationPosted(sbn: StatusBarNotification) {
|
|
124
|
+
val packageName = sbn.packageName
|
|
125
|
+
Log.d(TAG, "Received notification from $packageName (key: ${sbn.key})")
|
|
126
|
+
|
|
127
|
+
if (isSystemPackageToBlock(packageName)) {
|
|
128
|
+
Log.d(TAG, "Blocking system package: $packageName")
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (notificationsBlocklist.contains(packageName)) {
|
|
133
|
+
Log.d(TAG, "Notification in blocklist, returning")
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!notificationsEnabled) {
|
|
138
|
+
Log.d(TAG, "Notifications disabled globally")
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
val notification = sbn.notification
|
|
143
|
+
val extras = notification.extras
|
|
144
|
+
val title = extras.getCharSequence("android.title")?.toString() ?: ""
|
|
145
|
+
val text = extras.getCharSequence("android.text")?.toString() ?: ""
|
|
146
|
+
|
|
147
|
+
if (title.isEmpty() && text.isEmpty()) {
|
|
148
|
+
Log.d(TAG, "Ignoring empty notification")
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (text.matches(Regex("^\\d+ new messages$"))) {
|
|
153
|
+
Log.d(TAG, "Ignoring summary notification: $text")
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
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
|
+
val notificationKey = "$packageName|$title|$text"
|
|
167
|
+
|
|
168
|
+
synchronized(notificationBuffer) {
|
|
169
|
+
notificationBuffer[notificationKey]?.let {
|
|
170
|
+
notificationHandler.removeCallbacks(it)
|
|
171
|
+
notificationBuffer.remove(notificationKey)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
val task =
|
|
175
|
+
Runnable {
|
|
176
|
+
try {
|
|
177
|
+
Log.d(TAG, "Processing buffered notification from $appName")
|
|
178
|
+
CrustModule.emitPhoneNotification(
|
|
179
|
+
notificationKey = sbn.key,
|
|
180
|
+
packageName = packageName,
|
|
181
|
+
appName = appName,
|
|
182
|
+
title = title,
|
|
183
|
+
text = text,
|
|
184
|
+
timestamp = sbn.postTime,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
val notificationData =
|
|
188
|
+
NotificationData(
|
|
189
|
+
packageName = packageName,
|
|
190
|
+
title = title,
|
|
191
|
+
text = text,
|
|
192
|
+
timestamp = sbn.postTime,
|
|
193
|
+
id = sbn.id,
|
|
194
|
+
tag = sbn.tag,
|
|
195
|
+
)
|
|
196
|
+
listeners.forEach { listener ->
|
|
197
|
+
listener.onNotificationReceived(notificationData)
|
|
198
|
+
}
|
|
199
|
+
} catch (e: Exception) {
|
|
200
|
+
Log.e(TAG, "Error processing notification: ${e.message}", e)
|
|
201
|
+
} finally {
|
|
202
|
+
synchronized(notificationBuffer) {
|
|
203
|
+
notificationBuffer.remove(notificationKey)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
notificationBuffer[notificationKey] = task
|
|
209
|
+
Log.d(TAG, "Buffering notification (${duplicateThresholdMs}ms delay)")
|
|
210
|
+
notificationHandler.postDelayed(task, duplicateThresholdMs)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Called internally by the service when a notification is removed. */
|
|
215
|
+
internal fun onNotificationRemoved(sbn: StatusBarNotification) {
|
|
216
|
+
val packageName = sbn.packageName
|
|
217
|
+
val notificationKey = sbn.key
|
|
218
|
+
|
|
219
|
+
Log.d(TAG, "Notification removed - package: $packageName, key: $notificationKey")
|
|
220
|
+
|
|
221
|
+
CrustModule.emitPhoneNotificationDismissed(
|
|
222
|
+
notificationKey = notificationKey,
|
|
223
|
+
packageName = packageName,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
listeners.forEach { listener -> listener.onNotificationRemoved(packageName, sbn.id) }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Interface for notification callbacks. */
|
|
230
|
+
interface OnNotificationReceivedListener {
|
|
231
|
+
fun onNotificationReceived(notification: NotificationData)
|
|
232
|
+
fun onNotificationRemoved(packageName: String, notificationId: Int) {}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Data class for notification info. */
|
|
236
|
+
data class NotificationData(
|
|
237
|
+
val packageName: String,
|
|
238
|
+
val title: String,
|
|
239
|
+
val text: String,
|
|
240
|
+
val timestamp: Long,
|
|
241
|
+
val id: Int,
|
|
242
|
+
val tag: String?,
|
|
243
|
+
)
|
|
244
|
+
|
|
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
|
+
/** Clean up resources when the service is destroyed. */
|
|
281
|
+
fun cleanup() {
|
|
282
|
+
Log.d(TAG, "Cleaning up notification handler thread")
|
|
283
|
+
synchronized(notificationBuffer) {
|
|
284
|
+
notificationBuffer.values.forEach { notificationHandler.removeCallbacks(it) }
|
|
285
|
+
notificationBuffer.clear()
|
|
286
|
+
}
|
|
287
|
+
notificationThread.quitSafely()
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** The actual NotificationListenerService implementation. */
|
|
292
|
+
class NotificationListenerServiceImpl : NotificationListenerService() {
|
|
293
|
+
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
|
294
|
+
super.onNotificationPosted(sbn)
|
|
295
|
+
NotificationListener.getInstance(applicationContext).onNotificationPosted(sbn)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
override fun onNotificationRemoved(sbn: StatusBarNotification) {
|
|
299
|
+
super.onNotificationRemoved(sbn)
|
|
300
|
+
NotificationListener.getInstance(applicationContext).onNotificationRemoved(sbn)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
override fun onListenerConnected() {
|
|
304
|
+
super.onListenerConnected()
|
|
305
|
+
Log.d("CrustNotificationListener", "NotificationListenerService connected and ready")
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
override fun onListenerDisconnected() {
|
|
309
|
+
super.onListenerDisconnected()
|
|
310
|
+
Log.d("CrustNotificationListener", "NotificationListenerService disconnected, requesting rebind")
|
|
311
|
+
requestRebind(ComponentName(this, NotificationListenerServiceImpl::class.java))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
override fun onDestroy() {
|
|
315
|
+
super.onDestroy()
|
|
316
|
+
Log.d("CrustNotificationListener", "NotificationListenerService being destroyed")
|
|
317
|
+
NotificationListener.getInstance(applicationContext).cleanup()
|
|
318
|
+
}
|
|
319
|
+
}
|