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