1
1
package tech.httptoolkit.android
2
2
3
3
import android.app.Application
4
+ import android.content.Context
4
5
import android.util.Log
5
6
import com.android.installreferrer.api.InstallReferrerClient
6
7
import com.android.installreferrer.api.InstallReferrerClient.InstallReferrerResponse
@@ -11,10 +12,17 @@ import com.google.android.gms.analytics.HitBuilders
11
12
import com.google.android.gms.analytics.Tracker
12
13
import io.sentry.Sentry
13
14
import 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.*
14
21
import java.util.concurrent.atomic.AtomicBoolean
15
22
import kotlin.coroutines.resume
16
23
import kotlin.coroutines.suspendCoroutine
17
24
25
+
18
26
class HttpToolkitApplication : Application () {
19
27
20
28
private val TAG = HttpToolkitApplication ::class .simpleName
@@ -149,4 +157,87 @@ class HttpToolkitApplication : Application() {
149
157
analytics?.setLocalDispatchPeriod(120 ) // Set dispatching back to Android default
150
158
}
151
159
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
152
243
}
0 commit comments