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