11// Copyright (c) Tailscale Inc & AUTHORS
22// SPDX-License-Identifier: BSD-3-Clause
33package com.tailscale.ipn
4+
45import android.Manifest
56import android.app.Application
67import android.app.Notification
@@ -37,6 +38,10 @@ import com.tailscale.ipn.ui.viewModel.AppViewModelFactory
3738import com.tailscale.ipn.util.FeatureFlags
3839import com.tailscale.ipn.util.ShareFileHelper
3940import 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
4045import kotlinx.coroutines.CoroutineScope
4146import kotlinx.coroutines.Dispatchers
4247import kotlinx.coroutines.SupervisorJob
@@ -48,12 +53,10 @@ import kotlinx.coroutines.launch
4853import kotlinx.serialization.encodeToString
4954import kotlinx.serialization.json.Json
5055import libtailscale.Libtailscale
51- import java.io.IOException
52- import java.net.NetworkInterface
53- import java.security.GeneralSecurityException
54- import java.util.Locale
56+
5557class App : UninitializedApp (), libtailscale.AppContext, ViewModelStoreOwner {
5658 val applicationScope = CoroutineScope (SupervisorJob () + Dispatchers .Default )
59+
5760 companion object {
5861 private const val FILE_CHANNEL_ID = " tailscale-files"
5962 // Key to store the SAF URI in EncryptedSharedPreferences.
@@ -70,26 +73,34 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
7073 return appInstance
7174 }
7275 }
76+
7377 val dns = DnsConfig ()
7478 private lateinit var connectivityManager: ConnectivityManager
7579 private lateinit var mdmChangeReceiver: MDMSettingsChangedReceiver
7680 private lateinit var app: libtailscale.Application
7781 override val viewModelStore: ViewModelStore
7882 get() = appViewModelStore
83+
7984 private val appViewModelStore: ViewModelStore by lazy { ViewModelStore () }
8085 var healthNotifier: HealthNotifier ? = null
86+
8187 override fun getPlatformDNSConfig (): String = dns.dnsConfigAsString
88+
8289 override fun getInstallSource (): String = AppSourceChecker .getInstallSource(this )
90+
8391 override fun shouldUseGoogleDNSFallback (): Boolean = BuildConfig .USE_GOOGLE_DNS_FALLBACK
92+
8493 override fun log (s : String , s1 : String ) {
8594 Log .d(s, s1)
8695 }
96+
8797 fun getLibtailscaleApp (): libtailscale.Application {
8898 if (! isInitialized) {
8999 initOnce() // Calls the synchronized initialization logic
90100 }
91101 return app
92102 }
103+
93104 override fun onCreate () {
94105 super .onCreate()
95106 appInstance = this
@@ -113,6 +124,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
113124 getString(R .string.health_channel_description),
114125 NotificationManagerCompat .IMPORTANCE_HIGH )
115126 }
127+
116128 override fun onTerminate () {
117129 super .onTerminate()
118130 Notifier .stop()
@@ -121,7 +133,9 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
121133 viewModelStore.clear()
122134 unregisterReceiver(mdmChangeReceiver)
123135 }
136+
124137 @Volatile private var isInitialized = false
138+
125139 @Synchronized
126140 private fun initOnce () {
127141 if (isInitialized) {
@@ -130,6 +144,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
130144 initializeApp()
131145 isInitialized = true
132146 }
147+
133148 private fun initializeApp () {
134149 // Check if a directory URI has already been stored.
135150 val storedUri = getStoredDirectoryUri()
@@ -244,6 +259,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
244259 EncryptedSharedPreferences .PrefKeyEncryptionScheme .AES256_SIV ,
245260 EncryptedSharedPreferences .PrefValueEncryptionScheme .AES256_GCM )
246261 }
262+
247263 fun getStoredDirectoryUri (): Uri ? {
248264 val uriString = getEncryptedPrefs().getString(PREF_KEY_SAF_URI , null )
249265 return uriString?.let { Uri .parse(it) }
@@ -258,6 +274,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
258274 QuickToggleService .updateTile()
259275 TSLog .d(" App" , " Set Tile Ready: $ableToStartVPN " )
260276 }
277+
261278 override fun getModelName (): String {
262279 val manu = Build .MANUFACTURER
263280 var model = Build .MODEL
@@ -268,10 +285,13 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
268285 }
269286 return " $manu $model "
270287 }
288+
271289 override fun getOSVersion (): String = Build .VERSION .RELEASE
290+
272291 override fun isChromeOS (): Boolean {
273292 return packageManager.hasSystemFeature(" android.hardware.type.pc" )
274293 }
294+
275295 override fun getInterfacesAsString (): String {
276296 val interfaces: ArrayList <NetworkInterface > =
277297 java.util.Collections .list(NetworkInterface .getNetworkInterfaces())
@@ -303,11 +323,13 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
303323 }
304324 return sb.toString()
305325 }
326+
306327 @Throws(
307328 IOException ::class , GeneralSecurityException ::class , MDMSettings .NoSuchKeyException ::class )
308329 override fun getSyspolicyBooleanValue (key : String ): Boolean {
309330 return getSyspolicyStringValue(key) == " true"
310331 }
332+
311333 @Throws(
312334 IOException ::class , GeneralSecurityException ::class , MDMSettings .NoSuchKeyException ::class )
313335 override fun getSyspolicyStringValue (key : String ): String {
@@ -317,6 +339,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
317339 }
318340 return setting.value?.toString() ? : " "
319341 }
342+
320343 @Throws(
321344 IOException ::class , GeneralSecurityException ::class , MDMSettings .NoSuchKeyException ::class )
322345 override fun getSyspolicyStringArrayJSONValue (key : String ): String {
@@ -332,6 +355,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
332355 throw MDMSettings .NoSuchKeyException ()
333356 }
334357 }
358+
335359 fun notifyPolicyChanged () {
336360 app.notifyPolicyChanged()
337361 }
@@ -374,19 +398,23 @@ open class UninitializedApp : Application() {
374398 }
375399 }
376400 }
401+
377402 protected fun setUnprotectedInstance (instance : UninitializedApp ) {
378403 appInstance = instance
379404 }
405+
380406 protected fun setAbleToStartVPN (rdy : Boolean ) {
381407 getUnencryptedPrefs().edit().putBoolean(ABLE_TO_START_VPN_KEY , rdy).apply ()
382408 }
383409 /* * This function can be called without initializing the App. */
384410 fun isAbleToStartVPN (): Boolean {
385411 return getUnencryptedPrefs().getBoolean(ABLE_TO_START_VPN_KEY , false )
386412 }
413+
387414 private fun getUnencryptedPrefs (): SharedPreferences {
388415 return getSharedPreferences(UNENCRYPTED_PREFERENCES , MODE_PRIVATE )
389416 }
417+
390418 fun startVPN () {
391419 val intent = Intent (this , IPNService ::class .java).apply { action = IPNService .ACTION_START_VPN }
392420 // FLAG_UPDATE_CURRENT ensures that if the intent is already pending, the existing intent will
@@ -411,6 +439,7 @@ open class UninitializedApp : Application() {
411439 TSLog .e(TAG , " startVPN hit exception: $e " )
412440 }
413441 }
442+
414443 fun stopVPN () {
415444 val intent = Intent (this , IPNService ::class .java).apply { action = IPNService .ACTION_STOP_VPN }
416445 try {
@@ -421,6 +450,7 @@ open class UninitializedApp : Application() {
421450 TSLog .e(TAG , " stopVPN hit exception in startService(): $e " )
422451 }
423452 }
453+
424454 fun restartVPN () {
425455 val intent =
426456 Intent (this , IPNService ::class .java).apply { action = IPNService .ACTION_RESTART_VPN }
@@ -432,19 +462,22 @@ open class UninitializedApp : Application() {
432462 TSLog .e(TAG , " restartVPN hit exception in startService(): $e " )
433463 }
434464 }
465+
435466 fun createNotificationChannel (id : String , name : String , description : String , importance : Int ) {
436467 val channel = NotificationChannel (id, name, importance)
437468 channel.description = description
438469 notificationManager = NotificationManagerCompat .from(this )
439470 notificationManager.createNotificationChannel(channel)
440471 }
472+
441473 fun notifyStatus (
442474 vpnRunning : Boolean ,
443475 hideDisconnectAction : Boolean ,
444476 exitNodeName : String? = null
445477 ) {
446478 notifyStatus(buildStatusNotification(vpnRunning, hideDisconnectAction, exitNodeName))
447479 }
480+
448481 fun notifyStatus (notification : Notification ) {
449482 if (ActivityCompat .checkSelfPermission(this , Manifest .permission.POST_NOTIFICATIONS ) !=
450483 PackageManager .PERMISSION_GRANTED ) {
@@ -459,6 +492,7 @@ open class UninitializedApp : Application() {
459492 }
460493 notificationManager.notify(STATUS_NOTIFICATION_ID , notification)
461494 }
495+
462496 fun buildStatusNotification (
463497 vpnRunning : Boolean ,
464498 hideDisconnectAction : Boolean ,
@@ -504,6 +538,7 @@ open class UninitializedApp : Application() {
504538 }
505539 return builder.build()
506540 }
541+
507542 fun updateUserDisallowedPackageNames (packageNames : List <String >) {
508543 if (packageNames.any { it.isEmpty() }) {
509544 TSLog .e(TAG , " updateUserDisallowedPackageNames called with empty packageName(s)" )
@@ -512,6 +547,7 @@ open class UninitializedApp : Application() {
512547 getUnencryptedPrefs().edit().putStringSet(DISALLOWED_APPS_KEY , packageNames.toSet()).apply ()
513548 this .restartVPN()
514549 }
550+
515551 fun disallowedPackageNames (): List <String > {
516552 val mdmDisallowed =
517553 MDMSettings .excludedPackages.flow.value.value?.split(" ," )?.map { it.trim() } ? : emptyList()
@@ -553,4 +589,4 @@ open class UninitializedApp : Application() {
553589 // Android Connectivity Service https://github.com/tailscale/tailscale/issues/14128
554590 " com.google.android.apps.scone" ,
555591 )
556- }
592+ }
0 commit comments