From 65a0400f2e0c9ca1446bded52669fa51de5dc114 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Tue, 18 Feb 2020 18:33:19 +0100 Subject: [PATCH] Prompt users to update via play store, if available & necessary 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. --- app/build.gradle | 1 + .../android/HttpToolkitApplication.kt | 91 +++++++++++++++++++ .../tech/httptoolkit/android/MainActivity.kt | 36 ++++++++ 3 files changed, 128 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index 01dc1b9..72ed13b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/java/tech/httptoolkit/android/HttpToolkitApplication.kt b/app/src/main/java/tech/httptoolkit/android/HttpToolkitApplication.kt index fdb2042..340e582 100644 --- a/app/src/main/java/tech/httptoolkit/android/HttpToolkitApplication.kt +++ b/app/src/main/java/tech/httptoolkit/android/HttpToolkitApplication.kt @@ -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 @@ -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 @@ -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(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 } \ No newline at end of file diff --git a/app/src/main/java/tech/httptoolkit/android/MainActivity.kt b/app/src/main/java/tech/httptoolkit/android/MainActivity.kt index 3cc12b0..d4993df 100644 --- a/app/src/main/java/tech/httptoolkit/android/MainActivity.kt +++ b/app/src/main/java/tech/httptoolkit/android/MainActivity.kt @@ -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 @@ -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.* @@ -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() { @@ -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 } \ No newline at end of file