aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/build.gradle17
-rw-r--r--app/src/main/AndroidManifest.xml8
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt19
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt6
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/UpdateActivity.kt31
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/iitems/ReleaseIItem.kt38
-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
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Const.kt9
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/ReleasePrefs.kt13
-rw-r--r--app/src/main/res/values/strings_updater.xml6
-rw-r--r--app/src/test/kotlin/com/pitchedapps/frost/services/UpdateServiceTest.kt18
-rw-r--r--files/.gitignore5
-rw-r--r--gradle.properties3
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