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_NOT_VPN)
.build()
lateinit var appInstance: App
private lateinit var appInstance: App
@JvmStatic
fun startActivityForResult(act: Activity, intent: Intent?, request: Int) {
@ -78,8 +78,13 @@ class App : Application(), libtailscale.AppContext {
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
fun getApplication(): App {
appInstance.initOnce()
return appInstance
}
}
@ -98,6 +103,24 @@ class App : Application(), libtailscale.AppContext {
override fun 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
// 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
// user notifications for incoming files.
val directFileDir = this.prepareDownloadsFolder()
app = Libtailscale.start(dataDir, directFileDir.absolutePath, this)
Request.setApp(app)
Notifier.setApp(app)
@ -116,18 +138,11 @@ class App : Application(), libtailscale.AppContext {
STATUS_CHANNEL_ID, "VPN Status", NotificationManagerCompat.IMPORTANCE_LOW)
createNotificationChannel(
FILE_CHANNEL_ID, "File transfers", NotificationManagerCompat.IMPORTANCE_DEFAULT)
appInstance = this
applicationScope.launch {
Notifier.connStatus.collect { connStatus -> updateConnStatus(connStatus) }
}
}
override fun onTerminate() {
super.onTerminate()
Notifier.stop()
applicationScope.cancel()
}
fun setWantRunning(wantRunning: Boolean) {
val callback: (Result<Ipn.Prefs>) -> Unit = { result ->
result.fold(
@ -181,7 +196,6 @@ class App : Application(), libtailscale.AppContext {
startService(intent)
}
// encryptToPref a byte array of data using the Jetpack Security
// library and writes it to a global encrypted preference store.
@Throws(IOException::class, GeneralSecurityException::class)
@ -215,26 +229,17 @@ class App : Application(), libtailscale.AppContext {
QuickToggleService.setReady(this, ready)
Log.d("App", "Set Tile Ready: $ready")
val action = if (ready) IPNReceiver.INTENT_DISCONNECT_VPN else IPNReceiver.INTENT_CONNECT_VPN
val intent = Intent(this, IPNReceiver::class.java).apply {
this.action = action
}
val pendingIntent : PendingIntent = PendingIntent.getBroadcast(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val intent = Intent(this, IPNReceiver::class.java).apply { this.action = action }
val pendingIntent: PendingIntent =
PendingIntent.getBroadcast(
this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
if (ready) {
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(
"Tailscale",
notificationMessage,
STATUS_CHANNEL_ID,
pendingIntent,
STATUS_NOTIFICATION_ID
)
"Tailscale", notificationMessage, STATUS_CHANNEL_ID, pendingIntent, STATUS_NOTIFICATION_ID)
}
fun getHostname(): String {
@ -337,7 +342,8 @@ class App : Application(), libtailscale.AppContext {
}
val pending: PendingIntent =
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) {
@ -346,7 +352,13 @@ class App : Application(), libtailscale.AppContext {
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 =
NotificationCompat.Builder(this, channel)
.setSmallIcon(R.drawable.ic_notification)

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

@ -11,12 +11,10 @@ import android.content.RestrictionsManager
import android.content.pm.ActivityInfo
import android.content.res.Configuration.SCREENLAYOUT_SIZE_LARGE
import android.content.res.Configuration.SCREENLAYOUT_SIZE_MASK
import android.net.Uri
import android.net.VpnService
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
@ -100,6 +98,9 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
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
// proper landscape layout support
if (!isLandscapeCapable()) {

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

Loading…
Cancel
Save