expo-notifications 58.1.0 → 58.1.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 (63) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/java/expo/modules/notifications/Exceptions.kt +6 -0
  4. package/android/src/main/java/expo/modules/notifications/notifications/ArgumentsNotificationContentBuilder.java +8 -1
  5. package/android/src/main/java/expo/modules/notifications/notifications/NotificationSerializer.java +4 -0
  6. package/android/src/main/java/expo/modules/notifications/notifications/interfaces/INotificationContent.kt +1 -0
  7. package/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java +20 -0
  8. package/android/src/main/java/expo/modules/notifications/notifications/model/NotificationData.kt +3 -0
  9. package/android/src/main/java/expo/modules/notifications/notifications/model/RemoteNotificationContent.kt +3 -0
  10. package/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt +5 -0
  11. package/android/src/main/java/expo/modules/notifications/permissions/NotificationPermissionRecords.kt +40 -0
  12. package/android/src/main/java/expo/modules/notifications/permissions/NotificationPermissionsModule.kt +91 -34
  13. package/android/src/main/java/expo/modules/notifications/service/NotificationsService.kt +38 -0
  14. package/android/src/main/java/expo/modules/notifications/service/delegates/ExpoPresentationDelegate.kt +145 -20
  15. package/android/src/main/java/expo/modules/notifications/service/interfaces/PresentationDelegate.kt +2 -0
  16. package/build/NotificationPermissions.d.ts +29 -0
  17. package/build/NotificationPermissions.js +49 -1
  18. package/build/NotificationPermissions.js.map +1 -1
  19. package/build/NotificationPermissionsModule.types.d.ts +3 -0
  20. package/build/NotificationPermissionsModule.types.js.map +1 -1
  21. package/build/Notifications.types.d.ts +9 -2
  22. package/build/Notifications.types.js.map +1 -1
  23. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/{58.1.0/expo.modules.notifications-58.1.0-sources.jar → 58.1.1/expo.modules.notifications-58.1.1-sources.jar} +0 -0
  24. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.1/expo.modules.notifications-58.1.1-sources.jar.md5 +1 -0
  25. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.1/expo.modules.notifications-58.1.1-sources.jar.sha1 +1 -0
  26. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.1/expo.modules.notifications-58.1.1-sources.jar.sha256 +1 -0
  27. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.1/expo.modules.notifications-58.1.1-sources.jar.sha512 +1 -0
  28. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.1/expo.modules.notifications-58.1.1.aar +0 -0
  29. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.1/expo.modules.notifications-58.1.1.aar.md5 +1 -0
  30. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.1/expo.modules.notifications-58.1.1.aar.sha1 +1 -0
  31. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.1/expo.modules.notifications-58.1.1.aar.sha256 +1 -0
  32. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.1/expo.modules.notifications-58.1.1.aar.sha512 +1 -0
  33. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/{58.1.0/expo.modules.notifications-58.1.0.module → 58.1.1/expo.modules.notifications-58.1.1.module} +22 -22
  34. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.1/expo.modules.notifications-58.1.1.module.md5 +1 -0
  35. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.1/expo.modules.notifications-58.1.1.module.sha1 +1 -0
  36. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.1/expo.modules.notifications-58.1.1.module.sha256 +1 -0
  37. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.1/expo.modules.notifications-58.1.1.module.sha512 +1 -0
  38. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/{58.1.0/expo.modules.notifications-58.1.0.pom → 58.1.1/expo.modules.notifications-58.1.1.pom} +1 -1
  39. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.1/expo.modules.notifications-58.1.1.pom.md5 +1 -0
  40. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.1/expo.modules.notifications-58.1.1.pom.sha1 +1 -0
  41. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.1/expo.modules.notifications-58.1.1.pom.sha256 +1 -0
  42. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.1/expo.modules.notifications-58.1.1.pom.sha512 +1 -0
  43. package/package.json +6 -6
  44. package/src/NotificationPermissions.ts +57 -1
  45. package/src/NotificationPermissionsModule.types.ts +3 -0
  46. package/src/Notifications.types.ts +9 -2
  47. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.0/expo.modules.notifications-58.1.0-sources.jar.md5 +0 -1
  48. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.0/expo.modules.notifications-58.1.0-sources.jar.sha1 +0 -1
  49. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.0/expo.modules.notifications-58.1.0-sources.jar.sha256 +0 -1
  50. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.0/expo.modules.notifications-58.1.0-sources.jar.sha512 +0 -1
  51. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.0/expo.modules.notifications-58.1.0.aar +0 -0
  52. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.0/expo.modules.notifications-58.1.0.aar.md5 +0 -1
  53. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.0/expo.modules.notifications-58.1.0.aar.sha1 +0 -1
  54. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.0/expo.modules.notifications-58.1.0.aar.sha256 +0 -1
  55. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.0/expo.modules.notifications-58.1.0.aar.sha512 +0 -1
  56. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.0/expo.modules.notifications-58.1.0.module.md5 +0 -1
  57. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.0/expo.modules.notifications-58.1.0.module.sha1 +0 -1
  58. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.0/expo.modules.notifications-58.1.0.module.sha256 +0 -1
  59. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.0/expo.modules.notifications-58.1.0.module.sha512 +0 -1
  60. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.0/expo.modules.notifications-58.1.0.pom.md5 +0 -1
  61. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.0/expo.modules.notifications-58.1.0.pom.sha1 +0 -1
  62. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.0/expo.modules.notifications-58.1.0.pom.sha256 +0 -1
  63. package/local-maven-repo/host/exp/exponent/expo.modules.notifications/58.1.0/expo.modules.notifications-58.1.0.pom.sha512 +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 58.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [Android] Add support for grouping notifications via `threadIdentifier`. ([#43388](https://github.com/expo/expo/pull/43388) by [@vonovak](https://github.com/vonovak))
8
+ - [Android] Add `getExactAlarmPermissionsAsync` and `requestExactAlarmPermissionsAsync` to check and request the exact alarm permission. ([#50919](https://github.com/expo/expo/pull/50919) by [@vonovak](https://github.com/vonovak))
9
+ - [Internal] [Android] Return notification permission responses as records instead of bundles. ([#51060](https://github.com/expo/expo/pull/51060) by [@vonovak](https://github.com/vonovak))
10
+ - Updated dependencies. ([#51077](https://github.com/expo/expo/pull/51077))
11
+ - expo-application@58.0.4
12
+ - @expo/image-utils@0.12.4
13
+ - expo-constants@58.0.10
14
+
3
15
  ## 58.1.0
4
16
 
5
17
  ### Minor Changes
@@ -5,13 +5,13 @@ plugins {
5
5
  }
6
6
 
7
7
  group = 'host.exp.exponent'
8
- version = '58.1.0'
8
+ version = '58.1.1'
9
9
 
10
10
  android {
11
11
  namespace "expo.modules.notifications"
12
12
  defaultConfig {
13
13
  versionCode 21
14
- versionName '58.1.0'
14
+ versionName '58.1.1'
15
15
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16
16
  consumerProguardFiles 'proguard-rules.pro'
17
17
  }
@@ -7,3 +7,9 @@ class ModuleNotFoundException(moduleClass: KClass<*>) :
7
7
  CodedException(message = "$moduleClass module not found")
8
8
 
9
9
  class NotificationWasAlreadyHandledException(val id: String) : CodedException("Failed to handle notification $id, it has already been handled.")
10
+
11
+ class ExactAlarmPermissionNotDeclaredException :
12
+ CodedException(
13
+ "Cannot request the exact alarm permission because the app does not declare android.permission.SCHEDULE_EXACT_ALARM. " +
14
+ "Add it to `android.permissions` in your app config and rebuild the app."
15
+ )
@@ -29,6 +29,7 @@ public class ArgumentsNotificationContentBuilder extends NotificationContent.Bui
29
29
  private static final String AUTO_DISMISS_KEY = "autoDismiss";
30
30
  private static final String CATEGORY_IDENTIFIER_KEY = "categoryIdentifier";
31
31
  private static final String STICKY_KEY = "sticky";
32
+ private static final String THREAD_IDENTIFIER_KEY = "threadIdentifier";
32
33
 
33
34
  private SoundResolver mSoundResolver;
34
35
 
@@ -46,7 +47,8 @@ public class ArgumentsNotificationContentBuilder extends NotificationContent.Bui
46
47
  .setColor(getColor(payload))
47
48
  .setAutoDismiss(getAutoDismiss(payload))
48
49
  .setCategoryId(getCategoryId(payload))
49
- .setSticky(getSticky(payload));
50
+ .setSticky(getSticky(payload))
51
+ .setGroup(getGroup(payload));
50
52
 
51
53
  if (shouldPlayDefaultSound(payload)) {
52
54
  useDefaultSound();
@@ -146,4 +148,9 @@ public class ArgumentsNotificationContentBuilder extends NotificationContent.Bui
146
148
  // TODO: the default value should be determined by NotificationContent.Builder
147
149
  return payload.getBoolean(STICKY_KEY, false);
148
150
  }
151
+
152
+ @Nullable
153
+ protected String getGroup(ReadableArguments payload) {
154
+ return payload.getString(THREAD_IDENTIFIER_KEY, null);
155
+ }
149
156
  }
@@ -129,6 +129,7 @@ public class NotificationSerializer {
129
129
  serializedContent.putString("categoryIdentifier", content.getCategoryId());
130
130
  }
131
131
  serializedContent.putBoolean("sticky", content.isSticky());
132
+ serializedContent.putString("threadIdentifier", content.getGroup());
132
133
  return serializedContent;
133
134
  }
134
135
 
@@ -187,6 +188,9 @@ public class NotificationSerializer {
187
188
  serializedContent.putBundle("data", filteredBundleForJSTypeConverter(extras));
188
189
  }
189
190
 
191
+ String threadIdentifier = extras.getString("threadIdentifier");
192
+ serializedContent.putString("threadIdentifier", threadIdentifier == null || threadIdentifier.isEmpty() ? null : threadIdentifier);
193
+
190
194
  Bundle serializedTrigger = new Bundle();
191
195
  serializedTrigger.putString("type", "push");
192
196
  serializedTrigger.putString("channelId", extras.getString("channelId"));
@@ -31,6 +31,7 @@ interface INotificationContent : Parcelable {
31
31
  val color: Number?
32
32
  val isAutoDismiss: Boolean
33
33
  val categoryId: String?
34
+ val group: String?
34
35
  val isSticky: Boolean
35
36
 
36
37
  fun containsImage(): Boolean
@@ -44,6 +44,7 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
44
44
  private boolean mAutoDismiss;
45
45
  private String mCategoryId;
46
46
  private boolean mSticky;
47
+ private String mGroup;
47
48
 
48
49
  protected NotificationContent() {
49
50
  }
@@ -134,6 +135,11 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
134
135
  return mCategoryId;
135
136
  }
136
137
 
138
+ @Nullable
139
+ public String getGroup() {
140
+ return mGroup;
141
+ }
142
+
137
143
  public boolean isSticky() {
138
144
  return mSticky;
139
145
  }
@@ -165,6 +171,7 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
165
171
  mAutoDismiss = in.readByte() == 1;
166
172
  mCategoryId = in.readString();
167
173
  mSticky = in.readByte() == 1;
174
+ mGroup = in.readString();
168
175
  }
169
176
 
170
177
  @Override
@@ -183,6 +190,7 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
183
190
  dest.writeByte((byte) (mAutoDismiss ? 1 : 0));
184
191
  dest.writeString(mCategoryId);
185
192
  dest.writeByte((byte) (mSticky ? 1 : 0));
193
+ dest.writeString(mGroup);
186
194
  }
187
195
 
188
196
  // EXPONOTIFCONTENT02
@@ -210,6 +218,7 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
210
218
  out.writeByte(mAutoDismiss ? 1 : 0);
211
219
  out.writeObject(mCategoryId != null ? mCategoryId.toString() : null);
212
220
  out.writeByte(mSticky ? 1 : 0);
221
+ out.writeObject(mGroup);
213
222
  }
214
223
 
215
224
  private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
@@ -257,6 +266,11 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
257
266
  mCategoryId = new String(categoryIdString);
258
267
  }
259
268
  mSticky = in.readByte() == 1;
269
+ try {
270
+ mGroup = (String) in.readObject();
271
+ } catch (java.io.OptionalDataException | java.io.EOFException e) {
272
+ // Backward compatibility: old serialized data won't have this field
273
+ }
260
274
  }
261
275
 
262
276
  private void readObjectNoData() throws ObjectStreamException {
@@ -354,6 +368,12 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
354
368
  return this;
355
369
  }
356
370
 
371
+ public Builder setGroup(String group) {
372
+ // An empty group would be nameless. iOS also reports it as null.
373
+ content.mGroup = group == null || group.isEmpty() ? null : group;
374
+ return this;
375
+ }
376
+
357
377
  public NotificationContent build() {
358
378
  return content;
359
379
  }
@@ -58,6 +58,9 @@ value class NotificationData(private val data: Map<String, String>) {
58
58
  val categoryId: String?
59
59
  get() = data["categoryId"]
60
60
 
61
+ val group: String?
62
+ get() = data["threadIdentifier"]?.takeIf { it.isNotEmpty() }
63
+
61
64
  val subText: String?
62
65
  get() = data["subtitle"]
63
66
 
@@ -67,6 +67,9 @@ class RemoteNotificationContent(private val remoteMessage: RemoteMessage) : INot
67
67
 
68
68
  override val categoryId = notificationData.categoryId
69
69
 
70
+ // FCM's notification payload has no group field.
71
+ override val group = notificationData.group
72
+
70
73
  override val isSticky = notificationData.isSticky
71
74
 
72
75
  override val subText = notificationData.subText
@@ -114,6 +114,11 @@ open class ExpoNotificationBuilder(
114
114
 
115
115
  applySoundsAndVibrations(content, builder)
116
116
 
117
+ content.group?.let {
118
+ builder.setGroup(it)
119
+ builder.setDeleteIntent(NotificationsService.createGroupedNotificationDeletedIntent(context, notification))
120
+ }
121
+
117
122
  if (content.body != null) {
118
123
  // Add body - JSON data - to extras
119
124
  val extras = builder.extras
@@ -0,0 +1,40 @@
1
+ package expo.modules.notifications.permissions
2
+
3
+ import expo.modules.interfaces.permissions.PermissionsResponse
4
+ import expo.modules.kotlin.records.Field
5
+ import expo.modules.kotlin.records.Record
6
+ import expo.modules.kotlin.types.OptimizedRecord
7
+
8
+ @OptimizedRecord
9
+ class AndroidNotificationPermissionDetails(
10
+ @Field
11
+ val importance: Int,
12
+ @Field
13
+ val interruptionFilter: Int
14
+ ) : Record
15
+
16
+ @OptimizedRecord
17
+ class NotificationPermissionResponse(
18
+ @Field
19
+ val status: String,
20
+ @Field
21
+ val expires: String = PermissionsResponse.PERMISSION_EXPIRES_NEVER,
22
+ @Field
23
+ val canAskAgain: Boolean,
24
+ @Field
25
+ val granted: Boolean,
26
+ @Field
27
+ val android: AndroidNotificationPermissionDetails
28
+ ) : Record
29
+
30
+ @OptimizedRecord
31
+ class ExactAlarmPermissionResponse(
32
+ @Field
33
+ val status: String,
34
+ @Field
35
+ val expires: String = PermissionsResponse.PERMISSION_EXPIRES_NEVER,
36
+ @Field
37
+ val canAskAgain: Boolean,
38
+ @Field
39
+ val granted: Boolean
40
+ ) : Record
@@ -1,25 +1,27 @@
1
1
  package expo.modules.notifications.permissions
2
2
 
3
3
  import android.Manifest
4
- import android.app.NotificationManager
4
+ import android.app.AlarmManager
5
5
  import android.content.Context
6
+ import android.content.Intent
7
+ import android.content.pm.PackageManager
8
+ import android.net.Uri
6
9
  import android.os.Build
10
+ import android.provider.Settings
7
11
  import androidx.annotation.RequiresApi
8
12
  import androidx.core.app.NotificationManagerCompat
9
- import androidx.core.os.bundleOf
10
13
  import expo.modules.core.arguments.ReadableArguments
11
14
  import expo.modules.interfaces.permissions.Permissions
12
15
  import expo.modules.interfaces.permissions.PermissionsResponse
13
16
  import expo.modules.interfaces.permissions.PermissionsStatus
14
17
  import expo.modules.kotlin.Promise
15
18
  import expo.modules.kotlin.exception.Exceptions
19
+ import expo.modules.kotlin.functions.Queues
16
20
  import expo.modules.kotlin.modules.Module
17
21
  import expo.modules.kotlin.modules.ModuleDefinition
22
+ import expo.modules.notifications.ExactAlarmPermissionNotDeclaredException
18
23
  import expo.modules.notifications.ModuleNotFoundException
19
24
 
20
- private const val ANDROID_RESPONSE_KEY = "android"
21
- private const val IMPORTANCE_KEY = "importance"
22
- private const val INTERRUPTION_FILTER_KEY = "interruptionFilter"
23
25
  private val PERMISSIONS: Array<String> = arrayOf(Manifest.permission.POST_NOTIFICATIONS)
24
26
 
25
27
  class NotificationPermissionsModule : Module() {
@@ -29,6 +31,8 @@ class NotificationPermissionsModule : Module() {
29
31
  private val context: Context
30
32
  get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
31
33
 
34
+ private val pendingExactAlarmPromises = mutableListOf<Promise>()
35
+
32
36
  override fun definition() = ModuleDefinition {
33
37
  Name("ExpoNotificationPermissionsModule")
34
38
 
@@ -47,6 +51,72 @@ class NotificationPermissionsModule : Module() {
47
51
  getPermissionsWithPromiseImplClassic(promise)
48
52
  }
49
53
  }
54
+
55
+ AsyncFunction("getExactAlarmPermissionsAsync") {
56
+ getExactAlarmPermissionsResponse()
57
+ }
58
+
59
+ AsyncFunction("requestExactAlarmPermissionsAsync") { promise: Promise ->
60
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || canScheduleExactAlarms()) {
61
+ promise.resolve(getExactAlarmPermissionsResponse())
62
+ return@AsyncFunction
63
+ }
64
+ if (!isScheduleExactAlarmDeclared()) {
65
+ throw ExactAlarmPermissionNotDeclaredException()
66
+ }
67
+ val activity = appContext.currentActivity ?: throw Exceptions.MissingActivity()
68
+ pendingExactAlarmPromises.add(promise)
69
+ val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM, Uri.parse("package:${context.packageName}"))
70
+ try {
71
+ activity.startActivity(intent)
72
+ } catch (e: Throwable) {
73
+ pendingExactAlarmPromises.remove(promise)
74
+ throw e
75
+ }
76
+ }.runOnQueue(Queues.MAIN)
77
+
78
+ OnActivityEntersForeground {
79
+ if (pendingExactAlarmPromises.isNotEmpty()) {
80
+ val promises = pendingExactAlarmPromises.toList()
81
+ pendingExactAlarmPromises.clear()
82
+ val response = getExactAlarmPermissionsResponse()
83
+ promises.forEach { it.resolve(response) }
84
+ }
85
+ }
86
+ }
87
+
88
+ private fun canScheduleExactAlarms(): Boolean {
89
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
90
+ return true
91
+ }
92
+ val alarmManager = context.getSystemService(AlarmManager::class.java)
93
+ return alarmManager?.canScheduleExactAlarms() ?: false
94
+ }
95
+
96
+ private fun isScheduleExactAlarmDeclared(): Boolean {
97
+ val requested = if (Build.VERSION.SDK_INT >= 33) {
98
+ context.packageManager
99
+ .getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()))
100
+ .requestedPermissions
101
+ } else {
102
+ @Suppress("DEPRECATION")
103
+ context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS).requestedPermissions
104
+ }
105
+ return requested?.contains(Manifest.permission.SCHEDULE_EXACT_ALARM) == true
106
+ }
107
+
108
+ private fun getExactAlarmPermissionsResponse(): ExactAlarmPermissionResponse {
109
+ val granted = canScheduleExactAlarms()
110
+ val status = if (granted) {
111
+ PermissionsStatus.GRANTED
112
+ } else {
113
+ PermissionsStatus.DENIED
114
+ }
115
+ return ExactAlarmPermissionResponse(
116
+ status = status.status,
117
+ canAskAgain = granted || isScheduleExactAlarmDeclared(),
118
+ granted = granted
119
+ )
50
120
  }
51
121
 
52
122
  @RequiresApi(33)
@@ -55,15 +125,6 @@ class NotificationPermissionsModule : Module() {
55
125
  { permissionsMap: Map<String, PermissionsResponse> ->
56
126
  val managerCompat = NotificationManagerCompat.from(context)
57
127
  val areEnabled = managerCompat.areNotificationsEnabled()
58
- val platformBundle = bundleOf(
59
- IMPORTANCE_KEY to managerCompat.importance
60
- ).apply {
61
- val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
62
- if (notificationManager != null) {
63
- putInt(INTERRUPTION_FILTER_KEY, notificationManager.currentInterruptionFilter)
64
- }
65
- }
66
-
67
128
  val areAllGranted = permissionsMap.all { (_, response) -> response.status == PermissionsStatus.GRANTED }
68
129
  val areAllDenied = permissionsMap.all { (_, response) -> response.status == PermissionsStatus.DENIED }
69
130
  val canAskAgain = permissionsMap.all { (_, response) -> response.canAskAgain }
@@ -75,12 +136,11 @@ class NotificationPermissionsModule : Module() {
75
136
  }
76
137
 
77
138
  promise.resolve(
78
- bundleOf(
79
- PermissionsResponse.EXPIRES_KEY to PermissionsResponse.PERMISSION_EXPIRES_NEVER,
80
- PermissionsResponse.STATUS_KEY to status,
81
- PermissionsResponse.CAN_ASK_AGAIN_KEY to canAskAgain,
82
- PermissionsResponse.GRANTED_KEY to areAllGranted,
83
- ANDROID_RESPONSE_KEY to platformBundle
139
+ NotificationPermissionResponse(
140
+ status = status,
141
+ canAskAgain = canAskAgain,
142
+ granted = areAllGranted,
143
+ android = getAndroidDetails(managerCompat)
84
144
  )
85
145
  )
86
146
  },
@@ -96,26 +156,23 @@ class NotificationPermissionsModule : Module() {
96
156
  } else {
97
157
  PermissionsStatus.DENIED
98
158
  }
99
- val platformBundle = bundleOf(
100
- IMPORTANCE_KEY to managerCompat.importance
101
- ).apply {
102
- val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
103
- if (notificationManager != null) {
104
- putInt(INTERRUPTION_FILTER_KEY, notificationManager.currentInterruptionFilter)
105
- }
106
- }
107
159
 
108
160
  promise.resolve(
109
- bundleOf(
110
- PermissionsResponse.EXPIRES_KEY to PermissionsResponse.PERMISSION_EXPIRES_NEVER,
111
- PermissionsResponse.STATUS_KEY to status.status,
112
- PermissionsResponse.CAN_ASK_AGAIN_KEY to areEnabled,
113
- PermissionsResponse.GRANTED_KEY to (status == PermissionsStatus.GRANTED),
114
- ANDROID_RESPONSE_KEY to platformBundle
161
+ NotificationPermissionResponse(
162
+ status = status.status,
163
+ canAskAgain = areEnabled,
164
+ granted = status == PermissionsStatus.GRANTED,
165
+ android = getAndroidDetails(managerCompat)
115
166
  )
116
167
  )
117
168
  }
118
169
 
170
+ private fun getAndroidDetails(managerCompat: NotificationManagerCompat) =
171
+ AndroidNotificationPermissionDetails(
172
+ importance = managerCompat.importance,
173
+ interruptionFilter = managerCompat.currentInterruptionFilter
174
+ )
175
+
119
176
  @RequiresApi(33)
120
177
  private fun requestPermissionsWithPromiseImplApi33(promise: Promise) {
121
178
  permissions.askForPermissions(
@@ -56,6 +56,7 @@ open class NotificationsService : BroadcastReceiver() {
56
56
  private const val GET_SCHEDULED_TYPE = "getScheduled"
57
57
  private const val REMOVE_SELECTED_TYPE = "removeSelected"
58
58
  private const val REMOVE_ALL_TYPE = "removeAll"
59
+ private const val GROUPED_NOTIFICATION_DELETED_TYPE = "groupedNotificationDeleted"
59
60
 
60
61
  // Messages parts
61
62
  const val SUCCESS_CODE = 0
@@ -443,6 +444,28 @@ open class NotificationsService : BroadcastReceiver() {
443
444
  )
444
445
  }
445
446
 
447
+ fun createGroupedNotificationDeletedIntent(context: Context, notification: Notification): PendingIntent {
448
+ // The identifier in the URI keeps FLAG_UPDATE_CURRENT from replacing one child's extras with another's.
449
+ val intent = Intent(
450
+ NOTIFICATION_EVENT_ACTION,
451
+ getUriBuilderForIdentifier(notification.notificationRequest.identifier).appendPath("groupedDeleted").build()
452
+ ).also { intent ->
453
+ findDesignatedBroadcastReceiver(context, intent)?.let {
454
+ intent.component = ComponentName(it.packageName, it.name)
455
+ }
456
+ intent.putExtra(EVENT_TYPE_KEY, GROUPED_NOTIFICATION_DELETED_TYPE)
457
+ intent.putExtra(NOTIFICATION_KEY, notification)
458
+ // Byte-array copy: Parcelable extras can come back null from a PendingIntent, see #38908
459
+ marshalObject(notification)?.let { intent.putExtra(NOTIFICATION_BYTES_KEY, it) }
460
+ }
461
+ return PendingIntent.getBroadcast(
462
+ context,
463
+ 0,
464
+ intent,
465
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
466
+ )
467
+ }
468
+
446
469
  /**
447
470
  * Creates and returns a pending intent that will trigger [NotificationsService]'s "response received"
448
471
  * event.
@@ -694,6 +717,8 @@ open class NotificationsService : BroadcastReceiver() {
694
717
 
695
718
  TRIGGER_TYPE -> onNotificationTriggered(context, intent)
696
719
 
720
+ GROUPED_NOTIFICATION_DELETED_TYPE -> onGroupedNotificationDeleted(context, intent)
721
+
697
722
  else -> throw IllegalArgumentException("Received event of unrecognized type: $eventType. Ignoring.")
698
723
  }
699
724
 
@@ -753,11 +778,24 @@ open class NotificationsService : BroadcastReceiver() {
753
778
  open fun onReceiveNotificationResponse(context: Context, intent: Intent) {
754
779
  val response = getNotificationResponseFromBroadcastIntent(intent)
755
780
  getHandlingDelegate(context).handleNotificationResponse(response)
781
+ // Auto-cancel on tap does not fire the delete intent. Action buttons do not dismiss.
782
+ val content = response.notification.notificationRequest.content
783
+ val isTap = response.actionIdentifier == NotificationResponse.DEFAULT_ACTION_IDENTIFIER
784
+ if (isTap && content.isAutoDismiss && content.group != null) {
785
+ getPresentationDelegate(context).removeOrphanedGroupSummaries(response.notification)
786
+ }
756
787
  }
757
788
 
758
789
  open fun onNotificationsDropped(context: Context, intent: Intent) =
759
790
  getHandlingDelegate(context).handleNotificationsDropped()
760
791
 
792
+ open fun onGroupedNotificationDeleted(context: Context, intent: Intent) {
793
+ val notification = intent.getParcelableExtra<Notification>(NOTIFICATION_KEY)
794
+ ?: unmarshalObject(Notification.CREATOR, intent.getByteArrayExtra(NOTIFICATION_BYTES_KEY))
795
+ ?: throw IllegalArgumentException("$NOTIFICATION_KEY not found in the intent extras.")
796
+ getPresentationDelegate(context).removeOrphanedGroupSummaries(notification)
797
+ }
798
+
761
799
  //endregion
762
800
 
763
801
  //region Category handling