From 4c0ba22041ac01a5f0e1cc88a6c292034d697955 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Mon, 9 Apr 2018 01:17:21 -0400 Subject: 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 --- .../frost/services/FrostNotifications.kt | 121 +---------------- .../frost/services/NotificationUtils.kt | 147 +++++++++++++++++++++ .../pitchedapps/frost/services/UpdateService.kt | 146 ++++++++++++++++++++ 3 files changed, 297 insertions(+), 117 deletions(-) create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/services/NotificationUtils.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/services/UpdateService.kt (limited to 'app/src/main/kotlin/com/pitchedapps/frost/services') 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(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(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 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 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? = 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(UPDATE_PERIODIC_JOB, if (enable) 1440 else -1) + +const val UPDATE_JOB_NOW = 6 + +fun Context.fetchUpdates(): Boolean = + fetchJob(UPDATE_JOB_NOW) \ No newline at end of file -- cgit v1.2.3