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