11package tech.httptoolkit.android
22
33import android.app.Application
4- import android.content.*
4+ import android.content.Context
5+ import android.content.SharedPreferences
56import android.content.pm.PackageManager
67import android.os.Build
78import android.util.Log
9+ import androidx.core.content.edit
810import com.android.installreferrer.api.InstallReferrerClient
911import com.android.installreferrer.api.InstallReferrerClient.InstallReferrerResponse
1012import com.android.installreferrer.api.InstallReferrerStateListener
13+ import com.beust.klaxon.Json
1114import com.beust.klaxon.Klaxon
1215import io.sentry.Sentry
1316import kotlinx.coroutines.Dispatchers
@@ -16,7 +19,9 @@ import net.swiftzer.semver.SemVer
1619import okhttp3.OkHttpClient
1720import okhttp3.Request
1821import java.text.SimpleDateFormat
19- import java.util.*
22+ import java.util.Calendar
23+ import java.util.Date
24+ import java.util.Locale
2025import java.util.concurrent.atomic.AtomicBoolean
2126import kotlin.coroutines.resume
2227import kotlin.coroutines.suspendCoroutine
@@ -25,19 +30,22 @@ private const val VPN_START_TIME_PREF = "vpn-start-time"
2530private const val LAST_UPDATE_CHECK_TIME_PREF = " update-check-time"
2631private const val APP_CRASHED_PREF = " app-crashed"
2732private const val FIRST_RUN_PREF = " is-first-run"
33+ private const val HTTP_TOOLKIT_PREFERENCES_NAME = " tech.httptoolkit.android"
34+ private const val LAST_PROXY_CONFIG_PREF_KEY = " last-proxy-config"
35+ private const val TIME_PATTERN = " yyyy-MM-dd'T'HH:mm:ss"
2836
2937private val isProbablyEmulator =
30- Build .FINGERPRINT .startsWith(" generic" )
31- || Build .FINGERPRINT .startsWith(" unknown" )
32- || Build .MODEL .contains(" google_sdk" )
33- || Build .MODEL .contains(" sdk_gphone" )
34- || Build .MODEL .contains(" Emulator" )
35- || Build .MODEL .contains(" Android SDK built for x86" )
36- || Build .BOARD == " QC_Reference_Phone"
37- || Build .MANUFACTURER .contains(" Genymotion" )
38- || Build .HOST .startsWith(" Build" )
39- || (Build .BRAND .startsWith(" generic" ) && Build .DEVICE .startsWith(" generic" ))
40- || Build .PRODUCT == " google_sdk"
38+ Build .FINGERPRINT .startsWith(" generic" )
39+ || Build .FINGERPRINT .startsWith(" unknown" )
40+ || Build .MODEL .contains(" google_sdk" )
41+ || Build .MODEL .contains(" sdk_gphone" )
42+ || Build .MODEL .contains(" Emulator" )
43+ || Build .MODEL .contains(" Android SDK built for x86" )
44+ || Build .BOARD == " QC_Reference_Phone"
45+ || Build .MANUFACTURER .contains(" Genymotion" )
46+ || Build .HOST .startsWith(" Build" )
47+ || (Build .BRAND .startsWith(" generic" ) && Build .DEVICE .startsWith(" generic" ))
48+ || Build .PRODUCT == " google_sdk"
4149
4250private val bootTime = (System .currentTimeMillis() - android.os.SystemClock .elapsedRealtime())
4351
@@ -52,23 +60,23 @@ class HttpToolkitApplication : Application() {
5260 }
5361 set(value) {
5462 if (value) {
55- prefs.edit(). putLong(VPN_START_TIME_PREF , System .currentTimeMillis()). apply ()
63+ prefs.edit { putLong(VPN_START_TIME_PREF , System .currentTimeMillis()) }
5664 } else {
57- prefs.edit(). putLong(VPN_START_TIME_PREF , - 1 ). apply ()
65+ prefs.edit { putLong(VPN_START_TIME_PREF , - 1 ) }
5866 }
5967 }
6068
6169 override fun onCreate () {
6270 super .onCreate()
63- prefs = getSharedPreferences(" tech.httptoolkit.android " , MODE_PRIVATE )
71+ prefs = getSharedPreferences(HTTP_TOOLKIT_PREFERENCES_NAME , MODE_PRIVATE )
6472
6573 Thread .setDefaultUncaughtExceptionHandler { _, _ ->
66- prefs.edit(). putBoolean(APP_CRASHED_PREF , true ). apply ()
74+ prefs.edit { putBoolean(APP_CRASHED_PREF , true ) }
6775 }
6876
6977 // Check if we've been recreated unexpectedly, with no crashes in the meantime:
7078 val appCrashed = prefs.getBoolean(APP_CRASHED_PREF , false )
71- prefs.edit(). putBoolean(APP_CRASHED_PREF , false ). apply ()
79+ prefs.edit { putBoolean(APP_CRASHED_PREF , false ) }
7280
7381 vpnWasKilled = vpnShouldBeRunning && ! isVpnActive() && ! appCrashed && ! isProbablyEmulator
7482 if (vpnWasKilled) {
@@ -94,10 +102,10 @@ class HttpToolkitApplication : Application() {
94102 /* *
95103 * Grab any first run params, drop them for future usage, and return them.
96104 * This will return first-run params at most once (per install).
97- */
105+ */
98106 suspend fun popFirstRunParams (): String? {
99107 val isFirstRun = prefs.getBoolean(FIRST_RUN_PREF , true )
100- prefs.edit(). putBoolean(FIRST_RUN_PREF , false ). apply ()
108+ prefs.edit { putBoolean(FIRST_RUN_PREF , false ) }
101109
102110 val installTime = packageManager.getPackageInfo(packageName, 0 ).firstInstallTime
103111 val now = System .currentTimeMillis()
@@ -128,6 +136,7 @@ class HttpToolkitApplication : Application() {
128136 Log .i(TAG , " Returning first run referrer: $referrer " )
129137 resume(referrer)
130138 }
139+
131140 else -> {
132141 Log .w(TAG , " Couldn't get install referrer, skipping: $responseCode " )
133142 resume(null )
@@ -146,14 +155,15 @@ class HttpToolkitApplication : Application() {
146155 var lastProxy: ProxyConfig ?
147156 get() {
148157 Log .i(TAG , " Loading last proxy config" )
149- val prefs = getSharedPreferences(" tech.httptoolkit.android " , MODE_PRIVATE )
150- val serialized = prefs.getString(" last-proxy-config " , null )
158+ val prefs = getSharedPreferences(HTTP_TOOLKIT_PREFERENCES_NAME , MODE_PRIVATE )
159+ val serialized = prefs.getString(LAST_PROXY_CONFIG_PREF_KEY , null )
151160
152161 return when {
153162 serialized != null -> {
154163 Log .i(TAG , " Found last proxy config: $serialized " )
155164 Klaxon ().converter(CertificateConverter ).parse<ProxyConfig >(serialized)
156165 }
166+
157167 else -> {
158168 Log .i(TAG , " No proxy config found" )
159169 null
@@ -162,19 +172,19 @@ class HttpToolkitApplication : Application() {
162172 }
163173 set(proxyConfig) {
164174 Log .i(TAG , if (proxyConfig == null ) " Clearing proxy config" else " Saving proxy config" )
165- val prefs = getSharedPreferences(" tech.httptoolkit.android " , MODE_PRIVATE )
175+ val prefs = getSharedPreferences(HTTP_TOOLKIT_PREFERENCES_NAME , MODE_PRIVATE )
166176
167177 if (proxyConfig != null ) {
168178 val serialized = Klaxon ().converter(CertificateConverter ).toJsonString(proxyConfig)
169- prefs.edit(). putString(" last-proxy-config " , serialized). apply ()
179+ prefs.edit { putString(LAST_PROXY_CONFIG_PREF_KEY , serialized) }
170180 } else {
171- prefs.edit(). remove(" last-proxy-config " ). apply ()
181+ prefs.edit { remove(LAST_PROXY_CONFIG_PREF_KEY ) }
172182 }
173183 }
174184
175185 var uninterceptedApps: Set <String >
176186 get() {
177- val prefs = getSharedPreferences(" tech.httptoolkit.android " , MODE_PRIVATE )
187+ val prefs = getSharedPreferences(HTTP_TOOLKIT_PREFERENCES_NAME , MODE_PRIVATE )
178188 val packagesSet = prefs.getStringSet(" unintercepted-packages" , null )
179189 val allPackages = packageManager.getInstalledPackages(PackageManager .GET_META_DATA )
180190 .map { pkg -> pkg.packageName }
@@ -183,20 +193,20 @@ class HttpToolkitApplication : Application() {
183193 .toSet()
184194 }
185195 set(packageNames) {
186- val prefs = getSharedPreferences(" tech.httptoolkit.android " , MODE_PRIVATE )
187- prefs.edit(). putStringSet(" unintercepted-packages" , packageNames). apply ()
196+ val prefs = getSharedPreferences(HTTP_TOOLKIT_PREFERENCES_NAME , MODE_PRIVATE )
197+ prefs.edit { putStringSet(" unintercepted-packages" , packageNames) }
188198 }
189199
190200 var interceptedPorts: Set <Int >
191201 get() {
192- val prefs = getSharedPreferences(" tech.httptoolkit.android " , MODE_PRIVATE )
202+ val prefs = getSharedPreferences(HTTP_TOOLKIT_PREFERENCES_NAME , MODE_PRIVATE )
193203 val portsSet = prefs.getStringSet(" intercepted-ports" , null )
194204 return portsSet?.map(String ::toInt)?.toSortedSet()
195205 ? : DEFAULT_PORTS
196206 }
197207 set(ports) {
198- val prefs = getSharedPreferences(" tech.httptoolkit.android " , MODE_PRIVATE )
199- prefs.edit(). putStringSet(" intercepted-ports" , ports.map(Int ::toString).toSet()). apply ()
208+ val prefs = getSharedPreferences(HTTP_TOOLKIT_PREFERENCES_NAME , MODE_PRIVATE )
209+ prefs.edit { putStringSet(" intercepted-ports" , ports.map(Int ::toString).toSet()) }
200210 }
201211
202212 suspend fun isUpdateRequired (): Boolean {
@@ -208,7 +218,8 @@ class HttpToolkitApplication : Application() {
208218 return @withContext false
209219 }
210220
211- val lastUpdateTime = prefs.getLong(LAST_UPDATE_CHECK_TIME_PREF ,
221+ val lastUpdateTime = prefs.getLong(
222+ LAST_UPDATE_CHECK_TIME_PREF ,
212223 firstInstallTime(this @HttpToolkitApplication)
213224 )
214225
@@ -230,9 +241,11 @@ class HttpToolkitApplication : Application() {
230241 val release = Klaxon ().parse<GithubRelease >(response)!!
231242 val releaseVersion =
232243 tryParseSemver(release.name)
233- ? : tryParseSemver(release.tag_name)
234- ? : throw RuntimeException (" Could not parse release version ${release.tag_name} " )
235- val releaseDate = SimpleDateFormat (" yyyy-MM-dd'T'HH:mm:ss" ).parse(release.published_at)!!
244+ ? : tryParseSemver(release.tagName)
245+ ? : throw RuntimeException (" Could not parse release version ${release.tagName} " )
246+
247+ val releaseDate =
248+ SimpleDateFormat (TIME_PATTERN , Locale .getDefault()).parse(release.publishedAt)!!
236249
237250 val installedVersion = getInstalledVersion(this @HttpToolkitApplication)
238251
@@ -242,7 +255,8 @@ class HttpToolkitApplication : Application() {
242255 // series of releases. Better to start chasing users only after a week stable.
243256 val updateNotTooRecent = releaseDate.before(daysAgo(7 ))
244257
245- Log .i(TAG ,
258+ Log .i(
259+ TAG ,
246260 if (updateAvailable && updateNotTooRecent)
247261 " New version available, released > 1 week"
248262 else if (updateAvailable)
@@ -251,7 +265,7 @@ class HttpToolkitApplication : Application() {
251265 " App is up to date"
252266 )
253267
254- prefs.edit(). putLong(LAST_UPDATE_CHECK_TIME_PREF , System .currentTimeMillis()). apply ()
268+ prefs.edit { putLong(LAST_UPDATE_CHECK_TIME_PREF , System .currentTimeMillis()) }
255269 return @withContext updateAvailable && updateNotTooRecent
256270 } catch (e: Exception ) {
257271 Log .w(TAG , e)
@@ -263,17 +277,24 @@ class HttpToolkitApplication : Application() {
263277}
264278
265279private fun wasInstalledFromStore (context : Context ): Boolean {
266- return context.packageManager.getInstallerPackageName(context.packageName) != null
280+ return if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .R ) {
281+ context.packageManager.getInstallSourceInfo(context.packageName).installingPackageName != null
282+ } else {
283+ context.packageManager.getInstallerPackageName(context.packageName) != null
284+ }
267285}
268286
269287private fun firstInstallTime (context : Context ): Long {
270288 return context.packageManager.getPackageInfo(context.packageName, 0 ).firstInstallTime
271289}
272290
273291private data class GithubRelease (
274- val tag_name : String? ,
292+ @Json(name = " tag_name" )
293+ val tagName : String? ,
294+ @Json(name = " name" )
275295 val name : String? ,
276- val published_at : String
296+ @Json(name = " published_at" )
297+ val publishedAt : String ,
277298)
278299
279300private fun tryParseSemver (version : String? ): SemVer ? = try {
@@ -296,4 +317,4 @@ private fun daysAgo(days: Int): Date {
296317 val calendar = Calendar .getInstance()
297318 calendar.add(Calendar .DAY_OF_YEAR , - days)
298319 return calendar.time
299- }
320+ }
0 commit comments