android: lazily initialize App to avoid starting Tailscale when unnecessary

For example, when viewing the QuickSettings tile, there's no need
to start Tailscale's backend.

Fixes tailscale/corp#19860

Signed-off-by: Percy Wegmann <percy@tailscale.com>
percy/psychic_quicksettings
Percy Wegmann 2 years ago
parent eeda767748
commit cbb12590f2
No known key found for this signature in database
GPG Key ID: 29D8CDEB4C13D48B

@ -70,7 +70,7 @@ class App : Application(), libtailscale.AppContext {
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build() .build()
lateinit var appInstance: App private lateinit var appInstance: App
@JvmStatic @JvmStatic
fun startActivityForResult(act: Activity, intent: Intent?, request: Int) { fun startActivityForResult(act: Activity, intent: Intent?, request: Int) {
@ -78,8 +78,13 @@ class App : Application(), libtailscale.AppContext {
f.startActivityForResult(intent, request) f.startActivityForResult(intent, request)
} }
/**
* Initializes the app (if necessary) and returns the singleton app instance. Always use this
* function to obtain an App reference to make sure the app initializes.
*/
@JvmStatic @JvmStatic
fun getApplication(): App { fun getApplication(): App {
appInstance.initOnce()
return appInstance return appInstance
} }
} }
@ -98,6 +103,24 @@ class App : Application(), libtailscale.AppContext {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
appInstance = this
}
override fun onTerminate() {
super.onTerminate()
Notifier.stop()
applicationScope.cancel()
}
var initialized = false
@Synchronized
private fun initOnce() {
if (initialized) {
return
}
initialized = true
val dataDir = this.filesDir.absolutePath val dataDir = this.filesDir.absolutePath
// Set this to enable direct mode for taildrop whereby downloads will be saved directly // Set this to enable direct mode for taildrop whereby downloads will be saved directly
@ -105,7 +128,6 @@ class App : Application(), libtailscale.AppContext {
// an app local directory "Taildrop" if we cannot create that. This mode does not support // an app local directory "Taildrop" if we cannot create that. This mode does not support
// user notifications for incoming files. // user notifications for incoming files.
val directFileDir = this.prepareDownloadsFolder() val directFileDir = this.prepareDownloadsFolder()
app = Libtailscale.start(dataDir, directFileDir.absolutePath, this) app = Libtailscale.start(dataDir, directFileDir.absolutePath, this)
Request.setApp(app) Request.setApp(app)
Notifier.setApp(app) Notifier.setApp(app)
@ -116,22 +138,15 @@ class App : Application(), libtailscale.AppContext {
STATUS_CHANNEL_ID, "VPN Status", NotificationManagerCompat.IMPORTANCE_LOW) STATUS_CHANNEL_ID, "VPN Status", NotificationManagerCompat.IMPORTANCE_LOW)
createNotificationChannel( createNotificationChannel(
FILE_CHANNEL_ID, "File transfers", NotificationManagerCompat.IMPORTANCE_DEFAULT) FILE_CHANNEL_ID, "File transfers", NotificationManagerCompat.IMPORTANCE_DEFAULT)
appInstance = this
applicationScope.launch { applicationScope.launch {
Notifier.connStatus.collect { connStatus -> updateConnStatus(connStatus) } Notifier.connStatus.collect { connStatus -> updateConnStatus(connStatus) }
} }
} }
override fun onTerminate() {
super.onTerminate()
Notifier.stop()
applicationScope.cancel()
}
fun setWantRunning(wantRunning: Boolean) { fun setWantRunning(wantRunning: Boolean) {
val callback: (Result<Ipn.Prefs>) -> Unit = { result -> val callback: (Result<Ipn.Prefs>) -> Unit = { result ->
result.fold( result.fold(
onSuccess = { }, onSuccess = {},
onFailure = { error -> onFailure = { error ->
Log.d("TAG", "Set want running: failed to update preferences: ${error.message}") Log.d("TAG", "Set want running: failed to update preferences: ${error.message}")
}) })
@ -181,7 +196,6 @@ class App : Application(), libtailscale.AppContext {
startService(intent) startService(intent)
} }
// encryptToPref a byte array of data using the Jetpack Security // encryptToPref a byte array of data using the Jetpack Security
// library and writes it to a global encrypted preference store. // library and writes it to a global encrypted preference store.
@Throws(IOException::class, GeneralSecurityException::class) @Throws(IOException::class, GeneralSecurityException::class)
@ -215,26 +229,17 @@ class App : Application(), libtailscale.AppContext {
QuickToggleService.setReady(this, ready) QuickToggleService.setReady(this, ready)
Log.d("App", "Set Tile Ready: $ready") Log.d("App", "Set Tile Ready: $ready")
val action = if (ready) IPNReceiver.INTENT_DISCONNECT_VPN else IPNReceiver.INTENT_CONNECT_VPN val action = if (ready) IPNReceiver.INTENT_DISCONNECT_VPN else IPNReceiver.INTENT_CONNECT_VPN
val intent = Intent(this, IPNReceiver::class.java).apply { val intent = Intent(this, IPNReceiver::class.java).apply { this.action = action }
this.action = action val pendingIntent: PendingIntent =
} PendingIntent.getBroadcast(
val pendingIntent : PendingIntent = PendingIntent.getBroadcast( this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
this, if (ready) {
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
if (ready){
startVPN() startVPN()
} }
val notificationMessage = if (ready) getString(R.string.connected) else getString(R.string.not_connected) val notificationMessage =
if (ready) getString(R.string.connected) else getString(R.string.not_connected)
notify( notify(
"Tailscale", "Tailscale", notificationMessage, STATUS_CHANNEL_ID, pendingIntent, STATUS_NOTIFICATION_ID)
notificationMessage,
STATUS_CHANNEL_ID,
pendingIntent,
STATUS_NOTIFICATION_ID
)
} }
fun getHostname(): String { fun getHostname(): String {
@ -337,7 +342,8 @@ class App : Application(), libtailscale.AppContext {
} }
val pending: PendingIntent = val pending: PendingIntent =
PendingIntent.getActivity(this, 0, viewIntent, PendingIntent.FLAG_UPDATE_CURRENT) PendingIntent.getActivity(this, 0, viewIntent, PendingIntent.FLAG_UPDATE_CURRENT)
notify(getString(R.string.file_notification), msg, FILE_CHANNEL_ID, pending, FILE_NOTIFICATION_ID) notify(
getString(R.string.file_notification), msg, FILE_CHANNEL_ID, pending, FILE_NOTIFICATION_ID)
} }
fun createNotificationChannel(id: String?, name: String?, importance: Int) { fun createNotificationChannel(id: String?, name: String?, importance: Int) {
@ -346,7 +352,13 @@ class App : Application(), libtailscale.AppContext {
nm.createNotificationChannel(channel) nm.createNotificationChannel(channel)
} }
fun notify(title: String?, message: String?, channel: String, intent: PendingIntent?, notificationID: Int) { fun notify(
title: String?,
message: String?,
channel: String,
intent: PendingIntent?,
notificationID: Int
) {
val builder: NotificationCompat.Builder = val builder: NotificationCompat.Builder =
NotificationCompat.Builder(this, channel) NotificationCompat.Builder(this, channel)
.setSmallIcon(R.drawable.ic_notification) .setSmallIcon(R.drawable.ic_notification)

@ -8,8 +8,6 @@ import android.content.pm.PackageManager
import android.net.VpnService import android.net.VpnService
import android.os.Build import android.os.Build
import android.system.OsConstants import android.system.OsConstants
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import libtailscale.Libtailscale import libtailscale.Libtailscale
import java.util.UUID import java.util.UUID
@ -20,8 +18,13 @@ open class IPNService : VpnService(), libtailscale.IPNService {
return randomID return randomID
} }
override fun onCreate() {
super.onCreate()
// grab app to make sure it initializes
App.getApplication()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val app = applicationContext as App
if (intent != null && "android.net.VpnService" == intent.action) { if (intent != null && "android.net.VpnService" == intent.action) {
// Start VPN and connect to it due to Always-on VPN // Start VPN and connect to it due to Always-on VPN
val i = Intent(IPNReceiver.INTENT_CONNECT_VPN) val i = Intent(IPNReceiver.INTENT_CONNECT_VPN)
@ -30,15 +33,14 @@ open class IPNService : VpnService(), libtailscale.IPNService {
sendBroadcast(i) sendBroadcast(i)
} }
Libtailscale.requestVPN(this) Libtailscale.requestVPN(this)
app.setWantRunning(true) App.getApplication().setWantRunning(true)
return START_STICKY return START_STICKY
} }
override public fun close() { override public fun close() {
stopForeground(true) stopForeground(true)
Libtailscale.serviceDisconnect(this) Libtailscale.serviceDisconnect(this)
val app = applicationContext as App App.getApplication().setWantRunning(false)
app.setWantRunning(false)
} }
override fun onDestroy() { override fun onDestroy() {

@ -11,12 +11,10 @@ import android.content.RestrictionsManager
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.res.Configuration.SCREENLAYOUT_SIZE_LARGE import android.content.res.Configuration.SCREENLAYOUT_SIZE_LARGE
import android.content.res.Configuration.SCREENLAYOUT_SIZE_MASK import android.content.res.Configuration.SCREENLAYOUT_SIZE_MASK
import android.net.Uri
import android.net.VpnService import android.net.VpnService
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
@ -100,6 +98,9 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// grab app to make sure it initializes
App.getApplication()
// (jonathan) TODO: Force the app to be portrait on small screens until we have // (jonathan) TODO: Force the app to be portrait on small screens until we have
// proper landscape layout support // proper landscape layout support
if (!isLandscapeCapable()) { if (!isLandscapeCapable()) {
@ -349,9 +350,9 @@ class MainActivity : ComponentActivity() {
private fun openApplicationSettings() { private fun openApplicationSettings() {
val intent = val intent =
Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, packageName) putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
} }
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent) startActivity(intent)
} }

@ -14,8 +14,10 @@ import com.tailscale.ipn.ui.util.AndroidTVUtil.isAndroidTV
object AndroidTVUtil { object AndroidTVUtil {
fun isAndroidTV(): Boolean { fun isAndroidTV(): Boolean {
return (App.appInstance.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEVISION) || return (App.getApplication()
App.appInstance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) .packageManager
.hasSystemFeature(PackageManager.FEATURE_TELEVISION) ||
App.getApplication().packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK))
} }
} }

Loading…
Cancel
Save