android/ui: cleanup unused code

Updates tailscale/corp#18202

Removes unused code from various legacy ported classes.  Fixes a number of soft warnings.

Removes/renames a few composables we were no longer using.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
jonathan/cleanup
Jonathan Nobels 7 months ago committed by Jonathan Nobels
parent b4c0a6931d
commit 59ddead48e

@ -1,39 +1,25 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn package com.tailscale.ipn
import android.Manifest
import android.app.Activity import android.app.Activity
import android.app.Application import android.app.Application
import android.app.DownloadManager
import android.app.Fragment
import android.app.FragmentTransaction
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.PendingIntent
import android.app.UiModeManager
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.LinkProperties import android.net.LinkProperties
import android.net.Network import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import android.net.Uri
import android.net.VpnService import android.net.VpnService
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import androidx.browser.customtabs.CustomTabsIntent import androidx.core.app.ActivityCompat.startActivityForResult
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey import androidx.security.crypto.MasterKey
@ -62,22 +48,17 @@ class App : Application(), libtailscale.AppContext {
const val STATUS_NOTIFICATION_ID = 1 const val STATUS_NOTIFICATION_ID = 1
const val NOTIFY_CHANNEL_ID = "tailscale-notify" const val NOTIFY_CHANNEL_ID = "tailscale-notify"
const val NOTIFY_NOTIFICATION_ID = 2 const val NOTIFY_NOTIFICATION_ID = 2
private const val PEER_TAG = "peer"
private const val FILE_CHANNEL_ID = "tailscale-files" private const val FILE_CHANNEL_ID = "tailscale-files"
private const val FILE_NOTIFICATION_ID = 3 private const val FILE_NOTIFICATION_ID = 3
private const val TAG = "App" private const val TAG = "App"
private val networkConnectivityRequest = private val networkConnectivityRequest =
NetworkRequest.Builder() NetworkRequest.Builder()
.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
@JvmStatic lateinit var appInstance: App
fun startActivityForResult(act: Activity, intent: Intent?, request: Int) {
val f: Fragment = act.fragmentManager.findFragmentByTag(PEER_TAG)
f.startActivityForResult(intent, request)
}
@JvmStatic @JvmStatic
fun getApplication(): App { fun getApplication(): App {
@ -88,6 +69,7 @@ class App : Application(), libtailscale.AppContext {
val dns = DnsConfig() val dns = DnsConfig()
var autoConnect = false var autoConnect = false
var vpnReady = false var vpnReady = false
private lateinit var connectivityManager: ConnectivityManager private lateinit var connectivityManager: ConnectivityManager
private lateinit var app: libtailscale.Application private lateinit var app: libtailscale.Application
@ -107,12 +89,13 @@ class App : Application(), libtailscale.AppContext {
// to the given folder. We will preferentially use <shared>/Downloads and fallback to // to the given folder. We will preferentially use <shared>/Downloads and fallback to
// 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().absolutePath
app = Libtailscale.start(dataDir, directFileDir.absolutePath, this) app = Libtailscale.start(dataDir, directFileDir, this)
Request.setApp(app) Request.setApp(app)
Notifier.setApp(app) Notifier.setApp(app)
Notifier.start(applicationScope) Notifier.start(applicationScope)
connectivityManager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager connectivityManager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
setAndRegisterNetworkCallbacks() setAndRegisterNetworkCallbacks()
createNotificationChannel( createNotificationChannel(
@ -121,6 +104,7 @@ 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 appInstance = this
applicationScope.launch { applicationScope.launch {
Notifier.tileReady.collect { isTileReady -> setTileReady(isTileReady) } Notifier.tileReady.collect { isTileReady -> setTileReady(isTileReady) }
@ -219,9 +203,6 @@ class App : Application(), libtailscale.AppContext {
} }
fun setTileReady(ready: Boolean) { fun setTileReady(ready: Boolean) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return
}
QuickToggleService.setReady(this, ready) QuickToggleService.setReady(this, ready)
Log.d("App", "Set Tile Ready: $ready $autoConnect") Log.d("App", "Set Tile Ready: $ready $autoConnect")
vpnReady = ready vpnReady = ready
@ -231,9 +212,6 @@ class App : Application(), libtailscale.AppContext {
} }
fun setTileStatus(status: Boolean) { fun setTileStatus(status: Boolean) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return
}
QuickToggleService.setStatus(this, status) QuickToggleService.setStatus(this, status)
} }
@ -265,18 +243,6 @@ class App : Application(), libtailscale.AppContext {
return null return null
} }
// attachPeer adds a Peer fragment for tracking the Activity
// lifecycle.
fun attachPeer(act: Activity) {
act.runOnUiThread(
Runnable {
val ft: FragmentTransaction = act.fragmentManager.beginTransaction()
ft.add(Peer(), PEER_TAG)
ft.commit()
act.fragmentManager.executePendingTransactions()
})
}
override fun isChromeOS(): Boolean { override fun isChromeOS(): Boolean {
return packageManager.hasSystemFeature("android.hardware.type.pc") return packageManager.hasSystemFeature("android.hardware.type.pc")
} }
@ -288,102 +254,12 @@ class App : Application(), libtailscale.AppContext {
if (intent == null) { if (intent == null) {
startVPN() startVPN()
} else { } else {
startActivityForResult(act, intent, reqCode) startActivityForResult(act, intent, reqCode, null)
} }
}) })
} }
fun showURL(act: Activity, url: String?) {
act.runOnUiThread(
Runnable {
val builder: CustomTabsIntent.Builder = CustomTabsIntent.Builder()
val headerColor = -0xb69b6b
builder.setToolbarColor(headerColor)
val intent: CustomTabsIntent = builder.build()
intent.launchUrl(act, Uri.parse(url))
})
}
@get:Throws(Exception::class)
val packageCertificate: ByteArray?
// getPackageSignatureFingerprint returns the first package signing certificate, if any.
get() {
val info: PackageInfo
info = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
for (signature in info.signatures) {
return signature.toByteArray()
}
return null
}
@Throws(IOException::class)
fun insertMedia(name: String?, mimeType: String): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val resolver: ContentResolver = contentResolver
val contentValues = ContentValues()
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name)
if ("" != mimeType) {
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
}
val root: Uri = MediaStore.Files.getContentUri("external")
resolver.insert(root, contentValues).toString()
} else {
val dir: File = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
dir.mkdirs()
val f = File(dir, name)
Uri.fromFile(f).toString()
}
}
@Throws(IOException::class)
fun openUri(uri: String?, mode: String?): Int? {
val resolver: ContentResolver = contentResolver
return mode?.let { resolver.openFileDescriptor(Uri.parse(uri), it)?.detachFd() }
}
fun deleteUri(uri: String?) {
val resolver: ContentResolver = contentResolver
resolver.delete(Uri.parse(uri), null, null)
}
fun notifyFile(uri: String?, msg: String?) {
val viewIntent: Intent
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
viewIntent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
} else {
// uri is a file:// which is not allowed to be shared outside the app.
viewIntent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
}
val pending: PendingIntent =
PendingIntent.getActivity(this, 0, viewIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val builder: NotificationCompat.Builder =
NotificationCompat.Builder(this, FILE_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("File received")
.setContentText(msg)
.setContentIntent(pending)
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
val nm: NotificationManagerCompat = NotificationManagerCompat.from(this)
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return
}
nm.notify(FILE_NOTIFICATION_ID, builder.build())
}
fun createNotificationChannel(id: String?, name: String?, importance: Int) { fun createNotificationChannel(id: String?, name: String?, importance: Int) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return
}
val channel = NotificationChannel(id, name, importance) val channel = NotificationChannel(id, name, importance)
val nm: NotificationManagerCompat = NotificationManagerCompat.from(this) val nm: NotificationManagerCompat = NotificationManagerCompat.from(this)
nm.createNotificationChannel(channel) nm.createNotificationChannel(channel)
@ -424,12 +300,7 @@ class App : Application(), libtailscale.AppContext {
return sb.toString() return sb.toString()
} }
fun isTV(): Boolean { private fun prepareDownloadsFolder(): File {
val mm = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
return mm.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION
}
fun prepareDownloadsFolder(): File {
var downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) var downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
try { try {

@ -0,0 +1,23 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn
// The package identifiers for applications which are known to break when Tailscale is enabled.
object DisallowedApps {
val apps =
arrayOf(
// RCS/Jibe https://github.com/tailscale/tailscale/issues/2322
"com.google.android.apps.messaging",
// Stadia https://github.com/tailscale/tailscale/issues/3460
"com.google.stadia.android",
// Android Auto https://github.com/tailscale/tailscale/issues/3828
"com.google.android.projection.gearhead",
// GoPro https://github.com/tailscale/tailscale/issues/2554
"com.gopro.smarty",
// Sonos https://github.com/tailscale/tailscale/issues/2548
"com.sonos.acr",
"com.sonos.acr2",
// Google Chromecast https://github.com/tailscale/tailscale/issues/3636
"com.google.android.apps.chromecast.app")
}

@ -44,7 +44,7 @@ open class IPNService : VpnService(), libtailscale.IPNService {
return START_STICKY return START_STICKY
} }
override public fun close() { override fun close() {
stopForeground(true) stopForeground(true)
Libtailscale.serviceDisconnect(this) Libtailscale.serviceDisconnect(this)
val app = applicationContext as App val app = applicationContext as App
@ -72,7 +72,7 @@ open class IPNService : VpnService(), libtailscale.IPNService {
private fun disallowApp(b: Builder, name: String) { private fun disallowApp(b: Builder, name: String) {
try { try {
b.addDisallowedApplication(name) b.addDisallowedApplication(name)
} catch (e: PackageManager.NameNotFoundException) {} } catch (_: PackageManager.NameNotFoundException) {}
} }
override fun newBuilder(): VPNServiceBuilder { override fun newBuilder(): VPNServiceBuilder {
@ -83,27 +83,10 @@ open class IPNService : VpnService(), libtailscale.IPNService {
.allowFamily(OsConstants.AF_INET6) .allowFamily(OsConstants.AF_INET6)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
b.setMetered(false) // Inherit the metered status from the underlying networks. b.setMetered(false) // Inherit the metered status from the underlying networks.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) b.setUnderlyingNetworks(null) // Use all available networks.
b.setUnderlyingNetworks(null) // Use all available networks.
// RCS/Jibe https://github.com/tailscale/tailscale/issues/2322 DisallowedApps.apps.forEach { disallowApp(b, it) }
disallowApp(b, "com.google.android.apps.messaging")
// Stadia https://github.com/tailscale/tailscale/issues/3460
disallowApp(b, "com.google.stadia.android")
// Android Auto https://github.com/tailscale/tailscale/issues/3828
disallowApp(b, "com.google.android.projection.gearhead")
// GoPro https://github.com/tailscale/tailscale/issues/2554
disallowApp(b, "com.gopro.smarty")
// Sonos https://github.com/tailscale/tailscale/issues/2548
disallowApp(b, "com.sonos.acr")
disallowApp(b, "com.sonos.acr2")
// Google Chromecast https://github.com/tailscale/tailscale/issues/3636
disallowApp(b, "com.google.android.apps.chromecast.app")
return VPNServiceBuilder(b) return VPNServiceBuilder(b)
} }

@ -51,16 +51,15 @@ class ShareActivity : ComponentActivity() {
} }
// Loads the files from the intent. // Loads the files from the intent.
fun loadFiles() { private fun loadFiles() {
if (intent == null) { if (intent == null) {
Log.e(TAG, "Share failure - No intent found") Log.e(TAG, "Share failure - No intent found")
return return
} }
val act = intent.action val act = intent.action
val uris: List<Uri?>?
uris = val uris: List<Uri?>? =
when (act) { when (act) {
Intent.ACTION_SEND -> { Intent.ACTION_SEND -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {

@ -11,6 +11,7 @@ import android.content.Intent;
import android.net.VpnService; import android.net.VpnService;
import android.os.Build; import android.os.Build;
import androidx.annotation.NonNull;
import androidx.work.Worker; import androidx.work.Worker;
import androidx.work.WorkerParameters; import androidx.work.WorkerParameters;
@ -22,6 +23,7 @@ public final class StartVPNWorker extends Worker {
super(appContext, workerParams); super(appContext, workerParams);
} }
@NonNull
@Override @Override
public Result doWork() { public Result doWork() {
App app = ((App) getApplicationContext()); App app = ((App) getApplicationContext());

@ -6,26 +6,20 @@ package com.tailscale.ipn.ui.view
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tailscale.ipn.ui.theme.link import com.tailscale.ipn.ui.theme.link
// Action button that fills the full width of it's container with 12dp of vertical padding
@Composable @Composable
fun PrimaryActionButton(onClick: () -> Unit, content: @Composable RowScope.() -> Unit) { fun FullWidthButton(onClick: () -> Unit, content: @Composable RowScope.() -> Unit) {
Button( Button(
onClick = onClick, onClick = onClick,
contentPadding = PaddingValues(vertical = 12.dp), contentPadding = PaddingValues(vertical = 12.dp),
@ -33,6 +27,7 @@ fun PrimaryActionButton(onClick: () -> Unit, content: @Composable RowScope.() ->
content = content) content = content)
} }
// Hyperlink style text button
@Composable @Composable
fun OpenURLButton(title: String, url: String) { fun OpenURLButton(title: String, url: String) {
val handler = LocalUriHandler.current val handler = LocalUriHandler.current
@ -46,19 +41,3 @@ fun OpenURLButton(title: String, url: String) {
) )
} }
} }
@Composable
fun ClearButton(onClick: () -> Unit) {
IconButton(onClick = onClick, modifier = Modifier.size(24.dp)) {
Icon(Icons.Outlined.Clear, null)
}
}
@Composable
fun CloseButton() {
val focusManager = LocalFocusManager.current
IconButton(onClick = { focusManager.clearFocus() }, modifier = Modifier.size(24.dp)) {
Icon(Icons.Outlined.Close, null)
}
}

@ -10,7 +10,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.tailscale.ipn.R import com.tailscale.ipn.R
// Defines the different types of error dialogs.
// Provides a message and title for each error type.
enum class ErrorDialogType { enum class ErrorDialogType {
INVALID_CUSTOM_URL, INVALID_CUSTOM_URL,
LOGOUT_FAILED, LOGOUT_FAILED,
@ -64,6 +65,6 @@ fun ErrorDialog(
title = { Text(text = stringResource(id = title)) }, title = { Text(text = stringResource(id = title)) },
text = { Text(text = stringResource(id = message)) }, text = { Text(text = stringResource(id = message)) },
confirmButton = { confirmButton = {
PrimaryActionButton(onClick = onDismiss) { Text(text = stringResource(id = buttonText)) } FullWidthButton(onClick = onDismiss) { Text(text = stringResource(id = buttonText)) }
}) })
} }

@ -22,7 +22,6 @@ import com.tailscale.ipn.mdm.MDMSettings
import com.tailscale.ipn.ui.util.itemsWithDividers import com.tailscale.ipn.ui.util.itemsWithDividers
import com.tailscale.ipn.ui.viewModel.IpnViewModel import com.tailscale.ipn.ui.viewModel.IpnViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MDMSettingsDebugView(nav: BackNavigation, model: IpnViewModel = viewModel()) { fun MDMSettingsDebugView(nav: BackNavigation, model: IpnViewModel = viewModel()) {
Scaffold(topBar = { Header(R.string.current_mdm_settings, onBack = nav.onBack) }) { innerPadding Scaffold(topBar = { Header(R.string.current_mdm_settings, onBack = nav.onBack) }) { innerPadding

@ -28,7 +28,6 @@ import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
@ -276,7 +275,7 @@ fun ConnectView(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
Spacer(modifier = Modifier.size(1.dp)) Spacer(modifier = Modifier.size(1.dp))
PrimaryActionButton(onClick = connectAction) { FullWidthButton(onClick = connectAction) {
Text( Text(
text = stringResource(id = R.string.connect), text = stringResource(id = R.string.connect),
fontSize = MaterialTheme.typography.titleMedium.fontSize) fontSize = MaterialTheme.typography.titleMedium.fontSize)
@ -293,7 +292,7 @@ fun ConnectView(
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
textAlign = TextAlign.Center) textAlign = TextAlign.Center)
Spacer(modifier = Modifier.size(1.dp)) Spacer(modifier = Modifier.size(1.dp))
PrimaryActionButton(onClick = loginAction) { FullWidthButton(onClick = loginAction) {
Text( Text(
text = stringResource(id = R.string.log_in), text = stringResource(id = R.string.log_in),
fontSize = MaterialTheme.typography.titleMedium.fontSize) fontSize = MaterialTheme.typography.titleMedium.fontSize)
@ -304,7 +303,7 @@ fun ConnectView(
} }
} }
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun PeerList( fun PeerList(
viewModel: MainViewModel, viewModel: MainViewModel,

@ -22,7 +22,6 @@ import com.tailscale.ipn.ui.viewModel.ExitNodePickerNav
import com.tailscale.ipn.ui.viewModel.ExitNodePickerViewModel import com.tailscale.ipn.ui.viewModel.ExitNodePickerViewModel
import com.tailscale.ipn.ui.viewModel.ExitNodePickerViewModelFactory import com.tailscale.ipn.ui.viewModel.ExitNodePickerViewModelFactory
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MullvadExitNodePicker( fun MullvadExitNodePicker(
countryCode: String, countryCode: String,

@ -40,7 +40,6 @@ import com.tailscale.ipn.ui.util.itemsWithDividers
import com.tailscale.ipn.ui.viewModel.PeerDetailsViewModel import com.tailscale.ipn.ui.viewModel.PeerDetailsViewModel
import com.tailscale.ipn.ui.viewModel.PeerDetailsViewModelFactory import com.tailscale.ipn.ui.viewModel.PeerDetailsViewModelFactory
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun PeerDetails( fun PeerDetails(
nav: BackNavigation, nav: BackNavigation,

@ -23,7 +23,6 @@ import com.tailscale.ipn.ui.model.Permissions
import com.tailscale.ipn.ui.theme.success import com.tailscale.ipn.ui.theme.success
import com.tailscale.ipn.ui.util.itemsWithDividers import com.tailscale.ipn.ui.util.itemsWithDividers
@OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
fun PermissionsView(nav: BackNavigation, openApplicationSettings: () -> Unit) { fun PermissionsView(nav: BackNavigation, openApplicationSettings: () -> Unit) {
val permissions = Permissions.withGrantedStatus val permissions = Permissions.withGrantedStatus

@ -119,7 +119,7 @@ fun FileShareConnectView(onToggle: () -> Unit) {
stringResource(R.string.connect_to_your_tailnet_to_share_files), stringResource(R.string.connect_to_your_tailnet_to_share_files),
style = MaterialTheme.typography.titleMedium) style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.size(1.dp)) Spacer(modifier = Modifier.size(1.dp))
PrimaryActionButton(onClick = onToggle) { FullWidthButton(onClick = onToggle) {
Text( Text(
text = stringResource(id = R.string.connect), text = stringResource(id = R.string.connect),
fontSize = MaterialTheme.typography.titleMedium.fontSize) fontSize = MaterialTheme.typography.titleMedium.fontSize)

@ -20,14 +20,14 @@ import kotlinx.coroutines.flow.StateFlow
import kotlin.concurrent.timer import kotlin.concurrent.timer
// DotsMatrix represents the state of the progress indicator. // DotsMatrix represents the state of the progress indicator.
typealias DotsMatrix = List<List<Boolean>> typealias DotsMatrix = Array<Array<Boolean>>
// The initial DotsMatrix that represents the Tailscale logo (T-shaped). // The initial DotsMatrix that represents the Tailscale logo (T-shaped).
val logoDotsMatrix: DotsMatrix = val logoDotsMatrix: DotsMatrix =
listOf( arrayOf(
listOf(false, false, false), arrayOf(false, false, false),
listOf(true, true, true), arrayOf(true, true, true),
listOf(false, true, false), arrayOf(false, true, false),
) )
@Composable @Composable
@ -81,56 +81,56 @@ fun TailscaleLogoView(animated: Boolean = false, modifier: Modifier) {
} }
} }
val gameOfLife: List<DotsMatrix> = val gameOfLife: Array<DotsMatrix> =
listOf( arrayOf(
listOf( arrayOf(
listOf(false, true, true), arrayOf(false, true, true),
listOf(true, false, true), arrayOf(true, false, true),
listOf(false, false, true), arrayOf(false, false, true),
), ),
listOf( arrayOf(
listOf(false, true, true), arrayOf(false, true, true),
listOf(false, false, true), arrayOf(false, false, true),
listOf(false, true, false), arrayOf(false, true, false),
), ),
listOf( arrayOf(
listOf(false, true, true), arrayOf(false, true, true),
listOf(false, false, false), arrayOf(false, false, false),
listOf(false, false, true), arrayOf(false, false, true),
), ),
listOf( arrayOf(
listOf(false, false, true), arrayOf(false, false, true),
listOf(false, true, false), arrayOf(false, true, false),
listOf(false, false, false), arrayOf(false, false, false),
), ),
listOf( arrayOf(
listOf(false, true, false), arrayOf(false, true, false),
listOf(false, false, false), arrayOf(false, false, false),
listOf(false, false, false), arrayOf(false, false, false),
), ),
listOf( arrayOf(
listOf(false, false, false), arrayOf(false, false, false),
listOf(false, false, true), arrayOf(false, false, true),
listOf(false, false, false), arrayOf(false, false, false),
), ),
listOf( arrayOf(
listOf(false, false, false), arrayOf(false, false, true),
listOf(false, false, false), arrayOf(false, false, false),
listOf(false, false, false), arrayOf(false, false, false),
), ),
listOf( arrayOf(
listOf(false, false, true), arrayOf(false, false, false),
listOf(false, false, false), arrayOf(false, false, false),
listOf(false, false, false), arrayOf(true, false, false),
), ),
listOf( arrayOf(
listOf(false, false, false), arrayOf(false, false, false), arrayOf(false, false, false), arrayOf(true, true, false)),
listOf(false, false, false), arrayOf(
listOf(true, false, false), arrayOf(false, false, false), arrayOf(true, false, false), arrayOf(true, true, false)),
), arrayOf(
listOf(listOf(false, false, false), listOf(false, false, false), listOf(true, true, false)), arrayOf(false, false, false), arrayOf(true, true, false), arrayOf(false, true, false)),
listOf(listOf(false, false, false), listOf(true, false, false), listOf(true, true, false)), arrayOf(
listOf(listOf(false, false, false), listOf(true, true, false), listOf(false, true, false)), arrayOf(false, false, false), arrayOf(true, true, false), arrayOf(false, true, true)),
listOf(listOf(false, false, false), listOf(true, true, false), listOf(false, true, true)), arrayOf(
listOf(listOf(false, false, false), listOf(true, true, true), listOf(false, false, true)), arrayOf(false, false, false), arrayOf(true, true, true), arrayOf(false, false, true)),
listOf(listOf(false, true, false), listOf(true, true, true), listOf(true, false, true))) arrayOf(arrayOf(false, true, false), arrayOf(true, true, true), arrayOf(true, false, true)))

@ -77,11 +77,8 @@ fun UserSwitcherView(
} }
// When switch is invoked, this stores the ID of the user we're trying to switch to // When switch is invoked, this stores the ID of the user we're trying to switch to
// so we can decorate it with a spinner. The actual logged in user will not change // so we can decorate it with a spinner. The actual logged in user will get updated
// until // as soon as we switch states.
// we get our first netmap update back with the new userId for SelfNode.
// (jonathan) TODO: This user switch is not immediate. We may need to represent the
// "switching users" state globally (if ipnState is insufficient)
val nextUserId = remember { mutableStateOf<String?>(null) } val nextUserId = remember { mutableStateOf<String?>(null) }
LazyColumn { LazyColumn {
@ -165,7 +162,7 @@ fun FusMenu(viewModel: UserSwitcherViewModel) {
Spacer(modifier = Modifier.padding(8.dp)) Spacer(modifier = Modifier.padding(8.dp))
PrimaryActionButton(onClick = { viewModel.setControlURL(url) }) { FullWidthButton(onClick = { viewModel.setControlURL(url) }) {
Text(stringResource(id = R.string.add_account_short)) Text(stringResource(id = R.string.add_account_short))
} }
} }

@ -3,9 +3,6 @@
package com.tailscale.ipn.ui.viewModel package com.tailscale.ipn.ui.viewModel
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -60,13 +57,6 @@ open class IpnViewModel : ViewModel() {
Log.d(TAG, "Created") Log.d(TAG, "Created")
} }
protected fun Context.findActivity(): Activity? =
when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}
private fun loadUserProfiles() { private fun loadUserProfiles() {
Client(viewModelScope).profiles { result -> Client(viewModelScope).profiles { result ->
result.onSuccess(loginProfiles::set).onFailure { result.onSuccess(loginProfiles::set).onFailure {
@ -136,59 +126,4 @@ open class IpnViewModel : ViewModel() {
completionHandler(it) completionHandler(it)
} }
} }
fun deleteProfile(profile: IpnLocal.LoginProfile, completionHandler: (Result<String>) -> Unit) {
Client(viewModelScope).deleteProfile(profile) {
viewModelScope.launch { loadUserProfiles() }
completionHandler(it)
}
}
// The below handle all types of preference modifications typically invoked by the UI.
// Callers generally shouldn't care about the returned prefs value - the source of
// truth is the IPNModel, who's prefs flow will change in value to reflect the true
// value of the pref setting in the back end (and will match the value returned here).
// Generally, you will want to inspect the returned value in the callback for errors
// to indicate why a particular setting did not change in the interface.
//
// Usage:
// - User/Interface changed to new value. Render the new value.
// - Submit the new value to the PrefsEditor
// - Observe the prefs on the IpnModel and update the UI when/if the value changes.
// For a typical flow, the changed value should reflect the value already shown.
// - Inform the user of any error which may have occurred
//
// The "toggle' functions here will attempt to set the pref value to the inverse of
// what is currently known in the IpnModel.prefs. If IpnModel.prefs is not available,
// the callback will be called with a NO_PREFS error
fun setWantRunning(wantRunning: Boolean, callback: (Result<Ipn.Prefs>) -> Unit) {
Ipn.MaskedPrefs().WantRunning = wantRunning
Client(viewModelScope).editPrefs(Ipn.MaskedPrefs(), callback)
}
fun toggleShieldsUp(callback: (Result<Ipn.Prefs>) -> Unit) {
val prefs =
Notifier.prefs.value
?: run {
callback(Result.failure(Exception("no prefs")))
return@toggleShieldsUp
}
val prefsOut = Ipn.MaskedPrefs()
prefsOut.ShieldsUp = !prefs.ShieldsUp
Client(viewModelScope).editPrefs(prefsOut, callback)
}
fun toggleRouteAll(callback: (Result<Ipn.Prefs>) -> Unit) {
val prefs =
Notifier.prefs.value
?: run {
callback(Result.failure(Exception("no prefs")))
return@toggleRouteAll
}
val prefsOut = Ipn.MaskedPrefs()
prefsOut.RouteAll = !prefs.RouteAll
Client(viewModelScope).editPrefs(prefsOut, callback)
}
} }

Loading…
Cancel
Save