aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt')
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt172
1 files changed, 125 insertions, 47 deletions
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt
index 08cf321c..b6427e5b 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt
@@ -1,5 +1,6 @@
package com.pitchedapps.frost.services
+import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
@@ -13,12 +14,13 @@ import android.net.Uri
import android.os.BaseBundle
import android.os.Build
import android.os.Bundle
+import android.os.PersistableBundle
+import android.support.annotation.RequiresApi
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationManagerCompat
import ca.allanwang.kau.utils.color
import ca.allanwang.kau.utils.dpToPx
import ca.allanwang.kau.utils.string
-import com.pitchedapps.frost.BuildConfig
import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.FrostWebActivity
import com.pitchedapps.frost.dbflow.CookieModel
@@ -43,32 +45,66 @@ import java.util.*
*
* Logic for build notifications, scheduling notifications, and showing notifications
*/
+const val NOTIF_CHANNEL_GENERAL = "general"
+const val NOTIF_CHANNEL_MESSAGES = "messages"
+
fun setupNotificationChannels(c: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val manager = c.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- val mainChannel = NotificationChannel(BuildConfig.APPLICATION_ID, c.getString(R.string.frost_name), NotificationManager.IMPORTANCE_DEFAULT)
- mainChannel.lightColor = c.color(R.color.facebook_blue)
- mainChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
- manager.createNotificationChannel(mainChannel)
+ val appName = c.string(R.string.frost_name)
+ val msg = c.string(R.string.messages)
+ manager.notificationChannels
+ .filter { it.id != NOTIF_CHANNEL_GENERAL && it.id != NOTIF_CHANNEL_MESSAGES }
+ .forEach { manager.deleteNotificationChannel(it.id) }
+ manager.createNotificationChannel(NOTIF_CHANNEL_GENERAL, appName)
+ manager.createNotificationChannel(NOTIF_CHANNEL_MESSAGES, "$appName: $msg")
+ L.d { "Created notification channels: ${manager.notificationChannels.size} channels, ${manager.notificationChannelGroups.size} groups" }
}
-inline val Context.frostNotification: NotificationCompat.Builder
- get() = NotificationCompat.Builder(this, BuildConfig.APPLICATION_ID).apply {
- setSmallIcon(R.drawable.frost_f_24)
- setAutoCancel(true)
- setStyle(NotificationCompat.BigTextStyle())
- color = color(R.color.frost_notification_accent)
- }
+@RequiresApi(Build.VERSION_CODES.O)
+private fun NotificationManager.createNotificationChannel(id: String, name: String): NotificationChannel {
+ val channel = NotificationChannel(id,
+ name, NotificationManager.IMPORTANCE_DEFAULT)
+ channel.enableLights(true)
+ channel.lightColor = Prefs.accentColor
+ channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
+ createNotificationChannel(channel)
+ return channel
+}
-fun NotificationCompat.Builder.withDefaults(ringtone: String = Prefs.notificationRingtone) = apply {
- var defaults = 0
- if (Prefs.notificationVibrate) defaults = defaults or Notification.DEFAULT_VIBRATE
- if (Prefs.notificationSound) {
- if (ringtone.isNotBlank()) setSound(Uri.parse(ringtone))
- else defaults = defaults or Notification.DEFAULT_SOUND
+fun Context.frostNotification(id: String) =
+ NotificationCompat.Builder(this, id)
+ .apply {
+ setSmallIcon(R.drawable.frost_f_24)
+ setAutoCancel(true)
+ setOnlyAlertOnce(true)
+ setStyle(NotificationCompat.BigTextStyle())
+ color = color(R.color.frost_notification_accent)
+ }
+
+/**
+ * Dictates whether a notification should have sound/vibration/lights or not
+ * Delegates to channels if Android O and up
+ * Otherwise uses our provided preferences
+ */
+fun NotificationCompat.Builder.setFrostAlert(enable: Boolean, ringtone: String): NotificationCompat.Builder {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ setGroupAlertBehavior(
+ if (enable) Notification.GROUP_ALERT_CHILDREN
+ else Notification.GROUP_ALERT_SUMMARY)
+ } else if (!enable) {
+ setDefaults(0)
+ } else {
+ var defaults = 0
+ if (Prefs.notificationVibrate) defaults = defaults or Notification.DEFAULT_VIBRATE
+ if (Prefs.notificationSound) {
+ if (ringtone.isNotBlank()) setSound(Uri.parse(ringtone))
+ else defaults = defaults or Notification.DEFAULT_SOUND
+ }
+ if (Prefs.notificationLights) defaults = defaults or Notification.DEFAULT_LIGHTS
+ setDefaults(defaults)
}
- if (Prefs.notificationLights) defaults = defaults or Notification.DEFAULT_LIGHTS
- setDefaults(defaults)
+ return this
}
private val _40_DP = 40.dpToPx
@@ -77,6 +113,7 @@ private val _40_DP = 40.dpToPx
* Enum to handle notification creations
*/
enum class NotificationType(
+ private val channelId: String,
private val overlayContext: OverlayContext,
private val fbItem: FbItem,
private val parser: FrostParser<ParseNotification>,
@@ -84,7 +121,8 @@ enum class NotificationType(
private val putTime: (notif: NotificationModel, time: Long) -> NotificationModel,
private val ringtone: () -> String) {
- GENERAL(OverlayContext.NOTIFICATION,
+ GENERAL(NOTIF_CHANNEL_GENERAL,
+ OverlayContext.NOTIFICATION,
FbItem.NOTIFICATIONS,
NotifParser,
NotificationModel::epoch,
@@ -95,7 +133,8 @@ enum class NotificationType(
FrostRunnable.prepareMarkNotificationRead(content.id, cookie)
},
- MESSAGE(OverlayContext.MESSAGE,
+ MESSAGE(NOTIF_CHANNEL_MESSAGES,
+ OverlayContext.MESSAGE,
FbItem.MESSAGES,
MessageParser,
NotificationModel::epochIm,
@@ -121,36 +160,48 @@ enum class NotificationType(
* Get unread data from designated parser
* Display notifications for those after old epoch
* Save new epoch
+ *
+ * Returns the number of notifications generated,
+ * or -1 if an error occurred
*/
- fun fetch(context: Context, data: CookieModel) {
+ fun fetch(context: Context, data: CookieModel): Int {
val response = parser.parse(data.cookie)
- ?: return L.v { "$name notification data not found" }
- val notifs = response.data.getUnreadNotifications(data).filter {
+ if (response == null) {
+ L.v { "$name notification data not found" }
+ return -1
+ }
+ val notifContents = response.data.getUnreadNotifications(data).filter {
val text = it.text
Prefs.notificationKeywords.none { text.contains(it, true) }
}
- if (notifs.isEmpty()) return
- var notifCount = 0
+ if (notifContents.isEmpty()) return 0
val userId = data.id
val prevNotifTime = lastNotificationTime(userId)
val prevLatestEpoch = getTime(prevNotifTime)
L.v { "Notif $name prev epoch $prevLatestEpoch" }
var newLatestEpoch = prevLatestEpoch
- notifs.forEach { notif ->
+ val notifs = mutableListOf<FrostNotification>()
+ notifContents.forEach { notif ->
L.v { "Notif timestamp ${notif.timestamp}" }
if (notif.timestamp <= prevLatestEpoch) return@forEach
- createNotification(context, notif, notifCount == 0)
+ notifs.add(createNotification(context, notif))
if (notif.timestamp > newLatestEpoch)
newLatestEpoch = notif.timestamp
- notifCount++
}
if (newLatestEpoch > prevLatestEpoch)
putTime(prevNotifTime, newLatestEpoch).save()
L.d { "Notif $name new epoch ${getTime(lastNotificationTime(userId))}" }
- summaryNotification(context, userId, notifCount)
+ frostAnswersCustom("Notifications", "Type" to name, "Count" to notifs.size)
+ if (notifs.size > 1)
+ summaryNotification(context, userId, notifs.size).notify(context)
+ val ringtone = ringtone()
+ notifs.forEachIndexed { i, notif ->
+ notif.withAlert(i < 2, ringtone).notify(context)
+ }
+ return notifs.size
}
- private fun debugNotification(context: Context, data: CookieModel) {
+ fun debugNotification(context: Context, data: CookieModel) {
val content = NotificationContent(data,
System.currentTimeMillis(),
"https://github.com/AllanWang/Frost-for-Facebook",
@@ -158,15 +209,13 @@ enum class NotificationType(
"Test 123",
System.currentTimeMillis() / 1000,
"https://www.iconexperience.com/_img/v_collection_png/256x256/shadow/dog.png")
- createNotification(context, content, true)
+ createNotification(context, content).notify(context)
}
/**
* Create and submit a new notification with the given [content]
- * If [withDefaults] is set, it will also add the appropriate sound, vibration, and light
- * Note that when we have multiple notifications coming in at once, we don't want to have defaults for all of them
*/
- private fun createNotification(context: Context, content: NotificationContent, withDefaults: Boolean) {
+ private fun createNotification(context: Context, content: NotificationContent): FrostNotification {
with(content) {
val intent = Intent(context, FrostWebActivity::class.java)
intent.data = Uri.parse(href)
@@ -176,7 +225,7 @@ enum class NotificationType(
val group = "${groupPrefix}_${data.id}"
val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
- val notifBuilder = context.frostNotification
+ val notifBuilder = context.frostNotification(channelId)
.setContentTitle(title ?: context.string(R.string.frost_name))
.setContentText(text)
.setContentIntent(pendingIntent)
@@ -184,9 +233,6 @@ enum class NotificationType(
.setSubText(data.name)
.setGroup(group)
- if (withDefaults)
- notifBuilder.withDefaults(ringtone())
-
if (timestamp != -1L) notifBuilder.setWhen(timestamp * 1000)
L.v { "Notif load $content" }
@@ -204,32 +250,37 @@ enum class NotificationType(
}
}
- NotificationManagerCompat.from(context).notify(group, notifId, notifBuilder.build())
+ return FrostNotification(group, notifId, notifBuilder)
}
}
+
/**
* Create a summary notification to wrap the previous ones
* This will always produce sound, vibration, and lights based on preferences
* and will only show if we have at least 2 notifications
*/
- private fun summaryNotification(context: Context, userId: Long, count: Int) {
- frostAnswersCustom("Notifications", "Type" to name, "Count" to count)
- if (count <= 1) return
+ private fun summaryNotification(context: Context, userId: Long, count: Int): FrostNotification {
val intent = Intent(context, FrostWebActivity::class.java)
intent.data = Uri.parse(fbItem.url)
intent.putExtra(ARG_USER_ID, userId)
+ val group = "${groupPrefix}_$userId"
val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
- val notifBuilder = context.frostNotification.withDefaults(ringtone())
+ val notifBuilder = context.frostNotification(channelId)
.setContentTitle(context.string(R.string.frost_name))
.setContentText("$count ${context.string(fbItem.titleId)}")
- .setGroup("${groupPrefix}_$userId")
+ .setGroup(group)
.setGroupSummary(true)
.setContentIntent(pendingIntent)
.setCategory(Notification.CATEGORY_SOCIAL)
- NotificationManagerCompat.from(context).notify("${groupPrefix}_$userId", userId.toInt(), notifBuilder.build())
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ notifBuilder.setGroupAlertBehavior(Notification.GROUP_ALERT_CHILDREN)
+ }
+
+ return FrostNotification(group, 1, notifBuilder)
}
+
}
/**
@@ -247,6 +298,31 @@ data class NotificationContent(val data: CookieModel,
}
+/**
+ * Wrapper for a complete notification builder and identifier
+ * which can be immediately notified when given a [Context]
+ */
+data class FrostNotification(private val tag: String,
+ private val id: Int,
+ val notif: NotificationCompat.Builder) {
+
+ fun withAlert(enable: Boolean, ringtone: String): FrostNotification {
+ notif.setFrostAlert(enable, ringtone)
+ return this
+ }
+
+ fun notify(context: Context) =
+ NotificationManagerCompat.from(context).notify(tag, id, notif.build())
+}
+
+const val NOTIFICATION_PARAM_ID = "notif_param_id"
+
+private fun JobInfo.Builder.setExtras(id: Int): JobInfo.Builder {
+ val bundle = PersistableBundle()
+ bundle.putInt(NOTIFICATION_PARAM_ID, id)
+ return setExtras(bundle)
+}
+
const val NOTIFICATION_PERIODIC_JOB = 7
/**
@@ -260,6 +336,7 @@ fun Context.scheduleNotifications(minutes: Long): Boolean {
val serviceComponent = ComponentName(this, NotificationService::class.java)
val builder = JobInfo.Builder(NOTIFICATION_PERIODIC_JOB, serviceComponent)
.setPeriodic(minutes * 60000)
+ .setExtras(NOTIFICATION_PERIODIC_JOB)
.setPersisted(true)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) //TODO add options
val result = scheduler.schedule(builder.build())
@@ -280,6 +357,7 @@ fun Context.fetchNotifications(): Boolean {
val serviceComponent = ComponentName(this, NotificationService::class.java)
val builder = JobInfo.Builder(NOTIFICATION_JOB_NOW, serviceComponent)
.setMinimumLatency(0L)
+ .setExtras(NOTIFICATION_JOB_NOW)
.setOverrideDeadline(2000L)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
val result = scheduler.schedule(builder.build())