aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/kotlin/com/pitchedapps/frost/services
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2018-04-09 01:17:21 -0400
committerGitHub <noreply@github.com>2018-04-09 01:17:21 -0400
commit4c0ba22041ac01a5f0e1cc88a6c292034d697955 (patch)
tree0cba14ad715988b64cbcdc60c85f81671af679fd /app/src/main/kotlin/com/pitchedapps/frost/services
parenta70ce6a2e2d25aa001a085169f25ba39532b3f11 (diff)
downloadfrost-4c0ba22041ac01a5f0e1cc88a6c292034d697955.tar.gz
frost-4c0ba22041ac01a5f0e1cc88a6c292034d697955.tar.bz2
frost-4c0ba22041ac01a5f0e1cc88a6c292034d697955.zip
Feature/download manager (#855)
* Add initial github release check * Create update service * Clean old manager directory * Update kau * Update updateActivity snippet * Add back gradle keys * Remove update service functionality
Diffstat (limited to 'app/src/main/kotlin/com/pitchedapps/frost/services')
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt121
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/NotificationUtils.kt147
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/UpdateService.kt146
3 files changed, 297 insertions, 117 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 ab61b37d..0af76b31 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt
@@ -1,23 +1,15 @@
package com.pitchedapps.frost.services
import android.app.Notification
-import android.app.NotificationChannel
-import android.app.NotificationManager
import android.app.PendingIntent
-import android.app.job.JobInfo
-import android.app.job.JobScheduler
-import android.content.ComponentName
import android.content.Context
import android.content.Intent
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.R
@@ -44,68 +36,6 @@ 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 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" }
-}
-
-@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 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)
- }
- return this
-}
-
private val _40_DP = 40.dpToPx
/**
@@ -314,55 +244,12 @@ data class FrostNotification(private val tag: String,
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
-/**
- * [interval] is # of min, which must be at least 15
- * returns false if an error occurs; true otherwise
- */
-fun Context.scheduleNotifications(minutes: Long): Boolean {
- val scheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
- scheduler.cancel(NOTIFICATION_PERIODIC_JOB)
- if (minutes < 0L) return true
- 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())
- if (result <= 0) {
- L.eThrow("Notification scheduler failed")
- return false
- }
- return true
-}
+fun Context.scheduleNotifications(minutes: Long): Boolean =
+ scheduleJob<NotificationService>(NOTIFICATION_PERIODIC_JOB, minutes)
const val NOTIFICATION_JOB_NOW = 6
-/**
- * Run notification job right now
- */
-fun Context.fetchNotifications(): Boolean {
- val scheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
- 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())
- if (result <= 0) {
- L.eThrow("Notification scheduler failed")
- return false
- }
- return true
-} \ No newline at end of file
+fun Context.fetchNotifications(): Boolean =
+ fetchJob<NotificationService>(NOTIFICATION_JOB_NOW) \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationUtils.kt
new file mode 100644
index 00000000..7014cb78
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationUtils.kt
@@ -0,0 +1,147 @@
+package com.pitchedapps.frost.services
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.job.JobInfo
+import android.app.job.JobScheduler
+import android.app.job.JobService
+import android.content.ComponentName
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import android.os.PersistableBundle
+import android.support.annotation.RequiresApi
+import android.support.v4.app.NotificationCompat
+import ca.allanwang.kau.utils.color
+import ca.allanwang.kau.utils.string
+import com.pitchedapps.frost.R
+import com.pitchedapps.frost.utils.L
+import com.pitchedapps.frost.utils.Prefs
+
+/**
+ * Created by Allan Wang on 07/04/18.
+ */
+const val NOTIF_CHANNEL_GENERAL = "general"
+const val NOTIF_CHANNEL_MESSAGES = "messages"
+const val NOTIF_CHANNEL_UPDATES = "updates"
+
+fun setupNotificationChannels(c: Context) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
+ val manager = c.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val appName = c.string(R.string.frost_name)
+ val msg = c.string(R.string.messages)
+ val updates = c.string(R.string.updates)
+ manager.notificationChannels
+ .filter {
+ it.id != NOTIF_CHANNEL_GENERAL
+ && it.id != NOTIF_CHANNEL_MESSAGES
+ && it.id != NOTIF_CHANNEL_UPDATES
+ }
+ .forEach { manager.deleteNotificationChannel(it.id) }
+ manager.createNotificationChannel(NOTIF_CHANNEL_GENERAL, appName)
+ manager.createNotificationChannel(NOTIF_CHANNEL_MESSAGES, "$appName: $msg")
+ manager.createNotificationChannel(NOTIF_CHANNEL_UPDATES, "$appName: $updates")
+ L.d { "Created notification channels: ${manager.notificationChannels.size} channels, ${manager.notificationChannelGroups.size} groups" }
+}
+
+@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 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)
+ }
+ return this
+}
+
+/*
+ * -----------------------------------
+ * Job Scheduler
+ * -----------------------------------
+ */
+
+const val NOTIFICATION_PARAM_ID = "notif_param_id"
+
+fun JobInfo.Builder.setExtras(id: Int): JobInfo.Builder {
+ val bundle = PersistableBundle()
+ bundle.putInt(NOTIFICATION_PARAM_ID, id)
+ return setExtras(bundle)
+}
+
+/**
+ * interval is # of min, which must be at least 15
+ * returns false if an error occurs; true otherwise
+ */
+inline fun <reified T : JobService> Context.scheduleJob(id: Int, minutes: Long): Boolean {
+ val scheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
+ scheduler.cancel(id)
+ if (minutes < 0L) return true
+ val serviceComponent = ComponentName(this, T::class.java)
+ val builder = JobInfo.Builder(id, serviceComponent)
+ .setPeriodic(minutes * 60000)
+ .setExtras(id)
+ .setPersisted(true)
+ .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) //TODO add options
+ val result = scheduler.schedule(builder.build())
+ if (result <= 0) {
+ L.eThrow("${T::class.java.simpleName} scheduler failed")
+ return false
+ }
+ return true
+}
+
+/**
+ * Run notification job right now
+ */
+inline fun <reified T : JobService> Context.fetchJob(id: Int): Boolean {
+ val scheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
+ val serviceComponent = ComponentName(this, T::class.java)
+ val builder = JobInfo.Builder(id, serviceComponent)
+ .setMinimumLatency(0L)
+ .setExtras(id)
+ .setOverrideDeadline(2000L)
+ .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
+ val result = scheduler.schedule(builder.build())
+ if (result <= 0) {
+ L.eThrow("${T::class.java.simpleName} instant scheduler failed")
+ return false
+ }
+ return true
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/UpdateService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/UpdateService.kt
new file mode 100644
index 00000000..0a528a0f
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/UpdateService.kt
@@ -0,0 +1,146 @@
+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.kotlin.firstOrNull
+import ca.allanwang.kau.utils.string
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.pitchedapps.frost.BuildConfig
+import com.pitchedapps.frost.R
+import com.pitchedapps.frost.facebook.requests.httpClient
+import com.pitchedapps.frost.utils.L
+import com.pitchedapps.frost.utils.Prefs
+import com.pitchedapps.frost.utils.ReleasePrefs
+import com.pitchedapps.frost.utils.frostEvent
+import okhttp3.Request
+import org.jetbrains.anko.doAsync
+import org.joda.time.DateTime
+import java.util.concurrent.Future
+
+/**
+ * Created by Allan Wang on 07/04/18.
+ */
+data class FrostRelease(val versionName: String,
+ val timestamp: Long,
+ val apk: FrostApkRelease? = null,
+ val category: String = "")
+
+data class FrostApkRelease(val size: Long,
+ val name: String,
+ val url: String,
+ val timestamp: Long,
+ val downloadCount: Long = -1)
+
+object UpdateManager {
+ internal fun getLatestGithubRelease(): FrostRelease? {
+ try {
+ val data = getGithubReleaseJsonV3() ?: return null
+ return parseGithubReleaseV3(data)
+ } catch (e: Exception) {
+ L.e(e) {
+ "Failed to get github release"
+ }
+ return null
+ }
+ }
+
+ private fun JsonNode.asMillis(): Long = DateTime(asText()).millis
+
+ private fun getGithubReleaseJsonV3(): JsonNode? {
+ val mapper = ObjectMapper()
+ val response = httpClient.newCall(Request.Builder()
+ .url("https://api.github.com/repos/AllanWang/Frost-for-Facebook/releases/latest")
+ .get().build()).execute().body()?.string() ?: return null
+ return mapper.readTree(response)
+ }
+
+ private fun parseGithubReleaseV3(data: JsonNode): FrostRelease? {
+ val versionName = data.get("tag_name").asText()
+ if (versionName.isEmpty()) return null
+ val release = FrostRelease(
+ versionName = versionName,
+ timestamp = data.get("created_at").asMillis(),
+ category = "Github")
+ val assets = data.get("assets")
+ if (!assets.isArray) return release
+ val apkRelease = assets.elements().firstOrNull {
+ it.get("content_type").asText().contains("android")
+ } ?: return release
+ val apk = FrostApkRelease(size = apkRelease.get("size").asLong(),
+ name = apkRelease.get("name").asText(),
+ url = apkRelease.get("browser_download_url").asText(),
+ timestamp = apkRelease.get("updated_at").asMillis(),
+ downloadCount = apkRelease.get("download_count").asLong())
+ return release.copy(apk = apk)
+ }
+}
+
+class UpdateService : JobService() {
+
+ private var future: Future<Unit>? = null
+
+ private val startTime = System.currentTimeMillis()
+
+ override fun onStopJob(params: JobParameters?): Boolean {
+ val time = System.currentTimeMillis() - startTime
+ L.d { "Update service has finished abruptly in $time ms" }
+ frostEvent("UpdateTime",
+ "Type" to "Service force stop",
+ "Duration" to time)
+ future?.cancel(true)
+ future = null
+ return false
+ }
+
+ fun finish(params: JobParameters?) {
+ val time = System.currentTimeMillis() - startTime
+ L.i { "Update service has finished in $time ms" }
+ frostEvent("UpdateTime",
+ "Type" to "Service",
+ "Duration" to time)
+ jobFinished(params, false)
+ future?.cancel(true)
+ future = null
+ }
+
+ override fun onStartJob(params: JobParameters?): Boolean {
+// L.i { "Fetching update" }
+// future = doAsync {
+// fetch()
+// finish(params)
+// }
+// return true
+ return false
+ }
+
+ private fun fetch() {
+ val release = UpdateManager.getLatestGithubRelease() ?: return
+ val timestamp = release.apk?.timestamp ?: return
+ if (ReleasePrefs.lastTimeStamp >= timestamp) return
+ ReleasePrefs.lastTimeStamp = timestamp
+ if (BuildConfig.VERSION_NAME.contains(release.apk.name)) return
+ updateNotification(release)
+ }
+
+ private fun updateNotification(release: FrostRelease) {
+ val notifBuilder = frostNotification(NOTIF_CHANNEL_UPDATES)
+ .setFrostAlert(true, Prefs.notificationRingtone)
+ .setContentTitle(string(R.string.frost_name))
+ .setContentText(string(R.string.update_notif_message))
+ NotificationManagerCompat.from(this).notify(release.versionName.hashCode(), notifBuilder.build())
+ }
+
+}
+
+const val UPDATE_PERIODIC_JOB = 7
+
+fun Context.scheduleUpdater(enable: Boolean): Boolean =
+ scheduleJob<UpdateService>(UPDATE_PERIODIC_JOB, if (enable) 1440 else -1)
+
+const val UPDATE_JOB_NOW = 6
+
+fun Context.fetchUpdates(): Boolean =
+ fetchJob<UpdateService>(UPDATE_JOB_NOW) \ No newline at end of file