diff options
author | Allan Wang <me@allanwang.ca> | 2018-03-11 15:47:35 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-03-11 15:47:35 -0400 |
commit | 67988a25d83fc10b187fcc821c3ceacfad0195d5 (patch) | |
tree | 58d6622f3eccf0275d55ee7f6ef2ba76845211ab /app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt | |
parent | 7f74e08270e34eb66a824ac18519293d4a42278c (diff) | |
download | frost-67988a25d83fc10b187fcc821c3ceacfad0195d5.tar.gz frost-67988a25d83fc10b187fcc821c3ceacfad0195d5.tar.bz2 frost-67988a25d83fc10b187fcc821c3ceacfad0195d5.zip |
Fix/notification sound (#763)
* Update dependencies
* Update theme
* Fix bad css merge
* Add notification feedback for fetching now
* Hide notif settings that no longer work with channels
* Wip android o channels
* Revert back to old group method
* Update dependencies
* Update rxnetwork version
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.kt | 172 |
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()) |