From 26d662161b206c0250d1c741987b670b03cc1d31 Mon Sep 17 00:00:00 2001 From: kari-ts Date: Mon, 23 Feb 2026 16:45:55 -0800 Subject: [PATCH] android: ktfmt in CI This also ktfmt's Updates #cleanup Signed-off-by: kari-ts --- .github/workflows/android.yml | 3 + .../main/java/com/tailscale/ipn/IPNService.kt | 2 +- .../com/tailscale/ipn/ui/model/TailCfg.kt | 2 +- .../ipn/ui/notifier/HealthNotifierUtil.kt | 50 ++++--- .../com/tailscale/ipn/ui/view/SharedViews.kt | 7 +- .../ipn/ui/view/SplitTunnelAppPickerView.kt | 28 ++-- .../ipn/ui/viewModel/MainViewModel.kt | 2 +- .../SplitTunnelAppPickerViewModel.kt | 3 +- .../tailscale/ipn/util/HardwareKeyStore.kt | 135 +++++++++--------- 9 files changed, 109 insertions(+), 123 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 7602419e6b..190098de14 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -30,6 +30,9 @@ jobs: - name: Clean run: make clean + - name: Format check (ktfmt) + run: make fmt-check + - name: Build APKs run: make tailscale-debug.apk diff --git a/android/src/main/java/com/tailscale/ipn/IPNService.kt b/android/src/main/java/com/tailscale/ipn/IPNService.kt index 9b547e9ea1..75c11ad26f 100644 --- a/android/src/main/java/com/tailscale/ipn/IPNService.kt +++ b/android/src/main/java/com/tailscale/ipn/IPNService.kt @@ -78,7 +78,7 @@ open class IPNService : VpnService(), libtailscale.IPNService { // This means that we were restarted after the service was killed // (potentially due to OOM). if (UninitializedApp.get().isAbleToStartVPN()) { - showForegroundNotification() + showForegroundNotification() App.get() Libtailscale.requestVPN(this) START_STICKY diff --git a/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt b/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt index 0b353d87b2..c423b5f47b 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt @@ -15,9 +15,9 @@ import com.tailscale.ipn.ui.util.DisplayAddress import com.tailscale.ipn.ui.util.TimeUtil import com.tailscale.ipn.ui.util.flag import com.tailscale.ipn.ui.viewModel.PeerSettingInfo +import java.util.Date import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement -import java.util.Date class Tailcfg { @Serializable diff --git a/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifierUtil.kt b/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifierUtil.kt index 39dfb4679c..59f79ac9c8 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifierUtil.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/notifier/HealthNotifierUtil.kt @@ -8,39 +8,37 @@ import com.tailscale.ipn.ui.util.set import kotlinx.coroutines.flow.MutableStateFlow /** - * Injects a fake Health.State for testing purposes. - * This bypasses the normal IPN bus notification flow. + * Injects a fake Health.State for testing purposes. This bypasses the normal IPN bus notification + * flow. */ fun Notifier.injectFakeHealthState( includeHighSeverity: Boolean = true, includeConnectivityImpact: Boolean = false, customWarnings: List = emptyList() -) { +) { val warnings = mutableMapOf() - + if (includeHighSeverity) { - warnings["test-high-severity"] = Health.UnhealthyState( - WarnableCode = "test-high-severity", - Severity = Health.Severity.high, - Title = "Test High Severity Warning", - Text = "This is a test warning with high severity", - ImpactsConnectivity = includeConnectivityImpact, - DependsOn = null - ) - } - - warnings["test-low-severity"] = Health.UnhealthyState( - WarnableCode = "test-low-severity", - Severity = Health.Severity.low, - Title = "Test Low Severity Warning", - Text = "This is a test warning with low severity", - ImpactsConnectivity = false, - DependsOn = null - ) - - customWarnings.forEach { warning -> - warnings[warning.WarnableCode] = warning + warnings["test-high-severity"] = + Health.UnhealthyState( + WarnableCode = "test-high-severity", + Severity = Health.Severity.high, + Title = "Test High Severity Warning", + Text = "This is a test warning with high severity", + ImpactsConnectivity = includeConnectivityImpact, + DependsOn = null) } - + + warnings["test-low-severity"] = + Health.UnhealthyState( + WarnableCode = "test-low-severity", + Severity = Health.Severity.low, + Title = "Test Low Severity Warning", + Text = "This is a test warning with low severity", + ImpactsConnectivity = false, + DependsOn = null) + + customWarnings.forEach { warning -> warnings[warning.WarnableCode] = warning } + (health as MutableStateFlow).set(Health.State(Warnings = warnings)) } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt b/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt index a612ef2572..cfd407cb23 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt @@ -128,13 +128,12 @@ fun SimpleActivityIndicator(size: Int = 32) { fun ActivityIndicator(progress: Double, size: Int = 32) { // LinearProgressIndicator defaults to a 4.dp height in Material3. val height = 4.dp - + LinearProgressIndicator( - progress = {progress.toFloat()}, + progress = { progress.toFloat() }, modifier = Modifier.width(size.dp), color = ts_color_light_blue, trackColor = MaterialTheme.colorScheme.secondary, gapSize = -height, - drawStopIndicator = {} - ) + drawStopIndicator = {}) } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/SplitTunnelAppPickerView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/SplitTunnelAppPickerView.kt index 82e8046275..5994532ed0 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/SplitTunnelAppPickerView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/SplitTunnelAppPickerView.kt @@ -90,16 +90,14 @@ fun SplitTunnelAppPickerView( ListItem( headlineContent = { Text(stringResource(R.string.certain_apps_are_not_routed_via_tailscale)) - } - ) + }) } } else if (mdmIncludedPackages.value?.isNotEmpty() == true) { item("mdmIncludedNotice") { ListItem( headlineContent = { Text(stringResource(R.string.only_specific_apps_are_routed_via_tailscale)) - } - ) + }) } } else { item("header") { @@ -110,19 +108,15 @@ fun SplitTunnelAppPickerView( if (allowSelected) R.string.selected_apps_will_access_tailscale else R.string - .selected_apps_will_access_the_internet_directly_without_using_tailscale - ) - ) - } - ) + .selected_apps_will_access_the_internet_directly_without_using_tailscale)) + }) } item("resolversHeader") { Lists.SectionDivider( stringResource( if (allowSelected) R.string.count_included_apps else R.string.count_excluded_apps, selectedPackageNames.count(), - ) - ) + )) } if (installedApps.isEmpty()) { item("spinner") { @@ -200,8 +194,7 @@ fun FusMenu(viewModel: SplitTunnelAppPickerViewModel, onSwitchClick: (() -> Unit text = stringResource( if (allowSelected) R.string.switch_to_select_to_exclude - else R.string.switch_to_select_to_include - ), + else R.string.switch_to_select_to_include), ) } } @@ -211,14 +204,12 @@ fun SwitchAlertDialog(allowSelected: Boolean, onConfirm: (() -> Unit), onDismiss val switchString = stringResource( if (allowSelected) R.string.switch_to_select_to_exclude - else R.string.switch_to_select_to_include - ) + else R.string.switch_to_select_to_include) val switchDescription = stringResource( if (allowSelected) R.string.selected_apps_will_access_the_internet_directly_without_using_tailscale - else R.string.selected_apps_will_access_tailscale - ) + else R.string.selected_apps_will_access_tailscale) AlertDialog( title = { Text(text = "$switchString?") }, @@ -226,8 +217,7 @@ fun SwitchAlertDialog(allowSelected: Boolean, onConfirm: (() -> Unit), onDismiss Text( text = stringResource(R.string.your_current_selection_will_be_cleared) + - "\n$switchDescription" - ) + "\n$switchDescription") }, onDismissRequest = onDismiss, confirmButton = { TextButton(onClick = onConfirm) { Text(text = switchString) } }, diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt index c642e8fd6f..04bf0e4b9b 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt @@ -26,6 +26,7 @@ import com.tailscale.ipn.ui.util.PeerSet import com.tailscale.ipn.ui.util.TimeUtil import com.tailscale.ipn.ui.util.set import com.tailscale.ipn.util.TSLog +import java.time.Duration import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job @@ -34,7 +35,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.launch -import java.time.Duration class MainViewModelFactory(private val appViewModel: AppViewModel) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/SplitTunnelAppPickerViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/SplitTunnelAppPickerViewModel.kt index b283d683fc..8b0522a3b4 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/SplitTunnelAppPickerViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/SplitTunnelAppPickerViewModel.kt @@ -60,8 +60,7 @@ class SplitTunnelAppPickerViewModel : ViewModel() { } } .intersect(installedApps.value.map { it.packageName }.toSet()) - .toList() - ) + .toList()) } fun performSelectionSwitch() { diff --git a/android/src/main/java/com/tailscale/ipn/util/HardwareKeyStore.kt b/android/src/main/java/com/tailscale/ipn/util/HardwareKeyStore.kt index e4392863bb..0dfb9be5d9 100644 --- a/android/src/main/java/com/tailscale/ipn/util/HardwareKeyStore.kt +++ b/android/src/main/java/com/tailscale/ipn/util/HardwareKeyStore.kt @@ -12,89 +12,86 @@ import java.security.Signature import kotlin.random.Random class NoSuchKeyException : Exception("no key found matching the provided ID") + class HardwareKeysNotSupported : Exception("hardware-backed keys are not supported on this device") // HardwareKeyStore implements the callbacks necessary to implement key.HardwareAttestationKey on // the Go side. It uses KeyStore with a StrongBox processor. class HardwareKeyStore() { - // keyStoreKeys should be a singleton. Even if multiple HardwareKeyStores are created, we should - // not create distinct underlying key maps. - companion object { - val keyStoreKeys: HashMap by lazy { - HashMap() - } - } - val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { - load(null) - } + // keyStoreKeys should be a singleton. Even if multiple HardwareKeyStores are created, we should + // not create distinct underlying key maps. + companion object { + val keyStoreKeys: HashMap by lazy { HashMap() } + } + + val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } - @OptIn(ExperimentalStdlibApi::class) - fun newID(): String { - var id: String - do { - id = Random.nextBytes(4).toHexString() - } while (keyStoreKeys.containsKey(id)) - return id + @OptIn(ExperimentalStdlibApi::class) + fun newID(): String { + var id: String + do { + id = Random.nextBytes(4).toHexString() + } while (keyStoreKeys.containsKey(id)) + return id + } + + fun createKey(): String { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + throw HardwareKeysNotSupported() } + val id = newID() + val kpg: KeyPairGenerator = + KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore") + val parameterSpec: KeyGenParameterSpec = + KeyGenParameterSpec.Builder(id, KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY) + .run { + // Use DIGEST_NONE because hashing is done on the Go side. + setDigests(KeyProperties.DIGEST_NONE) + setIsStrongBoxBacked(true) + build() + } - fun createKey(): String { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { - throw HardwareKeysNotSupported() - } - val id = newID() - val kpg: KeyPairGenerator = KeyPairGenerator.getInstance( - KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore" - ) - val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder( - id, KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY - ).run { - // Use DIGEST_NONE because hashing is done on the Go side. - setDigests(KeyProperties.DIGEST_NONE) - setIsStrongBoxBacked(true) - build() - } + kpg.initialize(parameterSpec) - kpg.initialize(parameterSpec) + val kp = kpg.generateKeyPair() + keyStoreKeys[id] = kp + return id + } - val kp = kpg.generateKeyPair() - keyStoreKeys[id] = kp - return id - } + fun releaseKey(id: String) { + keyStoreKeys.remove(id) + } - fun releaseKey(id: String) { - keyStoreKeys.remove(id) + fun sign(id: String, data: ByteArray): ByteArray { + val key = keyStoreKeys[id] + if (key == null) { + throw NoSuchKeyException() } - - fun sign(id: String, data: ByteArray): ByteArray { - val key = keyStoreKeys[id] - if (key == null) { - throw NoSuchKeyException() - } - // Use NONEwithECDSA because hashing is done on the Go side. - return Signature.getInstance("NONEwithECDSA").run { - initSign(key.private) - update(data) - sign() - } + // Use NONEwithECDSA because hashing is done on the Go side. + return Signature.getInstance("NONEwithECDSA").run { + initSign(key.private) + update(data) + sign() } + } - fun public(id: String): ByteArray { - val key = keyStoreKeys[id] - if (key == null) { - throw NoSuchKeyException() - } - return key.public.encoded + fun public(id: String): ByteArray { + val key = keyStoreKeys[id] + if (key == null) { + throw NoSuchKeyException() } + return key.public.encoded + } - fun load(id: String) { - if (keyStoreKeys[id] != null) { - // Already loaded. - return - } - val entry: KeyStore.Entry = keyStore.getEntry(id, null) - if (entry !is KeyStore.PrivateKeyEntry) { - throw NoSuchKeyException() - } - keyStoreKeys[id] = KeyPair(entry.certificate.publicKey, entry.privateKey) + fun load(id: String) { + if (keyStoreKeys[id] != null) { + // Already loaded. + return + } + val entry: KeyStore.Entry = keyStore.getEntry(id, null) + if (entry !is KeyStore.PrivateKeyEntry) { + throw NoSuchKeyException() } -} \ No newline at end of file + keyStoreKeys[id] = KeyPair(entry.certificate.publicKey, entry.privateKey) + } +}