Skip to content

Commit

Permalink
Prompt users to update via play store, if available & necessary
Browse files Browse the repository at this point in the history
This is intended for users who sideload the app (via the ADB
interceptor) and then continue to use it for a while. These users won't
receive updates at all. Not perfect, but for now we'll nudge them across
onto the play store version in that case. Later the ADB interceptor will
also update installs automatically, to handle users using ADB setup long
term.
  • Loading branch information
pimterry committed Feb 18, 2020
1 parent 9f8b340 commit 65a0400
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 0 deletions.
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ dependencies {
implementation 'com.beust:klaxon:5.0.1'
implementation 'com.squareup.okhttp3:okhttp:4.3.0'
implementation 'com.google.android.material:material:1.1.0-beta02'
implementation 'net.swiftzer.semver:semver:1.1.0'
implementation 'io.sentry:sentry-android:1.7.27'
implementation 'org.slf4j:slf4j-nop:1.7.25'
implementation 'com.google.android.gms:play-services-analytics:10.2.4'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tech.httptoolkit.android

import android.app.Application
import android.content.Context
import android.util.Log
import com.android.installreferrer.api.InstallReferrerClient
import com.android.installreferrer.api.InstallReferrerClient.InstallReferrerResponse
Expand All @@ -11,10 +12,17 @@ import com.google.android.gms.analytics.HitBuilders
import com.google.android.gms.analytics.Tracker
import io.sentry.Sentry
import io.sentry.android.AndroidSentryClientFactory
import kotlinx.coroutines.*
import net.swiftzer.semver.SemVer
import okhttp3.OkHttpClient
import okhttp3.Request
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine


class HttpToolkitApplication : Application() {

private val TAG = HttpToolkitApplication::class.simpleName
Expand Down Expand Up @@ -149,4 +157,87 @@ class HttpToolkitApplication : Application() {
analytics?.setLocalDispatchPeriod(120) // Set dispatching back to Android default
}

suspend fun isUpdateRequired(): Boolean {
return withContext(Dispatchers.IO) {
if (wasInstalledFromStore(this@HttpToolkitApplication)) {
// We only check for updates for side-loaded/ADB-loaded versions. This is useful
// because otherwise anything outside the play store gets no updates.
Log.i(TAG, "Installed from play store, no update prompting required")
return@withContext false
}

val httpClient = OkHttpClient()
val request = Request.Builder()
.url("https://api.github.com/repos/httptoolkit/httptoolkit-android/releases/latest")
.build()

try {
val response = httpClient.newCall(request).execute().use { response ->
if (response.code != 200) throw RuntimeException("Failed to check for updates")
response.body!!.string()
}

val release = Klaxon().parse<GithubRelease>(response)!!
val releaseVersion =
tryParseSemver(release.name)
?: tryParseSemver(release.tag_name)
?: throw RuntimeException("Could not parse release version ${release.tag_name}")
val releaseDate = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse(release.published_at)

val installedVersion = getInstalledVersion(this@HttpToolkitApplication)

val updateAvailable = releaseVersion > installedVersion
// We avoid immediately prompting for updates because a) there's a review delay
// before new updates go live, and b) it's annoying otherwise, if there's a rapid
// series of releases. Better to start chasing users only after a week stable.
val updateNotTooRecent = releaseDate.before(daysAgo(0))

Log.i(TAG,
if (updateAvailable && updateNotTooRecent)
"New version available, released > 1 week"
else if (updateAvailable)
"New version available, but still recent, released $releaseDate"
else
"App is up to date"
)
return@withContext updateAvailable && updateNotTooRecent
} catch (e: Exception) {
Log.w(TAG, e)
return@withContext false
}
}
}

}

private fun wasInstalledFromStore(context: Context): Boolean {
return context.packageManager.getInstallerPackageName(context.packageName) != null
}

private data class GithubRelease(
val tag_name: String?,
val name: String?,
val published_at: String
)

private fun tryParseSemver(version: String?): SemVer? = try {
if (version == null) null
else SemVer.parse(
// Strip leading 'v'
version.replace(Regex("^v"), "")
)
} catch (e: IllegalArgumentException) {
null
}

private fun getInstalledVersion(context: Context): SemVer {
return SemVer.parse(
context.packageManager.getPackageInfo(context.packageName, 0).versionName
)
}

private fun daysAgo(days: Int): Date {
val calendar = Calendar.getInstance()
calendar.add(Calendar.DAY_OF_YEAR, -days)
return calendar.time
}
36 changes: 36 additions & 0 deletions app/src/main/java/tech/httptoolkit/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.net.Uri
import android.net.VpnService
import android.os.Bundle
Expand All @@ -18,6 +19,7 @@ import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.google.android.gms.common.GooglePlayServicesUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.sentry.Sentry
import kotlinx.coroutines.*
Expand Down Expand Up @@ -94,6 +96,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
}
}
}

// Async check for updates, and maybe prompt the user if necessary (if using play store)
launch {
supervisorScope {
if (isStoreAvailable(this@MainActivity) && app.isUpdateRequired()) promptToUpdate()
}
}
}

override fun onResume() {
Expand Down Expand Up @@ -460,4 +469,31 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
}
}

private suspend fun promptToUpdate() {
withContext(Dispatchers.Main) {
MaterialAlertDialogBuilder(this@MainActivity)
.setTitle("Updates available")
.setIcon(R.drawable.ic_info_circle)
.setMessage("An updated version of HTTP Toolkit is available")
.setNegativeButton("Ignore") { _, _ -> }
.setPositiveButton("Update now") { _, _ ->
// Open the app in the market. That a release is available on github doesn't
// *strictly* mean that it's available on the Android market right now, but
// it is imminent, and installing from play means it'll update fully later.
startActivity(
Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("market://details?id=tech.httptoolkit.android.v1")
}
)
}
.show()
}
}
}

private fun isStoreAvailable(context: Context): Boolean = try {
context.packageManager.getPackageInfo(GooglePlayServicesUtil.GOOGLE_PLAY_STORE_PACKAGE, 0)
true
} catch (e: PackageManager.NameNotFoundException) {
false
}

0 comments on commit 65a0400

Please sign in to comment.