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