| D-M5-03-0 |
2026-10-25 |
Built on main after M5-01 and M5-02 landed; the changelog entry and these rulings are fragments. Differences to the brief's names, following the merged code: the goldens lie in test/goldens/phone/android_goldens_test.dart like every golden of the app (the brief named test/phone/); the iOS and Android login and passkey adapters share lib/platform/browser_session.dart (BrowserSessionLogin, DomainPasskeys), on which AppleWebAuthentication and ApplePasskeys of M5-02 now build unchanged in behaviour; the Pigeon file is pigeons/android_platform_api.dart with its own interface AndroidPlatformApi, because the iOS channel shares no method with it but the login call; the Kotlin package of the runner is com.example.client (the namespace), independent of the application ID of the branding; the Pigeon copyright header names no single source file any more, so one header serves both channels. |
D-W-100 (merged names win over the brief's). |
| D-M5-03-1 |
2026-10-25 |
Build configuration and backups: tool/branding_defines.dart writes android/branding.properties (application ID, label, URL scheme, App Links host), which android/app/build.gradle.kts reads; the optional branding key application_id defaults to bundle_id and must be lower-case letters and digits in segments that start with a letter, because it is also the private-use scheme of the OIDC redirect (a bundle identifier with a hyphen needs its own application_id). Minimum SDK 26. android:allowBackup="false", and dataExtractionRules (cloud backup and device transfer) and fullBackupContent exclude every domain, so neither the database, nor the secure storage, nor the preferences leave the device. The manifest declares only INTERNET; the permissions of the plugins (biometrics of the device keys, the microphone of voice messages, notifications) come from their own manifests. |
15 6.3; ADR 0044; brief Design note 1. |
| D-M5-03-2 |
2026-10-25 |
App Links only with a configured host: the branding key associated_domain is the public domain of both phone builds (the associated domain on iOS, the App Links host on Android). With it the activity alias .AppLinks is enabled with android:autoVerify="true" for https://<host> and the content paths /s/, /c/, /t/ of 15 11, which the app opens like links of its URL scheme; without it the alias stays disabled, verifies nothing, and its placeholder host is the reserved applinks.invalid. The OIDC redirect stays the private-use scheme, so a login never depends on the verification of a domain. |
15 8, 11; RFC 8252 7.1, 7.2; brief Design notes 1 and 2. |
| D-M5-03-3 |
2026-10-25 |
Login and passkeys: one Custom Tab per login (androidx.browser), the default browser where no Custom Tabs provider exists, never a web view. Every link from outside reaches LinkActivity, which hands it with CLEAR_TOP and SINGLE_TOP to the running MainActivity (single top), so the tab above it closes and no second Flutter engine starts; the OIDC redirect <application ID>:/oidc/callback goes to the waiting login, every other link to the router. Android delivers a new intent before onResume, so a login that still waits when the app is in front again was cancelled (error=cancelled). Passkeys through the Credential Manager (androidx.credentials) with the platform provider only, which exists from API 34: the Play services provider library is proprietary and not on the license list, so devices below API 34 get no passkey button. Passkeys are offered only when the App Links host equals the instance's host (the relying party ID); the instance lists android:apk-key-hash:<hash> in auth.webauthn_origins and the host serves the Digital Asset Links (README). Secrets use flutter_secure_storage with AES-GCM and the key wrapped by an RSA key of the Android Keystore, without a biometric prompt (the step-up is the device key's of M5-01); the app's activity is a FlutterFragmentActivity because that step-up prompt needs one. |
15 8, 9; 08 2.2, 2.5, 18; quality/licenses/allowed.txt; brief Design notes 2 and 3. |
| D-M5-03-4 |
2026-10-25 |
Notification channels: the stable IDs mentions, questions, messages, runs; mentions and questions start with high importance, messages and runs with default. Kinds: mention and dm to mentions; input_needed, security and admin to questions (each asks the person to act); message to messages; run_done and run_failed to runs; a kind the build does not know to messages. Names and descriptions come from l10n in the device language and are handed over through Pigeon at every start, which creates missing channels and updates the texts of existing ones (the importance stays the person's). A quiet notification goes into the same channel with setSilent(true) (M5-05). The runner keeps the mapping in its private preferences for the push service of M5-05. The runtime permission for notifications (API 33 and later) stays with M5-05. |
01 10.3; 13 2, 3.4; 15 10.2; brief Design note 4. |
| D-M5-03-5 |
2026-10-25 |
Recent apps protection per API level, the narrowest rule that hides the content: API 33 and later setRecentsScreenshotEnabled(false) from the start (no picture at all; screenshots of the open app stay possible); API 26 to 32 FLAG_SECURE on the window from onPause (before the system takes the picture) until onResume, so it blocks screenshots and screen recording only while the app is not in front. The privacy cover of M5-02 is drawn on inactive in addition; the gateway is suspended on paused and resumed on resumed by the lifecycle mapping of M5-02. A permanent FLAG_SECURE for the app lock (M5-08) and the terminal (M5-12) is theirs to set. |
01 5.6 E6; 15 7.2; R-PR-12; brief Design note 5. |
| D-M5-03-6 |
2026-10-25 |
M5-03 builds on the phone layouts of M5-02, not on separate screens: the phone shell chooses the Android controls by the build's platform (AppPorts.platform == 'android'): AndroidBackScope (a NavigatorPopHandler) lets system back pop the tab's navigator (a page or a sheet opened in it) before the router gets it, which closes a sheet of the app's navigator or leaves the app (SystemNavigator.pop) on any tab's first page; deeper pages show AndroidTopAppBar (the up arrow named after the page below, the page's own name as the title; a thread names itself in its header) instead of the iOS back button. Pages are MaterialPages and selects use the Material list by the target platform, as M5-02 already chose. |
01 5.3, 5.4; brief Goal and fixed requirement 11. |
| D-M5-03-7 |
2026-10-25 |
Tests: the widget and golden tests on every host; with one device or emulator, tool/run_android_tests.sh in the android entry of platform-tests.json runs integration_test/phone_suite_test.dart (the files of M5-02; back is system back on Android) and the instrumented tests of android/app/src/androidTest (channels, backup rules, recent apps). Their test libraries stay at androidx.test:runner 1.3.0, the version the integration_test plugin brings into the app, because Gradle's consistent resolution holds the test APK to it. The apps-android-26 and apps-android-36 jobs set up Flutter and get 60 minutes instead of 45 for the app build. The runner apps/<app>/android/ joins the coverage exclusions of the app runners (D-M4-18-16, D-M5-02-10); its Dart side stays in the gate and is tested against fakes of the Pigeon channel. |
Brief Tests; M4-01 Design note 6; D-M5-01-5. |
| D-M5-03-8 |
2026-10-25 |
Supply chain and release hardening of the runner, after the pull request's Sonar scan: the app's resolved Gradle dependencies are locked in android/gradle.lockfile (all configurations; the Flutter engine artifacts io.flutter:* are left out, because they follow the pinned Flutter version and a build resolves only those of its target ABIs); a changed dependency or Flutter version needs ./gradlew :app:dependencies --write-locks in android/. The release build runs R8 with shrinking and obfuscation (proguard-rules.pro adds no rule; a release APK was started on an API 26 emulator and registered its channels). The manifest sets android:usesCleartextTraffic="false", which API 27 and earlier would otherwise allow; the integration tests' fake server on 127.0.0.1 is reached through dart:io, which this setting does not govern. |
Sonar rules S8569, S7204, S5332 on the pull request; 15 8. |