diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 066ef38..57dc91d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,8 +23,8 @@ android { applicationId = "io.github.miuzarte.scrcpyforandroid" minSdk = 26 targetSdk = 36 - versionCode = 5 - versionName = "0.0.5" + versionCode = 6 + versionName = "0.1.0" externalNativeBuild { cmake { diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/MainActivity.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/MainActivity.kt index c9fda86..39cbeab 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/MainActivity.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/MainActivity.kt @@ -5,10 +5,15 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import io.github.miuzarte.scrcpyforandroid.pages.MainPage +import io.github.miuzarte.scrcpyforandroid.storage.Storage class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + // initialize settings singleton + Storage.init(applicationContext) + enableEdgeToEdge() setContent { diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/DirectAdbClient.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/DirectAdbClient.kt index 16f50c3..9f8e5fd 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/DirectAdbClient.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/DirectAdbClient.kt @@ -4,8 +4,10 @@ import android.content.Context import android.os.Build import android.util.Base64 import android.util.Log -import androidx.core.content.edit +import io.github.miuzarte.scrcpyforandroid.storage.AdbClientData import io.github.miuzarte.scrcpyforandroid.storage.AppSettings +import io.github.miuzarte.scrcpyforandroid.storage.Storage +import kotlinx.coroutines.runBlocking import java.io.BufferedInputStream import java.io.Closeable import java.io.EOFException @@ -44,7 +46,7 @@ import kotlin.concurrent.thread */ internal class DirectAdbTransport(private val context: Context) { - private val keys: Pair by lazy { loadOrCreate() } + private val keys: Pair by lazy { runBlocking { loadOrCreate() } } val privateKey: PrivateKey get() = keys.first val publicKeyX509: ByteArray get() = keys.second @@ -101,40 +103,52 @@ internal class DirectAdbTransport(private val context: Context) { } /** - * Load persisted RSA keypair from shared preferences, or generate a new one. + * Load persisted RSA keypair from DataStore, or generate a new one. * Returns (privateKey, publicX509Bytes). */ - private fun loadOrCreate(): Pair { - // TODO: migrate to data store - val prefs = context.getSharedPreferences( - "nativecore_adb_rsa", - Context.MODE_PRIVATE - ) - val privB64 = prefs.getString("priv", null) - if (privB64 != null) { + private suspend fun loadOrCreate( + forceNew: Boolean = false, + ): Pair { + val adbClientData = Storage.adbClientData + + val privB64 = adbClientData.rsaPrivateKey.get() + + if (privB64.isNotBlank() && !forceNew) { try { val kf = KeyFactory.getInstance("RSA") - val priv = - kf.generatePrivate(PKCS8EncodedKeySpec(Base64.decode(privB64, Base64.DEFAULT))) + val priv = kf.generatePrivate( + PKCS8EncodedKeySpec( + Base64.decode(privB64, Base64.DEFAULT) + ) + ) val pub = derivePublicX509(priv) - Log.i(TAG, "loadOrCreate(): loaded persisted RSA key pair, fp=${fingerprint(pub)}") + Log.i( + TAG, + "loadOrCreate(): loaded persisted RSA key pair from DataStore, " + + "fp=${fingerprint(pub)}" + ) return Pair(priv, pub) } catch (e: Exception) { - Log.w(TAG, "loadOrCreate(): failed to load persisted key, regenerating", e) + Log.w( + TAG, + "loadOrCreate(): failed to load persisted key from DataStore, regenerating", + e + ) } } val kpg = KeyPairGenerator.getInstance("RSA") kpg.initialize(2048) val kp = kpg.generateKeyPair() - prefs.edit { - putString( - "priv", - Base64.encodeToString(kp.private.encoded, Base64.NO_WRAP) - ) - } + + // 保存到 DataStore + val privateKeyB64 = Base64.encodeToString(kp.private.encoded, Base64.NO_WRAP) + val publicKeyB64 = Base64.encodeToString(kp.public.encoded, Base64.NO_WRAP) + adbClientData.rsaPrivateKey.set(privateKeyB64) + adbClientData.rsaPublicKeyX509.set(publicKeyB64) + Log.i( TAG, - "loadOrCreate(): generated new RSA key pair, fp=${fingerprint(kp.public.encoded)}" + "loadOrCreate(): generated new RSA key pair and saved to DataStore, fp=${fingerprint(kp.public.encoded)}" ) return Pair(kp.private, kp.public.encoded) } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AdvancedConfigPage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AdvancedConfigPage.kt index 9bfcd92..f01cf62 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AdvancedConfigPage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AdvancedConfigPage.kt @@ -28,8 +28,7 @@ import io.github.miuzarte.scrcpyforandroid.constants.ScrcpyPresets import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlide -import io.github.miuzarte.scrcpyforandroid.storage.AppSettings -import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions +import io.github.miuzarte.scrcpyforandroid.storage.Storage import kotlinx.coroutines.launch import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.ScrollBehavior @@ -89,10 +88,9 @@ internal fun AdvancedConfigPage( onRefreshEncoders: () -> Unit, onRefreshCameraSizes: () -> Unit, ) { - val context = LocalContext.current + val scrcpyOptions = Storage.scrcpyOptions - val appSettings = remember(context) { AppSettings(context) } - val scrcpyOptions = remember(context) { ScrcpyOptions(context) } + val context = LocalContext.current val focusManager = LocalFocusManager.current diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DevicePage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DevicePage.kt index ac1aec7..6502619 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DevicePage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DevicePage.kt @@ -34,11 +34,7 @@ import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.ListOptions import io.github.miuzarte.scrcpyforandroid.services.EventLogger import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent import io.github.miuzarte.scrcpyforandroid.services.fetchConnectedDeviceInfo -import io.github.miuzarte.scrcpyforandroid.services.parseQuickTarget -import io.github.miuzarte.scrcpyforandroid.services.upsertQuickDevice -import io.github.miuzarte.scrcpyforandroid.storage.AppSettings -import io.github.miuzarte.scrcpyforandroid.storage.QuickDevices -import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions +import io.github.miuzarte.scrcpyforandroid.storage.Storage import io.github.miuzarte.scrcpyforandroid.widgets.ConfigPanel import io.github.miuzarte.scrcpyforandroid.widgets.DeviceEditorScreen import io.github.miuzarte.scrcpyforandroid.widgets.DeviceTile @@ -131,11 +127,11 @@ fun DeviceTabScreen( onOpenAdvancedPage: () -> Unit, onOpenFullscreenPage: (ScrcpySessionInfo) -> Unit, ) { - val context = LocalContext.current + val appSettings = Storage.appSettings + val quickDevices = Storage.quickDevices + val scrcpyOptions = Storage.scrcpyOptions - val appSettings = remember(context) { AppSettings(context) } - val quickDevices = remember(context) { QuickDevices(context) } - val scrcpyOptions = remember(context) { ScrcpyOptions(context) } + val context = LocalContext.current val haptics = rememberAppHaptics() @@ -216,11 +212,19 @@ fun DeviceTabScreen( } var quickDevicesList by quickDevices.quickDevicesList.asMutableState() - var savedShortcuts = rememberSaveable(saver = DeviceShortcutsSaver) { + var savedShortcuts = rememberSaveable(quickDevicesList, saver = DeviceShortcutsSaver) { DeviceShortcuts.unmarshalFrom(quickDevicesList) } var quickConnectInput by quickDevices.quickConnectInput.asMutableState() + // save changes when [savedShortcuts] was modified + LaunchedEffect(savedShortcuts) { + val serialized = savedShortcuts.marshalToString() + if (serialized != quickDevicesList) { + quickDevicesList = serialized + } + } + /** * Disconnect the current ADB connection and stop any running scrcpy session. * @@ -232,7 +236,7 @@ fun DeviceTabScreen( * - Calls `nativeCore.scrcpyStop()` and `nativeCore.adbDisconnect()` (best-effort). * - Resets UI-visible connection state: `adbConnected`, `currentTargetHost/Port`, * `sessionInfo`, device capability flags, `statusLine`, and `connectedDeviceLabel`. - * - Updates the saved quick-device list via [upsertQuickDevice] when a target is provided. + * - Updates the saved quick-device list via [savedShortcuts.update] when a target is provided. * - Logs an optional [logMessage] to the local event log. * - Shows an optional snackbar message asynchronously (launched on the composition scope) * so callers don't get blocked by `snack.showSnackbar` (it is suspending). @@ -263,8 +267,9 @@ fun DeviceTabScreen( connectedDeviceLabel = "未连接" clearQuickOnlineForTarget?.let { target -> if (target.host.isNotBlank()) - savedShortcuts = - savedShortcuts.update(host = target.host, port = target.port, online = false) + savedShortcuts = savedShortcuts.update( + host = target.host, port = target.port, online = false + ) } logMessage?.let { logEvent(it) } if (!showSnackMessage.isNullOrBlank()) { @@ -539,10 +544,24 @@ fun DeviceTabScreen( connectedDeviceLabel = info.model applyConnectedDeviceCapabilities(info.sdkInt, info.androidRelease) - quickDevices.updateQuickDeviceNameIfEmpty(host, port, fullLabel) + savedShortcuts = savedShortcuts.update( + host = host, + port = port, + name = fullLabel, + updateNameOnlyWhenEmpty = true + ) statusLine = "$host:$port" - logEvent("ADB 已连接: model=${info.model}, serial=${info.serial.ifBlank { "unknown" }}, manufacturer=${info.manufacturer.ifBlank { "unknown" }}, brand=${info.brand.ifBlank { "unknown" }}, device=${info.device.ifBlank { "unknown" }}, android=${info.androidRelease.ifBlank { "unknown" }}, sdk=${info.sdkInt}") + logEvent( + "ADB 已连接: " + + "model=${info.model}, " + + "serial=${info.serial.ifBlank { "unknown" }}, " + + "manufacturer=${info.manufacturer.ifBlank { "unknown" }}, " + + "brand=${info.brand.ifBlank { "unknown" }}, " + + "device=${info.device.ifBlank { "unknown" }}, " + + "android=${info.androidRelease.ifBlank { "unknown" }}, " + + "sdk=${info.sdkInt}" + ) scope.launch { snack.showSnackbar("ADB 已连接") } @@ -857,7 +876,8 @@ fun DeviceTabScreen( try { connectWithTimeout(host, port) adbConnected = true - savedShortcuts = savedShortcuts.update(host = host, port = port, online = true) + savedShortcuts = + savedShortcuts.update(host = host, port = port, online = true) handleAdbConnected(host, port) } catch (e: Exception) { statusLine = "ADB 连接失败" @@ -886,23 +906,32 @@ fun DeviceTabScreen( // "快速连接" QuickConnectCard( input = quickConnectInput, - onInputChange = { quickConnectInput = it }, + onValueChange = { quickConnectInput = it }, enabled = !adbConnecting, onAddDevice = { - val target = parseQuickTarget(quickConnectInput) ?: return@QuickConnectCard - savedShortcuts = savedShortcuts.update(host = target.host, port = target.port) + val target = ConnectionTarget.unmarshalFrom(quickConnectInput) + ?: return@QuickConnectCard + savedShortcuts = savedShortcuts.upsert( + DeviceShortcut(host = target.host, port = target.port) + ) + Log.i("SavedShortcuts", "size: ${savedShortcuts.size}, list: $quickDevicesList") scope.launch { snack.showSnackbar("已添加设备: ${target.host}:${target.port}") } }, onConnect = { - val target = parseQuickTarget(quickConnectInput) ?: return@QuickConnectCard + val target = ConnectionTarget.unmarshalFrom(quickConnectInput) + ?: return@QuickConnectCard runAdbConnect("连接 ADB", onFinished = { activeDeviceActionId = null }) { disconnectCurrentTargetBeforeConnecting(target.host, target.port) try { connectWithTimeout(target.host, target.port) adbConnected = true - savedShortcuts = savedShortcuts.update(host = target.host, port = target.port, online = true) + savedShortcuts = savedShortcuts.update( + host = target.host, + port = target.port, + online = true + ) handleAdbConnected(target.host, target.port) } catch (e: Exception) { statusLine = "ADB 连接失败" diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlPage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlPage.kt index d9df992..6ac4fc8 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlPage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlPage.kt @@ -22,8 +22,7 @@ import androidx.core.view.WindowInsetsControllerCompat import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade.ScrcpySessionInfo import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics -import io.github.miuzarte.scrcpyforandroid.storage.AppSettings -import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions +import io.github.miuzarte.scrcpyforandroid.storage.Storage import io.github.miuzarte.scrcpyforandroid.widgets.FullscreenControlScreen import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonBar @@ -46,10 +45,9 @@ fun FullscreenControlPage( // Disable predictive back handler temporarily to avoid decoding issues. BackHandler(enabled = true, onBack = onDismiss) - val context = LocalContext.current + val appSettings = Storage.appSettings - val appSettings = remember(context) { AppSettings(context) } - val scrcpyOptions = remember(context) { ScrcpyOptions(context) } + val context = LocalContext.current val haptics = rememberAppHaptics() diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainPage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainPage.kt index 203aa10..eff434a 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainPage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainPage.kt @@ -51,7 +51,7 @@ import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.storage.AppSettings -import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions +import io.github.miuzarte.scrcpyforandroid.storage.Storage import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -191,8 +191,8 @@ fun MainPage() { } } - val appSettings = remember(context) { AppSettings(context) } - val scrcpyOptions = remember(context) { ScrcpyOptions(context) } + val appSettings = Storage.appSettings + val scrcpyOptions = Storage.scrcpyOptions val videoEncoderOptions = remember { mutableStateListOf() } val audioEncoderOptions = remember { mutableStateListOf() } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsPage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsPage.kt index 206c42a..3130e87 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsPage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsPage.kt @@ -1,6 +1,7 @@ package io.github.miuzarte.scrcpyforandroid.pages import android.content.Intent +import android.os.Process import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -13,6 +14,7 @@ import androidx.compose.material.icons.rounded.FileOpen import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -22,12 +24,16 @@ import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlide import io.github.miuzarte.scrcpyforandroid.storage.AppSettings -import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions +import io.github.miuzarte.scrcpyforandroid.storage.PreferenceMigration +import io.github.miuzarte.scrcpyforandroid.storage.Storage import io.github.miuzarte.scrcpyforandroid.widgets.SectionSmallTitle +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton import top.yukonga.miuix.kmp.basic.ScrollBehavior +import top.yukonga.miuix.kmp.basic.SnackbarHostState import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TextField import top.yukonga.miuix.kmp.extra.SuperArrow @@ -35,6 +41,7 @@ import top.yukonga.miuix.kmp.extra.SuperDropdown import top.yukonga.miuix.kmp.extra.SuperSwitch import top.yukonga.miuix.kmp.theme.ColorSchemeMode import kotlin.math.roundToInt +import kotlin.system.exitProcess private data class ThemeModeOption( val label: String, @@ -68,10 +75,13 @@ fun SettingsScreen( onPickServer: () -> Unit, scrollBehavior: ScrollBehavior, ) { + val appContext = LocalContext.current.applicationContext + val appSettings = Storage.appSettings + val context = LocalContext.current - val appSettings = remember(context) { AppSettings(context) } - val scrcpyOptions = remember(context) { ScrcpyOptions(context) } + val scope = rememberCoroutineScope() + val snackHostState = remember { SnackbarHostState() } val baseModeItems = THEME_BASE_OPTIONS.map { it.label } @@ -255,17 +265,49 @@ fun SettingsScreen( ) } + SectionSmallTitle("应用") + Card { + SuperArrow( + title = "恢复旧版本配置", + summary = "从旧版本的 SharedPreferences 恢复至 DataStore", + onClick = { + scope.launch { + val migration = PreferenceMigration(appContext) + migration.migrate(clearSharedPrefs = false) + snackHostState.showSnackbar("迁移完成,应用将重启") + + delay(1000) + + val intent = context.packageManager.getLaunchIntentForPackage( + context.packageName + ) + intent?.apply { + addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK + or Intent.FLAG_ACTIVITY_CLEAR_TASK + ) + } + context.startActivity(intent) + + Process.killProcess(Process.myPid()) + exitProcess(0) + } + }, + ) + } + SectionSmallTitle("关于") Card { SuperArrow( title = "前往仓库", summary = "github.com/Miuzarte/ScrcpyForAndroid", onClick = { - val intent = Intent( - Intent.ACTION_VIEW, - "https://github.com/Miuzarte/ScrcpyForAndroid".toUri() + context.startActivity( + Intent( + Intent.ACTION_VIEW, + "https://github.com/Miuzarte/ScrcpyForAndroid".toUri(), + ) ) - context.startActivity(intent) }, ) } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderPage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderPage.kt index 39b289b..f30d70e 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderPage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderPage.kt @@ -8,8 +8,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn -import io.github.miuzarte.scrcpyforandroid.storage.AppSettings -import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions +import io.github.miuzarte.scrcpyforandroid.storage.Storage import io.github.miuzarte.scrcpyforandroid.widgets.ReorderableList import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions @@ -22,10 +21,9 @@ internal fun VirtualButtonOrderPage( contentPadding: PaddingValues, scrollBehavior: ScrollBehavior, ) { - val context = LocalContext.current + val appSettings = Storage.appSettings - val appSettings = remember(context) { AppSettings(context) } - val scrcpyOptions = remember(context) { ScrcpyOptions(context) } + val context = LocalContext.current var previewVirtualButtonShowText by appSettings.previewVirtualButtonShowText.asMutableState() var virtualButtonsLayout by appSettings.virtualButtonsLayout.asMutableState() diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/QuickDeviceStore.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/QuickDeviceStore.kt index 67fa3d3..c89c6db 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/QuickDeviceStore.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/QuickDeviceStore.kt @@ -4,7 +4,6 @@ import android.content.Context import androidx.core.content.edit import io.github.miuzarte.scrcpyforandroid.constants.AppPreferenceKeys import io.github.miuzarte.scrcpyforandroid.constants.Defaults -import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut internal fun loadQuickDevices(context: Context): List { @@ -63,70 +62,3 @@ internal fun saveQuickDevices(context: Context, quickDevices: List, - host: String, - port: Int, - online: Boolean, -) { - val idx = quickDevices.indexOfFirst { it.id == "$host:$port" } - val existingName = if (idx >= 0) quickDevices[idx].name else "" - val item = DeviceShortcut( - name = existingName, - host = host, - port = port, - online = online, - ) - if (idx >= 0) quickDevices[idx] = item else quickDevices.add(0, item) - saveQuickDevices(context, quickDevices) -} - -internal fun updateQuickDeviceNameIfEmpty( - context: Context, - quickDevices: MutableList, - host: String, - port: Int, - fallbackName: String, -) { - val idx = quickDevices.indexOfFirst { it.host == host && it.port == port } - if (idx >= 0 && quickDevices[idx].name.isBlank()) { - quickDevices[idx] = quickDevices[idx].copy(name = fallbackName) - saveQuickDevices(context, quickDevices) - } -} - -internal fun replaceQuickDevicePort( - context: Context, - quickDevices: MutableList, - host: String, - oldPort: Int, - newPort: Int, - online: Boolean, -) { - val idx = quickDevices.indexOfFirst { it.host == host && it.port == oldPort } - if (idx < 0) return - - val old = quickDevices[idx] - val updated = old.copy( - port = newPort, - online = online, - ) - - quickDevices[idx] = updated - val dedup = quickDevices.distinctBy { it.id } - quickDevices.clear() - quickDevices.addAll(dedup) - saveQuickDevices(context, quickDevices) -} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/AdbClientData.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/AdbClientData.kt new file mode 100644 index 0000000..5b92a29 --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/AdbClientData.kt @@ -0,0 +1,20 @@ +package io.github.miuzarte.scrcpyforandroid.storage + +import android.content.Context +import androidx.datastore.preferences.core.stringPreferencesKey + +class AdbClientData(context: Context) : Settings(context, "AdbClient") { + companion object { + val RSA_PRIVATE_KEY = Pair( + stringPreferencesKey("rsa_private_key"), + "", + ) + val RSA_PUBLIC_KEY_X509 = Pair( + stringPreferencesKey("rsa_public_key_x509"), + "", + ) + } + + val rsaPrivateKey by setting(RSA_PRIVATE_KEY) + val rsaPublicKeyX509 by setting(RSA_PUBLIC_KEY_X509) +} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/AppSettings.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/AppSettings.kt index a1e7d5e..e0f4f8b 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/AppSettings.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/AppSettings.kt @@ -87,30 +87,6 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") { val adbAutoReconnectPairedDevice by setting(ADB_AUTO_RECONNECT_PAIRED_DEVICE) val adbMdnsLanDiscovery by setting(ADB_MDNS_LAN_DISCOVERY) - override suspend fun toMap(): Map = mapOf( - // Theme Settings - THEME_BASE_INDEX.name to themeBaseIndex.get(), - MONET.name to monet.get(), - - // Scrcpy Settings - FULLSCREEN_DEBUG_INFO.name to fullscreenDebugInfo.get(), - SHOW_FULLSCREEN_VIRTUAL_BUTTONS.name to showFullscreenVirtualButtons.get(), - KEEP_SCREEN_ON_WHEN_STREAMING.name to keepScreenOnWhenStreaming.get(), - DEVICE_PREVIEW_CARD_HEIGHT_DP.name to devicePreviewCardHeightDp.get(), - PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT.name to previewVirtualButtonShowText.get(), - VIRTUAL_BUTTONS_LAYOUT.name to virtualButtonsLayout.get(), - - // Scrcpy Server Settings - CUSTOM_SERVER_URI.name to customServerUri.get(), - SERVER_REMOTE_PATH.name to serverRemotePath.get(), - - // ADB Settings - ADB_KEY_NAME.name to adbKeyName.get(), - ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN.name to adbPairingAutoDiscoverOnDialogOpen.get(), - ADB_AUTO_RECONNECT_PAIRED_DEVICE.name to adbAutoReconnectPairedDevice.get(), - ADB_MDNS_LAN_DISCOVERY.name to adbMdnsLanDiscovery.get() - ) - // TODO? - override fun validate(): Boolean = true + // fun validate(): Boolean = true } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/PreferenceMigration.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/PreferenceMigration.kt index 952ae5d..0954abb 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/PreferenceMigration.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/PreferenceMigration.kt @@ -3,69 +3,80 @@ package io.github.miuzarte.scrcpyforandroid.storage import android.content.Context import android.content.SharedPreferences import android.util.Log +import androidx.core.content.edit import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults import io.github.miuzarte.scrcpyforandroid.constants.AppPreferenceKeys import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext private const val TAG = "PreferenceMigration" -private const val MIGRATION_COMPLETED_KEY = "migration_completed" /** * 从旧的 SharedPreferences 迁移到新的 DataStore */ -class PreferenceMigration(private val context: Context) { - +class PreferenceMigration(private val appContext: Context) { + private val appSharedPrefs: SharedPreferences by lazy { + appContext.getSharedPreferences(AppPreferenceKeys.PREFS_NAME, Context.MODE_PRIVATE) + } + private val sharedPrefs: SharedPreferences by lazy { - context.getSharedPreferences(AppPreferenceKeys.PREFS_NAME, Context.MODE_PRIVATE) + appContext.getSharedPreferences("nativecore_adb_rsa", Context.MODE_PRIVATE) } /** - * 检查是否已完成迁移 + * 检查是否需要迁移(SharedPreferences 是否包含数据) */ - @Deprecated("做成设置内入口手动调用,这个没必要") - suspend fun isMigrationCompleted(): Boolean = withContext(Dispatchers.IO) { - sharedPrefs.getBoolean(MIGRATION_COMPLETED_KEY, false) + suspend fun needsMigration(): Boolean = withContext(Dispatchers.IO) { + appSharedPrefs.all.isNotEmpty() || sharedPrefs.all.isNotEmpty() } /** * 执行完整迁移 - * @return 迁移是否成功 */ - suspend fun migrate(): Boolean = withContext(Dispatchers.IO) { - try { - if (isMigrationCompleted()) { - Log.i(TAG, "Migration already completed, skipping") - return@withContext true - } - - Log.i(TAG, "Starting migration from SharedPreferences to DataStore") - - // 迁移 AppSettings - migrateAppSettings() - - // 迁移 ScrcpyOptions - migrateScrcpyOptions() - - // 迁移 QuickDevices - migrateQuickDevices() - - // 标记迁移完成 - markMigrationCompleted() - - Log.i(TAG, "Migration completed successfully") - true - } catch (e: Exception) { - Log.e(TAG, "Migration failed", e) - false + suspend fun migrate( + clearSharedPrefs: Boolean = false, + ) = withContext(Dispatchers.IO) { + if (!needsMigration()) { + Log.i(TAG, "No data to migrate, skipping") + return@withContext + } else { + val appList = appSharedPrefs.all.entries.joinToString { (k, v) -> "\"$k\" to $v" } + val adbList = sharedPrefs.all.entries.joinToString { (k, v) -> "\"$k\" to $v" } + Log.d(TAG, "Migrating appSharedPrefs ($appList)") } + + Log.i(TAG, "Starting migration from SharedPreferences to DataStore") + + // 迁移 AppSettings + migrateAppSettings() + + // 迁移 ScrcpyOptions + migrateScrcpyOptions() + + // 迁移 QuickDevices + migrateQuickDevices() + + // 迁移 ADB 密钥 + migrateAdbClientData() + + // 清空 SharedPreferences + if (clearSharedPrefs) { + appSharedPrefs.edit { clear() } + sharedPrefs.edit { clear() } + Log.d(TAG, "SharedPreferences cleared") + } + + Log.i( + TAG, "Migration completed successfully" + + " and SharedPreferences ${if (clearSharedPrefs) "" else "is not "}cleared" + ) } /** * 迁移应用设置 */ private suspend fun migrateAppSettings() { - val appSettings = AppSettings(context) + val appSettings = Storage.appSettings // Theme Settings migrateInt( @@ -152,7 +163,7 @@ class PreferenceMigration(private val context: Context) { * 迁移 Scrcpy 选项 */ private suspend fun migrateScrcpyOptions() { - val scrcpyOptions = ScrcpyOptions(context) + val scrcpyOptions = Storage.scrcpyOptions // Audio & Video Codecs migrateString( @@ -383,10 +394,10 @@ class PreferenceMigration(private val context: Context) { * 迁移快速设备列表 */ private suspend fun migrateQuickDevices() { - val quickDevices = QuickDevices(context) + val quickDevices = Storage.quickDevices // Migrate quick devices list - val quickDevicesRaw = sharedPrefs.getString( + val quickDevicesRaw = appSharedPrefs.getString( AppPreferenceKeys.QUICK_DEVICES, "" ).orEmpty() @@ -403,12 +414,21 @@ class PreferenceMigration(private val context: Context) { Log.d(TAG, "QuickDevices migration completed") } - + /** - * 标记迁移完成 + * 迁移 ADB 客户端数据(RSA 密钥) */ - private fun markMigrationCompleted() { - sharedPrefs.edit().putBoolean(MIGRATION_COMPLETED_KEY, true).apply() + private suspend fun migrateAdbClientData() { + val adbClientData = Storage.adbClientData + + // 迁移 RSA 私钥 + val privKey = sharedPrefs.getString("priv", null) + if (privKey != null) { + adbClientData.rsaPrivateKey.set(privKey) + Log.d(TAG, "ADB RSA private key migrated") + } + + Log.d(TAG, "AdbClientData migration completed") } // Helper methods for different data types @@ -418,7 +438,7 @@ class PreferenceMigration(private val context: Context) { defaultValue: String, settingProperty: Settings.SettingProperty ) { - val value = sharedPrefs.getString(key, defaultValue) + val value = appSharedPrefs.getString(key, defaultValue) .orEmpty() .ifBlank { defaultValue } settingProperty.set(value) @@ -429,7 +449,7 @@ class PreferenceMigration(private val context: Context) { defaultValue: String, action: suspend (String) -> Unit ) { - val value = sharedPrefs.getString(key, defaultValue) + val value = appSharedPrefs.getString(key, defaultValue) .orEmpty() .ifBlank { defaultValue } action(value) @@ -440,7 +460,7 @@ class PreferenceMigration(private val context: Context) { defaultValue: Int, settingProperty: Settings.SettingProperty ) { - val value = sharedPrefs.getInt(key, defaultValue) + val value = appSharedPrefs.getInt(key, defaultValue) settingProperty.set(value) } @@ -449,7 +469,7 @@ class PreferenceMigration(private val context: Context) { defaultValue: Boolean, settingProperty: Settings.SettingProperty ) { - val value = sharedPrefs.getBoolean(key, defaultValue) + val value = appSharedPrefs.getBoolean(key, defaultValue) settingProperty.set(value) } @@ -458,7 +478,7 @@ class PreferenceMigration(private val context: Context) { defaultValue: Boolean, action: suspend (Boolean) -> Unit ) { - val value = sharedPrefs.getBoolean(key, defaultValue) + val value = appSharedPrefs.getBoolean(key, defaultValue) action(value) } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/QuickDevices.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/QuickDevices.kt index 2d96a42..ebba540 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/QuickDevices.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/QuickDevices.kt @@ -2,9 +2,6 @@ package io.github.miuzarte.scrcpyforandroid.storage import android.content.Context import androidx.datastore.preferences.core.stringPreferencesKey -import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map class QuickDevices(context: Context) : Settings(context, "QuickDevices") { companion object { @@ -20,145 +17,4 @@ class QuickDevices(context: Context) : Settings(context, "QuickDevices") { val quickDevicesList by setting(QUICK_DEVICES_LIST) val quickConnectInput by setting(QUICK_CONNECT_INPUT) - - override suspend fun toMap(): Map = mapOf( - QUICK_DEVICES_LIST.name to quickDevicesList.get(), - QUICK_CONNECT_INPUT.name to quickConnectInput.get(), - ) - - override fun validate(): Boolean = true - - suspend fun getQuickDevices(): List { - val raw = quickDevicesList.get() - if (raw.isBlank()) return emptyList() - - val result = mutableListOf() - raw.lineSequence().forEach { line -> - DeviceShortcut.unmarshalFrom(line)?.let { result.add(it) } - } - return result - } - - suspend fun setQuickDevices(list: List) = - quickDevicesList.set(list.joinToString("\n") { it.marshalToString() }) - - fun observeQuickDevices(): Flow> = quickDevicesList.observe() - .map { raw -> - if (raw.isBlank()) return@map emptyList() - - val result = mutableListOf() - raw.lineSequence().forEach { line -> - DeviceShortcut.unmarshalFrom(line)?.let { result.add(it) } - } - result - } - - /** - * 插入或更新快速设备 - */ - suspend fun upsertQuickDevice( - host: String, - port: Int, - online: Boolean, - index: Int? = 0, - ) { - val quickDevices = getQuickDevices().toMutableList() - val id = "$host:$port" - val idx = quickDevices.indexOfFirst { it.id == id } - val existingName = if (idx >= 0) quickDevices[idx].name else "" - val item = DeviceShortcut( - name = existingName, - host = host, - port = port, - online = online, - ) - if (idx >= 0) { - quickDevices[idx] = item - } else { - if (index != null) - quickDevices.add(index, item) - else quickDevices.add(item) - } - setQuickDevices(quickDevices) - } - - /** - * 如果设备名称为空,则更新为备用名称 - */ - suspend fun updateQuickDeviceNameIfEmpty( - host: String, - port: Int, - fallbackName: String, - ) { - val quickDevices = getQuickDevices().toMutableList() - val idx = quickDevices.indexOfFirst { it.host == host && it.port == port } - if (idx >= 0 && quickDevices[idx].name.isBlank()) { - quickDevices[idx] = quickDevices[idx].copy(name = fallbackName) - setQuickDevices(quickDevices) - } - } - - /** - * 替换快速设备的端口 - */ - suspend fun replaceQuickDevicePort( - host: String, - oldPort: Int, - newPort: Int, - online: Boolean, - ) { - val quickDevices = getQuickDevices().toMutableList() - val idx = quickDevices.indexOfFirst { it.host == host && it.port == oldPort } - if (idx < 0) return - - val old = quickDevices[idx] - val updated = old.copy( - port = newPort, - online = online, - ) - - quickDevices[idx] = updated - val dedup = quickDevices.distinctBy { it.id } - setQuickDevices(dedup) - } - - /** - * 删除快速设备 - */ - suspend fun removeQuickDevice(id: String) { - val quickDevices = getQuickDevices().toMutableList() - quickDevices.removeAll { it.id == id } - setQuickDevices(quickDevices) - } - - /** - * 更新设备在线状态 - */ - suspend fun updateDeviceOnlineStatus(host: String, port: Int, online: Boolean) { - val quickDevices = getQuickDevices().toMutableList() - val idx = quickDevices.indexOfFirst { it.host == host && it.port == port } - if (idx >= 0) { - quickDevices[idx] = quickDevices[idx].copy(online = online) - setQuickDevices(quickDevices) - } - } - - /** - * 更新设备名称 - */ - suspend fun updateDeviceName(id: String, name: String) { - val quickDevices = getQuickDevices().toMutableList() - val idx = quickDevices.indexOfFirst { it.id == id } - if (idx >= 0) { - quickDevices[idx] = quickDevices[idx].copy(name = name) - setQuickDevices(quickDevices) - } - } - - /** - * 清空所有快速设备 - */ - suspend fun clearQuickDevices() { - quickDevicesList.set("") - } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt index c979f36..d53509d 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt @@ -278,61 +278,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { val vdDestroyContent by setting(VD_DESTROY_CONTENT) val vdSystemDecorations by setting(VD_SYSTEM_DECORATIONS) - override suspend fun toMap(): Map = mapOf( - CROP.name to crop.get(), - RECORD_FILENAME.name to recordFilename.get(), - VIDEO_CODEC_OPTIONS.name to videoCodecOptions.get(), - AUDIO_CODEC_OPTIONS.name to audioCodecOptions.get(), - VIDEO_ENCODER.name to videoEncoder.get(), - AUDIO_ENCODER.name to audioEncoder.get(), - CAMERA_ID.name to cameraId.get(), - CAMERA_SIZE.name to cameraSize.get(), - CAMERA_AR.name to cameraAr.get(), - CAMERA_FPS.name to cameraFps.get(), - LOG_LEVEL.name to logLevel.get(), - VIDEO_CODEC.name to videoCodec.get(), - AUDIO_CODEC.name to audioCodec.get(), - VIDEO_SOURCE.name to videoSource.get(), - AUDIO_SOURCE.name to audioSource.get(), - RECORD_FORMAT.name to recordFormat.get(), - CAMERA_FACING.name to cameraFacing.get(), - MAX_SIZE.name to maxSize.get(), - VIDEO_BIT_RATE.name to videoBitRate.get(), - AUDIO_BIT_RATE.name to audioBitRate.get(), - MAX_FPS.name to maxFps.get(), - ANGLE.name to angle.get(), - CAPTURE_ORIENTATION.name to captureOrientation.get(), - CAPTURE_ORIENTATION_LOCK.name to captureOrientationLock.get(), - DISPLAY_ORIENTATION.name to displayOrientation.get(), - RECORD_ORIENTATION.name to recordOrientation.get(), - DISPLAY_IME_POLICY.name to displayImePolicy.get(), - DISPLAY_ID.name to displayId.get(), - SCREEN_OFF_TIMEOUT.name to screenOffTimeout.get(), - SHOW_TOUCHES.name to showTouches.get(), - FULLSCREEN.name to fullscreen.get(), - CONTROL.name to control.get(), - VIDEO_PLAYBACK.name to videoPlayback.get(), - AUDIO_PLAYBACK.name to audioPlayback.get(), - TURN_SCREEN_OFF.name to turnScreenOff.get(), - STAY_AWAKE.name to stayAwake.get(), - DISABLE_SCREENSAVER.name to disableScreensaver.get(), - POWER_OFF_ON_CLOSE.name to powerOffOnClose.get(), - CLEANUP.name to cleanup.get(), - POWER_ON.name to powerOn.get(), - VIDEO.name to video.get(), - AUDIO.name to audio.get(), - REQUIRE_AUDIO.name to requireAudio.get(), - KILL_ADB_ON_CLOSE.name to killAdbOnClose.get(), - CAMERA_HIGH_SPEED.name to cameraHighSpeed.get(), - LIST.name to list.get(), - AUDIO_DUP.name to audioDup.get(), - NEW_DISPLAY.name to newDisplay.get(), - START_APP.name to startApp.get(), - VD_DESTROY_CONTENT.name to vdDestroyContent.get(), - VD_SYSTEM_DECORATIONS.name to vdSystemDecorations.get() - ) - - override fun validate(): Boolean = runBlocking { + fun validate(): Boolean = runBlocking { runCatching { toClientOptions().validate() true diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/Settings.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/Settings.kt index ba530cf..020d165 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/Settings.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/Settings.kt @@ -42,11 +42,16 @@ abstract class Settings( } /** - * 设置项委托类,自动提供 get/set/observe/observeAsState/asMutableState 方法 + * 设置项委托类,自动提供 get/set/observe/asState/asMutableState 方法 */ inner class SettingProperty( - private val pair: Pair + val pair: Pair ) { + // 创建时注册自身 + init { + registerProperty(pair.name, this) + } + operator fun getValue(thisRef: Any?, property: KProperty<*>): SettingProperty = this suspend fun get(): T = getValue(pair) @@ -62,6 +67,15 @@ abstract class Settings( fun asMutableState(): MutableState = this@Settings.asMutableState(pair) } + // 注册表, 用于遍历 + private val propertyRegistry = mutableMapOf>() + + private fun registerProperty(name: String, property: SettingProperty) { + propertyRegistry[name] = property + } + + protected fun getAllProperties(): Map> = propertyRegistry.toMap() + // 为 Context 添加扩展委托属性,确保 DataStore 单例 private val Context.dataStore: DataStore by preferencesDataStore( name = this.name, @@ -69,21 +83,16 @@ abstract class Settings( scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) ) - abstract suspend fun toMap(): Map - abstract fun validate(): Boolean - // 对外暴露的 DataStore 实例 protected val dataStore: DataStore = context.dataStore - protected fun setting(pair: Pair): SettingProperty = - SettingProperty(pair) + protected fun setting(pair: Pair) = SettingProperty(pair) protected suspend fun getValue(pair: Pair): T = dataStore.data.first()[pair.key] ?: pair.defaultValue - protected suspend fun setValue(pair: Pair, value: T) { + protected suspend fun setValue(pair: Pair, value: T) = dataStore.edit { preferences -> preferences[pair.key] = value } - } protected fun observe(pair: Pair): Flow = dataStore.data.map { preferences -> preferences[pair.key] ?: pair.defaultValue } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/Storage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/Storage.kt new file mode 100644 index 0000000..57ffa88 --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/Storage.kt @@ -0,0 +1,17 @@ +package io.github.miuzarte.scrcpyforandroid.storage + +import android.content.Context + +// settings singleton +object Storage { + private lateinit var appContext: Context + + fun init(context: Context) { + appContext = context.applicationContext + } + + val appSettings: AppSettings by lazy { AppSettings(appContext) } + val quickDevices: QuickDevices by lazy { QuickDevices(appContext) } + val scrcpyOptions: ScrcpyOptions by lazy { ScrcpyOptions(appContext) } + val adbClientData: AdbClientData by lazy { AdbClientData(appContext) } +} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/StorageMigration.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/StorageMigration.kt deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt index 63b9cf6..d52f8ca 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt @@ -48,6 +48,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput @@ -71,8 +74,7 @@ import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlide -import io.github.miuzarte.scrcpyforandroid.storage.AppSettings -import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions +import io.github.miuzarte.scrcpyforandroid.storage.Storage import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -132,10 +134,9 @@ internal fun StatusCard( busyLabel: String?, connectedDeviceLabel: String, ) { - val context = LocalContext.current + val appSettings = Storage.appSettings - val appSettings = remember(context) { AppSettings(context) } - val scrcpyOptions = remember(context) { ScrcpyOptions(context) } + val context = LocalContext.current val themeBaseIndex by appSettings.themeBaseIndex.asState() @@ -381,10 +382,9 @@ internal fun ConfigPanel( onStop: () -> Unit, sessionStarted: Boolean, ) { - val context = LocalContext.current + val scrcpyOptions = Storage.scrcpyOptions - // val appSettings = remember(context) { AppSettings(context) } - val scrcpyOptions = remember(context) { ScrcpyOptions(context) } + val context = LocalContext.current SectionSmallTitle("Scrcpy") Card { @@ -1221,12 +1221,16 @@ internal fun DeviceTile( @Composable internal fun QuickConnectCard( input: String, - onInputChange: (String) -> Unit, + onValueChange: (String) -> Unit, onConnect: () -> Unit, onAddDevice: () -> Unit, enabled: Boolean = true, ) { + val scope = rememberCoroutineScope() val focusManager = LocalFocusManager.current + val focusRequester = remember { FocusRequester() } + var tempText by remember(input) { mutableStateOf(input) } + var tempFocusState by remember(input) { mutableStateOf(false) } Card( colors = CardDefaults.defaultColors(color = MiuixTheme.colorScheme.primaryContainer), @@ -1256,10 +1260,8 @@ internal fun QuickConnectCard( ) } TextField( - value = input, - onValueChange = { - if (enabled) onInputChange(it) - }, + value = tempText, + onValueChange = { tempText = it }, label = "IP:PORT", enabled = enabled, useLabelAsPlaceholder = true, @@ -1267,7 +1269,15 @@ internal fun QuickConnectCard( modifier = Modifier .fillMaxWidth() .padding(horizontal = UiSpacing.CardContent) - .padding(bottom = UiSpacing.SectionTitleLeadingGap), + .padding(bottom = UiSpacing.SectionTitleLeadingGap) + .onFocusChanged { focusState -> + // 失去焦点时回调 + if (!focusState.isFocused && tempFocusState) { + onValueChange(tempText) + } + tempFocusState = focusState.isFocused + } + .focusRequester(focusRequester), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), )