1
1
package tech.httptoolkit.android
2
2
3
3
import android.app.Application
4
- import android.content.*
4
+ import android.content.Context
5
+ import android.content.SharedPreferences
5
6
import android.content.pm.PackageManager
6
7
import android.os.Build
7
8
import android.util.Log
9
+ import androidx.core.content.edit
8
10
import com.android.installreferrer.api.InstallReferrerClient
9
11
import com.android.installreferrer.api.InstallReferrerClient.InstallReferrerResponse
10
12
import com.android.installreferrer.api.InstallReferrerStateListener
13
+ import com.beust.klaxon.Json
11
14
import com.beust.klaxon.Klaxon
12
15
import io.sentry.Sentry
13
16
import kotlinx.coroutines.Dispatchers
@@ -16,7 +19,9 @@ import net.swiftzer.semver.SemVer
16
19
import okhttp3.OkHttpClient
17
20
import okhttp3.Request
18
21
import java.text.SimpleDateFormat
19
- import java.util.*
22
+ import java.util.Calendar
23
+ import java.util.Date
24
+ import java.util.Locale
20
25
import java.util.concurrent.atomic.AtomicBoolean
21
26
import kotlin.coroutines.resume
22
27
import kotlin.coroutines.suspendCoroutine
@@ -25,19 +30,22 @@ private const val VPN_START_TIME_PREF = "vpn-start-time"
25
30
private const val LAST_UPDATE_CHECK_TIME_PREF = " update-check-time"
26
31
private const val APP_CRASHED_PREF = " app-crashed"
27
32
private 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"
28
36
29
37
private 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"
41
49
42
50
private val bootTime = (System .currentTimeMillis() - android.os.SystemClock .elapsedRealtime())
43
51
@@ -52,23 +60,23 @@ class HttpToolkitApplication : Application() {
52
60
}
53
61
set(value) {
54
62
if (value) {
55
- prefs.edit(). putLong(VPN_START_TIME_PREF , System .currentTimeMillis()). apply ()
63
+ prefs.edit { putLong(VPN_START_TIME_PREF , System .currentTimeMillis()) }
56
64
} else {
57
- prefs.edit(). putLong(VPN_START_TIME_PREF , - 1 ). apply ()
65
+ prefs.edit { putLong(VPN_START_TIME_PREF , - 1 ) }
58
66
}
59
67
}
60
68
61
69
override fun onCreate () {
62
70
super .onCreate()
63
- prefs = getSharedPreferences(" tech.httptoolkit.android " , MODE_PRIVATE )
71
+ prefs = getSharedPreferences(HTTP_TOOLKIT_PREFERENCES_NAME , MODE_PRIVATE )
64
72
65
73
Thread .setDefaultUncaughtExceptionHandler { _, _ ->
66
- prefs.edit(). putBoolean(APP_CRASHED_PREF , true ). apply ()
74
+ prefs.edit { putBoolean(APP_CRASHED_PREF , true ) }
67
75
}
68
76
69
77
// Check if we've been recreated unexpectedly, with no crashes in the meantime:
70
78
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 ) }
72
80
73
81
vpnWasKilled = vpnShouldBeRunning && ! isVpnActive() && ! appCrashed && ! isProbablyEmulator
74
82
if (vpnWasKilled) {
@@ -94,10 +102,10 @@ class HttpToolkitApplication : Application() {
94
102
/* *
95
103
* Grab any first run params, drop them for future usage, and return them.
96
104
* This will return first-run params at most once (per install).
97
- */
105
+ */
98
106
suspend fun popFirstRunParams (): String? {
99
107
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 ) }
101
109
102
110
val installTime = packageManager.getPackageInfo(packageName, 0 ).firstInstallTime
103
111
val now = System .currentTimeMillis()
@@ -128,6 +136,7 @@ class HttpToolkitApplication : Application() {
128
136
Log .i(TAG , " Returning first run referrer: $referrer " )
129
137
resume(referrer)
130
138
}
139
+
131
140
else -> {
132
141
Log .w(TAG , " Couldn't get install referrer, skipping: $responseCode " )
133
142
resume(null )
@@ -146,14 +155,15 @@ class HttpToolkitApplication : Application() {
146
155
var lastProxy: ProxyConfig ?
147
156
get() {
148
157
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 )
151
160
152
161
return when {
153
162
serialized != null -> {
154
163
Log .i(TAG , " Found last proxy config: $serialized " )
155
164
Klaxon ().converter(CertificateConverter ).parse<ProxyConfig >(serialized)
156
165
}
166
+
157
167
else -> {
158
168
Log .i(TAG , " No proxy config found" )
159
169
null
@@ -162,19 +172,19 @@ class HttpToolkitApplication : Application() {
162
172
}
163
173
set(proxyConfig) {
164
174
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 )
166
176
167
177
if (proxyConfig != null ) {
168
178
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) }
170
180
} else {
171
- prefs.edit(). remove(" last-proxy-config " ). apply ()
181
+ prefs.edit { remove(LAST_PROXY_CONFIG_PREF_KEY ) }
172
182
}
173
183
}
174
184
175
185
var uninterceptedApps: Set <String >
176
186
get() {
177
- val prefs = getSharedPreferences(" tech.httptoolkit.android " , MODE_PRIVATE )
187
+ val prefs = getSharedPreferences(HTTP_TOOLKIT_PREFERENCES_NAME , MODE_PRIVATE )
178
188
val packagesSet = prefs.getStringSet(" unintercepted-packages" , null )
179
189
val allPackages = packageManager.getInstalledPackages(PackageManager .GET_META_DATA )
180
190
.map { pkg -> pkg.packageName }
@@ -183,20 +193,20 @@ class HttpToolkitApplication : Application() {
183
193
.toSet()
184
194
}
185
195
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) }
188
198
}
189
199
190
200
var interceptedPorts: Set <Int >
191
201
get() {
192
- val prefs = getSharedPreferences(" tech.httptoolkit.android " , MODE_PRIVATE )
202
+ val prefs = getSharedPreferences(HTTP_TOOLKIT_PREFERENCES_NAME , MODE_PRIVATE )
193
203
val portsSet = prefs.getStringSet(" intercepted-ports" , null )
194
204
return portsSet?.map(String ::toInt)?.toSortedSet()
195
205
? : DEFAULT_PORTS
196
206
}
197
207
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()) }
200
210
}
201
211
202
212
suspend fun isUpdateRequired (): Boolean {
@@ -208,7 +218,8 @@ class HttpToolkitApplication : Application() {
208
218
return @withContext false
209
219
}
210
220
211
- val lastUpdateTime = prefs.getLong(LAST_UPDATE_CHECK_TIME_PREF ,
221
+ val lastUpdateTime = prefs.getLong(
222
+ LAST_UPDATE_CHECK_TIME_PREF ,
212
223
firstInstallTime(this @HttpToolkitApplication)
213
224
)
214
225
@@ -230,9 +241,11 @@ class HttpToolkitApplication : Application() {
230
241
val release = Klaxon ().parse<GithubRelease >(response)!!
231
242
val releaseVersion =
232
243
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)!!
236
249
237
250
val installedVersion = getInstalledVersion(this @HttpToolkitApplication)
238
251
@@ -242,7 +255,8 @@ class HttpToolkitApplication : Application() {
242
255
// series of releases. Better to start chasing users only after a week stable.
243
256
val updateNotTooRecent = releaseDate.before(daysAgo(7 ))
244
257
245
- Log .i(TAG ,
258
+ Log .i(
259
+ TAG ,
246
260
if (updateAvailable && updateNotTooRecent)
247
261
" New version available, released > 1 week"
248
262
else if (updateAvailable)
@@ -251,7 +265,7 @@ class HttpToolkitApplication : Application() {
251
265
" App is up to date"
252
266
)
253
267
254
- prefs.edit(). putLong(LAST_UPDATE_CHECK_TIME_PREF , System .currentTimeMillis()). apply ()
268
+ prefs.edit { putLong(LAST_UPDATE_CHECK_TIME_PREF , System .currentTimeMillis()) }
255
269
return @withContext updateAvailable && updateNotTooRecent
256
270
} catch (e: Exception ) {
257
271
Log .w(TAG , e)
@@ -263,17 +277,24 @@ class HttpToolkitApplication : Application() {
263
277
}
264
278
265
279
private 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
+ }
267
285
}
268
286
269
287
private fun firstInstallTime (context : Context ): Long {
270
288
return context.packageManager.getPackageInfo(context.packageName, 0 ).firstInstallTime
271
289
}
272
290
273
291
private data class GithubRelease (
274
- val tag_name : String? ,
292
+ @Json(name = " tag_name" )
293
+ val tagName : String? ,
294
+ @Json(name = " name" )
275
295
val name : String? ,
276
- val published_at : String
296
+ @Json(name = " published_at" )
297
+ val publishedAt : String ,
277
298
)
278
299
279
300
private fun tryParseSemver (version : String? ): SemVer ? = try {
@@ -296,4 +317,4 @@ private fun daysAgo(days: Int): Date {
296
317
val calendar = Calendar .getInstance()
297
318
calendar.add(Calendar .DAY_OF_YEAR , - days)
298
319
return calendar.time
299
- }
320
+ }
0 commit comments