diff options
Diffstat (limited to 'app/src/main/kotlin/com/pitchedapps/frost/services')
3 files changed, 228 insertions, 16 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 afa30a91..2d9e0803 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt @@ -11,7 +11,9 @@ import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.net.Uri +import android.os.BaseBundle import android.os.Build +import android.os.Bundle import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationManagerCompat import ca.allanwang.kau.utils.color @@ -28,11 +30,16 @@ import com.pitchedapps.frost.dbflow.NotificationModel import com.pitchedapps.frost.dbflow.lastNotificationTime import com.pitchedapps.frost.enums.OverlayContext import com.pitchedapps.frost.facebook.FbItem +import com.pitchedapps.frost.glide.FrostGlide +import com.pitchedapps.frost.glide.transform import com.pitchedapps.frost.parsers.FrostParser import com.pitchedapps.frost.parsers.MessageParser import com.pitchedapps.frost.parsers.NotifParser import com.pitchedapps.frost.parsers.ParseNotification -import com.pitchedapps.frost.utils.* +import com.pitchedapps.frost.utils.ARG_USER_ID +import com.pitchedapps.frost.utils.L +import com.pitchedapps.frost.utils.Prefs +import com.pitchedapps.frost.utils.frostAnswersCustom import org.jetbrains.anko.runOnUiThread import java.util.* @@ -54,6 +61,7 @@ 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) } @@ -68,9 +76,6 @@ fun NotificationCompat.Builder.withDefaults(ringtone: String = Prefs.notificatio setDefaults(defaults) } -inline val NotificationCompat.Builder.withBigText: NotificationCompat.BigTextStyle - get() = NotificationCompat.BigTextStyle(this) - /** * Created by Allan Wang on 2017-07-08. * @@ -85,7 +90,7 @@ class FrostNotificationTarget(val context: Context, override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>) { builder.setLargeIcon(resource) - NotificationManagerCompat.from(context).notify(notifTag, notifId, builder.withBigText.build()) + NotificationManagerCompat.from(context).notify(notifTag, notifId, builder.build()) } } @@ -99,12 +104,18 @@ enum class NotificationType( private val getTime: (notif: NotificationModel) -> Long, private val putTime: (notif: NotificationModel, time: Long) -> NotificationModel, private val ringtone: () -> String) { + GENERAL(OverlayContext.NOTIFICATION, FbItem.NOTIFICATIONS, NotifParser, NotificationModel::epoch, { notif, time -> notif.copy(epoch = time) }, - Prefs::notificationRingtone), + Prefs::notificationRingtone) { + + override fun bindRequest(content: NotificationContent, cookie: String) = + FrostRunnable.prepareMarkNotificationRead(content.id, cookie) + }, + MESSAGE(OverlayContext.MESSAGE, FbItem.MESSAGES, MessageParser, @@ -115,6 +126,19 @@ enum class NotificationType( private val groupPrefix = "frost_${name.toLowerCase(Locale.CANADA)}" /** + * Optional binder to return the request bundle builder + */ + internal open fun bindRequest(content: NotificationContent, cookie: String): (BaseBundle.() -> Unit)? = null + + private fun bindRequest(intent: Intent, content: NotificationContent, cookie: String?) { + cookie ?: return + val binder = bindRequest(content, cookie) ?: return + val bundle = Bundle() + bundle.binder() + intent.putExtras(bundle) + } + + /** * Get unread data from designated parser * Display notifications for those after old epoch * Save new epoch @@ -138,7 +162,7 @@ enum class NotificationType( newLatestEpoch = notif.timestamp notifCount++ } - if (newLatestEpoch != prevLatestEpoch) + if (newLatestEpoch > prevLatestEpoch) putTime(prevNotifTime, newLatestEpoch).save() L.d("Notif $name new epoch ${getTime(lastNotificationTime(userId))}") summaryNotification(context, userId, notifCount) @@ -154,9 +178,11 @@ enum class NotificationType( val intent = Intent(context, FrostWebActivity::class.java) intent.data = Uri.parse(href) intent.putExtra(ARG_USER_ID, data.id) - intent.putExtra(ARG_OVERLAY_CONTEXT, overlayContext) + overlayContext.put(intent) + bindRequest(intent, content, data.cookie) + val group = "${groupPrefix}_${data.id}" - val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0) + val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) val notifBuilder = context.frostNotification .setContentTitle(title ?: context.string(R.string.frost_name)) .setContentText(text) @@ -170,15 +196,15 @@ enum class NotificationType( if (timestamp != -1L) notifBuilder.setWhen(timestamp * 1000) L.v("Notif load", context.toString()) - NotificationManagerCompat.from(context).notify(group, notifId, notifBuilder.withBigText.build()) + NotificationManagerCompat.from(context).notify(group, notifId, notifBuilder.build()) - if (profileUrl.isNotBlank()) { + if (profileUrl != null) { context.runOnUiThread { //todo verify if context is valid? Glide.with(context) .asBitmap() .load(profileUrl) - .withRoundIcon() + .transform(FrostGlide.circleCrop) .into(FrostNotificationTarget(context, notifId, group, notifBuilder)) } } @@ -196,7 +222,7 @@ enum class NotificationType( val intent = Intent(context, FrostWebActivity::class.java) intent.data = Uri.parse(fbItem.url) intent.putExtra(ARG_USER_ID, userId) - val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0) + val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) val notifBuilder = context.frostNotification.withDefaults(ringtone()) .setContentTitle(context.string(R.string.frost_name)) .setContentText("$count ${context.string(fbItem.titleId)}") @@ -213,12 +239,16 @@ enum class NotificationType( * Notification data holder */ data class NotificationContent(val data: CookieModel, - val notifId: Int, + val id: Long, val href: String, val title: String? = null, // defaults to frost title val text: String, val timestamp: Long, - val profileUrl: String) + val profileUrl: String?) { + + val notifId = Math.abs(id.toInt()) + +} const val NOTIFICATION_PERIODIC_JOB = 7 diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt new file mode 100644 index 00000000..74a8b98d --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt @@ -0,0 +1,182 @@ +package com.pitchedapps.frost.services + +import android.app.job.JobInfo +import android.app.job.JobParameters +import android.app.job.JobScheduler +import android.app.job.JobService +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.os.BaseBundle +import android.os.PersistableBundle +import com.pitchedapps.frost.facebook.RequestAuth +import com.pitchedapps.frost.facebook.fbRequest +import com.pitchedapps.frost.facebook.markNotificationRead +import com.pitchedapps.frost.utils.EnumBundle +import com.pitchedapps.frost.utils.EnumBundleCompanion +import com.pitchedapps.frost.utils.EnumCompanion +import com.pitchedapps.frost.utils.L +import org.jetbrains.anko.doAsync +import java.util.concurrent.Future + +/** + * Created by Allan Wang on 28/12/17. + */ + +/** + * Private helper data + */ +private enum class FrostRequestCommands : EnumBundle<FrostRequestCommands> { + + NOTIF_READ { + + override fun invoke(auth: RequestAuth, bundle: PersistableBundle) { + val id = bundle.getLong(ARG_0, -1L) + val success = auth.markNotificationRead(id).invoke() + L.d("Marked notif $id as read: $success") + } + + override fun propagate(bundle: BaseBundle) = + FrostRunnable.prepareMarkNotificationRead( + bundle.getLong(ARG_0), + bundle.getCookie()) + + }; + + override val bundleContract: EnumBundleCompanion<FrostRequestCommands> + get() = Companion + + /** + * Call request with arguments inside bundle + */ + abstract fun invoke(auth: RequestAuth, bundle: PersistableBundle) + + /** + * Return bundle builder given arguments in the old bundle + * Must not write to old bundle! + */ + abstract fun propagate(bundle: BaseBundle): BaseBundle.() -> Unit + + companion object : EnumCompanion<FrostRequestCommands>("frost_arg_commands", values()) + +} + +private const val ARG_COMMAND = "frost_request_command" +private const val ARG_COOKIE = "frost_request_cookie" +private const val ARG_0 = "frost_request_arg_0" +private const val ARG_1 = "frost_request_arg_1" +private const val ARG_2 = "frost_request_arg_2" +private const val ARG_3 = "frost_request_arg_3" +private const val JOB_REQUEST_BASE = 928 + +private fun BaseBundle.getCookie() = getString(ARG_COOKIE) +private fun BaseBundle.putCookie(cookie: String) = putString(ARG_COOKIE, cookie) + +/** + * Singleton handler for running requests in [FrostRequestService] + * Requests are typically completely decoupled from the UI, + * and are optional enhancers. + * + * Nothing guarantees the completion time, or whether it even executes at all + * + * Design: + * prepare function - creates a bundle binder + * actor function - calls the service with the given arguments + * + * Global: + * propagator - given a bundle with a command, extracts and executes the requests + */ +object FrostRunnable { + + fun prepareMarkNotificationRead(id: Long, cookie: String): BaseBundle.() -> Unit = { + FrostRequestCommands.NOTIF_READ.put(this) + putLong(ARG_0, id) + putCookie(cookie) + } + + fun markNotificationRead(context: Context, id: Long, cookie: String): Boolean { + if (id <= 0) { + L.d("Invalid notification id $id for marking as read") + return false + } + return schedule(context, FrostRequestCommands.NOTIF_READ, + prepareMarkNotificationRead(id, cookie)) + } + + fun propagate(context: Context, intent: Intent?) { + intent?.extras ?: return + val command = FrostRequestCommands[intent] ?: return + intent.removeExtra(ARG_COMMAND) // reset + L.d("Propagating command ${command.name}") + val builder = command.propagate(intent.extras) + schedule(context, command, builder) + } + + private fun schedule(context: Context, + command: FrostRequestCommands, + bundleBuilder: PersistableBundle.() -> Unit): Boolean { + val scheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler + val serviceComponent = ComponentName(context, FrostRequestService::class.java) + val bundle = PersistableBundle() + bundle.bundleBuilder() + bundle.putString(ARG_COMMAND, command.name) + + if (bundle.getCookie().isNullOrBlank()) { + L.e("Scheduled frost request with empty cookie)") + return false + } + + val builder = JobInfo.Builder(JOB_REQUEST_BASE + command.ordinal, serviceComponent) + .setMinimumLatency(0L) + .setExtras(bundle) + .setOverrideDeadline(2000L) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + val result = scheduler.schedule(builder.build()) + if (result <= 0) { + L.eThrow("FrostRequestService scheduler failed for ${command.name}") + return false + } + L.d("Scheduled ${command.name}") + return true + } + +} + +class FrostRequestService : JobService() { + + var future: Future<Unit>? = null + + override fun onStopJob(params: JobParameters?): Boolean { + future?.cancel(true) + future = null + return false + } + + override fun onStartJob(params: JobParameters?): Boolean { + val bundle = params?.extras + if (bundle == null) { + L.eThrow("Launched ${this::class.java.simpleName} without param data") + return false + } + val cookie = bundle.getCookie() + if (cookie.isNullOrBlank()) { + L.eThrow("Launched ${this::class.java.simpleName} without cookie") + return false + } + val command = FrostRequestCommands[bundle] + if (command == null) { + L.eThrow("Launched ${this::class.java.simpleName} without command") + return false + } + val now = System.currentTimeMillis() + future = doAsync { + cookie.fbRequest { + L.d("Requesting frost service for ${command.name}") + command.invoke(this, bundle) + } + L.d("Finished frost service for ${command.name} in ${System.currentTimeMillis() - now} ms") + jobFinished(params, false) + } + return true + } +}
\ No newline at end of file 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 adeefec6..c1a4ace1 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt @@ -77,7 +77,7 @@ class NotificationService : JobService() { return null } - private fun Context.debugNotification(text: String) { + private fun Context.debugNotification(text: String = string(R.string.kau_lorem_ipsum)) { if (!BuildConfig.DEBUG) return val notifBuilder = frostNotification.withDefaults() .setContentTitle(string(R.string.frost_name)) |