From f6fb2d68e51c0a12ecb0aa9463e843dfd23c8b01 Mon Sep 17 00:00:00 2001 From: wgqqqqq Date: Fri, 11 Sep 2026 11:48:56 +0800 Subject: [PATCH 1/2] feat(mobile): align native connection flows --- src/apps/mobile/android/app/build.gradle.kts | 6 +- .../app/src/androidTest/assets/pairing-qr.png | Bin 0 -> 773 bytes .../mobile/app/ConnectionSheetChromeTest.kt | 57 ++++ .../mobile/app/InlineQrDecoderTest.kt | 34 +++ .../app/MobilePreviewStatesVisualTest.kt | 5 +- .../android/app/src/main/AndroidManifest.xml | 6 +- .../infrastructure/camera/InlineQrScanner.kt | 114 ++++++++ .../mobile/app/state/AppShellState.kt | 17 ++ .../mobile/app/ui/account/AccountScreen.kt | 57 ++-- .../app/ui/common/AdaptiveModalSurface.kt | 20 +- .../app/ui/common/ConnectionSheetChrome.kt | 54 ++++ .../ui/common/SignedOutConnectionActions.kt | 11 +- .../mobile/app/ui/remote/ConnectView.kt | 255 +++++++----------- .../mobile/app/ui/remote/PairingScreen.kt | 47 +++- .../mobile/app/ui/shell/MobileScreen.kt | 86 +++--- .../openbitfun/mobile/app/ui/theme/Theme.kt | 8 +- .../app/src/main/res/values-zh/strings.xml | 9 +- .../app/src/main/res/values/strings.xml | 9 +- .../mobile/app/state/AppShellStateTest.kt | 52 ++++ .../components/mobile-components.json | 7 + .../preview/generated/mobile-design-data.js | 24 ++ .../pages/components/AppRootPresentation.ets | 2 + .../main/ets/pages/components/AppShell.ets | 14 +- .../components/OpenBitFunAccountLoginPage.ets | 3 +- .../components/RemoteControlSettingsSheet.ets | 2 +- .../Account/AccountSettingsView.swift | 90 +++---- .../AdaptiveModalComponents.swift | 93 ++++++- .../Features/Pairing/PairingSheet.swift | 227 ++++++++-------- .../Features/Remote/RemoteHomeViews.swift | 9 +- .../Features/Shell/MobileShellView.swift | 12 +- .../Resources/Localizable.xcstrings | 16 ++ .../core/feature/account/AccountUiState.kt | 6 - 32 files changed, 895 insertions(+), 457 deletions(-) create mode 100644 src/apps/mobile/android/app/src/androidTest/assets/pairing-qr.png create mode 100644 src/apps/mobile/android/app/src/androidTest/kotlin/com/openbitfun/mobile/app/ConnectionSheetChromeTest.kt create mode 100644 src/apps/mobile/android/app/src/androidTest/kotlin/com/openbitfun/mobile/app/InlineQrDecoderTest.kt create mode 100644 src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/infrastructure/camera/InlineQrScanner.kt create mode 100644 src/apps/mobile/android/app/src/main/kotlin/com/openbitfun/mobile/app/ui/common/ConnectionSheetChrome.kt create mode 100644 src/apps/mobile/android/app/src/test/kotlin/com/openbitfun/mobile/app/state/AppShellStateTest.kt diff --git a/src/apps/mobile/android/app/build.gradle.kts b/src/apps/mobile/android/app/build.gradle.kts index fd5dec0842..fdc0014bc0 100644 --- a/src/apps/mobile/android/app/build.gradle.kts +++ b/src/apps/mobile/android/app/build.gradle.kts @@ -76,7 +76,11 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.lifecycle.runtime.compose) - implementation(libs.google.code.scanner) + implementation("androidx.camera:camera-camera2:1.4.2") + implementation("androidx.camera:camera-lifecycle:1.4.2") + implementation("androidx.camera:camera-view:1.4.2") + // Bundle QR recognition so pairing also works without Play Services downloads. + implementation("com.google.mlkit:barcode-scanning:17.3.0") implementation(libs.androidx.window) testImplementation("junit:junit:4.13.2") diff --git a/src/apps/mobile/android/app/src/androidTest/assets/pairing-qr.png b/src/apps/mobile/android/app/src/androidTest/assets/pairing-qr.png new file mode 100644 index 0000000000000000000000000000000000000000..2e7307b5426a519bd81913518ee9afc8e7438466 GIT binary patch literal 773 zcmV+g1N!`lP)tur+K)m)T>t_uO zKT&1N7KpviH0YdsuPJg!o!=PS;f?i9EFIG4WA(`fw*1kTAogSGJYDoXimNEzfHf4)Rtcr{Jmi-vl`7kHld@DU2Iau zNsa^c$p`J=Ub;T2=Bjrthd~hqw*MAvUkLfLcAck!H@2=`cvkjDM>6Ne)w! zjya5T$Y6&a93HGsE-0HBaCJUPFH)y+xPvE#?bzRR68c~0vjy5@T$^97*6crGg9E{7 zRF+BE&w4zm{$%`J^OFEWJ+jA&5eDg5cA8o}(DJJyzc&a~QA8ZAo8~vFIB93+3&q+O z?f>0H*^7*y1f|8l!T4#=hI|dF?(ky{Y>ByJU`C00000NkvXXu0mjf DGgf=? literal 0 HcmV?d00001 diff --git a/src/apps/mobile/android/app/src/androidTest/kotlin/com/openbitfun/mobile/app/ConnectionSheetChromeTest.kt b/src/apps/mobile/android/app/src/androidTest/kotlin/com/openbitfun/mobile/app/ConnectionSheetChromeTest.kt new file mode 100644 index 0000000000..c51e5dc72a --- /dev/null +++ b/src/apps/mobile/android/app/src/androidTest/kotlin/com/openbitfun/mobile/app/ConnectionSheetChromeTest.kt @@ -0,0 +1,57 @@ +package com.openbitfun.mobile.app + +import androidx.compose.foundation.layout.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.v2.createComposeRule +import androidx.compose.ui.unit.dp +import com.openbitfun.mobile.app.ui.common.connectionSheetTextStyle +import com.openbitfun.mobile.app.ui.common.ConnectionSheetHeader +import com.openbitfun.mobile.app.ui.common.ConnectionSheetFooter +import com.openbitfun.mobile.app.ui.theme.OpenBitFunTheme +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test + +class ConnectionSheetChromeTest { + @get:Rule val composeRule = createComposeRule() + + @Test fun closeAndManualActionsKeepHarmonyGeometryAndRemainClickable() { + var closed = false + var manual = false + composeRule.setContent { + OpenBitFunTheme(dark = false) { + Column(Modifier.requiredSize(360.dp, 700.dp)) { + ConnectionSheetHeader { closed = true } + Spacer(Modifier.weight(1f)) + ConnectionSheetFooter("Pair manually", onClick = { manual = true }) + } + } + } + composeRule.onNodeWithContentDescription( + androidx.test.platform.app.InstrumentationRegistry.getInstrumentation().targetContext.getString(R.string.common_close)) + .assertWidthIsEqualTo(48.dp).assertHeightIsEqualTo(48.dp).performClick() + composeRule.onNodeWithText("Pair manually").assertHeightIsEqualTo(48.dp) + .assertWidthIsEqualTo(320.dp).performClick() + composeRule.runOnIdle { assertTrue(closed); assertTrue(manual) } + } + @Test fun chineseSheetTextUsesTheReferenceLineBoxes() { + composeRule.setContent { + OpenBitFunTheme(dark = false) { + Column { + androidx.compose.material3.Text("扫描桌面端二维码", + style = androidx.compose.material3.MaterialTheme.typography.displayMedium.connectionSheetTextStyle()) + androidx.compose.material3.Text("第一行说明\n第二行说明", + style = androidx.compose.material3.MaterialTheme.typography.bodyMedium.connectionSheetTextStyle()) + } + } + } + composeRule.onNodeWithText("扫描桌面端二维码").assertIsDisplayed().assertHeightIsEqualTo(28.dp) + val body = composeRule.onNodeWithText("第一行说明\n第二行说明").assertIsDisplayed() + // Android rounds the first/last font baselines independently; preserve + // the two 21dp lines within one layout unit without padding the glyphs. + val bounds = body.getUnclippedBoundsInRoot() + assertTrue(kotlin.math.abs((bounds.bottom - bounds.top).value - 42f) <= 1f) + } + +} diff --git a/src/apps/mobile/android/app/src/androidTest/kotlin/com/openbitfun/mobile/app/InlineQrDecoderTest.kt b/src/apps/mobile/android/app/src/androidTest/kotlin/com/openbitfun/mobile/app/InlineQrDecoderTest.kt new file mode 100644 index 0000000000..39d39f2d27 --- /dev/null +++ b/src/apps/mobile/android/app/src/androidTest/kotlin/com/openbitfun/mobile/app/InlineQrDecoderTest.kt @@ -0,0 +1,34 @@ +package com.openbitfun.mobile.app + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Color +import androidx.test.platform.app.InstrumentationRegistry +import com.google.android.gms.tasks.Tasks +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.barcode.BarcodeScannerOptions +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.common.InputImage +import org.junit.Assert.* +import org.junit.Test +import java.util.concurrent.TimeUnit + +class InlineQrDecoderTest { + @Test fun bundledDecoderReadsQrAndIgnoresBlankFrames() { + val scanner = BarcodeScanning.getClient(BarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_QR_CODE).build()) + val bitmap = InstrumentationRegistry.getInstrumentation().context.assets.open("pairing-qr.png") + .use { BitmapFactory.decodeStream(it) } + val blank = Bitmap.createBitmap(320, 320, Bitmap.Config.ARGB_8888).apply { eraseColor(Color.WHITE) } + try { + val codes = Tasks.await(scanner.process(InputImage.fromBitmap(bitmap, 0)), 20, TimeUnit.SECONDS) + assertEquals(listOf("openbitfun-qr-decoder-fixture"), codes.map { it.rawValue }) + val empty = Tasks.await(scanner.process(InputImage.fromBitmap(blank, 0)), 20, TimeUnit.SECONDS) + assertTrue(empty.isEmpty()) + } finally { + scanner.close() + bitmap.recycle() + blank.recycle() + } + } +} diff --git a/src/apps/mobile/android/app/src/androidTest/kotlin/com/openbitfun/mobile/app/MobilePreviewStatesVisualTest.kt b/src/apps/mobile/android/app/src/androidTest/kotlin/com/openbitfun/mobile/app/MobilePreviewStatesVisualTest.kt index 0c8263b411..ab6e79f760 100644 --- a/src/apps/mobile/android/app/src/androidTest/kotlin/com/openbitfun/mobile/app/MobilePreviewStatesVisualTest.kt +++ b/src/apps/mobile/android/app/src/androidTest/kotlin/com/openbitfun/mobile/app/MobilePreviewStatesVisualTest.kt @@ -82,6 +82,7 @@ class MobilePreviewStatesVisualTest { fun circleStatesHaveStableTouchTargetsAndMoreActionIsSafe() { composeRule.setContent { MobilePreviewCircleStates() } composeRule.onNodeWithTag(MOBILE_PREVIEW_CIRCLE_STATES_TEST_TAG).assertIsDisplayed() + // The shared control_touch_size contract is 48dp on all three platforms. listOf( MOBILE_PREVIEW_CIRCLE_SIDEBAR_TEST_TAG, MOBILE_PREVIEW_CIRCLE_MORE_TEST_TAG, @@ -89,8 +90,8 @@ class MobilePreviewStatesVisualTest { ).forEach { tag -> composeRule.onNodeWithTag(tag) .assertIsDisplayed() - .assertWidthIsEqualTo(44.dp) - .assertHeightIsEqualTo(44.dp) + .assertWidthIsEqualTo(48.dp) + .assertHeightIsEqualTo(48.dp) } composeRule.onNodeWithContentDescription("More actions").performClick() composeRule.onNodeWithTag(MOBILE_PREVIEW_CIRCLE_MORE_TEST_TAG).assertIsDisplayed() diff --git a/src/apps/mobile/android/app/src/main/AndroidManifest.xml b/src/apps/mobile/android/app/src/main/AndroidManifest.xml index 0b7609a584..6e6f48f512 100644 --- a/src/apps/mobile/android/app/src/main/AndroidManifest.xml +++ b/src/apps/mobile/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,9 @@ + + + -