@glassly/crust 0.1.0-dev.1

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.
Files changed (67) hide show
  1. package/README.md +41 -0
  2. package/android/build.gradle +146 -0
  3. package/android/src/internal/AndroidManifest.xml +9 -0
  4. package/android/src/internal/java/com/glassly/crust/receivers/CaptionsTesterIncidentReceiver.kt +46 -0
  5. package/android/src/main/AndroidManifest.xml +33 -0
  6. package/android/src/main/java/com/glassly/crust/CrustModule.kt +1074 -0
  7. package/android/src/main/java/com/glassly/crust/CrustView.kt +30 -0
  8. package/android/src/main/java/com/glassly/crust/heading/HeadingManager.kt +150 -0
  9. package/android/src/main/java/com/glassly/crust/jsc/JSCDispatcher.kt +189 -0
  10. package/android/src/main/java/com/glassly/crust/jsc/JSCPolyfillBridge.kt +287 -0
  11. package/android/src/main/java/com/glassly/crust/jsc/JSCRuntime.kt +593 -0
  12. package/android/src/main/java/com/glassly/crust/navigation/NavigationManager.kt +1466 -0
  13. package/android/src/main/java/com/glassly/crust/services/NotificationListener.kt +502 -0
  14. package/android/src/main/java/com/glassly/crust/services/NotificationProcessBridge.kt +222 -0
  15. package/android/src/main/java/com/glassly/crust/utils/ImageProcessor.java +452 -0
  16. package/android/src/main/java/com/glassly/crust/utils/VideoStabilizer.kt +556 -0
  17. package/android/src/main/res/values/strings.xml +3 -0
  18. package/app.plugin.js +3 -0
  19. package/build/Crust.types.d.ts +153 -0
  20. package/build/Crust.types.d.ts.map +1 -0
  21. package/build/Crust.types.js +2 -0
  22. package/build/Crust.types.js.map +1 -0
  23. package/build/CrustModule.d.ts +186 -0
  24. package/build/CrustModule.d.ts.map +1 -0
  25. package/build/CrustModule.js +4 -0
  26. package/build/CrustModule.js.map +1 -0
  27. package/build/CrustModule.web.d.ts +39 -0
  28. package/build/CrustModule.web.d.ts.map +1 -0
  29. package/build/CrustModule.web.js +69 -0
  30. package/build/CrustModule.web.js.map +1 -0
  31. package/build/CrustView.d.ts +4 -0
  32. package/build/CrustView.d.ts.map +1 -0
  33. package/build/CrustView.js +8 -0
  34. package/build/CrustView.js.map +1 -0
  35. package/build/CrustView.web.d.ts +3 -0
  36. package/build/CrustView.web.d.ts.map +1 -0
  37. package/build/CrustView.web.js +5 -0
  38. package/build/CrustView.web.js.map +1 -0
  39. package/build/index.d.ts +4 -0
  40. package/build/index.d.ts.map +1 -0
  41. package/build/index.js +6 -0
  42. package/build/index.js.map +1 -0
  43. package/expo-module.config.json +9 -0
  44. package/ios/Crust.podspec +65 -0
  45. package/ios/CrustModule.swift +784 -0
  46. package/ios/CrustView.swift +38 -0
  47. package/ios/Resources/startup.js +804 -0
  48. package/ios/Source/JSCDispatcher.swift +226 -0
  49. package/ios/Source/JSCPolyfillBridge.swift +378 -0
  50. package/ios/Source/JSCRuntime.swift +674 -0
  51. package/ios/Source/utils/ImageProcessor.swift +392 -0
  52. package/ios/Source/utils/SystemGestures.swift +53 -0
  53. package/ios/Source/utils/VideoStabilizer.swift +374 -0
  54. package/ios/heading/HeadingManager.swift +74 -0
  55. package/ios/navigation/NavPayloads.swift +62 -0
  56. package/ios/navigation/NavigationManager.swift +751 -0
  57. package/package.json +69 -0
  58. package/plugin/build/index.d.ts +21 -0
  59. package/plugin/build/index.js +25 -0
  60. package/plugin/build/withAndroid.d.ts +2 -0
  61. package/plugin/build/withAndroid.js +133 -0
  62. package/src/Crust.types.ts +162 -0
  63. package/src/CrustModule.ts +194 -0
  64. package/src/CrustModule.web.ts +72 -0
  65. package/src/CrustView.tsx +10 -0
  66. package/src/CrustView.web.tsx +9 -0
  67. package/src/index.ts +5 -0
@@ -0,0 +1,502 @@
1
+ package com.glassly.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.glassly.crust.CrustModule
17
+ import java.util.concurrent.ConcurrentHashMap
18
+
19
+ class NotificationListener private constructor(private val context: Context) {
20
+ companion object {
21
+ private const val TAG = "CrustNotificationListener"
22
+ private const val PREFS_NAME = "glassly_crust_notification_prefs"
23
+ private const val PREF_LISTENER_ENABLED = "notification_listener_enabled"
24
+ private const val PREF_NOTIFICATIONS_BLOCKLIST = "notifications_blocklist"
25
+
26
+ @Volatile private var instance: NotificationListener? = null
27
+
28
+ fun getInstance(context: Context): NotificationListener {
29
+ return instance
30
+ ?: synchronized(this) {
31
+ instance
32
+ ?: NotificationListener(context.applicationContext).also {
33
+ instance = it
34
+ }
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
+ }
234
+ }
235
+
236
+ private val listeners = mutableListOf<OnNotificationReceivedListener>()
237
+ private val appNameCache = ConcurrentHashMap<String, String>()
238
+
239
+ // Deduplication tracking with dedicated background thread. Using HandlerThread
240
+ // keeps the service independent from the app lifecycle on newer Android versions.
241
+ private val notificationBuffer = mutableMapOf<String, Runnable>()
242
+ private val notificationThread = HandlerThread("NotificationHandler").apply { start() }
243
+ private val notificationHandler = Handler(notificationThread.looper)
244
+ private val duplicateThresholdMs = 200L
245
+
246
+ private val preferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
247
+
248
+ @Volatile private var listenerEnabled = preferences.getBoolean(PREF_LISTENER_ENABLED, false)
249
+ @Volatile
250
+ private var notificationsBlocklist =
251
+ preferences.getStringSet(PREF_NOTIFICATIONS_BLOCKLIST, emptySet())?.toSet() ?: emptySet()
252
+
253
+ private fun applyConfig(
254
+ listenerEnabled: Boolean,
255
+ blocklist: Set<String>,
256
+ ) {
257
+ this.listenerEnabled = listenerEnabled
258
+ notificationsBlocklist = blocklist
259
+ }
260
+
261
+ /** Add a listener for notifications. */
262
+ fun addListener(listener: OnNotificationReceivedListener) {
263
+ if (!listeners.contains(listener)) {
264
+ listeners.add(listener)
265
+ }
266
+ }
267
+
268
+ /** Remove a listener. */
269
+ fun removeListener(listener: OnNotificationReceivedListener) {
270
+ listeners.remove(listener)
271
+ }
272
+
273
+ /** Allowlist for messaging apps that should not be blocked even if they match system patterns. */
274
+ private val messagingAppAllowlist =
275
+ setOf(
276
+ "com.google.android.apps.messaging",
277
+ "com.samsung.android.messaging",
278
+ "com.android.mms",
279
+ "com.google.android.gm",
280
+ "com.samsung.android.email.provider",
281
+ )
282
+
283
+ /** Check if this is a system package that should be blocked. */
284
+ private fun isSystemPackageToBlock(packageName: String): Boolean {
285
+ if (messagingAppAllowlist.contains(packageName)) {
286
+ return false
287
+ }
288
+
289
+ val pkg = packageName.lowercase()
290
+ return pkg.contains("google") || pkg.contains("samsung") || pkg.contains(".sec.")
291
+ }
292
+
293
+ /** Called internally by the service when a notification is posted. */
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
+
305
+ val packageName = sbn.packageName
306
+ Log.d(TAG, "Received notification from $packageName (key: ${sbn.key})")
307
+
308
+ if (isSystemPackageToBlock(packageName)) {
309
+ Log.d(TAG, "Blocking system package: $packageName")
310
+ return
311
+ }
312
+
313
+ if (notificationsBlocklist.contains(packageName)) {
314
+ Log.d(TAG, "Notification in blocklist, returning")
315
+ return
316
+ }
317
+
318
+ val notification = sbn.notification
319
+ val extras = notification.extras
320
+ val title = extras.getCharSequence("android.title")?.toString() ?: ""
321
+ val text = extras.getCharSequence("android.text")?.toString() ?: ""
322
+
323
+ if (title.isEmpty() && text.isEmpty()) {
324
+ Log.d(TAG, "Ignoring empty notification")
325
+ return
326
+ }
327
+
328
+ if (text.matches(Regex("^\\d+ new messages$"))) {
329
+ Log.d(TAG, "Ignoring summary notification: $text")
330
+ return
331
+ }
332
+
333
+ val notificationKey = "$packageName|$title|$text"
334
+
335
+ synchronized(notificationBuffer) {
336
+ notificationBuffer[notificationKey]?.let {
337
+ notificationHandler.removeCallbacks(it)
338
+ notificationBuffer.remove(notificationKey)
339
+ }
340
+
341
+ val task =
342
+ Runnable {
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
+ }
364
+ Log.d(TAG, "Processing buffered notification from $appName")
365
+ CrustModule.emitPhoneNotification(
366
+ context = context,
367
+ notificationKey = sbn.key,
368
+ packageName = packageName,
369
+ appName = appName,
370
+ title = title,
371
+ text = text,
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,
377
+ )
378
+
379
+ val notificationData =
380
+ NotificationData(
381
+ packageName = packageName,
382
+ title = title,
383
+ text = text,
384
+ timestamp = sbn.postTime,
385
+ id = sbn.id,
386
+ tag = sbn.tag,
387
+ )
388
+ listeners.forEach { listener ->
389
+ listener.onNotificationReceived(notificationData)
390
+ }
391
+ } catch (e: Exception) {
392
+ Log.e(TAG, "Error processing notification: ${e.message}", e)
393
+ } finally {
394
+ synchronized(notificationBuffer) {
395
+ notificationBuffer.remove(notificationKey)
396
+ }
397
+ }
398
+ }
399
+
400
+ notificationBuffer[notificationKey] = task
401
+ Log.d(TAG, "Buffering notification (${duplicateThresholdMs}ms delay)")
402
+ notificationHandler.postDelayed(task, duplicateThresholdMs)
403
+ }
404
+ }
405
+
406
+ /** Called internally by the service when a notification is removed. */
407
+ internal fun onNotificationRemoved(sbn: StatusBarNotification) {
408
+ if (!listenerEnabled) {
409
+ return
410
+ }
411
+
412
+ val packageName = sbn.packageName
413
+ val notificationKey = sbn.key
414
+
415
+ Log.d(TAG, "Notification removed - package: $packageName, key: $notificationKey")
416
+
417
+ CrustModule.emitPhoneNotificationDismissed(
418
+ context = context,
419
+ notificationKey = notificationKey,
420
+ packageName = packageName,
421
+ )
422
+
423
+ listeners.forEach { listener -> listener.onNotificationRemoved(packageName, sbn.id) }
424
+ }
425
+
426
+ /** Interface for notification callbacks. */
427
+ interface OnNotificationReceivedListener {
428
+ fun onNotificationReceived(notification: NotificationData)
429
+ fun onNotificationRemoved(packageName: String, notificationId: Int) {}
430
+ }
431
+
432
+ /** Data class for notification info. */
433
+ data class NotificationData(
434
+ val packageName: String,
435
+ val title: String,
436
+ val text: String,
437
+ val timestamp: Long,
438
+ val id: Int,
439
+ val tag: String?,
440
+ )
441
+
442
+ /** Clean up resources when the service is destroyed. */
443
+ fun cleanup() {
444
+ Log.d(TAG, "Cleaning up notification handler thread")
445
+ synchronized(notificationBuffer) {
446
+ notificationBuffer.values.forEach { notificationHandler.removeCallbacks(it) }
447
+ notificationBuffer.clear()
448
+ }
449
+ notificationThread.quitSafely()
450
+ }
451
+ }
452
+
453
+ /** The actual NotificationListenerService implementation. */
454
+ class NotificationListenerServiceImpl : NotificationListenerService() {
455
+ override fun onNotificationPosted(sbn: StatusBarNotification) {
456
+ super.onNotificationPosted(sbn)
457
+ // Channel importance, not Notification.priority: the latter was deprecated
458
+ // at API 26 and reads 0 for apps that express intent through a channel
459
+ // instead, which would make every consumer treat them as ordinary. Only the
460
+ // service can see the ranking, so it is resolved here and passed down.
461
+ NotificationListener.getInstance(applicationContext).onNotificationPosted(sbn, importanceOf(sbn))
462
+ }
463
+
464
+ /** IMPORTANCE_* for this notification, or null when the ranking is unavailable. */
465
+ private fun importanceOf(sbn: StatusBarNotification): Int? {
466
+ return try {
467
+ val ranking = NotificationListenerService.Ranking()
468
+ if (currentRanking?.getRanking(sbn.key, ranking) == true) ranking.importance else null
469
+ } catch (e: Exception) {
470
+ Log.w("CrustNotificationListener", "could not read ranking importance", e)
471
+ null
472
+ }
473
+ }
474
+
475
+ override fun onNotificationRemoved(sbn: StatusBarNotification) {
476
+ super.onNotificationRemoved(sbn)
477
+ NotificationListener.getInstance(applicationContext).onNotificationRemoved(sbn)
478
+ }
479
+
480
+ override fun onListenerConnected() {
481
+ super.onListenerConnected()
482
+ Log.d("CrustNotificationListener", "NotificationListenerService connected and ready")
483
+ }
484
+
485
+ override fun onListenerDisconnected() {
486
+ super.onListenerDisconnected()
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
+ }
495
+ }
496
+
497
+ override fun onDestroy() {
498
+ super.onDestroy()
499
+ Log.d("CrustNotificationListener", "NotificationListenerService being destroyed")
500
+ NotificationListener.destroyInstance()
501
+ }
502
+ }