diff --git a/README.md b/README.md new file mode 100644 index 0000000..f4cdc54 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# Scrcpy For Android + +[Scrcpy](https://github.com/Genymobile/scrcpy) android client + +## 截图 + + + +

+ Screenshot 1 + Screenshot 2 + Screenshot 3 +

+ +## 已知问题 + +- ADB 配对流程未实现 +- 多指触控抬起后滞留 +- 快速离开再进入全屏会导致视频流关键帧丢失 + +## 构建 + +- JDK 17+ +- Android SDK(含 `compileSdk 36` / `buildTools 36.0.0`) +- Android NDK `28.2.13676358` + +```bash +./gradlew assembleDebug +``` + +Windows: + +```powershell +.\gradlew.bat assembleDebug +``` + +## Credits + +- [Genymobile/scrcpy](https://github.com/Genymobile/scrcpy) +- [YuKongA/miuix](https://github.com/compose-miuix-ui/miuix) +- [tiann/KernelSU/manager](https://github.com/tiann/KernelSU/tree/main/manager) + +## License + +[Apache License 2.0](LICENSE) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 44c1332..3f6f8a3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -28,6 +28,7 @@ android { ndk { abiFilters.clear() + //noinspection ChromeOsAbiSupport abiFilters += configuredAbiList } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/NativeCoreFacade.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/NativeCoreFacade.kt index 3161ab5..5a0a375 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/NativeCoreFacade.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/NativeCoreFacade.kt @@ -15,8 +15,8 @@ import java.io.File import java.time.LocalTime import java.time.format.DateTimeFormatter import java.util.ArrayDeque -import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.ExecutionException import java.util.concurrent.Executors @@ -34,7 +34,8 @@ class NativeCoreFacade(private val appContext: Context) { private val bootstrapLock = Any() private val bootstrapPackets = ArrayDeque() private var packetCount: Long = 0 - @Volatile private var audioPlayer: ScrcpyAudioPlayer? = null + @Volatile + private var audioPlayer: ScrcpyAudioPlayer? = null @Volatile private var currentSessionInfo: ScrcpySessionInfo? = null @@ -72,7 +73,10 @@ class NativeCoreFacade(private val appContext: Context) { val currentId = surfaceIdentityMap[tag] val requestId = surface?.let { System.identityHashCode(it) } if (requestId != null && currentId != null && requestId != currentId) { - Log.i(TAG, "unregisterVideoSurface(): skip stale request tag=$tag requestSurfaceId=$requestId currentSurfaceId=$currentId") + Log.i( + TAG, + "unregisterVideoSurface(): skip stale request tag=$tag requestSurfaceId=$requestId currentSurfaceId=$currentId" + ) return } Log.i(TAG, "unregisterVideoSurface(): tag=$tag surfaceId=$requestId") @@ -178,7 +182,12 @@ class NativeCoreFacade(private val appContext: Context) { audioPlayer?.release() audioPlayer = null if (info.audioCodecId != 0 && !request.noAudioPlayback) { - Log.i(TAG, "scrcpyStart(): create audio player codecId=0x${info.audioCodecId.toUInt().toString(16)}") + Log.i( + TAG, + "scrcpyStart(): create audio player codecId=0x${ + info.audioCodecId.toUInt().toString(16) + }" + ) val player = ScrcpyAudioPlayer(info.audioCodecId) audioPlayer = player sessionManager.attachAudioConsumer { packet -> @@ -208,7 +217,11 @@ class NativeCoreFacade(private val appContext: Context) { return true } - fun scrcpyListEncoders(customServerUri: String?, remotePath: String, serverVersion: String = "3.3.4"): ScrcpyEncoderLists { + fun scrcpyListEncoders( + customServerUri: String?, + remotePath: String, + serverVersion: String = "3.3.4" + ): ScrcpyEncoderLists { return ioCall { val serverJar = if (customServerUri.isNullOrBlank()) { extractAssetToCache(DEFAULT_SERVER_ASSET) @@ -232,7 +245,11 @@ class NativeCoreFacade(private val appContext: Context) { } } - fun scrcpyListCameraSizes(customServerUri: String?, remotePath: String, serverVersion: String = "3.3.4"): ScrcpyCameraSizeLists { + fun scrcpyListCameraSizes( + customServerUri: String?, + remotePath: String, + serverVersion: String = "3.3.4" + ): ScrcpyCameraSizeLists { return ioCall { val serverJar = if (customServerUri.isNullOrBlank()) { extractAssetToCache(DEFAULT_SERVER_ASSET) @@ -307,7 +324,17 @@ class NativeCoreFacade(private val appContext: Context) { buttons: Int = 0, ) { ioExecute { - runCatching { sessionManager.injectScroll(x, y, screenWidth, screenHeight, hScroll, vScroll, buttons) } + runCatching { + sessionManager.injectScroll( + x, + y, + screenWidth, + screenHeight, + hScroll, + vScroll, + buttons + ) + } } } @@ -475,10 +502,13 @@ class NativeCoreFacade(private val appContext: Context) { val mime = when (session.codec.lowercase()) { "h264" -> "video/avc" "h265" -> "video/hevc" - "av1" -> "video/av01" + "av1" -> "video/av01" else -> "video/avc" } - Log.i(TAG, "createOrReplaceDecoder(): tag=$tag codec=$mime size=${session.width}x${session.height}") + Log.i( + TAG, + "createOrReplaceDecoder(): tag=$tag codec=$mime size=${session.width}x${session.height}" + ) val decoder = AnnexBDecoder( width = session.width, height = session.height, @@ -489,7 +519,10 @@ class NativeCoreFacade(private val appContext: Context) { if (current == null || (current.width == width && current.height == height)) { return@AnnexBDecoder } - Log.i(TAG, "videoSizeChanged(): ${current.width}x${current.height} -> ${width}x${height}") + Log.i( + TAG, + "videoSizeChanged(): ${current.width}x${current.height} -> ${width}x${height}" + ) currentSessionInfo = current.copy(width = width, height = height) mainHandler.post { videoSizeListeners.forEach { listener -> @@ -542,14 +575,22 @@ class NativeCoreFacade(private val appContext: Context) { cacheBootstrapPacket(packet) packetCount += 1 if (packetCount == 1L || packetCount % 120L == 0L) { - Log.i(TAG, "videoFeed(): packets=$packetCount key=${packet.isKeyFrame} cfg=${packet.isConfig} decoders=${decoderMap.size}") + Log.i( + TAG, + "videoFeed(): packets=$packetCount key=${packet.isKeyFrame} cfg=${packet.isConfig} decoders=${decoderMap.size}" + ) } decoderMap.forEach { (tag, decoder) -> if (!surfaceIdentityMap.containsKey(tag)) { return@forEach } runCatching { - decoder.feedAnnexB(packet.data, packet.ptsUs, packet.isKeyFrame, packet.isConfig) + decoder.feedAnnexB( + packet.data, + packet.ptsUs, + packet.isKeyFrame, + packet.isConfig + ) } } } @@ -634,21 +675,3 @@ data class ScrcpySessionInfo( val codec: String, val controlEnabled: Boolean, ) - -object AndroidKeycodes { - const val HOME = 3 - const val BACK = 4 - const val POWER = 26 - const val APP_SWITCH = 187 - const val VOLUME_UP = 24 - const val VOLUME_DOWN = 25 -} - -object MotionActions { - const val DOWN = 0 - const val UP = 1 - const val MOVE = 2 - const val CANCEL = 3 - const val POINTER_DOWN = 5 - const val POINTER_UP = 6 -} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppDefaults.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppDefaults.kt index c0f811f..0053964 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppDefaults.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppDefaults.kt @@ -1,75 +1,79 @@ package io.github.miuzarte.scrcpyforandroid.constants object AppDefaults { - const val MaxEventLogLines = 512 - const val DefaultAdbPort = 5555 + const val EVENT_LOG_LINES = 512 + const val ADB_PORT = 5555 // Devices - const val DefaultQuickConnectInput = "" + const val QUICK_CONNECT_INPUT = "" - const val DefaultPairHost = "" - const val DefaultServerRemotePathInput = "" - const val DefaultAdbKeyNameInput = "" - const val DefaultPairPort = "" - const val DefaultPairCode = "" + const val PAIR_HOST = "" + const val PAIR_PORT = "" + const val PAIR_CODE = "" - const val DefaultAudioEnabled = true - const val DefaultAudioCodec = "opus" - const val DefaultAudioBitRateKbps = 128 - const val DefaultVideoCodec = "h264" - const val DefaultBitRateMbps = 8f - const val DefaultBitRateInput = "8.0" + const val AUDIO_ENABLED = true + const val AUDIO_CODEC = "opus" + const val AUDIO_BIT_RATE_KBPS = 128 + const val AUDIO_BIT_RATE_INPUT = "128" + const val VIDEO_CODEC = "h264" + const val VIDEO_BIT_RATE_MBPS = 8f + const val VIDEO_BIT_RATE_INPUT = "8.0" - const val DefaultTurnScreenOff = false - const val DefaultNoControl = false - const val DefaultNoVideo = false + const val TURN_SCREEN_OFF = false + const val NO_CONTROL = false + const val NO_VIDEO = false - const val DefaultVideoSourcePreset = "display" - const val DefaultDisplayIdInput = "" + const val VIDEO_SOURCE_PRESET = "display" + const val DISPLAY_ID = "" - const val DefaultCameraIdInput = "" - const val DefaultCameraFacingPreset = "" - const val DefaultCameraSizePreset = "" - const val DefaultCameraSizeCustom = "" - const val DefaultCameraArInput = "" - const val DefaultCameraFpsInput = "" - const val DefaultCameraHighSpeed = false + const val CAMERA_ID = "" + const val CAMERA_FACING_PRESET = "" + const val CAMERA_SIZE_PRESET = "" + const val CAMERA_SIZE_CUSTOM = "" + const val CAMERA_AR = "" + const val CAMERA_FPS = "" + const val CAMERA_HIGH_SPEED = false - const val DefaultAudioSourcePreset = "output" - const val DefaultAudioSourceCustom = "" - const val DefaultAudioDup = false - const val DefaultNoAudioPlayback = false - const val DefaultRequireAudio = false + const val AUDIO_SOURCE_PRESET = "output" + const val AUDIO_SOURCE_CUSTOM = "" + const val AUDIO_DUP = false + const val NO_AUDIO_PLAYBACK = false + const val REQUIRE_AUDIO = false - const val DefaultMaxSizeInput = "" - const val DefaultMaxFpsInput = "" + const val MAX_SIZE_INPUT = "" + const val MAX_FPS_INPUT = "" - const val DefaultVideoEncoder = "" - const val DefaultVideoCodecOptions = "" - const val DefaultAudioEncoder = "" - const val DefaultAudioCodecOptions = "" + const val VIDEO_ENCODER = "" + const val VIDEO_CODEC_OPTION = "" + const val AUDIO_ENCODER = "" + const val AUDIO_CODEC_OPTION = "" - const val DefaultNewDisplayWidth = "" - const val DefaultNewDisplayHeight = "" - const val DefaultNewDisplayDpi = "" + const val NEW_DISPLAY_WIDTH = "" + const val NEW_DISPLAY_HEIGHT = "" + const val NEW_DISPLAY_DPI = "" - const val DefaultCropWidth = "" - const val DefaultCropHeight = "" - const val DefaultCropX = "" - const val DefaultCropY = "" + const val CROP_WIDTH = "" + const val CROP_HEIGHT = "" + const val CROP_X = "" + const val CROP_Y = "" // Settings - const val DefaultThemeBaseIndex = 0 - const val DefaultMonetEnabled = false + const val THEME_BASE_INDEX = 0 + const val MONET = false - const val DefaultFullscreenDebugInfoEnabled = false - const val DefaultShowFullscreenVirtualButtons = true - const val DefaultDevicePreviewCardHeightDp = 320 - const val DefaultKeepScreenOnWhenStreamingEnabled = false - const val DefaultVirtualButtonsOutside = "more,home,back" - const val DefaultVirtualButtonsInMore = "app_switch,menu,notification,volume_up,volume_down,volume_mute,power,screenshot" + const val FULLSCREEN_DEBUG_INFO = false + const val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = true + const val KEEP_SCREEN_ON_WHEN_STREAMING = false + const val DEVICE_PREVIEW_CARD_HEIGHT_DP = 320 + const val VIRTUAL_BUTTONS_OUTSIDE = "more,home,back" + const val VIRTUAL_BUTTONS_IN_MORE = + "app_switch,menu,notification,volume_up,volume_down,volume_mute,power,screenshot" - const val DefaultServerRemotePath = "/data/local/tmp/scrcpy-server.jar" + const val CUSTOM_SERVER_URI = "" - const val DefaultAdbKeyName = "scrcpy" + const val SERVER_REMOTE_PATH = "/data/local/tmp/scrcpy-server.jar" + const val SERVER_REMOTE_PATH_INPUT = "" + + const val ADB_KEY_NAME = "scrcpy" + const val ADB_KEY_NAME_INPUT = "" } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppPreferenceKeys.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppPreferenceKeys.kt index 4702b14..69b4832 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppPreferenceKeys.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppPreferenceKeys.kt @@ -1,59 +1,78 @@ package io.github.miuzarte.scrcpyforandroid.constants object AppPreferenceKeys { - const val PrefsName = "scrcpy_app_prefs" - const val NativeAdbKeyPrefsName = "nativecore_adb_rsa" - const val NativeAdbPrivateKey = "priv" - const val ThemeBaseIndex = "theme_base_index" - const val MonetEnabled = "monet_enabled" - const val FullscreenDebugInfoEnabled = "fullscreen_debug_info_enabled" - const val ShowFullscreenVirtualButtons = "show_fullscreen_virtual_buttons" - const val DevicePreviewCardHeightDp = "device_preview_card_height_dp" - const val KeepScreenOnWhenStreamingEnabled = "keep_screen_on_when_streaming_enabled" - const val VirtualButtonsOutside = "virtual_buttons_outside" - const val VirtualButtonsInMore = "virtual_buttons_in_more" - const val CustomServerUri = "custom_server_uri" - const val ServerRemotePath = "server_remote_path" - const val VideoCodec = "video_codec" - const val AudioEnabled = "audio_enabled" - const val AudioCodec = "audio_codec" - const val PairHost = "pair_host" - const val PairPort = "pair_port" - const val PairCode = "pair_code" - const val QuickConnectInput = "quick_connect_input" - const val BitRateMbps = "bit_rate_mbps" - const val BitRateInput = "bit_rate_input" - const val AudioBitRateKbps = "audio_bit_rate_kbps" - const val MaxSizeInput = "max_size_input" - const val MaxFpsInput = "max_fps_input" - const val NoControl = "no_control" - const val VideoEncoder = "video_encoder" - const val VideoCodecOptions = "video_codec_options" - const val AudioEncoder = "audio_encoder" - const val AudioCodecOptions = "audio_codec_options" - const val AudioDup = "audio_dup" - const val AudioSourcePreset = "audio_source_preset" - const val AudioSourceCustom = "audio_source_custom" - const val VideoSourcePreset = "video_source_preset" - const val CameraIdInput = "camera_id_input" - const val CameraFacingPreset = "camera_facing_preset" - const val CameraSizePreset = "camera_size_preset" - const val CameraSizeCustom = "camera_size_custom" - const val CameraArInput = "camera_ar_input" - const val CameraFpsInput = "camera_fps_input" - const val CameraHighSpeed = "camera_high_speed" - const val NoAudioPlayback = "no_audio_playback" - const val NoVideo = "no_video" - const val RequireAudio = "require_audio" - const val TurnScreenOff = "turn_screen_off" - const val NewDisplayWidth = "new_display_width" - const val NewDisplayHeight = "new_display_height" - const val NewDisplayDpi = "new_display_dpi" - const val DisplayIdInput = "display_id_input" - const val CropWidth = "crop_width" - const val CropHeight = "crop_height" - const val CropX = "crop_x" - const val CropY = "crop_y" - const val AdbKeyName = "adb_key_name" - const val QuickDevices = "quick_devices" + const val PREFS_NAME = "scrcpy_app_prefs" + const val NATIVE_ADB_KEY_PREFS_NAME = "nativecore_adb_rsa" + const val NATIVE_ADB_PRIVATE_KEY = "priv" + + // Devices + const val QUICK_DEVICES = "quick_devices" + const val QUICK_CONNECT_INPUT = "quick_connect_input" + + const val PAIR_HOST = "pair_host" + const val PAIR_PORT = "pair_port" + const val PAIR_CODE = "pair_code" + + const val AUDIO_ENABLED = "audio_enabled" + const val AUDIO_CODEC = "audio_codec" + const val AUDIO_BIT_RATE_INPUT = "audio_bit_rate_input" + const val AUDIO_BIT_RATE_KBPS = "audio_bit_rate_kbps" + const val VIDEO_CODEC = "video_codec" + const val VIDEO_BIT_RATE_MBPS = "video_bit_rate_mbps" + const val VIDEO_BIT_RATE_INPUT = "video_bit_rate_input" + + const val TURN_SCREEN_OFF = "turn_screen_off" + const val NO_CONTROL = "no_control" + const val NO_VIDEO = "no_video" + + const val VIDEO_SOURCE_PRESET = "video_source_preset" + const val DISPLAY_ID = "display_id" + + const val CAMERA_ID = "camera_id" + const val CAMERA_FACING_PRESET = "camera_facing_preset" + const val CAMERA_SIZE_PRESET = "camera_size_preset" + const val CAMERA_SIZE_CUSTOM = "camera_size_custom" + const val CAMERA_AR = "camera_ar" + const val CAMERA_FPS = "camera_fps" + const val CAMERA_HIGH_SPEED = "camera_high_speed" + + const val AUDIO_SOURCE_PRESET = "audio_source_preset" + const val AUDIO_SOURCE_CUSTOM = "audio_source_custom" + const val AUDIO_DUP = "audio_dup" + const val NO_AUDIO_PLAYBACK = "no_audio_playback" + const val REQUIRE_AUDIO = "require_audio" + + const val MAX_SIZE_INPUT = "max_size_input" + const val MAX_FPS_INPUT = "max_fps_input" + + const val VIDEO_ENCODER = "video_encoder" + const val VIDEO_CODEC_OPTION = "video_codec_options" + const val AUDIO_ENCODER = "audio_encoder" + const val AUDIO_CODEC_OPTION = "audio_codec_options" + + const val NEW_DISPLAY_WIDTH = "new_display_width" + const val NEW_DISPLAY_HEIGHT = "new_display_height" + const val NEW_DISPLAY_DPI = "new_display_dpi" + + const val CROP_WIDTH = "crop_width" + const val CROP_HEIGHT = "crop_height" + const val CROP_X = "crop_x" + const val CROP_Y = "crop_y" + + // Settings + const val THEME_BASE_INDEX = "theme_base_index" + const val MONET = "monet" + + const val FULLSCREEN_DEBUG_INFO = "fullscreen_debug_info" + const val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = "show_fullscreen_virtual_buttons" + const val KEEP_SCREEN_ON_WHEN_STREAMING = "keep_screen_on_when_streaming" + const val DEVICE_PREVIEW_CARD_HEIGHT_DP = "device_preview_card_height_dp" + const val VIRTUAL_BUTTONS_OUTSIDE = "virtual_buttons_outside" + const val VIRTUAL_BUTTONS_IN_MORE = "virtual_buttons_in_more" + + const val CUSTOM_SERVER_URI = "custom_server_uri" + + const val SERVER_REMOTE_PATH = "server_remote_path" + + const val ADB_KEY_NAME = "adb_key_name" } \ No newline at end of file diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/UiMotion.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/UiMotion.kt index 2991b2d..d9ec651 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/UiMotion.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/UiMotion.kt @@ -1,6 +1,6 @@ package io.github.miuzarte.scrcpyforandroid.constants object UiMotion { - const val PageSwitchDampingRatio = 0.9f - const val PageSwitchStiffness = 700f + const val PAGE_SWITCH_DAMPING_RATIO = 0.9f + const val PAGE_SWITCH_STIFFNESS = 700f } \ No newline at end of file diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/AnnexBDecoder.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/AnnexBDecoder.kt index 0f9f415..6bb143f 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/AnnexBDecoder.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/AnnexBDecoder.kt @@ -24,6 +24,7 @@ class AnnexBDecoder( private var outCount = 0L private var fpsWindowStartNs = System.nanoTime() private var fpsWindowFrameCount = 0 + @Volatile private var released = false @@ -65,16 +66,26 @@ class AnnexBDecoder( codec.queueInputBuffer(inputIndex, 0, data.size, ptsUs, flags) inCount += 1 if (inCount == 1L || inCount % 180L == 0L || isConfig) { - Log.i(TAG, "feed(): mime=$decoderMime in=$inCount size=${data.size} key=$isKeyFrame cfg=$isConfig pts=$ptsUs") + Log.i( + TAG, + "feed(): mime=$decoderMime in=$inCount size=${data.size} key=$isKeyFrame cfg=$isConfig pts=$ptsUs" + ) } } else { if (isConfig || isKeyFrame) { - Log.w(TAG, "drop critical packet: mime=$decoderMime size=${data.size} key=$isKeyFrame cfg=$isConfig") + Log.w( + TAG, + "drop critical packet: mime=$decoderMime size=${data.size} key=$isKeyFrame cfg=$isConfig" + ) } } drainOutput() }.onFailure { - Log.w(TAG, "feed failed: mime=$decoderMime size=${data.size} key=$isKeyFrame cfg=$isConfig", it) + Log.w( + TAG, + "feed failed: mime=$decoderMime size=${data.size} key=$isKeyFrame cfg=$isConfig", + it + ) } } @@ -108,10 +119,12 @@ class AnnexBDecoder( Log.i(TAG, "drain(): mime=$decoderMime out=$outCount") } } + outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> { notifyOutputSize(codec.outputFormat) continue } + else -> return } } 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 24088bb..1fffb0c 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 @@ -41,23 +41,33 @@ internal class DirectAdbTransport(private val context: Context) { val privateKey: PrivateKey get() = keys.first val publicKeyX509: ByteArray get() = keys.second - @Volatile var keyName: String = AppDefaults.DefaultAdbKeyName + @Volatile + var keyName: String = AppDefaults.ADB_KEY_NAME fun connect(host: String, port: Int): DirectAdbConnection { Log.i(TAG, "connect(): opening direct adbd transport to $host:$port") - val conn = DirectAdbConnection(host, port, privateKey, publicKeyX509, keyName.ifBlank { AppDefaults.DefaultAdbKeyName }) + val conn = DirectAdbConnection( + host, + port, + privateKey, + publicKeyX509, + keyName.ifBlank { AppDefaults.ADB_KEY_NAME }) conn.handshake() Log.i(TAG, "connect(): handshake success for $host:$port") return conn } private fun loadOrCreate(): Pair { - val prefs = context.getSharedPreferences(AppPreferenceKeys.NativeAdbKeyPrefsName, Context.MODE_PRIVATE) - val privB64 = prefs.getString(AppPreferenceKeys.NativeAdbPrivateKey, null) + val prefs = context.getSharedPreferences( + AppPreferenceKeys.NATIVE_ADB_KEY_PREFS_NAME, + Context.MODE_PRIVATE + ) + val privB64 = prefs.getString(AppPreferenceKeys.NATIVE_ADB_PRIVATE_KEY, null) if (privB64 != null) { 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)}") return Pair(priv, pub) @@ -68,8 +78,16 @@ internal class DirectAdbTransport(private val context: Context) { val kpg = KeyPairGenerator.getInstance("RSA") kpg.initialize(2048) val kp = kpg.generateKeyPair() - prefs.edit { putString(AppPreferenceKeys.NativeAdbPrivateKey, Base64.encodeToString(kp.private.encoded, Base64.NO_WRAP)) } - Log.i(TAG, "loadOrCreate(): generated new RSA key pair, fp=${fingerprint(kp.public.encoded)}") + prefs.edit { + putString( + AppPreferenceKeys.NATIVE_ADB_PRIVATE_KEY, + Base64.encodeToString(kp.private.encoded, Base64.NO_WRAP) + ) + } + Log.i( + TAG, + "loadOrCreate(): generated new RSA key pair, fp=${fingerprint(kp.public.encoded)}" + ) return Pair(kp.private, kp.public.encoded) } @@ -96,7 +114,7 @@ internal class DirectAdbConnection( val port: Int, private val privateKey: PrivateKey, private val publicKeyX509: ByteArray, - private val keyName: String = AppDefaults.DefaultAdbKeyName, + private val keyName: String = AppDefaults.ADB_KEY_NAME, ) : AutoCloseable { private val sha1DigestInfoPrefix = byteArrayOf( @@ -122,7 +140,9 @@ internal class DirectAdbConnection( private lateinit var rawOut: OutputStream private val nextLocalId = AtomicInteger(1) private val streams = ConcurrentHashMap() - @Volatile private var closed = false + + @Volatile + private var closed = false private var readerThread: Thread? = null companion object { @@ -171,9 +191,17 @@ internal class DirectAdbConnection( throw IOException("ADB: connection rejected. Please accept the authorisation dialog on the target device.") } } - else -> throw IOException("ADB: unexpected message 0x${afterSign.command.toString(16)} after AUTH_SIGNATURE") + + else -> throw IOException( + "ADB: unexpected message 0x${ + afterSign.command.toString( + 16 + ) + } after AUTH_SIGNATURE" + ) } } + else -> throw IOException("ADB: unexpected initial message 0x${first.command.toString(16)}") } @@ -262,6 +290,7 @@ internal class DirectAdbConnection( sendMsg(A_CLSE, 0, msg.arg0) } } + A_CLSE -> streams.remove(msg.arg1)?.forceClose() A_OPEN -> sendMsg(A_CLSE, 0, msg.arg0) } @@ -275,7 +304,12 @@ internal class DirectAdbConnection( } @Synchronized - private fun sendMsg(command: Int, arg0: Int = 0, arg1: Int = 0, data: ByteArray = ByteArray(0)) { + private fun sendMsg( + command: Int, + arg0: Int = 0, + arg1: Int = 0, + data: ByteArray = ByteArray(0) + ) { val crc = data.fold(0L) { acc, b -> acc + (b.toLong() and 0xFF) }.toInt() val header = ByteBuffer.allocate(24).order(ByteOrder.LITTLE_ENDIAN) .putInt(command).putInt(arg0).putInt(arg1) @@ -296,7 +330,8 @@ internal class DirectAdbConnection( val dataLen = buf.int buf.int buf.int - val data = if (dataLen > 0) ByteArray(dataLen).also { rawIn.readExact(it) } else ByteArray(0) + val data = + if (dataLen > 0) ByteArray(dataLen).also { rawIn.readExact(it) } else ByteArray(0) return AdbMsg(command, arg0, arg1, data) } @@ -391,12 +426,16 @@ internal class AdbSocketStream( private const val A_CLSE = 0x45534c43 } - @Volatile var remoteId: Int = 0 - @Volatile var closed: Boolean = false + @Volatile + var remoteId: Int = 0 + + @Volatile + var closed: Boolean = false private val latch = CountDownLatch(1) private val latchOk = AtomicBoolean(false) private val queue = LinkedBlockingQueue() + private object EndOfStreamMarker val inputStream: InputStream = InStream() @@ -476,6 +515,7 @@ internal class AdbSocketStream( if (len == 0) return sender(A_WRTE, localId, remoteId, b.copyOfRange(off, off + len)) } + override fun flush() {} } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/NativeAdbService.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/NativeAdbService.kt index 0589b49..c00df64 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/NativeAdbService.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/NativeAdbService.kt @@ -7,13 +7,18 @@ import java.nio.file.Path class NativeAdbService(appContext: Context) { private val transport = DirectAdbTransport(appContext) - @Volatile private var connection: DirectAdbConnection? = null - @Volatile private var connectedHost: String? = null - @Volatile private var connectedPort: Int? = null + @Volatile + private var connection: DirectAdbConnection? = null + @Volatile + private var connectedHost: String? = null + @Volatile + private var connectedPort: Int? = null var keyName: String get() = transport.keyName - set(value) { transport.keyName = value } + set(value) { + transport.keyName = value + } @Synchronized fun pair(host: String, port: Int, pairingCode: String): Boolean { diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/ScrcpyAudioPlayer.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/ScrcpyAudioPlayer.kt index d34bf09..5e3ee36 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/ScrcpyAudioPlayer.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/ScrcpyAudioPlayer.kt @@ -4,7 +4,6 @@ import android.media.AudioAttributes import android.media.AudioFormat import android.media.AudioManager import android.media.AudioTrack -import android.os.Build import android.media.MediaCodec import android.media.MediaFormat import android.util.Log @@ -23,15 +22,22 @@ class ScrcpyAudioPlayer(private val codecId: Int) { private var audioTrack: AudioTrack? = null private val bufferInfo = MediaCodec.BufferInfo() - @Volatile private var prepared = false - @Volatile private var released = false + @Volatile + private var prepared = false + @Volatile + private var released = false private var packetCount = 0L fun feedPacket(data: ByteArray, ptsUs: Long, isConfig: Boolean) { if (released) return if (isConfig) { - Log.i(TAG, "feedPacket(): config packet size=${data.size} codec=0x${codecId.toUInt().toString(16)}") + Log.i( + TAG, + "feedPacket(): config packet size=${data.size} codec=0x${ + codecId.toUInt().toString(16) + }" + ) when (codecId) { AUDIO_CODEC_OPUS -> prepareOpus(data) AUDIO_CODEC_AAC -> prepareAac(data) @@ -47,13 +53,7 @@ class ScrcpyAudioPlayer(private val codecId: Int) { } if (codecId == AUDIO_CODEC_RAW) { - ensureRawAudioTrack()?.let { track -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - track.write(data, 0, data.size, AudioTrack.WRITE_NON_BLOCKING) - } else { - track.write(data, 0, data.size) - } - } + ensureRawAudioTrack()?.write(data, 0, data.size, AudioTrack.WRITE_NON_BLOCKING) return } @@ -74,11 +74,16 @@ class ScrcpyAudioPlayer(private val codecId: Int) { private fun prepareOpus(opusHead: ByteArray) { if (prepared || released) return runCatching { - val format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_OPUS, SAMPLE_RATE, CHANNELS) + val format = MediaFormat.createAudioFormat( + MediaFormat.MIMETYPE_AUDIO_OPUS, + SAMPLE_RATE, + CHANNELS + ) format.setByteBuffer("csd-0", ByteBuffer.wrap(opusHead)) // pre-skip field: 2 bytes LE at offset 10 of the OpusHead if (opusHead.size >= 12) { - val preSkip = ((opusHead[11].toInt() and 0xFF) shl 8) or (opusHead[10].toInt() and 0xFF) + val preSkip = + ((opusHead[11].toInt() and 0xFF) shl 8) or (opusHead[10].toInt() and 0xFF) val codecDelayNs = preSkip.toLong() * 1_000_000_000L / SAMPLE_RATE format.setByteBuffer("csd-1", longBuffer(codecDelayNs)) format.setByteBuffer("csd-2", longBuffer(OPUS_SEEK_PREROLL_NS)) @@ -90,7 +95,8 @@ class ScrcpyAudioPlayer(private val codecId: Int) { private fun prepareAac(aacConfig: ByteArray) { if (prepared || released) return runCatching { - val format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, SAMPLE_RATE, CHANNELS) + val format = + MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, SAMPLE_RATE, CHANNELS) format.setByteBuffer("csd-0", ByteBuffer.wrap(aacConfig)) startCodecAndTrack(format) }.onFailure { Log.w(TAG, "prepareAac failed", it) } @@ -99,7 +105,11 @@ class ScrcpyAudioPlayer(private val codecId: Int) { private fun prepareFlac(flacConfig: ByteArray) { if (prepared || released) return runCatching { - val format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_FLAC, SAMPLE_RATE, CHANNELS) + val format = MediaFormat.createAudioFormat( + MediaFormat.MIMETYPE_AUDIO_FLAC, + SAMPLE_RATE, + CHANNELS + ) if (flacConfig.isNotEmpty()) { format.setByteBuffer("csd-0", ByteBuffer.wrap(flacConfig)) } @@ -161,11 +171,7 @@ class ScrcpyAudioPlayer(private val codecId: Int) { val pcm = ByteArray(size) outBuf.position(bufferInfo.offset) outBuf.get(pcm) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - track.write(pcm, 0, size, AudioTrack.WRITE_NON_BLOCKING) - } else { - track.write(pcm, 0, size) - } + track.write(pcm, 0, size, AudioTrack.WRITE_NON_BLOCKING) } codec.releaseOutputBuffer(idx, false) idx = codec.dequeueOutputBuffer(bufferInfo, 0L) @@ -190,9 +196,9 @@ class ScrcpyAudioPlayer(private val codecId: Int) { companion object { private const val TAG = "ScrcpyAudioPlayer" const val AUDIO_CODEC_OPUS = 0x6f707573 - const val AUDIO_CODEC_AAC = 0x00616163 + const val AUDIO_CODEC_AAC = 0x00616163 const val AUDIO_CODEC_FLAC = 0x666c6163 - const val AUDIO_CODEC_RAW = 0x00726177 + const val AUDIO_CODEC_RAW = 0x00726177 private const val SAMPLE_RATE = 48000 private const val CHANNELS = 2 private const val CODEC_TIMEOUT_US = 10_000L diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/ScrcpySessionManager.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/ScrcpySessionManager.kt index 0405161..deb2af6 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/ScrcpySessionManager.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/ScrcpySessionManager.kt @@ -11,21 +11,25 @@ import java.io.InputStreamReader import java.nio.file.Path import java.security.SecureRandom import java.util.ArrayDeque -import java.util.LinkedHashSet import kotlin.concurrent.thread import kotlin.math.roundToInt class ScrcpySessionManager(private val adbService: NativeAdbService) { @Volatile private var activeSession: ActiveSession? = null + @Volatile private var videoConsumer: ((VideoPacket) -> Unit)? = null + @Volatile private var videoReaderThread: Thread? = null + @Volatile private var audioConsumer: ((AudioPacket) -> Unit)? = null + @Volatile private var audioReaderThread: Thread? = null + @Volatile private var lastServerCommand: String? = null private val serverLogBuffer = ArrayDeque() @@ -44,7 +48,10 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { adbService.push(serverJarPath, targetPath) val cmd = buildServerCommand(targetPath, scid, options) lastServerCommand = cmd - Log.i(TAG, "start(): socket=$socketName codec=${options.videoCodec} audio=${options.audio} audioCodec=${options.audioCodec}") + Log.i( + TAG, + "start(): socket=$socketName codec=${options.videoCodec} audio=${options.audio} audioCodec=${options.audioCodec}" + ) val serverStream = adbService.openShellStream(cmd) val serverLogThread = startServerLogThread(serverStream, socketName) Thread.sleep(SERVER_BOOT_DELAY_MS) @@ -64,13 +71,16 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { videoStream = firstStream videoInput = firstInput } + options.audio -> { audioStream = firstStream audioInput = firstInput } + options.control -> { controlStream = firstStream } + else -> { throw IllegalArgumentException("At least one of video/audio/control must be enabled") } @@ -128,11 +138,15 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { audioStream = audioStream, audioInput = audioInput, controlStream = controlStream, - controlWriter = controlStream?.outputStream?.let { ScrcpyControlWriter(DataOutputStream(it)) }, + controlWriter = controlStream?.outputStream?.let { + ScrcpyControlWriter( + DataOutputStream(it) + ) + }, ) return sessionInfo } catch (t: Throwable) { - val tail = snapshotServerLogs(maxLines = 120) + val tail = snapshotServerLogs() val detail = if (tail.isBlank()) "" else " | server_log_tail=\n$tail" throw IllegalStateException("scrcpy start failed: ${t.message}$detail", t) } @@ -173,7 +187,7 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { ) } catch (_: EOFException) { break - } catch (e: InterruptedException) { + } catch (_: InterruptedException) { // Ignore transient interrupts while session remains active. if (activeSession !== session || vStream.closed) { break @@ -212,10 +226,12 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { Log.w(TAG, "audio disabled by server") return@thread } + AUDIO_ERROR -> { Log.e(TAG, "audio stream configuration error from server") return@thread } + else -> { Log.i(TAG, "audio stream codec=0x${streamCodecId.toUInt().toString(16)}") } @@ -223,7 +239,9 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { if (session.info.audioCodecId != 0 && streamCodecId != session.info.audioCodecId) { Log.w( TAG, - "audio codec mismatch: requested=0x${session.info.audioCodecId.toUInt().toString(16)} stream=0x${streamCodecId.toUInt().toString(16)}", + "audio codec mismatch: requested=0x${ + session.info.audioCodecId.toUInt().toString(16) + } stream=0x${streamCodecId.toUInt().toString(16)}", ) } @@ -238,10 +256,16 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { val isConfig = (ptsAndFlags and PACKET_FLAG_CONFIG) != 0L val ptsUs = ptsAndFlags and PACKET_PTS_MASK - audioConsumer?.invoke(AudioPacket(data = payload, ptsUs = ptsUs, isConfig = isConfig)) + audioConsumer?.invoke( + AudioPacket( + data = payload, + ptsUs = ptsUs, + isConfig = isConfig + ) + ) } catch (_: EOFException) { break - } catch (e: InterruptedException) { + } catch (_: InterruptedException) { if (activeSession !== session || aStream.closed) break Thread.interrupted() } catch (e: Exception) { @@ -279,12 +303,38 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { actionButton: Int, buttons: Int, ) { - requireControlWriter().injectTouch(action, pointerId, x, y, screenWidth, screenHeight, pressure, actionButton, buttons) + requireControlWriter().injectTouch( + action, + pointerId, + x, + y, + screenWidth, + screenHeight, + pressure, + actionButton, + buttons + ) } @Synchronized - fun injectScroll(x: Int, y: Int, screenWidth: Int, screenHeight: Int, hScroll: Float, vScroll: Float, buttons: Int) { - requireControlWriter().injectScroll(x, y, screenWidth, screenHeight, hScroll, vScroll, buttons) + fun injectScroll( + x: Int, + y: Int, + screenWidth: Int, + screenHeight: Int, + hScroll: Float, + vScroll: Float, + buttons: Int + ) { + requireControlWriter().injectScroll( + x, + y, + screenWidth, + screenHeight, + hScroll, + vScroll, + buttons + ) } @Synchronized @@ -384,10 +434,15 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { } private fun requireControlWriter(): ScrcpyControlWriter { - return activeSession?.controlWriter ?: throw IllegalStateException("scrcpy control channel not available") + return activeSession?.controlWriter + ?: throw IllegalStateException("scrcpy control channel not available") } - private fun buildServerCommand(targetPath: String, scid: Int, options: ScrcpyStartOptions): String { + private fun buildServerCommand( + targetPath: String, + scid: Int, + options: ScrcpyStartOptions + ): String { val serverArgs = buildServerArgs(scid, options) return "CLASSPATH=$targetPath app_process / com.genymobile.scrcpy.Server ${options.serverVersion} $serverArgs" } @@ -474,7 +529,7 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { ServerArg( type = ServerArgType.NUMBER, key = "video_bit_rate", - value = options.videoBitRate, zeroValueBehavior = ZeroValueBehavior.KEEP, + value = options.videoBitRate, ), ServerArg( type = ServerArgType.NUMBER, @@ -528,7 +583,6 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { key = "audio_bit_rate", value = options.audioBitRate, includeWhen = options.audio, - zeroValueBehavior = ZeroValueBehavior.KEEP, ), ServerArg( type = ServerArgType.STRING, @@ -539,7 +593,8 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { ServerArg( type = ServerArgType.BOOLEAN, key = "audio_dup", - value = options.audioDup, includeWhen = options.audioDup, + value = options.audioDup, + includeWhen = options.audioDup, ), ServerArg( type = ServerArgType.STRING, @@ -579,15 +634,22 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { ServerArg( type = ServerArgType.BOOLEAN, key = "list_encoders", - value = options.listEncoders, includeWhen = options.listEncoders, + value = options.listEncoders, + includeWhen = options.listEncoders, ), ServerArg( type = ServerArgType.BOOLEAN, key = "list_camera_sizes", - value = options.listCameraSizes, includeWhen = options.listCameraSizes, + value = options.listCameraSizes, + includeWhen = options.listCameraSizes, ), // Reserved for future args that require repeated/list values. - // ServerArg(type = ServerArgType.LIST, key = "_unused_list", value = emptyList(), includeWhen = false), + // ServerArg( + // type = ServerArgType.LIST, + // key = "_unused_list", + // value = emptyList(), + // includeWhen = false, + // ), ) val rendered = args.mapNotNull { it.render() } @@ -608,8 +670,8 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { } private enum class ZeroValueBehavior { - KEEP, OMIT, + KEEP, } private data class ServerArg( @@ -658,7 +720,7 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { ?.filter { it.isNotBlank() } .orEmpty() if (items.isEmpty()) return null - return "$key=${items.joinToString(",")}" + return "$key=${items.joinToString(",")}" } } @@ -716,7 +778,12 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { private fun startServerLogThread(serverStream: AdbSocketStream, socketName: String): Thread { return thread(start = true, name = "scrcpy-server-log") { try { - BufferedReader(InputStreamReader(serverStream.inputStream, Charsets.UTF_8)).use { reader -> + BufferedReader( + InputStreamReader( + serverStream.inputStream, + Charsets.UTF_8 + ) + ).use { reader -> while (true) { val line = reader.readLine() ?: break appendServerLog(line) @@ -740,7 +807,7 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { } @Synchronized - private fun snapshotServerLogs(maxLines: Int): String { + private fun snapshotServerLogs(maxLines: Int = 120): String { if (serverLogBuffer.isEmpty()) { return "" } @@ -748,7 +815,10 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { return serverLogBuffer.toList().takeLast(take).joinToString("\n") } - private fun openAbstractSocketWithRetry(socketName: String, expectDummyByte: Boolean): AdbSocketStream { + private fun openAbstractSocketWithRetry( + socketName: String, + expectDummyByte: Boolean + ): AdbSocketStream { var lastEx: Exception? = null repeat(CONNECT_RETRY_COUNT) { attempt -> try { @@ -963,7 +1033,15 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { } @Synchronized - fun injectScroll(x: Int, y: Int, screenWidth: Int, screenHeight: Int, hScroll: Float, vScroll: Float, buttons: Int) { + fun injectScroll( + x: Int, + y: Int, + screenWidth: Int, + screenHeight: Int, + hScroll: Float, + vScroll: Float, + buttons: Int + ) { output.writeByte(TYPE_INJECT_SCROLL_EVENT) writePosition(x, y, screenWidth, screenHeight) output.writeShort(encodeSignedFixedPoint16(hScroll / 16f)) @@ -1033,6 +1111,7 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { private const val AUDIO_CODEC_AAC = 0x00616163 private const val AUDIO_CODEC_FLAC = 0x666c6163 private const val AUDIO_CODEC_RAW = 0x00726177 + // Audio stream disable codes from server (writeDisableStream) private const val AUDIO_DISABLED = 0 private const val AUDIO_ERROR = 1 @@ -1044,10 +1123,12 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) { private const val TYPE_SET_DISPLAY_POWER = 10 private val VIDEO_ENCODER_REGEX = Regex("--video-encoder=([\\w.\\-]+)") private val AUDIO_ENCODER_REGEX = Regex("--audio-encoder=([\\w.\\-]+)") - private val VIDEO_ENCODER_FALLBACK_REGEX = Regex("""--video-encoder=['\"]?([^'\"\s]+)""") - private val AUDIO_ENCODER_FALLBACK_REGEX = Regex("""--audio-encoder=['\"]?([^'\"\s]+)""") - private val VIDEO_ENCODER_TYPE_REGEX = Regex("""--video-codec=[^\s]+\s+--video-encoder=([^\s]+).*?\((hw|sw)\)""") - private val AUDIO_ENCODER_TYPE_REGEX = Regex("""--audio-codec=[^\s]+\s+--audio-encoder=([^\s]+).*?\((hw|sw)\)""") + private val VIDEO_ENCODER_FALLBACK_REGEX = Regex("""--video-encoder=['"]?([^'"\s]+)""") + private val AUDIO_ENCODER_FALLBACK_REGEX = Regex("""--audio-encoder=['"]?([^'"\s]+)""") + private val VIDEO_ENCODER_TYPE_REGEX = + Regex("""--video-codec=\S+\s+--video-encoder=(\S+).*?\((hw|sw)\)""") + private val AUDIO_ENCODER_TYPE_REGEX = + Regex("""--audio-codec=\S+\s+--audio-encoder=(\S+).*?\((hw|sw)\)""") private val CAMERA_SIZE_REGEX = Regex("--camera-size=([0-9]+x[0-9]+)") private val CAMERA_SIZE_FALLBACK_REGEX = Regex("\\b([1-9][0-9]{1,4}x[1-9][0-9]{1,4})\\b") 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 2484790..e0db43a 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 @@ -1,7 +1,6 @@ package io.github.miuzarte.scrcpyforandroid.pages import androidx.compose.foundation.layout.Arrangement -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -12,6 +11,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.platform.LocalFocusManager @@ -22,11 +22,10 @@ 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 kotlin.math.roundToInt import kotlinx.coroutines.launch import top.yukonga.miuix.kmp.basic.Card -import top.yukonga.miuix.kmp.basic.SnackbarHostState import top.yukonga.miuix.kmp.basic.ScrollBehavior +import top.yukonga.miuix.kmp.basic.SnackbarHostState import top.yukonga.miuix.kmp.basic.SpinnerEntry import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TextField @@ -34,6 +33,7 @@ import top.yukonga.miuix.kmp.extra.SuperArrow import top.yukonga.miuix.kmp.extra.SuperDropdown import top.yukonga.miuix.kmp.extra.SuperSpinner import top.yukonga.miuix.kmp.extra.SuperSwitch +import kotlin.math.roundToInt private val AUDIO_SOURCE_OPTIONS = listOf( "output" to "output", @@ -142,15 +142,20 @@ internal fun AdvancedConfigPage( ) { val focusManager = LocalFocusManager.current val scope = rememberCoroutineScope() - val maxSizePresetIndex = presetIndexFromInputForAdvancedPage(maxSizeInput, ScrcpyPresets.MaxSize) + val maxSizePresetIndex = + presetIndexFromInputForAdvancedPage(maxSizeInput, ScrcpyPresets.MaxSize) val maxFpsPresetIndex = presetIndexFromInputForAdvancedPage(maxFpsInput, ScrcpyPresets.MaxFPS) val audioSourceItems = AUDIO_SOURCE_OPTIONS.map { it.second } - val audioSourceIndex = AUDIO_SOURCE_OPTIONS.indexOfFirst { it.first == audioSourcePreset }.let { if (it >= 0) it else 0 } + val audioSourceIndex = AUDIO_SOURCE_OPTIONS.indexOfFirst { it.first == audioSourcePreset } + .let { if (it >= 0) it else 0 } val videoSourceItems = VIDEO_SOURCE_OPTIONS.map { it.second } - val videoSourceIndex = VIDEO_SOURCE_OPTIONS.indexOfFirst { it.first == videoSourcePreset }.let { if (it >= 0) it else 0 } + val videoSourceIndex = VIDEO_SOURCE_OPTIONS.indexOfFirst { it.first == videoSourcePreset } + .let { if (it >= 0) it else 0 } val cameraFacingItems = CAMERA_FACING_OPTIONS.map { it.second } - val cameraFacingIndex = CAMERA_FACING_OPTIONS.indexOfFirst { it.first == cameraFacingPreset }.let { if (it >= 0) it else 0 } - val cameraFpsPresetIndex = presetIndexFromInputForAdvancedPage(cameraFpsInput, CAMERA_FPS_PRESETS) + val cameraFacingIndex = CAMERA_FACING_OPTIONS.indexOfFirst { it.first == cameraFacingPreset } + .let { if (it >= 0) it else 0 } + val cameraFpsPresetIndex = + presetIndexFromInputForAdvancedPage(cameraFpsInput, CAMERA_FPS_PRESETS) val videoEncoderEntries = videoEncoderDropdownItems.map { encoderName -> if (encoderName == "默认") { SpinnerEntry(title = encoderName) @@ -269,7 +274,10 @@ internal fun AdvancedConfigPage( title = "摄像头分辨率", summary = "--camera-size", items = cameraSizeDropdownItems, - selectedIndex = cameraSizeIndex.coerceIn(0, (cameraSizeDropdownItems.size - 1).coerceAtLeast(0)), + selectedIndex = cameraSizeIndex.coerceIn( + 0, + (cameraSizeDropdownItems.size - 1).coerceAtLeast(0) + ), onSelectedIndexChange = { index -> onCameraSizePresetChange( when (index) { 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 eefb46d..81ac392 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 @@ -5,56 +5,48 @@ import android.app.Activity import android.util.Log import android.view.WindowManager import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Modifier -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.Alignment -import androidx.compose.ui.unit.sp -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.DragIndicator import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade import io.github.miuzarte.scrcpyforandroid.ScrcpySessionInfo import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing +import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut -import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics -import io.github.miuzarte.scrcpyforandroid.services.fetchConnectedDeviceInfo +import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn import io.github.miuzarte.scrcpyforandroid.services.DevicePageSettings -import io.github.miuzarte.scrcpyforandroid.services.loadQuickDevices +import io.github.miuzarte.scrcpyforandroid.services.fetchConnectedDeviceInfo import io.github.miuzarte.scrcpyforandroid.services.loadDevicePageSettings +import io.github.miuzarte.scrcpyforandroid.services.loadQuickDevices import io.github.miuzarte.scrcpyforandroid.services.parseQuickTarget -import io.github.miuzarte.scrcpyforandroid.services.saveQuickDevices import io.github.miuzarte.scrcpyforandroid.services.saveDevicePageSettings +import io.github.miuzarte.scrcpyforandroid.services.saveQuickDevices import io.github.miuzarte.scrcpyforandroid.services.updateQuickDeviceNameIfEmpty import io.github.miuzarte.scrcpyforandroid.services.upsertQuickDevice -import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn import io.github.miuzarte.scrcpyforandroid.widgets.ConfigPanel -import io.github.miuzarte.scrcpyforandroid.widgets.PairingCard import io.github.miuzarte.scrcpyforandroid.widgets.DeviceEditorScreen import io.github.miuzarte.scrcpyforandroid.widgets.DeviceTile import io.github.miuzarte.scrcpyforandroid.widgets.LogsPanel +import io.github.miuzarte.scrcpyforandroid.widgets.PairingCard import io.github.miuzarte.scrcpyforandroid.widgets.PreviewCard import io.github.miuzarte.scrcpyforandroid.widgets.QuickConnectCard import io.github.miuzarte.scrcpyforandroid.widgets.SectionSmallTitle @@ -66,9 +58,6 @@ import io.github.miuzarte.scrcpyforandroid.widgets.StatusCard import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonCard -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.delay @@ -77,11 +66,10 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import top.yukonga.miuix.kmp.basic.ScrollBehavior import top.yukonga.miuix.kmp.basic.SnackbarHostState -import top.yukonga.miuix.kmp.basic.Card -import top.yukonga.miuix.kmp.basic.Icon -import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.extra.SuperBottomSheet -import top.yukonga.miuix.kmp.theme.MiuixTheme +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale import kotlin.math.roundToInt private const val ADB_CONNECT_TIMEOUT_MS = 3_000L @@ -90,38 +78,40 @@ private const val ADB_KEEPALIVE_TIMEOUT_MS = 2_000L private const val DEVICE_SHORTCUT_SEPARATOR = "\u001F" private const val LOG_TAG = "DevicePage" -private val DeviceShortcutStateListSaver = listSaver, String>( - save = { list -> - list.map { item -> - listOf( - item.id, - item.name, - item.host, - item.port.toString(), - if (item.online) "1" else "0", - ).joinToString(DEVICE_SHORTCUT_SEPARATOR) - } - }, - restore = { saved -> - saved.mapNotNull { line -> - val parts = line.split(DEVICE_SHORTCUT_SEPARATOR) - if (parts.size != 5) return@mapNotNull null - val port = parts[3].toIntOrNull() ?: return@mapNotNull null - DeviceShortcut( - id = parts[0], - name = parts[1], - host = parts[2], - port = port, - online = parts[4] == "1", - ) - }.toMutableStateList() - }, -) +private val DeviceShortcutStateListSaver = + listSaver, String>( + save = { list -> + list.map { item -> + listOf( + item.id, + item.name, + item.host, + item.port.toString(), + if (item.online) "1" else "0", + ).joinToString(DEVICE_SHORTCUT_SEPARATOR) + } + }, + restore = { saved -> + saved.mapNotNull { line -> + val parts = line.split(DEVICE_SHORTCUT_SEPARATOR) + if (parts.size != 5) return@mapNotNull null + val port = parts[3].toIntOrNull() ?: return@mapNotNull null + DeviceShortcut( + id = parts[0], + name = parts[1], + host = parts[2], + port = port, + online = parts[4] == "1", + ) + }.toMutableStateList() + }, + ) -private val StringStateListSaver = listSaver, String>( - save = { it.toList() }, - restore = { it.toMutableStateList() }, -) +private val StringStateListSaver = + listSaver, String>( + save = { it.toList() }, + restore = { it.toMutableStateList() }, + ) @Composable fun DeviceTabScreen( @@ -231,7 +221,7 @@ fun DeviceTabScreen( var statusLine by rememberSaveable { mutableStateOf("未连接") } var adbConnected by rememberSaveable { mutableStateOf(false) } var currentTargetHost by rememberSaveable { mutableStateOf("") } - var currentTargetPort by rememberSaveable { mutableIntStateOf(AppDefaults.DefaultAdbPort) } + var currentTargetPort by rememberSaveable { mutableIntStateOf(AppDefaults.ADB_PORT) } var connectedDeviceLabel by rememberSaveable { mutableStateOf("未连接") } var sessionInfoWidth by rememberSaveable { mutableIntStateOf(0) } var sessionInfoHeight by rememberSaveable { mutableIntStateOf(0) } @@ -241,7 +231,13 @@ fun DeviceTabScreen( var sessionInfo by remember { mutableStateOf(null) } - LaunchedEffect(sessionInfoWidth, sessionInfoHeight, sessionInfoDeviceName, sessionInfoCodec, sessionInfoControlEnabled) { + LaunchedEffect( + sessionInfoWidth, + sessionInfoHeight, + sessionInfoDeviceName, + sessionInfoCodec, + sessionInfoControlEnabled + ) { sessionInfo = if (sessionInfoDeviceName.isNotBlank()) { ScrcpySessionInfo( width = sessionInfoWidth, @@ -261,18 +257,22 @@ fun DeviceTabScreen( var adbConnecting by rememberSaveable { mutableStateOf(false) } var connectHost by rememberSaveable { mutableStateOf("") } - var connectPort by rememberSaveable { mutableStateOf(AppDefaults.DefaultAdbPort.toString()) } + var connectPort by rememberSaveable { mutableStateOf(AppDefaults.ADB_PORT.toString()) } var quickConnectInput by rememberSaveable { mutableStateOf(initialSettings.quickConnectInput) } var audioForwardingSupported by rememberSaveable { mutableStateOf(true) } var cameraMirroringSupported by rememberSaveable { mutableStateOf(true) } - var bitRateMbps by rememberSaveable { mutableFloatStateOf(initialSettings.bitRateMbps) } - var bitRateInput by rememberSaveable { mutableStateOf(initialSettings.bitRateInput) } + var bitRateMbps by rememberSaveable { mutableFloatStateOf(initialSettings.videoBitRateMbps) } + var bitRateInput by rememberSaveable { mutableStateOf(initialSettings.videoBitRateInput) } var audioBitRateKbps by rememberSaveable { mutableIntStateOf(initialSettings.audioBitRateKbps) } - val currentTarget = if (currentTargetHost.isNotBlank()) ConnectionTarget(currentTargetHost, currentTargetPort) else null + val currentTarget = if (currentTargetHost.isNotBlank()) ConnectionTarget( + currentTargetHost, + currentTargetPort + ) else null val eventLog = rememberSaveable(saver = StringStateListSaver) { mutableStateListOf() } - val quickDevices = rememberSaveable(saver = DeviceShortcutStateListSaver) { mutableStateListOf() } + val quickDevices = + rememberSaveable(saver = DeviceShortcutStateListSaver) { mutableStateListOf() } LaunchedEffect(eventLog.size) { onCanClearLogsChange(eventLog.isNotEmpty()) @@ -282,13 +282,25 @@ fun DeviceTabScreen( val timestamp = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date()) val line = "[$timestamp] $message" eventLog.add(0, line) - if (eventLog.size > AppDefaults.MaxEventLogLines) { - eventLog.removeRange(AppDefaults.MaxEventLogLines, eventLog.size) + if (eventLog.size > AppDefaults.EVENT_LOG_LINES) { + eventLog.removeRange(AppDefaults.EVENT_LOG_LINES, eventLog.size) } when (level) { - Log.ERROR -> if (error != null) Log.e(LOG_TAG, message, error) else Log.e(LOG_TAG, message) - Log.WARN -> if (error != null) Log.w(LOG_TAG, message, error) else Log.w(LOG_TAG, message) - Log.DEBUG -> if (error != null) Log.d(LOG_TAG, message, error) else Log.d(LOG_TAG, message) + Log.ERROR -> if (error != null) Log.e(LOG_TAG, message, error) else Log.e( + LOG_TAG, + message + ) + + Log.WARN -> if (error != null) Log.w(LOG_TAG, message, error) else Log.w( + LOG_TAG, + message + ) + + Log.DEBUG -> if (error != null) Log.d(LOG_TAG, message, error) else Log.d( + LOG_TAG, + message + ) + else -> if (error != null) Log.i(LOG_TAG, message, error) else Log.i(LOG_TAG, message) } } @@ -298,13 +310,19 @@ fun DeviceTabScreen( audioForwardingSupported = audioSupported if (!audioSupported && audioEnabled) { onAudioEnabledChange(false) - logEvent("设备 Android ${release.ifBlank { "?" }} (SDK $sdkInt) 不支持音频转发,已自动关闭", Log.WARN) + logEvent( + "设备 Android ${release.ifBlank { "?" }} (SDK $sdkInt) 不支持音频转发,已自动关闭", + Log.WARN + ) } val cameraSupported = sdkInt !in 0..<31 cameraMirroringSupported = cameraSupported if (!cameraSupported && videoSourcePreset == "camera") { onVideoSourcePresetChange("display") - logEvent("设备 Android ${release.ifBlank { "?" }} (SDK $sdkInt) 不支持 camera mirroring,已切换为 display", Log.WARN) + logEvent( + "设备 Android ${release.ifBlank { "?" }} (SDK $sdkInt) 不支持 camera mirroring,已切换为 display", + Log.WARN + ) } } @@ -377,7 +395,7 @@ fun DeviceTabScreen( fun refreshEncoderLists() { if (!adbConnected) return - val remotePath = serverRemotePath.trim().ifBlank { AppDefaults.DefaultServerRemotePath } + val remotePath = serverRemotePath.trim().ifBlank { AppDefaults.SERVER_REMOTE_PATH } runCatching { nativeCore.scrcpyListEncoders( customServerUri = customServerUri, @@ -413,7 +431,7 @@ fun DeviceTabScreen( fun refreshCameraSizeLists() { if (!adbConnected) return - val remotePath = serverRemotePath.trim().ifBlank { AppDefaults.DefaultServerRemotePath } + val remotePath = serverRemotePath.trim().ifBlank { AppDefaults.SERVER_REMOTE_PATH } runCatching { nativeCore.scrcpyListCameraSizes( customServerUri = customServerUri, @@ -470,20 +488,14 @@ fun DeviceTabScreen( LaunchedEffect( quickConnectInput, + audioBitRateKbps, bitRateMbps, bitRateInput, - audioBitRateKbps, - maxSizeInput, - maxFpsInput, + turnScreenOff, noControl, - videoEncoder, - videoCodecOptions, - audioEncoder, - audioCodecOptions, - audioDup, - audioSourcePreset, - audioSourceCustom, + noVideo, videoSourcePreset, + displayIdInput, cameraIdInput, cameraFacingPreset, cameraSizePreset, @@ -491,14 +503,20 @@ fun DeviceTabScreen( cameraArInput, cameraFpsInput, cameraHighSpeed, + audioSourcePreset, + audioSourceCustom, + audioDup, noAudioPlayback, - noVideo, requireAudio, - turnScreenOff, + maxSizeInput, + maxFpsInput, + videoEncoder, + videoCodecOptions, + audioEncoder, + audioCodecOptions, newDisplayWidth, newDisplayHeight, newDisplayDpi, - displayIdInput, cropWidth, cropHeight, cropX, @@ -508,35 +526,36 @@ fun DeviceTabScreen( context, DevicePageSettings( quickConnectInput = quickConnectInput, - bitRateMbps = bitRateMbps, - bitRateInput = bitRateInput, audioBitRateKbps = audioBitRateKbps, - maxSizeInput = maxSizeInput, - maxFpsInput = maxFpsInput, + audioBitRateInput = audioBitRateKbps.toString(), + videoBitRateMbps = bitRateMbps, + videoBitRateInput = bitRateInput, + turnScreenOff = turnScreenOff, noControl = noControl, - videoEncoder = videoEncoder, - videoCodecOptions = videoCodecOptions, - audioEncoder = audioEncoder, - audioCodecOptions = audioCodecOptions, - audioDup = audioDup, - audioSourcePreset = audioSourcePreset, - audioSourceCustom = audioSourceCustom, + noVideo = noVideo, videoSourcePreset = videoSourcePreset, + displayIdInput = displayIdInput, cameraIdInput = cameraIdInput, cameraFacingPreset = cameraFacingPreset, cameraSizePreset = cameraSizePreset, cameraSizeCustom = cameraSizeCustom, - cameraArInput = cameraArInput, - cameraFpsInput = cameraFpsInput, + cameraAr = cameraArInput, + cameraFps = cameraFpsInput, cameraHighSpeed = cameraHighSpeed, + audioSourcePreset = audioSourcePreset, + audioSourceCustom = audioSourceCustom, + audioDup = audioDup, noAudioPlayback = noAudioPlayback, - noVideo = noVideo, requireAudio = requireAudio, - turnScreenOff = turnScreenOff, + maxSizeInput = maxSizeInput, + maxFpsInput = maxFpsInput, + videoEncoder = videoEncoder, + videoCodecOptions = videoCodecOptions, + audioEncoder = audioEncoder, + audioCodecOptions = audioCodecOptions, newDisplayWidth = newDisplayWidth, newDisplayHeight = newDisplayHeight, newDisplayDpi = newDisplayDpi, - displayIdInput = displayIdInput, cropWidth = cropWidth, cropHeight = cropHeight, cropX = cropX, @@ -747,7 +766,8 @@ fun DeviceTabScreen( itemsIndexed(quickDevices, key = { _, device -> device.id }) { _, device -> val host = device.host val port = device.port - val isConnectedTarget = adbConnected && currentTarget?.host == host && currentTarget.port == port + val isConnectedTarget = + adbConnected && currentTarget?.host == host && currentTarget.port == port DeviceTile( device = device, @@ -768,7 +788,7 @@ fun DeviceTabScreen( nativeCore.adbDisconnect() adbConnected = false currentTargetHost = "" - currentTargetPort = AppDefaults.DefaultAdbPort + currentTargetPort = AppDefaults.ADB_PORT audioForwardingSupported = true cameraMirroringSupported = true sessionInfo = null @@ -809,7 +829,13 @@ fun DeviceTabScreen( enabled = !adbConnecting, onAddDevice = { val target = parseQuickTarget(quickConnectInput) ?: return@QuickConnectCard - upsertQuickDevice(context, quickDevices, target.host, target.port, online = false) + upsertQuickDevice( + context, + quickDevices, + target.host, + target.port, + online = false + ) scope.launch { snack.showSnackbar("已添加设备: ${target.host}:${target.port}") } @@ -840,10 +866,13 @@ fun DeviceTabScreen( runBusy("执行配对") { val ok = nativeCore.adbPair( host.trim(), - port.toIntOrNull() ?: AppDefaults.DefaultAdbPort, + port.toIntOrNull() ?: AppDefaults.ADB_PORT, code.trim(), ) - logEvent(if (ok) "配对成功" else "配对失败", if (ok) Log.INFO else Log.ERROR) + logEvent( + if (ok) "配对成功" else "配对失败", + if (ok) Log.INFO else Log.ERROR + ) scope.launch { snack.showSnackbar(if (ok) "配对成功" else "配对失败") } 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 b96c365..8257e79 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 @@ -2,8 +2,8 @@ package io.github.miuzarte.scrcpyforandroid.pages import android.app.Activity import android.content.pm.ActivityInfo -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable @@ -21,8 +21,8 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade import io.github.miuzarte.scrcpyforandroid.ScrcpySessionInfo -import io.github.miuzarte.scrcpyforandroid.widgets.FullscreenControlScreen import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics +import io.github.miuzarte.scrcpyforandroid.widgets.FullscreenControlScreen import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonBar import top.yukonga.miuix.kmp.basic.Scaffold @@ -56,7 +56,9 @@ fun FullscreenControlPage( moreActions = virtualButtonLayout.second, ) } - val initialOrientation = remember(activity) { activity?.requestedOrientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } + val initialOrientation = remember(activity) { + activity?.requestedOrientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + } var session by remember(launch) { mutableStateOf( ScrcpySessionInfo( @@ -88,12 +90,15 @@ fun FullscreenControlPage( WindowCompat.setDecorFitsSystemWindows(window, false) val controller = WindowInsetsControllerCompat(window, window.decorView) controller.hide(WindowInsetsCompat.Type.systemBars()) - controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + controller.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE } onDispose { val restoreWindow = activity?.window if (restoreWindow != null) { - WindowInsetsControllerCompat(restoreWindow, restoreWindow.decorView).show(WindowInsetsCompat.Type.systemBars()) + WindowInsetsControllerCompat(restoreWindow, restoreWindow.decorView).show( + WindowInsetsCompat.Type.systemBars() + ) WindowCompat.setDecorFitsSystemWindows(restoreWindow, true) } } 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 c1f50ba..3085b42 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 @@ -6,8 +6,8 @@ import androidx.activity.compose.PredictiveBackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.core.spring -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState @@ -34,7 +34,6 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberDecoratedNavEntries @@ -92,14 +91,18 @@ fun MainPage() { val initialDeviceSettings = remember(context) { loadDevicePageSettings(context) } val snackHostState = remember { SnackbarHostState() } val tabs = remember { MainTabDestination.entries } - val pagerState = rememberPagerState(initialPage = MainTabDestination.Device.ordinal, pageCount = { tabs.size }) + val pagerState = rememberPagerState( + initialPage = MainTabDestination.Device.ordinal, + pageCount = { tabs.size }) val currentTab = tabs[pagerState.currentPage] val saveableStateHolder = rememberSaveableStateHolder() val scope = rememberCoroutineScope() val rootBackStack = remember { mutableStateListOf(RootScreen.Home) } val currentRootScreen = rootBackStack.lastOrNull() as? RootScreen ?: RootScreen.Home - val deviceScrollBehavior = MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Device }) - val settingsScrollBehavior = MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Settings }) + val deviceScrollBehavior = + MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Device }) + val settingsScrollBehavior = + MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Settings }) val advancedScrollBehavior = MiuixScrollBehavior( canScroll = { currentRootScreen is RootScreen.Advanced || currentRootScreen is RootScreen.VirtualButtonOrder @@ -110,27 +113,23 @@ fun MainPage() { restore = { restored -> restored.toList() }, ) + var audioEnabled by rememberSaveable { mutableStateOf(initialSettings.audioEnabled) } + var audioCodec by rememberSaveable { mutableStateOf(initialSettings.audioCodec) } + var videoCodec by rememberSaveable { mutableStateOf(initialSettings.videoCodec) } var themeBaseIndex by rememberSaveable { mutableIntStateOf(initialSettings.themeBaseIndex) } var monetEnabled by rememberSaveable { mutableStateOf(initialSettings.monetEnabled) } var fullscreenDebugInfoEnabled by rememberSaveable { mutableStateOf(initialSettings.fullscreenDebugInfoEnabled) } - var showFullscreenVirtualButtons by rememberSaveable { - mutableStateOf(initialSettings.showFullscreenVirtualButtons) - } - var keepScreenOnWhenStreamingEnabled by rememberSaveable { - mutableStateOf(initialSettings.keepScreenOnWhenStreamingEnabled) - } + var showFullscreenVirtualButtons by rememberSaveable { mutableStateOf(initialSettings.showFullscreenVirtualButtons) } + var keepScreenOnWhenStreamingEnabled by rememberSaveable { mutableStateOf(initialSettings.keepScreenOnWhenStreamingEnabled) } + var devicePreviewCardHeightDp by rememberSaveable { mutableIntStateOf(initialSettings.devicePreviewCardHeightDp) } var virtualButtonsOutside by rememberSaveable(stateSaver = stringListSaver) { mutableStateOf(VirtualButtonActions.parseStoredIds(initialSettings.virtualButtonsOutside)) } var virtualButtonsInMore by rememberSaveable(stateSaver = stringListSaver) { mutableStateOf(VirtualButtonActions.parseStoredIds(initialSettings.virtualButtonsInMore)) } - var devicePreviewCardHeightDp by rememberSaveable { mutableIntStateOf(initialSettings.devicePreviewCardHeightDp) } var customServerUri by rememberSaveable { mutableStateOf(initialSettings.customServerUri) } var serverRemotePath by rememberSaveable { mutableStateOf(initialSettings.serverRemotePath) } - var videoCodec by rememberSaveable { mutableStateOf(initialSettings.videoCodec) } - var audioEnabled by rememberSaveable { mutableStateOf(initialSettings.audioEnabled) } - var audioCodec by rememberSaveable { mutableStateOf(initialSettings.audioCodec) } var adbKeyName by rememberSaveable { mutableStateOf(initialSettings.adbKeyName) } var noControl by rememberSaveable { mutableStateOf(initialDeviceSettings.noControl) } var videoEncoder by rememberSaveable { mutableStateOf(initialDeviceSettings.videoEncoder) } @@ -145,8 +144,8 @@ fun MainPage() { var cameraFacingPreset by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraFacingPreset) } var cameraSizePreset by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraSizePreset) } var cameraSizeCustom by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraSizeCustom) } - var cameraArInput by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraArInput) } - var cameraFpsInput by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraFpsInput) } + var cameraArInput by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraAr) } + var cameraFpsInput by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraFps) } var cameraHighSpeed by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraHighSpeed) } var noAudioPlayback by rememberSaveable { mutableStateOf(initialDeviceSettings.noAudioPlayback) } var noVideo by rememberSaveable { mutableStateOf(initialDeviceSettings.noVideo) } @@ -178,47 +177,48 @@ fun MainPage() { val themeController = remember(themeMode) { ThemeController(colorSchemeMode = themeMode) } LaunchedEffect( + audioEnabled, + audioCodec, + videoCodec, themeBaseIndex, monetEnabled, fullscreenDebugInfoEnabled, showFullscreenVirtualButtons, keepScreenOnWhenStreamingEnabled, + devicePreviewCardHeightDp, virtualButtonsOutside, virtualButtonsInMore, - devicePreviewCardHeightDp, customServerUri, serverRemotePath, - videoCodec, - audioEnabled, - audioCodec, adbKeyName, ) { saveMainSettings( context, MainSettings( + audioEnabled = audioEnabled, + audioCodec = audioCodec, + videoCodec = videoCodec, themeBaseIndex = themeBaseIndex, monetEnabled = monetEnabled, fullscreenDebugInfoEnabled = fullscreenDebugInfoEnabled, showFullscreenVirtualButtons = showFullscreenVirtualButtons, keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled, + devicePreviewCardHeightDp = devicePreviewCardHeightDp, virtualButtonsOutside = VirtualButtonActions.encodeStoredIds(virtualButtonsOutside), virtualButtonsInMore = VirtualButtonActions.encodeStoredIds(virtualButtonsInMore), - devicePreviewCardHeightDp = devicePreviewCardHeightDp, customServerUri = customServerUri, serverRemotePath = serverRemotePath, - videoCodec = videoCodec, - audioEnabled = audioEnabled, - audioCodec = audioCodec, adbKeyName = adbKeyName, ), ) } LaunchedEffect(adbKeyName) { - nativeCore.setAdbKeyName(adbKeyName.ifBlank { AppDefaults.DefaultAdbKeyName }) + nativeCore.setAdbKeyName(adbKeyName.ifBlank { AppDefaults.ADB_KEY_NAME }) } - val canNavigateBack = rootBackStack.size > 1 || pagerState.currentPage != MainTabDestination.Device.ordinal + val canNavigateBack = + rootBackStack.size > 1 || pagerState.currentPage != MainTabDestination.Device.ordinal fun popRoot() { if (rootBackStack.size > 1) { @@ -234,8 +234,8 @@ fun MainPage() { pagerState.animateScrollToPage( page = MainTabDestination.Device.ordinal, animationSpec = spring( - dampingRatio = UiMotion.PageSwitchDampingRatio, - stiffness = UiMotion.PageSwitchStiffness, + dampingRatio = UiMotion.PAGE_SWITCH_DAMPING_RATIO, + stiffness = UiMotion.PAGE_SWITCH_STIFFNESS, ), ) } @@ -251,403 +251,424 @@ fun MainPage() { } } - val picker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? -> - if (uri == null) return@rememberLauncherForActivityResult - runCatching { - context.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + val picker = + rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? -> + if (uri == null) return@rememberLauncherForActivityResult + runCatching { + context.contentResolver.takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } + customServerUri = uri.toString() } - customServerUri = uri.toString() - } val rootEntryProvider = entryProvider { - entry(RootScreen.Home) { - Scaffold( - bottomBar = { - NavigationBar { - tabs.forEach { tab -> - NavigationBarItem( - selected = currentTab == tab, - onClick = { - scope.launch { - pagerState.animateScrollToPage( - page = tab.ordinal, - animationSpec = spring( - dampingRatio = UiMotion.PageSwitchDampingRatio, - stiffness = UiMotion.PageSwitchStiffness, - ), + entry(RootScreen.Home) { + Scaffold( + bottomBar = { + NavigationBar { + tabs.forEach { tab -> + NavigationBarItem( + selected = currentTab == tab, + onClick = { + scope.launch { + pagerState.animateScrollToPage( + page = tab.ordinal, + animationSpec = spring( + dampingRatio = UiMotion.PAGE_SWITCH_DAMPING_RATIO, + stiffness = UiMotion.PAGE_SWITCH_STIFFNESS, + ), + ) + } + }, + icon = tab.icon, + label = tab.label, + ) + } + } + }, + snackbarHost = { SnackbarHost(snackHostState) }, + ) { contentPadding -> + HorizontalPager( + modifier = Modifier + .fillMaxSize() + .padding(bottom = contentPadding.calculateBottomPadding()), + state = pagerState, + beyondViewportPageCount = 1, + ) { page -> + val tab = tabs[page] + saveableStateHolder.SaveableStateProvider(tab.name) { + when (tab) { + MainTabDestination.Device -> Scaffold( + topBar = { + TopAppBar( + title = tab.title, + actions = { + IconButton( + onClick = { showDeviceMenu = true }, + holdDownState = showDeviceMenu, + ) { + Icon( + Icons.Default.MoreVert, + contentDescription = "更多" + ) + } + DeviceMenuPopup( + show = showDeviceMenu, + canClearLogs = canClearLogs, + onDismissRequest = { showDeviceMenu = false }, + onReorderDevices = { + openReorderDevicesAction?.invoke() + showDeviceMenu = false + }, + onOpenVirtualButtonOrder = { + rootBackStack.add(RootScreen.VirtualButtonOrder) + showDeviceMenu = false + }, + onClearLogs = { + clearLogsAction?.invoke() + showDeviceMenu = false + }, ) + }, + scrollBehavior = deviceScrollBehavior, + ) + }, + ) { pagePadding -> + DeviceTabScreen( + contentPadding = pagePadding, + nativeCore = nativeCore, + snack = snackHostState, + scrollBehavior = deviceScrollBehavior, + keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled, + virtualButtonsOutside = virtualButtonsOutside, + virtualButtonsInMore = virtualButtonsInMore, + customServerUri = customServerUri, + serverRemotePath = serverRemotePath, + onServerRemotePathChange = { serverRemotePath = it }, + videoCodec = videoCodec, + onVideoCodecChange = { videoCodec = it }, + audioEnabled = audioEnabled, + onAudioEnabledChange = { audioEnabled = it }, + audioCodec = audioCodec, + onAudioCodecChange = { audioCodec = it }, + noControl = noControl, + onNoControlChange = { + noControl = it + if (it) { + turnScreenOff = false } }, - icon = tab.icon, - label = tab.label, + videoEncoder = videoEncoder, + onVideoEncoderChange = { videoEncoder = it }, + videoCodecOptions = videoCodecOptions, + onVideoCodecOptionsChange = { videoCodecOptions = it }, + audioEncoder = audioEncoder, + onAudioEncoderChange = { audioEncoder = it }, + audioCodecOptions = audioCodecOptions, + onAudioCodecOptionsChange = { audioCodecOptions = it }, + audioDup = audioDup, + onAudioDupChange = { audioDup = it }, + audioSourcePreset = audioSourcePreset, + onAudioSourcePresetChange = { audioSourcePreset = it }, + audioSourceCustom = audioSourceCustom, + onAudioSourceCustomChange = { audioSourceCustom = it }, + videoSourcePreset = videoSourcePreset, + onVideoSourcePresetChange = { videoSourcePreset = it }, + cameraIdInput = cameraIdInput, + onCameraIdInputChange = { cameraIdInput = it }, + cameraFacingPreset = cameraFacingPreset, + onCameraFacingPresetChange = { cameraFacingPreset = it }, + cameraSizePreset = cameraSizePreset, + onCameraSizePresetChange = { cameraSizePreset = it }, + cameraSizeCustom = cameraSizeCustom, + onCameraSizeCustomChange = { cameraSizeCustom = it }, + cameraArInput = cameraArInput, + onCameraArInputChange = { cameraArInput = it }, + cameraFpsInput = cameraFpsInput, + onCameraFpsInputChange = { cameraFpsInput = it }, + cameraHighSpeed = cameraHighSpeed, + onCameraHighSpeedChange = { cameraHighSpeed = it }, + noAudioPlayback = noAudioPlayback, + onNoAudioPlaybackChange = { noAudioPlayback = it }, + noVideo = noVideo, + requireAudio = requireAudio, + onRequireAudioChange = { requireAudio = it }, + turnScreenOff = turnScreenOff, + onTurnScreenOffChange = { turnScreenOff = it }, + maxSizeInput = maxSizeInput, + onMaxSizeInputChange = { maxSizeInput = it }, + maxFpsInput = maxFpsInput, + onMaxFpsInputChange = { maxFpsInput = it }, + newDisplayWidth = newDisplayWidth, + onNewDisplayWidthChange = { newDisplayWidth = it }, + newDisplayHeight = newDisplayHeight, + onNewDisplayHeightChange = { newDisplayHeight = it }, + newDisplayDpi = newDisplayDpi, + onNewDisplayDpiChange = { newDisplayDpi = it }, + displayIdInput = displayIdInput, + onDisplayIdInputChange = { displayIdInput = it }, + cropWidth = cropWidth, + onCropWidthChange = { cropWidth = it }, + cropHeight = cropHeight, + onCropHeightChange = { cropHeight = it }, + cropX = cropX, + onCropXChange = { cropX = it }, + cropY = cropY, + onCropYChange = { cropY = it }, + videoEncoderOptions = videoEncoderOptions, + onVideoEncoderOptionsChange = { + videoEncoderOptions.clear() + videoEncoderOptions.addAll(it) + }, + onVideoEncoderTypeMapChange = { + videoEncoderTypeMap.clear() + videoEncoderTypeMap.putAll(it) + }, + audioEncoderOptions = audioEncoderOptions, + onAudioEncoderOptionsChange = { + audioEncoderOptions.clear() + audioEncoderOptions.addAll(it) + }, + onAudioEncoderTypeMapChange = { + audioEncoderTypeMap.clear() + audioEncoderTypeMap.putAll(it) + }, + cameraSizeOptions = cameraSizeOptions, + onCameraSizeOptionsChange = { + cameraSizeOptions.clear() + cameraSizeOptions.addAll(it) + }, + onSessionStartedChange = { sessionStarted = it }, + onRefreshEncodersActionChange = { refreshEncodersAction = it }, + onRefreshCameraSizesActionChange = { + refreshCameraSizesAction = it + }, + onClearLogsActionChange = { clearLogsAction = it }, + onCanClearLogsChange = { canClearLogs = it }, + onOpenReorderDevicesActionChange = { + openReorderDevicesAction = it + }, + onOpenAdvancedPage = { rootBackStack.add(RootScreen.Advanced) }, + onOpenFullscreenPage = { session -> + rootBackStack.add( + RootScreen.Fullscreen( + launch = FullscreenControlLaunch( + deviceName = session.deviceName, + width = session.width, + height = session.height, + codec = session.codec, + ), + ), + ) + }, + previewCardHeightDp = devicePreviewCardHeightDp, ) } - } - }, - snackbarHost = { SnackbarHost(snackHostState) }, - ) { contentPadding -> - HorizontalPager( - modifier = Modifier - .fillMaxSize() - .padding(bottom = contentPadding.calculateBottomPadding()), - state = pagerState, - beyondViewportPageCount = 1, - ) { page -> - val tab = tabs[page] - saveableStateHolder.SaveableStateProvider(tab.name) { - when (tab) { - MainTabDestination.Device -> Scaffold( - topBar = { - TopAppBar( - title = tab.title, - actions = { - IconButton( - onClick = { showDeviceMenu = true }, - holdDownState = showDeviceMenu, - ) { - Icon(Icons.Default.MoreVert, contentDescription = "更多") - } - DeviceMenuPopup( - show = showDeviceMenu, - canClearLogs = canClearLogs, - onDismissRequest = { showDeviceMenu = false }, - onReorderDevices = { - openReorderDevicesAction?.invoke() - showDeviceMenu = false - }, - onOpenVirtualButtonOrder = { - rootBackStack.add(RootScreen.VirtualButtonOrder) - showDeviceMenu = false - }, - onClearLogs = { - clearLogsAction?.invoke() - showDeviceMenu = false - }, - ) - }, - scrollBehavior = deviceScrollBehavior, - ) - }, - ) { pagePadding -> - DeviceTabScreen( - contentPadding = pagePadding, - nativeCore = nativeCore, - snack = snackHostState, - scrollBehavior = deviceScrollBehavior, - keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled, - virtualButtonsOutside = virtualButtonsOutside, - virtualButtonsInMore = virtualButtonsInMore, - customServerUri = customServerUri, - serverRemotePath = serverRemotePath, - onServerRemotePathChange = { serverRemotePath = it }, - videoCodec = videoCodec, - onVideoCodecChange = { videoCodec = it }, - audioEnabled = audioEnabled, - onAudioEnabledChange = { audioEnabled = it }, - audioCodec = audioCodec, - onAudioCodecChange = { audioCodec = it }, - noControl = noControl, - onNoControlChange = { - noControl = it - if (it) { - turnScreenOff = false - } - }, - videoEncoder = videoEncoder, - onVideoEncoderChange = { videoEncoder = it }, - videoCodecOptions = videoCodecOptions, - onVideoCodecOptionsChange = { videoCodecOptions = it }, - audioEncoder = audioEncoder, - onAudioEncoderChange = { audioEncoder = it }, - audioCodecOptions = audioCodecOptions, - onAudioCodecOptionsChange = { audioCodecOptions = it }, - audioDup = audioDup, - onAudioDupChange = { audioDup = it }, - audioSourcePreset = audioSourcePreset, - onAudioSourcePresetChange = { audioSourcePreset = it }, - audioSourceCustom = audioSourceCustom, - onAudioSourceCustomChange = { audioSourceCustom = it }, - videoSourcePreset = videoSourcePreset, - onVideoSourcePresetChange = { videoSourcePreset = it }, - cameraIdInput = cameraIdInput, - onCameraIdInputChange = { cameraIdInput = it }, - cameraFacingPreset = cameraFacingPreset, - onCameraFacingPresetChange = { cameraFacingPreset = it }, - cameraSizePreset = cameraSizePreset, - onCameraSizePresetChange = { cameraSizePreset = it }, - cameraSizeCustom = cameraSizeCustom, - onCameraSizeCustomChange = { cameraSizeCustom = it }, - cameraArInput = cameraArInput, - onCameraArInputChange = { cameraArInput = it }, - cameraFpsInput = cameraFpsInput, - onCameraFpsInputChange = { cameraFpsInput = it }, - cameraHighSpeed = cameraHighSpeed, - onCameraHighSpeedChange = { cameraHighSpeed = it }, - noAudioPlayback = noAudioPlayback, - onNoAudioPlaybackChange = { noAudioPlayback = it }, - noVideo = noVideo, - requireAudio = requireAudio, - onRequireAudioChange = { requireAudio = it }, - turnScreenOff = turnScreenOff, - onTurnScreenOffChange = { turnScreenOff = it }, - maxSizeInput = maxSizeInput, - onMaxSizeInputChange = { maxSizeInput = it }, - maxFpsInput = maxFpsInput, - onMaxFpsInputChange = { maxFpsInput = it }, - newDisplayWidth = newDisplayWidth, - onNewDisplayWidthChange = { newDisplayWidth = it }, - newDisplayHeight = newDisplayHeight, - onNewDisplayHeightChange = { newDisplayHeight = it }, - newDisplayDpi = newDisplayDpi, - onNewDisplayDpiChange = { newDisplayDpi = it }, - displayIdInput = displayIdInput, - onDisplayIdInputChange = { displayIdInput = it }, - cropWidth = cropWidth, - onCropWidthChange = { cropWidth = it }, - cropHeight = cropHeight, - onCropHeightChange = { cropHeight = it }, - cropX = cropX, - onCropXChange = { cropX = it }, - cropY = cropY, - onCropYChange = { cropY = it }, - videoEncoderOptions = videoEncoderOptions, - onVideoEncoderOptionsChange = { - videoEncoderOptions.clear() - videoEncoderOptions.addAll(it) - }, - onVideoEncoderTypeMapChange = { - videoEncoderTypeMap.clear() - videoEncoderTypeMap.putAll(it) - }, - audioEncoderOptions = audioEncoderOptions, - onAudioEncoderOptionsChange = { - audioEncoderOptions.clear() - audioEncoderOptions.addAll(it) - }, - onAudioEncoderTypeMapChange = { - audioEncoderTypeMap.clear() - audioEncoderTypeMap.putAll(it) - }, - cameraSizeOptions = cameraSizeOptions, - onCameraSizeOptionsChange = { - cameraSizeOptions.clear() - cameraSizeOptions.addAll(it) - }, - onSessionStartedChange = { sessionStarted = it }, - onRefreshEncodersActionChange = { refreshEncodersAction = it }, - onRefreshCameraSizesActionChange = { refreshCameraSizesAction = it }, - onClearLogsActionChange = { clearLogsAction = it }, - onCanClearLogsChange = { canClearLogs = it }, - onOpenReorderDevicesActionChange = { openReorderDevicesAction = it }, - onOpenAdvancedPage = { rootBackStack.add(RootScreen.Advanced) }, - onOpenFullscreenPage = { session -> - rootBackStack.add( - RootScreen.Fullscreen( - launch = FullscreenControlLaunch( - deviceName = session.deviceName, - width = session.width, - height = session.height, - codec = session.codec, - ), - ), - ) - }, - previewCardHeightDp = devicePreviewCardHeightDp, - ) - } - MainTabDestination.Settings -> Scaffold( - topBar = { - TopAppBar( - title = tab.title, - scrollBehavior = settingsScrollBehavior, - ) - }, - ) { pagePadding -> - SettingsScreen( - contentPadding = pagePadding, - themeBaseIndex = themeBaseIndex, - onThemeBaseIndexChange = { themeBaseIndex = it }, - monetEnabled = monetEnabled, - onMonetEnabledChange = { monetEnabled = it }, - fullscreenDebugInfoEnabled = fullscreenDebugInfoEnabled, - onFullscreenDebugInfoEnabledChange = { fullscreenDebugInfoEnabled = it }, - showFullscreenVirtualButtons = showFullscreenVirtualButtons, - onShowFullscreenVirtualButtonsChange = { showFullscreenVirtualButtons = it }, - keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled, - onKeepScreenOnWhenStreamingEnabledChange = { - keepScreenOnWhenStreamingEnabled = it - }, - onOpenReorderDevices = { openReorderDevicesAction?.invoke() }, - onOpenVirtualButtonOrder = { rootBackStack.add(RootScreen.VirtualButtonOrder) }, - devicePreviewCardHeightDp = devicePreviewCardHeightDp, - onDevicePreviewCardHeightDpChange = { - devicePreviewCardHeightDp = it.coerceAtLeast(120) - }, - customServerUri = customServerUri, - onPickServer = { - picker.launch(arrayOf("application/java-archive", "application/octet-stream", "*/*")) - }, - onClearServer = { customServerUri = null }, - serverRemotePath = serverRemotePath, - onServerRemotePathChange = { serverRemotePath = it }, - adbKeyName = adbKeyName, - onAdbKeyNameChange = { adbKeyName = it }, + MainTabDestination.Settings -> Scaffold( + topBar = { + TopAppBar( + title = tab.title, scrollBehavior = settingsScrollBehavior, ) - } + }, + ) { pagePadding -> + SettingsScreen( + contentPadding = pagePadding, + themeBaseIndex = themeBaseIndex, + onThemeBaseIndexChange = { themeBaseIndex = it }, + monetEnabled = monetEnabled, + onMonetEnabledChange = { monetEnabled = it }, + fullscreenDebugInfoEnabled = fullscreenDebugInfoEnabled, + onFullscreenDebugInfoEnabledChange = { + fullscreenDebugInfoEnabled = it + }, + keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled, + onKeepScreenOnWhenStreamingEnabledChange = { + keepScreenOnWhenStreamingEnabled = it + }, + devicePreviewCardHeightDp = devicePreviewCardHeightDp, + onDevicePreviewCardHeightDpChange = { + devicePreviewCardHeightDp = it.coerceAtLeast(120) + }, + customServerUri = customServerUri, + onPickServer = { + picker.launch( + arrayOf( + "application/java-archive", + "application/octet-stream", + "*/*" + ) + ) + }, + onClearServer = { customServerUri = null }, + serverRemotePath = serverRemotePath, + onServerRemotePathChange = { serverRemotePath = it }, + adbKeyName = adbKeyName, + onAdbKeyNameChange = { adbKeyName = it }, + scrollBehavior = settingsScrollBehavior, + ) } } } } } + } - entry(RootScreen.Advanced) { - val videoEncoderDropdownItems = listOf("默认") + videoEncoderOptions - val audioEncoderDropdownItems = listOf("默认") + audioEncoderOptions - val videoEncoderIndex = (videoEncoderOptions.indexOf(videoEncoder) + 1).coerceAtLeast(0) - val audioEncoderIndex = (audioEncoderOptions.indexOf(audioEncoder) + 1).coerceAtLeast(0) + entry(RootScreen.Advanced) { + val videoEncoderDropdownItems = listOf("默认") + videoEncoderOptions + val audioEncoderDropdownItems = listOf("默认") + audioEncoderOptions + val videoEncoderIndex = (videoEncoderOptions.indexOf(videoEncoder) + 1).coerceAtLeast(0) + val audioEncoderIndex = (audioEncoderOptions.indexOf(audioEncoder) + 1).coerceAtLeast(0) - Scaffold( - topBar = { - TopAppBar( - title = "高级参数", - navigationIcon = { - IconButton(onClick = { popRoot() }) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回") - } - }, - scrollBehavior = advancedScrollBehavior, - ) - }, - snackbarHost = { SnackbarHost(snackHostState) }, - ) { pagePadding -> - AdvancedConfigPage( - contentPadding = pagePadding, - scrollBehavior = advancedScrollBehavior, - sessionStarted = sessionStarted, - snackbarHostState = snackHostState, - audioEnabled = audioEnabled, - noControl = noControl, - onNoControlChange = { - noControl = it - if (it) { - turnScreenOff = false + Scaffold( + topBar = { + TopAppBar( + title = "高级参数", + navigationIcon = { + IconButton(onClick = { popRoot() }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "返回" + ) } }, - audioDup = audioDup, - onAudioDupChange = { audioDup = it }, - audioSourcePreset = audioSourcePreset, - onAudioSourcePresetChange = { audioSourcePreset = it }, - audioSourceCustom = audioSourceCustom, - onAudioSourceCustomChange = { audioSourceCustom = it }, - videoSourcePreset = videoSourcePreset, - onVideoSourcePresetChange = { videoSourcePreset = it }, - cameraIdInput = cameraIdInput, - onCameraIdInputChange = { cameraIdInput = it }, - cameraFacingPreset = cameraFacingPreset, - onCameraFacingPresetChange = { cameraFacingPreset = it }, - cameraSizePreset = cameraSizePreset, - onCameraSizePresetChange = { cameraSizePreset = it }, - cameraSizeCustom = cameraSizeCustom, - onCameraSizeCustomChange = { cameraSizeCustom = it }, - cameraSizeDropdownItems = listOf("默认") + cameraSizeOptions + listOf("自定义"), - cameraSizeIndex = when (cameraSizePreset) { - "custom" -> cameraSizeOptions.size + 1 - in cameraSizeOptions -> cameraSizeOptions.indexOf(cameraSizePreset) + 1 - else -> 0 - }, - cameraArInput = cameraArInput, - onCameraArInputChange = { cameraArInput = it }, - cameraFpsInput = cameraFpsInput, - onCameraFpsInputChange = { cameraFpsInput = it }, - cameraHighSpeed = cameraHighSpeed, - onCameraHighSpeedChange = { cameraHighSpeed = it }, - noAudioPlayback = noAudioPlayback, - onNoAudioPlaybackChange = { noAudioPlayback = it }, - noVideo = noVideo, - onNoVideoChange = { noVideo = it }, - requireAudio = requireAudio, - onRequireAudioChange = { requireAudio = it }, - turnScreenOff = turnScreenOff, - onTurnScreenOffChange = { turnScreenOff = it }, - maxSizeInput = maxSizeInput, - onMaxSizeInputChange = { maxSizeInput = it }, - maxFpsInput = maxFpsInput, - onMaxFpsInputChange = { maxFpsInput = it }, - videoEncoderDropdownItems = videoEncoderDropdownItems, - videoEncoderTypeMap = videoEncoderTypeMap, - videoEncoderIndex = videoEncoderIndex, - onVideoEncoderChange = { videoEncoder = it }, - videoCodecOptions = videoCodecOptions, - onVideoCodecOptionsChange = { videoCodecOptions = it }, - audioEncoderDropdownItems = audioEncoderDropdownItems, - audioEncoderTypeMap = audioEncoderTypeMap, - audioEncoderIndex = audioEncoderIndex, - onAudioEncoderChange = { audioEncoder = it }, - audioCodecOptions = audioCodecOptions, - onAudioCodecOptionsChange = { audioCodecOptions = it }, - onRefreshEncoders = { refreshEncodersAction?.invoke() }, - onRefreshCameraSizes = { refreshCameraSizesAction?.invoke() }, - newDisplayWidth = newDisplayWidth, - onNewDisplayWidthChange = { newDisplayWidth = it }, - newDisplayHeight = newDisplayHeight, - onNewDisplayHeightChange = { newDisplayHeight = it }, - newDisplayDpi = newDisplayDpi, - onNewDisplayDpiChange = { newDisplayDpi = it }, - displayIdInput = displayIdInput, - onDisplayIdInputChange = { displayIdInput = it }, - cropWidth = cropWidth, - onCropWidthChange = { cropWidth = it }, - cropHeight = cropHeight, - onCropHeightChange = { cropHeight = it }, - cropX = cropX, - onCropXChange = { cropX = it }, - cropY = cropY, - onCropYChange = { cropY = it }, - ) - } - } - - entry(RootScreen.VirtualButtonOrder) { - Scaffold( - modifier = Modifier.nestedScroll(advancedScrollBehavior.nestedScrollConnection), - topBar = { - TopAppBar( - title = "虚拟按钮排序", - navigationIcon = { - IconButton(onClick = { popRoot() }) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回") - } - }, - scrollBehavior = advancedScrollBehavior, - ) - }, - ) { pagePadding -> - VirtualButtonOrderPage( - contentPadding = pagePadding, scrollBehavior = advancedScrollBehavior, - outsideIds = virtualButtonsOutside, - moreIds = virtualButtonsInMore, - onLayoutChange = { outside, more -> - virtualButtonsOutside = outside - virtualButtonsInMore = more - }, ) - } - } - - entry { screen -> - FullscreenControlPage( - launch = screen.launch, - nativeCore = nativeCore, - virtualButtonsOutside = virtualButtonsOutside, - virtualButtonsInMore = virtualButtonsInMore, - showDebugInfo = fullscreenDebugInfoEnabled, - showVirtualButtons = showFullscreenVirtualButtons, - onDismiss = { popRoot() }, + }, + snackbarHost = { SnackbarHost(snackHostState) }, + ) { pagePadding -> + AdvancedConfigPage( + contentPadding = pagePadding, + scrollBehavior = advancedScrollBehavior, + sessionStarted = sessionStarted, + snackbarHostState = snackHostState, + audioEnabled = audioEnabled, + noControl = noControl, + onNoControlChange = { + noControl = it + if (it) { + turnScreenOff = false + } + }, + audioDup = audioDup, + onAudioDupChange = { audioDup = it }, + audioSourcePreset = audioSourcePreset, + onAudioSourcePresetChange = { audioSourcePreset = it }, + audioSourceCustom = audioSourceCustom, + onAudioSourceCustomChange = { audioSourceCustom = it }, + videoSourcePreset = videoSourcePreset, + onVideoSourcePresetChange = { videoSourcePreset = it }, + cameraIdInput = cameraIdInput, + onCameraIdInputChange = { cameraIdInput = it }, + cameraFacingPreset = cameraFacingPreset, + onCameraFacingPresetChange = { cameraFacingPreset = it }, + cameraSizePreset = cameraSizePreset, + onCameraSizePresetChange = { cameraSizePreset = it }, + cameraSizeCustom = cameraSizeCustom, + onCameraSizeCustomChange = { cameraSizeCustom = it }, + cameraSizeDropdownItems = listOf("默认") + cameraSizeOptions + listOf("自定义"), + cameraSizeIndex = when (cameraSizePreset) { + "custom" -> cameraSizeOptions.size + 1 + in cameraSizeOptions -> cameraSizeOptions.indexOf(cameraSizePreset) + 1 + else -> 0 + }, + cameraArInput = cameraArInput, + onCameraArInputChange = { cameraArInput = it }, + cameraFpsInput = cameraFpsInput, + onCameraFpsInputChange = { cameraFpsInput = it }, + cameraHighSpeed = cameraHighSpeed, + onCameraHighSpeedChange = { cameraHighSpeed = it }, + noAudioPlayback = noAudioPlayback, + onNoAudioPlaybackChange = { noAudioPlayback = it }, + noVideo = noVideo, + onNoVideoChange = { noVideo = it }, + requireAudio = requireAudio, + onRequireAudioChange = { requireAudio = it }, + turnScreenOff = turnScreenOff, + onTurnScreenOffChange = { turnScreenOff = it }, + maxSizeInput = maxSizeInput, + onMaxSizeInputChange = { maxSizeInput = it }, + maxFpsInput = maxFpsInput, + onMaxFpsInputChange = { maxFpsInput = it }, + videoEncoderDropdownItems = videoEncoderDropdownItems, + videoEncoderTypeMap = videoEncoderTypeMap, + videoEncoderIndex = videoEncoderIndex, + onVideoEncoderChange = { videoEncoder = it }, + videoCodecOptions = videoCodecOptions, + onVideoCodecOptionsChange = { videoCodecOptions = it }, + audioEncoderDropdownItems = audioEncoderDropdownItems, + audioEncoderTypeMap = audioEncoderTypeMap, + audioEncoderIndex = audioEncoderIndex, + onAudioEncoderChange = { audioEncoder = it }, + audioCodecOptions = audioCodecOptions, + onAudioCodecOptionsChange = { audioCodecOptions = it }, + onRefreshEncoders = { refreshEncodersAction?.invoke() }, + onRefreshCameraSizes = { refreshCameraSizesAction?.invoke() }, + newDisplayWidth = newDisplayWidth, + onNewDisplayWidthChange = { newDisplayWidth = it }, + newDisplayHeight = newDisplayHeight, + onNewDisplayHeightChange = { newDisplayHeight = it }, + newDisplayDpi = newDisplayDpi, + onNewDisplayDpiChange = { newDisplayDpi = it }, + displayIdInput = displayIdInput, + onDisplayIdInputChange = { displayIdInput = it }, + cropWidth = cropWidth, + onCropWidthChange = { cropWidth = it }, + cropHeight = cropHeight, + onCropHeightChange = { cropHeight = it }, + cropX = cropX, + onCropXChange = { cropX = it }, + cropY = cropY, + onCropYChange = { cropY = it }, ) } + } + + entry(RootScreen.VirtualButtonOrder) { + Scaffold( + modifier = Modifier.nestedScroll(advancedScrollBehavior.nestedScrollConnection), + topBar = { + TopAppBar( + title = "虚拟按钮排序", + navigationIcon = { + IconButton(onClick = { popRoot() }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "返回" + ) + } + }, + scrollBehavior = advancedScrollBehavior, + ) + }, + ) { pagePadding -> + VirtualButtonOrderPage( + contentPadding = pagePadding, + scrollBehavior = advancedScrollBehavior, + outsideIds = virtualButtonsOutside, + moreIds = virtualButtonsInMore, + onLayoutChange = { outside, more -> + virtualButtonsOutside = outside + virtualButtonsInMore = more + }, + ) + } + } + + entry { screen -> + FullscreenControlPage( + launch = screen.launch, + nativeCore = nativeCore, + virtualButtonsOutside = virtualButtonsOutside, + virtualButtonsInMore = virtualButtonsInMore, + showDebugInfo = fullscreenDebugInfoEnabled, + showVirtualButtons = showFullscreenVirtualButtons, + onDismiss = { popRoot() }, + ) + } } val rootEntries = rememberDecoratedNavEntries( @@ -724,7 +745,8 @@ private fun DeviceMenuPopupItem( } val additionalTopPadding = if (index == 0) UiSpacing.PopupHorizontal else UiSpacing.PageItem - val additionalBottomPadding = if (index == optionSize - 1) UiSpacing.PopupHorizontal else UiSpacing.PageItem + val additionalBottomPadding = + if (index == optionSize - 1) UiSpacing.PopupHorizontal else UiSpacing.PageItem Text( text = text, fontSize = MiuixTheme.textStyles.body1.fontSize, 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 9a3a181..95f876a 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 @@ -22,7 +22,6 @@ import top.yukonga.miuix.kmp.basic.IconButton import top.yukonga.miuix.kmp.basic.ScrollBehavior import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TextField -import top.yukonga.miuix.kmp.extra.SuperArrow import top.yukonga.miuix.kmp.extra.SuperDropdown import top.yukonga.miuix.kmp.extra.SuperSwitch import top.yukonga.miuix.kmp.theme.ColorSchemeMode @@ -61,12 +60,8 @@ fun SettingsScreen( onMonetEnabledChange: (Boolean) -> Unit, fullscreenDebugInfoEnabled: Boolean, onFullscreenDebugInfoEnabledChange: (Boolean) -> Unit, - showFullscreenVirtualButtons: Boolean, - onShowFullscreenVirtualButtonsChange: (Boolean) -> Unit, keepScreenOnWhenStreamingEnabled: Boolean, onKeepScreenOnWhenStreamingEnabledChange: (Boolean) -> Unit, - onOpenReorderDevices: () -> Unit, - onOpenVirtualButtonOrder: () -> Unit, devicePreviewCardHeightDp: Int, onDevicePreviewCardHeightDpChange: (Int) -> Unit, customServerUri: String?, @@ -111,12 +106,6 @@ fun SettingsScreen( checked = fullscreenDebugInfoEnabled, onCheckedChange = onFullscreenDebugInfoEnabledChange, ) - SuperSwitch( - title = "全屏显示虚拟按钮", - summary = "在全屏模式底部显示返回/主页等虚拟按钮", - checked = showFullscreenVirtualButtons, - onCheckedChange = onShowFullscreenVirtualButtonsChange, - ) SuperSwitch( title = "投屏时不允许息屏", summary = "Scrcpy 启动后保持本机常亮,避免锁屏导致 ADB 断开", @@ -127,7 +116,11 @@ fun SettingsScreen( title = "预览卡高度", summary = "设备页预览卡高度", value = devicePreviewCardHeightDp.toFloat(), - onValueChange = { onDevicePreviewCardHeightDpChange(it.roundToInt().coerceAtLeast(120)) }, + onValueChange = { + onDevicePreviewCardHeightDpChange( + it.roundToInt().coerceAtLeast(120) + ) + }, valueRange = 160f..600f, steps = 439, unit = "dp", @@ -136,19 +129,10 @@ fun SettingsScreen( inputFilter = { it.filter(Char::isDigit) }, inputValueRange = 120f..Float.MAX_VALUE, onInputConfirm = { raw -> - raw.toIntOrNull()?.let { onDevicePreviewCardHeightDpChange(it.coerceAtLeast(120)) } + raw.toIntOrNull() + ?.let { onDevicePreviewCardHeightDpChange(it.coerceAtLeast(120)) } }, ) - SuperArrow( - title = "调整设备排序", - summary = "调整设备页快捷设备列表顺序", - onClick = onOpenReorderDevices, - ) - SuperArrow( - title = "虚拟按钮排序", - summary = "调整底部虚拟按钮与更多菜单顺序", - onClick = onOpenVirtualButtonOrder, - ) } SectionSmallTitle(text = "scrcpy-server") @@ -192,7 +176,7 @@ fun SettingsScreen( TextField( value = serverRemotePath, onValueChange = onServerRemotePathChange, - label = AppDefaults.DefaultServerRemotePath, + label = AppDefaults.SERVER_REMOTE_PATH, useLabelAsPlaceholder = true, singleLine = true, modifier = Modifier @@ -214,7 +198,7 @@ fun SettingsScreen( TextField( value = adbKeyName, onValueChange = onAdbKeyNameChange, - label = AppDefaults.DefaultAdbKeyName, + label = AppDefaults.ADB_KEY_NAME, useLabelAsPlaceholder = true, singleLine = true, modifier = Modifier 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 39da73e..b0bf6c7 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 @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Modifier @@ -54,7 +53,11 @@ internal fun VirtualButtonOrderPage( onLayoutChange(outsideState.toList(), moreState.toList()) } - fun reorderInside(list: androidx.compose.runtime.snapshots.SnapshotStateList, itemId: String, deltaY: Float) { + fun reorderInside( + list: androidx.compose.runtime.snapshots.SnapshotStateList, + itemId: String, + deltaY: Float + ) { val fromIndex = list.indexOf(itemId) if (fromIndex < 0) return @@ -88,7 +91,8 @@ internal fun VirtualButtonOrderPage( } fun handleOutsideDrop(payload: SortDropPayload) { - val transfer = payload.transferDirection == SortTransferDirection.TO_RIGHT && payload.deltaX >= LIST_TRANSFER_STEP_PX + val transfer = + payload.transferDirection == SortTransferDirection.TO_RIGHT && payload.deltaX >= LIST_TRANSFER_STEP_PX if (transfer) { if (payload.itemId == VirtualButtonAction.MORE.id) return transferToOther(outsideState, moreState, payload.itemId, payload.deltaY) @@ -98,7 +102,8 @@ internal fun VirtualButtonOrderPage( } fun handleMoreDrop(payload: SortDropPayload) { - val transfer = payload.transferDirection == SortTransferDirection.TO_LEFT && payload.deltaX <= -LIST_TRANSFER_STEP_PX + val transfer = + payload.transferDirection == SortTransferDirection.TO_LEFT && payload.deltaX <= -LIST_TRANSFER_STEP_PX if (transfer) { transferToOther(moreState, outsideState, payload.itemId, payload.deltaY) } else { diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/SuperSlide.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/SuperSlide.kt index e0e6c80..13240dd 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/SuperSlide.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/SuperSlide.kt @@ -58,7 +58,8 @@ fun SuperSlide( holdDownState = holdArrow, endActions = { val isZeroState = value == 0f && zeroStateText != null - val valueText = if (isZeroState) zeroStateText else (displayText ?: displayFormatter(value)) + val valueText = + if (isZeroState) zeroStateText else (displayText ?: displayFormatter(value)) val shouldShowUnit = unit.isNotBlank() && (!isZeroState || showUnitWhenZeroState) val text = if (shouldShowUnit) "$valueText $unit" else valueText Text( diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/AppSettingsStore.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/AppSettingsStore.kt index ff478cd..2b382d1 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/AppSettingsStore.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/AppSettingsStore.kt @@ -6,215 +6,495 @@ import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults import io.github.miuzarte.scrcpyforandroid.constants.AppPreferenceKeys internal data class MainSettings( - val themeBaseIndex: Int = AppDefaults.DefaultThemeBaseIndex, - val monetEnabled: Boolean = AppDefaults.DefaultMonetEnabled, - val fullscreenDebugInfoEnabled: Boolean = AppDefaults.DefaultFullscreenDebugInfoEnabled, - val showFullscreenVirtualButtons: Boolean = AppDefaults.DefaultShowFullscreenVirtualButtons, - val devicePreviewCardHeightDp: Int = AppDefaults.DefaultDevicePreviewCardHeightDp, - val keepScreenOnWhenStreamingEnabled: Boolean = AppDefaults.DefaultKeepScreenOnWhenStreamingEnabled, - val virtualButtonsOutside: String = AppDefaults.DefaultVirtualButtonsOutside, - val virtualButtonsInMore: String = AppDefaults.DefaultVirtualButtonsInMore, - val customServerUri: String? = null, - val serverRemotePath: String = AppDefaults.DefaultServerRemotePathInput, - val videoCodec: String = AppDefaults.DefaultVideoCodec, - val audioEnabled: Boolean = AppDefaults.DefaultAudioEnabled, - val audioCodec: String = AppDefaults.DefaultAudioCodec, - val adbKeyName: String = AppDefaults.DefaultAdbKeyNameInput, + val audioEnabled: Boolean = AppDefaults.AUDIO_ENABLED, + val audioCodec: String = AppDefaults.AUDIO_CODEC, + val videoCodec: String = AppDefaults.VIDEO_CODEC, + val themeBaseIndex: Int = AppDefaults.THEME_BASE_INDEX, + val monetEnabled: Boolean = AppDefaults.MONET, + val fullscreenDebugInfoEnabled: Boolean = AppDefaults.FULLSCREEN_DEBUG_INFO, + val showFullscreenVirtualButtons: Boolean = AppDefaults.SHOW_FULLSCREEN_VIRTUAL_BUTTONS, + val keepScreenOnWhenStreamingEnabled: Boolean = AppDefaults.KEEP_SCREEN_ON_WHEN_STREAMING, + val devicePreviewCardHeightDp: Int = AppDefaults.DEVICE_PREVIEW_CARD_HEIGHT_DP, + val virtualButtonsOutside: String = AppDefaults.VIRTUAL_BUTTONS_OUTSIDE, + val virtualButtonsInMore: String = AppDefaults.VIRTUAL_BUTTONS_IN_MORE, + val customServerUri: String? = AppDefaults.CUSTOM_SERVER_URI, + val serverRemotePath: String = AppDefaults.SERVER_REMOTE_PATH_INPUT, + val adbKeyName: String = AppDefaults.ADB_KEY_NAME_INPUT, ) internal data class DevicePageSettings( - val pairHost: String = AppDefaults.DefaultPairHost, - val pairPort: String = AppDefaults.DefaultPairPort, - val pairCode: String = AppDefaults.DefaultPairCode, - val quickConnectInput: String = AppDefaults.DefaultQuickConnectInput, - val bitRateMbps: Float = AppDefaults.DefaultBitRateMbps, - val bitRateInput: String = AppDefaults.DefaultBitRateInput, - val audioBitRateKbps: Int = AppDefaults.DefaultAudioBitRateKbps, - val maxSizeInput: String = AppDefaults.DefaultMaxSizeInput, - val maxFpsInput: String = AppDefaults.DefaultMaxFpsInput, - val noControl: Boolean = AppDefaults.DefaultNoControl, - val videoEncoder: String = AppDefaults.DefaultVideoEncoder, - val videoCodecOptions: String = AppDefaults.DefaultVideoCodecOptions, - val audioEncoder: String = AppDefaults.DefaultAudioEncoder, - val audioCodecOptions: String = AppDefaults.DefaultAudioCodecOptions, - val audioDup: Boolean = AppDefaults.DefaultAudioDup, - val audioSourcePreset: String = AppDefaults.DefaultAudioSourcePreset, - val audioSourceCustom: String = AppDefaults.DefaultAudioSourceCustom, - val videoSourcePreset: String = AppDefaults.DefaultVideoSourcePreset, - val cameraIdInput: String = AppDefaults.DefaultCameraIdInput, - val cameraFacingPreset: String = AppDefaults.DefaultCameraFacingPreset, - val cameraSizePreset: String = AppDefaults.DefaultCameraSizePreset, - val cameraSizeCustom: String = AppDefaults.DefaultCameraSizeCustom, - val cameraArInput: String = AppDefaults.DefaultCameraArInput, - val cameraFpsInput: String = AppDefaults.DefaultCameraFpsInput, - val cameraHighSpeed: Boolean = AppDefaults.DefaultCameraHighSpeed, - val noAudioPlayback: Boolean = AppDefaults.DefaultNoAudioPlayback, - val noVideo: Boolean = AppDefaults.DefaultNoVideo, - val requireAudio: Boolean = AppDefaults.DefaultRequireAudio, - val turnScreenOff: Boolean = AppDefaults.DefaultTurnScreenOff, - val newDisplayWidth: String = AppDefaults.DefaultNewDisplayWidth, - val newDisplayHeight: String = AppDefaults.DefaultNewDisplayHeight, - val newDisplayDpi: String = AppDefaults.DefaultNewDisplayDpi, - val displayIdInput: String = AppDefaults.DefaultDisplayIdInput, - val cropWidth: String = AppDefaults.DefaultCropWidth, - val cropHeight: String = AppDefaults.DefaultCropHeight, - val cropX: String = AppDefaults.DefaultCropX, - val cropY: String = AppDefaults.DefaultCropY, + val quickConnectInput: String = AppDefaults.QUICK_CONNECT_INPUT, + val pairHost: String = AppDefaults.PAIR_HOST, + val pairPort: String = AppDefaults.PAIR_PORT, + val pairCode: String = AppDefaults.PAIR_CODE, + val audioBitRateKbps: Int = AppDefaults.AUDIO_BIT_RATE_KBPS, + val audioBitRateInput: String = AppDefaults.AUDIO_BIT_RATE_INPUT, + val videoBitRateMbps: Float = AppDefaults.VIDEO_BIT_RATE_MBPS, + val videoBitRateInput: String = AppDefaults.VIDEO_BIT_RATE_INPUT, + val turnScreenOff: Boolean = AppDefaults.TURN_SCREEN_OFF, + val noControl: Boolean = AppDefaults.NO_CONTROL, + val noVideo: Boolean = AppDefaults.NO_VIDEO, + val videoSourcePreset: String = AppDefaults.VIDEO_SOURCE_PRESET, + val displayIdInput: String = AppDefaults.DISPLAY_ID, + val cameraIdInput: String = AppDefaults.CAMERA_ID, + val cameraFacingPreset: String = AppDefaults.CAMERA_FACING_PRESET, + val cameraSizePreset: String = AppDefaults.CAMERA_SIZE_PRESET, + val cameraSizeCustom: String = AppDefaults.CAMERA_SIZE_CUSTOM, + val cameraAr: String = AppDefaults.CAMERA_AR, + val cameraFps: String = AppDefaults.CAMERA_FPS, + val cameraHighSpeed: Boolean = AppDefaults.CAMERA_HIGH_SPEED, + val audioSourcePreset: String = AppDefaults.AUDIO_SOURCE_PRESET, + val audioSourceCustom: String = AppDefaults.AUDIO_SOURCE_CUSTOM, + val audioDup: Boolean = AppDefaults.AUDIO_DUP, + val noAudioPlayback: Boolean = AppDefaults.NO_AUDIO_PLAYBACK, + val requireAudio: Boolean = AppDefaults.REQUIRE_AUDIO, + val maxSizeInput: String = AppDefaults.MAX_SIZE_INPUT, + val maxFpsInput: String = AppDefaults.MAX_FPS_INPUT, + val videoEncoder: String = AppDefaults.VIDEO_ENCODER, + val videoCodecOptions: String = AppDefaults.VIDEO_CODEC_OPTION, + val audioEncoder: String = AppDefaults.AUDIO_ENCODER, + val audioCodecOptions: String = AppDefaults.AUDIO_CODEC_OPTION, + val newDisplayWidth: String = AppDefaults.NEW_DISPLAY_WIDTH, + val newDisplayHeight: String = AppDefaults.NEW_DISPLAY_HEIGHT, + val newDisplayDpi: String = AppDefaults.NEW_DISPLAY_DPI, + val cropWidth: String = AppDefaults.CROP_WIDTH, + val cropHeight: String = AppDefaults.CROP_HEIGHT, + val cropX: String = AppDefaults.CROP_X, + val cropY: String = AppDefaults.CROP_Y, ) internal fun loadMainSettings(context: Context): MainSettings { - val prefs = context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE) + val prefs = context.getSharedPreferences( + AppPreferenceKeys.PREFS_NAME, + Context.MODE_PRIVATE, + ) return MainSettings( - themeBaseIndex = prefs.getInt(AppPreferenceKeys.ThemeBaseIndex, AppDefaults.DefaultThemeBaseIndex), - monetEnabled = prefs.getBoolean(AppPreferenceKeys.MonetEnabled, AppDefaults.DefaultMonetEnabled), + audioEnabled = prefs.getBoolean( + AppPreferenceKeys.AUDIO_ENABLED, + AppDefaults.AUDIO_ENABLED, + ), + audioCodec = prefs.getString( + AppPreferenceKeys.AUDIO_CODEC, + AppDefaults.AUDIO_CODEC, + ).orEmpty().ifBlank { AppDefaults.AUDIO_CODEC }, + videoCodec = prefs.getString( + AppPreferenceKeys.VIDEO_CODEC, + AppDefaults.VIDEO_CODEC, + ).orEmpty().ifBlank { AppDefaults.VIDEO_CODEC }, + themeBaseIndex = prefs.getInt( + AppPreferenceKeys.THEME_BASE_INDEX, + AppDefaults.THEME_BASE_INDEX, + ), + monetEnabled = prefs.getBoolean( + AppPreferenceKeys.MONET, + AppDefaults.MONET, + ), fullscreenDebugInfoEnabled = prefs.getBoolean( - AppPreferenceKeys.FullscreenDebugInfoEnabled, - AppDefaults.DefaultFullscreenDebugInfoEnabled, + AppPreferenceKeys.FULLSCREEN_DEBUG_INFO, + AppDefaults.FULLSCREEN_DEBUG_INFO, ), showFullscreenVirtualButtons = prefs.getBoolean( - AppPreferenceKeys.ShowFullscreenVirtualButtons, - AppDefaults.DefaultShowFullscreenVirtualButtons, + AppPreferenceKeys.SHOW_FULLSCREEN_VIRTUAL_BUTTONS, + AppDefaults.SHOW_FULLSCREEN_VIRTUAL_BUTTONS, + ), + keepScreenOnWhenStreamingEnabled = prefs.getBoolean( + AppPreferenceKeys.KEEP_SCREEN_ON_WHEN_STREAMING, + AppDefaults.KEEP_SCREEN_ON_WHEN_STREAMING, ), devicePreviewCardHeightDp = prefs.getInt( - AppPreferenceKeys.DevicePreviewCardHeightDp, - AppDefaults.DefaultDevicePreviewCardHeightDp, + AppPreferenceKeys.DEVICE_PREVIEW_CARD_HEIGHT_DP, + AppDefaults.DEVICE_PREVIEW_CARD_HEIGHT_DP, ).coerceAtLeast(120), - keepScreenOnWhenStreamingEnabled = prefs.getBoolean( - AppPreferenceKeys.KeepScreenOnWhenStreamingEnabled, - AppDefaults.DefaultKeepScreenOnWhenStreamingEnabled, - ), virtualButtonsOutside = prefs.getString( - AppPreferenceKeys.VirtualButtonsOutside, - AppDefaults.DefaultVirtualButtonsOutside, - ).orEmpty().ifBlank { AppDefaults.DefaultVirtualButtonsOutside }, + AppPreferenceKeys.VIRTUAL_BUTTONS_OUTSIDE, + AppDefaults.VIRTUAL_BUTTONS_OUTSIDE, + ).orEmpty().ifBlank { AppDefaults.VIRTUAL_BUTTONS_OUTSIDE }, virtualButtonsInMore = prefs.getString( - AppPreferenceKeys.VirtualButtonsInMore, - AppDefaults.DefaultVirtualButtonsInMore, - ).orEmpty().ifBlank { AppDefaults.DefaultVirtualButtonsInMore }, - customServerUri = prefs.getString(AppPreferenceKeys.CustomServerUri, null), + AppPreferenceKeys.VIRTUAL_BUTTONS_IN_MORE, + AppDefaults.VIRTUAL_BUTTONS_IN_MORE, + ).orEmpty().ifBlank { AppDefaults.VIRTUAL_BUTTONS_IN_MORE }, + customServerUri = prefs.getString( + AppPreferenceKeys.CUSTOM_SERVER_URI, + AppDefaults.CUSTOM_SERVER_URI + ).orEmpty().ifBlank { null }, serverRemotePath = prefs.getString( - AppPreferenceKeys.ServerRemotePath, - AppDefaults.DefaultServerRemotePathInput, + AppPreferenceKeys.SERVER_REMOTE_PATH, + AppDefaults.SERVER_REMOTE_PATH_INPUT, + ).orEmpty(), + adbKeyName = prefs.getString( + AppPreferenceKeys.ADB_KEY_NAME, + AppDefaults.ADB_KEY_NAME_INPUT, ).orEmpty(), - videoCodec = prefs.getString(AppPreferenceKeys.VideoCodec, AppDefaults.DefaultVideoCodec) - .orEmpty() - .ifBlank { AppDefaults.DefaultVideoCodec }, - audioEnabled = prefs.getBoolean(AppPreferenceKeys.AudioEnabled, AppDefaults.DefaultAudioEnabled), - audioCodec = prefs.getString(AppPreferenceKeys.AudioCodec, AppDefaults.DefaultAudioCodec) - .orEmpty() - .ifBlank { AppDefaults.DefaultAudioCodec }, - adbKeyName = prefs.getString(AppPreferenceKeys.AdbKeyName, AppDefaults.DefaultAdbKeyNameInput).orEmpty(), ) } internal fun saveMainSettings(context: Context, settings: MainSettings) { - context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE) - .edit { - putInt(AppPreferenceKeys.ThemeBaseIndex, settings.themeBaseIndex) - .putBoolean(AppPreferenceKeys.MonetEnabled, settings.monetEnabled) - .putBoolean(AppPreferenceKeys.FullscreenDebugInfoEnabled, settings.fullscreenDebugInfoEnabled) - .putBoolean(AppPreferenceKeys.ShowFullscreenVirtualButtons, settings.showFullscreenVirtualButtons) - .putInt(AppPreferenceKeys.DevicePreviewCardHeightDp, settings.devicePreviewCardHeightDp.coerceAtLeast(120)) - .putBoolean(AppPreferenceKeys.KeepScreenOnWhenStreamingEnabled, settings.keepScreenOnWhenStreamingEnabled) - .putString(AppPreferenceKeys.VirtualButtonsOutside, settings.virtualButtonsOutside) - .putString(AppPreferenceKeys.VirtualButtonsInMore, settings.virtualButtonsInMore) - .putString(AppPreferenceKeys.CustomServerUri, settings.customServerUri) - .putString(AppPreferenceKeys.ServerRemotePath, settings.serverRemotePath) - .putString(AppPreferenceKeys.VideoCodec, settings.videoCodec) - .putBoolean(AppPreferenceKeys.AudioEnabled, settings.audioEnabled) - .putString(AppPreferenceKeys.AudioCodec, settings.audioCodec) - .putString(AppPreferenceKeys.AdbKeyName, settings.adbKeyName) - } + context.getSharedPreferences( + AppPreferenceKeys.PREFS_NAME, + Context.MODE_PRIVATE, + ).edit { + putBoolean( + AppPreferenceKeys.AUDIO_ENABLED, + settings.audioEnabled, + ) + .putString( + AppPreferenceKeys.AUDIO_CODEC, + settings.audioCodec, + ) + .putString( + AppPreferenceKeys.VIDEO_CODEC, + settings.videoCodec, + ) + .putInt( + AppPreferenceKeys.THEME_BASE_INDEX, + settings.themeBaseIndex, + ) + .putBoolean( + AppPreferenceKeys.MONET, + settings.monetEnabled, + ) + .putBoolean( + AppPreferenceKeys.FULLSCREEN_DEBUG_INFO, + settings.fullscreenDebugInfoEnabled, + ) + .putBoolean( + AppPreferenceKeys.SHOW_FULLSCREEN_VIRTUAL_BUTTONS, + settings.showFullscreenVirtualButtons, + ) + .putBoolean( + AppPreferenceKeys.KEEP_SCREEN_ON_WHEN_STREAMING, + settings.keepScreenOnWhenStreamingEnabled, + ) + .putInt( + AppPreferenceKeys.DEVICE_PREVIEW_CARD_HEIGHT_DP, + settings.devicePreviewCardHeightDp.coerceAtLeast(120), + ) + .putString( + AppPreferenceKeys.VIRTUAL_BUTTONS_OUTSIDE, + settings.virtualButtonsOutside, + ) + .putString( + AppPreferenceKeys.VIRTUAL_BUTTONS_IN_MORE, + settings.virtualButtonsInMore, + ) + .putString( + AppPreferenceKeys.CUSTOM_SERVER_URI, + settings.customServerUri, + ) + .putString( + AppPreferenceKeys.SERVER_REMOTE_PATH, + settings.serverRemotePath, + ) + .putString( + AppPreferenceKeys.ADB_KEY_NAME, + settings.adbKeyName, + ) + } } internal fun loadDevicePageSettings(context: Context): DevicePageSettings { - val prefs = context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE) + val prefs = context.getSharedPreferences( + AppPreferenceKeys.PREFS_NAME, + Context.MODE_PRIVATE, + ) + val audioBitRateKbps = prefs.getInt( + AppPreferenceKeys.AUDIO_BIT_RATE_KBPS, + AppDefaults.AUDIO_BIT_RATE_KBPS, + ) return DevicePageSettings( - pairHost = AppDefaults.DefaultPairHost, - pairPort = AppDefaults.DefaultPairPort, - pairCode = AppDefaults.DefaultPairCode, - quickConnectInput = prefs.getString(AppPreferenceKeys.QuickConnectInput, AppDefaults.DefaultQuickConnectInput).orEmpty(), - bitRateMbps = prefs.getFloat(AppPreferenceKeys.BitRateMbps, AppDefaults.DefaultBitRateMbps), - bitRateInput = prefs.getString(AppPreferenceKeys.BitRateInput, AppDefaults.DefaultBitRateInput) - .orEmpty() - .ifBlank { AppDefaults.DefaultBitRateInput }, - maxSizeInput = prefs.getString(AppPreferenceKeys.MaxSizeInput, AppDefaults.DefaultMaxSizeInput).orEmpty(), - audioBitRateKbps = prefs.getInt(AppPreferenceKeys.AudioBitRateKbps, AppDefaults.DefaultAudioBitRateKbps), - maxFpsInput = prefs.getString(AppPreferenceKeys.MaxFpsInput, AppDefaults.DefaultMaxFpsInput).orEmpty(), - noControl = prefs.getBoolean(AppPreferenceKeys.NoControl, AppDefaults.DefaultNoControl), - videoEncoder = prefs.getString(AppPreferenceKeys.VideoEncoder, AppDefaults.DefaultVideoEncoder).orEmpty(), - videoCodecOptions = prefs.getString(AppPreferenceKeys.VideoCodecOptions, AppDefaults.DefaultVideoCodecOptions).orEmpty(), - audioEncoder = prefs.getString(AppPreferenceKeys.AudioEncoder, AppDefaults.DefaultAudioEncoder).orEmpty(), - audioCodecOptions = prefs.getString(AppPreferenceKeys.AudioCodecOptions, AppDefaults.DefaultAudioCodecOptions).orEmpty(), - audioDup = prefs.getBoolean(AppPreferenceKeys.AudioDup, AppDefaults.DefaultAudioDup), - audioSourcePreset = prefs.getString(AppPreferenceKeys.AudioSourcePreset, AppDefaults.DefaultAudioSourcePreset) - .orEmpty() - .ifBlank { AppDefaults.DefaultAudioSourcePreset }, - audioSourceCustom = prefs.getString(AppPreferenceKeys.AudioSourceCustom, AppDefaults.DefaultAudioSourceCustom).orEmpty(), - videoSourcePreset = prefs.getString(AppPreferenceKeys.VideoSourcePreset, AppDefaults.DefaultVideoSourcePreset) - .orEmpty() - .ifBlank { AppDefaults.DefaultVideoSourcePreset }, - cameraIdInput = prefs.getString(AppPreferenceKeys.CameraIdInput, AppDefaults.DefaultCameraIdInput).orEmpty(), - cameraFacingPreset = prefs.getString(AppPreferenceKeys.CameraFacingPreset, AppDefaults.DefaultCameraFacingPreset).orEmpty(), - cameraSizePreset = prefs.getString(AppPreferenceKeys.CameraSizePreset, AppDefaults.DefaultCameraSizePreset).orEmpty(), - cameraSizeCustom = prefs.getString(AppPreferenceKeys.CameraSizeCustom, AppDefaults.DefaultCameraSizeCustom).orEmpty(), - cameraArInput = prefs.getString(AppPreferenceKeys.CameraArInput, AppDefaults.DefaultCameraArInput).orEmpty(), - cameraFpsInput = prefs.getString(AppPreferenceKeys.CameraFpsInput, AppDefaults.DefaultCameraFpsInput).orEmpty(), - cameraHighSpeed = prefs.getBoolean(AppPreferenceKeys.CameraHighSpeed, AppDefaults.DefaultCameraHighSpeed), - noAudioPlayback = prefs.getBoolean(AppPreferenceKeys.NoAudioPlayback, AppDefaults.DefaultNoAudioPlayback), - noVideo = prefs.getBoolean(AppPreferenceKeys.NoVideo, AppDefaults.DefaultNoVideo), - requireAudio = prefs.getBoolean(AppPreferenceKeys.RequireAudio, AppDefaults.DefaultRequireAudio), - turnScreenOff = prefs.getBoolean(AppPreferenceKeys.TurnScreenOff, AppDefaults.DefaultTurnScreenOff), - newDisplayWidth = prefs.getString(AppPreferenceKeys.NewDisplayWidth, AppDefaults.DefaultNewDisplayWidth).orEmpty(), - newDisplayHeight = prefs.getString(AppPreferenceKeys.NewDisplayHeight, AppDefaults.DefaultNewDisplayHeight).orEmpty(), - newDisplayDpi = prefs.getString(AppPreferenceKeys.NewDisplayDpi, AppDefaults.DefaultNewDisplayDpi).orEmpty(), - displayIdInput = prefs.getString(AppPreferenceKeys.DisplayIdInput, AppDefaults.DefaultDisplayIdInput).orEmpty(), - cropWidth = prefs.getString(AppPreferenceKeys.CropWidth, AppDefaults.DefaultCropWidth).orEmpty(), - cropHeight = prefs.getString(AppPreferenceKeys.CropHeight, AppDefaults.DefaultCropHeight).orEmpty(), - cropX = prefs.getString(AppPreferenceKeys.CropX, AppDefaults.DefaultCropX).orEmpty(), - cropY = prefs.getString(AppPreferenceKeys.CropY, AppDefaults.DefaultCropY).orEmpty(), + quickConnectInput = prefs.getString( + AppPreferenceKeys.QUICK_CONNECT_INPUT, + AppDefaults.QUICK_CONNECT_INPUT, + ).orEmpty(), + pairHost = AppDefaults.PAIR_HOST, + pairPort = AppDefaults.PAIR_PORT, + pairCode = AppDefaults.PAIR_CODE, + audioBitRateKbps = audioBitRateKbps, + audioBitRateInput = prefs.getString( + AppPreferenceKeys.AUDIO_BIT_RATE_INPUT, + AppDefaults.AUDIO_BIT_RATE_INPUT, + ).orEmpty().ifBlank { audioBitRateKbps.toString() }, + videoBitRateMbps = prefs.getFloat( + AppPreferenceKeys.VIDEO_BIT_RATE_MBPS, + AppDefaults.VIDEO_BIT_RATE_MBPS, + ), + videoBitRateInput = prefs.getString( + AppPreferenceKeys.VIDEO_BIT_RATE_INPUT, + AppDefaults.VIDEO_BIT_RATE_INPUT + ).orEmpty().ifBlank { AppDefaults.VIDEO_BIT_RATE_INPUT }, + turnScreenOff = prefs.getBoolean( + AppPreferenceKeys.TURN_SCREEN_OFF, + AppDefaults.TURN_SCREEN_OFF, + ), + noControl = prefs.getBoolean( + AppPreferenceKeys.NO_CONTROL, + AppDefaults.NO_CONTROL, + ), + noVideo = prefs.getBoolean( + AppPreferenceKeys.NO_VIDEO, + AppDefaults.NO_VIDEO, + ), + videoSourcePreset = prefs.getString( + AppPreferenceKeys.VIDEO_SOURCE_PRESET, + AppDefaults.VIDEO_SOURCE_PRESET, + ).orEmpty().ifBlank { AppDefaults.VIDEO_SOURCE_PRESET }, + displayIdInput = prefs.getString( + AppPreferenceKeys.DISPLAY_ID, + AppDefaults.DISPLAY_ID, + ) + .orEmpty(), + cameraIdInput = prefs.getString( + AppPreferenceKeys.CAMERA_ID, + AppDefaults.CAMERA_ID, + ) + .orEmpty(), + cameraFacingPreset = prefs.getString( + AppPreferenceKeys.CAMERA_FACING_PRESET, + AppDefaults.CAMERA_FACING_PRESET, + ).orEmpty(), + cameraSizePreset = prefs.getString( + AppPreferenceKeys.CAMERA_SIZE_PRESET, + AppDefaults.CAMERA_SIZE_PRESET, + ).orEmpty(), + cameraSizeCustom = prefs.getString( + AppPreferenceKeys.CAMERA_SIZE_CUSTOM, + AppDefaults.CAMERA_SIZE_CUSTOM, + ).orEmpty(), + cameraAr = prefs.getString( + AppPreferenceKeys.CAMERA_AR, + AppDefaults.CAMERA_AR, + ).orEmpty(), + cameraFps = prefs.getString( + AppPreferenceKeys.CAMERA_FPS, + AppDefaults.CAMERA_FPS, + ).orEmpty(), + cameraHighSpeed = prefs.getBoolean( + AppPreferenceKeys.CAMERA_HIGH_SPEED, + AppDefaults.CAMERA_HIGH_SPEED, + ), + audioSourcePreset = prefs.getString( + AppPreferenceKeys.AUDIO_SOURCE_PRESET, + AppDefaults.AUDIO_SOURCE_PRESET, + ).orEmpty().ifBlank { AppDefaults.AUDIO_SOURCE_PRESET }, + audioSourceCustom = prefs.getString( + AppPreferenceKeys.AUDIO_SOURCE_CUSTOM, + AppDefaults.AUDIO_SOURCE_CUSTOM, + ).orEmpty(), + audioDup = prefs.getBoolean( + AppPreferenceKeys.AUDIO_DUP, + AppDefaults.AUDIO_DUP, + ), + noAudioPlayback = prefs.getBoolean( + AppPreferenceKeys.NO_AUDIO_PLAYBACK, + AppDefaults.NO_AUDIO_PLAYBACK, + ), + requireAudio = prefs.getBoolean( + AppPreferenceKeys.REQUIRE_AUDIO, + AppDefaults.REQUIRE_AUDIO, + ), + maxSizeInput = prefs.getString( + AppPreferenceKeys.MAX_SIZE_INPUT, + AppDefaults.MAX_SIZE_INPUT, + ) + .orEmpty(), + maxFpsInput = prefs.getString( + AppPreferenceKeys.MAX_FPS_INPUT, + AppDefaults.MAX_FPS_INPUT, + ) + .orEmpty(), + videoEncoder = prefs.getString( + AppPreferenceKeys.VIDEO_ENCODER, + AppDefaults.VIDEO_ENCODER, + ) + .orEmpty(), + videoCodecOptions = prefs.getString( + AppPreferenceKeys.VIDEO_CODEC_OPTION, + AppDefaults.VIDEO_CODEC_OPTION, + ).orEmpty(), + audioEncoder = prefs.getString( + AppPreferenceKeys.AUDIO_ENCODER, + AppDefaults.AUDIO_ENCODER, + ).orEmpty(), + audioCodecOptions = prefs.getString( + AppPreferenceKeys.AUDIO_CODEC_OPTION, + AppDefaults.AUDIO_CODEC_OPTION, + ).orEmpty(), + newDisplayWidth = prefs.getString( + AppPreferenceKeys.NEW_DISPLAY_WIDTH, + AppDefaults.NEW_DISPLAY_WIDTH, + ).orEmpty(), + newDisplayHeight = prefs.getString( + AppPreferenceKeys.NEW_DISPLAY_HEIGHT, + AppDefaults.NEW_DISPLAY_HEIGHT, + ).orEmpty(), + newDisplayDpi = prefs.getString( + AppPreferenceKeys.NEW_DISPLAY_DPI, + AppDefaults.NEW_DISPLAY_DPI, + ).orEmpty(), + cropWidth = prefs.getString( + AppPreferenceKeys.CROP_WIDTH, + AppDefaults.CROP_WIDTH, + ).orEmpty(), + cropHeight = prefs.getString( + AppPreferenceKeys.CROP_HEIGHT, + AppDefaults.CROP_HEIGHT, + ).orEmpty(), + cropX = prefs.getString( + AppPreferenceKeys.CROP_X, + AppDefaults.CROP_X, + ).orEmpty(), + cropY = prefs.getString( + AppPreferenceKeys.CROP_Y, + AppDefaults.CROP_Y, + ).orEmpty(), ) } internal fun saveDevicePageSettings(context: Context, settings: DevicePageSettings) { - context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE) + context.getSharedPreferences(AppPreferenceKeys.PREFS_NAME, Context.MODE_PRIVATE) .edit { - remove(AppPreferenceKeys.PairHost) - .remove(AppPreferenceKeys.PairPort) - .remove(AppPreferenceKeys.PairCode) - .putString(AppPreferenceKeys.QuickConnectInput, settings.quickConnectInput) - .putFloat(AppPreferenceKeys.BitRateMbps, settings.bitRateMbps) - .putString(AppPreferenceKeys.BitRateInput, settings.bitRateInput) - .putInt(AppPreferenceKeys.AudioBitRateKbps, settings.audioBitRateKbps) - .putString(AppPreferenceKeys.MaxSizeInput, settings.maxSizeInput) - .putString(AppPreferenceKeys.MaxFpsInput, settings.maxFpsInput) - .putBoolean(AppPreferenceKeys.NoControl, settings.noControl) - .putString(AppPreferenceKeys.VideoEncoder, settings.videoEncoder) - .putString(AppPreferenceKeys.VideoCodecOptions, settings.videoCodecOptions) - .putString(AppPreferenceKeys.AudioEncoder, settings.audioEncoder) - .putString(AppPreferenceKeys.AudioCodecOptions, settings.audioCodecOptions) - .putBoolean(AppPreferenceKeys.AudioDup, settings.audioDup) - .putString(AppPreferenceKeys.AudioSourcePreset, settings.audioSourcePreset) - .putString(AppPreferenceKeys.AudioSourceCustom, settings.audioSourceCustom) - .putString(AppPreferenceKeys.VideoSourcePreset, settings.videoSourcePreset) - .putString(AppPreferenceKeys.CameraIdInput, settings.cameraIdInput) - .putString(AppPreferenceKeys.CameraFacingPreset, settings.cameraFacingPreset) - .putString(AppPreferenceKeys.CameraSizePreset, settings.cameraSizePreset) - .putString(AppPreferenceKeys.CameraSizeCustom, settings.cameraSizeCustom) - .putString(AppPreferenceKeys.CameraArInput, settings.cameraArInput) - .putString(AppPreferenceKeys.CameraFpsInput, settings.cameraFpsInput) - .putBoolean(AppPreferenceKeys.CameraHighSpeed, settings.cameraHighSpeed) - .putBoolean(AppPreferenceKeys.NoAudioPlayback, settings.noAudioPlayback) - .putBoolean(AppPreferenceKeys.NoVideo, settings.noVideo) - .putBoolean(AppPreferenceKeys.RequireAudio, settings.requireAudio) - .putBoolean(AppPreferenceKeys.TurnScreenOff, settings.turnScreenOff) - .putString(AppPreferenceKeys.NewDisplayWidth, settings.newDisplayWidth) - .putString(AppPreferenceKeys.NewDisplayHeight, settings.newDisplayHeight) - .putString(AppPreferenceKeys.NewDisplayDpi, settings.newDisplayDpi) - .putString(AppPreferenceKeys.DisplayIdInput, settings.displayIdInput) - .putString(AppPreferenceKeys.CropWidth, settings.cropWidth) - .putString(AppPreferenceKeys.CropHeight, settings.cropHeight) - .putString(AppPreferenceKeys.CropX, settings.cropX) - .putString(AppPreferenceKeys.CropY, settings.cropY) + remove(AppPreferenceKeys.PAIR_HOST) + .remove(AppPreferenceKeys.PAIR_PORT) + .remove(AppPreferenceKeys.PAIR_CODE) + .putString( + AppPreferenceKeys.QUICK_CONNECT_INPUT, + settings.quickConnectInput, + ) + .putInt( + AppPreferenceKeys.AUDIO_BIT_RATE_KBPS, + settings.audioBitRateKbps, + ) + .putString( + AppPreferenceKeys.AUDIO_BIT_RATE_INPUT, + settings.audioBitRateInput, + ) + .putFloat( + AppPreferenceKeys.VIDEO_BIT_RATE_MBPS, + settings.videoBitRateMbps, + ) + .putString( + AppPreferenceKeys.VIDEO_BIT_RATE_INPUT, + settings.videoBitRateInput, + ) + .putBoolean( + AppPreferenceKeys.TURN_SCREEN_OFF, + settings.turnScreenOff, + ) + .putBoolean( + AppPreferenceKeys.NO_CONTROL, + settings.noControl, + ) + .putBoolean( + AppPreferenceKeys.NO_VIDEO, + settings.noVideo, + ) + .putString( + AppPreferenceKeys.VIDEO_SOURCE_PRESET, + settings.videoSourcePreset, + ) + .putString( + AppPreferenceKeys.DISPLAY_ID, + settings.displayIdInput, + ) + .putString( + AppPreferenceKeys.CAMERA_ID, + settings.cameraIdInput, + ) + .putString( + AppPreferenceKeys.CAMERA_FACING_PRESET, + settings.cameraFacingPreset, + ) + .putString( + AppPreferenceKeys.CAMERA_SIZE_PRESET, + settings.cameraSizePreset, + ) + .putString( + AppPreferenceKeys.CAMERA_SIZE_CUSTOM, + settings.cameraSizeCustom, + ) + .putString( + AppPreferenceKeys.CAMERA_AR, + settings.cameraAr, + ) + .putString( + AppPreferenceKeys.CAMERA_FPS, + settings.cameraFps, + ) + .putBoolean( + AppPreferenceKeys.CAMERA_HIGH_SPEED, + settings.cameraHighSpeed, + ) + .putString( + AppPreferenceKeys.AUDIO_SOURCE_PRESET, + settings.audioSourcePreset, + ) + .putString( + AppPreferenceKeys.AUDIO_SOURCE_CUSTOM, + settings.audioSourceCustom, + ) + .putBoolean( + AppPreferenceKeys.AUDIO_DUP, + settings.audioDup, + ) + .putBoolean( + AppPreferenceKeys.NO_AUDIO_PLAYBACK, + settings.noAudioPlayback, + ) + .putBoolean( + AppPreferenceKeys.REQUIRE_AUDIO, + settings.requireAudio, + ) + .putString( + AppPreferenceKeys.MAX_SIZE_INPUT, + settings.maxSizeInput, + ) + .putString( + AppPreferenceKeys.MAX_FPS_INPUT, + settings.maxFpsInput, + ) + .putString( + AppPreferenceKeys.VIDEO_ENCODER, + settings.videoEncoder, + ) + .putString( + AppPreferenceKeys.VIDEO_CODEC_OPTION, + settings.videoCodecOptions, + ) + .putString( + AppPreferenceKeys.AUDIO_ENCODER, + settings.audioEncoder, + ) + .putString( + AppPreferenceKeys.AUDIO_CODEC_OPTION, + settings.audioCodecOptions, + ) + .putString( + AppPreferenceKeys.NEW_DISPLAY_WIDTH, + settings.newDisplayWidth, + ) + .putString( + AppPreferenceKeys.NEW_DISPLAY_HEIGHT, + settings.newDisplayHeight, + ) + .putString( + AppPreferenceKeys.NEW_DISPLAY_DPI, + settings.newDisplayDpi, + ) + .putString( + AppPreferenceKeys.CROP_WIDTH, + settings.cropWidth, + ) + .putString( + AppPreferenceKeys.CROP_HEIGHT, + settings.cropHeight, + ) + .putString( + AppPreferenceKeys.CROP_X, + settings.cropX, + ) + .putString( + AppPreferenceKeys.CROP_Y, + settings.cropY, + ) } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/DeviceConnectionLogic.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/DeviceConnectionLogic.kt index 6dcaf27..d7c3d3a 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/DeviceConnectionLogic.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/DeviceConnectionLogic.kt @@ -12,8 +12,13 @@ internal data class ConnectedDeviceInfo( val sdkInt: Int, ) -internal fun fetchConnectedDeviceInfo(nativeCore: NativeCoreFacade, host: String, port: Int): ConnectedDeviceInfo { - fun prop(name: String): String = runCatching { nativeCore.adbShell("getprop $name").trim() }.getOrDefault("") +internal fun fetchConnectedDeviceInfo( + nativeCore: NativeCoreFacade, + host: String, + port: Int +): ConnectedDeviceInfo { + fun prop(name: String): String = + runCatching { nativeCore.adbShell("getprop $name").trim() }.getOrDefault("") val model = prop("ro.product.model") val serial = prop("ro.serialno") 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 5a2b1bc..467a44f 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 @@ -8,8 +8,8 @@ import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut internal fun loadQuickDevices(context: Context): List { - val raw = context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE) - .getString(AppPreferenceKeys.QuickDevices, "") + val raw = context.getSharedPreferences(AppPreferenceKeys.PREFS_NAME, Context.MODE_PRIVATE) + .getString(AppPreferenceKeys.QUICK_DEVICES, "") .orEmpty() if (raw.isBlank()) return emptyList() @@ -21,7 +21,7 @@ internal fun loadQuickDevices(context: Context): List { 3 -> { val name = parts[0].trim() val host = parts[1].trim() - val port = parts[2].trim().toIntOrNull() ?: AppDefaults.DefaultAdbPort + val port = parts[2].trim().toIntOrNull() ?: AppDefaults.ADB_PORT if (host.isNotBlank()) { result.add( DeviceShortcut( @@ -39,7 +39,8 @@ internal fun loadQuickDevices(context: Context): List { // Backward compatibility with old format: name|host:port val name = parts[0].trim() val host = parts[1].substringBefore(":").trim() - val port = parts[1].substringAfter(":", AppDefaults.DefaultAdbPort.toString()).trim().toIntOrNull() ?: AppDefaults.DefaultAdbPort + val port = parts[1].substringAfter(":", AppDefaults.ADB_PORT.toString()).trim() + .toIntOrNull() ?: AppDefaults.ADB_PORT if (host.isNotBlank()) { result.add( DeviceShortcut( @@ -59,9 +60,9 @@ internal fun loadQuickDevices(context: Context): List { internal fun saveQuickDevices(context: Context, quickDevices: List) { val raw = quickDevices.joinToString("\n") { "${it.name}|${it.host}|${it.port}" } - context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE) + context.getSharedPreferences(AppPreferenceKeys.PREFS_NAME, Context.MODE_PRIVATE) .edit { - putString(AppPreferenceKeys.QuickDevices, raw) + putString(AppPreferenceKeys.QUICK_DEVICES, raw) } } @@ -70,7 +71,8 @@ internal fun parseQuickTarget(raw: String): ConnectionTarget? { if (value.isEmpty()) return null val host = value.substringBefore(':').trim() if (host.isEmpty()) return null - val port = value.substringAfter(':', AppDefaults.DefaultAdbPort.toString()).trim().toIntOrNull() ?: AppDefaults.DefaultAdbPort + val port = value.substringAfter(':', AppDefaults.ADB_PORT.toString()).trim().toIntOrNull() + ?: AppDefaults.ADB_PORT return ConnectionTarget(host, port) } 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 e1180df..e5044fb 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 @@ -13,7 +13,6 @@ import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -32,11 +31,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.AddLink import androidx.compose.material.icons.filled.Fullscreen -import androidx.compose.material.icons.filled.Home -import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.rounded.CheckCircleOutline import androidx.compose.material.icons.rounded.LinkOff import androidx.compose.material.icons.rounded.Wifi @@ -63,7 +59,6 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView @@ -96,7 +91,7 @@ import kotlin.math.roundToInt private val VIDEO_CODEC_OPTIONS = listOf( "h264" to "H.264", "h265" to "H.265", - "av1" to "AV1", + "av1" to "AV1", ) private val AUDIO_CODEC_OPTIONS = listOf( @@ -266,9 +261,13 @@ internal fun PreviewCard( } val containerAspect = maxWidth.value / maxHeight.value val fittedModifier = if (sessionAspect > containerAspect) { - Modifier.fillMaxWidth().aspectRatio(sessionAspect) + Modifier + .fillMaxWidth() + .aspectRatio(sessionAspect) } else { - Modifier.fillMaxHeight().aspectRatio(sessionAspect) + Modifier + .fillMaxHeight() + .aspectRatio(sessionAspect) } Box( @@ -358,10 +357,13 @@ internal fun ConfigPanel( sessionStarted: Boolean, ) { val videoCodecItems = remember { VIDEO_CODEC_OPTIONS.map { it.second } } - val videoCodecIndex = VIDEO_CODEC_OPTIONS.indexOfFirst { it.first == videoCodec }.coerceAtLeast(0) + val videoCodecIndex = + VIDEO_CODEC_OPTIONS.indexOfFirst { it.first == videoCodec }.coerceAtLeast(0) val audioCodecItems = remember { AUDIO_CODEC_OPTIONS.map { it.second } } - val audioCodecIndex = AUDIO_CODEC_OPTIONS.indexOfFirst { it.first == audioCodec }.coerceAtLeast(0) - val audioBitRatePresetIndex = presetIndexFromInput(audioBitRateKbps.toString(), ScrcpyPresets.AudioBitRate) + val audioCodecIndex = + AUDIO_CODEC_OPTIONS.indexOfFirst { it.first == audioCodec }.coerceAtLeast(0) + val audioBitRatePresetIndex = + presetIndexFromInput(audioBitRateKbps.toString(), ScrcpyPresets.AudioBitRate) SectionSmallTitle(text = "Scrcpy") Card { @@ -433,6 +435,7 @@ internal fun ConfigPanel( dotUsed = true true } + else -> false } } @@ -506,7 +509,9 @@ private fun PairingDialog( onValueChange = { host = it }, label = "IP 地址", singleLine = true, - modifier = Modifier.fillMaxWidth().padding(bottom = UiSpacing.CardContent), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = UiSpacing.CardContent), ) TextField( value = port, @@ -514,14 +519,18 @@ private fun PairingDialog( label = "端口", singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth().padding(bottom = UiSpacing.CardContent), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = UiSpacing.CardContent), ) TextField( value = code, onValueChange = { code = it }, label = "WLAN 配对码", singleLine = true, - modifier = Modifier.fillMaxWidth().padding(bottom = UiSpacing.Large), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = UiSpacing.Large), ) Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.PopupHorizontal)) { TextButton( @@ -599,8 +608,10 @@ fun FullscreenControlScreen( } fun mapToDevice(rawX: Float, rawY: Float): Pair { - val x = ((rawX / touchAreaSize.width) * session.width).roundToInt().coerceIn(0, (session.width - 1).coerceAtLeast(0)) - val y = ((rawY / touchAreaSize.height) * session.height).roundToInt().coerceIn(0, (session.height - 1).coerceAtLeast(0)) + val x = ((rawX / touchAreaSize.width) * session.width).roundToInt() + .coerceIn(0, (session.width - 1).coerceAtLeast(0)) + val y = ((rawY / touchAreaSize.height) * session.height).roundToInt() + .coerceIn(0, (session.height - 1).coerceAtLeast(0)) return x to y } @@ -612,7 +623,14 @@ fun FullscreenControlScreen( val py = event.getY(i) activePointerPositions[pointerId] = Offset(px, py) val (x, y) = mapToDevice(px, py) - onInjectTouch(UiMotionActions.MOVE, pointerId.toLong(), x, y, event.getPressure(i).coerceIn(0f, 1f), 1) + onInjectTouch( + UiMotionActions.MOVE, + pointerId.toLong(), + x, + y, + event.getPressure(i).coerceIn(0f, 1f), + 1 + ) } } @@ -626,7 +644,14 @@ fun FullscreenControlScreen( activePointerIds += pointerId activePointerPositions[pointerId] = Offset(event.x, event.y) activeTouchCount = activePointerIds.size - onInjectTouch(UiMotionActions.DOWN, pointerId.toLong(), x, y, event.getPressure(0).coerceIn(0f, 1f), 1) + onInjectTouch( + UiMotionActions.DOWN, + pointerId.toLong(), + x, + y, + event.getPressure(0).coerceIn(0f, 1f), + 1 + ) } MotionEvent.ACTION_POINTER_DOWN -> { @@ -638,7 +663,14 @@ fun FullscreenControlScreen( activePointerIds += pointerId activePointerPositions[pointerId] = Offset(px, py) activeTouchCount = activePointerIds.size - onInjectTouch(UiMotionActions.POINTER_DOWN, pointerId.toLong(), x, y, event.getPressure(index).coerceIn(0f, 1f), 1) + onInjectTouch( + UiMotionActions.POINTER_DOWN, + pointerId.toLong(), + x, + y, + event.getPressure(index).coerceIn(0f, 1f), + 1 + ) syncActivePointersFromEvent() } @@ -650,7 +682,14 @@ fun FullscreenControlScreen( val py = event.getY(i) activePointerPositions[pointerId] = Offset(px, py) val (x, y) = mapToDevice(px, py) - onInjectTouch(UiMotionActions.MOVE, pointerId.toLong(), x, y, event.getPressure(i).coerceIn(0f, 1f), 1) + onInjectTouch( + UiMotionActions.MOVE, + pointerId.toLong(), + x, + y, + event.getPressure(i).coerceIn(0f, 1f), + 1 + ) } } @@ -698,9 +737,13 @@ fun FullscreenControlScreen( } val containerAspect = maxWidth.value / maxHeight.value val fittedModifier = if (sessionAspect > containerAspect) { - Modifier.fillMaxWidth().aspectRatio(sessionAspect) + Modifier + .fillMaxWidth() + .aspectRatio(sessionAspect) } else { - Modifier.fillMaxHeight().aspectRatio(sessionAspect) + Modifier + .fillMaxHeight() + .aspectRatio(sessionAspect) } Box( @@ -780,13 +823,21 @@ private fun ScrcpyVideoSurface( factory = { context -> TextureView(context).apply { surfaceTextureListener = object : TextureView.SurfaceTextureListener { - override fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture, width: Int, height: Int) { + override fun onSurfaceTextureAvailable( + surfaceTexture: SurfaceTexture, + width: Int, + height: Int + ) { currentSurface?.release() // Release stale surface if any @SuppressLint("Recycle") currentSurface = Surface(surfaceTexture) } - override fun onSurfaceTextureSizeChanged(surfaceTexture: SurfaceTexture, width: Int, height: Int) = Unit + override fun onSurfaceTextureSizeChanged( + surfaceTexture: SurfaceTexture, + width: Int, + height: Int + ) = Unit override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean { val released = currentSurface @@ -820,7 +871,7 @@ internal fun DeviceTile( Card( colors = CardDefaults.defaultColors( color = if (device.online) MiuixTheme.colorScheme.surfaceContainer - else MiuixTheme.colorScheme.surfaceContainer.copy(alpha = 0.6f), + else MiuixTheme.colorScheme.surfaceContainer.copy(alpha = 0.6f), ), pressFeedbackType = PressFeedbackType.Sink, onClick = haptics.press, @@ -1016,7 +1067,9 @@ internal fun DeviceEditorScreen( .padding(top = UiSpacing.CardContent), ) Row( - modifier = Modifier.fillMaxWidth().padding(UiSpacing.CardContent), + modifier = Modifier + .fillMaxWidth() + .padding(UiSpacing.CardContent), horizontalArrangement = Arrangement.spacedBy(UiSpacing.Medium), ) { TextButton( @@ -1032,7 +1085,7 @@ internal fun DeviceEditorScreen( TextButton( text = "保存", onClick = { - val p = port.toIntOrNull() ?: AppDefaults.DefaultAdbPort + val p = port.toIntOrNull() ?: AppDefaults.ADB_PORT val h = host.trim() if (h.isNotBlank()) { onSave( diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/SortableCardList.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/SortableCardList.kt index 3834c5e..365d5f1 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/SortableCardList.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/SortableCardList.kt @@ -110,7 +110,10 @@ fun SortableCardList( Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = UiSpacing.CardContent, vertical = UiSpacing.FieldLabelBottom), + .padding( + horizontal = UiSpacing.CardContent, + vertical = UiSpacing.FieldLabelBottom + ), verticalAlignment = Alignment.CenterVertically, ) { Column(modifier = Modifier.weight(1f)) { diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/StatusCardModels.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/StatusCardModels.kt index 2809ddc..b5ffbbc 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/StatusCardModels.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/StatusCardModels.kt @@ -69,12 +69,16 @@ internal fun StatusCardLayout( ) { val haptics = rememberAppHaptics() Row( - modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min), + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min), horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem), verticalAlignment = Alignment.CenterVertically, ) { Card( - modifier = Modifier.weight(1f).fillMaxHeight(), + modifier = Modifier + .weight(1f) + .fillMaxHeight(), colors = defaultColors(color = spec.big.containerColor), pressFeedbackType = PressFeedbackType.Tilt, onClick = haptics.press, @@ -125,15 +129,21 @@ internal fun StatusCardLayout( } } - Column(modifier = Modifier.weight(1f).fillMaxHeight()) { + Column(modifier = Modifier + .weight(1f) + .fillMaxHeight()) { StatusMetricCard( spec = spec.firstSmall, - modifier = Modifier.fillMaxWidth().weight(1f), + modifier = Modifier + .fillMaxWidth() + .weight(1f), ) Spacer(Modifier.height(UiSpacing.PageItem)) StatusMetricCard( spec = spec.secondSmall, - modifier = Modifier.fillMaxWidth().weight(1f), + modifier = Modifier + .fillMaxWidth() + .weight(1f), ) } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt index ceb55ff..c764646 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt @@ -19,9 +19,6 @@ import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.PhotoCamera import androidx.compose.material.icons.filled.PowerSettingsNew -import androidx.compose.material.icons.filled.VolumeDown -import androidx.compose.material.icons.filled.VolumeOff -import androidx.compose.material.icons.filled.VolumeUp import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -50,36 +47,76 @@ enum class VirtualButtonAction( val icon: ImageVector, val keycode: Int?, ) { - MORE("more", "更多", Icons.Default.MoreVert, null), - HOME("home", "主页", Icons.Default.Home, UiAndroidKeycodes.HOME), - BACK("back", "返回", Icons.AutoMirrored.Filled.ArrowBack, UiAndroidKeycodes.BACK), - APP_SWITCH("app_switch", "多任务", Icons.Default.Apps, UiAndroidKeycodes.APP_SWITCH), - MENU("menu", "菜单", Icons.Default.Menu, UiAndroidKeycodes.MENU), - NOTIFICATION("notification", "通知栏", Icons.Default.Notifications, UiAndroidKeycodes.NOTIFICATION), - VOLUME_UP("volume_up", "音量+", Icons.AutoMirrored.Filled.VolumeUp, UiAndroidKeycodes.VOLUME_UP), - VOLUME_DOWN("volume_down", "音量-", Icons.AutoMirrored.Filled.VolumeDown, UiAndroidKeycodes.VOLUME_DOWN), - VOLUME_MUTE("volume_mute", "静音", Icons.AutoMirrored.Filled.VolumeOff, UiAndroidKeycodes.VOLUME_MUTE), - POWER("power", "锁屏", Icons.Default.PowerSettingsNew, UiAndroidKeycodes.POWER), - SCREENSHOT("screenshot", "截图", Icons.Default.PhotoCamera, UiAndroidKeycodes.SYSRQ), + MORE( + "more", + "更多", + Icons.Default.MoreVert, + null + ), + HOME( + "home", + "主页", + Icons.Default.Home, + UiAndroidKeycodes.HOME + ), + BACK( + "back", + "返回", + Icons.AutoMirrored.Filled.ArrowBack, + UiAndroidKeycodes.BACK + ), + APP_SWITCH( + "app_switch", + "多任务", + Icons.Default.Apps, + UiAndroidKeycodes.APP_SWITCH + ), + MENU( + "menu", + "菜单", + Icons.Default.Menu, + UiAndroidKeycodes.MENU + ), + NOTIFICATION( + "notification", + "通知栏", + Icons.Default.Notifications, + UiAndroidKeycodes.NOTIFICATION + ), + VOLUME_UP( + "volume_up", + "音量+", + Icons.AutoMirrored.Filled.VolumeUp, + UiAndroidKeycodes.VOLUME_UP + ), + VOLUME_DOWN( + "volume_down", + "音量-", + Icons.AutoMirrored.Filled.VolumeDown, + UiAndroidKeycodes.VOLUME_DOWN + ), + VOLUME_MUTE( + "volume_mute", + "静音", + Icons.AutoMirrored.Filled.VolumeOff, + UiAndroidKeycodes.VOLUME_MUTE + ), + POWER( + "power", + "锁屏", + Icons.Default.PowerSettingsNew, + UiAndroidKeycodes.POWER + ), + SCREENSHOT( + "screenshot", + "截图", + Icons.Default.PhotoCamera, + UiAndroidKeycodes.SYSRQ + ), } object VirtualButtonActions { val all = VirtualButtonAction.entries - val defaultOutsideIds = listOf( - VirtualButtonAction.MORE.id, - VirtualButtonAction.HOME.id, - VirtualButtonAction.BACK.id, - ) - val defaultMoreIds = listOf( - VirtualButtonAction.APP_SWITCH.id, - VirtualButtonAction.MENU.id, - VirtualButtonAction.NOTIFICATION.id, - VirtualButtonAction.VOLUME_UP.id, - VirtualButtonAction.VOLUME_DOWN.id, - VirtualButtonAction.VOLUME_MUTE.id, - VirtualButtonAction.POWER.id, - VirtualButtonAction.SCREENSHOT.id, - ) private val byId = all.associateBy { it.id }