From 67988a25d83fc10b187fcc821c3ceacfad0195d5 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Sun, 11 Mar 2018 15:47:35 -0400 Subject: 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 --- .../main/kotlin/com/pitchedapps/frost/FrostApp.kt | 4 +- .../pitchedapps/frost/fragments/WebFragments.kt | 2 - .../pitchedapps/frost/services/DownloadService.kt | 190 --------------------- .../frost/services/FrostNotifications.kt | 172 ++++++++++++++----- .../frost/services/NotificationService.kt | 40 +++-- .../pitchedapps/frost/settings/Notifications.kt | 97 +++++++---- .../kotlin/com/pitchedapps/frost/utils/Prefs.kt | 2 +- 7 files changed, 221 insertions(+), 286 deletions(-) delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/services/DownloadService.kt (limited to 'app/src/main/kotlin/com') diff --git a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt index bb9c45c0..36b51753 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt @@ -77,10 +77,10 @@ class FrostApp : Application() { super.onCreate() - applicationContext.scheduleNotifications(Prefs.notificationFreq) - setupNotificationChannels(applicationContext) + applicationContext.scheduleNotifications(Prefs.notificationFreq) + /** * Drawer profile loading logic * Reload the image on every version update diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragments.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragments.kt index 8000c106..d9edda78 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragments.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragments.kt @@ -30,7 +30,6 @@ class WebFragment : BaseFragment() { } override fun updateFab(contract: MainFabContract) { - L.e { "Update fab" } val web = core as? WebView if (web == null) { L.e { "Webview not found in fragment $baseEnum" } @@ -40,7 +39,6 @@ class WebFragment : BaseFragment() { contract.showFab(GoogleMaterial.Icon.gmd_edit) { JsActions.CREATE_POST.inject(web) } - L.e { "UPP" } return } super.updateFab(contract) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/DownloadService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/DownloadService.kt deleted file mode 100644 index 25f10398..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/DownloadService.kt +++ /dev/null @@ -1,190 +0,0 @@ -package com.pitchedapps.frost.services - -import android.annotation.SuppressLint -import android.app.IntentService -import android.app.Notification -import android.app.PendingIntent -import android.app.Service -import android.content.Context -import android.content.Intent -import android.support.v4.app.NotificationCompat -import android.support.v4.app.NotificationManagerCompat -import ca.allanwang.kau.utils.copyFromInputStream -import ca.allanwang.kau.utils.string -import com.pitchedapps.frost.R -import com.pitchedapps.frost.utils.L -import com.pitchedapps.frost.utils.createMediaFile -import com.pitchedapps.frost.utils.frostUriFromFile -import okhttp3.MediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.ResponseBody -import okio.* -import org.jetbrains.anko.toast -import java.io.File - -/** - * Created by Allan Wang on 2017-08-08. - * - * Not in use - * - * Background file downloader - * All we are given is a link and a mime type - * - * With reference to the OkHttp3 sample - */ -@SuppressLint("Registered") -class DownloadService : IntentService("FrostVideoDownloader") { - - companion object { - const val EXTRA_URL = "download_url" - private const val MAX_PROGRESS = 1000 - private const val DOWNLOAD_GROUP = "frost_downloads" - } - - val client: OkHttpClient by lazy { initClient() } - - val start = System.currentTimeMillis() - var totalSize = 0L - val downloaded = mutableSetOf() - - private lateinit var notifBuilder: NotificationCompat.Builder - private var notifId: Int = -1 - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - if (intent != null && intent.flags == PendingIntent.FLAG_CANCEL_CURRENT) { - L.i { "Cancelling download service" } - cancelDownload() - return Service.START_NOT_STICKY - } - return super.onStartCommand(intent, flags, startId) - } - - override fun onHandleIntent(intent: Intent?) { - val url: String = intent?.getStringExtra(EXTRA_URL) ?: return - - if (downloaded.contains(url)) return - - val request: Request = Request.Builder() - .url(url) - .build() - - notifBuilder = frostNotification - notifId = Math.abs(url.hashCode() + System.currentTimeMillis().toInt()) - val cancelIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT) - - notifBuilder.setContentTitle(string(R.string.downloading_video)) - .setCategory(Notification.CATEGORY_PROGRESS) - .setWhen(System.currentTimeMillis()) - .setProgress(MAX_PROGRESS, 0, false) - .setOngoing(true) - .addAction(R.drawable.ic_action_cancel, string(R.string.kau_cancel), cancelIntent) - .setGroup(DOWNLOAD_GROUP) - - client.newCall(request).execute().use { response -> - if (!response.isSuccessful) { - L.e { "Video download failed" } - toast("Video download failed") - return@use - } - - val stream = response.body()?.byteStream() ?: return@use - val extension = response.request().body()?.contentType()?.subtype() - val destination = createMediaFile(if (extension == null) "" else ".$extension") - destination.copyFromInputStream(stream) - - notifBuilder.setContentIntent(getPendingIntent(this, destination)) - notifBuilder.show() - } - } - - private fun NotificationCompat.Builder.show() { - NotificationManagerCompat.from(this@DownloadService).notify(DOWNLOAD_GROUP, notifId, build()) - } - - - private fun getPendingIntent(context: Context, file: File): PendingIntent { - val uri = context.frostUriFromFile(file) - val type = context.contentResolver.getType(uri) - L.i { "DownloadType: retrieved pending intent" } - L._i { "Contents: $uri $type" } - val intent = Intent(Intent.ACTION_VIEW, uri) - .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - .setDataAndType(uri, type) - return PendingIntent.getActivity(context, 0, intent, 0) - } - - /** - * Adds url to downloaded list and modifies the notif builder for the finished state - * Does not show the new notification - */ - private fun finishDownload(url: String) { - L.i { "Video download finished" } - downloaded.add(url) - notifBuilder.setContentTitle(string(R.string.downloaded_video)) - .setProgress(0, 0, false).setOngoing(false).setAutoCancel(true) - .apply { mActions.clear() } - } - - private fun cancelDownload() { - client.dispatcher().cancelAll() - NotificationManagerCompat.from(this).cancel(DOWNLOAD_GROUP, notifId) - } - - private fun onProgressUpdate(url: String, type: MediaType?, percentage: Float, done: Boolean) { - L.v { "Download request progress $percentage for $url" } - notifBuilder.setProgress(MAX_PROGRESS, (percentage * MAX_PROGRESS).toInt(), false) - if (done) finishDownload(url) - notifBuilder.show() - } - - override fun onTaskRemoved(rootIntent: Intent?) { - super.onTaskRemoved(rootIntent) - } - - private fun initClient(): OkHttpClient = OkHttpClient.Builder() - .addNetworkInterceptor { chain -> - val original = chain.proceed(chain.request()) - val body = original.body() ?: return@addNetworkInterceptor original - if (body.contentLength() > 0L) totalSize += body.contentLength() - return@addNetworkInterceptor original.newBuilder() - .body(ProgressResponseBody( - original.request().url().toString(), - body, - this@DownloadService::onProgressUpdate)) - .build() - } - .build() - - private class ProgressResponseBody( - val url: String, - val responseBody: ResponseBody, - val listener: (url: String, type: MediaType?, percentage: Float, done: Boolean) -> Unit) : ResponseBody() { - - private val bufferedSource: BufferedSource by lazy { Okio.buffer(source(responseBody.source())) } - - override fun contentLength(): Long = responseBody.contentLength() - - override fun contentType(): MediaType? = responseBody.contentType() - - override fun source(): BufferedSource = bufferedSource - - private fun source(source: Source): Source = object : ForwardingSource(source) { - - private var totalBytesRead = 0L - - override fun read(sink: Buffer, byteCount: Long): Long { - val bytesRead = super.read(sink, byteCount) - // read() returns the number of bytes read, or -1 if this source is exhausted. - totalBytesRead += if (bytesRead != -1L) bytesRead else 0 - listener( - url, - contentType(), - totalBytesRead.toFloat() / responseBody.contentLength(), - bytesRead == -1L - ) - return bytesRead - } - } - } -} \ No newline at end of file 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, @@ -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() + 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()) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt index fc946772..6ba968e7 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt @@ -2,11 +2,11 @@ package com.pitchedapps.frost.services import android.app.job.JobParameters import android.app.job.JobService -import android.content.Context import android.support.v4.app.NotificationManagerCompat import ca.allanwang.kau.utils.string import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R +import com.pitchedapps.frost.dbflow.CookieModel import com.pitchedapps.frost.dbflow.loadFbCookiesSync import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs @@ -24,9 +24,9 @@ import java.util.concurrent.Future */ class NotificationService : JobService() { - var future: Future? = null + private var future: Future? = null - val startTime = System.currentTimeMillis() + private val startTime = System.currentTimeMillis() override fun onStopJob(params: JobParameters?): Boolean { val time = System.currentTimeMillis() - startTime @@ -59,31 +59,51 @@ class NotificationService : JobService() { ?: return@doAsync L.eThrow("NotificationService had null weakRef to self") val currentId = Prefs.userId val cookies = loadFbCookiesSync() + val jobId = params?.extras?.getInt(NOTIFICATION_PARAM_ID, -1) ?: -1 + var notifCount = 0 cookies.forEach { val current = it.id == currentId if (Prefs.notificationsGeneral && (current || Prefs.notificationAllAccounts)) - NotificationType.GENERAL.fetch(context, it) + notifCount += fetch(jobId, NotificationType.GENERAL, it) if (Prefs.notificationsInstantMessages && (current || Prefs.notificationsImAllAccounts)) - NotificationType.MESSAGE.fetch(context, it) + notifCount += fetch(jobId, NotificationType.MESSAGE, it) } + + if (notifCount == 0 && jobId == NOTIFICATION_JOB_NOW) + generalNotification(665, R.string.no_new_notifications, BuildConfig.DEBUG) + finish(params) } return true } + /** + * Implemented fetch to also notify when an error occurs + * Also normalized the output to return the number of notifications received + */ + private fun fetch(jobId: Int, type: NotificationType, cookie: CookieModel): Int { + val count = type.fetch(this, cookie) + if (count < 0) { + if (jobId == NOTIFICATION_JOB_NOW) + generalNotification(666, R.string.error_notification, BuildConfig.DEBUG) + return 0 + } + return count + } + private fun logNotif(text: String): NotificationContent? { L.eThrow("NotificationService: $text") return null } - private fun Context.debugNotification(text: String = string(R.string.kau_lorem_ipsum)) { - if (!BuildConfig.DEBUG) return - val notifBuilder = frostNotification.withDefaults() + private fun generalNotification(id: Int, textRes: Int, withDefaults: Boolean) { + val notifBuilder = frostNotification(NOTIF_CHANNEL_GENERAL) + .setFrostAlert(withDefaults, Prefs.notificationRingtone) .setContentTitle(string(R.string.frost_name)) - .setContentText(text) - NotificationManagerCompat.from(this).notify(999, notifBuilder.build()) + .setContentText(string(textRes)) + NotificationManagerCompat.from(this).notify(id, notifBuilder.build()) } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt index 2ee086c0..962e60ae 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt @@ -1,14 +1,20 @@ package com.pitchedapps.frost.settings +import android.annotation.SuppressLint import android.content.Intent import android.media.RingtoneManager import android.net.Uri +import android.os.Build +import android.provider.Settings import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder import ca.allanwang.kau.kpref.activity.items.KPrefText import ca.allanwang.kau.utils.minuteToText import ca.allanwang.kau.utils.string +import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.SettingsActivity +import com.pitchedapps.frost.dbflow.NotificationModel +import com.pitchedapps.frost.dbflow.loadFbCookiesAsync import com.pitchedapps.frost.services.fetchNotifications import com.pitchedapps.frost.services.scheduleNotifications import com.pitchedapps.frost.utils.Prefs @@ -20,6 +26,7 @@ import com.pitchedapps.frost.views.Keywords /** * Created by Allan Wang on 2017-06-29. */ +@SuppressLint("InlinedApi") fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = { text(R.string.notification_frequency, Prefs::notificationFreq, { Prefs.notificationFreq = it }) { @@ -90,47 +97,69 @@ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = { enabler = Prefs::notificationsInstantMessages } - checkbox(R.string.notification_sound, Prefs::notificationSound, { - Prefs.notificationSound = it - reloadByTitle(R.string.notification_ringtone, - R.string.message_ringtone) - }) - - fun KPrefText.KPrefTextContract.ringtone(code: Int) { - enabler = Prefs::notificationSound - textGetter = { - if (it.isBlank()) string(R.string.kau_default) - else RingtoneManager.getRingtone(this@getNotificationPrefs, Uri.parse(it)) - ?.getTitle(this@getNotificationPrefs) ?: "---" //todo figure out why this happens + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + plainText(R.string.notification_channel) { + descRes = R.string.notification_channel_desc + onClick = { + val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, packageName) + startActivity(intent) + } } - onClick = { - val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply { - putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, string(R.string.select_ringtone)) - putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false) - putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true) - putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION) - if (item.pref.isNotBlank()) - putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, Uri.parse(item.pref)) + } else { + checkbox(R.string.notification_sound, Prefs::notificationSound, { + Prefs.notificationSound = it + reloadByTitle(R.string.notification_ringtone, + R.string.message_ringtone) + }) + + fun KPrefText.KPrefTextContract.ringtone(code: Int) { + enabler = Prefs::notificationSound + textGetter = { + if (it.isBlank()) string(R.string.kau_default) + else RingtoneManager.getRingtone(this@getNotificationPrefs, Uri.parse(it)) + ?.getTitle(this@getNotificationPrefs) + ?: "---" //todo figure out why this happens + } + onClick = { + val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply { + putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, string(R.string.select_ringtone)) + putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false) + putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true) + putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION) + if (item.pref.isNotBlank()) + putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, Uri.parse(item.pref)) + } + startActivityForResult(intent, code) } - startActivityForResult(intent, code) } - } - text(R.string.notification_ringtone, Prefs::notificationRingtone, - { Prefs.notificationRingtone = it }) { - ringtone(SettingsActivity.REQUEST_NOTIFICATION_RINGTONE) - } + text(R.string.notification_ringtone, Prefs::notificationRingtone, + { Prefs.notificationRingtone = it }) { + ringtone(SettingsActivity.REQUEST_NOTIFICATION_RINGTONE) + } - text(R.string.message_ringtone, Prefs::messageRingtone, - { Prefs.messageRingtone = it }) { - ringtone(SettingsActivity.REQUEST_MESSAGE_RINGTONE) - } + text(R.string.message_ringtone, Prefs::messageRingtone, + { Prefs.messageRingtone = it }) { + ringtone(SettingsActivity.REQUEST_MESSAGE_RINGTONE) + } + + checkbox(R.string.notification_vibrate, Prefs::notificationVibrate, + { Prefs.notificationVibrate = it }) - checkbox(R.string.notification_vibrate, Prefs::notificationVibrate, - { Prefs.notificationVibrate = it }) + checkbox(R.string.notification_lights, Prefs::notificationLights, + { Prefs.notificationLights = it }) + } - checkbox(R.string.notification_lights, Prefs::notificationLights, - { Prefs.notificationLights = it }) + if (BuildConfig.DEBUG) { + plainText(R.string.reset_notif_epoch) { + onClick = { + loadFbCookiesAsync { + it.map { NotificationModel(it.id) }.forEach { it.save() } + } + } + } + } plainText(R.string.notification_fetch_now) { descRes = R.string.notification_fetch_now_desc diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt index f9f471df..6db3f83c 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt @@ -41,7 +41,7 @@ object Prefs : KPref() { var exitConfirmation: Boolean by kpref("exit_confirmation", true) - var notificationFreq: Long by kpref("notification_freq", 60L) + var notificationFreq: Long by kpref("notification_freq", 15L) var versionCode: Int by kpref("version_code", -1) -- cgit v1.2.3