aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/kotlin/com/pitchedapps/frost/services
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2017-12-29 19:39:04 -0500
committerGitHub <noreply@github.com>2017-12-29 19:39:04 -0500
commit32e6b5be0e662bbac22806bcc87259fd1a2e2ed0 (patch)
treec97f7ef11b60231bbe7337f5960413b95da0a8c2 /app/src/main/kotlin/com/pitchedapps/frost/services
parent8fee0629c27edee847358efc82309118f3a9a3a5 (diff)
downloadfrost-32e6b5be0e662bbac22806bcc87259fd1a2e2ed0.tar.gz
frost-32e6b5be0e662bbac22806bcc87259fd1a2e2ed0.tar.bz2
frost-32e6b5be0e662bbac22806bcc87259fd1a2e2ed0.zip
Feature/native notifs (#579)
* Improve parser and add zip test * Remove ActivityOptionsCompat, resolves #555 * Create native notifs * Add animations * Add image rounder * Improve glide transformations * Add request service * Fix parser * Fix parser * Add thumbnail and fix notification text * Update parsers and regex * Auto mark as read * Add request implementation in pending intent * Remove unnecessary return data * Simplify command retrieval * Use name keys instead * Revamp all bundle calls * Fix up thumbnail layout
Diffstat (limited to 'app/src/main/kotlin/com/pitchedapps/frost/services')
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt60
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt182
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt2
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))