1
1
// Copyright (c) Tailscale Inc & AUTHORS
2
2
// SPDX-License-Identifier: BSD-3-Clause
3
3
package com.tailscale.ipn
4
+
4
5
import android.Manifest
5
6
import android.app.Application
6
7
import android.app.Notification
@@ -37,6 +38,10 @@ import com.tailscale.ipn.ui.viewModel.AppViewModelFactory
37
38
import com.tailscale.ipn.util.FeatureFlags
38
39
import com.tailscale.ipn.util.ShareFileHelper
39
40
import com.tailscale.ipn.util.TSLog
41
+ import java.io.IOException
42
+ import java.net.NetworkInterface
43
+ import java.security.GeneralSecurityException
44
+ import java.util.Locale
40
45
import kotlinx.coroutines.CoroutineScope
41
46
import kotlinx.coroutines.Dispatchers
42
47
import kotlinx.coroutines.SupervisorJob
@@ -48,12 +53,10 @@ import kotlinx.coroutines.launch
48
53
import kotlinx.serialization.encodeToString
49
54
import kotlinx.serialization.json.Json
50
55
import libtailscale.Libtailscale
51
- import java.io.IOException
52
- import java.net.NetworkInterface
53
- import java.security.GeneralSecurityException
54
- import java.util.Locale
56
+
55
57
class App : UninitializedApp (), libtailscale.AppContext, ViewModelStoreOwner {
56
58
val applicationScope = CoroutineScope (SupervisorJob () + Dispatchers .Default )
59
+
57
60
companion object {
58
61
private const val FILE_CHANNEL_ID = " tailscale-files"
59
62
// Key to store the SAF URI in EncryptedSharedPreferences.
@@ -70,26 +73,34 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
70
73
return appInstance
71
74
}
72
75
}
76
+
73
77
val dns = DnsConfig ()
74
78
private lateinit var connectivityManager: ConnectivityManager
75
79
private lateinit var mdmChangeReceiver: MDMSettingsChangedReceiver
76
80
private lateinit var app: libtailscale.Application
77
81
override val viewModelStore: ViewModelStore
78
82
get() = appViewModelStore
83
+
79
84
private val appViewModelStore: ViewModelStore by lazy { ViewModelStore () }
80
85
var healthNotifier: HealthNotifier ? = null
86
+
81
87
override fun getPlatformDNSConfig (): String = dns.dnsConfigAsString
88
+
82
89
override fun getInstallSource (): String = AppSourceChecker .getInstallSource(this )
90
+
83
91
override fun shouldUseGoogleDNSFallback (): Boolean = BuildConfig .USE_GOOGLE_DNS_FALLBACK
92
+
84
93
override fun log (s : String , s1 : String ) {
85
94
Log .d(s, s1)
86
95
}
96
+
87
97
fun getLibtailscaleApp (): libtailscale.Application {
88
98
if (! isInitialized) {
89
99
initOnce() // Calls the synchronized initialization logic
90
100
}
91
101
return app
92
102
}
103
+
93
104
override fun onCreate () {
94
105
super .onCreate()
95
106
appInstance = this
@@ -113,6 +124,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
113
124
getString(R .string.health_channel_description),
114
125
NotificationManagerCompat .IMPORTANCE_HIGH )
115
126
}
127
+
116
128
override fun onTerminate () {
117
129
super .onTerminate()
118
130
Notifier .stop()
@@ -121,7 +133,9 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
121
133
viewModelStore.clear()
122
134
unregisterReceiver(mdmChangeReceiver)
123
135
}
136
+
124
137
@Volatile private var isInitialized = false
138
+
125
139
@Synchronized
126
140
private fun initOnce () {
127
141
if (isInitialized) {
@@ -130,6 +144,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
130
144
initializeApp()
131
145
isInitialized = true
132
146
}
147
+
133
148
private fun initializeApp () {
134
149
// Check if a directory URI has already been stored.
135
150
val storedUri = getStoredDirectoryUri()
@@ -244,6 +259,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
244
259
EncryptedSharedPreferences .PrefKeyEncryptionScheme .AES256_SIV ,
245
260
EncryptedSharedPreferences .PrefValueEncryptionScheme .AES256_GCM )
246
261
}
262
+
247
263
fun getStoredDirectoryUri (): Uri ? {
248
264
val uriString = getEncryptedPrefs().getString(PREF_KEY_SAF_URI , null )
249
265
return uriString?.let { Uri .parse(it) }
@@ -258,6 +274,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
258
274
QuickToggleService .updateTile()
259
275
TSLog .d(" App" , " Set Tile Ready: $ableToStartVPN " )
260
276
}
277
+
261
278
override fun getModelName (): String {
262
279
val manu = Build .MANUFACTURER
263
280
var model = Build .MODEL
@@ -268,10 +285,13 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
268
285
}
269
286
return " $manu $model "
270
287
}
288
+
271
289
override fun getOSVersion (): String = Build .VERSION .RELEASE
290
+
272
291
override fun isChromeOS (): Boolean {
273
292
return packageManager.hasSystemFeature(" android.hardware.type.pc" )
274
293
}
294
+
275
295
override fun getInterfacesAsString (): String {
276
296
val interfaces: ArrayList <NetworkInterface > =
277
297
java.util.Collections .list(NetworkInterface .getNetworkInterfaces())
@@ -303,11 +323,13 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
303
323
}
304
324
return sb.toString()
305
325
}
326
+
306
327
@Throws(
307
328
IOException ::class , GeneralSecurityException ::class , MDMSettings .NoSuchKeyException ::class )
308
329
override fun getSyspolicyBooleanValue (key : String ): Boolean {
309
330
return getSyspolicyStringValue(key) == " true"
310
331
}
332
+
311
333
@Throws(
312
334
IOException ::class , GeneralSecurityException ::class , MDMSettings .NoSuchKeyException ::class )
313
335
override fun getSyspolicyStringValue (key : String ): String {
@@ -317,6 +339,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
317
339
}
318
340
return setting.value?.toString() ? : " "
319
341
}
342
+
320
343
@Throws(
321
344
IOException ::class , GeneralSecurityException ::class , MDMSettings .NoSuchKeyException ::class )
322
345
override fun getSyspolicyStringArrayJSONValue (key : String ): String {
@@ -332,6 +355,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
332
355
throw MDMSettings .NoSuchKeyException ()
333
356
}
334
357
}
358
+
335
359
fun notifyPolicyChanged () {
336
360
app.notifyPolicyChanged()
337
361
}
@@ -374,19 +398,23 @@ open class UninitializedApp : Application() {
374
398
}
375
399
}
376
400
}
401
+
377
402
protected fun setUnprotectedInstance (instance : UninitializedApp ) {
378
403
appInstance = instance
379
404
}
405
+
380
406
protected fun setAbleToStartVPN (rdy : Boolean ) {
381
407
getUnencryptedPrefs().edit().putBoolean(ABLE_TO_START_VPN_KEY , rdy).apply ()
382
408
}
383
409
/* * This function can be called without initializing the App. */
384
410
fun isAbleToStartVPN (): Boolean {
385
411
return getUnencryptedPrefs().getBoolean(ABLE_TO_START_VPN_KEY , false )
386
412
}
413
+
387
414
private fun getUnencryptedPrefs (): SharedPreferences {
388
415
return getSharedPreferences(UNENCRYPTED_PREFERENCES , MODE_PRIVATE )
389
416
}
417
+
390
418
fun startVPN () {
391
419
val intent = Intent (this , IPNService ::class .java).apply { action = IPNService .ACTION_START_VPN }
392
420
// FLAG_UPDATE_CURRENT ensures that if the intent is already pending, the existing intent will
@@ -411,6 +439,7 @@ open class UninitializedApp : Application() {
411
439
TSLog .e(TAG , " startVPN hit exception: $e " )
412
440
}
413
441
}
442
+
414
443
fun stopVPN () {
415
444
val intent = Intent (this , IPNService ::class .java).apply { action = IPNService .ACTION_STOP_VPN }
416
445
try {
@@ -421,6 +450,7 @@ open class UninitializedApp : Application() {
421
450
TSLog .e(TAG , " stopVPN hit exception in startService(): $e " )
422
451
}
423
452
}
453
+
424
454
fun restartVPN () {
425
455
val intent =
426
456
Intent (this , IPNService ::class .java).apply { action = IPNService .ACTION_RESTART_VPN }
@@ -432,19 +462,22 @@ open class UninitializedApp : Application() {
432
462
TSLog .e(TAG , " restartVPN hit exception in startService(): $e " )
433
463
}
434
464
}
465
+
435
466
fun createNotificationChannel (id : String , name : String , description : String , importance : Int ) {
436
467
val channel = NotificationChannel (id, name, importance)
437
468
channel.description = description
438
469
notificationManager = NotificationManagerCompat .from(this )
439
470
notificationManager.createNotificationChannel(channel)
440
471
}
472
+
441
473
fun notifyStatus (
442
474
vpnRunning : Boolean ,
443
475
hideDisconnectAction : Boolean ,
444
476
exitNodeName : String? = null
445
477
) {
446
478
notifyStatus(buildStatusNotification(vpnRunning, hideDisconnectAction, exitNodeName))
447
479
}
480
+
448
481
fun notifyStatus (notification : Notification ) {
449
482
if (ActivityCompat .checkSelfPermission(this , Manifest .permission.POST_NOTIFICATIONS ) !=
450
483
PackageManager .PERMISSION_GRANTED ) {
@@ -459,6 +492,7 @@ open class UninitializedApp : Application() {
459
492
}
460
493
notificationManager.notify(STATUS_NOTIFICATION_ID , notification)
461
494
}
495
+
462
496
fun buildStatusNotification (
463
497
vpnRunning : Boolean ,
464
498
hideDisconnectAction : Boolean ,
@@ -504,6 +538,7 @@ open class UninitializedApp : Application() {
504
538
}
505
539
return builder.build()
506
540
}
541
+
507
542
fun updateUserDisallowedPackageNames (packageNames : List <String >) {
508
543
if (packageNames.any { it.isEmpty() }) {
509
544
TSLog .e(TAG , " updateUserDisallowedPackageNames called with empty packageName(s)" )
@@ -512,6 +547,7 @@ open class UninitializedApp : Application() {
512
547
getUnencryptedPrefs().edit().putStringSet(DISALLOWED_APPS_KEY , packageNames.toSet()).apply ()
513
548
this .restartVPN()
514
549
}
550
+
515
551
fun disallowedPackageNames (): List <String > {
516
552
val mdmDisallowed =
517
553
MDMSettings .excludedPackages.flow.value.value?.split(" ," )?.map { it.trim() } ? : emptyList()
@@ -553,4 +589,4 @@ open class UninitializedApp : Application() {
553
589
// Android Connectivity Service https://github.com/tailscale/tailscale/issues/14128
554
590
" com.google.android.apps.scone" ,
555
591
)
556
- }
592
+ }
0 commit comments