diff options
author | Allan Wang <me@allanwang.ca> | 2018-04-09 01:17:21 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-04-09 01:17:21 -0400 |
commit | 4c0ba22041ac01a5f0e1cc88a6c292034d697955 (patch) | |
tree | 0cba14ad715988b64cbcdc60c85f81671af679fd | |
parent | a70ce6a2e2d25aa001a085169f25ba39532b3f11 (diff) | |
download | frost-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
17 files changed, 459 insertions, 134 deletions
diff --git a/app/build.gradle b/app/build.gradle index 3b913a82..c34e6a53 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -43,6 +43,8 @@ android { textOutput 'stdout' } + def withReleaseSigning = file('../files/release.keystore').exists() + signingConfigs { debug { @@ -59,6 +61,18 @@ android { keyPassword "testkey" } + if (withReleaseSigning) { + def releaseProps = new Properties() + file("../files/release.properties").withInputStream { releaseProps.load(it) } + + release { + storeFile file("../files/release.keystore") + storePassword releaseProps.getProperty('storePassword') + keyAlias releaseProps.getProperty('keyAlias') + keyPassword releaseProps.getProperty('keyPassword') + } + } + } buildTypes { @@ -85,6 +99,7 @@ android { release { minifyEnabled true shrinkResources true + if (withReleaseSigning) signingConfig signingConfigs.release proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' resValue "string", "frost_name", "Frost" resValue "string", "frost_web", "Frost Web" @@ -172,6 +187,8 @@ dependencies { implementation "org.jsoup:jsoup:${JSOUP}" + implementation "joda-time:joda-time:${JODA}" + implementation "com.squareup.okhttp3:okhttp:${OKHTTP}" implementation "com.squareup.okhttp3:logging-interceptor:${OKHTTP}" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f95e6f62..c22aa94a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -127,6 +127,9 @@ <activity android:name=".activities.SelectorActivity" /> <activity android:name=".activities.TabCustomizerActivity" /> <activity + android:name=".activities.UpdateActivity" + android:theme="@style/FrostTheme.Settings" /> + <activity android:name=".activities.SettingsActivity" android:theme="@style/FrostTheme.Settings" /> <activity @@ -143,6 +146,11 @@ android:label="@string/frost_notifications" android:permission="android.permission.BIND_JOB_SERVICE" /> <service + android:name=".services.UpdateService" + android:enabled="true" + android:label="@string/updates" + android:permission="android.permission.BIND_JOB_SERVICE" /> + <service android:name=".services.FrostRequestService" android:enabled="true" android:label="@string/frost_requests" diff --git a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt index cbb3d0ab..dec22e88 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt @@ -18,12 +18,8 @@ import com.pitchedapps.frost.dbflow.FbTabsDb import com.pitchedapps.frost.dbflow.NotificationDb import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.glide.GlideApp -import com.pitchedapps.frost.services.scheduleNotifications -import com.pitchedapps.frost.services.setupNotificationChannels -import com.pitchedapps.frost.utils.FrostPglAdBlock -import com.pitchedapps.frost.utils.L -import com.pitchedapps.frost.utils.Prefs -import com.pitchedapps.frost.utils.Showcase +import com.pitchedapps.frost.services.* +import com.pitchedapps.frost.utils.* import com.raizlabs.android.dbflow.config.DatabaseConfig import com.raizlabs.android.dbflow.config.FlowConfig import com.raizlabs.android.dbflow.config.FlowManager @@ -64,17 +60,21 @@ class FrostApp : Application() { .build()) Showcase.initialize(this, "${BuildConfig.APPLICATION_ID}.showcase") Prefs.initialize(this, "${BuildConfig.APPLICATION_ID}.prefs") + ReleasePrefs.initialize(this, "${BuildConfig.APPLICATION_ID}.manager") // if (LeakCanary.isInAnalyzerProcess(this)) return // refWatcher = LeakCanary.install(this) if (!BuildConfig.DEBUG) { Bugsnag.init(this) - val releaseStage = setOf("production", "releaseTest", "github", "release") - Bugsnag.setNotifyReleaseStages(*releaseStage.toTypedArray(), "unnamed") + val releaseStage = setOf(FLAVOUR_PRODUCTION, + FLAVOUR_TEST, + FLAVOUR_GITHUB, + FLAVOUR_RELEASE) + Bugsnag.setNotifyReleaseStages(*releaseStage.toTypedArray(), FLAVOUR_UNNAMED) val versionSegments = BuildConfig.VERSION_NAME.split("_") if (versionSegments.size > 1) { Bugsnag.setAppVersion(versionSegments.first()) Bugsnag.setReleaseStage(if (versionSegments.last() in releaseStage) versionSegments.last() - else "unnamed") + else FLAVOUR_UNNAMED) Bugsnag.setUserName(BuildConfig.VERSION_NAME) } @@ -95,6 +95,7 @@ class FrostApp : Application() { setupNotificationChannels(applicationContext) applicationContext.scheduleNotifications(Prefs.notificationFreq) + applicationContext.scheduleUpdater(ReleasePrefs.enableUpdater) /** * Drawer profile loading logic diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt index 97d82884..02f5bc49 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt @@ -141,6 +141,12 @@ class SettingsActivity : KPrefActivity() { onClick = { launchNewTask<IntroActivity>(cookies(), true) } } +// plainText(R.string.updates) { +// descRes = R.string.updates_desc +// iicon = CommunityMaterial.Icon.cmd_github_circle +// onClick = { launchNewTask<UpdateActivity>() } +// } + subItems(R.string.debug_frost, getDebugPrefs()) { descRes = R.string.debug_frost_desc iicon = CommunityMaterial.Icon.cmd_android_debug_bridge diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/UpdateActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/UpdateActivity.kt new file mode 100644 index 00000000..28569e6d --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/UpdateActivity.kt @@ -0,0 +1,31 @@ +package com.pitchedapps.frost.activities + +import android.os.Bundle +import ca.allanwang.kau.ui.activities.SwipeRecyclerActivity +import ca.allanwang.kau.ui.views.SwipeRecyclerView +import com.mikepenz.fastadapter.FastAdapter +import com.pitchedapps.frost.iitems.ReleaseIItem +import com.pitchedapps.frost.services.UpdateManager +import org.jetbrains.anko.AnkoAsyncContext +import org.jetbrains.anko.uiThread + +/** + * Created by Allan Wang on 07/04/18. + */ +class UpdateActivity : SwipeRecyclerActivity<ReleaseIItem>() { + + override fun onCreate(savedInstanceState: Bundle?, fastAdapter: FastAdapter<ReleaseIItem>) { + fastAdapter.withOnClickListener { _, _, item, _ -> + if (item is ReleaseIItem) { + // todo download + } + true + } + } + + override fun AnkoAsyncContext<SwipeRecyclerView>.onRefresh() { + val release = UpdateManager.getLatestGithubRelease() ?: return + uiThread { adapter.set(listOf(ReleaseIItem(release))) } + } + +}
\ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt index c70ae1ae..b5c2e4e9 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt @@ -69,7 +69,7 @@ internal inline fun <T : Any?> RequestAuth.frostRequest( return FrostRequest(request.call(), invoke) } -private val client: OkHttpClient by lazy { +val httpClient: OkHttpClient by lazy { val builder = OkHttpClient.Builder() if (BuildConfig.DEBUG) builder.addInterceptor(HttpLoggingInterceptor() @@ -97,7 +97,7 @@ private fun String.requestBuilder() = Request.Builder() .header("User-Agent", USER_AGENT_BASIC) .cacheControl(CacheControl.FORCE_NETWORK) -fun Request.Builder.call() = client.newCall(build())!! +fun Request.Builder.call() = httpClient.newCall(build())!! fun String.getAuth(): RequestAuth { L.v { "Getting auth for ${hashCode()}" } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/iitems/ReleaseIItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/iitems/ReleaseIItem.kt new file mode 100644 index 00000000..abba63b5 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/iitems/ReleaseIItem.kt @@ -0,0 +1,38 @@ +package com.pitchedapps.frost.iitems + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import ca.allanwang.kau.iitems.KauIItem +import ca.allanwang.kau.utils.* +import com.mikepenz.fastadapter.FastAdapter +import com.mikepenz.fastadapter.IItem +import com.mikepenz.fastadapter_extensions.drag.IDraggable +import com.pitchedapps.frost.R +import com.pitchedapps.frost.facebook.FbItem +import com.pitchedapps.frost.services.FrostRelease +import com.pitchedapps.frost.utils.Prefs + +/** + * Created by Allan Wang on 26/11/17. + */ +class ReleaseIItem(val item: FrostRelease) : KauIItem<ReleaseIItem, ReleaseIItem.ViewHolder>( + R.layout.iitem_tab_preview, + { ViewHolder(it) } +) { + + class ViewHolder(itemView: View) : FastAdapter.ViewHolder<ReleaseIItem>(itemView) { + + val image: ImageView by bindView(R.id.image) + val text: TextView by bindView(R.id.text) + + override fun bindView(item: ReleaseIItem, payloads: MutableList<Any>) { + + } + + override fun unbindView(item: ReleaseIItem) { + + } + + } +}
\ 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 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 diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Const.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Const.kt index 0cb57ed5..7909f125 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Const.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Const.kt @@ -15,4 +15,11 @@ const val REQUEST_TEXT_ZOOM = 1 shl 14 const val REQUEST_NAV = 1 shl 15 const val REQUEST_SEARCH = 1 shl 16 -const val MAIN_TIMEOUT_DURATION = 30 * 60 * 1000 // 30 min
\ No newline at end of file +const val MAIN_TIMEOUT_DURATION = 30 * 60 * 1000 // 30 min + +// Flavours +const val FLAVOUR_PRODUCTION = "production" +const val FLAVOUR_TEST = "releaseTest" +const val FLAVOUR_GITHUB = "github" +const val FLAVOUR_RELEASE = "release" +const val FLAVOUR_UNNAMED = "unnamed"
\ No newline at end of file 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 95a5f39b..59112e70 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt @@ -137,7 +137,7 @@ object Prefs : KPref() { var verboseLogging: Boolean by kpref("verbose_logging", false) - var analytics: Boolean by kpref("analytics", false) + var analytics: Boolean by kpref("analytics", true) var overlayEnabled: Boolean by kpref("overlay_enabled", true) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/ReleasePrefs.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/ReleasePrefs.kt new file mode 100644 index 00000000..1e9dd763 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/ReleasePrefs.kt @@ -0,0 +1,13 @@ +package com.pitchedapps.frost.utils + +import ca.allanwang.kau.kpref.KPref +import ca.allanwang.kau.kpref.kpref +import com.pitchedapps.frost.BuildConfig + +/** + * Created by Allan Wang on 07/04/18. + */ +object ReleasePrefs : KPref() { + var lastTimeStamp: Long by kpref("last_time_stamp", -1L) + var enableUpdater: Boolean by kpref("enable_updater", BuildConfig.FLAVOR == FLAVOUR_GITHUB) +}
\ No newline at end of file diff --git a/app/src/main/res/values/strings_updater.xml b/app/src/main/res/values/strings_updater.xml new file mode 100644 index 00000000..6050a1e0 --- /dev/null +++ b/app/src/main/res/values/strings_updater.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="updates">Updates</string> + <string name="updates_desc">Get updates from Github releases</string> + <string name="update_notif_message">New update is available! Click to download.</string> +</resources>
\ No newline at end of file diff --git a/app/src/test/kotlin/com/pitchedapps/frost/services/UpdateServiceTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/services/UpdateServiceTest.kt new file mode 100644 index 00000000..69ff7941 --- /dev/null +++ b/app/src/test/kotlin/com/pitchedapps/frost/services/UpdateServiceTest.kt @@ -0,0 +1,18 @@ +package com.pitchedapps.frost.services + +import org.junit.Test +import kotlin.test.assertNotNull + +/** + * Created by Allan Wang on 07/04/18. + */ +class UpdateServiceTest { + + @Test + fun getRelease() { + val release = UpdateManager.getLatestGithubRelease() + assertNotNull(release) + assertNotNull(release!!.apk, "Apk not uploaded for $release") + } + +}
\ No newline at end of file diff --git a/files/.gitignore b/files/.gitignore index 50650209..63a31573 100644 --- a/files/.gitignore +++ b/files/.gitignore @@ -1,4 +1,3 @@ -gplay-keys.json -play.keystore -play.properties +release.keystore +release.properties update-dev.sh diff --git a/gradle.properties b/gradle.properties index e715978b..f0ef560d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryErro APP_ID=Frost APP_GROUP=com.pitchedapps -KAU=b3a4e35 +KAU=7ed19aa KOTLIN=1.2.31 BUGSNAG=4.3.2 @@ -24,6 +24,7 @@ GIT_PLUGIN=0.4.3 COMMONS_TEXT=1.2 DBFLOW=4.2.4 EXOMEDIA=4.1.0 +JODA=2.9.9 JSOUP=1.11.2 LEAK_CANARY=1.5.4 MATERIAL_DRAWER_KT=1.3.3 |