11package tech.httptoolkit.android
22
33import android.app.Application
4+ import android.content.Context
45import android.util.Log
56import com.android.installreferrer.api.InstallReferrerClient
67import com.android.installreferrer.api.InstallReferrerClient.InstallReferrerResponse
@@ -11,10 +12,17 @@ import com.google.android.gms.analytics.HitBuilders
1112import com.google.android.gms.analytics.Tracker
1213import io.sentry.Sentry
1314import io.sentry.android.AndroidSentryClientFactory
15+ import kotlinx.coroutines.*
16+ import net.swiftzer.semver.SemVer
17+ import okhttp3.OkHttpClient
18+ import okhttp3.Request
19+ import java.text.SimpleDateFormat
20+ import java.util.*
1421import java.util.concurrent.atomic.AtomicBoolean
1522import kotlin.coroutines.resume
1623import kotlin.coroutines.suspendCoroutine
1724
25+
1826class HttpToolkitApplication : Application () {
1927
2028 private val TAG = HttpToolkitApplication ::class .simpleName
@@ -149,4 +157,87 @@ class HttpToolkitApplication : Application() {
149157 analytics?.setLocalDispatchPeriod(120 ) // Set dispatching back to Android default
150158 }
151159
160+ suspend fun isUpdateRequired (): Boolean {
161+ return withContext(Dispatchers .IO ) {
162+ if (wasInstalledFromStore(this @HttpToolkitApplication)) {
163+ // We only check for updates for side-loaded/ADB-loaded versions. This is useful
164+ // because otherwise anything outside the play store gets no updates.
165+ Log .i(TAG , " Installed from play store, no update prompting required" )
166+ return @withContext false
167+ }
168+
169+ val httpClient = OkHttpClient ()
170+ val request = Request .Builder ()
171+ .url(" https://api.github.com/repos/httptoolkit/httptoolkit-android/releases/latest" )
172+ .build()
173+
174+ try {
175+ val response = httpClient.newCall(request).execute().use { response ->
176+ if (response.code != 200 ) throw RuntimeException (" Failed to check for updates" )
177+ response.body!! .string()
178+ }
179+
180+ val release = Klaxon ().parse<GithubRelease >(response)!!
181+ val releaseVersion =
182+ tryParseSemver(release.name)
183+ ? : tryParseSemver(release.tag_name)
184+ ? : throw RuntimeException (" Could not parse release version ${release.tag_name} " )
185+ val releaseDate = SimpleDateFormat (" yyyy-MM-dd'T'HH:mm:ss" ).parse(release.published_at)
186+
187+ val installedVersion = getInstalledVersion(this @HttpToolkitApplication)
188+
189+ val updateAvailable = releaseVersion > installedVersion
190+ // We avoid immediately prompting for updates because a) there's a review delay
191+ // before new updates go live, and b) it's annoying otherwise, if there's a rapid
192+ // series of releases. Better to start chasing users only after a week stable.
193+ val updateNotTooRecent = releaseDate.before(daysAgo(0 ))
194+
195+ Log .i(TAG ,
196+ if (updateAvailable && updateNotTooRecent)
197+ " New version available, released > 1 week"
198+ else if (updateAvailable)
199+ " New version available, but still recent, released $releaseDate "
200+ else
201+ " App is up to date"
202+ )
203+ return @withContext updateAvailable && updateNotTooRecent
204+ } catch (e: Exception ) {
205+ Log .w(TAG , e)
206+ return @withContext false
207+ }
208+ }
209+ }
210+
211+ }
212+
213+ private fun wasInstalledFromStore (context : Context ): Boolean {
214+ return context.packageManager.getInstallerPackageName(context.packageName) != null
215+ }
216+
217+ private data class GithubRelease (
218+ val tag_name : String? ,
219+ val name : String? ,
220+ val published_at : String
221+ )
222+
223+ private fun tryParseSemver (version : String? ): SemVer ? = try {
224+ if (version == null ) null
225+ else SemVer .parse(
226+ // Strip leading 'v'
227+ version.replace(Regex (" ^v" ), " " )
228+ )
229+ } catch (e: IllegalArgumentException ) {
230+ null
231+ }
232+
233+ private fun getInstalledVersion (context : Context ): SemVer {
234+ return SemVer .parse(
235+ context.packageManager.getPackageInfo(context.packageName, 0 ).versionName
236+ )
237+ }
238+
239+ private fun daysAgo (days : Int ): Date {
240+ val calendar = Calendar .getInstance()
241+ calendar.add(Calendar .DAY_OF_YEAR , - days)
242+ return calendar.time
152243}
0 commit comments