diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index ae6ec07..d1330fe 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -1,39 +1,25 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause + package com.tailscale.ipn -import android.Manifest import android.app.Activity import android.app.Application -import android.app.DownloadManager -import android.app.Fragment -import android.app.FragmentTransaction 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.Intent 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.LinkProperties import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest -import android.net.Uri import android.net.VpnService import android.os.Build import android.os.Environment -import android.provider.MediaStore import android.provider.Settings import android.util.Log -import androidx.browser.customtabs.CustomTabsIntent -import androidx.core.app.ActivityCompat -import androidx.core.app.NotificationCompat +import androidx.core.app.ActivityCompat.startActivityForResult import androidx.core.app.NotificationManagerCompat import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey @@ -62,22 +48,17 @@ class App : Application(), libtailscale.AppContext { const val STATUS_NOTIFICATION_ID = 1 const val NOTIFY_CHANNEL_ID = "tailscale-notify" const val NOTIFY_NOTIFICATION_ID = 2 - private const val PEER_TAG = "peer" private const val FILE_CHANNEL_ID = "tailscale-files" private const val FILE_NOTIFICATION_ID = 3 private const val TAG = "App" + private val networkConnectivityRequest = NetworkRequest.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) .build() - lateinit var appInstance: App - @JvmStatic - fun startActivityForResult(act: Activity, intent: Intent?, request: Int) { - val f: Fragment = act.fragmentManager.findFragmentByTag(PEER_TAG) - f.startActivityForResult(intent, request) - } + lateinit var appInstance: App @JvmStatic fun getApplication(): App { @@ -88,6 +69,7 @@ class App : Application(), libtailscale.AppContext { val dns = DnsConfig() var autoConnect = false var vpnReady = false + private lateinit var connectivityManager: ConnectivityManager private lateinit var app: libtailscale.Application @@ -107,12 +89,13 @@ class App : Application(), libtailscale.AppContext { // to the given folder. We will preferentially use /Downloads and fallback to // an app local directory "Taildrop" if we cannot create that. This mode does not support // 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) Notifier.setApp(app) Notifier.start(applicationScope) + connectivityManager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager setAndRegisterNetworkCallbacks() createNotificationChannel( @@ -121,6 +104,7 @@ 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.tileReady.collect { isTileReady -> setTileReady(isTileReady) } @@ -219,9 +203,6 @@ class App : Application(), libtailscale.AppContext { } fun setTileReady(ready: Boolean) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - return - } QuickToggleService.setReady(this, ready) Log.d("App", "Set Tile Ready: $ready $autoConnect") vpnReady = ready @@ -231,9 +212,6 @@ class App : Application(), libtailscale.AppContext { } fun setTileStatus(status: Boolean) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - return - } QuickToggleService.setStatus(this, status) } @@ -265,18 +243,6 @@ class App : Application(), libtailscale.AppContext { 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 { return packageManager.hasSystemFeature("android.hardware.type.pc") } @@ -288,102 +254,12 @@ class App : Application(), libtailscale.AppContext { if (intent == null) { startVPN() } 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) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - return - } val channel = NotificationChannel(id, name, importance) val nm: NotificationManagerCompat = NotificationManagerCompat.from(this) nm.createNotificationChannel(channel) @@ -424,12 +300,7 @@ class App : Application(), libtailscale.AppContext { return sb.toString() } - fun isTV(): Boolean { - val mm = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager - return mm.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION - } - - fun prepareDownloadsFolder(): File { + private fun prepareDownloadsFolder(): File { var downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) try { diff --git a/android/src/main/java/com/tailscale/ipn/DisallowedApps.kt b/android/src/main/java/com/tailscale/ipn/DisallowedApps.kt new file mode 100644 index 0000000..018999f --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/DisallowedApps.kt @@ -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") +} diff --git a/android/src/main/java/com/tailscale/ipn/IPNService.kt b/android/src/main/java/com/tailscale/ipn/IPNService.kt index f67b800..39d91b5 100644 --- a/android/src/main/java/com/tailscale/ipn/IPNService.kt +++ b/android/src/main/java/com/tailscale/ipn/IPNService.kt @@ -44,7 +44,7 @@ open class IPNService : VpnService(), libtailscale.IPNService { return START_STICKY } - override public fun close() { + override fun close() { stopForeground(true) Libtailscale.serviceDisconnect(this) val app = applicationContext as App @@ -72,7 +72,7 @@ open class IPNService : VpnService(), libtailscale.IPNService { private fun disallowApp(b: Builder, name: String) { try { b.addDisallowedApplication(name) - } catch (e: PackageManager.NameNotFoundException) {} + } catch (_: PackageManager.NameNotFoundException) {} } override fun newBuilder(): VPNServiceBuilder { @@ -83,27 +83,10 @@ open class IPNService : VpnService(), libtailscale.IPNService { .allowFamily(OsConstants.AF_INET6) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) 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 - disallowApp(b, "com.google.android.apps.messaging") + DisallowedApps.apps.forEach { disallowApp(b, it) } - // 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) } diff --git a/android/src/main/java/com/tailscale/ipn/ShareActivity.kt b/android/src/main/java/com/tailscale/ipn/ShareActivity.kt index 8f2cbaa..32c7d01 100644 --- a/android/src/main/java/com/tailscale/ipn/ShareActivity.kt +++ b/android/src/main/java/com/tailscale/ipn/ShareActivity.kt @@ -51,16 +51,15 @@ class ShareActivity : ComponentActivity() { } // Loads the files from the intent. - fun loadFiles() { + private fun loadFiles() { if (intent == null) { Log.e(TAG, "Share failure - No intent found") return } val act = intent.action - val uris: List? - uris = + val uris: List? = when (act) { Intent.ACTION_SEND -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { diff --git a/android/src/main/java/com/tailscale/ipn/StartVPNWorker.java b/android/src/main/java/com/tailscale/ipn/StartVPNWorker.java index 4179151..000e4cd 100644 --- a/android/src/main/java/com/tailscale/ipn/StartVPNWorker.java +++ b/android/src/main/java/com/tailscale/ipn/StartVPNWorker.java @@ -11,6 +11,7 @@ import android.content.Intent; import android.net.VpnService; import android.os.Build; +import androidx.annotation.NonNull; import androidx.work.Worker; import androidx.work.WorkerParameters; @@ -22,6 +23,7 @@ public final class StartVPNWorker extends Worker { super(appContext, workerParams); } + @NonNull @Override public Result doWork() { App app = ((App) getApplicationContext()); diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/Buttons.kt b/android/src/main/java/com/tailscale/ipn/ui/view/Buttons.kt index 1b80163..46fd123 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/Buttons.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/Buttons.kt @@ -6,26 +6,20 @@ package com.tailscale.ipn.ui.view import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope 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.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import com.tailscale.ipn.ui.theme.link +// Action button that fills the full width of it's container with 12dp of vertical padding @Composable -fun PrimaryActionButton(onClick: () -> Unit, content: @Composable RowScope.() -> Unit) { +fun FullWidthButton(onClick: () -> Unit, content: @Composable RowScope.() -> Unit) { Button( onClick = onClick, contentPadding = PaddingValues(vertical = 12.dp), @@ -33,6 +27,7 @@ fun PrimaryActionButton(onClick: () -> Unit, content: @Composable RowScope.() -> content = content) } +// Hyperlink style text button @Composable fun OpenURLButton(title: String, url: String) { 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) - } -} diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/ErrorDialog.kt b/android/src/main/java/com/tailscale/ipn/ui/view/ErrorDialog.kt index 0615edb..416d9f3 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/ErrorDialog.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/ErrorDialog.kt @@ -10,7 +10,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import com.tailscale.ipn.R - +// Defines the different types of error dialogs. +// Provides a message and title for each error type. enum class ErrorDialogType { INVALID_CUSTOM_URL, LOGOUT_FAILED, @@ -64,6 +65,6 @@ fun ErrorDialog( title = { Text(text = stringResource(id = title)) }, text = { Text(text = stringResource(id = message)) }, confirmButton = { - PrimaryActionButton(onClick = onDismiss) { Text(text = stringResource(id = buttonText)) } + FullWidthButton(onClick = onDismiss) { Text(text = stringResource(id = buttonText)) } }) } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt index c3b91c9..cb0464b 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt @@ -22,7 +22,6 @@ import com.tailscale.ipn.mdm.MDMSettings import com.tailscale.ipn.ui.util.itemsWithDividers import com.tailscale.ipn.ui.viewModel.IpnViewModel -@OptIn(ExperimentalMaterial3Api::class) @Composable fun MDMSettingsDebugView(nav: BackNavigation, model: IpnViewModel = viewModel()) { Scaffold(topBar = { Header(R.string.current_mdm_settings, onBack = nav.onBack) }) { innerPadding diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt index f289b1d..ab76046 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt @@ -28,7 +28,6 @@ import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.Button -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem @@ -276,7 +275,7 @@ fun ConnectView( textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.size(1.dp)) - PrimaryActionButton(onClick = connectAction) { + FullWidthButton(onClick = connectAction) { Text( text = stringResource(id = R.string.connect), fontSize = MaterialTheme.typography.titleMedium.fontSize) @@ -293,7 +292,7 @@ fun ConnectView( style = MaterialTheme.typography.titleSmall, textAlign = TextAlign.Center) Spacer(modifier = Modifier.size(1.dp)) - PrimaryActionButton(onClick = loginAction) { + FullWidthButton(onClick = loginAction) { Text( text = stringResource(id = R.string.log_in), fontSize = MaterialTheme.typography.titleMedium.fontSize) @@ -304,7 +303,7 @@ fun ConnectView( } } -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class) @Composable fun PeerList( viewModel: MainViewModel, diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MullvadExitNodePicker.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MullvadExitNodePicker.kt index 34e004f..95e03a2 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MullvadExitNodePicker.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MullvadExitNodePicker.kt @@ -22,7 +22,6 @@ import com.tailscale.ipn.ui.viewModel.ExitNodePickerNav import com.tailscale.ipn.ui.viewModel.ExitNodePickerViewModel import com.tailscale.ipn.ui.viewModel.ExitNodePickerViewModelFactory -@OptIn(ExperimentalMaterial3Api::class) @Composable fun MullvadExitNodePicker( countryCode: String, diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/PeerDetails.kt b/android/src/main/java/com/tailscale/ipn/ui/view/PeerDetails.kt index 22bb698..ba5e5a8 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/PeerDetails.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/PeerDetails.kt @@ -40,7 +40,6 @@ import com.tailscale.ipn.ui.util.itemsWithDividers import com.tailscale.ipn.ui.viewModel.PeerDetailsViewModel import com.tailscale.ipn.ui.viewModel.PeerDetailsViewModelFactory -@OptIn(ExperimentalMaterial3Api::class) @Composable fun PeerDetails( nav: BackNavigation, diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/PermissionsView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/PermissionsView.kt index 37e8667..d7306bb 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/PermissionsView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/PermissionsView.kt @@ -23,7 +23,6 @@ import com.tailscale.ipn.ui.model.Permissions import com.tailscale.ipn.ui.theme.success import com.tailscale.ipn.ui.util.itemsWithDividers -@OptIn(ExperimentalPermissionsApi::class) @Composable fun PermissionsView(nav: BackNavigation, openApplicationSettings: () -> Unit) { val permissions = Permissions.withGrantedStatus diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/TaildropView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/TaildropView.kt index 2c14ae1..fc9174e 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/TaildropView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/TaildropView.kt @@ -119,7 +119,7 @@ fun FileShareConnectView(onToggle: () -> Unit) { stringResource(R.string.connect_to_your_tailnet_to_share_files), style = MaterialTheme.typography.titleMedium) Spacer(modifier = Modifier.size(1.dp)) - PrimaryActionButton(onClick = onToggle) { + FullWidthButton(onClick = onToggle) { Text( text = stringResource(id = R.string.connect), fontSize = MaterialTheme.typography.titleMedium.fontSize) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/TailscaleLogoView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/TailscaleLogoView.kt index 967b8f3..df67be9 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/TailscaleLogoView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/TailscaleLogoView.kt @@ -20,14 +20,14 @@ import kotlinx.coroutines.flow.StateFlow import kotlin.concurrent.timer // DotsMatrix represents the state of the progress indicator. -typealias DotsMatrix = List> +typealias DotsMatrix = Array> // The initial DotsMatrix that represents the Tailscale logo (T-shaped). val logoDotsMatrix: DotsMatrix = - listOf( - listOf(false, false, false), - listOf(true, true, true), - listOf(false, true, false), + arrayOf( + arrayOf(false, false, false), + arrayOf(true, true, true), + arrayOf(false, true, false), ) @Composable @@ -81,56 +81,56 @@ fun TailscaleLogoView(animated: Boolean = false, modifier: Modifier) { } } -val gameOfLife: List = - listOf( - listOf( - listOf(false, true, true), - listOf(true, false, true), - listOf(false, false, true), +val gameOfLife: Array = + arrayOf( + arrayOf( + arrayOf(false, true, true), + arrayOf(true, false, true), + arrayOf(false, false, true), ), - listOf( - listOf(false, true, true), - listOf(false, false, true), - listOf(false, true, false), + arrayOf( + arrayOf(false, true, true), + arrayOf(false, false, true), + arrayOf(false, true, false), ), - listOf( - listOf(false, true, true), - listOf(false, false, false), - listOf(false, false, true), + arrayOf( + arrayOf(false, true, true), + arrayOf(false, false, false), + arrayOf(false, false, true), ), - listOf( - listOf(false, false, true), - listOf(false, true, false), - listOf(false, false, false), + arrayOf( + arrayOf(false, false, true), + arrayOf(false, true, false), + arrayOf(false, false, false), ), - listOf( - listOf(false, true, false), - listOf(false, false, false), - listOf(false, false, false), + arrayOf( + arrayOf(false, true, false), + arrayOf(false, false, false), + arrayOf(false, false, false), ), - listOf( - listOf(false, false, false), - listOf(false, false, true), - listOf(false, false, false), + arrayOf( + arrayOf(false, false, false), + arrayOf(false, false, true), + arrayOf(false, false, false), ), - listOf( - listOf(false, false, false), - listOf(false, false, false), - listOf(false, false, false), + arrayOf( + arrayOf(false, false, true), + arrayOf(false, false, false), + arrayOf(false, false, false), ), - listOf( - listOf(false, false, true), - listOf(false, false, false), - listOf(false, false, false), + arrayOf( + arrayOf(false, false, false), + arrayOf(false, false, false), + arrayOf(true, false, false), ), - listOf( - listOf(false, false, false), - listOf(false, false, false), - listOf(true, false, false), - ), - listOf(listOf(false, false, false), listOf(false, false, false), listOf(true, true, false)), - listOf(listOf(false, false, false), listOf(true, false, false), listOf(true, true, false)), - listOf(listOf(false, false, false), listOf(true, true, false), listOf(false, true, false)), - listOf(listOf(false, false, false), listOf(true, true, false), listOf(false, true, true)), - listOf(listOf(false, false, false), listOf(true, true, true), listOf(false, false, true)), - listOf(listOf(false, true, false), listOf(true, true, true), listOf(true, false, true))) + arrayOf( + arrayOf(false, false, false), arrayOf(false, false, false), arrayOf(true, true, false)), + arrayOf( + arrayOf(false, false, false), arrayOf(true, false, false), arrayOf(true, true, false)), + arrayOf( + arrayOf(false, false, false), arrayOf(true, true, false), arrayOf(false, true, false)), + arrayOf( + arrayOf(false, false, false), arrayOf(true, true, false), arrayOf(false, true, true)), + arrayOf( + arrayOf(false, false, false), arrayOf(true, true, true), arrayOf(false, false, true)), + arrayOf(arrayOf(false, true, false), arrayOf(true, true, true), arrayOf(true, false, true))) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt index 95877b3..fe8f799 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt @@ -77,11 +77,8 @@ fun UserSwitcherView( } // 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 - // until - // 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) + // so we can decorate it with a spinner. The actual logged in user will get updated + // as soon as we switch states. val nextUserId = remember { mutableStateOf(null) } LazyColumn { @@ -165,7 +162,7 @@ fun FusMenu(viewModel: UserSwitcherViewModel) { Spacer(modifier = Modifier.padding(8.dp)) - PrimaryActionButton(onClick = { viewModel.setControlURL(url) }) { + FullWidthButton(onClick = { viewModel.setControlURL(url) }) { Text(stringResource(id = R.string.add_account_short)) } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt index 9636a43..0f27848 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt @@ -3,9 +3,6 @@ package com.tailscale.ipn.ui.viewModel -import android.app.Activity -import android.content.Context -import android.content.ContextWrapper import android.content.Intent import android.util.Log import androidx.lifecycle.ViewModel @@ -60,13 +57,6 @@ open class IpnViewModel : ViewModel() { Log.d(TAG, "Created") } - protected fun Context.findActivity(): Activity? = - when (this) { - is Activity -> this - is ContextWrapper -> baseContext.findActivity() - else -> null - } - private fun loadUserProfiles() { Client(viewModelScope).profiles { result -> result.onSuccess(loginProfiles::set).onFailure { @@ -136,59 +126,4 @@ open class IpnViewModel : ViewModel() { completionHandler(it) } } - - fun deleteProfile(profile: IpnLocal.LoginProfile, completionHandler: (Result) -> 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) -> Unit) { - Ipn.MaskedPrefs().WantRunning = wantRunning - Client(viewModelScope).editPrefs(Ipn.MaskedPrefs(), callback) - } - - fun toggleShieldsUp(callback: (Result) -> 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) -> 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) - } }