1 package host.exp.exponent.notifications 2 3 import android.app.* 4 import android.content.Context 5 import android.content.Intent 6 import android.os.Build 7 import androidx.core.app.NotificationManagerCompat 8 import expo.modules.jsonutils.getNullable 9 import expo.modules.manifests.core.Manifest 10 import host.exp.exponent.Constants 11 import host.exp.exponent.analytics.EXL 12 import host.exp.exponent.di.NativeModuleDepsProvider 13 import host.exp.exponent.kernel.ExperienceKey 14 import host.exp.exponent.kernel.KernelConstants 15 import host.exp.exponent.storage.ExponentSharedPreferences 16 import host.exp.expoview.R 17 import org.json.JSONArray 18 import org.json.JSONException 19 import org.json.JSONObject 20 import java.util.* 21 import javax.inject.Inject 22 23 class ExponentNotificationManager(private val context: Context) { 24 @Inject 25 lateinit var exponentSharedPreferences: ExponentSharedPreferences 26 maybeCreateNotificationChannelGroupnull27 fun maybeCreateNotificationChannelGroup(manifest: Manifest) { 28 if (Constants.isStandaloneApp()) { 29 // currently we only support groups in the client, with one group per experience 30 return 31 } 32 33 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 34 try { 35 val experienceScopeKey = manifest.getScopeKey() 36 if (!notificationChannelGroupIds.contains(experienceScopeKey)) { 37 val name = manifest.getName() 38 val channelName = name ?: experienceScopeKey 39 val group = NotificationChannelGroup(experienceScopeKey, channelName) 40 context.getSystemService(NotificationManager::class.java).createNotificationChannelGroup(group) 41 notificationChannelGroupIds.add(experienceScopeKey) 42 } 43 } catch (e: Exception) { 44 EXL.e(TAG, "Could not create notification channel: " + e.message) 45 } 46 } 47 } 48 maybeCreateExpoPersistentNotificationChannelnull49 fun maybeCreateExpoPersistentNotificationChannel() { 50 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 51 if (isExpoPersistentNotificationCreated) { 52 return 53 } 54 55 val manager = context.getSystemService(NotificationManager::class.java) 56 val channel = NotificationChannel( 57 NotificationConstants.NOTIFICATION_EXPERIENCE_CHANNEL_ID, 58 context.getString(R.string.persistent_notification_channel_name), 59 NotificationManager.IMPORTANCE_DEFAULT 60 ).apply { 61 setSound(null, null) 62 description = context.getString(R.string.persistent_notification_channel_desc) 63 } 64 65 if (!Constants.isStandaloneApp()) { 66 val group = NotificationChannelGroup( 67 NotificationConstants.NOTIFICATION_EXPERIENCE_CHANNEL_GROUP_ID, 68 context.getString(R.string.persistent_notification_channel_group) 69 ) 70 manager.createNotificationChannelGroup(group) 71 channel.group = NotificationConstants.NOTIFICATION_EXPERIENCE_CHANNEL_GROUP_ID 72 } 73 74 manager.createNotificationChannel(channel) 75 76 isExpoPersistentNotificationCreated = true 77 } 78 } 79 createNotificationChannelnull80 fun createNotificationChannel(experienceKey: ExperienceKey, channel: NotificationChannel) { 81 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 82 if (!Constants.isStandaloneApp()) { 83 channel.group = experienceKey.scopeKey 84 } 85 86 context.getSystemService(NotificationManager::class.java).createNotificationChannel(channel) 87 } 88 } 89 saveChannelSettingsnull90 fun saveChannelSettings( 91 experienceKey: ExperienceKey, 92 channelId: String, 93 details: Map<*, *> 94 ) { 95 try { 96 val metadata = exponentSharedPreferences.getExperienceMetadata(experienceKey) ?: JSONObject() 97 val allChannels: JSONObject = metadata.getNullable(ExponentSharedPreferences.EXPERIENCE_METADATA_NOTIFICATION_CHANNELS) ?: JSONObject() 98 allChannels.put(channelId, JSONObject(details)) 99 metadata.put(ExponentSharedPreferences.EXPERIENCE_METADATA_NOTIFICATION_CHANNELS, allChannels) 100 exponentSharedPreferences.updateExperienceMetadata(experienceKey, metadata) 101 } catch (e: JSONException) { 102 EXL.e(TAG, "Could not store channel in shared preferences: " + e.message) 103 } 104 } 105 readChannelSettingsnull106 fun readChannelSettings(experienceKey: ExperienceKey, channelId: String?): JSONObject? { 107 try { 108 val metadata = exponentSharedPreferences.getExperienceMetadata(experienceKey) ?: JSONObject() 109 val allChannels: JSONObject = metadata.getNullable(ExponentSharedPreferences.EXPERIENCE_METADATA_NOTIFICATION_CHANNELS) ?: JSONObject() 110 return allChannels.optJSONObject(channelId) 111 } catch (e: JSONException) { 112 EXL.e(TAG, "Could not read channel from shared preferences: " + e.message) 113 } 114 return null 115 } 116 getNotificationChannelnull117 fun getNotificationChannel( 118 experienceKey: ExperienceKey, 119 channelId: String 120 ): NotificationChannel? { 121 return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 122 context.getSystemService(NotificationManager::class.java) 123 .getNotificationChannel( 124 getScopedChannelId( 125 experienceKey, 126 channelId 127 ) 128 ) 129 } else { 130 null 131 } 132 } 133 deleteNotificationChannelnull134 fun deleteNotificationChannel(experienceKey: ExperienceKey, channelId: String) { 135 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 136 context.getSystemService(NotificationManager::class.java).deleteNotificationChannel( 137 getScopedChannelId(experienceKey, channelId) 138 ) 139 } 140 } 141 notifynull142 fun notify(experienceKey: ExperienceKey, id: Int, notification: Notification) { 143 NotificationManagerCompat.from(context).notify(experienceKey.scopeKey, id, notification) 144 try { 145 val metadata = exponentSharedPreferences.getExperienceMetadata(experienceKey) ?: JSONObject() 146 val notifications = metadata.optJSONArray(ExponentSharedPreferences.EXPERIENCE_METADATA_ALL_NOTIFICATION_IDS) ?: JSONArray() 147 notifications.put(id) 148 metadata.put(ExponentSharedPreferences.EXPERIENCE_METADATA_ALL_NOTIFICATION_IDS, notifications) 149 150 exponentSharedPreferences.updateExperienceMetadata(experienceKey, metadata) 151 } catch (e: JSONException) { 152 e.printStackTrace() 153 } 154 } 155 cancelnull156 fun cancel(experienceKey: ExperienceKey, id: Int) { 157 NotificationManagerCompat.from(context).cancel(experienceKey.scopeKey, id) 158 try { 159 val metadata = exponentSharedPreferences.getExperienceMetadata(experienceKey) ?: return 160 val oldNotifications = metadata.optJSONArray(ExponentSharedPreferences.EXPERIENCE_METADATA_ALL_NOTIFICATION_IDS) ?: return 161 val newNotifications = JSONArray() 162 for (i in 0 until oldNotifications.length()) { 163 if (oldNotifications.getInt(i) != id) { 164 newNotifications.put(oldNotifications.getInt(i)) 165 } 166 } 167 metadata.put(ExponentSharedPreferences.EXPERIENCE_METADATA_ALL_NOTIFICATION_IDS, newNotifications) 168 169 exponentSharedPreferences.updateExperienceMetadata(experienceKey, metadata) 170 } catch (e: JSONException) { 171 e.printStackTrace() 172 } 173 } 174 cancelAllnull175 fun cancelAll(experienceKey: ExperienceKey) { 176 try { 177 val metadata = exponentSharedPreferences.getExperienceMetadata(experienceKey) ?: return 178 val notifications = metadata.optJSONArray(ExponentSharedPreferences.EXPERIENCE_METADATA_ALL_NOTIFICATION_IDS) ?: return 179 val manager = NotificationManagerCompat.from(context) 180 for (i in 0 until notifications.length()) { 181 manager.cancel(experienceKey.scopeKey, notifications.getInt(i)) 182 } 183 metadata.put(ExponentSharedPreferences.EXPERIENCE_METADATA_ALL_NOTIFICATION_IDS, null) 184 metadata.put(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS, null) 185 186 exponentSharedPreferences.updateExperienceMetadata(experienceKey, metadata) 187 } catch (e: JSONException) { 188 e.printStackTrace() 189 } 190 } 191 getAllNotificationsIdsnull192 fun getAllNotificationsIds(experienceKey: ExperienceKey): List<Int> { 193 return try { 194 val metadata = exponentSharedPreferences.getExperienceMetadata(experienceKey) ?: return emptyList() 195 val notifications = metadata.optJSONArray(ExponentSharedPreferences.EXPERIENCE_METADATA_ALL_NOTIFICATION_IDS) ?: return emptyList() 196 val notificationsIds = mutableListOf<Int>() 197 for (i in 0 until notifications.length()) { 198 notificationsIds.add(notifications.getInt(i)) 199 } 200 notificationsIds 201 } catch (e: JSONException) { 202 e.printStackTrace() 203 emptyList() 204 } 205 } 206 207 @Throws(ClassNotFoundException::class) schedulenull208 fun schedule( 209 experienceKey: ExperienceKey, 210 id: Int, 211 details: HashMap<*, *>?, 212 time: Long, 213 interval: Long? 214 ) { 215 val notificationIntent = Intent(context, ScheduledNotificationReceiver::class.java).apply { 216 type = experienceKey.scopeKey 217 action = id.toString() 218 putExtra(KernelConstants.NOTIFICATION_ID_KEY, id) 219 putExtra(KernelConstants.NOTIFICATION_OBJECT_KEY, details) 220 } 221 222 // We're defaulting to the behaviour prior API 31 (mutable) even though Android recommends immutability 223 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0 224 val pendingIntent = PendingIntent.getBroadcast(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag) 225 val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager 226 if (interval != null) { 227 alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, time, interval, pendingIntent) 228 } else { 229 alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, time, pendingIntent) 230 } 231 try { 232 val metadata = exponentSharedPreferences.getExperienceMetadata(experienceKey) ?: JSONObject() 233 val notifications = metadata.optJSONArray(ExponentSharedPreferences.EXPERIENCE_METADATA_ALL_SCHEDULED_NOTIFICATION_IDS) ?: JSONArray() 234 notifications.put(id) 235 metadata.put(ExponentSharedPreferences.EXPERIENCE_METADATA_ALL_SCHEDULED_NOTIFICATION_IDS, notifications) 236 237 exponentSharedPreferences.updateExperienceMetadata(experienceKey, metadata) 238 } catch (e: JSONException) { 239 e.printStackTrace() 240 } 241 } 242 243 @Throws(ClassNotFoundException::class) cancelSchedulednull244 fun cancelScheduled(experienceKey: ExperienceKey, id: Int) { 245 val notificationIntent = Intent(context, ScheduledNotificationReceiver::class.java).apply { 246 type = experienceKey.scopeKey 247 action = id.toString() 248 } 249 // We're defaulting to the behaviour prior API 31 (mutable) even though Android recommends immutability 250 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0 251 val pendingIntent = PendingIntent.getBroadcast(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag) 252 val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager 253 alarmManager.cancel(pendingIntent) 254 cancel(experienceKey, id) 255 } 256 257 @Throws(ClassNotFoundException::class) cancelAllSchedulednull258 fun cancelAllScheduled(experienceKey: ExperienceKey) { 259 try { 260 val metadata = exponentSharedPreferences.getExperienceMetadata(experienceKey) ?: return 261 val notifications = metadata.optJSONArray(ExponentSharedPreferences.EXPERIENCE_METADATA_ALL_SCHEDULED_NOTIFICATION_IDS) ?: return 262 for (i in 0 until notifications.length()) { 263 cancelScheduled(experienceKey, notifications.getInt(i)) 264 } 265 metadata.put(ExponentSharedPreferences.EXPERIENCE_METADATA_ALL_SCHEDULED_NOTIFICATION_IDS, null) 266 267 exponentSharedPreferences.updateExperienceMetadata(experienceKey, metadata) 268 } catch (e: JSONException) { 269 e.printStackTrace() 270 } 271 } 272 273 companion object { 274 private val TAG = ExponentNotificationManager::class.java.simpleName 275 276 private val notificationChannelGroupIds = mutableSetOf<String>() 277 278 private var isExpoPersistentNotificationCreated = false 279 getScopedChannelIdnull280 fun getScopedChannelId(experienceKey: ExperienceKey, channelId: String): String { 281 return if (Constants.isStandaloneApp()) { 282 channelId 283 } else { 284 experienceKey.scopeKey + "/" + channelId 285 } 286 } 287 } 288 289 init { 290 NativeModuleDepsProvider.instance.inject(ExponentNotificationManager::class.java, this) 291 } 292 } 293