From c1591fa849e6c68db4b573ab59b8db58afd69adc Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Mon, 8 Jun 2026 12:38:57 +0200 Subject: [PATCH] BE sync --- .cursor/rules/git-flow.mdc | 1 + app/build.gradle | 2 + app/src/main/assets/licenses/combined.html | 40 +- .../com/foxx/androidcast/AppPreferences.java | 10 + .../DeveloperSettingsActivity.java | 19 +- .../androidcast/crash/CrashSettingsStore.java | 3 +- .../dev/DevDiagnosticsController.java | 5 +- .../androidcast/dev/DevDiagnosticsProbe.java | 26 +- .../dev/DevNetworkSelfTestProbe.java | 133 ++--- .../androidcast/dev/StreamDumpWriter.java | 245 ++++++++++ .../discovery/CastLanPeerProbe.java | 214 ++++++++ .../androidcast/network/BackendEndpoints.java | 29 ++ .../transport/StreamProtectionCapability.java | 12 + .../remoteaccess/RemoteAccessCoordinator.java | 2 +- .../remoteaccess/RemoteAccessMode.java | 4 +- .../remoteaccess/RemoteAccessService.java | 44 +- .../RemoteAccessTunnelHelper.java | 13 + .../remoteaccess/ReverseSshTunnelBridge.java | 116 +++++ .../foxx/androidcast/sender/CastSendPump.java | 1 + .../foxx/androidcast/sender/CodecCatalog.java | 29 ++ .../androidcast/sender/ScreenCastService.java | 2 + .../layout/activity_dev_diagnostics_panel.xml | 4 + .../layout/activity_dev_settings_panel.xml | 14 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values/strings.xml | 10 +- .../androidcast/dev/StreamDumpWriterTest.java | 27 + .../network/BackendEndpointsTest.java | 20 + .../remoteaccess/RemoteAccessModeTest.java | 9 +- ...20260602_REVERSE_SSH_proposals_summary.pdf | 4 +- docs/20260607-2FA-email-mobile-auth-flow.md | 418 ++++++++++++++++ docs/20260607-2FA-email-mobile-auth-flow.pdf | 462 ++++++++++++++++++ docs/20260608_ALPHA_PRIORITIES.md | 333 +++++++++++++ docs/20260608_ALPHA_PRIORITIES.pdf | 362 ++++++++++++++ docs/20260608_STREAM_DUMP_DEBUG.md | 122 +++++ docs/20260608_STREAM_DUMP_DEBUG.pdf | 232 +++++++++ docs/ALPHA.md | 34 +- docs/ALPHA.pdf | 229 +++++---- docs/ALPHA_SOAK.md | 2 +- docs/ALPHA_SOAK.pdf | 10 +- docs/AV_QUALITY_QA_SESSION.pdf | 4 +- docs/BUILD_DEPLOY.pdf | 4 +- docs/COMMERCIAL.pdf | 4 +- docs/CRASH_REPORTER.pdf | 4 +- docs/GIT_FLOW.md | 45 +- docs/GIT_FLOW.pdf | 4 +- docs/GRAFANA_vs_others_graphvis_pivot.pdf | 4 +- docs/INFRA.pdf | 4 +- docs/OPEN_API.pdf | 4 +- docs/OPEN_TASKS_GRAPH.md | 121 +++++ docs/OPEN_TASKS_GRAPH.pdf | 190 +++++++ docs/OPUS_SPEEX_VALIDATION.pdf | 288 +++++++++++ docs/OTA.pdf | 4 +- docs/PRO_AAR.pdf | 4 +- docs/README.md | 7 +- docs/README.pdf | 20 +- docs/REMOTE_ACCESS_IMPL.md | 16 +- docs/REMOTE_ACCESS_IMPL.pdf | 121 ++--- docs/REMOTE_ACCESS_VALIDATION.pdf | 148 +++--- docs/ROADMAP.md | 47 +- docs/ROADMAP.pdf | 253 ++++++---- docs/TICKETS_ROADMAP.md | 9 + docs/TICKETS_ROADMAP.pdf | 136 +++--- docs/USB_HDMI_CAST.pdf | 4 +- docs/_pdf_build/build_all_docs_pdf.py | 4 + examples/crash_reporter/ROADMAP.md | 3 +- examples/crash_reporter/backend/README.md | 8 +- .../backend/THIRD_PARTY_LICENSES.md | 46 ++ .../backend/config/config.example.php | 22 + .../backend/public/api/auth_register.php | 26 + .../backend/public/assets/js/remote_access.js | 2 +- .../scripts/ingest_priorities_20260608.php | 104 ++++ .../ingest_roadmap_tickets_20260604.php | 4 +- .../sql/migrations/008_auth_email_2fa.sql | 41 ++ .../sql/migrations/009_rssh_sessions.sql | 8 + .../migrations/009_rssh_sessions.sqlite.sql | 1 + .../backend/src/AuthEmailSchema.php | 133 +++++ .../crash_reporter/backend/src/AuthMailer.php | 142 ++++++ .../backend/src/AuthRegistration.php | 94 ++++ .../crash_reporter/backend/src/Database.php | 3 + .../backend/src/RemoteAccessRepository.php | 135 ++++- .../backend/src/RsshSessionProvisioner.php | 50 ++ .../crash_reporter/backend/src/bootstrap.php | 4 + examples/settings.json | 2 +- ndk/CMakeLists.txt | 50 ++ ndk/README.md | 33 +- scripts/build-native-codecs.sh | 115 ++++- 86 files changed, 5081 insertions(+), 637 deletions(-) create mode 100644 app/src/main/java/com/foxx/androidcast/dev/StreamDumpWriter.java create mode 100644 app/src/main/java/com/foxx/androidcast/discovery/CastLanPeerProbe.java create mode 100644 app/src/main/java/com/foxx/androidcast/network/BackendEndpoints.java create mode 100644 app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessTunnelHelper.java create mode 100644 app/src/main/java/com/foxx/androidcast/remoteaccess/ReverseSshTunnelBridge.java create mode 100644 app/src/test/java/com/foxx/androidcast/dev/StreamDumpWriterTest.java create mode 100644 app/src/test/java/com/foxx/androidcast/network/BackendEndpointsTest.java create mode 100644 docs/20260607-2FA-email-mobile-auth-flow.md create mode 100644 docs/20260607-2FA-email-mobile-auth-flow.pdf create mode 100644 docs/20260608_ALPHA_PRIORITIES.md create mode 100644 docs/20260608_ALPHA_PRIORITIES.pdf create mode 100644 docs/20260608_STREAM_DUMP_DEBUG.md create mode 100644 docs/20260608_STREAM_DUMP_DEBUG.pdf create mode 100644 docs/OPEN_TASKS_GRAPH.md create mode 100644 docs/OPEN_TASKS_GRAPH.pdf create mode 100644 docs/OPUS_SPEEX_VALIDATION.pdf create mode 100644 examples/crash_reporter/backend/THIRD_PARTY_LICENSES.md create mode 100644 examples/crash_reporter/backend/public/api/auth_register.php create mode 100644 examples/crash_reporter/backend/scripts/ingest_priorities_20260608.php create mode 100644 examples/crash_reporter/backend/sql/migrations/008_auth_email_2fa.sql create mode 100644 examples/crash_reporter/backend/sql/migrations/009_rssh_sessions.sql create mode 100644 examples/crash_reporter/backend/sql/migrations/009_rssh_sessions.sqlite.sql create mode 100644 examples/crash_reporter/backend/src/AuthEmailSchema.php create mode 100644 examples/crash_reporter/backend/src/AuthMailer.php create mode 100644 examples/crash_reporter/backend/src/AuthRegistration.php create mode 100644 examples/crash_reporter/backend/src/RsshSessionProvisioner.php diff --git a/.cursor/rules/git-flow.mdc b/.cursor/rules/git-flow.mdc index 60517f6..3c65670 100644 --- a/.cursor/rules/git-flow.mdc +++ b/.cursor/rules/git-flow.mdc @@ -26,6 +26,7 @@ Follow [docs/GIT_FLOW.md](docs/GIT_FLOW.md) and [docs/GIT_FLOW.pdf](docs/GIT_FLO 6. **Hotfix (next-only bug):** `fix/*` from **next** → merge to **next** only. 7. **Never** force-push `master`; force-push `next` only when the user explicitly requests history rewrite on integration. 8. When working on a named branch, call **SetActiveBranch** with that branch name. +9. **Fast track on `next`:** if committing directly on `next` (alpha sprint, user asked), **one feature per atomic commit** — message names the feature; no mixed RSSH + graphs + docs in one commit. Revert with `git revert `. See [GIT_FLOW.md §4.1](docs/GIT_FLOW.md#41-fast-track-on-next-alpha-sprint). ## Quick commands diff --git a/app/build.gradle b/app/build.gradle index 314da5b..b689be9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -131,6 +131,8 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'org.json:json:20240303' + implementation 'com.github.mwiede:jsch:0.2.21' + if (project.findProject(':tunnel') != null) { implementation project(':tunnel') } diff --git a/app/src/main/assets/licenses/combined.html b/app/src/main/assets/licenses/combined.html index 55fe4c2..86b2271 100644 --- a/app/src/main/assets/licenses/combined.html +++ b/app/src/main/assets/licenses/combined.html @@ -1,4 +1,4 @@ -

Android Cast

+

Android Cast (application)

Copyright © Android Cast contributors.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

    @@ -8,25 +8,29 @@

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

-

AndroidX AppCompat

-

Licensed under the Apache License, Version 2.0.

-

Copyright The Android Open Source Project.

- -

Google Material Components for Android

-

Licensed under the Apache License, Version 2.0.

- -

AndroidX ConstraintLayout

-

Licensed under the Apache License, Version 2.0.

- -

Google Play services Cronet

-

Licensed under the BSD 3-Clause License (Chromium Cronet).

+

Android Open Source components (Gradle)

+

JUnit 4

-

Licensed under the Eclipse Public License 1.0 (test dependency only).

+

Licensed under the Eclipse Public License 1.0 (test dependency only; not shipped in release APK).

-

Future components (not yet bundled)

+

Native codecs (optional; linked when built)

+

Built from git submodules under third-party/ via scripts/build-native-codecs.sh. CMake sets ANDROIDCAST_HAVE_* when build/native/<abi>/lib*.a exists for each ABI in the APK. JNI encode/decode sinks may still be partial while libraries are linked.

+ +

WireGuard tunnel module (optional Gradle module)

+

When third-party/wireguard-android is initialized and the :tunnel module is included, the app may link the WireGuard Android tunnel library (Apache License 2.0; see upstream wireguard-android). Remote access still uses Android VpnService (system VPN permission).

diff --git a/app/src/main/java/com/foxx/androidcast/AppPreferences.java b/app/src/main/java/com/foxx/androidcast/AppPreferences.java index 9fedc41..343d961 100644 --- a/app/src/main/java/com/foxx/androidcast/AppPreferences.java +++ b/app/src/main/java/com/foxx/androidcast/AppPreferences.java @@ -36,6 +36,7 @@ public final class AppPreferences { private static final String KEY_PLAY_INCOMING_AUDIO = "play_incoming_audio"; private static final String KEY_DEV_RECEIVER_DEBUG_OVERLAY = "dev_receiver_debug_overlay"; private static final String KEY_DEV_GRAB_SESSION_STATS = "dev_grab_session_stats"; + private static final String KEY_DEV_STREAM_DUMP = "dev_stream_dump"; private static final String KEY_SEND_ANONYMOUS_CRASH_LOGS = "send_anonymous_crash_logs"; private static final String KEY_DEV_SHOW_CODEC_PRIORITY_SCORES = "dev_show_codec_priority_scores"; private static final String KEY_DEV_RECEIVER_VIDEO_PRESET = "dev_receiver_video_preset"; @@ -160,6 +161,15 @@ public final class AppPreferences { prefs(context).edit().putBoolean(KEY_DEV_GRAB_SESSION_STATS, grab).apply(); } + /** Developer: record elementary cast streams to zip (FR 0.1; default off). */ + public static boolean isDevStreamDumpEnabled(Context context) { + return prefs(context).getBoolean(KEY_DEV_STREAM_DUMP, false); + } + + public static void setDevStreamDumpEnabled(Context context, boolean enabled) { + prefs(context).edit().putBoolean(KEY_DEV_STREAM_DUMP, enabled).apply(); + } + /** Send anonymous crash reports via the standalone crash watcher process (default on). */ public static boolean isSendAnonymousCrashLogs(Context context) { return prefs(context).getBoolean(KEY_SEND_ANONYMOUS_CRASH_LOGS, true); diff --git a/app/src/main/java/com/foxx/androidcast/DeveloperSettingsActivity.java b/app/src/main/java/com/foxx/androidcast/DeveloperSettingsActivity.java index dbe17b3..a30afdc 100644 --- a/app/src/main/java/com/foxx/androidcast/DeveloperSettingsActivity.java +++ b/app/src/main/java/com/foxx/androidcast/DeveloperSettingsActivity.java @@ -83,10 +83,11 @@ public class DeveloperSettingsActivity extends AppCompatActivity { CheckBox debugOverlay = findViewById(R.id.check_receiver_debug_overlay); CheckBox codecScores = findViewById(R.id.check_show_codec_priority_scores); CheckBox sessionStats = findViewById(R.id.check_grab_session_stats); + CheckBox streamDump = findViewById(R.id.check_dev_stream_dump); CheckBox adaptiveDisplay = findViewById(R.id.check_receiver_adaptive_display); CheckBox backendNtpSync = findViewById(R.id.check_dev_backend_ntp_sync); - refreshFromPreferences(debugOverlay, codecScores, sessionStats, adaptiveDisplay, backendNtpSync); + refreshFromPreferences(debugOverlay, codecScores, sessionStats, streamDump, adaptiveDisplay, backendNtpSync); findViewById(R.id.btn_open_session_stats).setOnClickListener(v -> startActivity(new android.content.Intent(this, SessionStatsAnalyzerActivity.class))); @@ -144,6 +145,7 @@ public class DeveloperSettingsActivity extends AppCompatActivity { findViewById(R.id.check_receiver_debug_overlay), findViewById(R.id.check_show_codec_priority_scores), findViewById(R.id.check_grab_session_stats), + findViewById(R.id.check_dev_stream_dump), findViewById(R.id.check_receiver_adaptive_display), findViewById(R.id.check_dev_backend_ntp_sync)); bindDiagPingSection(); @@ -167,7 +169,7 @@ public class DeveloperSettingsActivity extends AppCompatActivity { String[] labels = new String[]{ getString(R.string.dev_remote_access_disabled), getString(R.string.dev_remote_access_wireguard), - getString(R.string.dev_remote_access_rssh_unavailable), + getString(R.string.dev_remote_access_rssh), }; ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, labels); @@ -186,15 +188,6 @@ public class DeveloperSettingsActivity extends AppCompatActivity { return; } RemoteAccessMode picked = modes[position]; - if (picked == RemoteAccessMode.RSSH) { - Toast.makeText(DeveloperSettingsActivity.this, - R.string.dev_remote_access_rssh_toast, Toast.LENGTH_SHORT).show(); - suppressRemoteAccessSpinnerEvents = true; - remoteAccessSpinner.setSelection( - AppPreferences.getDevRemoteAccessMode(DeveloperSettingsActivity.this).ordinal(), false); - suppressRemoteAccessSpinnerEvents = false; - return; - } applyRemoteAccessMode(picked); } @@ -518,13 +511,15 @@ public class DeveloperSettingsActivity extends AppCompatActivity { } private void refreshFromPreferences(CheckBox debugOverlay, CheckBox codecScores, CheckBox sessionStats, - CheckBox adaptiveDisplay, CheckBox backendNtpSync) { + CheckBox streamDump, CheckBox adaptiveDisplay, CheckBox backendNtpSync) { bindDeveloperCheckbox(debugOverlay, AppPreferences.isShowReceiverDebugOverlay(this), AppPreferences::setShowReceiverDebugOverlay); bindDeveloperCheckbox(codecScores, AppPreferences.isShowCodecPriorityScores(this), AppPreferences::setShowCodecPriorityScores); bindDeveloperCheckbox(sessionStats, AppPreferences.isGrabSessionStats(this), AppPreferences::setGrabSessionStats); + bindDeveloperCheckbox(streamDump, AppPreferences.isDevStreamDumpEnabled(this), + AppPreferences::setDevStreamDumpEnabled); bindDeveloperCheckbox(adaptiveDisplay, AppPreferences.isDevReceiverAdaptiveDisplay(this), AppPreferences::setDevReceiverAdaptiveDisplay); bindDeveloperCheckbox(backendNtpSync, AppPreferences.isDevBackendNtpSyncEnabled(this), diff --git a/app/src/main/java/com/foxx/androidcast/crash/CrashSettingsStore.java b/app/src/main/java/com/foxx/androidcast/crash/CrashSettingsStore.java index f9c90cc..55a8e1d 100644 --- a/app/src/main/java/com/foxx/androidcast/crash/CrashSettingsStore.java +++ b/app/src/main/java/com/foxx/androidcast/crash/CrashSettingsStore.java @@ -14,6 +14,7 @@ package com.foxx.androidcast.crash; import android.content.Context; import com.foxx.androidcast.AppPreferences; +import com.foxx.androidcast.network.BackendEndpoints; import com.foxx.androidcast.settings.AppSettingsJson; import org.json.JSONObject; @@ -39,7 +40,7 @@ public final class CrashSettingsStore { } String url = crash.optString("upload_url", "").trim(); if (!url.isEmpty()) { - uploadUrl = url; + uploadUrl = BackendEndpoints.normalizeCrashesUrl(url); } if (crash.has("prune_after_upload")) { prune = parseBool(crash.optString("prune_after_upload", "true"), prune); diff --git a/app/src/main/java/com/foxx/androidcast/dev/DevDiagnosticsController.java b/app/src/main/java/com/foxx/androidcast/dev/DevDiagnosticsController.java index 6c6ac67..52cb1a1 100644 --- a/app/src/main/java/com/foxx/androidcast/dev/DevDiagnosticsController.java +++ b/app/src/main/java/com/foxx/androidcast/dev/DevDiagnosticsController.java @@ -26,7 +26,8 @@ public final class DevDiagnosticsController { AUDIO(R.id.dev_diag_audio, R.string.dev_diag_audio_title), AUDIO_CODECS(R.id.dev_diag_audio_codecs, R.string.dev_diag_audio_codecs_title), NETWORK(R.id.dev_diag_network, R.string.dev_diag_network_title), - CASTING(R.id.dev_diag_casting, R.string.dev_diag_casting_title); + CASTING(R.id.dev_diag_casting, R.string.dev_diag_casting_title), + ADVERTISEMENT(R.id.dev_diag_advertisement, R.string.dev_diag_advertisement_title); final int includeId; final int titleRes; @@ -107,6 +108,8 @@ public final class DevDiagnosticsController { DevDiagnosticsProbe.probeNetwork(activity, cancel), sectionText); runSection(Section.CASTING, () -> DevDiagnosticsProbe.probeCastingExtras(activity), sectionText); + runSection(Section.ADVERTISEMENT, () -> + DevDiagnosticsProbe.probeFullAdvertisementInfo(activity), sectionText); } finally { if (!cancel.get()) { lastReport = new DevDiagnosticsReport(System.currentTimeMillis(), sectionText); diff --git a/app/src/main/java/com/foxx/androidcast/dev/DevDiagnosticsProbe.java b/app/src/main/java/com/foxx/androidcast/dev/DevDiagnosticsProbe.java index f6ad704..98e488f 100644 --- a/app/src/main/java/com/foxx/androidcast/dev/DevDiagnosticsProbe.java +++ b/app/src/main/java/com/foxx/androidcast/dev/DevDiagnosticsProbe.java @@ -29,6 +29,7 @@ import com.foxx.androidcast.media.CodecPriorityCatalog; import com.foxx.androidcast.media.CodecTier; import com.foxx.androidcast.media.codec.jni.NativeCodecBridge; import com.foxx.androidcast.network.CastCodecFlags; +import com.foxx.androidcast.network.transport.StreamProtectionCapability; import com.foxx.androidcast.sender.CodecCatalog; import java.io.IOException; @@ -53,9 +54,9 @@ public final class DevDiagnosticsProbe { sb.append("API ").append(Build.VERSION.SDK_INT).append('\n'); sb.append("Native JNI probes:\n"); sb.append(" libvpx: ").append(NativeCodecBridge.isLibvpxAvailable() ? "yes" : "no").append('\n'); - sb.append(" Opus: ").append(NativeCodecBridge.isOpusAvailable() ? "yes (native)" : "no (stub→AAC)") + sb.append(" Opus: ").append(NativeCodecBridge.isOpusAvailable() ? "yes (lib linked)" : "no (stub→AAC)") .append('\n'); - sb.append(" Speex: ").append(NativeCodecBridge.isSpeexAvailable() ? "yes (native)" : "no (stub→AAC)") + sb.append(" Speex: ").append(NativeCodecBridge.isSpeexAvailable() ? "yes (lib linked)" : "no (stub→AAC)") .append('\n'); sb.append('\n').append("Video encoders (MediaCodec):\n"); appendVideoCodecLines(sb, true, settings); @@ -173,6 +174,27 @@ public final class DevDiagnosticsProbe { return sb.toString().trim(); } + /** Wire advertisement summary (last card on diagnostics tab). */ + public static String probeFullAdvertisementInfo(Context context) { + CastSettings settings = AppPreferences.loadSenderDefaults(context); + StringBuilder sb = new StringBuilder(); + sb.append("[audio adv] ").append(join(CodecCatalog.localAdvertisedAudioCodecsInOrder(settings))) + .append(" · mask 0x") + .append(Integer.toHexString(CastCodecFlags.localAudioMask())) + .append('\n'); + List videoMimes = CodecCatalog.localAdvertisedVideoMimesInOrder(); + sb.append("[video adv] ").append(join(videoMimes)); + List videoCodecs = CodecCatalog.localAdvertisedVideoCodecsInOrder(settings); + if (!videoCodecs.isEmpty()) { + sb.append(" (").append(join(videoCodecs)).append(')'); + } + sb.append('\n'); + sb.append("[protection adv] ").append(join(StreamProtectionCapability.advertisedProtectionInOrder())) + .append('\n'); + sb.append("-----"); + return sb.toString(); + } + public static String probeCastingExtras(Context context) { StringBuilder sb = new StringBuilder(); PackageManager pm = context.getPackageManager(); diff --git a/app/src/main/java/com/foxx/androidcast/dev/DevNetworkSelfTestProbe.java b/app/src/main/java/com/foxx/androidcast/dev/DevNetworkSelfTestProbe.java index 04c6c17..cb105c7 100644 --- a/app/src/main/java/com/foxx/androidcast/dev/DevNetworkSelfTestProbe.java +++ b/app/src/main/java/com/foxx/androidcast/dev/DevNetworkSelfTestProbe.java @@ -12,8 +12,10 @@ import android.os.PowerManager; import android.telephony.TelephonyManager; import com.foxx.androidcast.AppPreferences; -import com.foxx.androidcast.BuildConfig; +import com.foxx.androidcast.CastConfig; import com.foxx.androidcast.crash.CrashSettingsStore; +import com.foxx.androidcast.discovery.CastLanPeerProbe; +import com.foxx.androidcast.network.BackendEndpoints; import java.io.BufferedReader; import java.io.InputStream; @@ -51,7 +53,7 @@ public final class DevNetworkSelfTestProbe { probeOverall(context, caps, cancel, report); probeLink(context, caps, lp, report); probeCaptive(caps, cancel, report); - probeIntraLan(lp, cancel, report); + probeIntraLan(context, lp, cancel, report); probeBackend(context, cancel, report); probeNtp(context, cancel, report); probeNatExternal(caps, context, cancel, report); @@ -186,7 +188,8 @@ public final class DevNetworkSelfTestProbe { } } - private static void probeIntraLan(LinkProperties lp, AtomicBoolean cancel, DevNetworkSelfTestReport r) { + private static void probeIntraLan(Context context, LinkProperties lp, AtomicBoolean cancel, + DevNetworkSelfTestReport r) { r.section("LAN", "Intra-network"); String ip = firstIpv4Host(lp); if (ip == null) { @@ -203,62 +206,36 @@ public final class DevNetworkSelfTestProbe { return; } String prefix = parts[0] + "." + parts[1] + "." + parts[2] + "."; - int self = Integer.parseInt(parts[3]); - List rtts = new ArrayList<>(); - int peers = 0; - long t0 = System.currentTimeMillis(); - for (int d = -8; d <= 8; d++) { - if (cancel != null && cancel.get()) { - break; - } - int host = self + d; - if (host <= 1 || host >= 255 || host == self) { - continue; - } - try { - InetAddress addr = InetAddress.getByName(prefix + host); - long p0 = System.currentTimeMillis(); - if (addr.isReachable(180)) { - peers++; - rtts.add(System.currentTimeMillis() - p0); - } - } catch (Exception ignored) { - } - } - long elapsed = System.currentTimeMillis() - t0; r.itemPass("subnet", prefix + "x (self " + ip + ")"); - if (peers == 0) { - r.itemWarn("peers", "0 reachable (+/-8 quick scan)"); - r.itemNa("avg-rtt", "n/a"); + CastLanPeerProbe.Result scan = CastLanPeerProbe.scan(context, cancel); + r.itemPass("scan-ms", Long.toString(scan.elapsedMs)); + if (scan.peers == 0) { + r.itemWarn("cast-peers", + "0 on LAN (UDP :" + CastConfig.DISCOVERY_PORT + ", need peer listening/Receive)"); } else { - long sum = 0; - for (Long v : rtts) { - sum += v; - } - double avg = (double) sum / peers; - r.itemPass("peers", peers + " reachable"); - r.itemPass("avg-rtt", String.format(Locale.US, "%.0f ms", avg)); + r.itemPass("cast-peers", scan.peers + " · " + scan.summary); } - r.itemPass("scan-ms", Long.toString(elapsed)); } private static void probeBackend(Context context, AtomicBoolean cancel, DevNetworkSelfTestReport r) { r.section("BACKEND", "Backend reachability"); - String url = resolveBackendUrl(context); - if (url.isEmpty()) { - r.itemWarn("url", "not configured"); - return; + String heartbeat = BackendEndpoints.HEARTBEAT_URL; + r.itemPass("url", shortenUrl(heartbeat)); + String configured = BackendEndpoints.normalizeCrashesUrl( + CrashSettingsStore.load(context).uploadUrl); + if (!configured.isEmpty() && !configured.equals(BackendEndpoints.UPLOAD_URL)) { + r.itemWarn("configured", shortenUrl(configured) + " (legacy host; probe uses apps.f0xx.org)"); } - r.itemPass("url", shortenUrl(url)); if (cancel != null && cancel.get()) { r.itemWarn("probe", "stopped"); return; } - HttpResult head = httpHead(url); + HttpResult head = httpHead(BackendEndpoints.CRASHES_BASE + "/"); reportHttp(r, "head", head.code, head.summary); - HttpReadResult read = httpRead(url, 32 * 1024); + HttpReadResult read = httpRead(BackendEndpoints.CRASHES_BASE + "/", 32 * 1024); reportHttpRead(r, "downlink", read.code, read.summary); - HttpResult uplink = httpPostJson(url, "{\"ping\":\"bandwidth\"}"); + HttpResult uplink = httpPostJson(BackendEndpoints.HEARTBEAT_URL, + "{\"heartbeat\":{\"type\":\"ping\"}}"); reportHttp(r, "uplink", uplink.code, uplink.summary); } @@ -273,12 +250,7 @@ public final class DevNetworkSelfTestProbe { if (!enabled) { return; } - String backend = resolveBackendUrl(context); - if (backend.isEmpty()) { - r.itemWarn("be-ntp", "backend URL missing"); - return; - } - String heartbeatUrl = toHeartbeatUrl(backend); + String backend = BackendEndpoints.HEARTBEAT_URL; if (cancel != null && cancel.get()) { r.itemWarn("be-ntp", "stopped"); return; @@ -286,7 +258,7 @@ public final class DevNetworkSelfTestProbe { long now = System.currentTimeMillis() / 1000L; String tz = new java.text.SimpleDateFormat("Z", Locale.US).format(new java.util.Date()); String body = "{\"heartbeat\":{\"type\":\"ntp\",\"date\":" + now + ",\"tz\":\"" + tz + "\"}}"; - HttpTextResult text = httpPostJsonText(heartbeatUrl, body); + HttpTextResult text = httpPostJsonText(backend, body); if (httpSuccess(text.code)) { r.itemPass("be-ntp", text.summary); if (text.body != null && !text.body.isEmpty()) { @@ -319,29 +291,17 @@ public final class DevNetworkSelfTestProbe { r.itemWarn("external-ip", "stopped"); return; } - String backend = resolveBackendUrl(context); - if (!backend.isEmpty()) { - HttpTextResult viaBackend = httpPostJsonText(toHeartbeatUrl(backend), - "{\"heartbeat\":{\"type\":\"ip\"}}"); - if (viaBackend.code < 0) { - r.itemFail("external-ip", viaBackend.summary); - } else if (!httpSuccess(viaBackend.code)) { - r.itemFail("external-ip", viaBackend.summary); - } else { - r.itemPass("external-ip", viaBackend.summary); - if (viaBackend.body != null && !viaBackend.body.isEmpty()) { - r.itemPass("be-ip-body", truncate(viaBackend.body, 80)); - } - } - return; - } - HttpTextResult web = httpGetText("https://api.ipify.org?format=json"); - if (web.code < 0) { - r.itemFail("external-ip", web.summary); - } else if (!httpSuccess(web.code)) { - r.itemFail("external-ip", web.summary); + HttpTextResult viaBackend = httpPostJsonText(BackendEndpoints.HEARTBEAT_URL, + "{\"heartbeat\":{\"type\":\"ip\"}}"); + if (viaBackend.code < 0) { + r.itemFail("external-ip", viaBackend.summary); + } else if (!httpSuccess(viaBackend.code)) { + r.itemFail("external-ip", viaBackend.summary); } else { - r.itemPass("external-ip", truncate(web.summary, 100)); + r.itemPass("external-ip", viaBackend.summary); + if (viaBackend.body != null && !viaBackend.body.isEmpty()) { + r.itemPass("be-ip-body", truncate(viaBackend.body, 80)); + } } } @@ -457,31 +417,6 @@ public final class DevNetworkSelfTestProbe { return oneLine.substring(0, max - 3) + "..."; } - private static String toHeartbeatUrl(String backend) { - if (backend.contains("/api/upload.php")) { - return backend.replace("/api/upload.php", "/api/heartbeat.php"); - } - if (backend.endsWith("/")) { - return backend + "api/heartbeat.php"; - } - return backend + "/api/heartbeat.php"; - } - - private static String resolveBackendUrl(Context context) { - String crash = CrashSettingsStore.load(context).uploadUrl; - if (crash != null && !crash.trim().isEmpty()) { - return crash.trim(); - } - String ota = AppPreferences.getOtaChannelUrl(context); - if (ota != null && !ota.trim().isEmpty()) { - return ota.trim(); - } - if (BuildConfig.OTA_CHANNEL_URL_DEFAULT != null && !BuildConfig.OTA_CHANNEL_URL_DEFAULT.trim().isEmpty()) { - return BuildConfig.OTA_CHANNEL_URL_DEFAULT.trim(); - } - return ""; - } - private static boolean hasTransport(NetworkCapabilities caps, int t) { return caps != null && caps.hasTransport(t); } diff --git a/app/src/main/java/com/foxx/androidcast/dev/StreamDumpWriter.java b/app/src/main/java/com/foxx/androidcast/dev/StreamDumpWriter.java new file mode 100644 index 0000000..e585eed --- /dev/null +++ b/app/src/main/java/com/foxx/androidcast/dev/StreamDumpWriter.java @@ -0,0 +1,245 @@ +package com.foxx.androidcast.dev; + +import android.content.Context; +import android.util.Log; + +import com.foxx.androidcast.network.CastProtocol; + +import org.json.JSONObject; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * FR 0.1 — length-prefixed elementary stream capture while casting (dev-only). + */ +public final class StreamDumpWriter { + private static final String TAG = "StreamDumpWriter"; + private static final int SCHEMA_VERSION = 1; + private static final long MAX_BYTES = 100L * 1024L * 1024L; + private static final long MAX_DURATION_MS = 5L * 60L * 1000L; + + private static final Object LOCK = new Object(); + private static StreamDumpWriter active; + + private final String sessionId; + private final String datePrefix; + private final long startedAtMs; + private final File audioFile; + private final File videoFile; + private final File workDir; + private BufferedOutputStream audioOut; + private BufferedOutputStream videoOut; + private final AtomicInteger audioFrames = new AtomicInteger(); + private final AtomicInteger videoFrames = new AtomicInteger(); + private final AtomicLong bytesWritten = new AtomicLong(); + private volatile boolean closed; + + private StreamDumpWriter(Context context) throws IOException { + startedAtMs = System.currentTimeMillis(); + SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); + sessionId = fmt.format(new Date(startedAtMs)); + datePrefix = sessionId.substring(0, 8); + workDir = new File(context.getFilesDir(), "stream_dumps"); + if (!workDir.exists() && !workDir.mkdirs()) { + throw new IOException("cannot create stream_dumps dir"); + } + audioFile = new File(workDir, datePrefix + "_audio_stream.raw"); + videoFile = new File(workDir, datePrefix + "_video_stream.raw"); + audioOut = new BufferedOutputStream(new FileOutputStream(audioFile)); + videoOut = new BufferedOutputStream(new FileOutputStream(videoFile)); + } + + public static void startIfEnabled(Context context) { + if (!com.foxx.androidcast.AppPreferences.isDevStreamDumpEnabled(context)) { + return; + } + synchronized (LOCK) { + if (active != null) { + return; + } + try { + active = new StreamDumpWriter(context.getApplicationContext()); + Log.i(TAG, "recording " + active.sessionId); + } catch (IOException e) { + Log.w(TAG, "start failed: " + e.getMessage()); + } + } + } + + public static void finalizeIfActive(Context context) { + StreamDumpWriter w; + synchronized (LOCK) { + w = active; + active = null; + } + if (w != null) { + w.closeAndZip(context.getApplicationContext()); + } + } + + public static void recordFrame(byte type, byte[] payload) { + StreamDumpWriter w = active; + if (w == null || w.closed || payload == null) { + return; + } + w.append(type, payload); + } + + private void append(byte type, byte[] payload) { + if (closed) { + return; + } + if (bytesWritten.get() > MAX_BYTES + || System.currentTimeMillis() - startedAtMs > MAX_DURATION_MS) { + closed = true; + return; + } + boolean audio = type == CastProtocol.MSG_AUDIO_FRAME || type == CastProtocol.MSG_AUDIO_CONFIG; + boolean video = type == CastProtocol.MSG_VIDEO_FRAME; + if (!audio && !video) { + return; + } + byte[] frame = lengthPrefix(payload); + synchronized (LOCK) { + if (closed) { + return; + } + try { + if (audio) { + audioOut.write(frame); + audioFrames.incrementAndGet(); + } else { + videoOut.write(frame); + videoFrames.incrementAndGet(); + } + bytesWritten.addAndGet(frame.length); + } catch (IOException e) { + Log.w(TAG, "write failed: " + e.getMessage()); + closed = true; + } + } + } + + private static byte[] lengthPrefix(byte[] payload) { + ByteBuffer buf = ByteBuffer.allocate(4 + payload.length); + buf.putInt(payload.length); + buf.put(payload); + return buf.array(); + } + + private void closeAndZip(Context context) { + synchronized (LOCK) { + closed = true; + closeQuietly(audioOut); + closeQuietly(videoOut); + audioOut = null; + videoOut = null; + } + File zip = new File(workDir, sessionId + "_sess_record.zip"); + try { + writeZip(zip); + if (audioFile.exists()) { + audioFile.delete(); + } + if (videoFile.exists()) { + videoFile.delete(); + } + Log.i(TAG, "wrote " + zip.getAbsolutePath()); + } catch (IOException e) { + Log.w(TAG, "zip failed: " + e.getMessage()); + } + } + + private void writeZip(File zip) throws IOException { + JSONObject meta = new JSONObject(); + try { + meta.put("schema_version", SCHEMA_VERSION); + meta.put("session_id", sessionId); + meta.put("role", "sender"); + meta.put("record_mode", "raw_elementary"); + meta.put("started_at_epoch_ms", startedAtMs); + meta.put("ended_at_epoch_ms", System.currentTimeMillis()); + JSONObject audio = new JSONObject(); + audio.put("file", audioFile.getName()); + audio.put("format", "length_prefixed"); + audio.put("frame_count", audioFrames.get()); + meta.put("audio", audio); + JSONObject video = new JSONObject(); + video.put("file", videoFile.getName()); + video.put("format", "length_prefixed"); + video.put("frame_count", videoFrames.get()); + meta.put("video", video); + } catch (org.json.JSONException e) { + throw new IOException(e); + } + byte[] metaBytes; + try { + metaBytes = meta.toString(2).getBytes(java.nio.charset.StandardCharsets.UTF_8); + } catch (org.json.JSONException e) { + throw new IOException(e); + } + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zip))) { + putEntry(zos, "meta.json", metaBytes); + if (audioFrames.get() > 0 && audioFile.isFile()) { + putFileEntry(zos, audioFile.getName(), audioFile); + } + if (videoFrames.get() > 0 && videoFile.isFile()) { + putFileEntry(zos, videoFile.getName(), videoFile); + } + } + } + + private static void putEntry(ZipOutputStream zos, String name, byte[] data) throws IOException { + zos.putNextEntry(new ZipEntry(name)); + zos.write(data); + zos.closeEntry(); + } + + private static void putFileEntry(ZipOutputStream zos, String name, File file) throws IOException { + zos.putNextEntry(new ZipEntry(name)); + byte[] buf = new byte[8192]; + try (java.io.FileInputStream in = new java.io.FileInputStream(file)) { + int n; + while ((n = in.read(buf)) >= 0) { + zos.write(buf, 0, n); + } + } + zos.closeEntry(); + } + + private static void closeQuietly(BufferedOutputStream out) { + if (out == null) { + return; + } + try { + out.flush(); + out.close(); + } catch (IOException ignored) { + } + } + + /** For unit tests. */ + static byte[] encodeLengthPrefixedFrame(byte[] payload) { + return lengthPrefix(payload); + } + + /** Exposed for tests — parse length-prefixed payload round-trip. */ + static int readLengthPrefix(byte[] framed) { + if (framed.length < 4) { + return -1; + } + return ByteBuffer.wrap(framed, 0, 4).getInt(); + } +} diff --git a/app/src/main/java/com/foxx/androidcast/discovery/CastLanPeerProbe.java b/app/src/main/java/com/foxx/androidcast/discovery/CastLanPeerProbe.java new file mode 100644 index 0000000..0722ec0 --- /dev/null +++ b/app/src/main/java/com/foxx/androidcast/discovery/CastLanPeerProbe.java @@ -0,0 +1,214 @@ +package com.foxx.androidcast.discovery; + +import android.content.Context; +import android.net.wifi.WifiManager; + +import com.foxx.androidcast.CastConfig; +import com.foxx.androidcast.network.CastProtoHeader; + +import org.json.JSONObject; + +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +/** Quick UDP cast discovery scan for developer network self-test. */ +public final class CastLanPeerProbe { + private static final int SCAN_MS = 2_500; + private static final int PROBE_INTERVAL_MS = 700; + + public static final class Result { + public final int peers; + public final String summary; + public final long elapsedMs; + + Result(int peers, String summary, long elapsedMs) { + this.peers = peers; + this.summary = summary; + this.elapsedMs = elapsedMs; + } + } + + private CastLanPeerProbe() {} + + public static Result scan(Context context, AtomicBoolean cancel) { + long t0 = System.currentTimeMillis(); + Map peers = new LinkedHashMap<>(); + WifiManager.MulticastLock lock = acquireMulticastLock(context); + try (DatagramSocket sock = openSocket()) { + if (sock == null) { + return new Result(0, "socket bind failed", System.currentTimeMillis() - t0); + } + sock.setSoTimeout(250); + byte[] buf = new byte[2048]; + long deadline = System.currentTimeMillis() + SCAN_MS; + long nextProbe = System.currentTimeMillis(); + while (System.currentTimeMillis() < deadline) { + if (cancel != null && cancel.get()) { + break; + } + long now = System.currentTimeMillis(); + if (now >= nextProbe) { + sendDiscoverProbe(sock); + nextProbe = now + PROBE_INTERVAL_MS; + } + DatagramPacket packet = new DatagramPacket(buf, buf.length); + try { + sock.receive(packet); + recordPeer(context, packet, peers); + } catch (java.net.SocketTimeoutException ignored) { + } catch (Exception ignored) { + } + } + } catch (Exception e) { + long ms = System.currentTimeMillis() - t0; + return new Result(0, e.getClass().getSimpleName(), ms); + } finally { + releaseMulticastLock(lock); + } + long ms = System.currentTimeMillis() - t0; + if (peers.isEmpty()) { + return new Result(0, "UDP :" + CastConfig.DISCOVERY_PORT + " · " + ms + " ms", ms); + } + StringBuilder names = new StringBuilder(); + for (Map.Entry e : peers.entrySet()) { + if (names.length() > 0) { + names.append(", "); + } + names.append(e.getValue()).append('@').append(e.getKey()); + if (names.length() > 96) { + names.append("…"); + break; + } + } + return new Result(peers.size(), names.toString(), ms); + } + + private static DatagramSocket openSocket() { + try { + DatagramSocket sock = new DatagramSocket(null); + sock.setReuseAddress(true); + sock.setBroadcast(true); + try { + sock.bind(new InetSocketAddress(CastConfig.DISCOVERY_PORT)); + } catch (Exception ignored) { + sock.bind(null); + } + return sock; + } catch (Exception e) { + return null; + } + } + + private static void sendDiscoverProbe(DatagramSocket sock) { + try { + JSONObject json = new JSONObject(); + json.put("magic", CastConfig.DISCOVERY_MAGIC); + json.put("discover", true); + byte[] bytes = json.toString().getBytes(StandardCharsets.UTF_8); + InetAddress broadcast = InetAddress.getByName("255.255.255.255"); + DatagramPacket packet = new DatagramPacket(bytes, bytes.length, broadcast, CastConfig.DISCOVERY_PORT); + sock.send(packet); + } catch (Exception ignored) { + } + } + + private static void recordPeer(Context context, DatagramPacket packet, Map peers) { + try { + JSONObject json = parseAnnounceJson(packet); + if (json == null) { + return; + } + if (!CastConfig.DISCOVERY_MAGIC.equals(json.optString("magic"))) { + return; + } + if (json.optBoolean("discover", false)) { + return; + } + String host = packet.getAddress().getHostAddress(); + String remoteUuid = json.optString("device_uuid", ""); + if (DiscoverySelfFilter.isSelf(context, host, remoteUuid)) { + return; + } + String name = json.optString("name", "Unknown"); + int port = json.optInt("port", CastConfig.CAST_PORT); + boolean listening = json.optBoolean("listening", true); + if (!listening) { + return; + } + peers.put(host + ":" + port, name); + } catch (Exception ignored) { + } + } + + private static JSONObject parseAnnounceJson(DatagramPacket packet) { + try { + byte[] raw = packet.getData(); + int off = packet.getOffset(); + int len = packet.getLength(); + String text; + if (len >= CastProtoHeader.HEADER_SIZE_V1 && readMagicLe(raw, off) == CastProtoHeader.MAGIC) { + CastProtoHeader hdr = CastProtoHeader.decode(raw, off, len); + int bodyOff = off + hdr.headerSize; + int bodyLen = len - hdr.headerSize; + if (bodyLen > 5) { + byte type = raw[bodyOff]; + int plen = readIntBe(raw, bodyOff + 1); + if (plen >= 0 && bodyOff + 5 + plen <= off + len) { + text = new String(raw, bodyOff + 5, plen, StandardCharsets.UTF_8); + } else { + text = new String(raw, bodyOff, bodyLen, StandardCharsets.UTF_8); + } + } else { + text = new String(raw, bodyOff, bodyLen, StandardCharsets.UTF_8); + } + } else { + text = new String(raw, off, len, StandardCharsets.UTF_8); + } + return new JSONObject(text); + } catch (Exception e) { + return null; + } + } + + private static int readMagicLe(byte[] b, int off) { + return (b[off] & 0xff) | ((b[off + 1] & 0xff) << 8) + | ((b[off + 2] & 0xff) << 16) | ((b[off + 3] & 0xff) << 24); + } + + private static int readIntBe(byte[] b, int off) { + return ((b[off] & 0xff) << 24) | ((b[off + 1] & 0xff) << 16) + | ((b[off + 2] & 0xff) << 8) | (b[off + 3] & 0xff); + } + + private static WifiManager.MulticastLock acquireMulticastLock(Context context) { + if (context == null) { + return null; + } + WifiManager wifi = context.getApplicationContext().getSystemService(WifiManager.class); + if (wifi == null) { + return null; + } + WifiManager.MulticastLock lock = wifi.createMulticastLock("androidcast-lan-probe"); + lock.setReferenceCounted(false); + lock.acquire(); + return lock; + } + + private static void releaseMulticastLock(WifiManager.MulticastLock lock) { + if (lock == null) { + return; + } + try { + if (lock.isHeld()) { + lock.release(); + } + } catch (RuntimeException ignored) { + } + } +} diff --git a/app/src/main/java/com/foxx/androidcast/network/BackendEndpoints.java b/app/src/main/java/com/foxx/androidcast/network/BackendEndpoints.java new file mode 100644 index 0000000..8367698 --- /dev/null +++ b/app/src/main/java/com/foxx/androidcast/network/BackendEndpoints.java @@ -0,0 +1,29 @@ +package com.foxx.androidcast.network; + +/** Canonical production BE URLs on {@code apps.f0xx.org} (network self-test + defaults). */ +public final class BackendEndpoints { + public static final String APPS_HOST = "apps.f0xx.org"; + public static final String CRASHES_BASE = + "https://" + APPS_HOST + "/app/androidcast_project/crashes"; + public static final String HEARTBEAT_URL = CRASHES_BASE + "/api/heartbeat.php"; + public static final String UPLOAD_URL = CRASHES_BASE + "/api/upload.php"; + + private BackendEndpoints() {} + + /** Maps legacy {@code f0xx.org}/{@code foxx.org} crash paths to {@link #APPS_HOST}. */ + public static String normalizeCrashesUrl(String url) { + if (url == null) { + return ""; + } + String trimmed = url.trim(); + if (trimmed.isEmpty()) { + return trimmed; + } + if (trimmed.contains("://f0xx.org/app/androidcast_project/crashes") + || trimmed.contains("://foxx.org/app/androidcast_project/crashes")) { + return trimmed.replace("://f0xx.org/", "://" + APPS_HOST + "/") + .replace("://foxx.org/", "://" + APPS_HOST + "/"); + } + return trimmed; + } +} diff --git a/app/src/main/java/com/foxx/androidcast/network/transport/StreamProtectionCapability.java b/app/src/main/java/com/foxx/androidcast/network/transport/StreamProtectionCapability.java index 75e2db5..a8d94dc 100644 --- a/app/src/main/java/com/foxx/androidcast/network/transport/StreamProtectionCapability.java +++ b/app/src/main/java/com/foxx/androidcast/network/transport/StreamProtectionCapability.java @@ -13,7 +13,9 @@ package com.foxx.androidcast.network.transport; **********************************************************************/ import com.foxx.androidcast.CastSettings; +import java.util.ArrayList; import java.util.EnumSet; +import java.util.List; /** Runtime FEC/NACK implementations on this device (not a user setting). */ public final class StreamProtectionCapability { @@ -54,4 +56,14 @@ public final class StreamProtectionCapability { } return EnumSet.of(CastSettings.StreamProtection.NONE); } + + /** Modes this device advertises on the wire (handshake uses AUTO → full set). */ + public static List advertisedProtectionInOrder() { + List out = new ArrayList<>(); + out.add("AUTO (handshake)"); + for (CastSettings.StreamProtection mode : receiverImplementations()) { + out.add(mode.name()); + } + return out; + } } diff --git a/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessCoordinator.java b/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessCoordinator.java index fd77faa..acb4139 100644 --- a/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessCoordinator.java +++ b/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessCoordinator.java @@ -21,7 +21,7 @@ public final class RemoteAccessCoordinator { AppPreferences.clearDevRemoteAccessSession(app); return; } - if (mode == RemoteAccessMode.WIREGUARD) { + if (mode == RemoteAccessMode.WIREGUARD || mode == RemoteAccessMode.RSSH) { RemoteAccessService.start(app, mode); return; } diff --git a/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessMode.java b/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessMode.java index 3441bde..53c72b2 100644 --- a/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessMode.java +++ b/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessMode.java @@ -8,7 +8,7 @@ public enum RemoteAccessMode { DISABLED, /** WireGuard outbound tunnel (v1). */ WIREGUARD, - /** Reverse SSH — reserved; not selectable in UI until implemented. */ + /** Reverse SSH — alpha essential; selectable when implemented. */ RSSH; public static RemoteAccessMode fromStored(String name) { @@ -39,6 +39,6 @@ public enum RemoteAccessMode { } public boolean isActive() { - return this == WIREGUARD; + return this == WIREGUARD || this == RSSH; } } diff --git a/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessService.java b/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessService.java index 5276862..0d13f85 100644 --- a/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessService.java +++ b/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessService.java @@ -62,7 +62,7 @@ public final class RemoteAccessService extends Service { long exp = AppPreferences.getDevRemoteAccessExpiresAt(this); if (exp > 0 && exp < System.currentTimeMillis() / 1000L) { Log.i(TAG, "session expired — tearing down tunnel"); - WireGuardTunnelBridge.tearDown(this); + RemoteAccessTunnelHelper.tearDownAll(this); AppPreferences.clearDevRemoteAccessSession(this); } } @@ -95,7 +95,7 @@ public final class RemoteAccessService extends Service { String action = intent != null ? intent.getAction() : ACTION_POLL; if (ACTION_STOP.equals(action)) { cancelPoll(this); - WireGuardTunnelBridge.tearDown(this); + RemoteAccessTunnelHelper.tearDownAll(this); stopForeground(true); stopSelf(); return START_NOT_STICKY; @@ -135,7 +135,12 @@ public final class RemoteAccessService extends Service { hb.put("app_version", com.foxx.androidcast.BuildConfig.VERSION_NAME); hb.put("tunnel_mode", mode.toApiValue()); JSONArray caps = new JSONArray(); - caps.put("wireguard"); + if (mode == RemoteAccessMode.WIREGUARD) { + caps.put("wireguard"); + } else if (mode == RemoteAccessMode.RSSH) { + caps.put("rssh"); + caps.put("ssh_reverse"); + } caps.put("files_app_home"); hb.put("capabilities", caps); @@ -147,7 +152,7 @@ public final class RemoteAccessService extends Service { if ("connect".equals(raAction)) { handleConnect(resp); } else if ("disabled".equals(raAction) || "deny".equals(raAction)) { - WireGuardTunnelBridge.tearDown(this); + RemoteAccessTunnelHelper.tearDownAll(this); } } catch (Exception e) { Log.w(TAG, "poll failed: " + e.getMessage()); @@ -156,7 +161,7 @@ public final class RemoteAccessService extends Service { private void postDisableHeartbeat() { try { - WireGuardTunnelBridge.tearDown(this); + RemoteAccessTunnelHelper.tearDownAll(this); AppPreferences.clearDevRemoteAccessSession(this); JSONObject hb = new JSONObject(); hb.put("type", "ra"); @@ -170,26 +175,34 @@ public final class RemoteAccessService extends Service { } private void handleConnect(JSONObject resp) { - if (!"wireguard".equals(resp.optString("tunnel", ""))) { - Log.w(TAG, "unsupported tunnel in connect"); - return; - } + String tunnel = resp.optString("tunnel", ""); String sessionId = resp.optString("session_id", ""); long expiresAt = resp.optLong("expires_at", 0L); if (!sessionId.isEmpty()) { AppPreferences.setDevRemoteAccessSession(this, sessionId, expiresAt); } JSONObject creds = resp.optJSONObject("credentials"); - String wgConfig = WireGuardConfigBuilder.fromConnectCredentials(creds); - if (wgConfig.isEmpty()) { + if (creds == null) { return; } - if (WireGuardTunnelBridge.applyConfig(this, wgConfig)) { - updateNotificationConnected(sessionId); + boolean ok = false; + if ("wireguard".equals(tunnel)) { + String wgConfig = WireGuardConfigBuilder.fromConnectCredentials(creds); + if (!wgConfig.isEmpty()) { + ok = WireGuardTunnelBridge.applyConfig(this, wgConfig); + } + } else if ("ssh_reverse".equals(tunnel)) { + ok = ReverseSshTunnelBridge.applyConfig(this, creds); + } else { + Log.w(TAG, "unsupported tunnel in connect: " + tunnel); + return; + } + if (ok) { + updateNotificationConnected(sessionId, tunnel); } } - private void updateNotificationConnected(String sessionId) { + private void updateNotificationConnected(String sessionId, String tunnel) { NotificationManager nm = getSystemService(NotificationManager.class); if (nm == null) { return; @@ -197,6 +210,9 @@ public final class RemoteAccessService extends Service { String text = sessionId.isEmpty() ? getString(R.string.dev_remote_access_notification_connected) : getString(R.string.dev_remote_access_notification_session, sessionId); + if ("ssh_reverse".equals(tunnel)) { + text = text + " (RSSH)"; + } nm.notify(NOTIF_ID, new NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setContentTitle(getString(R.string.dev_remote_access_notification_title)) diff --git a/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessTunnelHelper.java b/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessTunnelHelper.java new file mode 100644 index 0000000..220258e --- /dev/null +++ b/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessTunnelHelper.java @@ -0,0 +1,13 @@ +package com.foxx.androidcast.remoteaccess; + +import android.content.Context; + +/** Tear down all remote-access transports (WireGuard + RSSH). */ +public final class RemoteAccessTunnelHelper { + private RemoteAccessTunnelHelper() {} + + public static void tearDownAll(Context context) { + WireGuardTunnelBridge.tearDown(context); + ReverseSshTunnelBridge.tearDown(context); + } +} diff --git a/app/src/main/java/com/foxx/androidcast/remoteaccess/ReverseSshTunnelBridge.java b/app/src/main/java/com/foxx/androidcast/remoteaccess/ReverseSshTunnelBridge.java new file mode 100644 index 0000000..93f7a55 --- /dev/null +++ b/app/src/main/java/com/foxx/androidcast/remoteaccess/ReverseSshTunnelBridge.java @@ -0,0 +1,116 @@ +package com.foxx.androidcast.remoteaccess; + +import android.content.Context; +import android.util.Log; + +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.Session; + +import org.json.JSONObject; + +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Outbound reverse SSH (-R) to bastion. Alpha path — no {@link android.net.VpnService}. + */ +public final class ReverseSshTunnelBridge { + private static final String TAG = "RsshTunnelBridge"; + private static final int CONNECT_TIMEOUT_MS = 30_000; + + private static volatile Thread worker; + private static volatile Session session; + private static final AtomicBoolean connected = new AtomicBoolean(false); + + private ReverseSshTunnelBridge() {} + + public static boolean applyConfig(Context context, JSONObject creds) { + if (creds == null) { + return false; + } + tearDown(context); + String host = creds.optString("bastion_host", ""); + int port = creds.optInt("bastion_port", 443); + String user = creds.optString("username", ""); + String pass = creds.optString("password", ""); + int remotePort = creds.optInt("remote_bind_port", 0); + String localHost = creds.optString("local_forward_host", "127.0.0.1"); + int localPort = creds.optInt("local_forward_port", 8022); + if (host.isEmpty() || user.isEmpty() || pass.isEmpty() || remotePort <= 0) { + Log.w(TAG, "incomplete RSSH credentials"); + return false; + } + CountDownLatch connectedLatch = new CountDownLatch(1); + AtomicBoolean ok = new AtomicBoolean(false); + worker = new Thread(() -> { + try { + JSch jsch = new JSch(); + Session s = jsch.getSession(user, host, port); + s.setPassword(pass); + Properties cfg = new Properties(); + cfg.put("StrictHostKeyChecking", "no"); + s.setConfig(cfg); + s.setServerAliveInterval(30_000); + s.connect(CONNECT_TIMEOUT_MS); + s.setPortForwardingR(remotePort, localHost, localPort); + session = s; + connected.set(true); + ok.set(true); + connectedLatch.countDown(); + Log.i(TAG, "RSSH -R " + remotePort + " -> " + localHost + ":" + localPort); + while (s.isConnected() && !Thread.currentThread().isInterrupted()) { + Thread.sleep(5_000); + } + } catch (Exception e) { + Log.w(TAG, "RSSH tunnel failed: " + e.getMessage()); + connectedLatch.countDown(); + } finally { + connected.set(false); + Session s = session; + session = null; + if (s != null) { + try { + s.disconnect(); + } catch (Exception ignored) { + } + } + } + }, "androidcast-rssh"); + worker.setDaemon(true); + worker.start(); + try { + if (!connectedLatch.await(CONNECT_TIMEOUT_MS + 5_000L, TimeUnit.MILLISECONDS)) { + Log.w(TAG, "RSSH connect timed out"); + return false; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + return ok.get(); + } + + public static void tearDown(Context context) { + Thread w = worker; + worker = null; + Session s = session; + session = null; + connected.set(false); + if (s != null) { + try { + s.disconnect(); + } catch (Exception ignored) { + } + } + if (w != null) { + w.interrupt(); + } + } + + public static boolean isConnected() { + Session s = session; + return s != null && s.isConnected(); + } +} diff --git a/app/src/main/java/com/foxx/androidcast/sender/CastSendPump.java b/app/src/main/java/com/foxx/androidcast/sender/CastSendPump.java index 78c442a..aa76e7a 100644 --- a/app/src/main/java/com/foxx/androidcast/sender/CastSendPump.java +++ b/app/src/main/java/com/foxx/androidcast/sender/CastSendPump.java @@ -150,6 +150,7 @@ public final class CastSendPump { } byte[] payload = item.buildPayload(); session.send(item.type, payload); + com.foxx.androidcast.dev.StreamDumpWriter.recordFrame(item.type, payload); int payloadSize = payload != null ? payload.length : 0; SessionStatsContext.recordOutboundMessage(item.type, item.keyFrame, payloadSize); if (statsPlane != null) { diff --git a/app/src/main/java/com/foxx/androidcast/sender/CodecCatalog.java b/app/src/main/java/com/foxx/androidcast/sender/CodecCatalog.java index 83ffc7d..d428a99 100644 --- a/app/src/main/java/com/foxx/androidcast/sender/CodecCatalog.java +++ b/app/src/main/java/com/foxx/androidcast/sender/CodecCatalog.java @@ -118,6 +118,35 @@ public final class CodecCatalog { return localAdvertisedAudioEncoderLines(settings); } + /** Audio codec names advertised on wire (priority order; excludes AUTO). */ + public static List localAdvertisedAudioCodecsInOrder(CastSettings settings) { + List out = new ArrayList<>(); + for (AudioCodecChoice c : audioCodecChoicesForUi(settings)) { + if (c.codec == CastSettings.AudioCodec.AUTO) { + continue; + } + out.add(c.codec.name()); + } + return out; + } + + /** Video MIME types advertised on wire (encoder catalog order). */ + public static List localAdvertisedVideoMimesInOrder() { + return localEncoderMimes(); + } + + /** Video codec labels for diagnostics (priority order; excludes AUTO). */ + public static List localAdvertisedVideoCodecsInOrder(CastSettings settings) { + List out = new ArrayList<>(); + for (VideoCodecChoice c : videoCodecChoicesForUi(settings)) { + if (c.codec == CastSettings.VideoCodec.AUTO) { + continue; + } + out.add(c.codec.name()); + } + return out; + } + private static String audioAdvertisedLine(CastSettings.AudioCodec codec, String mime, CastSettings settings) { CodecTier tier = com.foxx.androidcast.media.CodecPriorityCatalog.tierForAudioPreference(codec); int score = com.foxx.androidcast.media.CodecPriorityCatalog.priorityScore(codec, settings); diff --git a/app/src/main/java/com/foxx/androidcast/sender/ScreenCastService.java b/app/src/main/java/com/foxx/androidcast/sender/ScreenCastService.java index 9160c3f..57c63c2 100644 --- a/app/src/main/java/com/foxx/androidcast/sender/ScreenCastService.java +++ b/app/src/main/java/com/foxx/androidcast/sender/ScreenCastService.java @@ -294,6 +294,7 @@ public class ScreenCastService extends Service implements sendPump = new CastSendPump(); sendPump.start(session, () -> stopping.set(true), plane); } + com.foxx.androidcast.dev.StreamDumpWriter.startIfEnabled(this); casting.set(true); CastActiveState.setSenderCasting(true); CastTrayNotifier.refresh(this); @@ -989,6 +990,7 @@ public class ScreenCastService extends Service implements sendPump.stop(); sendPump = null; } + com.foxx.androidcast.dev.StreamDumpWriter.finalizeIfActive(this); if (fanoutPump != null) { fanoutPump.stop(); fanoutPump = null; diff --git a/app/src/main/res/layout/activity_dev_diagnostics_panel.xml b/app/src/main/res/layout/activity_dev_diagnostics_panel.xml index 06d80b4..e5aa696 100644 --- a/app/src/main/res/layout/activity_dev_diagnostics_panel.xml +++ b/app/src/main/res/layout/activity_dev_diagnostics_panel.xml @@ -56,6 +56,10 @@ + + diff --git a/app/src/main/res/layout/activity_dev_settings_panel.xml b/app/src/main/res/layout/activity_dev_settings_panel.xml index d7ea754..a36c13d 100644 --- a/app/src/main/res/layout/activity_dev_settings_panel.xml +++ b/app/src/main/res/layout/activity_dev_settings_panel.xml @@ -30,6 +30,20 @@ android:layout_marginTop="12dp" android:text="@string/dev_grab_session_stats" /> + + + + Аудиокодеки (сжатие) Сеть Другой вывод / трансляция + Полная реклама возможностей Разрешение отправителя (тест) Режим списка разрешений Раздельный (Оригинал / 720p / 480p / Авто) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5a85b39..77efd7d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -223,6 +223,7 @@ Audio codecs (compression) Network Other casting / display + Full advertisement info Sender resolution UI (test) Resolution preset mode Separated (Original / 720p / 480p / Auto) @@ -254,6 +255,8 @@ Show debug overlay on receiver Show codec priority scores in settings Grab anonymous session stats + Record stream dump while casting (FR 0.1) + Writes zip under files/stream_dumps/ (max 5 min or 100 MB). Pull via adb. Diagnostic cast ping-pong (~1/s) Ping response mode (this device) Mirror as-is (dummy pong) @@ -265,8 +268,9 @@ Remote access (on-demand debug) Disabled WireGuard - RSSH (reverse SSH — not available yet) - When enabled, device polls BE every 1–7 min (type: ra). Disabled sends teardown + BE notification. RSSH is reserved for a future release. + RSSH (reverse SSH) + RSSH (reverse SSH — alpha; not available yet) + When enabled, device polls BE every 1–7 min (type:ra). WireGuard needs Android VPN permission once. RSSH uses outbound SSH — no VPN prompt. Reverse SSH (RSSH) is not implemented yet. Remote access: %1$s VPN permission required for WireGuard remote access. @@ -277,7 +281,7 @@ Connected · session %1$s Network self-test Network self-test tab unlocked - Hidden diagnostics tab. Long-press the tabs row to unlock. Runs quick network checks: connectivity, captive portal, CIDR/DNS/MTU, peer reachability, backend RTT/throughput, NAT and NTP correction. + Hidden diagnostics tab. Long-press the tabs row to unlock. Runs quick network checks: connectivity, captive portal, CIDR/DNS/MTU, cast LAN peers (UDP discovery), backend RTT/throughput (apps.f0xx.org), NAT and NTP correction. Run self-test Force re-check Waiting for self-test run… diff --git a/app/src/test/java/com/foxx/androidcast/dev/StreamDumpWriterTest.java b/app/src/test/java/com/foxx/androidcast/dev/StreamDumpWriterTest.java new file mode 100644 index 0000000..12b8103 --- /dev/null +++ b/app/src/test/java/com/foxx/androidcast/dev/StreamDumpWriterTest.java @@ -0,0 +1,27 @@ +package com.foxx.androidcast.dev; + +import org.junit.Test; + +import java.nio.ByteBuffer; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class StreamDumpWriterTest { + + @Test + public void lengthPrefixRoundTrip() { + byte[] payload = new byte[] {1, 2, 3, 4, 5}; + byte[] framed = StreamDumpWriter.encodeLengthPrefixedFrame(payload); + assertEquals(payload.length, StreamDumpWriter.readLengthPrefix(framed)); + byte[] body = new byte[payload.length]; + System.arraycopy(framed, 4, body, 0, payload.length); + assertArrayEquals(payload, body); + } + + @Test + public void lengthPrefixBigEndian() { + byte[] framed = StreamDumpWriter.encodeLengthPrefixedFrame(new byte[10]); + assertEquals(10, ByteBuffer.wrap(framed, 0, 4).getInt()); + } +} diff --git a/app/src/test/java/com/foxx/androidcast/network/BackendEndpointsTest.java b/app/src/test/java/com/foxx/androidcast/network/BackendEndpointsTest.java new file mode 100644 index 0000000..e3cd2b6 --- /dev/null +++ b/app/src/test/java/com/foxx/androidcast/network/BackendEndpointsTest.java @@ -0,0 +1,20 @@ +package com.foxx.androidcast.network; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class BackendEndpointsTest { + @Test + public void canonicalUrlsUseAppsHost() { + assertTrue(BackendEndpoints.HEARTBEAT_URL.startsWith("https://apps.f0xx.org/")); + assertTrue(BackendEndpoints.UPLOAD_URL.contains("/api/upload.php")); + } + + @Test + public void normalizeCrashesUrlRewritesLegacyHost() { + String legacy = "https://f0xx.org/app/androidcast_project/crashes/api/upload.php"; + assertEquals(BackendEndpoints.UPLOAD_URL, BackendEndpoints.normalizeCrashesUrl(legacy)); + } +} diff --git a/app/src/test/java/com/foxx/androidcast/remoteaccess/RemoteAccessModeTest.java b/app/src/test/java/com/foxx/androidcast/remoteaccess/RemoteAccessModeTest.java index 2ac89e4..453af31 100644 --- a/app/src/test/java/com/foxx/androidcast/remoteaccess/RemoteAccessModeTest.java +++ b/app/src/test/java/com/foxx/androidcast/remoteaccess/RemoteAccessModeTest.java @@ -29,9 +29,14 @@ public class RemoteAccessModeTest { } @Test - public void isActive_onlyWireGuard() { + public void toApiValue_rssh() { + assertEquals("rssh", RemoteAccessMode.RSSH.toApiValue()); + } + + @Test + public void isActive_wireguardAndRssh() { assertFalse(RemoteAccessMode.DISABLED.isActive()); assertTrue(RemoteAccessMode.WIREGUARD.isActive()); - assertFalse(RemoteAccessMode.RSSH.isActive()); + assertTrue(RemoteAccessMode.RSSH.isActive()); } } diff --git a/docs/20260602_REVERSE_SSH_proposals_summary.pdf b/docs/20260602_REVERSE_SSH_proposals_summary.pdf index 7c1d454..f0bf320 100644 --- a/docs/20260602_REVERSE_SSH_proposals_summary.pdf +++ b/docs/20260602_REVERSE_SSH_proposals_summary.pdf @@ -453,7 +453,7 @@ endobj endobj 78 0 obj << -/Author (Android Cast project) /CreationDate (D:20260605123816+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123816+02'00') /Producer (ReportLab PDF Library - \(opensource\)) +/Author (Android Cast project) /CreationDate (D:20260608122520+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122520+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Subject (\(unspecified\)) /Title (Android Cast \204 On-Demand Remote Access \(Reverse Tunnel\) Proposals) /Trapped /False >> endobj @@ -995,7 +995,7 @@ xref trailer << /ID -[<27be2a803248e97757fa9331ed0b01e2><27be2a803248e97757fa9331ed0b01e2>] +[<0bc2f0c306f87efd0a3d63157383ae9d><0bc2f0c306f87efd0a3d63157383ae9d>] % ReportLab generated PDF document -- digest (opensource) /Info 78 0 R diff --git a/docs/20260607-2FA-email-mobile-auth-flow.md b/docs/20260607-2FA-email-mobile-auth-flow.md new file mode 100644 index 0000000..ab748ca --- /dev/null +++ b/docs/20260607-2FA-email-mobile-auth-flow.md @@ -0,0 +1,418 @@ +# Android Cast — Email agent config & 2FA / mobile auth flow + +_Date: 2026-06-07 — FR §9 (email) + FR §10 (registration, 2FA, mobile) — **alpha must-have**_ + +--- + +--- + +## Table of contents + + +- [Document history](#document-history) +- [Executive summary](#executive-summary) +- [Current state (repo)](#current-state-repo) +- [§9 — Email addresses & agent config](#9-email-addresses-agent-config) + - [§9.1 Prerequisites](#91-prerequisites) + - [§9.2 Register addresses (free — DNS, not “buy mailboxes”)](#92-register-addresses-free-dns-not-buy-mailboxes) + - [§9.3 Forward / duplicate to Gmail (info@ → foxxspambox0@gmail.com)](#93-forward-duplicate-to-gmail-info-foxxspambox0gmailcom) + - [§9.4 Recommended prod layout for this project](#94-recommended-prod-layout-for-this-project) + - [§9.5 Outbound mail from BE (verification emails)](#95-outbound-mail-from-be-verification-emails) +- [§10 — Registration, 2FA, mobile auth](#10-registration-2fa-mobile-auth) + - [§10.1 Registration & login stages](#101-registration-login-stages) + - [§10.2 Open-source building blocks (recommended)](#102-open-source-building-blocks-recommended) + - [§10.3 Users DB migration (privacy-first)](#103-users-db-migration-privacy-first) + - [§10.4 On-demand user maintenance (admin + self-service)](#104-on-demand-user-maintenance-admin-self-service) + - [§10.5 Operator checklist (track progress)](#105-operator-checklist-track-progress) + - [§10.6 Mobile-friendly UX (not BE-only)](#106-mobile-friendly-ux-not-be-only) + - [§10.7 Alpha implementation plan](#107-alpha-implementation-plan) +- [UI/UX alignment (existing consoles)](#uiux-alignment-existing-consoles) +- [Security & compliance notes](#security-compliance-notes) +- [Open questions](#open-questions) +- [Source linkage](#source-linkage) + + +**Documentation index:** [README.md](README.md) + +--- + +## Document history + +| Rev | Date | Focus | +|-----|------|--------| +| **1** | 2026-06-07 | Initial FR capture: email forwarding + registration/2FA for alpha; infra + BE + mobile flows. | + +**Alpha gate:** agreed with project owners — user registration with verified email + second factor is **must-have for full alpha** (alongside RSSH remote access). LAN-only cast demo may still skip account flows. + +--- + +## Executive summary + +| Track | Goal | +|-------|------| +| **§9 Email** | Public-facing addresses `info@`, `admin@`, `root@` on **`apps.f0xx.org`** (and optional aliases on **`f0xx.org`**) that **forward** to personal Gmail; no paid mailbox SKU required. | +| **§10 Auth** | Replace “Register (coming soon)” with **email verification + flexible 2FA** (TOTP QR, optional WebAuthn/passkey on Android), captcha on abuse-prone endpoints, **privacy-preserving** audit tables, admin tools for root/slug admins. | +| **Mobile** | Same flows usable on phone browsers **and** deep links / Custom Tabs from the Android app where appropriate. | +| **Licenses** | Prefer **MIT / BSD / Apache-2.0** PHP libraries; avoid proprietary-only auth SaaS for core path. | + +--- + +## Current state (repo) + +| Area | Today | +|------|--------| +| BE login | Username + `password_hash` only — [`Auth.php`](../examples/crash_reporter/backend/src/Auth.php) | +| Registration | UI placeholder: “Register (coming soon)” — [`login.php`](../examples/crash_reporter/backend/views/login.php) | +| Users table | `username`, `password_hash`, `role` — no email, no 2FA — [`schema.mariadb.sql`](../examples/crash_reporter/backend/sql/schema.mariadb.sql) | +| Outbound email | **Not configured** — no SMTP in [`config.example.php`](../examples/crash_reporter/backend/config/config.example.php) | +| Mobile app | No account linking to crashes console yet | +| Roadmap | “User registration + email verification” — [`examples/crash_reporter/ROADMAP.md`](../examples/crash_reporter/ROADMAP.md) | + +--- + +## §9 — Email addresses & agent config + +Target addresses (any one canonical pair is fine; document both for flexibility): + +| Role | Preferred | Alias | +|------|-----------|--------| +| Public / info | `info@apps.f0xx.org` | `info@f0xx.org` | +| Admin / ops | `admin@apps.f0xx.org` | `admin@f0xx.org`, `root@apps.f0xx.org` | + +**Agent config** (future): BE `config.php` → `mail.from`, `mail.reply_to`, `mail.envelope_from` pointing at these addresses once DNS and forwarding work. + +### §9.1 Prerequisites + +You do **not** need to buy Google Workspace or Microsoft 365 mailboxes. You **do** need: + +| # | Prerequisite | Why | +|---|--------------|-----| +| 1 | **DNS control** for `f0xx.org` and `apps.f0xx.org` | MX, SPF, DKIM, DMARC live at the DNS host (registrar or Cloudflare). | +| 2 | **Decide canonical mail domain** | Recommend **`apps.f0xx.org`** for product mail; keep `f0xx.org` as forward/alias only if desired. | +| 3 | **Inbound path** | MX records → free forwarder **or** self-hosted Postfix alias on FE/BE. | +| 4 | **Outbound path (BE)** | SMTP relay for verification/reset mail (see §9.5) — can be Gmail SMTP, SendGrid free tier, or local Postfix with SPF alignment. | +| 5 | **TLS on web** | Already have `https://apps.f0xx.org` — verification links use same vhost. | +| 6 | **Gmail destination** | e.g. `foxxspambox0@gmail.com` — receives forwarded + BCC copies. | + +**Deliverability minimum (avoid spam folder):** + +```text +SPF TXT @apps.f0xx.org "v=spf1 include: ~all" +DKIM TXT (provider gives name/value) +DMARC TXT _dmarc.apps.f0xx.org "v=DMARC1; p=none; rua=mailto:admin@apps.f0xx.org" +``` + +Start with `p=none`; tighten after monitoring. + +### §9.2 Register addresses (free — DNS, not “buy mailboxes”) + +**“Register” here = DNS + forwarder rules**, not purchasing seats. + +#### Option A — **Cloudflare Email Routing** (recommended if DNS is on Cloudflare) + +1. Cloudflare dashboard → **Email** → **Email Routing** → enable for `apps.f0xx.org`. +2. Add **destination address** `foxxspambox0@gmail.com` (verify Gmail link once). +3. Create **Custom addresses**: + - `info@apps.f0xx.org` → forward to Gmail + - `admin@apps.f0xx.org` → forward to Gmail + - `root@apps.f0xx.org` → forward to Gmail (or same rule as admin) +4. Cloudflare adds MX automatically; copy **SPF** hint from dashboard. +5. Repeat for **`f0xx.org`** zone if you want `info@f0xx.org` (separate zone rules or redirect-only). + +**Cost:** $0 on Cloudflare free plan. + +#### Option B — **ImprovMX** (DNS at any registrar) + +1. Sign up at [improvmx.com](https://improvmx.com) (free tier: limited aliases). +2. Add domain `apps.f0xx.org` → set MX to ImprovMX hosts (they show exact records). +3. Create aliases `info`, `admin`, `root` → `foxxspambox0@gmail.com`. +4. Add SPF include for ImprovMX. + +#### Option C — **Self-hosted alias** (Gentoo FE or Alpine BE) + +If you already run Postfix on FE: + +```text +/etc/postfix/virtual: +info@apps.f0xx.org foxxspambox0@gmail.com +admin@apps.f0xx.org foxxspambox0@gmail.com +root@apps.f0xx.org foxxspambox0@gmail.com +``` + +Then `postmap /etc/postfix/virtual && systemctl reload postfix`. Requires **MX** pointing to FE public IP and port **25** reachable (often blocked on residential; OK on HVM with firewall rule). + +**Do first:** pick **A or B** unless you explicitly want to operate SMTP on FE. + +### §9.3 Forward / duplicate to Gmail (info@ → foxxspambox0@gmail.com) + +| Method | Inbox copy | Send-as From | IMAP on apps domain | +|--------|------------|--------------|---------------------| +| **Forward only** (Cloudflare / ImprovMX) | Yes — same subject/body in Gmail | Reply-from Gmail unless configured | N/A — read in Gmail | +| **Gmail “Send mail as”** | N/A | Can send as `info@apps.f0xx.org` if SMTP creds exist | Optional | +| **BCC archive** | Forward + hidden copy to second Gmail | — | Rarely needed | + +For FR §9.3 (“same topic and body redirected”): **plain forward** is enough. Gmail shows original `To:` header; you can filter with Gmail rule `to:info@apps.f0xx.org`. + +**Optional Gmail filter:** + +```text +To: info@apps.f0xx.org → label: androidcast-info +``` + +No IMAP/POP3 server on `apps.f0xx.org` is required for this FR. + +### §9.4 Recommended prod layout for this project + +```text +Inbound (public): + info@apps.f0xx.org ──forward──► foxxspambox0@gmail.com + admin@apps.f0xx.org ──forward──► foxxspambox0@gmail.com + root@apps.f0xx.org ──forward──► foxxspambox0@gmail.com (same inbox or +tag) + +Outbound (BE PHP): + From: Android Cast Issues + Reply-To: info@apps.f0xx.org + SMTP: transactional relay (see §9.5) +``` + +Document chosen provider in [`INFRA.md`](INFRA.md) § changelog once live. + +### §9.5 Outbound mail from BE (verification emails) + +Add to `config.example.php` (implementation phase): + +```php +'mail' => [ + 'transport' => 'smtp', // smtp | sendmail + 'from' => 'Android Cast Issues ', + 'reply_to' => 'info@apps.f0xx.org', + 'smtp' => [ + 'host' => 'smtp.example.com', + 'port' => 587, + 'encryption' => 'tls', + 'username' => '', + 'password' => '', // env / secrets file, not git + ], +], +``` + +**Free/low-cost SMTP options:** + +| Provider | Notes | +|----------|--------| +| Gmail SMTP + App Password | OK for low volume; From may need “Send as” alignment | +| Brevo / SendGrid free tier | Better for `noreply@apps.f0xx.org` alignment | +| Local Postfix on BE | Full control; must publish SPF for BE egress IP | + +PHP library: **Symfony Mailer** (MIT) — already common in PHP ecosystems. + +--- + +## §10 — Registration, 2FA, mobile auth + +### §10.1 Registration & login stages + +Proposed **alpha** flow (flexible — toggles in config): + +```mermaid +sequenceDiagram + participant U as User (mobile browser) + participant BE as apps.f0xx.org PHP + participant M as Mail / Gmail + participant A as Android app (optional) + + U->>BE: POST /register (email, username, password) + BE->>BE: captcha verify, rate limit + BE->>BE: store pending registration (hashed secrets) + BE->>M: Send verify link (24h token) + M->>U: Email with https://apps.f0xx.org/.../verify?token= + U->>BE: GET verify (or Custom Tab from app) + BE->>U: Prompt enroll 2FA (TOTP QR and/or WebAuthn) + U->>BE: POST /2fa/enroll (TOTP secret or passkey) + BE->>U: Account active → login + + Note over U,BE: Login thereafter + U->>BE: username + password + BE->>U: 2FA challenge (TOTP code or WebAuthn) + U->>BE: verify second factor + BE->>U: Session cookie (existing ac_crash_sess) +``` + +| Stage | Method | Mobile-friendly | +|-------|--------|-----------------| +| **10.1.2 Email confirmation** | HTTPS link with signed token | Email app → Chrome Custom Tab; large tap target | +| **10.1.3 TOTP / QR** | Standard `otpauth://` QR + manual key | Scan with Google Authenticator / Aegis; or type 6 digits | +| **10.1.4 WebAuthn / passkey** | Platform authenticator (fingerprint) | **Android Chrome** + future app WebView if needed | +| **Captcha** | ALTCHA (MIT, self-hosted) or rate limit only at alpha | Invisible/PoW — no Google reCAPTCHA dependency | +| **Recovery** | Backup codes + optional second recovery email | Printable one-time codes; store hashed | + +**Roles:** new self-registered users default to `viewer` + default company membership (existing RBAC). Elevation to `admin`/`root` remains **admin-only**. + +### §10.2 Open-source building blocks (recommended) + +| Function | Library | License | Notes | +|----------|---------|---------|-------| +| SMTP | [symfony/mailer](https://github.com/symfony/mailer) | MIT | Transport abstraction | +| TOTP | [spomky-labs/otphp](https://github.com/spomky-labs/otphp) | MIT | RFC 6238; QR via endroid/qr-code | +| WebAuthn | [web-auth/webauthn-lib](https://github.com/web-auth/webauthn-lib) | MIT | Passkeys; PHP 8.1+ | +| Password hash | `password_hash()` | PHP core | Already used | +| Captcha | [altcha-org/altcha](https://github.com/altcha-org/altcha) | MIT | Self-hosted PoW widget | +| JWT / tokens | Signed random + HMAC in DB | — | Prefer opaque tokens stored hashed | + +**Avoid as hard dependency for alpha:** proprietary Auth0/Okta-only flows (OK as optional future SSO). + +**Android app (phase 2 of alpha auth):** + +| Feature | Approach | +|---------|----------| +| Open verify link | `CustomTabsIntent` → `https://apps.f0xx.org/.../verify` | +| TOTP | Optional in-app entry (Web login sufficient for alpha) | +| Passkey | Chrome on device; link account in settings later | +| Deep link | `androidcast://auth/verify?token=` optional — not required if HTTPS works | + +### §10.3 Users DB migration (privacy-first) + +New migration **`008_auth_email_2fa.sql`** (sketch — apply on MariaDB prod once implemented): + +**Extend `users`:** + +| Column | Type | Notes | +|--------|------|-------| +| `email_normalized` | VARCHAR(254) NULL UNIQUE | Lowercase email | +| `email_verified_at` | TIMESTAMP NULL | NULL until link clicked | +| `status` | ENUM | `pending`, `active`, `locked`, `disabled` | +| `recovery_email_normalized` | VARCHAR(254) NULL | Optional second address | + +**New tables (anonymous / hashed where possible):** + +```sql +-- auth_pending_registrations: delete after verify or TTL +-- auth_factors: user_id, type ENUM('totp','webauthn','backup_code'), secret_or_credential_encrypted, label, created_at +-- auth_tokens: purpose ENUM('verify_email','reset_password','login_magic'), token_hash, expires_at, used_at +-- auth_attempts: ip_hash, username_hash, outcome, created_at (no raw IP in prod if avoidable) +-- auth_audit: actor_user_id, action, meta_json, created_at +``` + +**Privacy rules:** + +- Store **hashed** opaque tokens (SHA-256), never raw token in DB. +- Store TOTP secrets **encrypted at rest** (libsodium `secretbox`, key in `config.php` env). +- `auth_attempts.ip_hash` = HMAC(server_pepper, ip) — supports rate limit without retaining IP. +- WebAuthn credentials: store `credential_id` + `public_key` only (standard). + +SQLite dev: mirror schema in `Database::ensureSchema` like existing RBAC migrations. + +### §10.4 On-demand user maintenance (admin + self-service) + +| Action | Who | UI location | +|--------|-----|-------------| +| Add / change **recovery email** | User | `?view=account_security` (new) | +| Re-enroll TOTP / add passkey | User | Same; requires recent password + 2FA | +| Revoke passkeys / backup codes | User | Same | +| Clear failed login/register attempts | `root`, company `admin` | `?view=rbac` → user row → “Clear auth lockouts” | +| Disable user / force password reset | `root`, `platform_admin` | RBAC admin | +| View auth audit tail | `root` | `?view=auth_audit` (read-only) | + +Session cookie path stays **`/app/androidcast_project`** (shared across crashes/build consoles). + +### §10.5 Operator checklist (track progress) + +Use this table during implementation (copy to ticket **#9 / #10**): + +| # | Task | Owner | Done | +|---|------|-------|------| +| E1 | Enable Cloudflare Email Routing (or ImprovMX) for `apps.f0xx.org` | Ops | ☐ | +| E2 | Forward `info@`, `admin@`, `root@` → Gmail | Ops | ☐ | +| E3 | SPF + DKIM + DMARC records live | Ops | ☐ | +| E4 | Test inbound: send mail to `info@apps.f0xx.org`, arrives Gmail | Ops | ☐ | +| E5 | Configure BE SMTP + send test from staging | Dev | ☐ | +| A1 | Migration `008_auth_email_2fa.sql` + `Database::ensureSchema` | Dev | ☐ | +| A2 | Register + verify email views (EN/RU, `app.css`) | Dev | ☐ | +| A3 | TOTP enroll + login challenge | Dev | ☐ | +| A4 | WebAuthn enroll (optional alpha) | Dev | ☐ | +| A5 | ALTCHA on register/login | Dev | ☐ | +| A6 | Admin: clear lockouts + audit view | Dev | ☐ | +| A7 | Mobile soak: register on phone Chrome, verify, login | QA | ☐ | +| A8 | Document secrets in INFRA + config.example.php | Dev | ☐ | + +### §10.6 Mobile-friendly UX (not BE-only) + +| Requirement | Implementation | +|-------------|----------------| +| Viewport | Already `width=device-width` in [`layout.php`](../examples/crash_reporter/backend/views/layout.php) | +| Touch targets | Buttons ≥ 44px; reuse `.login-card`, `.btn` from [`app.css`](../examples/crash_reporter/backend/public/assets/css/app.css) | +| i18n | Extend [`en.json` / `ru.json`](../examples/crash_reporter/backend/public/assets/i18n/) | +| Email links | Single primary button “Confirm email” — no raw URL only | +| 2FA QR | Responsive `` or SVG; “Can’t scan?” reveals manual secret | +| Errors | Same `.alert` pattern as login | +| Android app | Settings → “Project account” → opens Custom Tab to account/security (post-alpha polish OK) | + +### §10.7 Alpha implementation plan + +| Phase | Scope | Estimate | +|-------|--------|----------| +| **P0 — Email infra** | §9 DNS + forward + outbound SMTP test | 0.5–1 day ops | +| **P1 — Register + verify** | Pending table, mailer, verify link, captcha | 2–3 days | +| **P2 — TOTP login** | Enroll QR + challenge on login | 1–2 days | +| **P3 — Admin tools** | Recovery email, clear lockouts, audit | 1 day | +| **P4 — WebAuthn** | Passkey enroll/login (Chrome/Android) | 1–2 days optional for alpha | +| **P5 — Mobile app link** | Custom Tab entry points | 0.5 day | + +**Alpha sign-off (auth):** new user can register on phone, verify email, enroll TOTP, log in with 2FA; admin can clear lockout; inbound `info@` mail reaches Gmail. + +Update [`ALPHA.md`](ALPHA.md) § G (auth) and [`ROADMAP.md`](ROADMAP.md) alpha backend checklist when P1 starts. + +--- + +## UI/UX alignment (existing consoles) + +Do **not** introduce a new design system. Reuse: + +| Asset | Path | +|-------|------| +| Styles | `public/assets/css/app.css` — `.login-page`, `.login-card`, `.alert`, `.muted`, `.locale-picker` | +| Layout shell | `views/layout.php` — nav, hub card styling | +| i18n | `public/assets/js/i18n.js` + JSON catalogs | +| Analytics hook | `AnalyticsHead::render()` on auth pages (optional) | + +New views (planned): + +- `views/register.php` +- `views/verify_email.php` +- `views/account_security.php` +- `views/two_factor_challenge.php` + +Hub landing contact form (`landing-pages.inc.php`) should eventually use **`info@apps.f0xx.org`** as `mailto:` — align when email live. + +--- + +## Security & compliance notes + +- Enforce **rate limits** on register, login, verify resend (e.g. 5/min/IP hash). +- Lock account after N failed 2FA attempts; unlock via email or admin. +- Do **not** log passwords or TOTP codes. +- Backup codes: generate 10 one-time codes, display once, store bcrypt hashes. +- GDPR-style minimization: optional `auth_attempts` retention job (purge > 90 days). + +--- + +## Open questions + +| # | Question | Default proposal | +|---|----------|------------------| +| Q1 | Require WebAuthn for alpha or TOTP-only? | **TOTP required**; WebAuthn optional P4 | +| Q2 | Allow registration on prod immediately or invite-only? | **Open register** with captcha; `viewer` role | +| Q3 | Single Gmail inbox for all aliases? | Yes for now (`foxxspambox0@gmail.com`) | +| Q4 | Separate `noreply@` for outbound? | Yes — no forward needed; SPF only | + +--- + +## Source linkage + +| Topic | Doc / code | +|-------|------------| +| Infra / DNS | [INFRA.md](INFRA.md) | +| Crash console | [CRASH_REPORTER.md](CRASH_REPORTER.md), [backend README](../examples/crash_reporter/backend/README.md) | +| RBAC | [backend README § RBAC](../examples/crash_reporter/backend/README.md#rbac-phase-1--admin-ui) | +| Alpha gates | [ALPHA.md](ALPHA.md), [ROADMAP.md](ROADMAP.md) | +| PDF build | [`scripts/build-all-docs-pdf.sh`](../scripts/build-all-docs-pdf.sh) | diff --git a/docs/20260607-2FA-email-mobile-auth-flow.pdf b/docs/20260607-2FA-email-mobile-auth-flow.pdf new file mode 100644 index 0000000..b724220 --- /dev/null +++ b/docs/20260607-2FA-email-mobile-auth-flow.pdf @@ -0,0 +1,462 @@ +%PDF-1.4 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 10 0 R /F4 26 0 R /F5 28 0 R /F6 29 0 R + /F7 31 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 45.68504 716.2874 107.925 727.2874 ] /Subtype /Link /Type /Annot +>> +endobj +5 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 45.68504 705.2874 115.477 716.2874 ] /Subtype /Link /Type /Annot +>> +endobj +6 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 45.68504 694.2874 115.485 705.2874 ] /Subtype /Link /Type /Annot +>> +endobj +7 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 45.68504 682.6874 179.525 692.2874 ] /Subtype /Link /Type /Annot +>> +endobj +8 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 59.68504 672.8499 120.1275 682.8499 ] /Subtype /Link /Type /Annot +>> +endobj +9 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 32 0 R /Fit ] /Rect [ 59.68504 662.8499 258.0825 672.8499 ] /Subtype /Link /Type /Annot +>> +endobj +10 0 obj +<< +/BaseFont /Symbol /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +11 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 32 0 R /Fit ] /Rect [ 59.68504 652.8499 298.635 662.8499 ] /Subtype /Link /Type /Annot +>> +endobj +12 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 32 0 R /Fit ] /Rect [ 59.68504 642.8499 215.5875 652.8499 ] /Subtype /Link /Type /Annot +>> +endobj +13 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 32 0 R /Fit ] /Rect [ 59.68504 632.8499 220.9875 642.8499 ] /Subtype /Link /Type /Annot +>> +endobj +14 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 33 0 R /Fit ] /Rect [ 45.68504 622.2874 179.077 633.2874 ] /Subtype /Link /Type /Annot +>> +endobj +15 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 33 0 R /Fit ] /Rect [ 59.68504 611.2874 169.74 620.2874 ] /Subtype /Link /Type /Annot +>> +endobj +16 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 33 0 R /Fit ] /Rect [ 59.68504 601.8499 230.1675 611.8499 ] /Subtype /Link /Type /Annot +>> +endobj +17 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 33 0 R /Fit ] /Rect [ 59.68504 591.8499 190.9575 601.8499 ] /Subtype /Link /Type /Annot +>> +endobj +18 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 34 0 R /Fit ] /Rect [ 59.68504 581.8499 255.8025 591.8499 ] /Subtype /Link /Type /Annot +>> +endobj +19 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 34 0 R /Fit ] /Rect [ 59.68504 571.8499 195.5625 581.8499 ] /Subtype /Link /Type /Annot +>> +endobj +20 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 34 0 R /Fit ] /Rect [ 59.68504 561.8499 187.6425 571.8499 ] /Subtype /Link /Type /Annot +>> +endobj +21 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 35 0 R /Fit ] /Rect [ 59.68504 551.8499 168.9075 561.8499 ] /Subtype /Link /Type /Annot +>> +endobj +22 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 35 0 R /Fit ] /Rect [ 45.68504 541.2874 172.389 552.2874 ] /Subtype /Link /Type /Annot +>> +endobj +23 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 35 0 R /Fit ] /Rect [ 45.68504 529.6874 146.613 539.2874 ] /Subtype /Link /Type /Annot +>> +endobj +24 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 35 0 R /Fit ] /Rect [ 45.68504 519.2874 101.717 530.2874 ] /Subtype /Link /Type /Annot +>> +endobj +25 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 35 0 R /Fit ] /Rect [ 45.68504 508.2874 98.59704 519.2874 ] /Subtype /Link /Type /Annot +>> +endobj +26 0 obj +<< +/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font +>> +endobj +27 0 obj +<< +/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 11 0 R 12 0 R 13 0 R 14 0 R + 15 0 R 16 0 R 17 0 R 18 0 R 19 0 R 20 0 R 21 0 R 22 0 R 23 0 R 24 0 R + 25 0 R ] /Contents 61 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 60 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 + /Trans << + +>> /Type /Page +>> +endobj +28 0 obj +<< +/BaseFont /Helvetica-BoldOblique /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font +>> +endobj +29 0 obj +<< +/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F6 /Subtype /Type1 /Type /Font +>> +endobj +30 0 obj +<< +/Contents 62 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 60 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +31 0 obj +<< +/BaseFont /ZapfDingbats /Name /F7 /Subtype /Type1 /Type /Font +>> +endobj +32 0 obj +<< +/Contents 63 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 60 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +33 0 obj +<< +/Contents 64 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 60 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +34 0 obj +<< +/Contents 65 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 60 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +35 0 obj +<< +/Contents 66 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 60 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +36 0 obj +<< +/Outlines 38 0 R /PageMode /UseNone /Pages 60 0 R /Type /Catalog +>> +endobj +37 0 obj +<< +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Android Cast \204 Email agent config & 2FA / mobile auth flow) /Trapped /False +>> +endobj +38 0 obj +<< +/Count 23 /First 39 0 R /Last 59 0 R /Type /Outlines +>> +endobj +39 0 obj +<< +/Dest [ 30 0 R /Fit ] /Next 40 0 R /Parent 38 0 R /Title (Document history) +>> +endobj +40 0 obj +<< +/Dest [ 30 0 R /Fit ] /Next 41 0 R /Parent 38 0 R /Prev 39 0 R /Title (Executive summary) +>> +endobj +41 0 obj +<< +/Dest [ 30 0 R /Fit ] /Next 42 0 R /Parent 38 0 R /Prev 40 0 R /Title (Current state \(repo\)) +>> +endobj +42 0 obj +<< +/Count 5 /Dest [ 30 0 R /Fit ] /First 43 0 R /Last 47 0 R /Next 48 0 R /Parent 38 0 R + /Prev 41 0 R /Title (\2479 \204 Email addresses & agent config) +>> +endobj +43 0 obj +<< +/Dest [ 30 0 R /Fit ] /Next 44 0 R /Parent 42 0 R /Title (\2479.1 Prerequisites) +>> +endobj +44 0 obj +<< +/Dest [ 32 0 R /Fit ] /Next 45 0 R /Parent 42 0 R /Prev 43 0 R /Title (\2479.2 Register addresses \(free \204 DNS, not \215buy mailboxes\216\)) +>> +endobj +45 0 obj +<< +/Dest [ 32 0 R /Fit ] /Next 46 0 R /Parent 42 0 R /Prev 44 0 R /Title (\376\377\000\247\0009\000.\0003\000 \000F\000o\000r\000w\000a\000r\000d\000 \000/\000 \000d\000u\000p\000l\000i\000c\000a\000t\000e\000 \000t\000o\000 \000G\000m\000a\000i\000l\000 \000\(\000i\000n\000f\000o\000@\000 !\222\000 \000f\000o\000x\000x\000s\000p\000a\000m\000b\000o\000x\0000\000@\000g\000m\000a\000i\000l\000.\000c\000o\000m\000\)) +>> +endobj +46 0 obj +<< +/Dest [ 32 0 R /Fit ] /Next 47 0 R /Parent 42 0 R /Prev 45 0 R /Title (\2479.4 Recommended prod layout for this project) +>> +endobj +47 0 obj +<< +/Dest [ 32 0 R /Fit ] /Parent 42 0 R /Prev 46 0 R /Title (\2479.5 Outbound mail from BE \(verification emails\)) +>> +endobj +48 0 obj +<< +/Count 7 /Dest [ 33 0 R /Fit ] /First 49 0 R /Last 55 0 R /Next 56 0 R /Parent 38 0 R + /Prev 42 0 R /Title (\24710 \204 Registration, 2FA, mobile auth) +>> +endobj +49 0 obj +<< +/Dest [ 33 0 R /Fit ] /Next 50 0 R /Parent 48 0 R /Title (\24710.1 Registration & login stages) +>> +endobj +50 0 obj +<< +/Dest [ 33 0 R /Fit ] /Next 51 0 R /Parent 48 0 R /Prev 49 0 R /Title (\24710.2 Open-source building blocks \(recommended\)) +>> +endobj +51 0 obj +<< +/Dest [ 33 0 R /Fit ] /Next 52 0 R /Parent 48 0 R /Prev 50 0 R /Title (\24710.3 Users DB migration \(privacy-first\)) +>> +endobj +52 0 obj +<< +/Dest [ 34 0 R /Fit ] /Next 53 0 R /Parent 48 0 R /Prev 51 0 R /Title (\24710.4 On-demand user maintenance \(admin + self-service\)) +>> +endobj +53 0 obj +<< +/Dest [ 34 0 R /Fit ] /Next 54 0 R /Parent 48 0 R /Prev 52 0 R /Title (\24710.5 Operator checklist \(track progress\)) +>> +endobj +54 0 obj +<< +/Dest [ 34 0 R /Fit ] /Next 55 0 R /Parent 48 0 R /Prev 53 0 R /Title (\24710.6 Mobile-friendly UX \(not BE-only\)) +>> +endobj +55 0 obj +<< +/Dest [ 35 0 R /Fit ] /Parent 48 0 R /Prev 54 0 R /Title (\24710.7 Alpha implementation plan) +>> +endobj +56 0 obj +<< +/Dest [ 35 0 R /Fit ] /Next 57 0 R /Parent 38 0 R /Prev 48 0 R /Title (UI/UX alignment \(existing consoles\)) +>> +endobj +57 0 obj +<< +/Dest [ 35 0 R /Fit ] /Next 58 0 R /Parent 38 0 R /Prev 56 0 R /Title (Security & compliance notes) +>> +endobj +58 0 obj +<< +/Dest [ 35 0 R /Fit ] /Next 59 0 R /Parent 38 0 R /Prev 57 0 R /Title (Open questions) +>> +endobj +59 0 obj +<< +/Dest [ 35 0 R /Fit ] /Parent 38 0 R /Prev 58 0 R /Title (Source linkage) +>> +endobj +60 0 obj +<< +/Count 6 /Kids [ 27 0 R 30 0 R 32 0 R 33 0 R 34 0 R 35 0 R ] /Type /Pages +>> +endobj +61 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1295 +>> +stream +Gau0ChfIL2&:Vr4Ytm=N)B6jaiLZJpE-!i&V/=;t4n'Y3fr3Ph,KU)fqcha<6CKf)#DO*5/6Vl;\%CP##^H+fh03$'>gHe;,sZjFjZl-kK\6TtbUsqhaDPW-\a_*=6:d9Xon%1>n)6%98G6[B0WQ_D)GZV:i030)sd%H5F(p9Xd$hHV5\&\+%Xt=q&9\IUVl"?oC(fgEptf?>Erj7jTJI/)-T2Nn@Sbm1diXCq"ahn)O[7AjV2U>:E0F!3G8iIfK,WmNhW#;GNK$3`8%S!'Zrfm]GqLUP"=Poj9QUf;^i&[)tbb`KY`!u=YIbD3"!,(WuWKiR5.mO]^06]74HZMl9p&mPVs\E`b.RT^qF2T_9Fi:]\"qu+#K.aBtAG-/sWks2`_>Z&8l82_o0UDBG%2B%l9-j3]2nZAebOH/LXZ^%PFr1b,ir:B0dK)O<]N//*W4nX-/t:Fb#=:Y)L@7VE\2AMpHu.L6#WN3k===Gi]_/s$M124'bW%;3tK;E7%h(-#hCo_n9_VAd6"6(RpT=(79^DWmOlTKc(d3M,Q73Ia)q/,@.arUTMO(n]Tr?_5`b["Io$`LpLiQ+2)h-hi(I#D#p%\Lfht(=gh/fe\3aKLDq@;bW%dBA8WEWq^2cVhWQjboK.endstream +endobj +62 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 3710 +>> +stream +Gb!#^>Ar7W&q801+L&Yu3D!b<]%QJoEHCC/Su,5!(Hi-C-tnIGe]=Jb9.p6nLQlqhY0P37D&et=+ZV?Kq.;SnP2SnC^TBXc+-pSfV]Z/*OcU9:B9lr=L:X,E(eVNXMj0$\T7J`i)_m][&3ai_aaC3pO_KW=ZZtF0pV*HIhS>51!=Fm'F?pn7L8'jF0UT5D\U1/6?#N+N"po3)H/t'"Wnng8r?tj)m8dg2$af;L$'53SgTHZrriI]!-&L/(;)_ifi;I`b?Vd>%"+9`On2X\WD;^`al=Y@oEaV1c@.[&;\+e',jR6$pS>Tm`$]V]DIaJjh-CnN:I70,iH5Cb'[3KU13JHR#D,f6-l,'sJ8)3]qW2-EM^V=O=7hgNLPb24;cku6Up(#=s^;kBSrQJ5ak?WE[iMPcagn@\o5Kb_VH\+t#Z((Z^dcDPalV\`:.a>?,Ih'0_BH"Q>WVFc3:WLGM0&V:3Sl)*i$(A?_QUliAFFA?p)H[^)u'sAs)fY_Yc6mFPU`dP%EEq+LeoX14<'e&A#^]>nn^f*"]k\?hPra,;[]r0"B,'lGC4WK3k2*!R)C.b@32ENh71bsZ_>>iZD*MZ3Z0?o0>h/]-:E55(41.#Lj:hlb<-e]_.@)&WT+nk9M+A%g6%1kBr\2XZ,=qQN*p%,\:(&O*'(8'0])7iO4^,C&^DciL^a]_F9VsRR+=Y18MJXjp%.\(U_n52\W1YY0S'LUnHHLmQhlIe759QYj(1KWa%!UL'ha9tSAb4J,OWZN7?P!Oo-GgOQCkkYrCi.gHVIcF1bmV"l>gZgLtAe>?,:7;cCW[3!/HSHLY=`'2t6;&cg1c^hd'9>LqS#F:9&iLW(M`YgJjU:IOK'oQPYJ^gG6>/'&Ysh$Se9#.`,JM6hn6R):eq;<^@<$/Y+hBS(m*m5pkNVKtup4='0Ig6Ye*BO;0RpD/6be]D'!5<)c[-P.(Xk"8bD72-r^.U$"_gsibJH-n2C1OS4[uHJ(DflP"=qVZ%JkNsJ&hU11IU%CfVD$6+J->p2<(4D;/\`n[d'rg^Y0A&1c$/E3I1h9qoTcJ"k1KcVm+(p.5V!Vg3Tan3aVs0j'uVtj,3miFF1`p6X=hp3S0qdZ!Zcu.pB96NY(jm'';:5tU>^6'd@bmLL,kcTW5`\!*4kYuTkF`mV]bC(&q4Rr0lG9ART-pBH^[>oM"c:\>dF?h95P]b[i>iVA-U)U.O:K]+1[F+(nrarL3V&X]Nhd$qWh"N3q#j6eF^nNR5@M6DKppK4:p2Y_.-^Fb_tbpQ9raUKJ"SgLLK5<9EHg*en[eIXnig[=j?:7F=RgXo\Yrsn*o%QkqO8DBYBi0r/(\T9)<>46G6RGh3D1l?4dZe?O.E3cs]F7G&EDVgIUJ1A(41hB8[hgP]%Bn;-sW&&(D[$8n2AQML.n@6'T7=Rf$7,jPe8El(UEd@g0mjg:Y)Nc,?U!!qP2^BgqU(pi57i/5<"[r=TMV6A:Unl&8VH^dcdh.ZKr\N@"$I96\F>DpBBH8b&8*[32an]_m)KLF;^*m+F\@$&V+d;cbIbCmUm'YhL6Du"H63rGOX5q6Y:u\(p^p_0M5aVSh^GJQ4Tj\%@AGJAj3b+6G-K^;q2LS=HG<8T#QTdV`6[*\BsfV,:ZE?F/V$4G=4#@ij:0$e^LaWlQ."U[O>9hQ*arj?]Mj/4p2.lU7qWo66b8)&q&AJidqXg'sPA.BlrhLWuAD'G#qVF4Lu9o*_M5I$;K-NA'\n:W'FnYM#IQ9HB(3P?K=Zlb&\>/Zi=m(rZeLQ1VS*fdaEDBBc"\j$Kt>4u6(')C+B'(K#3W)S5:23]p5">ql-]CF6AEe9pRtd9TE1+!]<6LfA^j8@:TG(>.PCI)L;X<(c[X;-WKB1PYSsQYK$$_hQ"`S^A:b0`71K%)0KX7]4[gi?m]Re.;iZ4``c\9p'8N]u.0[M,#S&)eIH6W61S7;2Nm]/$+fu8ItsoEaO;cX;&liaf0,*kjC.J`;P^+$52d[=KXj8&'e*4P/,Z9[dDF-'YRZoZiF;%AH>tt,27mKF>&4?hEit4=L9Dp`?'0q7P5:(/f`++Ht18$P7L5$?#1K<#]`PUW%G=4j@3F7\3R^[)Y+AYf1]&Cl.2,0NI7!0Ap+.UN8JR]'>B(&.u%5,28EuF]`WHL\u#^B"FVQImS4`>50..'n#f$NoHN904+2lLB#mZFdbn&h$.!@@Fi&m^\9YI'335C>C2[lV*07_cchmU/kQ1(qF$^eD0AK9Tcp3rm8n>1V1Nlq;e[a&"Te>f!\(u-EWQ!^X[pj@6=O!.>ldGc+ip!2K7s91GpDtS2'8-TieBTo9t'+_J\Wd[sQVk0CkC[f.YSB`WCml9$.iD&'_lQm!j-d'l*~>endstream +endobj +63 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 3436 +>> +stream +Gb!#^HZ.Z[&c^a\@Z\$2jUtL>MNWAo@QSbY]lW2>Kd&rc*7j+Ag4\NBjO1JoU7]-*bD.L'Zm[*:eOF#)4.Mk4kAQ)35f[5!rBC_kcGn5G1r(eR49NLPC`*0Z(ZU-d#=_I&45+*+kVfrgU#oH%)t1[U>qO+g"6,4FRUs0/3B&b4-GM2T[_Y93M77'@Rh["!Rm@DFdH)A2E"-\,d$!a-i/(A\5Rb"G$%*0t\`.P\;lOkt0q**&#$NG`tNO"'@PU;i`""KH!(;(r\VsnPKpTkU;>a"2bLbiBP9T?PKg?hZ%V=ST14m$6#;,&eYIA5l2)1:[-s%MZ3JEp0?#FmKFl-C,K-j'LcK$5kHDa+[(B9-#Wk%I)1l#LFQRGs2CEF)jP)OVJE\>M9Q0`Tt*2"[mi_$p6@h;$sokR[44]ui/rj"-paZ_>tNJf`6U+!p[q9HYk^W4&BIPa+.VWktpO*@cNj?e=%mSiadeGMIZU'Y/1=,))l8rPF0UH6Z23p!MuKQ9F^d$VZh.2KRJdOiioI%&Q,S&l\InT7&0e0lT,Lnor8F_cI3p_3YBVarK__J,mb>d"!D]lTgDhTR?lO:/$^\E0\Tq.eoa).GYg5sReuHGF.Xcn#3.]OK:-!eZWcf`E,U0so)@i/qPicV`lETr_G;S!_K/r?13">n?5iog=3V[V\(]K09[c]hbK.eK]Aln'lOGi+@V8\\FW%)Ou+[rTkS"nC0id?QRNF)CWlbj%iIV$t2]mn)dMj)Z&6d.q06'ZR'uqb4Sp4(#62[Jb'p&Q_7QVo79A_s[m'Yo6Z')oqB-8Wm%OR#],.HY?9R4"H7i-cp\fMV<'JPG"k/nmp-2s837/PT.2\df>$X/XF92qWRE\WE_351$)f1LNs)R"Gm,&4Yuo+9>@m`gu8_36Ee"hGG-`lD3'!f$Xe&>'Oc,+T/.\?3!IF5?OD!*Qf>T\(V&,uhs1a#$b_#8ZN*'&e`P>sbjTO$K>Y<.KXjZ.DhR"8@-VoN^QRn[X53S*JBZ,+70"+2$A80Qg6e1T5QGNp^as?6ibM]LQ$@ZESA9mHqah2QWd.o>Ylk8,_,'oX,mO2Y4cj)@FdKnj95*;a4`YDU0prAHp?2?ZfFQAKX/tTnb'G%H.NQS?XH0>V@`k!(Ws8+*J[BQqkgAVGG1<+(Z)W+':'lig2DW+A0GAB7*P4e:?,Yhm<]C[dV^(@W?KW6R@^q7kF@10*uCMB38B.DnM_^M,W[m+;$FZ'7Ncr]cSA"U-Ai\iFB=f3Un5iX9UD1Nc-@IqkWZ1S)l6]Ll1805q#NhdX.T\iT^WjSR`h#3XaE9ZTpaF/l[ac-ugSI=IS=-kL6UO5j4O_%.c/\)"CD[dkC:?c[WRPk"IQFob!mf!\9fS\F&RMhQ6_Dd,[?V4#Xm(e,,\reN,eDbf:Q'b]!>8MnE3LAKg*-JWT`P@ZT@790kF9+j+"2mZVO1\]V)h#d?H>[@ij'n_Xskqs1`m^4Ok0O/IN.bo[JoZJdeJCmU?M'qQd(Mr=3pDdTA@e$i)Qkm,t]gM0_'T]=AU-k,t;BB1^RZUlVS>"@1I8K'daKZs3JogV_LIRjsq7EF>R0^ulu][L)%CV.i!8pMq?g1KCka<^h?^=QH^Rh/:g"[?+g9nsglN)0[pug%dXiGe.8nFM$Y+'tH%1MA5Rf(PKf`(bU1toL8FY=T(gU[`gtDa-@h&-mDdeLmk*ZZKJ=XH?=OQH(&r\Bd"\E53sO47hL7FLd-t.:nmGHonXC&5C-f%0f@MY@/%Q'V1E1uJPm.(d/7>H\/E"!H!Gd!m!P`1RL1+_4+EoL9GQHSBVI/160Uh;]bk#qkn[CNLmUEnL"\No>2b?H@piC#hZ<96Gc`4$*)X+F3m"0%VM#VFS'k*@(L7m`)i@ff?mo':Ot%,SKJ$I(HR:Z,R=<)go,?,Pl)Dotd4HIX!C9A!Vn2*Z"sTp&,`kVDq6?o-`@B?`0nXg@M1"uRO;&;+CqGm@aViC3OQ;q+[1hsQ??IT[Up^n37Q`R@-:5USki8n@fb$^7plef94=endstream +endobj +64 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 3839 +>> +stream +Gb!l">Begk%Xua-^ru.C)N3TL'G[8-Y3ak6g:108Ac(J.E^nPL@SLm1+_=1D^)5]uM)RG-?T(Yp;)8>/(^tp*Qma*;NtVEKEWN>/0Y17L;.7!97@/*uIYO?RRr<50A>VPV%!*uc0JQ:9XA*hb%u97[QuCk)"XPCn8n1u785]UIU*tpQ5/ZAd^eZEMqqr+AM(Ra.b^:Mp,R@]n?ah\k5I:7E@)Fss6,GF!p+F%3>L$JmaT4LrfC5`=g4FWd+9?r@JN3ASF/]u,8tuEUC/6Re3.5Bm!l=+'\(@k#@fi@[AOf[:a%R]2)5gFPj@7%Knn?toq9c;V:DnD9;r9V5L\Q<@D<10C--!F9S:o+ZP7#gR)YhN-ikQP)_T)Qbb_)sc%f>(KbO+/+^g^WkR<&]&*aOlB?TE;6K>maHd`BrJcn2.]!2&C$,kr,Sqs05PWY9f^5kXlhLH"G[?;_$hk'd<&d&<]?\bNjCqHD2-'kWs_KANX_J5>D04r":kD`WEOW%``tnURW7@s&,RfgEX@O,EAs.%ogf3`.55$VjJo2//4IW*3gR*R^T'2T[M>kksH\*.,X25J0dO1^PI\`PL@amf7Rq-^"Rp;&I+/i9o(bnZ=d\'/W]aangNlm^d(1[KhC<`Ro5;6L"bQXc4']+\Pb9I[LC0_dpEc8js>M<7Q5MC@kb9j.iVM'KHs$JSsitj?uDQ$W(bS(Z;S6Xq/T;gpKdh(j_/UAtj&s'k8`-30lp08j^js"W[NO2"?726Q+qP8^X!(eU7N(gt:^N)VkEkdN_J/Gb9![[aiVpAs1oTCC49CnGqqI)/V\?NP>bO+Uu(=.Ht^(QWKM?UCrLDjB+`0]h$aG@J-C+;e(PJ6G$U->(Q92.mbLO9%rg-Y10;jiQBW1Apt2X./i;^L(C0kE3SK/"qR:"mg4H*c^EJnqa&aAPlMf#mH7^TsZ-79Y%hs>QC35`?Xj*iT^/(Lp786%$7p,"^X%9a2*c=4#ER[>+C>`.@,2HUYqTF6UooG^8L6X1n?#ZG"IAeL)?'(*`*8:afUm7C6PiU*6$]6T%2gH[&d#>/V#8F/a)bXgfZ^ecEJj[)N63H@"9NhX/BKR)-I>"WO6QkoO`PcuF=:_R#uj%sYee+Hm\9qbiuaYoi9(`LR%=3^M3-4A$/K^Dp$4kYDpHD;SsVFdkm(.W`L)W6jM:#q"'qB/4)N2]he0Ic55`b%-c$@3Do)1"\l0]qG;0KaK04f3MiV%R4S3.nB:\'e:,`[S$/8l<1\M'^BP&4*KXIOJ\nOsfhm*6NI^gp)/RUWHG+d4o>rLNF5(L)Y$*UN6oX6al6d05$D188@-:U\H>@.3=On'm=%eb2GQ;Qh*Z4^a%/F+ZZq,XPD@RO/44Se*G[laIERBVBUdbT[Ic`"f%YmJ&%!LPG\oAh&+a=Y4.;ui<$Bb'V;+')R$B*Yt12mg\[@pF$p/pA0)bG2S/WRW65=7`%^7PpmHY)q)Wp5@;WiG*Q7pf;tcLH`c*pQu%c]*[E<)2p%d3+HAs"tiZ1Qc6a,&Om!6.7hqmVe!=Q:77;%RRe+*Q*-gN6!$!Ll"]7"RU31N?tO=JEP(tHAd"f$,SRZZ%,.?6_K(!Qi>g8]on%MRW9FseQ`'ZtUgM`/Ri$R+S`mksCVN*i+>2Q4gq8$l\-`*o?uji4!WC+m3Um0CnIWc6e/k+H;Zidfl<<=hhaQZ*l(Z\qdp_I5g`qJc425(erL%cUH@N42\(>#dRX$>)jKhR2`dhRkN=SJKeHFSEZ2Q1a.9^NEK`8>ck\;O(fa'fuhql_b=7$sST2DMGkYH@C_cKNT71[Y-P]oj+X\7@,LqG/#7S.^6r];hgM@@q%nQ3QbER9dtA@b!8'd#lDR=pErZT6$Xl)-1T"c:&WpB3Z3a1L$4k`Pjbe_sh+Df2q6m2X[UR1G2h6>[oTm\)=I)$8mqcGBoI^\AZr"]mPAFW)K$fc%7O&Bk!cK@ihBf,'k>30Tr8P\WE@W57o[pl%7%QU';P1ZFP)jCi*q*ouf[GUt]G@cQ14IZYBeT0a/6hrrL>!1W_t$(>!#.Of6f(AA*XgYoAGlIh>Ihjc7lk]$V(`O!K@nS4f@DptP<'EBXS0UJI:!<9gme5#7B)5g@V9]-.%e0WO!+"`XXeRYU?k3JZ"M+UG2MF@42['k0h<#Z$]6YqT]jHCCW[g3?nK3h9\8n"dfQGsH[=<#ZA.rr`.e@XAi:`OZ.&JmAr&=fP?ZM'cID:pn1]BV^S!%A#8o:;R;-AkDYGkE8d?VEG1M^ssD0@WC'8gF3*JPN&#>H5kr48jYR5M:WhkU+?^(e(YI%r6^?\M(#NS7lLeI@XmDpInG=ZlB`e<9^8^5pAHd^g[Y-Fbsm88EU].+k]k:iQkJPheFV[5?rJ-+@N.2r?uBH*=NJW7b(Z1>8<"+V#q,daeGDjZhkjI>R,jr%!F1pL\^b?5@E@2"[Di6rgfGloKPF4[W:&IEe)WBMH+DV;#*g2Stru!VEmKhZ~>endstream +endobj +65 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 4240 +>> +stream +GauGdD0+\pqKm[FAicJo]8>Ql6[HEeRuhOUP2.Z4D*K_l.(0+oN9unt&FS0-,9+C*N%L1c-LHDIr(,02S"q/Erfm%^H\Khe#s,h%OJ8Jk*%AIpUWG)+9hrEp.,1/%?]m:c5aH<(Q[oXMV5*Ci$X#*Q!b-9>'^6j]=^FpUY_AF/%)_?L8jP=Z@g)kV%)@D,h]jZh5?M&-B\'1.0gJMG!4osr\LiCf7%Vr]a!\VOt)?/S4)3*iI\_KqpFTI]5FY0E;.2F:l=)kl_7]VI)kmr.Hk6_hC71XYas[hG3lr)Y$#(-:paf"j$rgcCSeZn8pEtp!$lYB'3B%B[CS2a6XE2_q%C\bi+!9dJ=>pLlo:4t9sXG1q#B**e5n6X:f.+3r7d'=TQP!`A[KU=^P^hi!8!=Pj!^/q/Nmc1Ab=b,<-Moi:qh4p=SKXNAbLoqTB9Xt&.&NA'=`f37ifTTN4HBe;WNG'NJ>1/]OR3+d*/m0KiLM5F)9qL!J+20pIq`+sqG2SI+e+]b6TIG_1!M.')6bjmUDao,DDF2/+'RBJ1Gc(fOEP1=MMB0XO1LZa_'++joQHlq0R/g>*e-2-EE>5MWlG*p:Y]0Qo$85XGh+.lb`qXV4aD:"V$aA>6_lIc[7f-1&_np<$I>sB)a"+HD%$K6QELuL;'\OWf`65hi)SKn%F\.28F@=P,lET7Fk&pdT?R2Ou!5=I(8j4UntIGH8K.Sbu?Ue3CqK*S71lmn7(SKKK^pXFW-p'/nI'S&]\/Z%+ENk9lKDnAOMS(Yd[Y1&ghQA$B12C/#Kg\pb"ALfl>p*0o2r&l2@^R9C3HlATUL<3uA-Qk8qGLB@.TW=+d#6Lc-U"1.oR!b=WDcJ!]@oFcZ!]^>HN9c9oH_1*K"cf(RlG\B=mhbW\DKn/FKIO,H3u,/^3Q`Y(5e8/$n+pgN#dK%DrnuPmD_dqp&DV-moO_g>K)XcYAS71]=#E?4P*Y7O8?rYHTGRF3,0QN;Gi(P+#Ij>#\>F9L>eOKi8QGfQRR$s5XVn&h21p@iS'.@-,p"/*cF5m+&;U2(1?d"*`5+Xo+NVTB_lif./!ekN:Fc$bEp,Ioo@ck(0.P7-;tW7El_mYpUs)p':RW=jn%*5dM:Gpur9K86f"YVI%MD!tJSRuY)jba!b.Y&Ueh&ATJc?;#BM?^-:BSqd!ek]]#9jr4k'"J(K&Z+d;,A8#!sH_1K:76b=I9P)D")6h&'i"-Wn5-(_ATX=iEdY"b=TYcYq-DO/.^\fd>5!I7lkY"N-$YIdG"3B#(W$]_%b-[#9gBHPG7Y")[)e!7pFdPb1Gb,+rUT(9kPT$$!&5C2g_5?o;;Fs^n"1J9"-EOOG:@GDP+T5X2"@#NCqf&#EDiMRe,&GXXD9+\u6Q)X3Z8&1aeks)-#]f$G%ZN/hDD,sDe)P$H*("+ZZFY0c?=[sm",sFl@\jR"*K3jkC\:b9pJE*4DMK8ep^i/TNdR3&+j?S?NkmA>N!j%#;<`j[W)"fclFi+'XEW*mGI3+FN*_FLY(BYXPfR@C&$=&/`%Y;ipe""cA-)T[`>J_7ioD20e#cic'tAW@;*9)]9VX2-M_J<40oV[@;[SrA[+SGE_p29*LiFo9M.Ac("tgQLc[XhR/&)ioj[1(Cgo]_`aSYRd_33+\/c3W3WYYh;u>oI3k*=r[]YQA1--]3L6E.?G11H@K4<4`bISJ`Q?!;=^eh\F&j'ZM%;UuT0[+pH&o5j]WV5W5?*oR&[*/?\.;`SK_78eO8'*Cn3hXi/k-,!4dlnIE.r\\&Y"SD/Ip'BGl`b/IRFrui=ba\L%qZQbRau*1c#Q3HGD-h3.d#4T\`Er57LKJUV(YmSn^_R$cLZV@Y$5cUKO5tf*6tA2[bXrb#77-A>e8%]R^>(.dK\]#Lgp+nQ!,e+Un'XAn]&lhpCA^s*0b'h-GJ-1+.a%jf=tFcZn`)_W8TED>%@8PFK]q*R>pYZIsB7W^Eo;4AqH7PWlSQEH'"RsR&k0(Bs#K-(%dmomZnj65t5L2/99]Lq`>4Z=\*Os?qDnDI3h2TC_*htTJ)B6*^+aK8_q?N/pqG0p.C#Dp,^0r>?,EceMo3u(U*Hq`NZn=p)@m7aXeYG:FB.m%g@7!hucUbqR%ZSCj[XsuTRI\!]WXgFp,o7j0cI$FOlG#1bLbH#;AT_P0pY'IU3Ua.4W0fGdfu7b;q%3l3i;*fecU7!#0\rD"<1L8CQn7$fX/mqocOS+mf,hMHgWAam.^K^kA`c7_sbA=S5BFdn=`11Ch*LsPU)L+&F;i!L&E:/P"Zf*Nq.shekR2rokRE7nO%BE=I\1ZI"E`KT(=c;O-c3&K<"=)Z^;&ir""]iN\L~>endstream +endobj +66 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 3496 +>> +stream +Gb!;gD/\0"%fW&,6/[8\\nfK8\"!GBFdo2oDOWVo(O$)e#fbI%d48IA]7[&Gl,P/H7?&':dP1B&]lht@E8_\@Du$7IAtkFh%%%b%V#3Bd1XU80S>99M0OJA/#C56a/t(6=?FE,jmt,g?>F(eC[$Ra>%k#V!uI4-9`Egp_ARs%YdVeX_cj+o+Zh\`/iQY^S-i\gNaIC+@?n(J;D&r*m&[0G*-6?A9U2q)as-_"9QOmM:;\,ZRR(9\J3!hBs/haf/mBiH.Ce'Onqm.l/&*Q&%rJ.".M4E=)`'+r^;C3-]8uqR+abTI=Xn3:rc'\IO]DBX#WHe<0Jhq$E$)f3e#`V:t@qSSFP2`?t'4:0"]!a<%9#!O6LMp*4s][j/`kl>ua1jq1h6Pft7Z)6k6\p,[;j9t'-3]@2<#W(Xd28K_E4bEm2>kM<`WSc"%g*_mtRmH]K.jko)o-^"L+Z+8#]8hAeN$,4JT=1lj$nmT*(?Hup5OkjpD$ir__BZG]*^[3U4,KSQh_2;Uu&+-B4C/[%Y%'Y<6`cTdUF!n;Vt:UHL>)D8sO`PGTcp_]a$;Ch,]*0TtZ&[2E4;F8(uG2H&(V'*`G5[0eKqe6W&'Rh.TA>Xkc[i0I=7_%phMG@cNq*Teo#,,'Yh@d@]^?I48F\*4/Bb+n!#D9lFP$s]O+J3u6YMsFlO#"Ff\>h#C/k\j-20#un+X*US845V%0@O#K=Q&M[1?jq[DbWOS[Z"5cYU^Y(\rU1c28J'RuqG#0Kfq,l,e>N[2*n$b=,k;aoW$XQqhOkT'"r=c?)K@2--pM76#Ie3u,#jZe;1Ck4IJsg,PNf,27n)W74f0m_C99-@dFZ`J\NYTOk1.r,("S1862WIF)4K+\W%^'8/<>r*[:Oj.F3?X1erJMC^p[kqT@?6\dV,a;4jhosk\RbhQ4<2]&Wbr1,b0@^W3+^tHEgJdh&9[=&N.IeiSV@%Sd!AiX%OO0oA)(H`\#s,`;N>B'=RE4?I1E^$0"r_YH+h6&SlC<5iU5N)T#+W%dd,@0=EBGf2XqBeVI2gh7#ZeaOPLm'-_fJhoR1=U>WhV;[6Nf__s>Jl--]Z<61%8;Y$U*'T3&WV2-aFbjdpX*(A<#un&dV6%/@!XBl`=jl9\dH@41Elu6+i!5lpm/ksJjS3>3nSL7cpc(isB>je0^^#KM[D-38*5U*eqLHYF7jO%@`::$A7R7I&RYJ6@K&_2u_(qjaAq'$OW2r^8T-8:,2S/;.W#[@H+8?$LH6j_d74USaRf)H:\Kg:"jL;<79dWT2Wfm(6-E3ln2P:O?VtD/^YBRB%4b;GU1XEIU6thJPCsIA54!a`0/l0L.Ug2!"EEG.B$X0"UWL7L2DH`X#Xm8N/FJ%W/*RR#b'oZuTf*tu,FQ%"6Ku19G,o_dr^mERBNc.Z^^UArm;cdNq&4;YT.+d*c9_rI1qYnEA(JpL&b:`#%cT$Z%!,RKn]KQB`e[o6X#G[Vq7LJ+]L0[i5]]kLHQZR!k"\@D7Cp(,9=h3l[?sANCXO>r:hqcl1ruG>L(R1a0u1hb'/rMt[=.=Nf_-Erd7l3ZWaJ74-][5K4/K^TJJ/i^!U&60*1"H'kM4(@/s?5dT-OfDCHV[72_i6kkZI[4*GfZQ@$q&<`r&_kJ;mOZ#a&FN'COK[R7<5XVK@9k]rZ9N&3K^S>7>J`dXfEBbcR#;FktDdSdj._a00lk4F\g9dB8KHbL0gm&iKZ7=Nc\aC^hffZYP.2Ve%-VEf29\Mfh%?&M"j!3I?E$Smj4qjLbbJkqjRk+.'HWuUJ+M6,m"3,:FpUt&2:9"gC,mT-;s."CXr4];u1WuE'/aVokA4[MMu.4]90_"a]JcfQXfgEi-Ur:&3DX@a_#_+$)2\ERgH1sndMp]$bO=LLi8i5K+`r4o'D7R(80Eb:bc`EkBs,]_6TL?QjJ^$T5?AAmqs+#Y?f5R2s[G$rYo[T9be:E`XB^%Zt!q^%ges$O[lQ&Gq&l=km%];6<#rGB4FfVO&n10e-rmm_CgR@>C\sj-IXK?2DcoG6U&Hj6b(anE5o4O@0DNtESf%*g@oZ,\)NR$6HgKmM7/A'.#2ZIU?A?MMmp&D4$L]&BibAM!o6qq]@Jq/VCZ]R"s'>eKJ/]%]f9qV3<3iC-gZc$0o0X!Xs_9AoE+U8=6$rqr/1A``N(:.Ar^#8[X$&r"bZ>ao(d`^3P$)(0?5Ji-Zendstream +endobj +xref +0 67 +0000000000 65535 f +0000000061 00000 n +0000000160 00000 n +0000000267 00000 n +0000000379 00000 n +0000000526 00000 n +0000000673 00000 n +0000000820 00000 n +0000000967 00000 n +0000001115 00000 n +0000001263 00000 n +0000001341 00000 n +0000001489 00000 n +0000001638 00000 n +0000001787 00000 n +0000001935 00000 n +0000002082 00000 n +0000002231 00000 n +0000002380 00000 n +0000002529 00000 n +0000002678 00000 n +0000002827 00000 n +0000002976 00000 n +0000003124 00000 n +0000003272 00000 n +0000003420 00000 n +0000003569 00000 n +0000003685 00000 n +0000004050 00000 n +0000004170 00000 n +0000004276 00000 n +0000004482 00000 n +0000004566 00000 n +0000004772 00000 n +0000004978 00000 n +0000005184 00000 n +0000005390 00000 n +0000005477 00000 n +0000005813 00000 n +0000005888 00000 n +0000005986 00000 n +0000006098 00000 n +0000006215 00000 n +0000006390 00000 n +0000006493 00000 n +0000006659 00000 n +0000007097 00000 n +0000007240 00000 n +0000007375 00000 n +0000007551 00000 n +0000007669 00000 n +0000007816 00000 n +0000007956 00000 n +0000008111 00000 n +0000008252 00000 n +0000008390 00000 n +0000008506 00000 n +0000008638 00000 n +0000008760 00000 n +0000008869 00000 n +0000008965 00000 n +0000009061 00000 n +0000010448 00000 n +0000014250 00000 n +0000017778 00000 n +0000021709 00000 n +0000026041 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 37 0 R +/Root 36 0 R +/Size 67 +>> +startxref +29629 +%%EOF diff --git a/docs/20260608_ALPHA_PRIORITIES.md b/docs/20260608_ALPHA_PRIORITIES.md new file mode 100644 index 0000000..f8853c7 --- /dev/null +++ b/docs/20260608_ALPHA_PRIORITIES.md @@ -0,0 +1,333 @@ +# Android Cast — alpha priorities (2026-06-08 renumber) + +_Owner-agreed task order, dependencies, and implementation prep. Supersedes informal numbering in older roadmap tickets._ + +--- + +--- + +## Table of contents + + +- [Bottom line (agent rules)](#bottom-line-agent-rules) +- [Priority overview](#priority-overview) +- [0.x Emergency debug (not alpha blocker)](#0x-emergency-debug-not-alpha-blocker) +- [1.x Graphs console](#1x-graphs-console) +- [2.x Email → SMTP → 2FA → RBAC](#2x-email--smtp--2fa--rbac) +- [3.x Auth implementation (agent)](#3x-auth-implementation-agent) +- [4.x Reverse SSH alpha](#4x-reverse-ssh-alpha) +- [5.x Alpha backend validation](#5x-alpha-backend-validation) +- [6.x Alpha app LAN](#6x-alpha-app-lan) +- [9.x Landing (lowest)](#9x-landing-lowest) +- [Dependency graph](#dependency-graph) +- [Licenses & tests checklist](#licenses--tests-checklist) +- [Developer help (blocked on you)](#developer-help-blocked-on-you) +- [Source linkage](#source-linkage) + + +**Documentation index:** [README.md](README.md) + +--- + +## Bottom line (agent rules) + +Before and during alpha implementation: + +| Rule | Action | +|------|--------| +| **Docs** | All specs in `docs/` — classic TOC template; PDF via `scripts/build-all-docs-pdf.sh` | +| **Alpha must-have** | RSSH (4.x), auth (2.5 + 3.x), backend validation (5.x) — see [ALPHA.md](ALPHA.md) | +| **UI/UX** | Reuse crashes console `app.css`, hub styles, EN/RU i18n — no new design system | +| **Licenses** | Update `app/.../licenses/combined.html` + `THIRD_PARTY_LICENSES.md` when adding libs/tools | +| **Tests** | Unit tests for pure logic; simulate network/SSH where E2E needs no device; run `./gradlew :app:testDebugUnitTest` + PHP scripts | +| **Build** | `./gradlew assembleDebug` after native/Gradle changes | +| **Dev input** | Blocked items → brief list in `tmp/YYYYMMDD_help_request.md` + PDF; remind at end of session | +| **Git on `next`** | Alpha sprint may commit directly on `next`; **atomic commits** per feature for easy `git revert` — [GIT_FLOW.md §4.1](GIT_FLOW.md#41-fast-track-on-next-alpha-sprint) | + +--- + +## Priority overview + +| Tier | IDs | Owner split | Alpha blocker? | +|------|-----|-------------|----------------| +| **P0 debug** | 0.1 | Agent impl; enables 6.5/6.6 | No | +| **P1** | 1.x | Agent | No | +| **P2** | 2.1–2.4 dev, 2.5+3.x agent | Mixed | **Yes** (auth path) | +| **P3** | 4.x | Agent (+ deploy assist) | **Yes** (RSSH) | +| **P4** | 5.x | 50/50 OTA/URLs; rest agent | **Yes** | +| **P5** | 6.x | ~99% dev | LAN alpha | +| **P9** | 9.x landing | Agent when idle | No | + +**Effective order (agent):** 0.1 (parallel) → 4.x + 2.5/3.x + 5.3–5.5 → 1.x → 9.x when bandwidth allows. + +--- + +## 0.x Emergency debug (not alpha blocker) + +### 0.1 FR — Stream dump while casting + +**Priority:** High emergency debug. **Cyclic with 6.6** (same capability; 6.6 references 0.1 for audio lag / lifecycle analysis). + +**Goal:** While audio or video cast is active, optionally record what flows through the encode/send (or receive/decode) path for offline analysis. + +| Sub | Mode | Deliverable | +|-----|------|-------------| +| **0.1.1** | Structured | `YYYYMMDD_HHMMSS_sess_record.zip` containing `meta.json` + streams | +| **0.1.2** | Raw quick | Same archive layout; **uncompressed** elementary streams only | +| **0.1.3** | Tooling | Update session stats / graphs ingest to read `meta.json` schema | + +**Archive layout (both 0.1.1 and 0.1.2):** + +```text +20260608_120422_sess_record.zip +├── meta.json +├── 20260608_audio_stream.raw # or .aac / .opus per meta +└── 20260608_video_stream.raw # or .h264 / annex-b per meta +``` + +**`meta.json` (sketch):** + +```json +{ + "schema_version": 1, + "session_id": "20260608_120422", + "role": "sender", + "record_mode": "raw_elementary", + "started_at_epoch_ms": 0, + "ended_at_epoch_ms": 0, + "video": { + "mime": "video/avc", + "width": 1280, + "height": 720, + "frame_count": 1200, + "file": "20260608_video_stream.raw", + "format": "length_prefixed_avc", + "csd0_hex": "…", + "csd1_hex": "…" + }, + "audio": { + "mime": "audio/mp4a-latm", + "sample_rate": 44100, + "channels": 2, + "frame_count": 800, + "file": "20260608_audio_stream.raw", + "format": "length_prefixed_aac" + }, + "cast_settings_snapshot": { "transport": "udp", "protection": "NONE" } +} +``` + +**Implementation hooks (prep):** + +| Tap point | File | Notes | +|-----------|------|-------| +| Sender video frames | `ScreenCastService` / `CastSendPump` | After `onEncodedFrame`, before UDP wrap | +| Sender audio frames | `AudioEncoderSink.Callback.onEncodedFrame` | Same | +| Receiver (optional phase 2) | `ReceiverCastService` / `AudioDecoder` | Inbound before decode | + +**Dev gate:** Developer settings → **Record stream dump** (off by default); cap size/duration; write under `files/stream_dumps/`. + +**Spec detail:** [20260608_STREAM_DUMP_DEBUG.md](20260608_STREAM_DUMP_DEBUG.md) + +--- + +## 1.x Graphs console + +| ID | Task | Owner | +|----|------|-------| +| **1** | Graphs dashboard — parent | Agent | +| **1.1** | All pending **fixes** (5.1 brick drill-down, perf, permalinks, etc.) | Agent | +| **1.2** | All pending **FRs** (5.2 columns 1/2/4, full-width grid) | Agent | + +Depends on BE deploy (soft). Not alpha blocker. + +--- + +## 2.x Email → SMTP → 2FA → RBAC + +| ID | Task | Owner | Notes | +|----|------|-------|-------| +| **2.1** | DNS: MX + SPF + DKIM + DMARC for `apps.f0xx.org` | **Developer** | Web UI only; examples in `tmp/20260608_help_request.md` | +| **2.2** | Register forward addresses (`info@`, `admin@`, `root@` → Gmail) | **Developer** | Cloudflare Email Routing or ImprovMX | +| **2.3** | Inbound forward verification | **Developer** | Send test mail | +| **2.4** | Gmail filters / reply-as (optional) | **Developer** | | +| **2.5** | Outbound SMTP from BE (`noreply@`, verification mail) | **Agent** | Symfony Mailer; `config.php` | +| **3.x** | Registration + verify + TOTP + mobile UI + migration `008` | **Agent** | See [20260607-2FA-email-mobile-auth-flow.md](20260607-2FA-email-mobile-auth-flow.md) | +| **→ RBAC** | Finish admin UI after pages stable | **Agent** | Former task 7 | + +Chain: `2.1 → 2.2 → 2.3` (dev) ∥ `2.5` (agent after 2.1 DNS) → `3.x` → RBAC. + +--- + +## 3.x Auth implementation (agent) + +All items from [20260607-2FA-email-mobile-auth-flow.md §10.7](20260607-2FA-email-mobile-auth-flow.md#107-alpha-implementation-plan): + +- P1 Register + verify + ALTCHA +- P2 TOTP enroll + login challenge +- P3 Admin lockout clear + audit +- P4 WebAuthn (optional alpha) +- P5 Android Custom Tab entry (optional) + +**Tests:** PHP API smoke scripts; unit tests for token hash / TOTP verify helpers. + +--- + +## 4.x Reverse SSH alpha + +**Replace WireGuard as alpha deliverable** — WG stays for lab ([REMOTE_ACCESS_IMPL.md](REMOTE_ACCESS_IMPL.md)). + +| ID | Task | Owner | +|----|------|-------| +| **4.1** | BE: `opt_in_mode: rssh`, bastion user, `authorized_keys` / `-R` port map | Agent | +| **4.2** | BE: nginx `stream` or SSH on 443 per [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md) | Agent + deploy | +| **4.3** | Android: `RemoteAccessMode.RSSH` selectable, no `VpnService` | Agent | +| **4.4** | Android: outbound SSH client (libssh2 / dropbear JNI — TBD) + FG service | Agent | +| **4.5** | E2E: whitelist → open session → operator shell/SFTP on bastion | Agent + QA | +| **4.wg** | WireGuard fixes only if lab broken | Agent as needed | + +Depends on: BE deploy (5.x). Blocks **5.3**. + +--- + +## 5.x Alpha backend validation + +| ID | Task | Owner split | +|----|------|-------------| +| **5.1** | OTA channel + artifacts on `apps.f0xx.org` | 50% dev / 50% agent | +| **5.2** | Crash + OTA URLs on device (`settings.json`, dev settings) | 50% dev / 50% agent | +| **5.3** | RSSH BE validation | Agent (deploy assist dev) — **depends 4.x** | +| **5.4** | auth-2fa validation (G2–G6) | Agent | +| **5.5** | RBAC validation | Agent | + +--- + +## 6.x Alpha app LAN + +| ID | Task | Owner | Deps | +|----|------|-------|------| +| **6.1** | Opus/Speex E2E | Dev + agent JNI | native `.a` | +| **6.2** | LAN soak A/B/C | Dev | — | +| **6.3** | App lifecycle (notifications, recents) | Dev | | +| **6.4** | Rotation + preview crash | Dev | ↔ 6.3 cyclic | +| **6.5** | Audio lag — buffers, handshake, graphs | Dev | **0.1 helps** | +| **6.6** | Debug stream analysis | Dev | **= 0.1** | + +Agent: fixes / urgent FRs only (~1%). + +--- + +## 9.x Landing (lowest) + +Renumbered from former 6.x landing track: + +| ID | Task | +|----|------| +| **9** | Landing hub parent | +| **9.1** | Hub cards (graphs, builder, RA) | +| **9.2** | Globe / earth animations | +| **9.3** | Mobile chrome validation (LOW) | + +--- + +## Dependency graph + +```mermaid +flowchart TB + subgraph P0["P0 debug"] + S01["0.1 stream dump"] + end + + subgraph P2["P2 auth — dev"] + D21["2.1 DNS"] + D22["2.2 forward"] + D23["2.3 verify inbound"] + end + + subgraph P2a["P2 auth — agent"] + A25["2.5 SMTP"] + A3["3.x 2FA"] + RBAC["RBAC"] + end + + subgraph P3["P3 RSSH"] + R4["4.x reverse SSH"] + end + + subgraph P4["P4 backend alpha"] + B51["5.1 OTA"] + B52["5.2 URLs"] + B53["5.3 RSSH validate"] + B54["5.4 auth validate"] + end + + subgraph P5["P5 app LAN — dev"] + A61["6.1 Opus"] + A62["6.2 soak"] + A63["6.3 lifecycle"] + A64["6.4 rotation"] + A65["6.5 audio lag"] + end + + subgraph P9["P9 landing"] + L9["9.x"] + end + + ALPHA["FULL_ALPHA"] + + S01 -.-> A65 + S01 -.-> A64 + + D21 --> D22 --> D23 + D21 --> A25 --> A3 --> RBAC --> B54 + A3 --> ALPHA + + R4 --> B53 --> ALPHA + B51 --> ALPHA + B52 --> ALPHA + + A62 --> ALPHA + A63 -.-> A64 + A64 -.-> A63 + + L9 -.-> RBAC +``` + +--- + +## Licenses & tests checklist + +| When adding… | Update | +|--------------|--------| +| libssh2 / dropbear / Symfony Mailer | `combined.html`, `THIRD_PARTY_LICENSES.md` | +| Stream dump (no new deps if pure Java) | — | +| RSSH BE scripts | BE licenses doc | + +| Area | Test command | +|------|----------------| +| Android unit | `./gradlew :app:testDebugUnitTest` | +| RSSH API | `BASE=… ./scripts/ra_e2e_cli.sh` (extend for RSSH) | +| Auth API | `scripts/test_rbac_api.sh` + new `test_auth_api.sh` (TBD) | +| Stream dump | Unit test `StreamDumpWriter` framing + `meta.json` schema | + +--- + +## Developer help (blocked on you) + +**Email/DNS (2.1–2.4):** hands-on checklist with record examples: + +→ **[tmp/20260608_help_request.md](../tmp/20260608_help_request.md)** (PDF: `tmp/20260608_help_request.pdf`) + +Please complete 2.1–2.3 before agent enables production verification mail (2.5). + +--- + +## Source linkage + +| Doc | Topic | +|-----|--------| +| [20260608_STREAM_DUMP_DEBUG.md](20260608_STREAM_DUMP_DEBUG.md) | 0.1 implementation spec | +| [20260607-2FA-email-mobile-auth-flow.md](20260607-2FA-email-mobile-auth-flow.md) | 2.x / 3.x | +| [OPEN_TASKS_GRAPH.md](OPEN_TASKS_GRAPH.md) | Ticket-level graph | +| [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md) | 4.x | +| [ALPHA.md](ALPHA.md) | Sign-off F/G | diff --git a/docs/20260608_ALPHA_PRIORITIES.pdf b/docs/20260608_ALPHA_PRIORITIES.pdf new file mode 100644 index 0000000..aa71284 --- /dev/null +++ b/docs/20260608_ALPHA_PRIORITIES.pdf @@ -0,0 +1,362 @@ +%PDF-1.4 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 9 0 R /F4 20 0 R /F5 22 0 R /F6 23 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 45.68504 716.2874 132.829 727.2874 ] /Subtype /Link /Type /Annot +>> +endobj +5 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 45.68504 705.2874 104.357 716.2874 ] /Subtype /Link /Type /Annot +>> +endobj +6 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 45.68504 694.2874 191.973 705.2874 ] /Subtype /Link /Type /Annot +>> +endobj +7 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 59.68504 683.8499 183.8925 693.8499 ] /Subtype /Link /Type /Annot +>> +endobj +8 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 45.68504 673.2874 114.605 684.2874 ] /Subtype /Link /Type /Annot +>> +endobj +9 0 obj +<< +/BaseFont /Symbol /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +10 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 45.68504 662.2874 174.733 673.2874 ] /Subtype /Link /Type /Annot +>> +endobj +11 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 45.68504 651.2874 159.069 662.2874 ] /Subtype /Link /Type /Annot +>> +endobj +12 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 45.68504 640.2874 128.829 651.2874 ] /Subtype /Link /Type /Annot +>> +endobj +13 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 26 0 R /Fit ] /Rect [ 45.68504 629.2874 147.517 640.2874 ] /Subtype /Link /Type /Annot +>> +endobj +14 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 26 0 R /Fit ] /Rect [ 45.68504 618.2874 112.389 629.2874 ] /Subtype /Link /Type /Annot +>> +endobj +15 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 26 0 R /Fit ] /Rect [ 45.68504 607.2874 117.269 618.2874 ] /Subtype /Link /Type /Annot +>> +endobj +16 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 26 0 R /Fit ] /Rect [ 45.68504 596.2874 113.277 607.2874 ] /Subtype /Link /Type /Annot +>> +endobj +17 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 27 0 R /Fit ] /Rect [ 45.68504 584.6874 136.829 594.2874 ] /Subtype /Link /Type /Annot +>> +endobj +18 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 27 0 R /Fit ] /Rect [ 45.68504 574.2874 160.845 585.2874 ] /Subtype /Link /Type /Annot +>> +endobj +19 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 27 0 R /Fit ] /Rect [ 45.68504 563.2874 98.59704 574.2874 ] /Subtype /Link /Type /Annot +>> +endobj +20 0 obj +<< +/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font +>> +endobj +21 0 obj +<< +/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R + 15 0 R 16 0 R 17 0 R 18 0 R 19 0 R ] /Contents 47 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 + /Trans << + +>> /Type /Page +>> +endobj +22 0 obj +<< +/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font +>> +endobj +23 0 obj +<< +/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font +>> +endobj +24 0 obj +<< +/Contents 48 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +25 0 obj +<< +/Contents 49 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +26 0 obj +<< +/Contents 50 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +27 0 obj +<< +/Contents 51 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +28 0 obj +<< +/Outlines 30 0 R /PageMode /UseNone /Pages 46 0 R /Type /Catalog +>> +endobj +29 0 obj +<< +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Android Cast \204 alpha priorities \(2026-06-08 renumber\)) /Trapped /False +>> +endobj +30 0 obj +<< +/Count 16 /First 31 0 R /Last 45 0 R /Type /Outlines +>> +endobj +31 0 obj +<< +/Dest [ 24 0 R /Fit ] /Next 32 0 R /Parent 30 0 R /Title (Bottom line \(agent rules\)) +>> +endobj +32 0 obj +<< +/Dest [ 24 0 R /Fit ] /Next 33 0 R /Parent 30 0 R /Prev 31 0 R /Title (Priority overview) +>> +endobj +33 0 obj +<< +/Count 1 /Dest [ 24 0 R /Fit ] /First 34 0 R /Last 34 0 R /Next 35 0 R /Parent 30 0 R + /Prev 32 0 R /Title (0.x Emergency debug \(not alpha blocker\)) +>> +endobj +34 0 obj +<< +/Dest [ 24 0 R /Fit ] /Parent 33 0 R /Title (0.1 FR \204 Stream dump while casting) +>> +endobj +35 0 obj +<< +/Dest [ 25 0 R /Fit ] /Next 36 0 R /Parent 30 0 R /Prev 33 0 R /Title (1.x Graphs console) +>> +endobj +36 0 obj +<< +/Dest [ 25 0 R /Fit ] /Next 37 0 R /Parent 30 0 R /Prev 35 0 R /Title (\376\377\0002\000.\000x\000 \000E\000m\000a\000i\000l\000 !\222\000 \000S\000M\000T\000P\000 !\222\000 \0002\000F\000A\000 !\222\000 \000R\000B\000A\000C) +>> +endobj +37 0 obj +<< +/Dest [ 25 0 R /Fit ] /Next 38 0 R /Parent 30 0 R /Prev 36 0 R /Title (3.x Auth implementation \(agent\)) +>> +endobj +38 0 obj +<< +/Dest [ 25 0 R /Fit ] /Next 39 0 R /Parent 30 0 R /Prev 37 0 R /Title (4.x Reverse SSH alpha) +>> +endobj +39 0 obj +<< +/Dest [ 26 0 R /Fit ] /Next 40 0 R /Parent 30 0 R /Prev 38 0 R /Title (5.x Alpha backend validation) +>> +endobj +40 0 obj +<< +/Dest [ 26 0 R /Fit ] /Next 41 0 R /Parent 30 0 R /Prev 39 0 R /Title (6.x Alpha app LAN) +>> +endobj +41 0 obj +<< +/Dest [ 26 0 R /Fit ] /Next 42 0 R /Parent 30 0 R /Prev 40 0 R /Title (9.x Landing \(lowest\)) +>> +endobj +42 0 obj +<< +/Dest [ 26 0 R /Fit ] /Next 43 0 R /Parent 30 0 R /Prev 41 0 R /Title (Dependency graph) +>> +endobj +43 0 obj +<< +/Dest [ 27 0 R /Fit ] /Next 44 0 R /Parent 30 0 R /Prev 42 0 R /Title (Licenses & tests checklist) +>> +endobj +44 0 obj +<< +/Dest [ 27 0 R /Fit ] /Next 45 0 R /Parent 30 0 R /Prev 43 0 R /Title (Developer help \(blocked on you\)) +>> +endobj +45 0 obj +<< +/Dest [ 27 0 R /Fit ] /Parent 30 0 R /Prev 44 0 R /Title (Source linkage) +>> +endobj +46 0 obj +<< +/Count 5 /Kids [ 21 0 R 24 0 R 25 0 R 26 0 R 27 0 R ] /Type /Pages +>> +endobj +47 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 949 +>> +stream +Gatm:gMWKG&:Ml+bZ!V&$JA]e/q%dnDB_*CC\)3g>/S?V"%sGdS%nCl[M-8jMBddc,Oa`aR5o>^5k(JlGE;l2q/A?g5T0>m5V9kTIgigcfrm3CfkGHDFuchCE?'UuSqZ?HfcVn'\I"#Tfu.mHE&dG?$@6uPGL@Lj"+H+=$n/;&(om\IA!i3/)qY$"#/`"A.LtE@'ICs@F$M!r,Tt^DdW$/J!p5,]rrp6"$X..B\r_5eILc?Qn3=h0rf(U,%t'BT@8hfn)SlaQC^Ps4LIRX8Dp=r_4m4:ls"T8E"#?0gTC**JM9&j^Nf`*qIYT-d_d'+uG^P-8G8n+`Js'CffP;l>^/uWne.Vs7.qM`s*=/B9:<,<&/p&$KdK]YBQ).(rM)0uC(/#nQ\k6N')V]I\&@4#2PAT[9Zdd=+>lPo%p/k\c`:seNWRPU1n,f:g<(ljQVTOn((5WB!DtX)>/,+-de,o>5iW3n0;Bn:WE24#8MU\gOL\uZ9/#B9ES8,4*t]m,>(Dg!fOa+;GA**k`oM5FdfMI`q`ihGd)_s+;2ggD`[.`W\YP"C[rBR<;VR%Z15l4aiB\(WuDPn%?\mT3L13^@b6UFuN..)bY8DDllMT!Jph\X)->Ao8hOE?BtrakcH+1;3]O^%^u6B1endstream +endobj +48 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 3569 +>> +stream +Gatm>>Ar7U(4P_U+M&E*R3o:/h4'0gg8%FJ8aArQlkV9_ck:22W`sU;lV;dZ%F'<'";lqP,lZd59ti^p/A@dHQn\iB2#30KfO=;1E`g6]LBY-0P&j"n#J#ci\G>%]SL;#c4^&SYZWV6hA(>r-*V05N/O1S/O:2e_oV,)*kE?892`N#tp/Q[LZ3ZEkmaI]O[FgM\%M9km&E(f7GK'CshIU%:(`)U_i0d]c./V>tYQD-[S?/03hc`;4=&aO^#hsK-eb$!K=3^k:23X5@*kg[=fYnCfD=6V544*ikB)@erC6#>H1?Fh3(3T/]+E66H8R1eCpkc+gn].0DN(.D&!SlOW4$V8Yn7N"S3qUGSCA1=pS"W97H*eeaIT`lY[^>]:fW)6Fk:q0nX*(L-_m7/qnjODBUDpim/i:;oF6B93A(F:1l,3k^*?,2TFM`,l8T:XjHV4?"f<)Q2]f9,L#tGI-$SloQ6N\"p:Ls8$I+oq22<2W`>@o"_Pa+ZlRC()!)W#"e$`@uI$XK^C$FZC,d@buQ5aG95)"2kQuY?]ck.5q\'<6ihWep,mo5l#$Qelk2"R%b^'@h=f.%=cfigNhfgQ`Ubqg4ZCJmMD:]Kqk^36kcZ*p3d@.jhQ^k0H0I?\gJ8@3bLtF`r7gf1e,,W\O#N,RbuA:=DcqUhj7(je\Z980L8!4J:^*ip=R"(5Oq&%o@Z2+/i2.&SJ3AKHNhQr&K,8E1q[t3XTCLP8(6Uk:nb4L,4?+mN\03g#Q8o6g:rR[N5gkuqGJ"2nGe/a2j0<7h(:/oo!#56I&FeuiJ+S1031#I\P:K,2!J%14]%nZ=\Ttl`h#Q4Pd'fLoBC4K-@IQr2_&;-Rt1=JIP:AiL,O+JWW#u^/u<#JdUd_^g,WDsQnQ6`CtX!Dp@8Pk">8aoiDN7sk__b*a24WV&E'N15cIj/^f*uN`6'Ze+0']:%eXHRbg6>4&`%+j:mtg$%3Vu4J;6dc1XbO;KskT&eYS+XI^OTCJu8>rMgaMgukoBkVbm<]Nlk\W!@=ED,SAXUoWi:rbB['P^FqX%B7c?`8Ueei$+(%"M#!=9Tdl3p6IM2h5NQm?G`o1lYi2hYkPWi,9e,`bo=p&15H:iAYog6$Yt`j7J/)gQ$_.UkR3-mS)kH`ZVEAfr.7hHp$.GQn0S&GVe(^G8r%s+l"B,64I[.bqiZ8]Qqr)!AYSGcd[CQ%,tG(Pr;#4-a8l5RY8E!YGPYKmLkNu!/G[.=K-oBUA\G]^N!ekI1k*Q98?^ec8&Y]2K)(\46YX)]B.'`s*7*Ir_*?If$s9InB&J!MH8MJsFi:!4j0&!h_'&X;V_[.R;Xf!F`;[kV^#R+7QF6_n-gI+QOH<^ltr/AbWGtnPn)<"`mjUI[l?JANp;sg[J!IgKHN:M2ge\*7Jos!$EosVTV'"/MV1q*EPE9<[fY`AFrdN!i(2huOIKdG.+hLpp-+:AalS&8D%6mGFlTeFjDXL-ZipLA_K:8X]179:DM8h]B\S6O2[QhCL;dNLpE?:B&,@6`@i@/g,:E(-OI=92Shs$N,3!#ARYmBm4nP4TdIMQkKt$J^Mca7W\?LQJS&tPumC>-$G!WM>g8j^F9G6X^P=0NS!`7c[nki4[XB>s:!@Dh!nF,*B$W:lZFo_IP@;^#.MK0!W8ISr^%O^H7_5)^k-gAffSmbaU".2k@>'75MG^7r,%;dicZ?Qi'$WRCaXAQQ+J[S[\p0_]Ydn^onoBmT"Y#;Z'V%^C@0uZR$eTGMs6IFG#^6]VA%rLjTcoqV=Aa7R&Z[rn]HLC:ctR<7GpVLu?'qITLTHS1/;$LV7fXJAjts#em6>lRGD0F%(jNZ"HF(YggX5[BUcXBP9tKpMoX_@!6+3[td-pGa5W1Gn+<>?/)K#;j0,oqhdh#t8/\PkKq'Pg$q+VuN3'q>m/aL>&7ecjfKoKC@#l4QEW&8+]WdU4OZDQ]?(!*(n-s':l4]dXD#/u5BF!\jGNgd,X:*7`,32,Ah8?kl!;+MtDY>#7VD![DfQ5D2SH/q0;/9RPBYVTbG/NbI141VCo[0;K9+CIDe<\KaP3bm*)Cgb0/3XeS80\I!#X'f;1_INTnPp6,f9C;*h.oO=rnh/cq1ZEl7Ce+LX5k[C'Fn^r'Ca`0?9Q>6`_1"e%`;8a@5an'V]P+;Jso(B`Gt2Q_^]]T6=8I#>sr9tFUsia24lO(H5+mDP-Q>P=6k?P#eETZ]3=L&@R3fRtoeMbT;2I\bC"O/DpH3WLZTO"Fk_QS;TCjbb%2EMHV8!t$U]Ac~>endstream +endobj +49 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 3765 +>> +stream +Gb!#_>BAQ-&q9SYf^[1RE>k;n6&LniQ_W2RlCL%V-)KjYAi(2.=fb2H+Xg]DVZ6TG8F[m]=/G;SAh\mXA8S'Thjo')ufo!$C?BH)#mV@(lh6P;J[,%$UM/[hE?);]A:(E$/q(BW<>Y,7V\ar9HPBCDM5.'N+ba%*]GGmN+Q^TL:))U5brLbWXH1!+#N4L7ut5#5IN6g6fTM?%2V[aS?dBrsnWBG"q;Lo]X_4>lgf"/DJb.\Gna0*P="m:ZofJ&XVg%qrup)I"'gEK6LCU`>,R(gIhi9\=sf;rS48Y$5O]unc%'`"&G'6gNC)+W!=ND:><=f-LXRAV&[`g2WW6t_JdfNCZVSe85_;"PQ#SeTr%=Ci8SG/([=:aRnYTaV\JgKW,L,rSfF4TLqiDap'S<-22BUiTu+"O.ai%()CqW5^V]G_7`-Uo9!HM$EX%l7Ha=Z;QLsk;qlL[A"5Q!_V*(OC3l6$+PVRaJ>KiuhpIaDfofYEmB#D!+BLcr.lnLS?"Fg@[C(quenc_0dTcdKL3-5!fl,n,(Q"`BRg#=g6R%r"6rXk'*P`%nTb*u:e3_p%=if"&XNLDI&)j=2CM`:#WP.p(KlK_s,1%="5_8L@duPjnQCcQ5iT36BT3Rc&%UTq\=EVMnqq_BY^OQ!M`5[!M@dE$lam)HqGoQ@O4_6-/YG*5t"LA3=^k6?$&TA[PEke#&TL5FSk"*IasVO[4He;i#kDci*P^-=Yo8r)c_$?KdP%1*OI48#NU[J8RZf%i3/\mef@(m;3\]PP=;6S;>$?p/E@0(>OsX-7Yb&5H]td-s-6!F1o/UUE'e/;bW(''W$g0>,F>F:k$jb(=2DFS.[/TpA%ul0,LFp/]DgoKcmKb0:mOOe6BZRpg&G+/C"H_!is\Q*l/=,f=eEAZ[[uj_KC>,`@&uROh("'TTLNX&=f73GJU'kb..$!.$N\Yf[1i!9Ih8U*R*>c4pU24>\`H+hBLIqNZDslO<6g0mK.2P(ch]PSX14@KZ]CgnZo6'2(pJ9_.ZYY^o*-qBmYM-b8Sa8(E2eBM3heBWd3:#\X&Qk<)":QMI%6Z#=Oc_?&.!A9URnH;di<=Qk(8c81"D)]qWnn[RNk7!M7TQFR`lB/!jD"2!i"l#"4RX\mA/X[G`P"d$$O&j:2h6#u=;/P:jt=,"EpU/MOf+htJ3]DT;V[9g=T?=uhS"GiU9[E#M/\-T)5cWn9fLX&7sO<7\9/Q.7(g0+I5.iIX^1XMtWZstA2?U@P&nRE:@8J@T8Ya;C'B$HJ3qU=dOh#.Fq@(4#e+H6,9>)r:2&:n=dbfoj;`_B^W*W7kFQg?,;k3N!\q:[+Pna.c'IMT_FVIoYKK16&PR8/4dHggEGZ@2s%Bb=_[\l^LmL0aI/?R_=ek`[9(FO<:6!kS\k6tOs#JQn52Tm"pUE^>CXkD=PL&YIWfYl="`7]CDW%-]?!0I>EK-lQK]&a7Nm,iqlR]!N%PFTOt!BtVUS1RAR7,LUaI'ACWD;G9's7>Ii1CmVuL3oOrhuB7j,7;!WN)/!$Z.En95MWIl`X-B^N>)0lskCfH7W,(FCeGb.E$IrR]M.+7s!0)]t:!5Yg2H@-'OKZRbsdN=lVZ)=on"*ZbdU]qE\\L_!crdI"\7=WY$?AH/KMgrXGXJ0d-Ddu:eVJOd9Z.dZ[L=mSScG]Og@0%MY%DItJK?$(K#hckR$Yqb#1hELkWnK#>pR#$QX2<0VQ^jJCBa!5ZUOFPS8o5E@+rek3YLU3'2>Qi&jAX12r\p1c_Fj)At*NKiH77#p(?>Lg2DFpA1MbS5@lSmV2oc'o66Z"$K2M9QWAl6qg.uQ2cksN016:QetM"^0YDc\eJg"8:'Ps2#2[5fj>;^F1;OkQ%^MM>g23jd_,.HPo^X,ZB=n&eB6O,YH??$4UU-oI\ZEniAAn-[U@:Q%021qf-Ao3:WDo5tHO-(EocgO8l.?_1qeUIeE't)(>jtI`tO&)+.S3C#o5ob:kV\Q-'us'+9cS]AuCaekq*UeJ9uC*l*7b6(reedYC7I8R"8"9n7eKq8b`Vb6\^-)"d_A[&@49l/*\MJ"'7[CJb%VNW%a6AVoXDKfb?!(RK='u\l9d3ngS7pGN5@4=1T%q.,:d^nYPe_]7!sr1Md&rl9@&hqnM0fHUQ;Kh`3LpTX=]-pG?83)(2tUCS:Waj=U.=$)le#qJ+iG&U"U=dY53Wn&IF_a2kIVb>Ib,u*n@)0-(jfoO8C[SKIL'M5:TfFI'YO)Cg`iN!X?O4uY_`HN+^2K_I-=2kc/nO6]-J,LeJj(e^uMmfiLRniB8cR6p+e^\1g>Me5ms,9]gnYRAZoI`NN`_.H1mQsHcH9^hma@BJe%#:rd$bCe?gjbu;6rA8/oIcsb7u6P54*$#4mjfFsohO^a?)f/>Epo4eLWKQ57JF?7r2%GMq@5j;2[@HhQ-!NbZK3pe8g=[GAdf,"FT.s,nr^X-;umugAmS0S?,Zac!3`BA2r2&?8G9'c0Y15'QhI:DD+0V"lb,5@;N-qt?qHnr))Uu5_GfL/BUUEn<`1,C\.`"?-biERZcNi,/q]5hN12G,nu_)CiG2r#+[s$W5)4q=77f-(S+="IacpQ()q*m1WiA7*/IB$Kpbo#!@hB_c(?PH(57hOd5jiPY$oFk7[Gl;J3V][,jk%:$eh&i4[<9=HD_BGcR^1u8'!34CYRG*/AU2[R/_:U.Z8cor'm^r'QW[&l]*Bk&GCTI2Rfr`dLMK"F^4dLO!*pcHJ#LE9Re#R:HE80D:'g]Tk7&Kp>WaM/]tD8E*Z:DDiJNR=M01Ia2oo`2eh15*D=q/DrP);P!ILX"aF4>NW9$-Gp801_endstream +endobj +50 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2534 +>> +stream +Gau0F>BAQ-&q9SYf^VXIjMn#f!5)n*4NMO%[c8^;XYYB]c3$I#(1W5"8]d)^r8eO/-;"==Nk/,\Ub9TPCH?/6IYH5P?KlSSqA,4L6#nDq+;]'@];bHftKa'ikVbM8-CJm3AE;lW)ED01Df*k#8B^HZi5JTob/ELiU\=EhNs:aR.:)n#*Z5Eq*j$6$X\7-QAlo-9n/7-X)@Y"0G2nuT$1UT^5*KSEV'Us![9g\=gEJjEN<#QqM)s?=5W9;6,[!c.E"Qs`E_B\&,cJ#`:/T+C^Ua1n#fFp)kOBm0O^5B7g/S]75ZObql3MMc0"b4oegK_oIZQpJX)9rNCll=-P(bB=,fQEf0R':D)e,(10dl0E*ZG]Yc/@BC61d@T7_+B^bMLJ.Go'L^j7c?Aaj,M,rQs=bF*__QQKTf2UV@9VBai?R@Y:8)6JP3'XF+L-'g]R6<\b8fGb^1o3Ua[Be\6q%-j,KK2Ga:*!@G\&4kBTNrf+bj(Pd[0]N2:_)XXJ.%KmG\8GZ7jfW`n]6A%X*hbIqKDLVmH=46@nQE8Z4]f%N)`uke.MX0aa5HI#Gf[.)&)fJ_G,]tE?.A9-Y<"I>.Tp@-*LY#lFApZW=8dtIhs,lmO8ZUZhsT[j=8=(#*RB5GZ]'2LjDF9'l/@^a\O]DMK_A5*u(bkL&4e(kZt\sSM8b/_sf9Xcc^dkd@(ipLSh5%4&W?o$c=,D]H(["HHrK/gqg,D^`V?2dG3]GmAG8+/^qPFJ?Kt%jK48?p$GF`N6dsL$QUWQ>:_)R7CBH`2nV)__I]lHCJ?op2hVfO6Ntq$mSnKO%-sWG"Hee:iIp"Lc=MOM(4Bn\d3dP5b,E9Ae?oBPEsLpdgn?fA7V)e\28nLEP2@(j*T;VEZ^S\@nE!DLS9chRe'CL58(T$uNFW/4>Ym_ic6#>Ok:h?fDS&h^f:rQ=@i<%8MDmoODJO(+_oH9qhMRHQJk`i:-fch.g8Y%kp+*Y_'Bi:R/0;"qH9WKNp=<%8Ss)Z/i=kp-fWrs-5&%g7C$79@\'Sg!g;q?7:3J0%j'X$dTu.,[;PW`?,uAEO#B"MmAb)&)Ks[*-9mo`g=iBtK9bkES$l<#.ihCUaqOI,mem&=$o6OI(ra'kmUi]^acAXTp,e)p^;6kV+Y+cL("?$rU(doV"Fg$A]5iG;2(l-+nZ5:M88YZ!l!4M2NEjUZKeVp\@b1*Ge(2$RW,aWb@dZ=8286SV3Q'NjnU%Ar$IYc9Id>q.U!Ou#Dt],&i0YKEVThldl*3UOS7DkZ8i`/W\j,U\.P[oU:ou_);endstream +endobj +51 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1967 +>> +stream +Gb!;dD3*C?&H9tY(e&2YV.Hp`DfAT)R/hG&8P1\gW!\f$n8^MY>ATM8qtD*M)iV&<^*K_)gmqTom**cCbVP=)",@s^q7gqM#'9!gH/L-qf>*;4K_Ya?o@B6DT5'5rOa@]gJkQ?b$%3EK[=4PKD!_Cd&Ojp6)M-;q'(t1sKbjKt'41)?$0D^YO[+rAY+ZZDBES6egZQIEpmB5Es;/20RpX^MLQtOcmOkX$$4@-]YC@>VRPj2(ZW-L6r1N=k3R4INe\,/r!o_X*?!'-`04=kMF*O:.8@"_dH"!(;JC=iP07[7^.Q*J,k4DiPp_JPEAeKZ3@.:S_Ed*2-1m.)NX3\3h7RrB^H^/rFm^rZ8m2qi2Y"&XX'I*0\:?_dV:b;0Wik%/X[pG[U2Vk>;X:/@l0-hFqUb6sa+ME!K?:J1E"L:L]ZEQKg]-WP%0'e#2)q)L(qI+*mR_/b!N$sqI=d9[9;I,:Q6/]2NpTaPJjq+G<&pf2Z]et2aAD[jm%_ahB?%g1;7(rZ1jVWDV[iCM`f_nj0QGfX#WaZiQ:AEspkL`0';9L]L,4O*;k?@iBW6eK[@G*DElWVhu]32n2o(GWk$)qV`em06/IiVIA+^8jQn'GD]ni9feghRO-;Q$^J6phijg??#U.g,%5\CV/uSPc8fCl#.h+!l[_.L.A#Fbl6+."$"NKnDVAMO1IFKn.k9dUaf\r'(;p3\PV],?U_7BX2PDFTo'_&%;?cIIU/(*hYG*MV-rJJG"L-5Yo-0HB.o\4q[6%Z:bd.J2ETN"B.!;hcsugDrV*Y.pbXgoMf)E_+-,QV+pJI1\jE."./f/;.&EE0VnZ5+bmGg`8>OlLX77klI&SfVgZ*j@;%/C09\kJCN3Vo/&t#sqWqnBH*l7%L(dqHWIP.UXcRD,Uj]C,7)FEU1Z)Nu75E:Om$S8r/%[ilX,k_;'r#`[eOHl\RF'/90FQCUUIq@@;S1IWGhIgh:nnm*"D=Rc98j",O%].3PpC`f;XB;\LY;=B@VA)*(HM(V0(p'mBF+#4-$eWCt=G'nG*;G9u@#:.OdF\Z5V%kr1D@HYCRgJ1OulEelFUR=$gOjND"`P*-,G,U1#HuKIbC;ncZ\Tn*E)F$7#DlW+D6Xqendstream +endobj +xref +0 52 +0000000000 65535 f +0000000061 00000 n +0000000145 00000 n +0000000252 00000 n +0000000364 00000 n +0000000511 00000 n +0000000658 00000 n +0000000805 00000 n +0000000953 00000 n +0000001100 00000 n +0000001177 00000 n +0000001325 00000 n +0000001473 00000 n +0000001621 00000 n +0000001769 00000 n +0000001917 00000 n +0000002065 00000 n +0000002213 00000 n +0000002361 00000 n +0000002509 00000 n +0000002658 00000 n +0000002774 00000 n +0000003095 00000 n +0000003201 00000 n +0000003285 00000 n +0000003491 00000 n +0000003697 00000 n +0000003903 00000 n +0000004109 00000 n +0000004196 00000 n +0000004529 00000 n +0000004604 00000 n +0000004713 00000 n +0000004825 00000 n +0000005000 00000 n +0000005106 00000 n +0000005219 00000 n +0000005467 00000 n +0000005595 00000 n +0000005711 00000 n +0000005834 00000 n +0000005946 00000 n +0000006063 00000 n +0000006174 00000 n +0000006295 00000 n +0000006423 00000 n +0000006519 00000 n +0000006608 00000 n +0000007648 00000 n +0000011309 00000 n +0000015166 00000 n +0000017792 00000 n +trailer +<< +/ID +[<932c07bb3580b032147403333a208980><932c07bb3580b032147403333a208980>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 29 0 R +/Root 28 0 R +/Size 52 +>> +startxref +19851 +%%EOF diff --git a/docs/20260608_STREAM_DUMP_DEBUG.md b/docs/20260608_STREAM_DUMP_DEBUG.md new file mode 100644 index 0000000..91d89b1 --- /dev/null +++ b/docs/20260608_STREAM_DUMP_DEBUG.md @@ -0,0 +1,122 @@ +# Stream dump — developer debug recording (FR 0.1) + +_Emergency debug — not an alpha release blocker. Enables 6.5 (audio lag) and 6.6 analysis._ + +**Parent:** [20260608_ALPHA_PRIORITIES.md](20260608_ALPHA_PRIORITIES.md) §0.1 + +--- + +## Table of contents + + +- [Goals](#goals) +- [Modes 0.1.1 vs 0.1.2](#modes-011-vs-012) +- [Archive format](#archive-format) +- [meta.json schema](#metajson-schema) +- [Wire frame format (raw files)](#wire-frame-format-raw-files) +- [Android implementation plan](#android-implementation-plan) +- [Visualization / 0.1.3](#visualization--013) +- [Tests](#tests) + + +--- + +## Goals + +- Record **elementary** encoded streams during an active cast session (sender first; receiver optional later). +- Produce analyst-friendly bundles compatible with session stats tooling (0.1.3). +- Dev-only; default **off**; bounded disk (e.g. 100 MB or 5 min). + +--- + +## Modes 0.1.1 vs 0.1.2 + +| Mode | ID | Content | Use | +|------|-----|---------|-----| +| **Structured** | 0.1.1 | Raw annex-B / ADTS-style with rich `meta.json` | Preferred for graphs replay | +| **Raw quick** | 0.1.2 | Same files, minimal meta, no zip compression of streams | Fastest path to ship | + +Both use the **same archive naming**; only `record_mode` and optional fields differ. + +--- + +## Archive format + +```text +{filesDir}/stream_dumps/20260608_120422_sess_record.zip + meta.json + 20260608_audio_stream.raw + 20260608_video_stream.raw +``` + +Pull via `adb pull` or share intent from dev settings. + +--- + +## meta.json schema + +| Field | Type | Description | +|-------|------|-------------| +| `schema_version` | int | `1` | +| `session_id` | string | `yyyyMMdd_HHmmss` | +| `role` | string | `sender` \| `receiver` | +| `record_mode` | string | `raw_elementary` \| `compressed_wire` (future) | +| `started_at_epoch_ms` | long | | +| `ended_at_epoch_ms` | long | | +| `video` | object? | absent if audio-only | +| `audio` | object? | absent if video-only | +| `cast_settings_snapshot` | object | transport, protection, codecs | + +Per-stream object: + +| Field | Description | +|-------|-------------| +| `mime` | e.g. `video/avc`, `audio/mp4a-latm` | +| `file` | basename inside zip | +| `format` | `length_prefixed` — see below | +| `frame_count` | int | +| `sample_rate` / `channels` | audio | +| `width` / `height` | video | +| `csd0_hex` / `csd1_hex` | codec config (video) | + +--- + +## Wire frame format (raw files) + +Each frame in `.raw`: + +```text +[4 bytes BE length][payload bytes] +``` + +Payload = same bytes as `CastProtocol` audio/video frame body (after codec). PTS optional in meta sidecar or future v2 header. + +--- + +## Android implementation plan + +| Step | Component | +|------|-----------| +| 1 | `AppPreferences.KEY_DEV_STREAM_DUMP` | +| 2 | `StreamDumpWriter` — thread-safe append, rotate on session end | +| 3 | Hook `CastSendPump` or encoder callbacks when flag on | +| 4 | On `ScreenCastService` stop → finalize zip + `meta.json` | +| 5 | Dev settings UI: toggle + “Open dumps folder” | + +**No new native deps** if Java-only zip (`java.util.zip`). + +--- + +## Visualization / 0.1.3 + +| Tool | Update | +|------|--------| +| `SessionStatsRecorder` / graphs ingest | Accept `meta.json` + frame counts | +| Future offline decoder script | `scripts/decode_stream_dump.py` (TBD) | + +--- + +## Tests + +- `StreamDumpWriterTest` — length-prefix round-trip, zip contains expected names +- Manual: cast 30s with dump on → pull zip → verify `frame_count` > 0 diff --git a/docs/20260608_STREAM_DUMP_DEBUG.pdf b/docs/20260608_STREAM_DUMP_DEBUG.pdf new file mode 100644 index 0000000..04cb2fc --- /dev/null +++ b/docs/20260608_STREAM_DUMP_DEBUG.pdf @@ -0,0 +1,232 @@ +%PDF-1.4 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 12 0 R /F4 14 0 R /F5 15 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 716.2874 66.58104 727.2874 ] /Subtype /Link /Type /Annot +>> +endobj +5 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 705.2874 119.949 716.2874 ] /Subtype /Link /Type /Annot +>> +endobj +6 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 694.2874 97.25304 705.2874 ] /Subtype /Link /Type /Annot +>> +endobj +7 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 683.2874 110.597 694.2874 ] /Subtype /Link /Type /Annot +>> +endobj +8 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 672.2874 146.581 683.2874 ] /Subtype /Link /Type /Annot +>> +endobj +9 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 661.2874 147.061 672.2874 ] /Subtype /Link /Type /Annot +>> +endobj +10 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 17 0 R /Fit ] /Rect [ 45.68504 650.2874 115.053 661.2874 ] /Subtype /Link /Type /Annot +>> +endobj +11 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 17 0 R /Fit ] /Rect [ 45.68504 639.2874 65.24504 650.2874 ] /Subtype /Link /Type /Annot +>> +endobj +12 0 obj +<< +/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +13 0 obj +<< +/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 29 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 + /Trans << + +>> /Type /Page +>> +endobj +14 0 obj +<< +/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font +>> +endobj +15 0 obj +<< +/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font +>> +endobj +16 0 obj +<< +/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 29 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +17 0 obj +<< +/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 29 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +18 0 obj +<< +/Outlines 20 0 R /PageMode /UseNone /Pages 29 0 R /Type /Catalog +>> +endobj +19 0 obj +<< +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Stream dump \204 developer debug recording \(FR 0.1\)) /Trapped /False +>> +endobj +20 0 obj +<< +/Count 8 /First 21 0 R /Last 28 0 R /Type /Outlines +>> +endobj +21 0 obj +<< +/Dest [ 16 0 R /Fit ] /Next 22 0 R /Parent 20 0 R /Title (Goals) +>> +endobj +22 0 obj +<< +/Dest [ 16 0 R /Fit ] /Next 23 0 R /Parent 20 0 R /Prev 21 0 R /Title (Modes 0.1.1 vs 0.1.2) +>> +endobj +23 0 obj +<< +/Dest [ 16 0 R /Fit ] /Next 24 0 R /Parent 20 0 R /Prev 22 0 R /Title (Archive format) +>> +endobj +24 0 obj +<< +/Dest [ 16 0 R /Fit ] /Next 25 0 R /Parent 20 0 R /Prev 23 0 R /Title (meta.json schema) +>> +endobj +25 0 obj +<< +/Dest [ 16 0 R /Fit ] /Next 26 0 R /Parent 20 0 R /Prev 24 0 R /Title (Wire frame format \(raw files\)) +>> +endobj +26 0 obj +<< +/Dest [ 16 0 R /Fit ] /Next 27 0 R /Parent 20 0 R /Prev 25 0 R /Title (Android implementation plan) +>> +endobj +27 0 obj +<< +/Dest [ 17 0 R /Fit ] /Next 28 0 R /Parent 20 0 R /Prev 26 0 R /Title (Visualization / 0.1.3) +>> +endobj +28 0 obj +<< +/Dest [ 17 0 R /Fit ] /Parent 20 0 R /Prev 27 0 R /Title (Tests) +>> +endobj +29 0 obj +<< +/Count 3 /Kids [ 13 0 R 16 0 R 17 0 R ] /Type /Pages +>> +endobj +30 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 662 +>> +stream +Gatn$>Aoub'Z],&.F)EaBs)774pQV>JSH0M6p'n'6;j%Zt0uX";\`LJ*..@MM!e4ZiIU1i-N=lECf6-I,[M6&&Y4;X2joqrIB\PTK\$;8>m@%GRg&HNOTdL5eL4G0EMjk4SBZF71\it8!spO![Th1?n9(h<^6pQ::\15,_Q)l[Rq]d6ptc#Y6GfM%!P5q[A&f&G4jN9=jo"31INkBrcHIFhAZa,/[/e-upj[_\i30*L(MhOhN6L$);QhUiZ,r6bL'WF=hG*dc!$^Q2!Sl=LWRhL>`YPjt+WYtXg)$RnSWY!ZI\6).[O]S%R969j+he.RHr`lqaplmfQ%+QfUG#qE%IsdL/2ngn#VFBbfZgJ)fTWO<7#;:8KNXGj4W@)r%K&uDkHGgOmRT):8NsEEa?C?fGOJm\qGjT#hE0rP]]eU1"k?^Wf4'"X]Dlp3QNs])XGJ4n^..8(2ocK~>endstream +endobj +31 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 3255 +>> +stream +Gb"/)D3*F0')nJ00jbFCoQIX:?L7rP_^i5CA!>]9)P?Mqf=tCq3ih6ITb,iXBK'1e9M=3"$Qt)(^EmrPZ2IG7e*b/-QDQ/2$/;o4&'iHLVX*naKTf^M0oFrD\%Ko8bU*7YpU[IbM`F-]2)aF2YCcn6E8FrDnQ-i*FSlHjplJb#ssWS$+%.QOdGRVJ;E3W>"IW_sH3=1OFBH_o)BIqn#?EoM@fj89SX/()oTiMgFZ4S9Ke$N",nQ_FI]9if%"HT"N9\NN490,olBrZ);C`=F_)flP`3o$e2!r"fFePa+s.MlqJp6Z)!`Yf4an@%,5f"&?$RK(4@V.g,_92e'8cot>CH&TY`k@sVsNQG`kM*meD3)0R_S;d!W9,UYOAkehm)!t4&$s/1T$TmCR[rpB\5dRL+pQ<-s#Kn]JThW.=R*a&IqhA`JgJi3h)9l(#i]%Ld/hYM&'O?`7ebIQE\n+Z=E>7bGi>GqKD3-W!.pf=rg[ki=0oCY]jSA$fA=+f]"W((qr\_V69F1SfP(*oOW+g?W\V8)'S1rC6n'R:r&@s.V<;]8PjN%soJs]rFe(T,*3mi%J#=Y7gW]^C%2RTNH42;,/1MpZEWb=f,i/6`3J$AbR8W(iCMIG#3+Kr`u,q!=jo6^KMc>.^)U7;%$D=;5I'5+J#@3tiaf]d`WPbn3d4KOF626RskXU"/7$kbTq]WsLYoT0eaP_8,WV0&>.D?P=%M-8eT)s8]#LrV=lI!rC@:!e?3uN/'[/kY)#LH,q#4ST/+:,Gt3tMOqLPhb%9oIgA_pq+3X;aX&=YX/Em"^)WMQ'-6Yq+/A_dIn6.;"4j(GTGgFD%X$c)qICaWN\)!h2O[7&OdpKT-+@>24@9kk:"^ab90adKECdIBPuq<^dCr^YOIoPTF5h@.u(3N@qa=8eV'(KiashjHCBq=LSD[a#c>eArZW=^ra*T,9J^A6OYKMAC#o($1,-hrc+ij[I-^AgY)CcnGhXoDYH-H3?uDZPNW>GATj=LD_?!F00`G+dSS)5'6!<1=\S]WUn$0;]\HZ7!f+hcf+?bp]BQD3*uoGl$F?.,9/k]g!]JHg6$iEMVZgs74\fta,V13nCI?a_q;c4f7&l,mimP?QrOr:k[X9Vo#aXER$\*MtTZqH+=5&9N:+]9/h:U]ZPt,tR6"_g&n>puhY5o:1$n_D(@CY:rhodcPPrP:`-TOJ7)f2G2,i"SW4ld7i2M3a!1$Mr4l!7?:V'HaHrp0WnJ=;WQ0PNn"JQqm;"F`G);Cb%Rcm.jGN*Y)rcW/TQS2VKs_R2An#A%W7&UO@?\#@`P'@DhZb+-:>,FYEt\3$c6/E],98_D9i_AT7KR`%>#KS7A`/qVLM&k!\YD*,f/[2>$;e7p2e;5tOUR+P`*W=onI8puL88i7e-W=Pt`@LZol[1Rd9#;F?"%H(/FJ@S*c!2%C7o;M(eC=N;r%CH4aW[^Hg;5M>p0WZ?h=dG.C]s*p;KN(,TPb>'XZ1WC)W-g#640(c\5nrq?r?'%%U8V*Wm-"euHH5!Fi9Gk[CrL[HKs<9o#["IZ2GV-Bb5]+me^a6nEN(6q%$iH#T6k)e&$/hP($<$r[\m)DkbF/me@cTQ$A+;Z123*Ybi(Bg9dO((:"\9X#%Gn9PXD?a4;@3PWX%l@0A3]#m2MOC12WDnhFR"_GT>J1OAs"q?6D3"<+j*4&+iG1KsV6ek7XUioQM8c>M"\O#a7t*LQGJ'g9!L(e\j0d@mETMENi/iM!+\!Cj09$"Pfn-B)bir9OfU_XhqnmaTWD3.3E*f<#8*>h_.OUODOg,DRX'F1\\5AmYAUFf`igL=".E0TLm!Emh3u!+bBmFI,Eh5_OnTMP^4UY/rpV+1Wjo]r2Irc4sK_We3[)_oT7%7CNQO4#aX!=Pf!$H7Bd-_)Z$W#NQ6@"1k(Z*pkP(&r9/1@U-`PmjLZdR8=ILhq3"nDDkWTMn`8\R"endstream +endobj +32 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1025 +>> +stream +GatU2=\n'3&:X)OR)]84'AGNm=D8AjC=+)SL>A1g-q^%n.Mo?oNaKPbUM2D2'NUo%m5aP/B"f-2)#2JS4%oZMI5jEg5T0>/:m7:)s+%.<.$nO"e^mR,F\Q&06dig&\4\B/oPrm)8uF=Q?`+5eDB;6+XJu.eBof!<*W/_og@Q5$MZY1L_`k2M2b,egSi_'_;diu59)(+L5'??eAL#&tb_[1)nmPITl1.7BGeb]TGbJ:7,h.-N*^N.E64_0,$f)k*/O>VPg*brqS`"?5WlX5lgaWipe"jFugMV4e?]L4JWWAl%Lf$%b3A7o3?+u"hp?\slV)n?_,V,_o%D5+)^rUBg)EYTki'8DidE!urEK^ZS#L/t`qD9jG]bIGNBE6gE^(PlQUcg5nr]cit1UEW;7<(QE%"TA9KIlkD&J(\W8Fn[!tl]]T>o4/@A^YA4"0b9Mq4R_0`0HUCN?(L"5=`;K&S+N\A'%9!DJWa(YbY)a]?!0'3Ml!X+KAoo5[MC%:gMsuR%B%2hC=K&(eAIl;rR8++_1/k0jC#1Kip)O&gpHlPDn+'dgu,1[^qUC;qUXWN7W3XD;#K&##&V[/S=B4i"j?9IhUs<*,f/oCgR_eniXVN0e9s&fW'5$0e8]Ro9^Ct'_rCGq`_)KpL5ikYKQGt#6t?[J4"3c6`=Gch9BHnAL%_Je8n"Q8173-endstream +endobj +xref +0 33 +0000000000 65535 f +0000000061 00000 n +0000000135 00000 n +0000000242 00000 n +0000000354 00000 n +0000000502 00000 n +0000000649 00000 n +0000000797 00000 n +0000000944 00000 n +0000001091 00000 n +0000001238 00000 n +0000001386 00000 n +0000001535 00000 n +0000001651 00000 n +0000001919 00000 n +0000002025 00000 n +0000002103 00000 n +0000002309 00000 n +0000002515 00000 n +0000002602 00000 n +0000002930 00000 n +0000003004 00000 n +0000003091 00000 n +0000003206 00000 n +0000003315 00000 n +0000003426 00000 n +0000003552 00000 n +0000003674 00000 n +0000003790 00000 n +0000003877 00000 n +0000003952 00000 n +0000004705 00000 n +0000008052 00000 n +trailer +<< +/ID +[<0d244f03f80a81b6b033b4d323b34444><0d244f03f80a81b6b033b4d323b34444>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 19 0 R +/Root 18 0 R +/Size 33 +>> +startxref +9169 +%%EOF diff --git a/docs/ALPHA.md b/docs/ALPHA.md index 467238f..07eca0c 100644 --- a/docs/ALPHA.md +++ b/docs/ALPHA.md @@ -1,6 +1,6 @@ # Android Cast — alpha release & QA -Last updated: 2026-05-23. Use this document for **feature freeze**, **alpha builds**, and **sign-off** before widening testing. +Last updated: 2026-06-04. Use this document for **feature freeze**, **alpha builds**, and **sign-off** before widening testing. **Related:** [ROADMAP.md](ROADMAP.md) · [README.md](../README.md) @@ -24,6 +24,8 @@ Last updated: 2026-05-23. Use this document for **feature freeze**, **alpha buil - [E. Known limitations (not alpha blockers)](#e-known-limitations-not-alpha-blockers) - [Log capture (when something fails)](#log-capture-when-something-fails) - [Alpha backend (optional for LAN-only alpha)](#alpha-backend-optional-for-lan-only-alpha) + - [F. Remote access — RSSH (required for full alpha sign-off)](#f-remote-access--rssh-required-for-full-alpha-sign-off) + - [G. Email + registration / 2FA (required for full alpha sign-off)](#g-email--registration--2fa-required-for-full-alpha-sign-off) - [Ending feature freeze](#ending-feature-freeze) @@ -43,6 +45,7 @@ Last updated: 2026-05-23. Use this document for **feature freeze**, **alpha buil | Screen capture (full / user-choice on API 34+) | Secondary-display capture, Miracast in-app | | Camera cast (API 29+, best-effort) | Live sender mirror while casting | | Receiver playback + notification stop | Play Store / AdMob production | +| **On-demand remote debug (RSSH, hidden)** — developer settings + BE | WireGuard-only remote access as alpha deliverable | Code gate: `CastConfig.ALPHA_FEATURE_FREEZE = true` hides experimental transports and turns off multi-receiver selection. @@ -139,7 +142,7 @@ Run on **two physical devices**, same Wi‑Fi subnet. Record build: About / `ver - Some apps silent under playback capture policy. - Heavy rotation on some OEMs may still stress system WFD/WM (watchdog) — document device + lock state. -**Alpha approved when:** all **A**, **B1–B4**, and **C1** pass on your reference sender + receiver pair. +**LAN alpha approved when:** all **A**, **B1–B4**, and **C1** pass on your reference sender + receiver pair. **Full alpha** additionally requires backend deploy and **F1–F5** (RSSH). --- @@ -162,13 +165,38 @@ adb -s RECEIVER logcat -s ReceiverCastService:* AndroidCast:* ## Alpha backend (optional for LAN-only alpha) -LAN demo does **not** require OTA/crash backend. For **full alpha** with updates and crash upload: +LAN cast demo does **not** require OTA/crash backend. For **full alpha** (owner agreement) with updates, crash upload, and **production remote debug**: | Item | Notes | |------|--------| | Host `examples/ota/v0/` | See [OTA.md](OTA.md) | | Deploy crash reporter | See [CRASH_REPORTER.md](CRASH_REPORTER.md) | | Configure URLs | Developer settings or `local.properties` | +| **RSSH remote access** | **Alpha essential:** hidden mode (no VPN consent); device polls BE, opens outbound reverse SSH to bastion; operator connects via BE UI whitelist + session. WireGuard v1 is lab-only for alpha sign-off. See [REMOTE_ACCESS_IMPL.md](REMOTE_ACCESS_IMPL.md) | +| **Email + 2FA auth** | **Alpha essential:** `info@`/`admin@` forward to Gmail; user register + email verify + TOTP; see [20260607-2FA-email-mobile-auth-flow.md](20260607-2FA-email-mobile-auth-flow.md) | + +### F. Remote access — RSSH (required for full alpha sign-off) + +| # | Step | Pass | +|---|------|------| +| F1 | Developer settings → **RSSH** (not WireGuard) → no Android VPN permission dialog | ☐ | +| F2 | Device polls BE (`type: ra`); operator whitelists device + opens session in dashboard | ☐ | +| F3 | Reverse tunnel active; operator reaches device debug endpoint on bastion (shell or agreed file path) | ☐ | +| F4 | Session expiry / disable tears down tunnel; BE notified | ☐ | +| F5 | No persistent VPN key icon while remote debug is active | ☐ | + +**Full alpha approved when:** LAN sign-off (**A**, **B1–B4**, **C1**) **and** backend items above **and** **F1–F5** **and** **G1–G6** pass on reference environment. + +### G. Email + registration / 2FA (required for full alpha sign-off) + +| # | Step | Pass | +|---|------|------| +| G1 | Inbound mail to `info@apps.f0xx.org` arrives at configured Gmail (forward) | ☐ | +| G2 | BE sends verification email (SMTP); link opens on mobile browser | ☐ | +| G3 | New user registers → verifies email → enrolls TOTP (QR) | ☐ | +| G4 | Login requires password + TOTP; lockout after repeated failures | ☐ | +| G5 | User can set recovery email; admin/root can clear auth lockouts | ☐ | +| G6 | Register/login UI matches crashes console theme (EN/RU, mobile layout) | ☐ | --- diff --git a/docs/ALPHA.pdf b/docs/ALPHA.pdf index ccb3518..45fd14d 100644 --- a/docs/ALPHA.pdf +++ b/docs/ALPHA.pdf @@ -2,7 +2,7 @@ % ReportLab Generated PDF document (opensource) 1 0 obj << -/F1 2 0 R /F2 3 0 R /F3 16 0 R /F4 18 0 R /F5 19 0 R /F6 20 0 R +/F1 2 0 R /F2 3 0 R /F3 18 0 R /F4 20 0 R /F5 21 0 R /F6 22 0 R >> endobj 2 0 obj @@ -17,73 +17,83 @@ endobj endobj 4 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 716.2874 143.501 727.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 716.2874 143.501 727.2874 ] /Subtype /Link /Type /Annot >> endobj 5 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 705.2874 182.629 716.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 705.2874 182.629 716.2874 ] /Subtype /Link /Type /Annot >> endobj 6 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 694.2874 179.957 705.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 694.2874 179.957 705.2874 ] /Subtype /Link /Type /Annot >> endobj 7 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 683.2874 157.733 694.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 683.2874 157.733 694.2874 ] /Subtype /Link /Type /Annot >> endobj 8 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 59.68504 672.8499 182.22 682.8499 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 59.68504 672.8499 182.22 682.8499 ] /Subtype /Link /Type /Annot >> endobj 9 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 662.8499 138.0375 672.8499 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 59.68504 662.8499 138.0375 672.8499 ] /Subtype /Link /Type /Annot >> endobj 10 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 652.8499 243.495 662.8499 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 59.68504 652.8499 243.495 662.8499 ] /Subtype /Link /Type /Annot >> endobj 11 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 642.8499 156.795 652.8499 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 59.68504 642.8499 156.795 652.8499 ] /Subtype /Link /Type /Annot >> endobj 12 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 632.8499 195.15 642.8499 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 59.68504 632.8499 195.15 642.8499 ] /Subtype /Link /Type /Annot >> endobj 13 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 622.2874 170.181 633.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 45.68504 622.2874 170.181 633.2874 ] /Subtype /Link /Type /Annot >> endobj 14 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 611.2874 202.645 622.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 45.68504 611.2874 202.645 622.2874 ] /Subtype /Link /Type /Annot >> endobj 15 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 600.2874 122.173 611.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 59.68504 600.8499 256.83 610.8499 ] /Subtype /Link /Type /Annot >> endobj 16 0 obj << -/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 59.68504 590.8499 258.3 600.8499 ] /Subtype /Link /Type /Annot >> endobj 17 0 obj << +/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 45.68504 580.2874 122.173 591.2874 ] /Subtype /Link /Type /Annot +>> +endobj +18 0 obj +<< +/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +19 0 obj +<< /Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R - 14 0 R 15 0 R ] /Contents 40 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 39 0 R /Resources << + 14 0 R 15 0 R 16 0 R 17 0 R ] /Contents 44 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 43 0 R /Resources << /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] >> /Rotate 0 /Trans << @@ -91,44 +101,24 @@ endobj >> /Type /Page >> endobj -18 0 obj +20 0 obj << /BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font >> endobj -19 0 obj +21 0 obj << /BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font >> endobj -20 0 obj +22 0 obj << /BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font >> endobj -21 0 obj -<< -/Contents 41 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 39 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -22 0 obj -<< -/Contents 42 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 39 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj 23 0 obj << -/Contents 43 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 39 0 R /Resources << +/Contents 45 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 43 0 R /Resources << /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] >> /Rotate 0 /Trans << @@ -138,116 +128,147 @@ endobj endobj 24 0 obj << -/Outlines 26 0 R /PageMode /UseNone /Pages 39 0 R /Type /Catalog +/Contents 46 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 43 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page >> endobj 25 0 obj << -/Author (Android Cast project) /CreationDate (D:20260605123816+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123816+02'00') /Producer (ReportLab PDF Library - \(opensource\)) - /Subject (\(unspecified\)) /Title (Android Cast \204 alpha release & QA) /Trapped /False +/Contents 47 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 43 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page >> endobj 26 0 obj << -/Count 13 /First 27 0 R /Last 38 0 R /Type /Outlines +/Outlines 28 0 R /PageMode /UseNone /Pages 43 0 R /Type /Catalog >> endobj 27 0 obj << -/Dest [ 21 0 R /Fit ] /Next 28 0 R /Parent 26 0 R /Title (Alpha scope \(what we ship\)) +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Android Cast \204 alpha release & QA) /Trapped /False >> endobj 28 0 obj << -/Dest [ 21 0 R /Fit ] /Next 29 0 R /Parent 26 0 R /Prev 27 0 R /Title (Recommended defaults \(both devices\)) +/Count 16 /First 29 0 R /Last 42 0 R /Type /Outlines >> endobj 29 0 obj << -/Dest [ 21 0 R /Fit ] /Next 30 0 R /Parent 26 0 R /Prev 28 0 R /Title (Feature freeze checklist \(engineering\)) +/Dest [ 23 0 R /Fit ] /Next 30 0 R /Parent 28 0 R /Title (Alpha scope \(what we ship\)) >> endobj 30 0 obj << -/Count 5 /Dest [ 21 0 R /Fit ] /First 31 0 R /Last 35 0 R /Next 36 0 R /Parent 26 0 R - /Prev 29 0 R /Title (Alpha app sign-off \(manual QA\)) +/Dest [ 23 0 R /Fit ] /Next 31 0 R /Parent 28 0 R /Prev 29 0 R /Title (Recommended defaults \(both devices\)) >> endobj 31 0 obj << -/Dest [ 21 0 R /Fit ] /Next 32 0 R /Parent 30 0 R /Title (A. Smoke \(required \204 all must pass\)) +/Dest [ 23 0 R /Fit ] /Next 32 0 R /Parent 28 0 R /Prev 30 0 R /Title (Feature freeze checklist \(engineering\)) >> endobj 32 0 obj << -/Dest [ 22 0 R /Fit ] /Next 33 0 R /Parent 30 0 R /Prev 31 0 R /Title (B. Resilience \(required\)) +/Count 5 /Dest [ 23 0 R /Fit ] /First 33 0 R /Last 37 0 R /Next 38 0 R /Parent 28 0 R + /Prev 31 0 R /Title (Alpha app sign-off \(manual QA\)) >> endobj 33 0 obj << -/Dest [ 22 0 R /Fit ] /Next 34 0 R /Parent 30 0 R /Prev 32 0 R /Title (C. Settings matrix \(pick one row for official alpha demo\)) +/Dest [ 23 0 R /Fit ] /Next 34 0 R /Parent 32 0 R /Title (A. Smoke \(required \204 all must pass\)) >> endobj 34 0 obj << -/Dest [ 22 0 R /Fit ] /Next 35 0 R /Parent 30 0 R /Prev 33 0 R /Title (D. Regression guards \(quick\)) +/Dest [ 24 0 R /Fit ] /Next 35 0 R /Parent 32 0 R /Prev 33 0 R /Title (B. Resilience \(required\)) >> endobj 35 0 obj << -/Dest [ 22 0 R /Fit ] /Parent 30 0 R /Prev 34 0 R /Title (E. Known limitations \(not alpha blockers\)) +/Dest [ 24 0 R /Fit ] /Next 36 0 R /Parent 32 0 R /Prev 34 0 R /Title (C. Settings matrix \(pick one row for official alpha demo\)) >> endobj 36 0 obj << -/Dest [ 22 0 R /Fit ] /Next 37 0 R /Parent 26 0 R /Prev 30 0 R /Title (Log capture \(when something fails\)) +/Dest [ 24 0 R /Fit ] /Next 37 0 R /Parent 32 0 R /Prev 35 0 R /Title (D. Regression guards \(quick\)) >> endobj 37 0 obj << -/Dest [ 22 0 R /Fit ] /Next 38 0 R /Parent 26 0 R /Prev 36 0 R /Title (Alpha backend \(optional for LAN-only alpha\)) +/Dest [ 24 0 R /Fit ] /Parent 32 0 R /Prev 36 0 R /Title (E. Known limitations \(not alpha blockers\)) >> endobj 38 0 obj << -/Dest [ 23 0 R /Fit ] /Parent 26 0 R /Prev 37 0 R /Title (Ending feature freeze) +/Dest [ 24 0 R /Fit ] /Next 39 0 R /Parent 28 0 R /Prev 32 0 R /Title (Log capture \(when something fails\)) >> endobj 39 0 obj << -/Count 4 /Kids [ 17 0 R 21 0 R 22 0 R 23 0 R ] /Type /Pages +/Count 2 /Dest [ 24 0 R /Fit ] /First 40 0 R /Last 41 0 R /Next 42 0 R /Parent 28 0 R + /Prev 38 0 R /Title (Alpha backend \(optional for LAN-only alpha\)) >> endobj 40 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 871 +/Dest [ 25 0 R /Fit ] /Next 41 0 R /Parent 39 0 R /Title (F. Remote access \204 RSSH \(required for full alpha sign-off\)) >> -stream -Gatn%?#Q2d'Sc)R/'c\0:9'6J#\8/D7l4r-['j65`\@_X"`.qCB%sM]:"#u>g58o:.1t5-4FYV'%]0fIMtXou!P$50HpN=>arM^dis-?E=',mq.m`km-@VSMJ?bs5cubTM:aS,"Mo^ENUaB?+F#lA`D^_js\g*,nI^d+OZ#bC1&M9*t:*YuAnK.g>,N%"f]6o49&e\%L=Ufu*53%uRo#9;uN2Yg*.SGQG0\N_b("sdto]%V^L@A>cO^qO"4!F`MNS8XSBRK#sFpZsuJJt!&l.%OF/9fQ_aY!kSU(Oduae.T&@S+RN\TBe!c$H>%kh)?_"?$[^uhX#(_>P_?&H](!,keDm#a4J1hun*sWrp`\0g67j*aR<;K#7k2+eC/ldu5sISb_[KK'i%`o[;p`ek$$&[SgdhBh.\d#/=C'ZO^6/)(ZhTn%Nu,"P\0R4:N;POK^L48ihR.!3%V3%XDWR6g7`FS:n;"'_T\+=-lr>/6oKXn*Q8s5>e?gF@'rQppCBB9KH`2WVh@iT*5R.T7,~>endstream endobj 41 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 3456 +/Dest [ 25 0 R /Fit ] /Parent 39 0 R /Prev 40 0 R /Title (G. Email + registration / 2FA \(required for full alpha sign-off\)) >> -stream -Gb"/*D/\/u')q<+0u/3WCl'L*0+r]C%uOgT^,i\Ha]ID.TV08hZnYoe/nrr5of#O$'Ldj&=%S8O)Run7mW*JoG%PohRAgB?rb:VHB0O-..9p+nTFmR:5r02WVRLM%4o+,+>L,-"`Tiqm!PNqh0m26dmr0+.e-]AI0k'@"VMa9cP[[u2<7ATq!N3C:O!.Z::/'J:+=aY-WnA]0!Y:ef]OW#;E0g8ULg_>on.)=&O:KCrSBe)I-P;&X7-Rda*aC@=g8WA@][bG]VhB=JhjNd5DKS-UMkD$7kEWUI7$/2kYKWgD$Pj:P*ojGecC:C33ISo^IBr/2't'm>86&]#lD7siZ893:I/#N8.D?1N`9P26$D4NkYmipp6<'^-po0ZH'YUDZob/kq:Z!u=9X'bQWqPa+kXI;#+qmZa+sA3JLj1OFA<7i\T7nd&O/K0S-\b\0I*#EMmqOe"M'LA@YVa&#Y+kWHDX*nY[\`]oT_(oe)]C>M&u;&iOCMK<)R5s9koc5Y$u(U6Hk(%+cQ"5WBL)67rKXKB&k7'*kuq[D?'?6o=19.5ieT^&S9Kd,lAu;D;L_)X]TJ6&\$D4AeY?S[6B_``[NKO8'4e[QX"$LbMPXKH&\h+N#j6n^cD0kZ%2^hW2rBI)9>T[f(b`_%05O,R!=)Xr-g/%RF?VqE$p3qtI2YoOpL3X,)q0%glEOcT"n6JP5YtKWG0u?XYm`UI_-i('*X$?p1)E[M*1XrY@@Nh1004,5O-`_2/=,dW(NnA]qc1Kd-7_^g":)gO3d22i5-Rt,mkoMa,pjH4GSF.r7N!+Mp#K1&kgWKijd_#jo=c/Hd&JX1QZqBJ^0!>W!bIBu^j5LL.h6dJPdtC@M#7\!@*TN&D@[]pH>60?Y=WJZY_p;bc=5RVePbhPJ5ui.S7ebL7RqKjg?ZiQeaVR90`2Z1kWIn)rK4;iKd>[%/6(C3!;B--`>UOj`U2$dV;F]S0!)D=NXG1iSLe&]XI!'LANOP&_:N2LsVV/qlJX+Q^FpD3,rN@o1t)LN;hGegLUL6a;K6(:#Z^9VO6F0cNhI<&rGkkTfaAofkO;S@!mo1?,I&uR^G"YdK;JbpmD'#s&\WNkhnn)*p/QP62kHA8Cbolp>W$0%&u,51[g_3oQ?@$2#!jT0Mi[9B#$RSlLD`6RHfI*@@"5VA&S;a,_?EG5EFnc9/I9*=sR%q*1W)Si7u!/'"R!']KHpf&0IIgUr'/UmBc!$QN/IU]4o6b_$qFZ@Un1r[i7HC/9CWa`.Z8;$8;kYbM*G.apNrlX/BoPsBtp!mIH0Y[D^H@0l.h^eI<"pRX6=2P[>;\&Fq][)s1k)Qp-+*\-gd?OhS_r#b40&kC(gj]^9Cta]pm!_+)1_EpR'-r\b>&!3t_BZR9,f%eR[>_mZ*131(mcU8===,jW?-%<>!T+6N<)$13F6L%$NY&`2^;LOBb2"S8IscInLj0@sg)FpZ,2U'Via"'9XeobY>#\;*O/";pfD;OI[2$qOV)j5#\/\?F2;ibK41&U&[3E>mT,]m5Pgah'HN[O-M:nA0d;"dqVMhtaTJ4)=h'3KmkJZmmr73D$H.&=`bE;t\M-LDFT5oK:AHH*XH:^2@OJb9]&Vu6^4h^;JIXiJ:=23$QGQ@2<+l[atIe0a4;BBB\9*B*Z%1]n2q,nG9>mTM$'FJd4NBp"B,1/M!3U_2FEaI!^2-[c4E2a>^e?\I8n;pU[tm'M#ilr7&glkm]dn^**[%@noYY1?ZW%>=cWKIW)6S#X@$]dT&241/L=NZnTkm/IJ1E/h7_@QGLf?0d@"$JeT/oC\Zt_B08_6$[;EBfiM$<7m)fd>o^52XEaO*iAT3(IVF$,-)';.`!penQ.]-?5S_1;U=g]2\ga.Z*n(JngJB]B]?;d^Z(tldE#s7d1otI,_>RH0Me3@Rt?k%q@+C,0:ui>?!43.jfOHNGj"O`\s$T0:X4%'5GH`_+;&leQKJB(iL.F),8%:=f;#R+*/$-2V0HESpkpP5?X*1!aKm)aL,t(5Yk]6>6a;::F*[+/MWbGCY8C.`>gt=1?u'%>j]UJ,)X36Un4T(Lq_>M.VLt,17pi22U4%4(4`C_Z=2PMYI4C`cPR]Ah/XhU9&-f+Y;m]A!Bi]Zpm?K-]HRUSB]gDXR^Aa1.VZ^uX+aq$^^aH":OgJF*^PLGo-61QelE"#TYDDE-Ee;7"i:EU9$'D0tdbhfaEsZiV";(!c&ED@R2NH_Rq(La[ZE0k2l9&OS.Kg#o7^NO#$0\?YZT>_f^=K_n.ds?2.eG[.LN1i9:c-dD-D`eM2=^O[MP'+:g4ffU"6V<.m87:8)*7-G6D3b)7:&sE9S-'8\3"9O#FsMBr[Aa*b]]MPV/d=Ma#)>_3ZuF(hbL9F9dH-biA)4eRmVAWRJZ(.Tp"Ee>!MO_Lg6u-O[6*oArcCrAE>XKhg<,$?oKZ0%c1oe!Dol8\Sq*=qnO6g5D^aS1fKG8f-Q7TGj4k[8nO38+\/!:tbl?,4Vot\?0ANZeM"o)*7Mu0m29\ZPJ6e=[=-XUdgi,aS(b]bt!(&(8rf(U^"&ET]5W'SS$9c?fW-d3qL5p5:jA_:8M1l0t)pXrObG'7BKG\7#jHX,%"V6=qlpkH`j[f0+*Rt&Jr`s3%C'YAim-?(cgqjmr/b3&hdHgOCU:u[!4J(U9SuK4moOcV3U)Jk5+/dXO^$s/DDLt%?%gd>2&)\67,s369ST/>jeoPPnpG0CtsbeR^%JUNendstream endobj 42 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 3319 +/Dest [ 25 0 R /Fit ] /Parent 28 0 R /Prev 39 0 R /Title (Ending feature freeze) >> -stream -Gb!l"D/\/g')nJ06/Z]I2VZ!(.sg<409/pIjG1fr6c6d,LJBE/EJim+7n2^1Ic-mmcfuh.ms8]'Y3d8q(Yaq9TM`F6(9&*QHE_@O>miRV(T@!NYkl*u5q@,VmEnL$ndl0YTLARoYgd-*Udi%6qAU-D!,Eg(u7H00;#]t)Q6lr7a'D)q#:RfIe3>F)Ja`5#lfOef)#Q+@u**mnck/o,rLNH:I*)Be1rieM%^SKR,`en'&,e9jQg8-gS>.9Ri;,Mk8a&^g@kk]3,qGUcP8:cqD7Y\pfoG-Gj87p@pgY\q6Yr+3I=V>(DbPj/u;KrDBm-'Y4IH<;'=,!&sHg:@QFFeMNO8gBYI:]qQ2YV'aWK+d0Cj`@B$-iZ4_>kCqh&/-Wg\!E[94K/48HsSbsDKGQ_=[.WQh;c?G2*.DCcX`MO.9kJ22lc<'>)I9(_[B]_oSS(7LG,Wfc2h6bf)o3s49-7NVJrb"kb3CN4R2)6RSXSD0hil")iE+jX-hAhSh77!+Kjldju(AuZ<\CeFo_T)iO`.SeDL@3SNpQf?smeoN>Xtui@o7\/1uaREKY6/095>(%ci^9<.1@$F:1",Ym6Ia+WjMaB4HlBRDTRQF8]D#J7Q4)\\fI_fGI!D2^X2^^H<:&thCNkg'EF/+A(TPbOcA9;JKWNeAV&nVg(?71:"FLiHCE'1K.(/]r^2k%TIeF'BeAEXu4iB0"'_@a^BP.B4(B!$-li<`I5S$/8*98FQ^#!'E[(SofZbh[Lkmg5,1P;$a)d+io\YK']=nWcV)-?^]n`.D2rZ_Y&RNJ"k\e2SdpB=^ffp/HHc<1-Q;S[Rf55Xm*4&>=M^2t8$[P:7j+s:aN(,T#HMC'-CZ'h#b*OBMBat.aV>mC,aA@nkGhtE#dRu"l#eD5ZL(T#*QVB__fgES@),k>VFpAaV65#hI]C;+g\._,\gAo+J#pB._!9*A8lY^>9'\NmTnD;5qb?fC+Z0C_:/R6UZ>gj10@ggo)o`4C3ikdrZR`j3J5YI:3p^?HR/;F,K;@7'r"jOlsZ8q:4(q)FrZ6t69FNG5K%jqc)N"#n3FuU=c5`^BXd@K&la8_Gtg2@d'F#K*RQs'p7@K6HjJFkuPWQ5qF5Pg[_)4p(5aELCSrQ.mV2]OVq%\WeuZmDFfADk.WVe`_\[85srQ1PHbeqDHS4*LJ\jk[-NoZ>t*W>h/;52A:A0*tu1&MMR236hDCki]^`8Wq.C5[hRrP.n\-OKpcE2'Hb!I5a[#/'oJsoB8b4j:n`LMPG1C)V"__]os)8#?,<4fU9PhI\Dgnmp^[&gK2MsRScFgp`tV0E@q\7sRtq)'Pl%EnIH[L]H&N>pCGs$rYM+jm)]<%ZFpuM:%PmqFB-[r-R>c2u^-aU&Du-nf3Sg[K?W#lN+]5%0o\oSR$B6/.!],:meG_o@Nk_,&mYj'jrH4&b46sIce,m*mRFNLO\in,jdbk86kOD_Sq_s#c/3`XYB$W#Ebt^.`.qGGM$2]lP]%aW0JM%"#3gh2p%=,%QNf'J2b,eSE?a>Q.eobbQo),D_=+@..[2"JUR4fI>`^8n">/;#QEK7.=p\?B4tEq/ETINm=h/D^W$pG(boHHgGpVQMU"5kIksuFOo!4LA;ijp$I]Yc)OZ]O(Pu2ApaSZTo/_GVKJRYoFfUlqsOR[O5[VQ7uj-qO7rcE07OA3/(g[IonkXB_S%dO/L#k,o$s%_6?MNW3%Q_c6in5>QRRH$e?8tfMVJU:Tr?qj\H8_9O8Df:n'aZU:O!5Og0jMo3R%j;2U55bYu+5`<#1oF]$@sD`=F;mP+BPJD60$,$V36T[=K,#)6'qRPN[/tYIan'g_H^4/a>i&e8&;:HW4@rFU=V\aG>3'd^5$_NU5(.spr?;9_N%G#s4BRo1Ah7+;jhf/$+N1r^oh``hU<7L]]C5MpMqIMO%q)Vgr5u*"8=-Yq^d'PK1d.m6C/EJ4jdj?$``YJE\&Q'p%lhV61poGM2JDKO%.&V./MgK9j*rL;1@5T8akWhm.KUUEgj4$6ukXWU$:+PjjiGdl.+kNpcY'N?!P)?:YjmdZ]e&&0Z1]jeJ\p6_lX,V$gj7t8PN9j\^r;DoDEna?mp5Sk^BFh,aEt-k^qer`KcSD#0ccO)5=V&kAH)9nk4ob`4]NHEDceuQ6=dlEb'VTOM0m8P\S!1&B-uRd:#o@-3)iQe%m?W;rX,@c'$WgWT7CW$O;%]@;emY4ei4_Wf^.PClHCIYA9WkZ=\-c*cernZ89,O)in*<]"bd6Do9i!3(k$!BDZL1'$5U*8@FL$7Kpm&BBB>7mB80>MIQT\SEe>IZ25Z7pNV6!EF^[J2N>:/Vpfr]-8d4]$u,ufg'^Pnc/dI92^C,`9L[Dh8dQEr`.A,U/b-&%o'Y'.?tH%=6Jj15?V#+=>"MJa`9]Ad)?e/J[?f^Q#YmOr^m!$XeBEe`fAIlPjh0`~>endstream endobj 43 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 604 +/Count 4 /Kids [ 19 0 R 23 0 R 24 0 R 25 0 R ] /Type /Pages +>> +endobj +44 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 952 >> stream -Gat='D/[lW&H:NnnU/Pg9p;qi`t0?(c@K^28sOs\-<4r(8W=?Ef^?]Qof/WsU+fFf7u;W-]B:L'&:n%opWdO33lN3O&Oo;!KHku"1]^6r?[L`aF#_[Wi?Rh7i)J"6^@=PIZc$$K?60$&'`&:1m,4+FVh8I;nLjL_l1<4s9:Jr=83M>3o;.AZ$\b!suWea%Y#X7@%=L.Jp+n@/7AXn?mS8Q]T?1jn'ZN13eiDiYLj^W,`1<8L2GJRRqLP@WlcdPma]`7'Eq'dkMiKO:]p^`NT7oD@-I?L.boZc?QR\VemA)>pr[H97!>euLbehD.`AkEi5oV]gjc#a],BmR7!X*Db=B80J9G\-)@,aZ%D2-.9H3U6D5/kP6,-(D188%)[-GLPl2h>G[9m!\CL;,/M7'F"M$I1'V&T3'9lUX;DB@0N&LBP"^h9(pNU8L0_'SBu5e3T"[1lt0J*,o9QsW?LM8kO+fgRZc\6'_reQLlh/0at[,QAU)r05@*U?&\#L^3h#6~>endstream +Gatn%:N+uI&B4,6'Q[Hf?%`g_a-(djgUIE%'Q'RS/dsV#=\GP_;YBI,qAm^RO9BEn"G-UmZ7,:a2fG;0+ejmUT<\QD?c'd:"-9.WJYt&QEXS5qg!;I')OY2R=_(A:H(M>j=f(a34f0%krEM@I*R0_P,0:J/0.Vk=0[muf#+gOZ=]E>u+\PkZ-ng,4=hI?efEN+,?!3pPp`Q)^RlJ\Z[/EaM`>4.pp^=g>ha$u>b-u?hGSrjH:=nrRQ^hX]\1ER5C<>H![r!2T,[jj.9@+q+-&KJL>?G^Cl!V4'p3Fool42sEe[(db76[mRqZ.#<+$b(#<9GtWO,e>PWFAA_(T"WY&PNY^BTLlZ$%Q"AQ`m%:M,TAmn8\-o==bgoO)VEbm"C,rK%m:(k04$j.%(J($DluJe.l"\@4d5b4!M6nIRj.IPXIHaBXs#`,2/^ZK12^jidV5SLN;:Pi:7':7VP8kF>F6'G`?(%WL)gqE2q=KVQL6''7(3A2;0=Cr8fUpL%mrTZ0^+L7j8/hS_48F=St0-9*IH>5RZC5HM,-LX5.f&ZM+d1d&C'>Q4NL4Z"3I3Hm"^$HSUP4:3`i=+eW/`4W'/dblMQh.0XH4'p\?WTbV`_&o9T^m)L5RA'm;J1c0ih~>endstream +endobj +45 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 3642 +>> +stream +Gb"/*>Ar7W&q801(gp_!`)1WCQ,PqsY1$uY0!=SXJOP(k`M.3-](h(ZEme=06ckK;fs4#Tk@kI^e=]>ALVr(i^6u5-14GJ[s1&[0E<_c]$7ICJ(80lS%^T@Vbte6t;)TYA6SeOnXFo'l987'Ep`=V=Mb69.Pmj&UTZI'.2Po'h"Qf7h/`H?Q@50$Cf85?2"feYm(Gc!=-$Wr'iY?%_L@`>25n&Z*Je'R4Nc`d0cKTp?E=rjc7"kI\S'K8(VnMq?a'5_eGYY4#ZCS6qi7>q)\dU4.`M,7+SdD\hi/P;nTYV7M_IEG/dW)TL/uPoJfdA;N0?G5GV:K38+^QXq3]iO\_5Fo]=RoaB;G@NG,A`FFfnCM8;J"\Dr%ngY--_Ukai[\E)c%ulK+q%b@_(q*>2=-EH\AnW<$[OGF/O=Wc,FTn['$2A;)N?$]9Wi.7L-lR'$4%#J7PE3P_4_(:PTFUY`r%/o@8m.I@T\R"[$![6?%iNjtd\C5Fc*5?J_S4ZiVfks!+SQdPDC8;n8NN6hVL%7Zu4dlh(#/u]`B]L[b^Vnk/(u*ADS>q;b4nQd=:(RpciAOlsgp^E9?q04GQt:d_Zrd/Y5+%Z%-'Mba+;;GA1W@5&PXRKnB_L"&QZcbW1X)Ko8`pnVDTZ$7";a`7QgGb^$WGZZ?[Ek3UY:N4\\+:BReiWC^=9Mq@JU:]=q<5(AdGjaj$^ICMeLP6>[BGe+[]9NPrc@U(Z%mI.S?K>@abg?t]cQ7*%k&-PA2G=RBT4&Qfkd&4qJSid7=VKk3RN=as7q#*R0eU1h<:7YSe-1SsB=lY^7rk34lcAa4G^2LjrWXoq??"Nq88*ZCiXT\b;cZ$"rK8jFd93AgoP+J-KQZS?iq8CX*aD%K89?RnKmFa?0l9-[NNmeQATGbh;6s*K?Rg8SN/&XIIJA#+?i!mEkApjCDm'/HN"N[Ej3`GdI0s/5Yc*2HkLb>J2Ta8cQg:t6ImMO"KJ?cZ\W\Ll\(a]+i\B?2C!8j=3H@b6!9Q;@g5,%YPIk%W(5I#T$)RfZH8e,a:?oQ$4>^.!8oJmm)rKe:Fa'6WNMia#Va?,8\8L-LbgCD+W?dAo)6*>.6EmKHhGRcm>na^jag]P=gJ^nX%)\f;$1KEAIChH86E;G6Iq4-S)/$1\b)r%*5&c+=IbU38W4&@qkQ:c#UT2:+Y])3[7sSQ$.s[(mZVmG/"p@)nK>a&;9@_n3Z$qf%&$Y.YbC)UH?*L!-d)JcU%H:9M&K0DXp?EFCSbE6@V&8)Z]>2*aiG-I$(,X]n6'X%p,lA#$k)!gpVnSP]k9RaN&gS],e7_a+/?>*OHdN0u8D6Gft*cQG_+9p'?7dMLtbcE]%Th@c3BTAG\@?gfJ4(ZXm?=FB#7b85ICdjHs>./aR,rEeDpqX%pS`lcF/A5rnmQZHsE^\Eipf>o&tXWA%UcW.Vt^eTAVAG0%EFN^r.(Fr?+"^*>7)%`E[6.K"M7?lX_4=Qh'ee3TW$^dV)?Thg!lIJF#3.^+CU!0o6j,*W+43A9iV^j[minbK`1Im@8MYTs/i*lrmm\mY,QH&7_,,]VHhLL3al3lRG$"C9_TZ&jJqiJ.DU"VkPMc+Bp.p`q:`V#`W:4Nj&1!3b>>&4.*B":mH='d%Rp_[S1ks-guE=;K"M:Y-iO_7a^X-BE0gN8SO@EFB&TcB`E[G^OSi6(rt,IZ@3]cup#GX!8u\eT?B+A"e1bG;+(/F3n6uWn?%j#jmP$%FDgWLuX!m8DcaO])oP1/`a>iOPAJUDR0DTbq8!7.ZiC][ii?s]I8IRWY(#,^nDZbaMSIETk-j6='@Gh/(J/dgIC%,2"LW8FKZnpe24]r[B1VMcNhsNXO`\pcgO:D&Dg""UN,?I!+PsE\:eW[qgKSdjER"^9*mf=R^+J/IH[B&7i#X5Z@T]qR/#DDfF-jqn8Ma,b"X't)](>?nKpB+Fr:*(`%*Z(&Qod=';p)R3.Y>4\'mHE?e7lh+hXPu`Cht2\1UI,3T9M?GoEtR>M10WT_2#N\'='^MIu(k!5U)#Ff"4&9duLFGVfjDP@-ra6F]QI'EG)#=l>t;Af@Z!^(q0iMbO78hEc%#/_JHT\9,tofP'/C:dU^VOl'b"(9bZ1Karh"\=,2*La#`<3L$*AWOM3Z6J&j(`$L+k-$c4qP`:;.11NY"_]G[6[P1q9,N[J$V,*'b8R,A^lOS;2^STa:E"I>1&E9=go05QfM&Rk<%LY=1s:$eu&c&`tQ^B?jN<&]I>liWA#ipGti&]q-K!&BWi!I7ULWKqJ+*B[\53O;^>B1CQ/ffG+p@d28_)d2N%T+Q2Nc0F]!A#9B;TcEBO"Wk`S83qMNe[5C%S_EKUG9m\CES[h.,kG=LbUQY/*uMTcsIR,@,i\X>V,V`G9ej,<*P'NjoGOB;RU6B0d%LA_G`BPleclfOHSg+^%_UDP:r[tm?m#34TeYb^K!KYC7B*cS44!S&WReKhR%7V_mLcYWTRZ=sqFrU>Nqm%T"[ogj?p26CfX^pO8\1Y!*O/Z?qN$ZN,uWXNNtP2[7Y@T`M6WmIkI1O#\mZP]Xu`C6^JQ%*mH0*r;_`oKBJ=HQo&VTWa4Ns8U]Gk>8*ODHpZE10q8S_E,*MBFb\BgL6[6p0H__G=e/b%/fH[bN=cT8VNlXV+nc2a\nRJuX)"0Hpu6srZoZA+nZjc=n+2iAb='JT`q+#&?[^0">^S-o8t!e$@?kKf'"4/=JsGNAP4(Z'K:0?=6,M-CZ-E?`Cs@BiZlLeW)N_,7[b&9YJ_-](p>4te04g[:0BXuQ9d>!jXUKDiT]2LZ1PYpt"7g>W:2!gS]r^hG@:'M@C"SF+Y^Cq;Ei!@RmLEjKZn~>endstream +endobj +46 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 3361 +>> +stream +Gb!l"D/\/g')nJ06/\ttRl7Q?]k9Pi0>?SO9s\h_q^'%/Z"QY>P,M9!dbM%@Td7gE,\P2dB3oBs*I`t,Eu?3EG))r9!uVs3XcQt*(:sBn0(4h7*#[9ta4#.ZY`[t(5C?@gUuLf6?0!PRYBke$SAcoB@<=BRC9HkDgJ6)'-Rd.u,r(qe+FD_%`fc'P5aTrI_ikMj%#$u8a(;$iZ%HV?/S3U[j0A\crQSkOF7C!n.E.dm'Xa['/0dcMk[e!9at,SY(s9/Ra(:rPg!rY=#.a4NqLoa18gqT!0o_2ti^Yo.\8",3=b;N2kp[(2+1`JRoelbL_"Y(_'J1%b((*.O-f<;YFo_Pg01@PKuO%'Ye[N,?.)BI2YFo\UWF?lmu`GA7XPrrCZL#B[>'#:p'F)W"L*pu4S<':`e#Q=aP>dUXfr"K,Tb+fE/J'=>`6[UhD[eT;=k[YZiI!,"iDDh4=<"%RHJb)%o]Ps8`YN!l(Eeab[EVR[AeP>8UGJ]2Q$1^odFLn!#a*tn94Sa%Dsl+"_mJFF#i\nG"CFRKBQIG4>;(h\mjeI!0XZC%Y[_Et;U(($1K?r5g,4><(r.@OQ"8jI@GFWP?*4_;]ZL<',V]#kT^G)JOTRL8ud5aHo$3ST(8`.En^gb45Oau][(4G4;%cjMEkk!!A8S:'Gn\_pE[]aHYa.'$X"nT'pU6:ehjGeLD4/OdJP&F418rd=p%\d_atA8-,I[LPICek+IjL3l9!+BlJCOlR:[>W&c5PIga(rsGi\e59/25PR`)93;54=BhI_nu+/eXjlc00[AKhH`TV0"0!Zhl[r;?Il&;BD$T-9:jJPl@$K/3H(Q_H#X=#qI=\5CE2:Oh:c>ct*lR@rojmW*G@/i.0"u+=,>R7sL?@+VhflCIoA%uqZ05nY8W;6aI$emEo>;uLHhnh;LH@W;>ENYu=bsg\E6SZ].p_?PH)T$c4$&(J2N)j=mfa:Jp#fiH'kp&IpNL>Ik@C^8c@3a'Ql&YZ^d%g#ToLG2.nNW=r$F]I]B<-!4W\k2do70+3OM9a#a\p-6Y#u-5pJs/IAB_>'g%sMjn](W0=oL>;siUFh0LoN!Xu"=h%4%i&2+lTcP8EP:.&MJAd"?-%0ti_.@2XE_KL\HaQ1Z.aU3`oAu`ta9d0$)ieNYH]Y_+Ah,9AqJoWC*nPVlk^\.m+Z.(dV-01rD5/G6Z!WAWFOuM`gU+&F]nI'R2QH12)ecdjL.qnsD.%3p9W.=Y5I**YJL5N6EcIrYbfae=9^3Pb>U%C6OP33CO\d0u_TrteNf9*/e[LI[WD<-j>OB7$2/;*A]`T[Z)Nd+887>n9d*2(h9",``!cC+dn;gig4i.iK-aQCQ4,H4gGde7-0A_3o*jTO8XbZIG4!R_K`"XrQJEM7Q4E#MA_;-&rnC'N#*q#,q%C+30TWZpD9.%>Y(5%;a!)01!F)N^it>"XeuR7VQ+eN:m&9*bo$0c=g;ujO_[dKFU+d)#<0fdo$-TBjnUjB/:nI'*Hb5PfQA4^joURG8p:+@k3&=kA!-I`r*,NN8_<$3^(I+Z*Zg6%uk4EGl8PJN?%43&O[I=l?nq$qg;RGGV"Z*M7m*HAA!8L0(NQb!7o,BA,K'S=m:7jlL/8fQ@&YC/_L_^^:@Bu2%N/_2fQA$a6+DI:<%-@br?h,J%aGYit_ICr@"5l+L;7M"9=d+PHbFnTo4MC*%YC\uJQ(KZnAq^MA4],0XZt?Q/QRo$hqE2N[If9UcqcNoZJ?8i>ggf]FJ1W^bfN=OH"M_Fe*bHV,.'jr(T=FptJRT2"Y?JNIIpW$GM,R_e1LMnL:B6$=+),b!CK7Q3>DfH"3mCekRru``lOGm1@!>A2(iPEJ(%%QstJg(6EYIqbr8LbH2>ubJflV>$kWcL/;q]/OqaSEaN14jHg+c!]\(cimaAe@gb'B2n))1lbj[IFl)1b"-8R:t&cahos]VM4Y<^6"P7Xd:O1W]--YV#f!*Y?6dr(:b"V]N6#S"fE@PNZB%loWZ5iH@3(AK)FHTY:5p`jp#>7g+,/,;,TNp]Wdpoa:$>Mer-"+!p5Wj^#=W:5PI8ej\e)U:6Z5EG?h=4Z2\1uYo]sd'2m:FI:/ju?"B%%r1Yhb4lk-0IJJ^pEFKq1FUU=aAn-U=',lendstream +endobj +47 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2831 +>> +stream +Gb!#^D,91_')p1[.-\I]%P_k9-A,()^c35*(kr?*=#qKJ-gi8:EOri##DW-K=7*IV-B@K[kd3Yfln2@`q&81+$5Xog7"a_*LnPY>]`i%po&g>$,d"eTlGNgp&H$0Ki+'m-r"7*cL-ki&)!4*2Lh8r6kTD9)IQ:70Q>6.'W.&=hg#g#(G4\sA8]4H[l.*oeiC3U)RTP1dRDF&UYP-7F*GdgFYl"VQ]S94`[gE?1q9r+r%/:dcKc@.5\r_i;MDLY!!RkDOp2B`4SO(^^`DWgJ-@2=!Xs_,Bcd*9frp++[9U)qd4-:#=d<11Zc5-D"K0FXPgd]OHj>4GMSEMK1k:;Y>HD(?MWITfEZ-S^JDun'S(F(`$4q7;B%?s[[\_*hQ$dld9/SrqZ2GP;75%Bu24Jq&^k;6p^Vmb4&6-2s*C?R>W="^.hb$s6;5%Z-R%QT6.ms1/6RX5a?:)SiEmdFQ/R?W1^]*+2/pi?cc%tY(iSF_,jVs"hL:><]C^0r'dnj5c56!>"ER(6)\F*7lcTb$K"Hk@/HWC/DE.)lLg+GkqJ="Jc<'7A%,+h;FU=NCO+j6"$jck(Tl;PWbf2@6M`8"lqK7+6M=I=5W>\s>g-'OK09PP%q90@_:O8qJ#l*`b&H%F#J".p8br5%k:,@7t%sXj?Xn$p\;%0M;pXS7]9'hnb1/Jn)X*ao2Iq\ndnc,gt"_dQlY;n"l-JPB*CK-V^oj8O-(LK)WHhUfsCCc/Ug@#@l7\n^%K0`F>DM&n6KPH`aJ'q#+2pnMsF5b>##mdIVA6FWHD>DqX^UA2k&K`c(E8O("p0VJ>:C?C4=#BYiiH^L02G9^e$b=Sm5Yl#h'E]8U.$cdu+.%T%MMDb>GVF7"eU^n*KVGf*NXgcGG7K>S*"))kqU77kNprllQMq/c]?e!Ro>BoDB7g!ZXDg'[38qUB??e'9SZj"`8X&n9lRQ"g(VI01Q_&n(,mf;?crZE95nV<=L?$L__CB:$%3mh52VYd1oX_!*<'h[8elqdi2/17!^b1*8hf=;o#V7aR3PY;Cc2%A9>\HFa^Lnqr@mnoWDSUE[a:MgW1''ps`XR&W?KAHerK.r$D-m0[C*5a@rj0BcJ>_%u@K@4:mYB#N:9AQu1f:BAT)n?-.$gTm423uqA?c_2P%7?DCh=%8D/25s,/SQ/*`k#E3VF6DF8gB6$]24fgGRaL,oMSpoG@Wp2c`L7O$'1HE3.Ma6)Q*j00"Vm7F*h_l=(S%Vtpa`bMGoLFkf*/aRrf%Z63u09G`r[a>q*i`IpaHVJ\'3:sRM_^FgiM:_M/=+Q+_i6%gZ]?[gh%u#r'DZ*9pJ1CcRI`ui;m&$X#E\Hh6iA&UrH#56OGJ>+,\8mVc+Ta7jh*C@d\<,--==BoNJa(M0*(VmK4a`[S9-tJMm!Hn)[Z,1X*LGSUQjNa1*I+J[pp[TW8%Z8-gRE0j\n2tYs8)gKIGJ`[GAN56;8LMJ7\nM>PR5IcMhoL#1rf1BUs3'+m(oH,AX]0"eUl*fVUg*(Fj$,c`$.e+NtQF&b(WphoE.;Wc8RJfMp`P4KF&fE@gNf&5aZg,]FN,5gY(Td,?GqqiH;:s$#:I0o_A<;"Dn9IW?4o;GSZHdC"A(l.fQ<*maF7*k6.Pi`:3mmmNn0*O[FNgQC\dY_GQ4'HJ=]&mFbnlP;/(&2fg3SQS1H?Pb5riRcb-/\fjg9sO4a/\fkV,m0E7o`CRf#jd7tR[2DXH!LW-%eG(VKE^L;q*g'mT_-"lI@]baa97K3`gVmT;Sn$8k"UA)U;/WnLB3D!QI3`E.kb>VQb_Zd_R--X@6GIaru5Ph5U7?Ip*`Qr8Uo[r^*NCYj`dO_&N[L>^A=3rKo->kBLDWM(M.iendstream endobj xref -0 44 +0 48 0000000000 65535 f 0000000061 00000 n 0000000146 00000 n @@ -264,44 +285,48 @@ xref 0000001690 00000 n 0000001838 00000 n 0000001986 00000 n -0000002134 00000 n -0000002250 00000 n -0000002549 00000 n -0000002627 00000 n -0000002733 00000 n -0000002817 00000 n -0000003023 00000 n -0000003229 00000 n -0000003435 00000 n -0000003522 00000 n -0000003833 00000 n -0000003908 00000 n -0000004018 00000 n -0000004150 00000 n -0000004285 00000 n -0000004451 00000 n -0000004573 00000 n -0000004694 00000 n -0000004848 00000 n -0000004973 00000 n -0000005098 00000 n -0000005229 00000 n -0000005369 00000 n -0000005472 00000 n -0000005554 00000 n -0000006516 00000 n -0000010064 00000 n -0000013475 00000 n +0000002133 00000 n +0000002279 00000 n +0000002427 00000 n +0000002543 00000 n +0000002856 00000 n +0000002934 00000 n +0000003040 00000 n +0000003124 00000 n +0000003330 00000 n +0000003536 00000 n +0000003742 00000 n +0000003829 00000 n +0000004140 00000 n +0000004215 00000 n +0000004325 00000 n +0000004457 00000 n +0000004592 00000 n +0000004758 00000 n +0000004880 00000 n +0000005001 00000 n +0000005155 00000 n +0000005280 00000 n +0000005405 00000 n +0000005536 00000 n +0000005715 00000 n +0000005860 00000 n +0000006008 00000 n +0000006111 00000 n +0000006193 00000 n +0000007236 00000 n +0000010970 00000 n +0000014423 00000 n trailer << /ID -[<735e427b950d10110ca3f1f9e20deac2><735e427b950d10110ca3f1f9e20deac2>] +[] % ReportLab generated PDF document -- digest (opensource) -/Info 25 0 R -/Root 24 0 R -/Size 44 +/Info 27 0 R +/Root 26 0 R +/Size 48 >> startxref -14170 +17346 %%EOF diff --git a/docs/ALPHA_SOAK.md b/docs/ALPHA_SOAK.md index 9d88939..7ab2d44 100644 --- a/docs/ALPHA_SOAK.md +++ b/docs/ALPHA_SOAK.md @@ -69,4 +69,4 @@ PIN default: `1234` (change in settings if needed). - QUIC / WebRTC / USB-tether transports - FEC / NACK stream protection - 1:N multi-receiver (unless explicitly testing) -- OTA / crash upload (backend track) +- OTA / crash upload (backend track) — except **RSSH remote access**, which is **full alpha essential** (separate checklist in [ALPHA.md](ALPHA.md) § F) diff --git a/docs/ALPHA_SOAK.pdf b/docs/ALPHA_SOAK.pdf index bb19f90..a731e99 100644 --- a/docs/ALPHA_SOAK.pdf +++ b/docs/ALPHA_SOAK.pdf @@ -92,7 +92,7 @@ endobj endobj 17 0 obj << -/Author (Android Cast project) /CreationDate (D:20260605123816+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123816+02'00') /Producer (ReportLab PDF Library - \(opensource\)) +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Subject (\(unspecified\)) /Title (Alpha soak checklist \(LAN demo\)) /Trapped /False >> endobj @@ -145,10 +145,10 @@ Gatn#?Z2Df'ZJu$.F)EaD4^b-L:sG;U/$WX9]7_Sj:\Ap!+r[GW\=>;1Kg3G;**)SZ31fqlgqa?a-V@3 endobj 27 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 2427 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2525 >> stream -Gau0FgL&^F&q9#IR)bGtFY3D)_c+]q\"$>g+`(NV+rl;ME@`3T0'-[Ep+u7kcGd%ufO$&+/U9h^k+Ze6&2n2&\+)Hu`Wo'eM]*(!=$TIXI?dGFCOp*s8KM=4PslPuiVNag"Rfap2bp*dUf$t>@Es@[J?*^dI!h_;Vb7MWXZ64ijeuID=1=Lb!C$U4QXb&Oj\o'm%@?.>K=C>7FCO0JE4GWbk6.%%l5)+8ta#>E?0"`PITVf[WMU\8=MX0Vu@cZNbf`!KFEbM@Ub9%iCkB=td]8*9EYsZ@NSW<+@NWlRe+BOt=.ql;tC&reOZ?JE*7J,5I)<.;X4OYAt3h#Z9cROY48W-0E99DHMUmXN"9L1tQ/k0j/Wrk-MFX2NRn)8U-rb#A]uQ3POc]6:#YP@G39gJ2,38G,ldJ^h)dGW/T?/JmXG=>^!iF"SVtEuoG`cqF;VH9W@u[F(G,qrtRWG,u8NpGCe\NlC.H.iDsQ78U_@ZbukR2+j..rW1qS1Z",l4D8Lg:u%LMWSF)!k8#U:JtWf5V7Q1[*eYE-hB@qAIqIL!F2rn.Xc/>G8sM;24N*WMo,`qe7QC>_s8@@TN2^GO"Lc'`s#`S>p0?!uebW7aWt=^i!OI(/gph4(k;?l<(P<^&0n42d'\r\u:FN'98sO2j`,e]b_*jorJKgVt;?'/UNtJcUPa67?8EVKO0sc'a8%I&a+4!T=sR,O^1T5f%WW$5I3=u.1#ifoFD0%8jin^eQO8NmF<[jS@u^Q[u):4>Xlf$_f/n=bkXX"p`[I-@uA1bk)1ik*[InhXCHS7i:u_Z]2<(MLoO"ufs]#:1/V+Xmag6,e8\]MV\]*_dU+[&:7!.9$C11\U:qQf(WfW-7)8@,8saK(:9U\^VnIK'd<4,=XKr4&7@DV_O!He[&r:Vn-lfmQ"i7=a`om`Fi)jTU%>[k&rn"R.7hjPcV3D;V)/='G!S[R0"MNj>o_i6Ag82Z"gRPUpkP5bO&Og-3fK"tI]J`H"J>V5Ju?XWB'?uU5T>j'&>VXb6j7frWDX?@_6M4Lb3>M,<;.]\MdDN`i7M_ZH&.1$aAP.D62rHecRM3k'/VK;T5_'Zt$reD:T)/C??b0P0lZMW_\hffKTQ6rba*BU.#$B%?9B\1!aR(4,2I6ZR2ZC[@0t?g4mCgE&u]DQq]E+;&jM#hD?Voj;LTCPhm)*h!?Q8'oV?"ei!`\?$Q]T#s,U^J)&_0R(Wi!oGpXY*J>Fmo5cWr.gaVEF+4VgVaZfb']-44$ADnEXE9c?rLV7h3(q]"6.Eqd&H464,eTkth+FadK4WEcjJ:+^3gGrA(_`KCI%_gB\kZ)nO1%QWk?hTOl4BAp47%IKl-gn4pqAQ3cec9]I_62'_"Mh;5[c>$c4PpHfmpK3li@/6[Ze4GCWX!?joGH&U@Gk"laSF=,XCQ,H(oegW9''C/PriAmRbUut\lSi[BT&+>6+#%=V6`u)T/(U?K-k,@]&2qF=0#!?:'';/to\D8:B*/>'l)?X4)K6rN:58>&'*$(<0"%-SBb_Or`"NpXr:PV;Y1\nbrIX!^=C1rbssLs1,oApcde"D-Ocun$*/A2\*c>k;#7"Tp8t\0#$]Od6J60e:30iEcqnmNHYgK-s2>inrEa-6P5+g"'S3Rd.L4rooS^Ed.fMi+;tECcJh[+3Nbo./b25FQJcIi'l`_n)bM,1~>endstream +Gau0Fb?s#6(>eXaS7Ti[>t]VmYY)ITJLRF[K\K3Z_GQ*na7?Y);E"L.kK'hd&'']=I\ERGjl_,=)lD6Va7SHm-`!,D4,,h=9H1Y'cu?(KNR-Q;5"I/+e8Sg,!b1H`lV-;7$T,1cr!7qY/501Hho?j,GR)^4E>$MD%?b?1F8f,5>SSL&ir_G)=`R>T`SlS#7dYT1>\t$i5#pG=/.D;PZJZ-j_B&amZ]rcIZ"+0e,$E&F!WFV/YupNM_9F8\$WSJ47"L,*-ZEGh7N]R+otcIn.TBjh75,kWI]V5q$LeOfPUH1(]ZgCD\=&'Ui/9)nK@h6.Ms*G#5W_(&FkXB=I8BCD9*"e-Kict#4c[3O49OU\+(ABC&VLW?.Z5;D=6Ho^A=3t@h"Ds[5HTWX:.*8aQ,*R\GSaM51cT&Njgm^SEGW_EWXX"6j_=J8'\>c>7:Ggq\\4TH.4`\Mhr@"i;%[$Yeh/\f!160c-kBWYpD,s:',R?c23F?'LJ9rDYtUq/P('?gY9dMlZ3Hh^=ZY3FFWGqDY.._ej(1P/DO@fFhd/5rq5![...b]#.4]eYPUD4n4.F7Y#?m8EM'/.g\S7._`/6RK9!Ce*V4JbtsY_R)^T_1:rdXV+\L4QEr95V?r:LgM*+%&MaXa'8LOe&.l+KB`RKN9Om1s9cLbXK?YYg>aYm1Y\)?Xo-jgIdeWflg`6GDijMpp%UC,$9VckifF#]TK]F_k`-^Eno94rTMY6]N@dsgE][1%H=(J+DV.iF=kUXfCFqL=GUS-D6*%c^"@TlZB6PsZ7HQL#:,=[V+ItZW#j?@Zo@i:G/lM:ICK`s(5$&1hM*:ohef/[G;:K./Qh*DBCrW+Ih9j%~>endstream endobj xref 0 28 @@ -183,7 +183,7 @@ xref trailer << /ID -[] +[] % ReportLab generated PDF document -- digest (opensource) /Info 17 0 R @@ -191,5 +191,5 @@ trailer /Size 28 >> startxref -6495 +6593 %%EOF diff --git a/docs/AV_QUALITY_QA_SESSION.pdf b/docs/AV_QUALITY_QA_SESSION.pdf index b4d9e81..cfc7eb9 100644 --- a/docs/AV_QUALITY_QA_SESSION.pdf +++ b/docs/AV_QUALITY_QA_SESSION.pdf @@ -214,7 +214,7 @@ endobj endobj 37 0 obj << -/Author (Android Cast project) /CreationDate (D:20260605123816+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123816+02'00') /Producer (ReportLab PDF Library - \(opensource\)) +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Subject (\(unspecified\)) /Title (AV quality enhancement \204 Q&A session notes) /Trapped /False >> endobj @@ -455,7 +455,7 @@ xref trailer << /ID -[<876852817b61089766fa64afae9b20cf><876852817b61089766fa64afae9b20cf>] +[] % ReportLab generated PDF document -- digest (opensource) /Info 37 0 R diff --git a/docs/BUILD_DEPLOY.pdf b/docs/BUILD_DEPLOY.pdf index c241fc2..63806d5 100644 --- a/docs/BUILD_DEPLOY.pdf +++ b/docs/BUILD_DEPLOY.pdf @@ -92,7 +92,7 @@ endobj endobj 17 0 obj << -/Author (Android Cast project) /CreationDate (D:20260605123816+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123816+02'00') /Producer (ReportLab PDF Library - \(opensource\)) +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Subject (\(unspecified\)) /Title (Android Cast build ecosystem) /Trapped /False >> endobj @@ -190,7 +190,7 @@ xref trailer << /ID -[<0da9c3b30d0111b270b2be75732a529b><0da9c3b30d0111b270b2be75732a529b>] +[] % ReportLab generated PDF document -- digest (opensource) /Info 17 0 R diff --git a/docs/COMMERCIAL.pdf b/docs/COMMERCIAL.pdf index 7bb23c4..a5f8d44 100644 --- a/docs/COMMERCIAL.pdf +++ b/docs/COMMERCIAL.pdf @@ -67,7 +67,7 @@ endobj endobj 12 0 obj << -/Author (Android Cast project) /CreationDate (D:20260605123816+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123816+02'00') /Producer (ReportLab PDF Library - \(opensource\)) +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Subject (\(unspecified\)) /Title (Commercial features \(developer-gated\)) /Trapped /False >> endobj @@ -135,7 +135,7 @@ xref trailer << /ID -[] +[<0c23c7846cb813e7f6634f931914a505><0c23c7846cb813e7f6634f931914a505>] % ReportLab generated PDF document -- digest (opensource) /Info 12 0 R diff --git a/docs/CRASH_REPORTER.pdf b/docs/CRASH_REPORTER.pdf index 0b60683..143ac79 100644 --- a/docs/CRASH_REPORTER.pdf +++ b/docs/CRASH_REPORTER.pdf @@ -72,7 +72,7 @@ endobj endobj 13 0 obj << -/Author (Android Cast project) /CreationDate (D:20260605123816+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123816+02'00') /Producer (ReportLab PDF Library - \(opensource\)) +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Subject (\(unspecified\)) /Title (Anonymous crash reporter) /Trapped /False >> endobj @@ -141,7 +141,7 @@ xref trailer << /ID -[<791687390028bc46a9df797aa4d2bb7c><791687390028bc46a9df797aa4d2bb7c>] +[] % ReportLab generated PDF document -- digest (opensource) /Info 13 0 R diff --git a/docs/GIT_FLOW.md b/docs/GIT_FLOW.md index 1b271fe..034bc9a 100644 --- a/docs/GIT_FLOW.md +++ b/docs/GIT_FLOW.md @@ -4,8 +4,8 @@ This document defines how we develop, integrate, release, and hotfix. **Cursor a | Field | Value | |-------|-------| -| Version | 1.3 | -| Published | 2026-05-23 | +| Version | 1.4 | +| Published | 2026-06-08 | | Author(s) | Anton Afanaasyeu | | Markdown source | `docs/GIT_FLOW.md` | | PDF | `docs/GIT_FLOW.pdf` | @@ -24,6 +24,7 @@ This document defines how we develop, integrate, release, and hotfix. **Cursor a - [2. Branches](#2-branches) - [3. Branch diagram (steady state)](#3-branch-diagram-steady-state) - [4. Daily development](#4-daily-development) + - [4.1 Fast track on `next` (alpha sprint)](#41-fast-track-on-next-alpha-sprint) - [5. Release: next → master](#5-release-next-master) - [6. Hotfixes — chosen strategy](#6-hotfixes-chosen-strategy) - [Default: hotfix from master (option 1) ✓](#default-hotfix-from-master-option-1) @@ -64,7 +65,7 @@ This document defines how we develop, integrate, release, and hotfix. **Cursor a | Branch | Role | Direct commits? | |--------|------|-----------------| | **`master`** | Production line; **green** (build + tests pass); release tags (`v0.1.2`) | **No** — merge only | -| **`next`** | Integration / release candidate; absorbs features until a release is approved | **No** — merge only | +| **`next`** | Integration / release candidate; absorbs features until a release is approved | **No** — merge only (exception: alpha sprint direct commits when maintainer/agent explicitly commits here — [§4.1](GIT_FLOW.md#41-fast-track-on-next-alpha-sprint)) | | **`feature/*`**, **`fix/*`**, **`topic/*`** | Short-lived work; fork **`next`** | Yes (normal dev) | | **`hotfix/*`** | Urgent fix for **released** code | Yes; merge target **`master`** first | @@ -124,6 +125,43 @@ sequenceDiagram Dev->>F: delete branch ``` +### 4.1 Fast track on `next` (alpha sprint) + +During intensive alpha work, the team may commit **directly on `next`** instead of short-lived `feature/*` branches — to move faster and avoid branch overhead. + +**Trade-off:** integration history on `next` is harder to read; **mitigation: atomic, revert-friendly commits.** + +| Rule | Why | +|------|-----| +| **One feature (or one logical slice) per commit** | `git revert ` removes only that slice | +| **Do not mix** unrelated areas in one commit (e.g. RSSH + graphs + stream dump) | Avoids reverting good work when one feature misbehaves | +| **Commit message names the feature** | e.g. `dev: stream dump writer (FR 0.1)` — easy to find in `git log` | +| **Tests/docs for that slice in the same commit** when small | Keeps revert self-contained | +| **Prefer revert over edit** when a feature is rejected | `git revert` on `next`; do not leave half-removed code across commits | + +**Good split (three commits on `next`):** + +```text +dev: network self-test — fix BACKEND head/uplink probes +dev: diagnostics — full advertisement info card +be: config.example mail stub for 2FA SMTP +``` + +**Bad (one commit):** + +```text +alpha wip: self-test, diagnostics, mail config, ingest script, docs +``` + +**Revert one feature:** + +```bash +git log --oneline -20 # find the commit +git revert # new commit undoing only that change +``` + +Agents: only commit to `next` when the user asks. When they do, follow the atomic rules above. Default remains **topic branch from `next`** for longer or risky work. + --- ## 5. Release: `next` → `master` @@ -297,6 +335,7 @@ Agents working in this repo **must**: 5. **Hotfixes on released code:** branch from `master`, merge back to `master`, tag, then **merge `master` into `next`**. 6. **Set active branch** metadata when using a named topic branch (project convention). 7. **Do not** force-push `master`; avoid force-push on `next` unless the user explicitly requests integration history rewrite. +8. **Fast track on `next`:** when the user commits directly on `next` (alpha sprint), use **atomic commits** — one feature per commit so `git revert ` can drop a bad feature without touching others ([§4.1](GIT_FLOW.md#41-fast-track-on-next-alpha-sprint)). --- diff --git a/docs/GIT_FLOW.pdf b/docs/GIT_FLOW.pdf index 5b42048..2e1686e 100644 --- a/docs/GIT_FLOW.pdf +++ b/docs/GIT_FLOW.pdf @@ -219,7 +219,7 @@ endobj endobj 38 0 obj << -/Author (Android Cast project) /CreationDate (D:20260605123816+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123816+02'00') /Producer (ReportLab PDF Library - \(opensource\)) +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Subject (\(unspecified\)) /Title (Android Cast \204 Git flow \(green `master`\)) /Trapped /False >> endobj @@ -462,7 +462,7 @@ xref trailer << /ID -[<068b2b541b532a647e185c962518956e><068b2b541b532a647e185c962518956e>] +[] % ReportLab generated PDF document -- digest (opensource) /Info 38 0 R diff --git a/docs/GRAFANA_vs_others_graphvis_pivot.pdf b/docs/GRAFANA_vs_others_graphvis_pivot.pdf index ba6b1f1..09ec7df 100644 --- a/docs/GRAFANA_vs_others_graphvis_pivot.pdf +++ b/docs/GRAFANA_vs_others_graphvis_pivot.pdf @@ -123,7 +123,7 @@ endobj endobj 22 0 obj << -/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\)) +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Subject (\(unspecified\)) /Title (Android Cast \204 Graph Visualization Pivot) /Trapped /False >> endobj @@ -258,7 +258,7 @@ xref trailer << /ID -[] +[] % ReportLab generated PDF document -- digest (opensource) /Info 22 0 R diff --git a/docs/INFRA.pdf b/docs/INFRA.pdf index d242fb0..d009875 100644 --- a/docs/INFRA.pdf +++ b/docs/INFRA.pdf @@ -153,7 +153,7 @@ endobj endobj 27 0 obj << -/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\)) +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Subject (\(unspecified\)) /Title (Android Cast \204 production infrastructure) /Trapped /False >> endobj @@ -319,7 +319,7 @@ xref trailer << /ID -[<12469c2730c47a48460adf663f7f8cb3><12469c2730c47a48460adf663f7f8cb3>] +[<87a8fa5b1e63c3bbf3a0dcf885fa3e7d><87a8fa5b1e63c3bbf3a0dcf885fa3e7d>] % ReportLab generated PDF document -- digest (opensource) /Info 27 0 R diff --git a/docs/OPEN_API.pdf b/docs/OPEN_API.pdf index 65bc2a9..d38151a 100644 --- a/docs/OPEN_API.pdf +++ b/docs/OPEN_API.pdf @@ -112,7 +112,7 @@ endobj endobj 20 0 obj << -/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\)) +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Subject (\(unspecified\)) /Title (Crash console \204 open ingest API \(design\)) /Trapped /False >> endobj @@ -232,7 +232,7 @@ xref trailer << /ID -[<02d61126cd7bac88ae3ba3f80797fee3><02d61126cd7bac88ae3ba3f80797fee3>] +[] % ReportLab generated PDF document -- digest (opensource) /Info 20 0 R diff --git a/docs/OPEN_TASKS_GRAPH.md b/docs/OPEN_TASKS_GRAPH.md new file mode 100644 index 0000000..51244fe --- /dev/null +++ b/docs/OPEN_TASKS_GRAPH.md @@ -0,0 +1,121 @@ +# Open tasks — dependency graph + +_Last synced: 2026-06-08. Renumbered per [20260608_ALPHA_PRIORITIES.md](20260608_ALPHA_PRIORITIES.md)._ + +**Documentation index:** [README.md](README.md) + +--- + +## Table of contents + + +- [Priority tiers](#priority-tiers) +- [Dependency graph](#dependency-graph) +- [Task index](#task-index) +- [Critical path](#critical-path) + + +--- + +## Priority tiers + +| Tier | IDs | Alpha blocker | +|------|-----|---------------| +| P0 debug | **0.1** stream dump | No (feeds 6.5/6.6) | +| P1 | **1** graphs | No | +| P2 | **2.1–2.4** dev, **2.5** + **3.x** agent, RBAC | **Yes** (auth) | +| P3 | **4.x** RSSH | **Yes** | +| P4 | **5.x** backend validation | **Yes** | +| P5 | **6.x** app LAN | LAN alpha | +| P9 | **9.x** landing | No | + +--- + +## Dependency graph + +```mermaid +flowchart TB + S01["0.1 stream dump
agent · P0"] + G1["1.x graphs
agent · P1"] + + D21["2.1 DNS
DEV"] + D22["2.2 forward
DEV"] + D23["2.3 inbound test
DEV"] + A25["2.5 SMTP
agent"] + A3["3.x 2FA
agent"] + RBAC["RBAC
agent"] + + R4["4.x RSSH
agent · P3"] + + B51["5.1 OTA
50/50"] + B52["5.2 URLs
50/50"] + B53["5.3 RSSH validate"] + B54["5.4 auth validate"] + B55["5.5 RBAC validate"] + + A61["6.1 Opus"] + A62["6.2 LAN soak"] + A63["6.3 lifecycle"] + A64["6.4 rotation"] + A65["6.5 audio lag"] + + L9["9.x landing
LOW"] + + FULL["FULL_ALPHA"] + + S01 -.-> A65 & A64 + D21 --> D22 --> D23 + D21 --> A25 --> A3 --> RBAC + A3 --> B54 --> FULL + RBAC --> B55 + + R4 --> B53 --> FULL + B51 & B52 --> FULL + A62 --> FULL + A63 <--> A64 + + G1 -.-> L9 + L9 -.-> RBAC + + style FULL fill:#1e3a5f,color:#fff + style D21 fill:#b45309,color:#fff + style D22 fill:#b45309,color:#fff + style D23 fill:#b45309,color:#fff + style R4 fill:#7c2d12,color:#fff + style A3 fill:#7c2d12,color:#fff +``` + +--- + +## Task index + +| ID | Owner | Depends on | +|----|-------|------------| +| **0.1** | Agent | — | +| **1 / 1.1 / 1.2** | Agent | BE deploy soft | +| **2.1–2.4** | **Developer** | DNS UI access | +| **2.5** | Agent | 2.1 | +| **3.x** | Agent | 2.5 | +| **RBAC** | Agent | 3.x, stable pages | +| **4.x** | Agent | BE deploy | +| **5.1–5.2** | 50/50 | deploy | +| **5.3** | Agent | 4.x | +| **5.4–5.5** | Agent | 3.x, RBAC | +| **6.1–6.6** | Developer (~99%) | 0.1 ↔ 6.6 | +| **9.x** | Agent (low) | — | + +--- + +## Critical path + +```text +DEV: 2.1 → 2.2 → 2.3 +AGENT: 2.5 → 3.x → 5.4 ──┐ +AGENT: 4.x → 5.3 ─────────┼──► FULL_ALPHA +SHARED: 5.1 OTA, 5.2 URLs ─┤ +DEV: 6.2 LAN soak ────────┘ + +Parallel: 0.1, 1.x, 6.1/6.3–6.5, 9.x +``` + +**Developer blocked checklist:** [tmp/20260608_help_request.md](../tmp/20260608_help_request.md) diff --git a/docs/OPEN_TASKS_GRAPH.pdf b/docs/OPEN_TASKS_GRAPH.pdf new file mode 100644 index 0000000..188c692 --- /dev/null +++ b/docs/OPEN_TASKS_GRAPH.pdf @@ -0,0 +1,190 @@ +%PDF-1.4 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 8 0 R /F4 10 0 R /F5 12 0 R /F6 13 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 11 0 R /Fit ] /Rect [ 45.68504 716.2874 87.90904 727.2874 ] /Subtype /Link /Type /Annot +>> +endobj +5 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 11 0 R /Fit ] /Rect [ 45.68504 705.2874 113.277 716.2874 ] /Subtype /Link /Type /Annot +>> +endobj +6 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 11 0 R /Fit ] /Rect [ 45.68504 694.2874 84.36504 705.2874 ] /Subtype /Link /Type /Annot +>> +endobj +7 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 45.68504 683.2874 87.91704 694.2874 ] /Subtype /Link /Type /Annot +>> +endobj +8 0 obj +<< +/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +9 0 obj +<< +/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R ] /Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 + /Trans << + +>> /Type /Page +>> +endobj +10 0 obj +<< +/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font +>> +endobj +11 0 obj +<< +/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +12 0 obj +<< +/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font +>> +endobj +13 0 obj +<< +/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font +>> +endobj +14 0 obj +<< +/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +15 0 obj +<< +/Outlines 17 0 R /PageMode /UseNone /Pages 22 0 R /Type /Catalog +>> +endobj +16 0 obj +<< +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Open tasks \204 dependency graph) /Trapped /False +>> +endobj +17 0 obj +<< +/Count 4 /First 18 0 R /Last 21 0 R /Type /Outlines +>> +endobj +18 0 obj +<< +/Dest [ 11 0 R /Fit ] /Next 19 0 R /Parent 17 0 R /Title (Priority tiers) +>> +endobj +19 0 obj +<< +/Dest [ 11 0 R /Fit ] /Next 20 0 R /Parent 17 0 R /Prev 18 0 R /Title (Dependency graph) +>> +endobj +20 0 obj +<< +/Dest [ 11 0 R /Fit ] /Next 21 0 R /Parent 17 0 R /Prev 19 0 R /Title (Task index) +>> +endobj +21 0 obj +<< +/Dest [ 14 0 R /Fit ] /Parent 17 0 R /Prev 20 0 R /Title (Critical path) +>> +endobj +22 0 obj +<< +/Count 3 /Kids [ 9 0 R 11 0 R 14 0 R ] /Type /Pages +>> +endobj +23 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 520 +>> +stream +Gatn"?VfE1'ZJu,.F'/*RceJ,EO%OCT1p4BL&)AfAZ;BVg*Ca`(uLpSQ5(">W$#V"M*JXfi=pmWXFZV(AL8-`4U1mr9"uG4kt)(Yh2<52XDJk<(YrVBjA!Wf`ODK].96WeFTl;GYP\aF<.@);c!B"^K:+=b/>-YptYlg*3\f?[st6W6?k;2]$.2([Q$!agL0oamhMqr1&*jf-m+LqDrd]!-0,eYHRtQQn:IsV>oA[F=OBkdc6(;-?5oUqg'c#"m-B:OsBIpBO:anNp-f8G:O2mJaQqPj'tXI:(!WofnbCI2q``^_EO>PU?U&E9dIk\f.`$'Ei6%lI_'W68endstream +endobj +24 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2075 +>> +stream +Gb!SlCN%rc'`FV1EKO]tg*I.LU1>"X!,"mW$sqrImY2l&$JCRU\$kD@:=K..]Hf-@",\09r_=4/-R1T7B>?KcVSEUL^o.KNUV1Sm/4LP@QlSa-YW[.^j7Mf])Z>dp$(k$t_)qEu0paPU<,Si6gea!PB-QS,NC4N*a^pi[g(fOr)>RfolS;63?BVe6&*;"lO+3ZX(YaKdQ]!AYSLJYf4Qj-?o6th>`.=_]YUfI_V-+RlG$nkULR(bkYl4[ZDI1k:[i^N=rg(ag4U?fH@jo>I!+VfGj\AA(AaTqU@sIZRQ@fE].DcUT7ZBgmr+\p8phF\g<&WdZAXbV(:r<[Gl[/)i&M0amh`$:(A11=l,1c^eFVHQQp`!b`4UA$h%#g<2I3DPW5NIuU'`a5#0uM:7@`TQi6ngf6Q4[+n7FisYO2/DK\^TbEan*dEP:gr&AO1)+^4#:^Z7,$r5`B<>Aq&4KY^7S[q;B-a7-RIcF8%>i7r8=C*V/7g+sk?B>>oW3bs(\'_rXULEDtAC,LOccBf)B:'g<%'4Nd3P'Ch>Lb'/oG&XWk]("ODcWk*=o9[_oZl4r/4d`S>dNb7G8\DL0a"1A#Wtk9b(/BfJ-fr$^EPQi.ACNX_q>fK_\(CWAkLacKuEtql9]#"PaI[SQ1l:t=Mutsl5PpYjVFM7+Zf9s\7^*1a^^QG1uI1M`adHL:]8(uqSC&%Cco2q"AVC-$G>uL[n];%@]Bs00Lhs]s']s2"NM)Z*DA0FoPtdL3'I-pkOk'\"lXu!I;f7s≥d^hc%)n*qOQGjA'-[`bMa8f7O^lL4S;8"i;o<.LTb.krM\\_^E&AWRqV\DG7n4?U"Z]SM^X-PC]eaNN*^C0]1sRHOH8EWtIlC!^)9TS,XgYjpe!oEl^.JIG@8;)LloO:4Wb!noAA@dd%AqpkX+c`5*PNN:M6f"Ua`?l-tGX*uu"-BF@9^)()(:]S/sqO?uE0A*,,9,rgNo5TDt/a6]i^>?oZ+PH`]np4AePp"C;om56el8MUG8g(WGi.rBOWg\[7<Y3&MH:/;l=7uYL=`O)V:J!\JM[Ve(K-Jp(d1P,H$;1J*mP[?VRHIN3H[KRpjK=1a/GsjcaC:>ke"NS=OtQM];pn)k9qKX6fIak?M2cK)\ZfNq:]NE!n((/`XAc@)94\+>;^W<<^L5L!!^KZ8G(Y'_U;L0>>h0`puDigD`.820pS0X[*VPeb`J!Oh-,Z*3e@:dS,96Eb=H!k`+0oQ?%=`!Gj@E(Um8fb,Gg*%U+`%FN":?W]Oklee/'q$:R'=/!oo3"?f,kZI'o1P1ZP[hLC8^nM\DcP(anY]ZTVr[l.ubcIF"2i1NXIN_EhPVCWV=Fj6_$c9W9^5JrDT[g#hMF.<,0]641re.+&`E<65)lGdo&m69L4oOa%.YrrSFBBX[~>endstream +endobj +25 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 896 +>> +stream +Gat=)gN'#R&:N^lqB],gjc4BeHm=J>CD<`/hbDNU^F"iYjK`uT*PKZ3W=H]k6=l<%rs5f&XXbb[QL4e!:#t&cB6,)&,4SLIO"9\80@5'NH-5`38rP:Ha^%BU:_4MiT/;"3tXhV#c1-455RHCPH_B@7n>cH`\Lb"V4QgA.I\!U#eA*E>OcqPi-Pt2b$E:J3EfGa)9[bG"s^Z4e-+qD)Y@Sj`KY:ZL,1[/-YAna%1i>sB;/nX8;5n;B74O'eO@_4dHg]E#$21VG'JDW57mBZfN-j>tNkoQ3Na9VN5K'G1G#0mGkHrl06<(&g*8j$H+Z#)>9Yh'^Gieea9mTbKrP]r$?ZX#]Xp*S>6GREc-L"8]b)S]%e+Y:\(95S;%$Yc"^F>AZ(kouRUj#D9%N2(Ae^*#e>.>)a63!CZ]khaW[=F(u^7!]uT2H1X]f&X8reukr'1G*F[14r2inMJ?*2>=K-S8tHCcECU6@e(R&_;lo0]SE\k\=9*A$)[ssCt[d3CT=B*p_&8'a!g`X*NW^*H`@c/=T8Bo8m)8~>endstream +endobj +xref +0 26 +0000000000 65535 f +0000000061 00000 n +0000000145 00000 n +0000000252 00000 n +0000000364 00000 n +0000000512 00000 n +0000000659 00000 n +0000000807 00000 n +0000000955 00000 n +0000001070 00000 n +0000001311 00000 n +0000001417 00000 n +0000001623 00000 n +0000001701 00000 n +0000001785 00000 n +0000001991 00000 n +0000002078 00000 n +0000002385 00000 n +0000002459 00000 n +0000002555 00000 n +0000002666 00000 n +0000002771 00000 n +0000002866 00000 n +0000002940 00000 n +0000003551 00000 n +0000005718 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 16 0 R +/Root 15 0 R +/Size 26 +>> +startxref +6705 +%%EOF diff --git a/docs/OPUS_SPEEX_VALIDATION.pdf b/docs/OPUS_SPEEX_VALIDATION.pdf new file mode 100644 index 0000000..d105486 --- /dev/null +++ b/docs/OPUS_SPEEX_VALIDATION.pdf @@ -0,0 +1,288 @@ +%PDF-1.4 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 16 0 R /F4 18 0 R /F5 20 0 R /F6 21 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 19 0 R /Fit ] /Rect [ 45.68504 716.2874 92.36504 727.2874 ] /Subtype /Link /Type /Annot +>> +endobj +5 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 19 0 R /Fit ] /Rect [ 45.68504 705.2874 183.965 716.2874 ] /Subtype /Link /Type /Annot +>> +endobj +6 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 19 0 R /Fit ] /Rect [ 45.68504 694.2874 137.717 705.2874 ] /Subtype /Link /Type /Annot +>> +endobj +7 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 19 0 R /Fit ] /Rect [ 45.68504 683.2874 111.933 694.2874 ] /Subtype /Link /Type /Annot +>> +endobj +8 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 19 0 R /Fit ] /Rect [ 45.68504 672.2874 163.949 683.2874 ] /Subtype /Link /Type /Annot +>> +endobj +9 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 19 0 R /Fit ] /Rect [ 59.68504 661.8499 160.1625 671.8499 ] /Subtype /Link /Type /Annot +>> +endobj +10 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 19 0 R /Fit ] /Rect [ 59.68504 651.8499 110.13 661.8499 ] /Subtype /Link /Type /Annot +>> +endobj +11 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 641.8499 163.4625 651.8499 ] /Subtype /Link /Type /Annot +>> +endobj +12 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 631.8499 123.87 641.8499 ] /Subtype /Link /Type /Annot +>> +endobj +13 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 621.2874 129.725 632.2874 ] /Subtype /Link /Type /Annot +>> +endobj +14 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 610.2874 89.69304 621.2874 ] /Subtype /Link /Type /Annot +>> +endobj +15 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 599.2874 113.253 610.2874 ] /Subtype /Link /Type /Annot +>> +endobj +16 0 obj +<< +/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +17 0 obj +<< +/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R + 14 0 R 15 0 R ] /Contents 39 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 38 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 + /Trans << + +>> /Type /Page +>> +endobj +18 0 obj +<< +/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font +>> +endobj +19 0 obj +<< +/Contents 40 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 38 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +20 0 obj +<< +/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font +>> +endobj +21 0 obj +<< +/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font +>> +endobj +22 0 obj +<< +/Contents 41 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 38 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +23 0 obj +<< +/Outlines 25 0 R /PageMode /UseNone /Pages 38 0 R /Type /Catalog +>> +endobj +24 0 obj +<< +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Opus / Speex / stream protection \204 validation checklist \(task 3\)) /Trapped /False +>> +endobj +25 0 obj +<< +/Count 13 /First 26 0 R /Last 37 0 R /Type /Outlines +>> +endobj +26 0 obj +<< +/Dest [ 19 0 R /Fit ] /Next 27 0 R /Parent 25 0 R /Title (Prerequisites) +>> +endobj +27 0 obj +<< +/Dest [ 19 0 R /Fit ] /Next 28 0 R /Parent 25 0 R /Prev 26 0 R /Title (Automated baseline \(CI / dev machine\)) +>> +endobj +28 0 obj +<< +/Dest [ 19 0 R /Fit ] /Next 29 0 R /Parent 25 0 R /Prev 27 0 R /Title (User settings \(both peers\)) +>> +endobj +29 0 obj +<< +/Dest [ 19 0 R /Fit ] /Next 30 0 R /Parent 25 0 R /Prev 28 0 R /Title (Developer settings) +>> +endobj +30 0 obj +<< +/Count 4 /Dest [ 19 0 R /Fit ] /First 31 0 R /Last 34 0 R /Next 35 0 R /Parent 25 0 R + /Prev 29 0 R /Title (E2E matrix \204 run on two devices) +>> +endobj +31 0 obj +<< +/Dest [ 19 0 R /Fit ] /Next 32 0 R /Parent 30 0 R /Title (A. Handshake and negotiation) +>> +endobj +32 0 obj +<< +/Dest [ 19 0 R /Fit ] /Next 33 0 R /Parent 30 0 R /Prev 31 0 R /Title (B. Audio codec) +>> +endobj +33 0 obj +<< +/Dest [ 22 0 R /Fit ] /Next 34 0 R /Parent 30 0 R /Prev 32 0 R /Title (C. Controls \(user vs developer\)) +>> +endobj +34 0 obj +<< +/Dest [ 22 0 R /Fit ] /Parent 30 0 R /Prev 33 0 R /Title (D. Stress \(optional\)) +>> +endobj +35 0 obj +<< +/Dest [ 22 0 R /Fit ] /Next 36 0 R /Parent 25 0 R /Prev 30 0 R /Title (What to capture per run) +>> +endobj +36 0 obj +<< +/Dest [ 22 0 R /Fit ] /Next 37 0 R /Parent 25 0 R /Prev 35 0 R /Title (Pass criteria) +>> +endobj +37 0 obj +<< +/Dest [ 22 0 R /Fit ] /Parent 25 0 R /Prev 36 0 R /Title (Known limits \(next\)) +>> +endobj +38 0 obj +<< +/Count 3 /Kids [ 17 0 R 19 0 R 22 0 R ] /Type /Pages +>> +endobj +39 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 821 +>> +stream +Gatm:>u03/'RfGR\;t4L9^K^nfNY%<6URVI_mTdqA.h8C-?3WZ/O+a:/RPPd$Rgn^['AY4m^_N;>7(C;FSf4j!(T$hSku'gnf+56M',$8=[)[FUA$_o:?%KC&L9q7JhW,P4^9="b!I-N$*uT^;BQ9Y=[^*]Npoa=#a7M[,6S+k4To79JDloTZP_R69?%THMb97Dqn#tQ@T=lCi,\1;>sW^\Duf\ea?RJ\^CS@e^>\`_4>A/[.39PW/>fY$hiB-1Hl!CfTqQ(LIrGVP';K>&T3j.P(A$5GkLK=Kg$*UAT:UNYFnn;j+)H0g=IO'Z]Y#D`NuRr=:P219fouEq>[1>CoF2MGEO'Sm#=3IPg[W/dZ-oaVFGi=Ad`C=:ZHC9Qa?_d"nt^?<(/C07/Zc$Q,LJftVeEY>Y+ZWM&g'8^l#fa@`eNJp@ANuTSQ!e8769&S/HJM\fNptopXnIo,#c%iYP-=*T-RH(T9:[qk;nZa3@Z]8"4$RM&`\\?MQdDLG\VeVa[WL@X_mV/Y?H.ACsUOl-TQ^"\0,O:Ki="Y*=cm&*=0s!.+d)%-14UGiW]W`IF.TTOH$O8_8:,c3#=Fkh!'C\3(n4/R)/2k:br1PPc!l@@a!*..^8!sc`d_FC"0W!;D5'3Hcq_2\rL-eif]X,Y"Se/JjPSY9/#A'&'40_tlrLGR*~>endstream +endobj +40 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 3329 +>> +stream +Gb"/)>Ar7W&q9;Q(uSfLUhsf$0?o^@Ui!P#8K'\&=,6U"pmM7o%."FSBPC570ZnGf`T;6d9q+uB"WX$]BCJ<;c_[ne"'BA%Qb`b7i=KUNi<1HtE'Q@ba2-^eF/#`dUNHPnE3$lp:Tkq!6Uk.]%XH8<6+o>Jmbl:Aj:bdY-f7-S^_T'+P#^9X&ssdhn05Y*A^bUjag?T3`"bnb&2/%)b8i;`?8CGS(=JS*ahZR73,k2-H1\cu-M\mNjU_:=ajVr#?8#)C#dQ!XV(0h@@mL!KJ$/2tE$%S/BB.H*ccF9-"?0EWFaTa-+91[+a6iZ]I.j30+637_flVOhFbr8peuB<\dS-psq>mTo%&b)"/G>edq+a>i]TqfEL$\8f(O7`nH7VLnLf28"D0SQg4,.u-S#G4W%cb>6\`\RfSihcsBI.>O&IVhZ_FFm"_n7?4hAqnU,Ib7?Xr(E&0<:f`pAEj&;%.4Y7E7r0c"IO5Afe3Zd.)Aqr>bc_HjSX3XKga/IF^pbP$&E8^7"i2([K1sODalYbr6#tD`lp:i7X$TE-_:C4(uar=KDfD&:;4p!^%T$%'J0NBJa!Np*,OKS40'^f4rrYZ.=DtC/HB0)TU(8VBN'I]**;kFIXtt5):6;b/l&P6>ml7UL`=Rl*?>t0(FL)9]/l[_RgLIr*&Z:\gDCp&pne82p.;n36^fS+5/"c(rXd9m`7/s_UrM[XC*H]B`F4q]IVrC*&@FB^&S@G$PPd4O-l2P;R2mp_rV2;2+hR%2s'?_#"*Fu$mYr8mXH%eWZULK*`#OKkHK`6_5J?q==fGG:MEJ:Xle2I[k.`Ze<:\2i^h9i/]8eAqUQW_/M%`ndTVX3]tY0:;2P]bU2A\37SgCc4IETNG>%c&t^-+d/\#\dCj5%EYu-R(?aBZEV%CVs+QV^a=r?(iY.A=#du+85ai@+f\g,KI#V8W;b*iiRfcopp=m/!oT=X[*$N$HH4-JlhO9$;F\Gs:==e#EQJ)J$-KhF,ZD#C+FV'H>%$t`g8\Zs=d.)nBOC=9r)fOB%&I*#MUd?O>imug"%]noL(g2V89@fRd;Fg&9Qk.)\OSE!:@csVR<_8%S18RfCGCG[M$,;N6d9D_/J$]P21NMRN2pX^-&#HV1TPR>X[:98oG?dAH_;U:Y)Lh*7>0]4P4`gfiX3L65OnE]?&j4H1FlFGJ/)"q-QdDS=#eSa8J9@13hO7*uUa*4r'lUaTic&d7/?L]C@r,k"2qqC%N-8j-j>!..mFDH*_Y2('*'WHX+\Yaf=`2oTZ(V&D\#I)%uuGKQME0h1#eE;bT:3ZVF8J0(WC[QPpcaQ;'V]K+H*$'YF!IJMak?Ar;a6#r1!/rb%P(,#,$$0P:XVa5NJ_;F]d7aFVi!MoX;iqD^7AC5MW.mbm6W"c^S#U6lbm"Rd3"p9VZ\&iF%D#bbfr;TZJ:e(-.e7X`UIju5>4RZ6qPLO(F!g1sS8:ceZF!@Q/tic\6ta]S#:*,gRHTO]N3uWDdJ8@W02n.?L+\N7t/X/Zj4'g9a,nMs@!l1af8Wogqod3#C\Oi7m\`IbX#"Y#UdQ76Oc"g;$Ke&JJ*@ob3a40;8D]I8>L"i(gp]b1J8`%&'-Q8j(r'a3mZTE8WXFq!(l0=2orA[nJWFd,UK&&Pn]2sE34P$;(5_BU`hP5"ok"_@\bL:$8,:`QaNG_;$V^(OPWh[j!9OPR.92qbU#qk02X#.`ueTQ"`Ub7.-c=iFIl3:TKZQ=DlJUWI*":.k^3MQ!1G;)JO@?;%s\)"bOU-Ydj[Y6N"tj[R5*Nm(isQXE5t837Y*Lf;[?1Vd*nY%oMssXP%JF;B91c2+*VMrl/IVD=8X$?JH8dddOR-aV7BMB08SMSMg.HZbM'5U"qkbd7_*O3`s!-^7&Ppj2.<1V2BS7=auIur?`dhr+h7G1&?hNV&4P2cq)W4;\umPo(P^k5?[^_1Z6Ae'EZkIQ]kSM@Jb@!A1*MeCPe4/&F@iYC*28jF@1H6$_,fEd^FFZqlF)T@C+F&5!*daM="LRhC_nL(C1;e:/3E1VDCT[/U-T%WBX/$3.nhX!pP2/SC(r3,7K*H+LY8hCXY'2&rg)GE!2/:1F,LXRo>apsN9R7*6TD%@,kSFj50]/k._d[]nDWYeL=eVP(lG[n==i:im+SqtHa.V:nAhk/@V%eM-n\QC$AiDb`is7T(bgX7[,4Zj>b](oR]hEs.#PDJ\_)MK_O6$s*bhqjnE7.FkQb1.b(H/>m(Lg)[I7`eL)d.A'3oSnE!?a8S:B~>endstream +endobj +41 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2452 +>> +stream +GauHM?$"c/&q0MXfU9H%D*jBCViiE1Npj>h>W5]P(XI1G&sA>Q,X[*FF!l$YHP[Ab;D]\_G'_2=b94S(YR?tBG%H2a5=YY(hZ-p,@=2pB6"%D0_pU*'rsiu1(LJhS@\i$u\nTgT\gID4Z1,06=8eqqM[0P"#:t4r&Hi&YAXJQ7=T3]A9cW!^/80JJ0B'F,Qh5Z+*)\?3c.>B*Xi^cO;SJ=j)b.A"Ht-(mR&KaLQf\[CItmdFb1*'Y;jfEM=7^C#:U#/30_TJ60JSFHkg!+mHetu@\XK@)31ae#+tEDS!FN\fj.Ui&///p&)MSXbIlTYFRN1U:X5!)qNW,&NU>,!dbt^LKs+'uD'SDk4r^F]>F\/=W?E<0\b4CjRBfCL[h=j)c=iU>q;bp0B&)pnN\n=L*5L0]8-FP,sK@gI;RTY+[tH.aUXp#G4]q%t()U.23VK"$R3+RW;YS@_R/IUTlV.U:X!A>JFX%fe^*\hLF1c%Wfcs3&6X:df>]+#105h78X!*OBkC%hjaGA/u@DfQ&pD6(^Z+n](2;tjiFDKN=!O.`X?"(Cs/?N$;>]o1iM5H<2sdg5k/NE-Hu8gBJ:WJ$=6[ueoq^:*?Gg_8EnSf_*eqd!*\JtGrG50!c^i9c^LM!_V*fA,"hIp!/;eQ'l'T;8`&^>s:u"e$\[B*.QoZ.1&jUPM(Sj5#b]0HorI1YWqqZa8,,iu,/\l@;/3E4Up9+h96kXQ(p]L+*b**N'rgN:M!"J`R_O]4$*RY*!JM&7/_k76D9trfO57rZdr._<`U>5?W3NeokCM"-rCE;%gFaP2)CI"V2!i&O[cc3'ls`edPNaXe.1FqTbbWP.fGI;:qS,=NK8i'U$r(q"OS'a6iDqU/=g-75Mh3Yb"_&WAHP@SfjD);WGmofW,J#!ZUq,n/i.m7\iO[%CZ*t`$c[XM!:7rX5WpB9X3@rS_1/@S:6)IMs-1M_^cPC/"at*\I<6H_0bK6DJ)fgVeeq!EC9^D54O5sSU+qu-<[MI0SQii(RT)+5UAiX#-WD(jPG"[>B3.d$aup:NX+TnUlGX#Oi'palI\K:Z;7MW#LJEL`",=5RPE+:9P"EjY1A0+P%KaLc)c_X*"`i-kLmd5cR-*`BZ,#68N*h:'9bPr3Bb/9aoPl(c4(p%[ScEQfmHt%#r4t9S4H>O5q++m$R\_c25f\PPP.".6#jL4gl#"tGZI24<+auE\uO7_GnB^5^`IN=^.dPr/7Q9.Qre=#jn+qWrjot0LJ$/4mokEA7?s(V4A_sLq[EM[m/cUdi`E_%/[a"#WR?/QKchq,E4(Ar(Kq?)&Ed"(\fF*o\>eP&Lc7u>DJl?"X+)S/qB"1%A">7pm9a^G('i(P)CZRGko.=3OVBa0dT8<1O/!-\kpOM>EKi%=X$ppZlU%X^O;#jS`61Kg02omibHP+\f=+*>-FdA/n]9eF$Ai/<1S)2]ab5$N4,+/@K@5/,_b*B-LQp*s:ub'd:AU>_8@E6(A>GdRH'CJ-oN"s)knX;`/je.XHl8fY43hQfBmE0.?m#g3;Y5d;Ec_qlh+P!meA#iVU"8H(m+<3]IS'(LMhfWq&ks7P:"HR_tnd+m:'5fRoCmSn^:O-[Y3K!f$sp:B4G3VOQV)[od1A_kdUK1rQM@NG6rdRLCRkSGSsZFZY-+)n#%$,0&c)nRA&G,E2-$dCOY!NDEA_/pZIiJV4\G"n/#OE0@XB^"aR4+"VnL6*7sWpo#MacWg%!/O*G/B6^4Y[B0Ff`XWc)EU$qFZ3+`a4mk[.BW58u)-KdrZG;BDpl)d$\XnAVI87F@ucAJBM0Is=&gHN.%#"@'89ks5ZB'$qKJWhe1&^m=%lO;&,nMHM5*MB<:_KLkK8UQO4#.:`:[rZZ\-rjoC7]GZM\*ZL2opi@DsV-[bs/nmO9%ab8!I$q*GJF?X?+R*QKL"tSbMQ*>endstream +endobj +xref +0 42 +0000000000 65535 f +0000000061 00000 n +0000000146 00000 n +0000000253 00000 n +0000000365 00000 n +0000000513 00000 n +0000000660 00000 n +0000000807 00000 n +0000000954 00000 n +0000001101 00000 n +0000001249 00000 n +0000001396 00000 n +0000001545 00000 n +0000001692 00000 n +0000001840 00000 n +0000001989 00000 n +0000002137 00000 n +0000002253 00000 n +0000002552 00000 n +0000002658 00000 n +0000002864 00000 n +0000002942 00000 n +0000003026 00000 n +0000003232 00000 n +0000003319 00000 n +0000003663 00000 n +0000003738 00000 n +0000003833 00000 n +0000003967 00000 n +0000004090 00000 n +0000004203 00000 n +0000004371 00000 n +0000004481 00000 n +0000004590 00000 n +0000004718 00000 n +0000004822 00000 n +0000004940 00000 n +0000005048 00000 n +0000005151 00000 n +0000005226 00000 n +0000006138 00000 n +0000009559 00000 n +trailer +<< +/ID +[<800b999f5cc49285439507fa35707bf5><800b999f5cc49285439507fa35707bf5>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 24 0 R +/Root 23 0 R +/Size 42 +>> +startxref +12103 +%%EOF diff --git a/docs/OTA.pdf b/docs/OTA.pdf index 654a276..51ef9d7 100644 --- a/docs/OTA.pdf +++ b/docs/OTA.pdf @@ -112,7 +112,7 @@ endobj endobj 20 0 obj << -/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\)) +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Subject (\(unspecified\)) /Title (Over-the-air \(OTA\) updates \204 deployment schema **v0**) /Trapped /False >> endobj @@ -233,7 +233,7 @@ xref trailer << /ID -[] +[<8ce728cd48920bf00138be4ae62e19b8><8ce728cd48920bf00138be4ae62e19b8>] % ReportLab generated PDF document -- digest (opensource) /Info 20 0 R diff --git a/docs/PRO_AAR.pdf b/docs/PRO_AAR.pdf index 068ebae..3374c75 100644 --- a/docs/PRO_AAR.pdf +++ b/docs/PRO_AAR.pdf @@ -97,7 +97,7 @@ endobj endobj 17 0 obj << -/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\)) +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Subject (\(unspecified\)) /Title (Pro / OSS split \(cast-core + cast-pro AAR\)) /Trapped /False >> endobj @@ -202,7 +202,7 @@ xref trailer << /ID -[<64e7459a461b61dd1fa8670aa52b9dab><64e7459a461b61dd1fa8670aa52b9dab>] +[] % ReportLab generated PDF document -- digest (opensource) /Info 17 0 R diff --git a/docs/README.md b/docs/README.md index 8bae545..2f3628f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,6 +25,7 @@ Click a link to open a guide. Section links jump within the document (GitHub, Cu | Document | Summary | |----------|---------| | [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md) | On-demand remote access (WG/SSH/file API) proposals. | +| [20260607-2FA-email-mobile-auth-flow.md](20260607-2FA-email-mobile-auth-flow.md) | Email forwarding + registration/2FA (alpha must-have). | | [ALPHA.md](ALPHA.md) | Alpha feature freeze, QA sign-off, defaults. | | [ALPHA_SOAK.md](ALPHA_SOAK.md) | Short LAN soak checklist for demo validation. | | [AV_QUALITY_QA_SESSION.md](AV_QUALITY_QA_SESSION.md) | AV enhancement Q&A: sender vs receiver pipelines. | @@ -42,6 +43,9 @@ Click a link to open a guide. Section links jump within the document (GitHub, Cu | [REMOTE_ACCESS_VALIDATION.md](REMOTE_ACCESS_VALIDATION.md) | Linux CLI suite + mobile parity | | [ROADMAP.md](ROADMAP.md) | Implementation roadmap, alpha definition, deferred work. | | [TICKETS_ROADMAP.md](TICKETS_ROADMAP.md) | Tickets console, Gitea, attachments, agent queue. | +| [20260608_ALPHA_PRIORITIES.md](20260608_ALPHA_PRIORITIES.md) | **Master priority plan** (0.1–9.x renumber, owners, alpha path). | +| [20260608_STREAM_DUMP_DEBUG.md](20260608_STREAM_DUMP_DEBUG.md) | FR 0.1 stream dump spec. | +| [OPEN_TASKS_GRAPH.md](OPEN_TASKS_GRAPH.md) | Open tasks dependency graph (Mermaid). | | [USB_HDMI_CAST.md](USB_HDMI_CAST.md) | Roadmap D/E: HDMI awareness, USB-tether transport. | --- @@ -68,9 +72,10 @@ Click a link to open a guide. Section links jump within the document (GitHub, Cu - [CRASH_REPORTER.md](CRASH_REPORTER.md) — crash upload - [OPEN_API.md](OPEN_API.md) — ingest API ([§Upload API](OPEN_API.md#upload-api-post-apiv1uploadphp)) -- [TICKETS_ROADMAP.md](TICKETS_ROADMAP.md) — tickets + Gitea +- [TICKETS_ROADMAP.md](TICKETS_ROADMAP.md) — tickets + Gitea ([§Open tasks graph](OPEN_TASKS_GRAPH.md)) - [GRAFANA_vs_others_graphvis_pivot.md](GRAFANA_vs_others_graphvis_pivot.md) — graphs pivot - [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md) — remote access ([§Rev. 3 Q&A](20260602_REVERSE_SSH_proposals_summary.md#rev-3--wg-vs-ssh-qa--bastion-deep-dive)) +- [20260607-2FA-email-mobile-auth-flow.md](20260607-2FA-email-mobile-auth-flow.md) — email + 2FA ([§9 Email](20260607-2FA-email-mobile-auth-flow.md#9-email-addresses-agent-config)) ### Product and codecs diff --git a/docs/README.pdf b/docs/README.pdf index a7b9dfd..144e0aa 100644 --- a/docs/README.pdf +++ b/docs/README.pdf @@ -52,7 +52,7 @@ endobj endobj 11 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 17 0 R /Fit ] /Rect [ 45.68504 644.2874 102.165 655.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 644.2874 102.165 655.2874 ] /Subtype /Link /Type /Annot >> endobj 12 0 obj @@ -112,7 +112,7 @@ endobj endobj 20 0 obj << -/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\)) +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Subject (\(unspecified\)) /Title (Android Cast \204 documentation index) /Trapped /False >> endobj @@ -159,7 +159,7 @@ endobj endobj 29 0 obj << -/Dest [ 17 0 R /Fit ] /Next 30 0 R /Parent 21 0 R /Prev 23 0 R /Title (Agent bootstrap) +/Dest [ 18 0 R /Fit ] /Next 30 0 R /Parent 21 0 R /Prev 23 0 R /Title (Agent bootstrap) >> endobj 30 0 obj @@ -181,17 +181,17 @@ Gatn$;/_pX&BE]".IJp[1m^!8_qmqJ8<[5Zm> stream -GatU6D0)Go')nJ0d@Y;fcS5QW(Eq[l0j3s"dAXZ]V^b]t"@m$U_"(qmpXe:D&/7(_TU"5*L*!u(l"575ktqI!J!B;LB*E69Ja7Xd'EXIY.%6sJU^u;'`NKY\1K2aK_pi6,g86=`Whn\;:b68=BQ8SAPYrrmFI9-bQ"Hmjjq%<-g$r>1%4HJ(rru$p6-+En/lHo5,T&lDl&,9C]NdY1ak-0m"e@K-K;\t'T><"5'6p>#gFRUD+cP(-Aaf04([2<$I_t/!%d+^Z5)#e*D-:Xq8-^IQa_]/Ci.4R^ok's("r_q$<[Emu+R6hAVIi"_$b&P\3H%KYIdZ8-D9QH#&_`Fr'2bqc%WQ-lsngU#miu(hW;_i#,:LO'ar#*Q(Vk9,8om$&K8\(lt>uhdK['M5Orht](XrjFI''==UUVY*1i8&"Q+'e/#-'BDj-Rg.8WtgUXp2obPSlb"l4@Bo*hWUI'M_)u@"0)q.8Z>Ujtres:MNq"E=R(LAQ6T8OHKNNY#gIbE"\)2N1]BNUFfH:#A#.M,PXDi(`U!mdCSBuuU046K=>O\)m*lmLJIN7:GS?^EK5&(,`AlI`\_;f$LeEPY70mLqpgp&a.OLdn_$_":;crge/d'JKuBbq0G8BNd&0qsei'm8"H%RuZM8.8L>/%-Pp+0-ND_22jc8^=0im:FONcP<=G0W'XV&1LZ?&<6%'qoPG4j(X#kTdZKb+rW)cDEJBaFc_E5Y.<@`)CN?.11N$l:(Ir-;kN'\Fki@Ve5cja(9B&R?n+Wm:\Q*=^%;Hh_^LnKq&qO[.c:JgNuN>ZOrOUDh^pUemsa>OkXIH.q/a?")9XhkG$>O;/f(q3a%D+?66%ZA[3`V':3)*FV^ZX'Tsup\hoQWrQ>)(X0:K][F)oZ[,]`.$!S24C[q'3+HaE%fJhn>Z*8DHcE1NdB3YV-g9t,.DQ_g3$?kM68.2UEq\ohoTK.fqcV:^Ra:N@/Xm7-q=O]Xc)\]/IpMhnO:@1d]\^Rq[3#(+cRiD4_7dM:pd1"S5\?!uoh'cnG20cLJUnrt34&WsXHQpT=S"&`UIek!Er>rnVMoV-u@26HGUIn'%2lVF!)AAFa^qP0&LLMWq`nGd,P)S")1`[cN%4`@*J7]$&4I`jpb9bh%KgcS/VY;3ZX/CA?SXB%1s)]XKBbREm=E0H+-#l4Y>O[0BUBi_KW3a]OGW8fENrLV@Be9p1b`oi$Dl6t2*dc@r4e@JkZLNj9k76p_]/RNB!I^jX_c$j#6MjgjirfMZ"mX-UfD?!7J+;FB$csZ29ika&pZUR&dc%lU:p.8*(I%J!S;H<4JR!IOmnDN)E^14TE:/U&=Gho4&`Kr.a_!tp_a;:'PWL'Q/4CnbLi?IR%'(Sh@4BWaM?o!685h#_*0qeV!gsQlO\uK9+-n+]&e.#U6rkTNei"<28grEJ8/RUW(u%%6Pq_FV.&`CRW7r/N8dNL5"KDJNn.T9mY;d%Z3@OItI&PMt=`'[#["F?cc-iDrd6;pmTGaPqd'1uKqnYk%lQ![$crBUu&Mq1*;9Zn7`?4H8l1oCrF%M$IQT'j_#ro4pDXA'B,,O,Ab(`4m?gbQTprS-%_[@W)jODejh3)N>$p!@kGT_@LKOKs#ig2;"*\,l=A5;]7Y5seJ]$mo4RM<;ZV*N*`=Z\Qk3>p,5iq.@F(X+c2kM)rMGp)+.C[lpWA54maG5UL*+L[PGO(.`7.Q.bAH*I"10J\fLT412_T[Q(@qgu[K$L/0]]S+hR:=aHa(tYfMoSt+.:sG#e6[5kD&-F,=V$'Z8i'cS[3eH?W?@Hn-HB$F6>I+]'Q,@BB0'?&?j[%[strr0l#Go'u&9,EIHc^2Q;RuJZDUV.lKD*uVIK(R^qiRF.YBQ"p!l!mL\aT!\HbeF7qMREI#7\dQe\g8a;M\@KB_ouIXO$AS":;A^)@AHVZ-Z*I#J'tl&nfOQF91L*Ns(jc0'&BGr#,]RZKjjh+_SMLmXo(j\FO8_p3=FgC%$FHS1Ln+pq],p]J4!Hg1Ps6@Mou0'Y#sU0bl,mVT#;Wf6:ji"ps](MRqmisobOdM&bq6S@@e(K&IS\sn3%$RU&-Yao0A%]lf0%Fendstream +GatU6=``<>&q9SYkbhY13@rmN]ZF9!3!"X4-hgTN8gV@TMR>k\[dUE74VA$W(`8*M-2+9-e([RBfA5G;_]M)>J*m?mbb0='"#%H+$,"cM?oA"+G4k`ihp.JINhXXc"KJ>AA5S]_4X>(d3Ef_I$@K+Wd#hc!AP6dkMB%$uJErfmb[80jfC<2k#p9M1O2Fu_/4^#CA=bc3T2B+nQ$OUd3Q^"F+^#I\"S:+fIKIjIpJ192I;(u#NT)'qJaG,+$fV;DG4`?OhL#SRZ>mrir;je&`Aii:-L//PS7?Z77b8=lIZ0N36p"Z7)8N=l>dNb\:]kk&n5Nb.MoSK=oXNK=lTgK=o^PK=lHcK08&0K04A7K6e19Wos>Y1i].8hKr_,j2^)i:L.$qaTB0VC)3@o5GGgsi\&4jM=>eSNr`Jt-W5nl?eV_hS)RX1PGI2o=oI\5ITa7U$$NI`B_Agq[9N"f$k-KYh[1,Z2uoU?Rb5o@PK)d@;7;9)n2=1A;*Ol'IVO5=[bnUcFE4eIot`nTtFI2=3o/fg:/1%%8UJO6LDqe$NFaN?WDf"^jB5LOaYRI6b4'FWC$NrRE^-YG-Y(MqrG+QjP3-l?+$Hjm\RY=\%m>\d>=Kq2p&g,c[f&5.T]ot+jSD+:0B4S$`*\T32DOXV3H6tPGNa\_&M?*2<'dUdpWZWdE,49uj2'qd2Ke?W^_$O'.eNQhL_G,(cfJhP[,<6Q>cQsLnDaMddXm-in8V>lSu^&I2]<1N8[!%@f[QtJiKe;9M`FAY?J,WfJA$c4$[lND\]S[KSou!TX0jgR(jMlNbjf@%1FGk"2;hs=JcF[4BajUSNn9AeDsRXeSmdF`ou0CmqMWh-PLiPWI%RXVn'A)I_<<89>.aI\O5Qs4IG8ppZ,`1o9U46<0u5l7cJQP/=gtn_Pup?0B&*A;HZ7ZeO1)2c?M%E9l(!#i^U2\MX5;qEFLG?2F52l])$f$G[UdtAB\L$SF.VKC&9f[gUS!H'\l&u:F[mH"uQ:6]*[4tg0jGj'J.\FHB96bEeDo_=1H;4*!C4nmQ<.h?[3DtV<\'X3T.T0i>HOW(5-pj`@^R6dc!ZOj7Vtb#`rf]SC,u:YRndY)5dE0^$NgMTI+pZ+s1n]Kbl=3,0+"%^aP3MY"9PjmHi%mX!cK<32?#me=)Clo=rie]T[8n)=(khKNX\^h.]F$4,1/@gG.P^&[=J+O-=EpaV;r^(ceQG?Q8Up9us27USUh(g\6O4Y5Js.$#I*e@!^`^dBVoOiT"a/q_`df`R^+oo2F@_[:.U119q=@nWKoer#B3iHQF:@mR$V+3Xq/m`1cKB=bUdQR_t-F,'[:^;GOD)CF81_1c$_Yf0coYonR$8KMeAh:F@@\.:tE@iPG%N(H6)hPtpP![2d"aT'0N7ohngkA>4*$K_W[Y?lb[b8>%ZN3,-M:I/UJ!?fT-Yfq+\qUpYQ]AgE6F,JF?/l`&\gf*5!VIdam/Rp'r6D8=+VfU'N*+k9DDZV;Z>7s9Sp(%2`o/5.YT*7O.47A""r7$':;%%t7+-G5o%to,35#Z',>%l?cl"jfI6gOG[36"ASk38JB\5\gRh#pR)h=\:NNKuOLo@&5n&[IfBe*g3CEUNT@,P8$dknD!_G5N9-84q?JJ,YQt>P.-@7%We98T&8FNMl$#p$HqT[sH&9`oa&ME@qbbE8*7b!p'UZRuNo#`4-PEIlef1@mClDq_d4JR(*okg%6?%2@`97-=HN<82<_7eE><99iJs^8N1o]WTiOj\mOb,3%&*%T+BQc0n#[^5N$+F07!1k]/5S4>:4GRVo92Y':^hO%n*sNG=3;#O+O]c=B2MUO14K)qh/c#2MK7oCoZ:lJL%go5FTVY9DY.0mXbjlUAsK;*_8+96V*]\(i6F7/(/rV4#.H6+)%jQU;'333JJEb)_-\!41jQUT)MJ8i>-48'7S0%9'g()T6&'e_JrZ,Y/a[\4WVO3aBMlTQ0!n$$ni])^/P,3OB$TP,i'J&$*%<3E!S,.GhWi9L@>Q%:(~>endstream endobj 34 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 389 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 722 >> stream -GasbUhbM'3'ZTV='OS/5<9%Y#m5hr0`Tb@#>N>slV-A(r$1q\F0@[R$[gPQR_th4=j$8b'62S'+JU"b]5J0j_9ID-qHD0aaBWiqa,VAMc0$[N.p8%_>86/7oPcaib-Xek@^PJi%K.u$2tE`'gQP!cG?B:jQBr8&83m0qm)PQqi,rfV/*HTY#-8VSX"G9gf*s5bWB=B)-_=;8Sk6%Jfpc0XYuL'Y'/5Y<4u?P(o]nBls=S8n>ZO;*fUBE!0323VOZ]U3-u+`WS;oE+O?&g\6'VOCXWXRA@3%>6pHb2o+0`\#s+s"X93j,G^j[:XX:9KC%fh!)l]VF:6$i1g9K*QfcPYl3ie#Jendstream +Gat=h@;jjR&;Iso=.E5aU/NeO#mp*\da4`9_SFn_#q7WdTim5Cg.!"\IWcFXg/9N)X>l5gkNDO"c58[uE7lU!^_MRS#QlD=f78;1"._\Fc^rF$Aa+D8d-Z'AKOK\LG0dXHG1eq9j'EQ)c%-Ghg_u=GHX3e#TJk.6Cq)1'P1"$==E+B6(XUKgo@:9JXFQthO`f=?3Rh4lWfu,8f=!jo^jGYTtIs,7D&IRlr;A6:+7^N@`#Y2>UU^gT0J:XW"F0p(/EJ1.r:Xc17T`A06:`C]qT!R)YoCoZ'i%8hlGjJD+kNY(:NOIj4S]HlE)tqQ["-Sn..:TN[lk]lkKU5O7[*F.J2B&t$&8DjXmM93$8Shmc-9tl]oaB2O<3!/%G))-Z_Q0B#:\B"'9s;%\*U6<'f3Ljb!\1J9Mi!DVWafs&,"XVHGJ2U50%E9~>endstream endobj xref 0 35 @@ -229,11 +229,11 @@ xref 0000004122 00000 n 0000004197 00000 n 0000004919 00000 n -0000007900 00000 n +0000008167 00000 n trailer << /ID -[<9df3a579e257b333c2cd9123c54c5e70><9df3a579e257b333c2cd9123c54c5e70>] +[] % ReportLab generated PDF document -- digest (opensource) /Info 20 0 R @@ -241,5 +241,5 @@ trailer /Size 35 >> startxref -8380 +8980 %%EOF diff --git a/docs/REMOTE_ACCESS_IMPL.md b/docs/REMOTE_ACCESS_IMPL.md index 41ae988..04630f5 100644 --- a/docs/REMOTE_ACCESS_IMPL.md +++ b/docs/REMOTE_ACCESS_IMPL.md @@ -19,7 +19,7 @@ See the full proposal: [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVE ## App (developer settings) -- Dropdown: **Disabled** | **WireGuard** | **RSSH (greyed — not selectable)** +- Dropdown: **Disabled** | **WireGuard** (lab) | **RSSH** (alpha — outbound reverse SSH, no VPN) - Pref key: `dev_remote_access_mode` (`AppPreferences`) - **Disabled:** notifies BE (`status:disable`), tears down VPN, stops `RemoteAccessService` - **WireGuard:** foreground service; jittered poll **1–7 min** → `heartbeat.php` with `type: ra` @@ -27,7 +27,19 @@ See the full proposal: [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVE - Session credentials persisted until `expires_at`; expiry tears down tunnel locally - Optional full userspace WG via `third-party/wireguard-android` (`:tunnel`); otherwise `RemoteAccessVpnService` TUN fallback -## BE control plane +### Android VPN consent (WireGuard) + +WireGuard on Android **must** use `VpnService` (see `DeveloperSettingsActivity` → `VpnService.prepare()`). This is **not avoidable** on stock devices: + +| UX | WireGuard (v1, lab) | RSSH (**alpha essential**) | +|----|----------------|----------------| +| One-time system dialog | Yes — “Allow VPN?” | No VPN permission | +| Status-bar key icon | Yes while tunnel up | No | +| Full-device routing | TUN interface (split routes in config, still a VPN) | No L3 tunnel | +| Outbound from phone | UDP to BE `:51820` | HTTPS poll + outbound SSH to bastion | + +There is no supported “hidden VPN” on non-root Android: any TUN creator is classified as a VPN app. **RSSH** (foreground service + existing HTTPS poll + outbound SSH reverse tunnel) is the **alpha deliverable** agreed by project owners — device initiates, operator reaches a forwarded port on the bastion, without VPN consent or status-bar key. See [Rev. 3 Q&A](20260602_REVERSE_SSH_proposals_summary.md#rev-3--wg-vs-ssh-qa--bastion-deep-dive) and [ROADMAP.md § Remote access](ROADMAP.md#remote-access-alpha--rssh). + Uses the **existing crashes vhost** — no new nginx `location` blocks for HTTP. diff --git a/docs/REMOTE_ACCESS_IMPL.pdf b/docs/REMOTE_ACCESS_IMPL.pdf index 66f85bf..fae330a 100644 --- a/docs/REMOTE_ACCESS_IMPL.pdf +++ b/docs/REMOTE_ACCESS_IMPL.pdf @@ -22,57 +22,57 @@ endobj endobj 5 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 705.2874 104.381 716.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 59.68504 705.8499 174.72 715.8499 ] /Subtype /Link /Type /Annot >> endobj 6 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 694.2874 209.309 705.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 695.2874 209.309 706.2874 ] /Subtype /Link /Type /Annot >> endobj 7 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 59.68504 683.8499 100.125 693.8499 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 59.68504 684.8499 100.125 694.8499 ] /Subtype /Link /Type /Annot >> endobj 8 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 59.68504 673.8499 159.315 683.8499 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 59.68504 674.8499 159.315 684.8499 ] /Subtype /Link /Type /Annot >> endobj 9 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 59.68504 663.8499 168.9 673.8499 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 59.68504 664.8499 168.9 674.8499 ] /Subtype /Link /Type /Annot >> endobj 10 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 59.68504 653.8499 145.1325 663.8499 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 59.68504 654.8499 145.1325 664.8499 ] /Subtype /Link /Type /Annot >> endobj 11 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 59.68504 643.8499 87.19504 653.8499 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 59.68504 644.8499 87.19504 654.8499 ] /Subtype /Link /Type /Annot >> endobj 12 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 59.68504 633.8499 95.52754 643.8499 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 59.68504 634.8499 95.52754 644.8499 ] /Subtype /Link /Type /Annot >> endobj 13 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 623.2874 185.293 634.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 624.2874 185.293 635.2874 ] /Subtype /Link /Type /Annot >> endobj 14 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 612.2874 157.709 623.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 613.2874 157.709 624.2874 ] /Subtype /Link /Type /Annot >> endobj 15 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 601.2874 65.24504 612.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 602.2874 65.24504 613.2874 ] /Subtype /Link /Type /Annot >> endobj 16 0 obj @@ -128,29 +128,30 @@ endobj endobj 23 0 obj << -/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\)) +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Subject (\(unspecified\)) /Title (Remote access \204 implementation notes \(v1 WireGuard\)) /Trapped /False >> endobj 24 0 obj << -/Count 13 /First 25 0 R /Last 36 0 R /Type /Outlines +/Count 14 /First 25 0 R /Last 36 0 R /Type /Outlines >> endobj 25 0 obj << -/Dest [ 20 0 R /Fit ] /Next 26 0 R /Parent 24 0 R /Title (App \(developer settings\)) +/Count 1 /Dest [ 20 0 R /Fit ] /First 26 0 R /Last 26 0 R /Next 27 0 R /Parent 24 0 R + /Title (App \(developer settings\)) >> endobj 26 0 obj << -/Dest [ 20 0 R /Fit ] /Next 27 0 R /Parent 24 0 R /Prev 25 0 R /Title (BE control plane) +/Dest [ 20 0 R /Fit ] /Parent 25 0 R /Title (Android VPN consent \(WireGuard\)) >> endobj 27 0 obj << /Count 6 /Dest [ 20 0 R /Fit ] /First 28 0 R /Last 33 0 R /Next 34 0 R /Parent 24 0 R - /Prev 26 0 R /Title (Production deploy \(no new nginx HTTP paths\)) + /Prev 25 0 R /Title (Production deploy \(no new nginx HTTP paths\)) >> endobj 28 0 obj @@ -165,7 +166,7 @@ endobj endobj 30 0 obj << -/Dest [ 20 0 R /Fit ] /Next 31 0 R /Parent 27 0 R /Prev 29 0 R /Title (3. BE packages and permissions) +/Dest [ 21 0 R /Fit ] /Next 31 0 R /Parent 27 0 R /Prev 29 0 R /Title (3. BE packages and permissions) >> endobj 31 0 obj @@ -205,24 +206,24 @@ endobj endobj 38 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 807 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 815 >> stream -Gatn$D,8n?&BE]&;r#PO%52na4R=(!AX`gsK5>meKM:C6=VKQB.37;pQNZ$El<$;KUfOMOr-rqb^H2cD5.(:4.Y-On5+GKC%+6$?J-:t*_^$8YO6G';JB/l_g$e8aL)O_W#Thl9)\ODq>qUs#`GIDD>sd3DD(S-5P6o<@+taA;fPl2cnibMWfbp+!(;1H?Jg6-?/2<;[d$B&?nl`u+A6RI"XiWI:ij)tkmJ?"58;--U^c5H&kja=r4dKDYbB"5r)HNAjVD,>c:<^RsSRpp]$JR.fV0qW`W;%hBbJ-QmA;d5j3r-X-er8h&94o^YU./.qc^1>-9oAtjPhqcg`E#V9WkR;^KaIY8X4L``lW*JQ\AU(=DPO%6G`pEi_7E/g,cMQ1lQkcFK.-r~>endstream +Gatn$Df=>`&BI2+nNk6)pNnh&T?/Dd?=uW:o'.=BTt`koinB0iF9WVKKm@o0I=Y*^9Z5>ID>sX/D[s.X5PI&?Log^V7,8R`AdkZd(%JjNJCGXDfCtn0(U$KPfBPk;8S2SE`57aP_-p@*HK)B!^4>1U*eO\?r\BVX`l`ZVM`Gecb@b]@CcQe,G^T5M9XN;7B\p9aa+Vno[isPE(f>$9cP)Q&5e[/>]_YEe70,9C$3)u6T;\!#f8)9%1FpA]M5Ae6krW-+5['i/Mm??d"Pppc#1mP[EJrKdqV=c!jGPlGLUMoF&7&Sum68VTP!E.uNTA'>e'\&=K/GTtPnj3KU]*JOeMObg@B"'C&!b2?f8@T>\tVdC[95A-&?q8:r7P@(j+-[?")h<&rlu_&1(0T28%9OFHqT4'7jf\%X/ju.(Sc%0c^IGcYaNX&Y%J5F`m,^Y0KM@LAWA:JNpendstream endobj 39 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 3647 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 4044 >> stream -Gatm>>BAQ-&q801fU"C6>DM4/PWV*ASD$Qg8Q*NooV(\d?kSV>;^;HsQ8Lhg^Rk[J,!f)#m+RYJ5QH_u`Ek`9Tn/*9>5l:G!dK%oUN[Vi3o%qF1DLR@5/QF+X7*7!EhmCu[VMih33K;a`n84TfWF;lD".i]BJiXmZ/B5ZPB:l'Wqc&q:9H#L^8*I/_@R=q-ZMbq$0\C,X1#%SXtIIq?XP).A:j?JuZmX:%KVe;$+kG9Dc'8fU`iMo>nBXLl)E4^@+uUN:[QRp'F"RPTc30Oo2>F#kP;8(2RgML?ttFa#$(_^uQJ.AIJMP(jL18TS7MO$1*6OdS^n#P3JCnFj.<8BM7bdYV=)mKdh?S._->YS['qrHnr$O-BY@,h'HiPM(7W]P].pO-8PC>^mFTp^.IMk(ceZrHgJDpX?KV=_saj11%FklN]Gq[W\ktl0\Ro7&HAt?qk_;PNQ)gR&TUTo)Y!fK!+<<2K9W,r)1Dm'7FS.;9dlPE0!Td.H.SqX%QoXn)F>9f,RcU66uqX\m`k2,7u9c=AA/IH3DG<"iK*a&(m'd2/-G%L8T5n4UG^3%&?bPX6p?N]:?_+^jeS!AZZg$nA+!po+JntP^t0iH7Y8WP2n,DfN[hXghST>qRHACh*R:u.n%:X>/4P^4P%[*OZLr3E;Eqf][jf=!!DB4b6D)Wa0Y\FlK4>shE@Mo0PoCm@CPW`loHYqgfU:B?H/^u14JX1<_2"MLBg9&2>t$V-$d>m0&5lA2h67;aN3TKG/guaVO%i?!Yt.PE`i'rs*1Cc_)pJA;Yj4fT.=SI;MF>f2W'%AQ_1HN0B>69UW1-NHl1]/(W`DY\Wp6qlV1Jeq(WI1irE+?J.%Y)R*jbjAZP@-bq6iQ4)`8l9G.BZ#mhWk_+"IZ[Xb:-r4"Nlj]1[0LK]>s`D)`;?e:;LqX%Z[o(^NMp=UUHR4UH)@&19890@ZL`&D0I@Zj4\ii7Q)Mj+&Uo=CFq5Wtr8-6aS>0T>g[eb^931/+*AFTk[.`H_a=0](.KcC<$upnd30bjk]_5&EIW`$`$-PhO)5/4paheT6W"qAH#S8n5<5c6E\;OR:XSfi;+UM163fJD:89d0f1:Ej^h9ObqK4URX&ET`:q2.X*h"Yeb`F(o@Gi&?>\YfBSLJsGH&gs[Im[>$u?@:+XGdd0)Sdp+l$jV@C,B"gB?%Y$]'?Q4kQbq&G\4FYZ5fg\nuOsX6MrDgZZFRe^%b8nn-FklW:^gH(/]/c?QbOoYfpkFKq*GiZ+&hsKkn(JqOIniNR&\$-b#oa][QrKLSdqG6V@jVi>JX,4rqA859AS5$j]@pW0uh7h!`;*Jseu#+UF-hKi)84Z7W8Xl5<+!A`(^%U>,k>NiD?dVSfH\DNTi3tE;8[bfZLYm0h(Bj0a;ZP?d%;jK%g"OUbNIaZoY[!U33?mJ&`I,^(bnabEUi3>tdTIA,tn[a`I-TPF-.4sj7*YLeLL9&MhMbfZFk"Q'`/f@K^2Oq%HeU)9]p3DF]@DEM3FaCLnn]CpT!LC9ip4^)ZdX9/k\c*[!Q&q4*ApA=LXRe`Q+Ol0,TlJP\a/)T*\1#W]330@@mj5)eHgZ#aJX49F2EioRY;pc,!DAC!H!Sg+A_9X*ktG%l.*-SD9I/Zckn*M4Rd*01BHA]CGJ*UMo"pmDmo^diD`uIlJ.O`l"F#/u8,S]u]GImhi4JLSHt!9'6n^-(SB#l[McWp8L#BO4JL/?6VbmjGlC1cXB9CqW[kRm5+D/lr^"g4;Hh*+U(3X(h^j63%N&h[]2s:@EP#F:g$o9`%bc3qf*^@#0n4CseS*7+r(AKHQBi3DmJ$r'3_5Xnc6W&@J^HeG7;%\B"_mJ"g@n9nY"73~>endstream +Gb"/*lZ:gj&c\H;@_];5gt_^k_[r77p<>[H#1H'#4R\=R?$1Yr"_d+%-##('(Q,T_HG+_h>Wl0`"rpm]PO6m_uXO7bY1eCX0`HqqqqgRV`N23"DF#e]_YZOFiT.2)Q?@(5h0<;:O]cCH/l$']M-,a//5l`5C#%Z+d[&7Eim#6VL!]EN(5k[Z1YP!9&n]BkNAUVK+DFM5pnY8*qR.(uQiP@?V'ST>r=KV_VEuG$_6p=o'+K!U_dP2!=2l4-.7[uZZ=1ZMMK2gBIEPKM`0#F!9M<@_Z`f"e.lV(<5iFY`GPg]U;Q_$_R)>KU$kUE)aq]?fBVeT?\WL7u'V1sCOXZobM12h"cRoI3*A&n;PUQM_(he#%@0iaiZt+8jJ1^;&pJP3Y5SDa$WFE^]iD7kRZ(7796FTARMCOdb"J)5!Kh\.P4e)YoiDUN&bT.1b)A,@;=:T(#h9]2oQ8;"]3$1luX`_D&@*!2B68#pd@\-bQB9)ViT*!%+]I5kbClKp(-JJ>CuIiGA0TqJ2TmS2Y>U1XL6eYebLa_S0a\^pInc1i)==mJo)q2MZ]JK$s^T3Atb7686mEPdk-`RdTIY,H,JjaMZ-B3!G!l)^hr`Tk-!ha?=dunAL"+1UBqcZVo5,3;rYX"gQn9_B%6C6f9Q:s/XBi\uch)G"I0tVe0C-LL_mM=U^Yi&*?-\4$pEH\@"elp,T0&/(KZjq/0h8upi%S:d_"dkuPefuW[=)G[(YB,JYY7u9RZ'1G*Cea+\N(.b(paC[2@@#.m?e;`iUdj9i_JK?EE<5*,U$F$?mmg_O`bBTaJ#6f^SGo/A.2d<\*0i:Ej7,;41+:_tE@>sfK(L0o&3V,kf*:Bl*\7m=6QoY%/T$O"$U9*k][CDplmYo;kX6lAI[ik$Oulq1`3ZdCo:N)rfAeLd[`f"Z8mD*Rm/br9&%b$U",Zd6>3+dX^-b3Zs)LC42g8\_Q4j_Z&GE=b@jCLs#HQX*2Lk&>(6j\JhT1E+l][>S"9]g2@?'dbXCHP90/Y?d#pUKUd@IiL4MaTeh%b=&%kckkS(sD"oGYs'0-+s/i$)tn5?D[-4)2d-SBg>I100P/g*_tS0A3%NQ`r&TjA-E_BJ$Z3MpDP"UkD4g9+A9fNCcG"l[_RRcVApA8.$b73rFg/EJ8rPb+4?'u2%%V3hALLm%5T5\<)rkXn3poI3'KInouJi$M.Vk2XGBqhpTFOF)a22@6K.a\d3%FJ@%$FKDK0XX\EGkY<:6gQpn$Yg7&*eq4bb:fP/ZXs'1;i2LJ;AAF_)QGUED2LH5p<e-+#c"`"[aNhS35O]e;8R@+n##\)6*"FGtrB=.tcfSS:#dD+V/m_G!7\8NOZ3"`J'J74AHsV/0c4=R#J-q0-s]UL.'mE-L[[:4k%q*60;i2KD#K)i&ZXNj"bVP(SeaR$RDM(>p.O8i\PLKQFTQuV)3Kgk&H([14M#a@-#dnpJ/Y"3aL[WHoUk9[&kFm`I3O'B2lK]h(L9Jf0>U&?Ji/@X:PQ\QP8l^k@cHgSGrUJkOlftm@7b%@)iFi'5Q]&7*0(qZdUB6.Z[+#-`mm$f%Ms>9X?7Ln!-D)&ctK;\gTj&_8/_t+^T$S-$5[QiIq!A6XQ^n:1S_rmifA`&]s6)]g"q"cW88f1Fd6GZe[bggAdF^ENj<=IGKj/S40^H78#h>j5#L"F:9jZi'-^)On/_cV@@b2h4ntb*#0EHY]t(K^?rEl+>4?_aoSqJN3FfUFrk-@n@7,HDjPtk\Boh'=T]l*63BD_%>\n[f`EC:5YPBq^o(\+![bqM8dXRYo7p!JNFBT`\shXfXi@m.Ea6j[41&&7R$DG)F_pk/3A`S..*BF52=X(]ChOk_l"4Als'M*To=AJ^ScIZZd=\FUHKZs[]@RcDeC!4/p2YZ\[qkP\5O]>Lglr-?Rsl5+WQJSfMt%;[D9#mpZ[:KRQSJJnjIl%+L+chI>pp+LfCX0]"Z/3p*AQ+@*@FqempOOF/H)Lf09D9K8T_kB!<%B%V4RdMXA+p=.@q[qjAU7Q+jE>"oW5/j#lnPm@qhYC,!-PWUkH+;/%cq.Et8^l*ARI6%bP,(Pjr/eBLP3'l8>MmH`IZt1%=3a=;qQ!:G./j<%1UamL:jAp46uPP!=EuN-3#Rk$16mg,j0j9,!5Or!ZJ"3D1)QN#n4FdD"MTi\gYO<7FG9Ch$-EeGV&LqL"X6hI=AST%sj`-g19-2ReY6h722lXs3m)l/*&%'Wp7D357?B!q]1R2DD*iDg4`=_[ISE"b0dbk3K-GX3=R&k=/2d$S68^?'N*gM8%2<"H91n)kso'"8s^-_5H/%J/t\O4@d^K2_W$EYgAUF^IGBhOdZ78p[>Dp&1=&>0K)KX-'<.9q9h-Tb*TaHXuKSV6f@1;6cp/1M-q1WA0)=+k<0*NL_Ve!B$-Jt0?,WJJ$3*@f".9qDg/l\sW[,V5/1(MW0/IId!Da.MP2NZ;E'tboaOiu?!k2Fa\Wfh(q*;L$j4N[`>0L1R_[WkdV?X9ZZlpd\UMr3%L&7(U<("0bjW&3\^l*ht&T;$_0RgpdNhqp'T:\$TCO=s.`J8%D2BfKisG1?BH,]bIG:4=T/R31AM^VCqf'u5b-!)=rG_>~>endstream endobj 40 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1666 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2140 >> stream -Gb!;cgMYb8&:O:S$rAdCSS39\8R6p1aL9kBE#^g6P8=8:MHibq.?`On>5$itQD>@S1+TqR*sr%[cL/oJ3HOo8!$*.Nrp6`e:0l4!$nt@o"#)98JU7P6pZUSW-mL=53!3BoTKb8hhA]Y.mc`7Ac@U_QW0GN@Tq!1gGrT.NiJpBm`#ptJ.njB&UYZ2/:^K0,o+U>"Y[4NF5*Zi5$P:KlNN`15\G+pQW%.dkZ&ZYNo_V*bea[[1@oWO?]CcSiHAoKDmD=Nb;:VOikZ@u5GS7p$,FB#c^@NO:l"G=h`CQ6b4!LJ%]l!JL2iH40a"$O1lLE&lftN;U"]AEetL%B8ru[MRi\^F5E"/A\@D@/iq>=s#3fm+&CR<57_]7j_d'MSHtPPMSe("(qGrlZ/[Kc:#&59DT!1ghiqE;l8*'hP!Lfjp%)F1VmJ6-0L4i,h%-'$Mcki$oBX"9u#iUpVGq4fGE91HI^1FaQEcV[#W!EA-DL$,'LO5SdRb?&mC)kWZqDJs1Fa&eVjA,#Q[2-K`,VT7roRgsLaH5m!a4BW'3>9ktN's+U)XktT%]/Xi$+f/n'@tO3H;=Y^HM`iN2k#(>V8"[53/&H)GsJ`C$4p^js.g$KQn*K:Mh^B]AM8>(APH.%@0Q+(ID]WT"HAgKLP(_+$qqZkN-?JT+Kk*_D#nd^f.Y)!TS4of*YLQhEQA^K2N"&B42`TN?;J1$aB4:$N@^T_*@]m4^4\"F$mWoma3'][OUjGL8bC(h-934\TOQF0nqOr*Y,29+I=nk4bj[ejP;&,\I^re.;92&)o.J#7-C0V=92`RFic4sh;^>p;J%NP,l6r/@n!Vh*k*9#AITSE4[27dOqiPBendstream +Gb!;cD,]1Y&H9tYfU5J:,dH4f[n\tO^hK2[#t[h]`?!8JSa7T!([b$8rdFMd=kGKe7FQ]6b[;BnG5%STG5#,Y/Guo6^Hi:tFpd@n)f,bV&_N]6_=RdUdt?BW]<_'VUHeB&%gK1T=RO8Jk72sS'JrEN9U):1,n]4R.a5lk!'T%Y&:\*_0T@fA#CZP/Jt\3D"Sd&J>0L[nAgpC&+h[Uk%L"RXQ:W=,mdH-7/B5CeFKEC3BPa-!jk$"+E-6=1b>bKp`,qNG6G`$Ph#u"eS4qlYVDf@9h[VPcr-n<51QAl-fl?Q4o]9R\P);=Nk%24$GPY-J2fgObaCY5R1KuFr0%1QL>#gkOHj\:)rPQj!#4Qp/l0A3hW@MY3MrItPj`UM?3S#.=)4/<+rt;^'7J5p8]d.7Fp(BJOgS)7?Fk@XQIRs"bj\^`k9UU^A8UW-:Z\9AJAr^fU;0pU'P(0.8FK[MGeUrX(eE^`:K,q$"p94FWVj,SittO&E4\hYVe!/Ol.6<=PbYc'_7BX)kYs7T!lDtmkNpXT#\jhZMU,'QXnbMX9*JF0_%P6qTD=N8]!7]"eYFs.&ngJ1&D3MITC5b12])f=dR5h,-0.WMaYfub1kH?H5q7jG,bKai58psr26J-R.mlQ-oNb>Lc+pkU,mTKjG7ieZ'Du75hco4sK^t,K%Bqg.K:I^0;O=)dUd?;h6kg40!$-!_u=MB9,4E#LL%g*P!u5/]_,bZMG"s0^jfO&M2Bb%.bC@*bPJcGQHJQ)qlD:_(m=]//J[T(Gnl%SWa&a-k3P6?>l7o6G@HP^"Bp)0l["b415W^V_Zn+jE@lNYN/G%('eN=ibD$u38k`J-l,fNEiu!YDPr#]^>;p^4b^pV7I@;I0M]NUe)0*>ja1jRl`:n6nP18K.bPmQg@3b@2;`qf<3YS[6QG!aj)ejulaBUt#t=;SS8g%'5S1Ioc=g'?`L:$(>R6W$*PAte3@;iFV'22ukI]^RgSC>!p^K0A'U>\,68F92FWATX[Wj^Y+b=oBjZ&2EIsH,)@E[hYS)gVsNV[MGP)fnNQG4F>4WYU$u=du3g4.^Z^@uqTi?[k,skm9Jsq6QnQG`'mSj(FcOfhLjF9lEf26>,A>CYomL*\2q;OU"R:eqs)+&K]=[$!b@Qpd[;Pi`G3r`1tlOj@r#hS=$Ph6qIkNjH_rJ)8EI)c#?VoG!$haC^rM8"C[Z2XhWJ<'dftaaKMlk4u+qeH*GLoMR1f,uS(2+;K0`=ua3beE]WpeCr10ra@b"6M8emI6;>g3Fco(e\DA'8rtIgS==h[C,dZYAP%aI\-'D#W.t%ZFi1c@YmKhRHKQfF_<8"H5Hu.C$XFWo?aK>d*1iHRCi+?egG]>%_=MABmCQ)u1`,<1es?a"(`=]1G-#7hc9kS+1?H#:W3dZM!`Z;QkEcCCQ-(3>?%K>eY;ZPP,)n+QRHk$OC9q4"K&sCKe%I?3-,\T*@?Z2VF.\]gX@kp"LQBip13V0afP@c9'j/=Pmr<]9q/r[m[`-*MOH@p+N:A78]%P*5Ln2rZI"<7*03EN$cU5@,Z-!Y[?7O+O(M%c[[(;>@nC&C=L?H-k.mMk(>oPWh^thB(DjrM)S_<s2!sI8m[Ou$kA-B2_52Fjmr*2oHM%=90RkJiT?MFnA;C]QN<1KZ^M0nr*-oep>9^\sBm`O2[6.9a]B^CdNTKN:"0l-Ee-bI^@S]kKFkD58TjL=bBeeEdUDB*u&[.;;3Zm.ko`$0V#&.mA.5m_+h.W<TTO5%r9+@U=c6h)bLnGgD$ml^rq5'L]=9?4A3=K6eAR>^=V'lS:P2B.tcRlU,d"\2._i7m<=4>P]D)H?0!Q4cP#/Tn2CNj1$J~>endstream endobj xref 0 41 @@ -232,45 +233,45 @@ xref 0000000242 00000 n 0000000354 00000 n 0000000501 00000 n -0000000648 00000 n -0000000795 00000 n -0000000942 00000 n -0000001089 00000 n -0000001234 00000 n -0000001383 00000 n -0000001532 00000 n -0000001681 00000 n -0000001829 00000 n -0000001977 00000 n -0000002126 00000 n -0000002242 00000 n -0000002541 00000 n -0000002647 00000 n -0000002725 00000 n -0000002931 00000 n -0000003137 00000 n -0000003224 00000 n -0000003555 00000 n -0000003630 00000 n -0000003738 00000 n -0000003849 00000 n -0000004028 00000 n -0000004121 00000 n -0000004248 00000 n -0000004373 00000 n -0000004495 00000 n -0000004599 00000 n -0000004691 00000 n -0000004825 00000 n -0000004953 00000 n -0000005040 00000 n -0000005115 00000 n -0000006013 00000 n -0000009752 00000 n +0000000647 00000 n +0000000794 00000 n +0000000941 00000 n +0000001088 00000 n +0000001233 00000 n +0000001382 00000 n +0000001531 00000 n +0000001680 00000 n +0000001828 00000 n +0000001976 00000 n +0000002125 00000 n +0000002241 00000 n +0000002540 00000 n +0000002646 00000 n +0000002724 00000 n +0000002930 00000 n +0000003136 00000 n +0000003223 00000 n +0000003554 00000 n +0000003629 00000 n +0000003776 00000 n +0000003878 00000 n +0000004057 00000 n +0000004150 00000 n +0000004277 00000 n +0000004402 00000 n +0000004524 00000 n +0000004628 00000 n +0000004720 00000 n +0000004854 00000 n +0000004982 00000 n +0000005069 00000 n +0000005144 00000 n +0000006050 00000 n +0000010186 00000 n trailer << /ID -[<3c7062d12426f42b4b67ec68dbf63f96><3c7062d12426f42b4b67ec68dbf63f96>] +[<0ea4a9599ca2b4815b3cdd11dc5a5773><0ea4a9599ca2b4815b3cdd11dc5a5773>] % ReportLab generated PDF document -- digest (opensource) /Info 23 0 R @@ -278,5 +279,5 @@ trailer /Size 41 >> startxref -11510 +12418 %%EOF diff --git a/docs/REMOTE_ACCESS_VALIDATION.pdf b/docs/REMOTE_ACCESS_VALIDATION.pdf index e1e01aa..2de663f 100644 --- a/docs/REMOTE_ACCESS_VALIDATION.pdf +++ b/docs/REMOTE_ACCESS_VALIDATION.pdf @@ -2,7 +2,7 @@ % ReportLab Generated PDF document (opensource) 1 0 obj << -/F1 2 0 R /F2 3 0 R /F3 11 0 R /F4 13 0 R /F5 14 0 R +/F1 2 0 R /F2 3 0 R /F3 12 0 R /F4 14 0 R /F5 15 0 R >> endobj 2 0 obj @@ -17,47 +17,52 @@ endobj endobj 4 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 716.2874 147.501 727.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 716.2874 147.501 727.2874 ] /Subtype /Link /Type /Annot >> endobj 5 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 705.2874 70.13304 716.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 705.2874 70.13304 716.2874 ] /Subtype /Link /Type /Annot >> endobj 6 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 694.2874 156.381 705.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 694.2874 156.381 705.2874 ] /Subtype /Link /Type /Annot >> endobj 7 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 683.2874 129.285 694.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 683.2874 129.285 694.2874 ] /Subtype /Link /Type /Annot >> endobj 8 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 672.2874 182.189 683.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 17 0 R /Fit ] /Rect [ 45.68504 672.2874 217.317 683.2874 ] /Subtype /Link /Type /Annot >> endobj 9 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 661.2874 127.037 672.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 17 0 R /Fit ] /Rect [ 45.68504 661.2874 182.189 672.2874 ] /Subtype /Link /Type /Annot >> endobj 10 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 650.2874 103.045 661.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 17 0 R /Fit ] /Rect [ 45.68504 650.2874 127.037 661.2874 ] /Subtype /Link /Type /Annot >> endobj 11 0 obj << -/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +/Border [ 0 0 0 ] /Contents () /Dest [ 17 0 R /Fit ] /Rect [ 45.68504 639.2874 103.045 650.2874 ] /Subtype /Link /Type /Annot >> endobj 12 0 obj << -/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R ] /Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +13 0 obj +<< +/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 29 0 R /Resources << /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] >> /Rotate 0 /Trans << @@ -65,29 +70,19 @@ endobj >> /Type /Page >> endobj -13 0 obj +14 0 obj << /BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font >> endobj -14 0 obj +15 0 obj << /BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font >> endobj -15 0 obj -<< -/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj 16 0 obj << -/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 29 0 R /Resources << /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] >> /Rotate 0 /Trans << @@ -97,83 +92,98 @@ endobj endobj 17 0 obj << -/Outlines 19 0 R /PageMode /UseNone /Pages 27 0 R /Type /Catalog +/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 29 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page >> endobj 18 0 obj << -/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\)) - /Subject (\(unspecified\)) /Title (Remote access \204 validation \(Linux CLI + mobile parity\)) /Trapped /False +/Outlines 20 0 R /PageMode /UseNone /Pages 29 0 R /Type /Catalog >> endobj 19 0 obj << -/Count 7 /First 20 0 R /Last 26 0 R /Type /Outlines +/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Remote access \204 validation \(Linux CLI + mobile parity\)) /Trapped /False >> endobj 20 0 obj << -/Dest [ 15 0 R /Fit ] /Next 21 0 R /Parent 19 0 R /Title (What the CLI suite simulates) +/Count 8 /First 21 0 R /Last 28 0 R /Type /Outlines >> endobj 21 0 obj << -/Dest [ 15 0 R /Fit ] /Next 22 0 R /Parent 19 0 R /Prev 20 0 R /Title (Scripts) +/Dest [ 16 0 R /Fit ] /Next 22 0 R /Parent 20 0 R /Title (What the CLI suite simulates) >> endobj 22 0 obj << -/Dest [ 15 0 R /Fit ] /Next 23 0 R /Parent 19 0 R /Prev 21 0 R /Title (Quick start \(local orchestration\)) +/Dest [ 16 0 R /Fit ] /Next 23 0 R /Parent 20 0 R /Prev 21 0 R /Title (Scripts) >> endobj 23 0 obj << -/Dest [ 15 0 R /Fit ] /Next 24 0 R /Parent 19 0 R /Prev 22 0 R /Title (Production / staging BE) +/Dest [ 16 0 R /Fit ] /Next 24 0 R /Parent 20 0 R /Prev 22 0 R /Title (Quick start \(local orchestration\)) >> endobj 24 0 obj << -/Dest [ 15 0 R /Fit ] /Next 25 0 R /Parent 19 0 R /Prev 23 0 R /Title (Optional: bring up WireGuard on Linux) +/Dest [ 16 0 R /Fit ] /Next 25 0 R /Parent 20 0 R /Prev 23 0 R /Title (Production / staging BE) >> endobj 25 0 obj << -/Dest [ 16 0 R /Fit ] /Next 26 0 R /Parent 19 0 R /Prev 24 0 R /Title (Mobile vs CLI checklist) +/Dest [ 17 0 R /Fit ] /Next 26 0 R /Parent 20 0 R /Prev 24 0 R /Title (UDP / WireGuard data plane \(ra_udp_debug.sh\)) >> endobj 26 0 obj << -/Dest [ 16 0 R /Fit ] /Parent 19 0 R /Prev 25 0 R /Title (Troubleshooting) +/Dest [ 17 0 R /Fit ] /Next 27 0 R /Parent 20 0 R /Prev 25 0 R /Title (Optional: bring up WireGuard on Linux) >> endobj 27 0 obj << -/Count 3 /Kids [ 12 0 R 15 0 R 16 0 R ] /Type /Pages +/Dest [ 17 0 R /Fit ] /Next 28 0 R /Parent 20 0 R /Prev 26 0 R /Title (Mobile vs CLI checklist) >> endobj 28 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 691 +/Dest [ 17 0 R /Fit ] /Parent 20 0 R /Prev 27 0 R /Title (Troubleshooting) >> -stream -Gatn#?Z4XP'ZJu*'_d`-RN">.rq)2uF@)^s/_9cMG6aptu-a_5Do/kRE(PClTDP+QE\7BQ-N<']#:OiN]!#n;JH9ZcOAS",((fN)<.#O,L-\;0@9d;;<54nsP8a&StTSSaV^n;K,[jU%S7)2DB5lfA'JriHP"pi86KPmCCd(^BnZ2uf,KeY8+,_A`ka?6e,B6C1LNFtI=a,%Ds87s\T,h^L_8ITTH6ip%#=:=(I"Qt+1O&d.ciN0`pa3"/TKen(E:I"(g/obPbbLYn"A&6OZ7+'N\BnlDF+"1^u"JbaSlJA/=X74W[H`)ZD+Y%"/$%Cs6KQcQ&FD>iE#ncR9?aed?`)-d^GF@Mp.&2+J+'4O=Ae%h8Oc/OAflMeWR,OfM#M9mR$HF!2]Kh(ouBom;r49:]->/*9F`].^++:h=Chd+fbcOG"?EdH)G,=X;h,>\:D,-jQeZc81hR,REiX"[25qI&PZ"hFL1?k)q/EKSIT!Adb#$<6$t-dVH*#dWEC]^>ueIL',9FCs0IK65!esT_$Bh/J'e4mWn(E[^jUMb@Hlf[3)$7!a7"ZNk\@n&PAdDD\*_OuD86K]XUIendstream endobj 29 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 3400 +/Count 3 /Kids [ 13 0 R 16 0 R 17 0 R ] /Type /Pages >> -stream -Gatm>>B?9+&q9SYQo]T&Nh':.434h_.bRi8T<_tI;GLOt7?(\cYRs5,AD&X5gU@Cj6LMUr#6=$koIb/P/77'p:sNj4!gU-=9PB53r:j?dMf/"-3X7VW`JLF$#jnj,8i;FD>+;gg(:<7mK9_Jn!>id)I@4H7$j?^3[TtqWTa*AW<+1SgAK1Arj0IS8N;Y+A9kup?$PM@mjIF*+$X#F%q5ERW'NC03L,/m@tHMrR5)MsDl-XhC*paBPcp*0[=RA?mX$oc)*P0Fo5i%!Km3J\r&DO1,mZFV>/?P80)!fX=?c9aTk2.\4ER"?D]7M#p?8qfK`:K>QTBYHun&U4N=OlNZ9(e*0rpJR:t>PI!`tFRGldL:!(?cq=pE=j[p/-mN"S9I(ID4AmJ%i[@@1QWmuGaHEX"!VeDP@6VJqr0*,&bdoMqZCm)t^U%(;59CgC+jMnhrs]MblEq2ckqaJf84E[@2'E)TG+-$,lFgMs.783F_Y'WDmYr7d?L8-;UN;Xm1,:rD[(e$/cp]Qj+-quF^kB,(^F`4\#p\iaUSG[GN0sRl6o>!3*3Acq20M&#X*0Ymr+\oA>NMCa6n/rTkr1EW5esaYZ9n*JW$I;KXR,!f#Rf%n$A60u(/M=_mgYXIG[_:7LiA-`WI=GLJ^MGbedWce'3U+uZLQuRo0Q>S%NXT.H(kJ\76nW6/97bgd43WtbP'>UFd>J$m&?!?`,fk*WljuLWCq$5f[Q/d_+&n3@G=$qu3f#p@fe`tj*ERZ=l+MTO?3$EZr^>'RY(ctqce\!][<;.Z2S&j#Hu2SNl^X\d#U(Jq.)6DPQT.%ipIambH\"Psn9cn_!-]J1>OBE,D+GM(_7;KW!-Oo84g##$NX-a_B[^kI[Tm0P:a-Rrp:VB:!`;uX=BCL'''O5l"M5&8>IFpFf?CEK^OO/9a!8Eb%98Tgq=jiS,G5^>*-SN/=H[%fBUV$rM\!b>gL=T!@7\:&*4?Y*_.ETOY^,aN>\9#sOr'_*pIKg#trj;S@e;ut;G!pZGtCWb\A:BMf*f=AgSY$Y^gr8[@.pBiANCQZC=W>ELL.DR2H8+6pBqrPb+:geRNB`@Qi"6M4'JQ0B7Q)gJ^GhEffSj#OMb=dLM"[a)fQq7*-m--N88F[5Q3=2(RIjPHGT_iF];6NLpBe>:XRHaS=f@Lha1Jm#`/QreV#X8q_"`!7+j6&pleRb>V>70/BndR_jJ6-,aBBl+i#8DT\$G?llN`W5JYSRi33[ko4U_3cYH?bl-mmP]jK>"eXN#X#7R[/KG?8K%b]&t?27_ML294U+AchKj$UZX2Ug&nAk%_K8;Vp4qb^@;8C"n"Id]0ao&4qWV0#H2/qJC3D\GB8I$c-dCN-/CmWSSgXJ=1@cmdS]VL#<+j$;SQ/;!>%Vt+.].LF-Q=[i6ONE'-"TB"$f"%,&O(o"3AO5'7dqT:s=iJ?-OJTHe^uTg6"L&'ct0:RumPK[RMCC.[j`pWnrO3J;d?4t>J%LHs46t^(i?g`=Bmk#!J:lWM&\&!8hi@%jHC61i"H&ZB@6bh%t8I'HI.]TESODL0$"&TIe+4-4NI-4Ie^CFX+W:!S';@R3G$Qd;AfD\%0hPj2+03FiGjc.m`3e[PGr[gSk_J[E?(PZLQW(.!?^RM'MWQQ7p^Q`_#s0=b5ZF]Bcccu'uuY"=5U)$e&hQd'YUA&m`*^iH6\o@9G3).K"WkN[>YpA2P[rh.iX)?+3:AN8-dG:*i:D]IPM(6n8jV^T\SUt#3_>TOc,i'K#`BFi2jc8)nt*KIU%#7&_Rk>g"r4a\*$nAjnEK^3q2ZK&@s\@onMo03oIdSaG)/-UB?og2PB'oZJ7Q-P"p6@_PB;LEa%.!t.lY(^abbq"C7k3l?*&4[<<3R@QtLllr,\@XMJo/B@T`BlnY's8@eB3S0m[8I17cY5gK^@';rS"oP!m74>5qPp$*/gaTAljc`]QLk'^pd011Q/mg75fC`!+LpK`s3"+YZM6)@qA'IF>AHg"4R*KjgK':W2mSHJ$7F,8ACQRjc6^1(`,5+AW+G0h`#lT\I"NcINf-^Xe!1easc^5/#S''WJQeIE*Q)f3B*!^YVnc&ip!eZRGMhW))Ym=eJhZP8%".8#k.-/L@!\[KCj-ZWia,ET/\lb+ABTBV%BMkpoDrdO2iO_lT6a#V?nL6kGbML#dRDX'%LjTd47`rhc^h)dTK&>YsD608SQfg`?;Q5NY6FE6A[4YGu@Qf*"Onc,R(/WPFX=j)G#KZ\2RE];.k!Zm%ZLe?cX>X2CWuqJPW%LXH;p!FpX"T-VqGq!TAZVCH9588Q:gVBd"aW_1NpEO6\3'3=HZfRe*2!C^bt,uk5/CG/f)P\cA?rXZ%so%=~>endstream endobj 30 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 2296 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 732 >> stream -Gatm=D0)19&H;*)Z'2UP8fM3!=-60@34@82A%/J7[U1l/?oU^o""/.teMI0oh*V5e"@>'g-*^7Tm-[#\\,/:@J;PG3s6E/T+(oQgM#bq2[K'bb#0^sn4M,jL+?N66<324])ISUqMQ&ZA5)7V^+=V_QM@$TpLamo2jrR%@TpgIo,n-pl.8A_PbeE'fqNA1;2%N%k%^S-++o"0[WLrTPFq+_FchWHf'+(_$ni28;2[ESFWj\nKn47nr.M1NELD&h5bFH??,7^RXIs8.3;1qhn7B+4*ou"=V*.^5S(KL>Acp4Wm%(sk9A3R3p("B;'*`YeS9a'R?kK;qE\:H`)dJ67Mit,MGumR`BK5;fdqoh+3<"\9\1h%_U'YC:s5#O#iodr3(=E.GX6k\&AT'Cn-m]Z#nl`%g`E?cBFnB&6R4:AeQ"sA+7/K"#@MACJP5tN'jQ_$F(Sa'`cG.8qS#^KZNh%g33-m5-?O`2TEss7r8h;&_nWoQ&U(PY%+jglQ=T!t=j_&3^ZC=OPgn+W?T=]kpUXQD8?K'KQFo'Y5=89a_fKY%ARbta[C"jk&[%m>i_oOi@rjGg?kJ52TckK9!lXgdY;H-kC?K(W.%MI'=DQ+D[#hoL0]sf[/a2[nd`)qAQrfc-.TN$)lN[1Ds/tdq&13dTP9:+<"u1JXI@Bkdjb_uja4LG65GhBJ3GJBU[$Y+;o[kl&9Pnl9CKK3XW,Oa:B/=[qFfrs!CT&C^uKWamG4l5YfO+m%SgY*D8^/KZ1c&5K;nQMNo#amJ#>5s=?UV[;$dVHWEM5=7C;(t:=28m)O[.DWV9$ap`@j/3tVtpdkn49]II9S(#h4`p;+Km7"I4fP+C/FB)ME56YVYAq`:TZYkLq<1eVq4-R,.!"GP]qP2^GpG'hBsh6.Hmme1"3Q9+WVfWIo:!3F,p%&+Ie*82Gqi'O&S6H7mG,7]X;Ql];KXS3^DuXsaAZW&)5)AU\LjFpgsga?RJ/@Xh!``1Lao"Ls7h;5X_MAhl)BRYliQI`9'/09hT2)^^.4chh'%OFV7A1)TCmO;s!/c)2$u%_l$pL)#mf7n=sW1H6j2O>']^E)]LR9rq%HW0fh"=IHs%0nBJ314c30!dS'_!_:uO@d;^S+gWd+qlFqjg2Fd$ibVdX#^l&;E]`G/p2mk1/6!JZJ$%eWMu^[:I-g)"cN*b.gq\cX]#KCKN(9\$DBr>E$-^%7ZGb@K[c4&U0Z5ipha%l#CaEfdl?MIQ:"C<'!5(R[;Lu$CPnrlYAo!6*bkD$&j6nNrc5aF(m3G@K7LrpomZU5_n1_ho2)q'IEIW_[r^C=#eRc9$ro_R1b=cq9l#'e5].20Lr(:hK82!@c4lR$I\]JLg:h<)i74HISd[hXmJucP!:,LO$hdB$+B9aG5`unf]IaKWM_#XC(PYGR0YaG`#/bkD/Mt024jg7bBjkLl_(g][ej6S3*o$L.\IFPjqOgVCO;o(!_&DXBYq.WeD*Sc^M7h>F^!#;<8O4WZ4s?:sn4o;$9u?XYXSn^@Br^$BQSjk007W/5(9-:XmMq&7f[pN^)ULP)O(Bi%Tm]bLC$jEadBR)(BeNWXKQ'U;B^pR5LSAia3]($fqKM>4TN00m?AY,ToW:(nZbQ^)-u-,9g=]:DNf(_nMi[1n.SdPW+Q%A^\n-h()_;,O=sbL/RBr'*8`JBW`/2/^5o2Ko`@7+haj?`6dBJ=9:jBendstream +Gatn#?Z4XP'ZJu*'_elg1cKUWd^Q[3$&K-qZLt%,16)_`+jOnZG2Ia-T`p1^.]ZAk+qZ3cGE7dq"V39?r/)6)AI<)%'I\V+&SOE28;GPf423iKARuF9-jdQei]&9Vh[T';mm^WR!/X(o%4ITh#JWj>1,5RTP[Hbo>(X"_+:JNM65JPO1kUuR\^F4S(ID:5%ueK3k./XE;n@L7PG'J+B=Jiii%qJ,9]ZmZDNt%sE-_lU:Im^dQbPbA'AVU?eeA]BiGOP"UYUaCiVd^&T7i[oBKZnfjjehh@1R4I\=Oh,CF(6.[6j.BlbAUsK,2I[-#nS2p&^k((UW'#l>="\mC(pujYQO+5D%79PD-=p!&5VrYOB4eQSd"87f@eP*Opf>XL"14NDij&SSSDfq8WQ)#c#O#_ae!U/BupAM'Z9ENPfl$>4(Tm$1]>m5WQ*iJ35DPZ+SZ0ap:@tXPJ/1O=E\*rS9aRt>F*oZ$@[Sb4=toaAYo$B6.r5d/d+Y0DD6taKBL;`%D\8at^+B%E2>[X/4OKE+nf7)o2l!`E]stZ=:F=O4(W/Q-+ruWVO]Ai(C+D6rcW.6Wn>+;A;uu%926<'jP/^s.@9_Eb/2a#$Klq)`%_rIWd)M6$d@p.:I"B'i~>endstream +endobj +31 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 3441 +>> +stream +GauHN=`^')(4Q"]kdki5Y+/UM&KHJ$5t#3-QMaR`9>)[;"'!R4m^nP70,jOoBY/m>b>>-bSLZco(qcHfU;mSIER?RNp?'nK+4(OfOj>1_-@2!).9\pP!ckq=DbW-SOlU,[C(F0BPf&u@YbILV4GQC!";X9Fk@Y(4B"Y]Hf&=LK4Gq-0hkAoP%P/M&+G-Jnaf''.d'IR#+Vb$]I>(Ink;5I?eIhO\[=G8f!_(^5gpOF-4'%BEo:Gnhi?6SKip%0PTcF^*gY:)k!V'B--BEjXotiKAbbLZ=KN1rX@*_3@?K,C\2tHH$g!YCE%>4HTJhZVp_B,dRO\(BO)6ffp6[*o>-u;*DZ\!@;&LVH1^"ahG6'gRTl9d*Y;!&eBaZlb`1p2uRU'Xh!r1/i\[G31_`/9u'ar;ZKM2h,,Dc'VNelSm$2\V#TMAukO,aYNf!]S)C=40>`KH1T7Ck`7gTJqS_=q_9.gGEHKHS6nT0GK:\>(J%3q&.o4fHG-/=<]s=&)AJ=M3XLI(S%9m1ce=)42QC"&!.mk[6mb6KAmN=gL^ho)qS,KCYqq\g&1a/`E^0JAViDsf$IB_IQuD\Imb4Z?'o&g=Tso^fC-XS([2:rorug?;tP@)n!MUP=Gu6f"a\)TE*%cdI`YrXFo]=2Qo0mKDIuOZ"V2M_ZS+2@.I^"'T]Ae\''f3Y2m=<5:tI!_:lbL6r;mbe/m&2J'0Y*[S-G'>GILXTqW5tBIfFhBV(W2IS=q3^OQ$?V?jQ6pi#dr*V/roT^`Hh:NZHc&3SkRC*e[poUMjSMcd<6rpEA;KURpnVLQAnIrOo_SiJ"@4n8u]WKD1:a*`P\IO%9.X_ob&4mTbgHgB-U8nX4=j`We8#;DB7"a*U]onZ@\bh1H3>fQbh'N,J$k:EgPVBI5f7NLPMfI`5(+Rm_]C_I)QM:SjbO2DHMM,9^TL'([Daf>)PG/qo98dVO3fCZ$/#)+59,T0,A$nD/@B"C)T%i%b#"RZ<^:C@M/^LMEG@"PJ#n;4U+Y\]XK#)slkI7s02Hk\6-5M,/.m?tH5[8^F#(?oqJ;lp?Hh0;'_]?1qh(g=i6Ns:,;1%A)Nq(H/r-=%8$?d`);=mcE-AR<@a&707kX:D"Dm*-I@oUgktn&V*Ad][EJXC0Sfu/f3sD!/*:b2N%4U[0]Dr'T0V:p0ifsi$&?jkW!2=L0i9L?aXT6].*l-<'DO"m@bH4`8&]T\hX9I_.A<[Duqi:2JP$8n+rOKfSf*(Uh7ac[jL]#I-a1Do(L5?h1EY^<_[A>3rs+4/4$q]rKTA$-n[8eiCBO*/=Xk!Vl31SL1AZQ2I,1+7QjGD'bT^Z.tH]518#9#3[>p($(4RbOUnfdhsR]5R:N1f4qs!+`I%c%[0'47W+RmY]"%aO?,(-3XYrEL(YiY[>6p(JlTpH0#9=':IAt7/L05pK$Mu!=,Ql3hCNbeJl1Y^Pn#Ghr*FlB5mP[sUPaC]cLZ'pBl->LoJ-%;!(V5aF0VNg8Z8!\Q=(U`AkDIQ0_]oq%Mi6U@\X;cXh6a`eg(2JfuVb/:q!5AJ/O@%eM1FS-ea+[LNb,JLerPWcBGfA$C-`ESghoCt]Y&,SO=\Op56+@^sEnSi%BB.s5WPBhg8h$_hpDB/_pYh'_LYSIRP^=gkG,]!h=:8l''l5CIK^u-?oR`Q_Mqj*iq)sW\di`e1&%LSV[6UGOeibjm?rdF5Cu!&f,7W-^+JUT/NTjVWEH1-:%FuYPN>!,L`HKmUq\5^Tmq'OTGTD>qFI;CTJP!R)7..s[i7^eQ9QBlJOp;8MS\#n9CD'Bk+F#LbdWg7PBpH_[qnrlX^k<+lW:YsUurhLk10G5F_P+j'6NMPOsCFp7+H%QV'H+38-D!JZWe3#E>1,hb+AB0"*jKD`G^kJ8tQ).&(5'EIC*U.kendstream +endobj +32 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 3054 +>> +stream +Gatm>>BAN>&q9SYkfW=m`>^%D#<=00MV.!1b"c&+G$h1GNR7a3Fj#_Sm&L$SgS5,^+X(e_:4bu!e(rL%Q`in-+QJNGr]g,X5"q[E;#s+>klMQ)dU@G(UZpHm1F8Zm)j338A0(OF^*IK[NpDdc$^D/S7=hEKW[\eY5-!jlO]#au9B^c<*^^J;EZ+h?&E\L1@KMZ^!Xc9Z3D:N6'abM13fM^J'A3l/:R2W#jj\lpB6Kk1RJnhGqu_H61c8:TW7M^i6<-GiT/Z#k#J7PW(GTLjGrct`80*9)@EBo,/q>5K/6M*I@^1iNeU3qQPdi9G2>q)k+u+,6!,qc+V+r3Fp!;8f;br;8+"Cahh:1][tiB23\f7kB%\o/ICKH'RFZqY&*6qtjoGpRX@Y)rqOL_o_!h1aN?Nm(?'Mg_o#aFl"Z@.X62X>1s8[pXI?FDX7kB5cdCQZA5=PE74sTeNc^a<^/*C>j?#%oeNOBdF?@Nk7ddV.:@Z9o&[">[5=Te%0Bq2A>c&ABrBu&(/MF&hR7@QZg4GSDaQi^9$ECCBeLGMhm&9\G)P5c&'#a2b9WJJNd5YP8LH2+>`JJ:.,:^B`KcsOjfL"IpkB@osThk*Hg>)@Ta4BK^\8HN(Co^lS>4X;Iq8?iI;Xt,A47b$1&j5`.>8`1,On-u"k\`3p^OPk@/;8o,jU2VRA9Rr!/cLH`0O[CY_!*1Co.XTo$7/>lmX7qR_utIn?J6iAYQ[Y,$W_1p'dDPNhnTmQ`+YoB,\Rd8dLJl@Vj,"fo@+J=FQZ76R&#)EXLhf]7G7oh!A$pk=q*e$N+j'Vj;Nbj8X?HFNNRgdjC,kDb]pI(MBEJ19135'hGAsP=E4Nr**2IfJ`'^0#97-(:MASC:.(`^d)OS-kO'@YoWGjhBP:E6NT^1>;[^kYCUbQ0VVM?,bmj&Vs$%ogg#5gh2^mLMSJ)lf_b,#eWK9ZAJ8/4ii@'=nH-)26&uaA`#tEsDTS.8n;aL[&NdW3<]$23h84eH`4(2<,F!nJnuqE)>M_G3E]'Ef.H[Sng$a%]6ul$fqeuXGioe2]ei*R$-6kX]P!4ArTqe2qY`!D$PtQ^O&JJ3oU\[)tYHFm?usG&*\pFGnZHd%L`s0%/Oel$AApdns"YmBe3XF(3WQ$_P!NXkcIE1VG,_FWUr0D4A?MWjUTYOL?g\.`uD=Jn;u0WjOU0]S)Xu#K21:\0n9/Q/6DYe!Y[&\0Q(8Z%u<82MuTEpDWDS.9>qf4r)h9J3M"&eimJIXqmrIdp%.V#)$9=#"q:?4(aOhW9N':K@J@$3RTQ$,^^.beM1VLSJ:Omm^emB_QrkQjW$!2s/.li#&.ei-#WO:>M!IY9*+]8lcnaJi>&OULXYneU%TC_5K7a(m;feU7"%fmO[BVa=#(/ugYK130hVB/=/\Kr>dkr8g7a#2"[3J@e>`rE=*sJu0enUJ_#AV9<%N5pB"jcktGlJMTmimA"8N,uSX\(^flH?O*G:?-[WuIfgHhgg;^!fS;r.*h&[M9GD);n/9E0S(cYjE^S>I.=cbooG&-kLH#IY#,URIrFN=`fJa)tQ2W;0;Y$XP'g^".Td8-Zh!JM/O6tpB[$Ujf]oqA[`6,l%qb+]`\%YGOl;X^_p>mpfm.[Bbu4Ou@qZ\OI"/lAB+U#;4(t9p$.54F@msqBkiPFSfo9ZA2L_UWLIRE]_"WhKnI8(phR\dNX]['RAY1'ue&C5:-IJ;Z~>endstream endobj xref -0 31 +0 33 0000000000 65535 f 0000000061 00000 n 0000000135 00000 n @@ -186,35 +196,37 @@ xref 0000001090 00000 n 0000001237 00000 n 0000001385 00000 n -0000001501 00000 n -0000001762 00000 n -0000001868 00000 n -0000001946 00000 n -0000002152 00000 n -0000002358 00000 n -0000002445 00000 n -0000002779 00000 n -0000002853 00000 n -0000002963 00000 n -0000003065 00000 n -0000003195 00000 n -0000003313 00000 n -0000003445 00000 n -0000003563 00000 n -0000003660 00000 n -0000003735 00000 n -0000004517 00000 n -0000008009 00000 n +0000001533 00000 n +0000001649 00000 n +0000001917 00000 n +0000002023 00000 n +0000002101 00000 n +0000002307 00000 n +0000002513 00000 n +0000002600 00000 n +0000002934 00000 n +0000003008 00000 n +0000003118 00000 n +0000003220 00000 n +0000003350 00000 n +0000003468 00000 n +0000003609 00000 n +0000003741 00000 n +0000003859 00000 n +0000003956 00000 n +0000004031 00000 n +0000004854 00000 n +0000008387 00000 n trailer << /ID -[] +[<4aefba77718541cc2646cdb6a66281c0><4aefba77718541cc2646cdb6a66281c0>] % ReportLab generated PDF document -- digest (opensource) -/Info 18 0 R -/Root 17 0 R -/Size 31 +/Info 19 0 R +/Root 18 0 R +/Size 33 >> startxref -10397 +11533 %%EOF diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 4e33ee7..22248a1 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # Android Cast — implementation roadmap -Last updated: 2026-05-23. Living document — refine as alpha and commercial tracks progress. +Last updated: 2026-06-04. Living document — refine as alpha and commercial tracks progress. --- @@ -22,6 +22,8 @@ Last updated: 2026-05-23. Living document — refine as alpha and commercial tra - [libvpx / Opus / Speex](#libvpx-opus-speex) - [Transports & protocols](#transports-protocols) - [Receiver / sender UX](#receiver-sender-ux) +- [Remote access (alpha — RSSH)](#remote-access-alpha--rssh) +- [Auth & email (alpha)](#auth--email-alpha) - [Deferred / post-alpha](#deferred-post-alpha) - [Related docs](#related-docs) @@ -35,7 +37,7 @@ Last updated: 2026-05-23. Living document — refine as alpha and commercial tra | Track | Goal | Status | |-------|------|--------| | **Android app** | Stable, feature-rich, **demo-ready** on LAN (discover → PIN → cast → stop; soak two devices) | Core path done; polish UX/docs and gate experimental options for show | -| **Backend** | Deployed **OTA channel** + **crash ingest** (+ optional MQTT per `docs/OTA.md`) | WIP — examples under `examples/`; deployment blocked | +| **Backend** | Deployed **OTA channel** + **crash ingest** + **on-demand remote access (RSSH, prod/hidden)** | WIP — examples under `examples/`; WireGuard v1 lab path done; RSSH implementation in progress | **Alpha app** can ship before backend is live if the demo is LAN-only. **Full alpha** includes OTA/crash URLs configured in developer settings or `local.properties`. @@ -58,6 +60,44 @@ Last updated: 2026-05-23. Living document — refine as alpha and commercial tra - [ ] Host `examples/ota/v0/` channel JSON + APK artifacts - [ ] Deploy `examples/crash_reporter/backend/` (see `nginx.conf`, `README.md`) - [ ] Set `ota.channel.url` and crash upload URL in dev settings / `local.properties` +- [ ] **RSSH remote access (alpha essential):** device opt-in via `heartbeat.php` (`type: ra`, `opt_in_mode: rssh`); outbound SSH reverse tunnel to bastion; **no VPN permission / no status-bar key**; operator whitelist + session open in BE UI; E2E validation per [REMOTE_ACCESS_VALIDATION.md](REMOTE_ACCESS_VALIDATION.md) (RSSH path TBD) +- [ ] WireGuard v1 remains available for lab/E2E; not required for alpha sign-off if RSSH meets owner criteria +- [ ] **Email infra (alpha essential):** `info@` / `admin@` / `root@` on `apps.f0xx.org` → Gmail forward; SPF/DKIM/DMARC — [20260607-2FA-email-mobile-auth-flow.md §9](20260607-2FA-email-mobile-auth-flow.md#9-email-addresses-agent-config) +- [ ] **Registration + 2FA (alpha essential):** email verify + TOTP + mobile-friendly login/register — [§10](20260607-2FA-email-mobile-auth-flow.md#10-registration-2fa-mobile-auth) + +--- + +## Remote access (alpha — RSSH) + +**Owner agreement:** production **RSSH** (reverse SSH, hidden — no `VpnService`) is an **essential part of the alpha release**, not post-alpha. WireGuard v1 is implemented for infra/E2E lab work; alpha remote-debug UX targets RSSH. + +| Item | Status | +|------|--------| +| BE control plane (`heartbeat.php` `type: ra`, admin UI, RBAC, purge cron) | Done | +| WireGuard v1 (device `VpnService`, UDP DNAT, `WireGuardPeerProvisioner`) | Done (lab) | +| BE `opt_in_mode: rssh` + bastion/session provisioning | TODO | +| Android RSSH mode selectable (no VPN prompt) | TODO | +| Outbound reverse SSH client + foreground service | TODO | +| Operator E2E (whitelist → open session → shell/file on bastion) | TODO | + +Implementation notes: [REMOTE_ACCESS_IMPL.md](REMOTE_ACCESS_IMPL.md) · proposal: [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md). + +--- + +## Auth & email (alpha) + +**Owner agreement:** public email aliases + **registration with email verification and 2FA (TOTP)** are **alpha must-have** for the crashes/build consoles. + +| Item | Status | +|------|--------| +| DNS forward `info@` / `admin@` / `root@` → Gmail | Ops TODO | +| BE SMTP outbound (`noreply@apps.f0xx.org`) | TODO | +| Migration `008_auth_email_2fa` | TODO | +| Register + verify + TOTP enroll/login | TODO | +| WebAuthn / passkeys (optional alpha) | TODO | +| Admin: recovery email, clear lockouts, audit | TODO | + +Full spec: [20260607-2FA-email-mobile-auth-flow.md](20260607-2FA-email-mobile-auth-flow.md). --- @@ -143,7 +183,8 @@ Enable in **Settings → Stream protection (UDP)** on both sender and receiver, | `LibvpxVideoEncoderSink` (surface + buffer) | Done | | libvpx decoder on receiver (`LibvpxCapableVideoDecoder`) | Done | | Surface → libvpx without MediaCodec passthrough on cast | Done (ImageReader) | -| Opus / Speex build + sinks | TODO | +| Opus / Speex cross-compile + CMake autolink (`build-native-codecs.sh`) | Done | +| Opus / Speex JNI encode/decode sinks | TODO | See [ndk/README.md](../ndk/README.md). diff --git a/docs/ROADMAP.pdf b/docs/ROADMAP.pdf index 85ed6ee..ee469be 100644 --- a/docs/ROADMAP.pdf +++ b/docs/ROADMAP.pdf @@ -2,7 +2,7 @@ % ReportLab Generated PDF document (opensource) 1 0 obj << -/F1 2 0 R /F2 3 0 R /F3 17 0 R /F4 19 0 R /F5 20 0 R /F6 21 0 R +/F1 2 0 R /F2 3 0 R /F3 19 0 R /F4 21 0 R /F5 22 0 R /F6 23 0 R >> endobj 2 0 obj @@ -17,78 +17,88 @@ endobj endobj 4 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 716.2874 100.381 727.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 45.68504 716.2874 100.381 727.2874 ] /Subtype /Link /Type /Annot >> endobj 5 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 705.8499 124.2975 715.8499 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 59.68504 705.8499 124.2975 715.8499 ] /Subtype /Link /Type /Annot >> endobj 6 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 695.8499 140.1375 705.8499 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 59.68504 695.8499 140.1375 705.8499 ] /Subtype /Link /Type /Annot >> endobj 7 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 685.2874 113.717 696.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 45.68504 685.2874 162.605 696.2874 ] /Subtype /Link /Type /Annot >> endobj 8 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 674.2874 265.741 685.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 45.68504 673.6874 118.157 683.2874 ] /Subtype /Link /Type /Annot >> endobj 9 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 663.2874 151.037 674.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 45.68504 663.2874 113.717 674.2874 ] /Subtype /Link /Type /Annot >> endobj 10 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 652.2874 131.949 663.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 45.68504 652.2874 265.741 663.2874 ] /Subtype /Link /Type /Annot >> endobj 11 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 641.2874 161.269 652.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 45.68504 641.2874 151.037 652.2874 ] /Subtype /Link /Type /Annot >> endobj 12 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 630.2874 121.277 641.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 45.68504 630.2874 131.949 641.2874 ] /Subtype /Link /Type /Annot >> endobj 13 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 618.6874 126.157 628.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 45.68504 619.2874 161.269 630.2874 ] /Subtype /Link /Type /Annot >> endobj 14 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 608.2874 121.709 619.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 26 0 R /Fit ] /Rect [ 45.68504 608.2874 121.277 619.2874 ] /Subtype /Link /Type /Annot >> endobj 15 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 45.68504 597.2874 120.829 608.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 26 0 R /Fit ] /Rect [ 45.68504 596.6874 126.157 606.2874 ] /Subtype /Link /Type /Annot >> endobj 16 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 45.68504 586.2874 92.37304 597.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 26 0 R /Fit ] /Rect [ 45.68504 586.2874 121.709 597.2874 ] /Subtype /Link /Type /Annot >> endobj 17 0 obj << -/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +/Border [ 0 0 0 ] /Contents () /Dest [ 26 0 R /Fit ] /Rect [ 45.68504 575.2874 120.829 586.2874 ] /Subtype /Link /Type /Annot >> endobj 18 0 obj << +/Border [ 0 0 0 ] /Contents () /Dest [ 26 0 R /Fit ] /Rect [ 45.68504 564.2874 92.37304 575.2874 ] /Subtype /Link /Type /Annot +>> +endobj +19 0 obj +<< +/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +20 0 obj +<< /Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R - 14 0 R 15 0 R 16 0 R ] /Contents 42 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 41 0 R /Resources << + 14 0 R 15 0 R 16 0 R 17 0 R 18 0 R ] /Contents 47 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources << /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] >> /Rotate 0 /Trans << @@ -96,44 +106,24 @@ endobj >> /Type /Page >> endobj -19 0 obj +21 0 obj << /BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font >> endobj -20 0 obj +22 0 obj << /BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font >> endobj -21 0 obj +23 0 obj << /BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font >> endobj -22 0 obj -<< -/Contents 43 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 41 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -23 0 obj -<< -/Contents 44 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 41 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj 24 0 obj << -/Contents 45 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 41 0 R /Resources << +/Contents 48 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources << /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] >> /Rotate 0 /Trans << @@ -143,121 +133,168 @@ endobj endobj 25 0 obj << -/Outlines 27 0 R /PageMode /UseNone /Pages 41 0 R /Type /Catalog +/Contents 49 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page >> endobj 26 0 obj << -/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\)) - /Subject (\(unspecified\)) /Title (Android Cast \204 implementation roadmap) /Trapped /False +/Contents 50 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page >> endobj 27 0 obj << -/Count 14 /First 28 0 R /Last 40 0 R /Type /Outlines +/Contents 51 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page >> endobj 28 0 obj << -/Count 2 /Dest [ 22 0 R /Fit ] /First 29 0 R /Last 30 0 R /Next 31 0 R /Parent 27 0 R - /Title (Alpha definition) +/Outlines 30 0 R /PageMode /UseNone /Pages 46 0 R /Type /Catalog >> endobj 29 0 obj << -/Dest [ 22 0 R /Fit ] /Next 30 0 R /Parent 28 0 R /Title (Alpha app checklist) +/Author (Android Cast project) /CreationDate (D:20260608122522+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122522+02'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Android Cast \204 implementation roadmap) /Trapped /False >> endobj 30 0 obj << -/Dest [ 22 0 R /Fit ] /Parent 28 0 R /Prev 29 0 R /Title (Alpha backend checklist) +/Count 16 /First 31 0 R /Last 45 0 R /Type /Outlines >> endobj 31 0 obj << -/Dest [ 22 0 R /Fit ] /Next 32 0 R /Parent 27 0 R /Prev 28 0 R /Title (Product positioning) +/Count 2 /Dest [ 24 0 R /Fit ] /First 32 0 R /Last 33 0 R /Next 34 0 R /Parent 30 0 R + /Title (Alpha definition) >> endobj 32 0 obj << -/Dest [ 22 0 R /Fit ] /Next 33 0 R /Parent 27 0 R /Prev 31 0 R /Title (USB-C / HDMI \(roadmap D\) and USB-tether cast \(roadmap E\)) +/Dest [ 24 0 R /Fit ] /Next 33 0 R /Parent 31 0 R /Title (Alpha app checklist) >> endobj 33 0 obj << -/Dest [ 22 0 R /Fit ] /Next 34 0 R /Parent 27 0 R /Prev 32 0 R /Title (Commercial \(Play Store prep\)) +/Dest [ 24 0 R /Fit ] /Parent 31 0 R /Prev 32 0 R /Title (Alpha backend checklist) >> endobj 34 0 obj << -/Dest [ 23 0 R /Fit ] /Next 35 0 R /Parent 27 0 R /Prev 33 0 R /Title (Crash console open API) +/Dest [ 24 0 R /Fit ] /Next 35 0 R /Parent 30 0 R /Prev 31 0 R /Title (Remote access \(alpha \204 RSSH\)) >> endobj 35 0 obj << -/Dest [ 23 0 R /Fit ] /Next 36 0 R /Parent 27 0 R /Prev 34 0 R /Title (Stream protection \(FEC / NACK\)) +/Dest [ 24 0 R /Fit ] /Next 36 0 R /Parent 30 0 R /Prev 34 0 R /Title (Auth & email \(alpha\)) >> endobj 36 0 obj << -/Dest [ 23 0 R /Fit ] /Next 37 0 R /Parent 27 0 R /Prev 35 0 R /Title (libvpx / Opus / Speex) +/Dest [ 25 0 R /Fit ] /Next 37 0 R /Parent 30 0 R /Prev 35 0 R /Title (Product positioning) >> endobj 37 0 obj << -/Dest [ 23 0 R /Fit ] /Next 38 0 R /Parent 27 0 R /Prev 36 0 R /Title (Transports & protocols) +/Dest [ 25 0 R /Fit ] /Next 38 0 R /Parent 30 0 R /Prev 36 0 R /Title (USB-C / HDMI \(roadmap D\) and USB-tether cast \(roadmap E\)) >> endobj 38 0 obj << -/Dest [ 23 0 R /Fit ] /Next 39 0 R /Parent 27 0 R /Prev 37 0 R /Title (Receiver / sender UX) +/Dest [ 25 0 R /Fit ] /Next 39 0 R /Parent 30 0 R /Prev 37 0 R /Title (Commercial \(Play Store prep\)) >> endobj 39 0 obj << -/Dest [ 24 0 R /Fit ] /Next 40 0 R /Parent 27 0 R /Prev 38 0 R /Title (Deferred / post-alpha) +/Dest [ 25 0 R /Fit ] /Next 40 0 R /Parent 30 0 R /Prev 38 0 R /Title (Crash console open API) >> endobj 40 0 obj << -/Dest [ 24 0 R /Fit ] /Parent 27 0 R /Prev 39 0 R /Title (Related docs) +/Dest [ 25 0 R /Fit ] /Next 41 0 R /Parent 30 0 R /Prev 39 0 R /Title (Stream protection \(FEC / NACK\)) >> endobj 41 0 obj << -/Count 4 /Kids [ 18 0 R 22 0 R 23 0 R 24 0 R ] /Type /Pages +/Dest [ 26 0 R /Fit ] /Next 42 0 R /Parent 30 0 R /Prev 40 0 R /Title (libvpx / Opus / Speex) >> endobj 42 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 821 +/Dest [ 26 0 R /Fit ] /Next 43 0 R /Parent 30 0 R /Prev 41 0 R /Title (Transports & protocols) >> -stream -Gau1->u03/'Sc)R/'c[E-=Q6dcH#?F9/SOC>oTSFoYrjCc'lSBm/m1ZF4*st%Rr^O+sg*6SG1-PEfNio]7VqQ?36XoE&&b*4@g8R!YdnG!G?-8B9-jR",as_iW,]$6J,'82pVpX*.'mB!Vo>*cX=hQKt6A8OI)sYV5PrO\ZA%hQBb,a62C]3O)Y$[I#%UD*R>ma"OST-MFeEn34p:VqhT-_8PSYi_=`Egk`<,&@n/3[bT:d'Thcoo`DNE1I"5"9!UW#rG\=&NA84>?2h_7B!%oef]JD,>?-;'-mm#M>2ti>&I8HjgK1mie.^,&e66*aGS-7d;V$GCBK(]J;;S\WNYDV,ff-/#lImDdC^(OH>-BQmK.MWUKZSf4bUhN_h=19dR,HiWrle1Om&5M0Tkjf_UrGD5EBRU*.R8OQ"@@':_D^p2PEAg$r!?/\MAtB/Hb-D_MkH&V6PCiabR?^s+dR(?.Tsulmc-3Jr>U/PYS\o;C5Bd19UH/>niKo#-X^1@EAuM,?O*51m0[5H(.)f!3<7Z)lnPQQ>76%mZXm\t$>(;D-@$!fBb[>(`8nl@Q\I4=gOOM7#lK;q_+_!h(SCCZ"e5IKfbRDH="=CI;Qh4H?nrY<>qYQ!2IMlqJm#/N#BOBUkfS#kendstream endobj 43 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 3729 +/Dest [ 26 0 R /Fit ] /Next 44 0 R /Parent 30 0 R /Prev 42 0 R /Title (Receiver / sender UX) >> -stream -Gb!#^>Bei3&Ur'0^n\6@-ka0aace9t)su@>A'%G@lQ..:KHtF`JkK!bggAqVhF6raBA4K"`$PKpJ=46t2]j.S&UpP>Ire-$`JP'W,pH3l=qaI>'XG:oo@:H;hapC872<1l.Omh.NAhQr0&=W[qbWAY=0-uZaK0U@KaVg]WFI.f-2R!m?hhGL-n;b0Y[k2oD-C=5p3ffX$=\j(S(8%po0dr.d3V$j5au::gp/X;F:[d&7nk@n"AtJ'ua$u#P]h:nE6M%,RN!j.I(Ym8YbH+8Td_P-BT&pe;>:#goXkd2M+Lm.adnJ=FrN,Z76MR*c%K6,(cuGYp,n;>kh"A@$5#N8lR#;8I"RGoe.F#J!)GRESa-MOtK'H:*J`Va[`S,r$-G7ac1\/&j:7LHK9<+;:S-CM`iX(0TOq)KoT$2*(eZ`;&@8##l^).:9@c(*".oRkpOPXL.8Z`8s5+)j&W23O@&gS3,pOBgD2M+Y?aq'WUNn;U=aJPWla=B:PAs83UE9;)(a!E"5c#Gk9LrqS&q);G'r4(_g5#q&i"$P]B!^IPKrO]iQSS0PD3WcVL4pe[qV1+.YGYc)cJ;qTSX+O3uL;,d9RPE.IYKSJCd?iC:Qs7INR8\:(&*:O&Dh+E%dBF`EGCDOKO=;a3&LcuAlY8l$h!`bBF[=>@(g5Ko,ro7)hLG(Q(:S9UA3Z$DkHKL'?0c`=C$Vu[WD<]<6nLkWp;u9$X=@HQ#64[I-]BJ7.Z'C>hu3B]deQP)1Nn%cLF7j.UB\!u?9EHak5>WMWl76?.5\h=g&QDoci?j]K;?YpJ"a[E+VQ8^>&44Uc5r/2YB?d0e#>$eja'arWD_<@CfVFuRK=W(`)0N0:ZHBuR4oDeYWluc4L$d3pSW7O&N_+qrQL)2-K17&%&3M,ioP^&cE!'(Hn=K%ml3%N.Z[uLVR*bIQb\`=n^b4b]*\OI\SHNo@2fDl@=W"QE81Nr$U`Q.!TALm1qDq'1Du2Q=:BM12@E-0FT?%mea'S%M?0P18sMBU(R8`g=MW/L&.cYg,9kX>*\i5M%>MLL]2+rHs&dk;jQcFp`WZVr8bOBETZ*53SIl.]P>n@i41.BZSlN91dl_0(o%C'AdDU/liP(1\di\ImOaI73<&aniMIYc=<<=&rO*nQ*.L`T9O\X>r_9osjD?qeVQX:Coa72c)sE!cU*c\`j7mf7pnm>abH\aY6biC@B+9)7KHYKS;UQqq3Qsr6GGpPi6..Ktd^lZBO;kT:$*SVgj^aA&q>;nn!Vr",Nt0($+KY\,s%^P0FNdMW4^A=-Hk]UZFDu4kfC'.us!t@b3_?;)%k-S4ob+0IY)WX`HFnbc^,>B7ph]K/-Vu!nps-3Mh\SL;t/9]2,3OeA4=HQ]gRZ5#$dX:!m<%ijK')NZ(3,-2tZdIup3A3)DXZBbH)JPa3E:9Ol+j!s'o/#O(!i=]KaUGE$7QFthl3JCI8HA+?bM@"5^=$<5'Z'tASKK=F;o?RML*L^$[/SOXN4j)`#ekF(%/*nkDa1nnB/j^&bbBiXd8D60,"jGu$hZqFk-&.Bch2u(i=!*"L%#f3rTfBG-kb.)H=?lDK"C[7tYOWB*HH]SGI@YE)IX483.2X4W"gZ"H$P$G=7b)W.fjn^ASX@VTZbNtH6t7#7=S=E\395MJNhg(q"t#!3rI!hmT.fqrUL"10#R?FPh]tpomh'LYBddN:KI46o>f%dq>;?+%]+47+Q<^%'83Iu`4!Gn2ni_o!>9eclkBs_'[XC6TtZ@T^ac0WdGn^a/g9[c9dUPX@?jc-K#asL*\\&uQo./5;f%'+ZjFm>645ne]S1gM>Nf4BHnYY*4cRh0(KJ^nV!YRSL8$/LUbs-%.'f4_33a;r$&]i.\KYa><])A"Y+pkZ.Tb[AP-1!uP(/V?4K!1[W%lr:_e1]:aATSqV@^jfb:bfY]-b#%'>oULDjXt$R'2rWCIrQUX)'jLWghe$9hY9_cacW=Uu0&C%?KFr@"S,Ad-FgsdeUb6NUFZ$nateDLdkGM__57E4QrE;0N=S$,HpAs+kiGS5,A#/1#96Yn8QLn8PMU_+>]'lg1t[.O\f/T$sTI1$d2-+%Lbh_$1\m'pjZ_K6&UHkp!9uAK(W`r^]ce&l#j=VZ5ST0=*<+*.-3:8>E$LK;V_69@-eA1kh:@'nlI^fPq.P(p#jQ'>+7L7^c'kbCug"56tq#M`)`LtiH=N/oM@HQ?_5KR`tfo>M&KeSh(E>r&qC20Ld7R&"koUPL!%ep5[k8VnQ^-SSU6ZKnUa>p.a&+4bjr/ueV8=cR#$)QZah)OZc+dmF(1Eh`hJ^TZc$d>CY(%e:hDsTJo+Vk;)NrWhua=-:_@5O;990Z"qZOT,Aj#"O$qJ^V96"q3+Qb9!Dmo"@&Z4%;KEp35VA-2S&<1>#\rUcOjRIXVn13]LF$LaGu:+XhSB1FmMb%2$VIG7JX6HB5pe:-^L<=MGqEXsO7U_c;+a)CB]C&bp&BZ0,0Q"t`n9R2GY$m*n4jsr-(DT0amGhGErYlQ0=K5hm%N8]M]":Z:ItH\"Ot6]i+A'nFp@lrV#pP__nB55(?7Hhq8c0H4o5#i6Zm_F]SX;u&+]lbqu~>endstream endobj 44 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 3017 +/Dest [ 26 0 R /Fit ] /Next 45 0 R /Parent 30 0 R /Prev 43 0 R /Title (Deferred / post-alpha) >> -stream -Gb"/*CNJK)(B*Z.EBg:eLP=TXCo+Qo`Y5Y,0Rtsk>HVubW<'t);PXMXs8CdiOq8`ZM(siG2<>^.e?-I@\*r@IX0r-Tr^bFTErg57L?=a(OppA"8?/?"p_nJ=ZTla2\>S1!6q,Ose^j8KU#KQZ/,oofB.iYKJ>sK=/BKrCj$c4AqYj0I#/DjrN)h2jh@lQ4]H/m+J<\=m+'+.!q&,=[]&b?0Rc`t`]G;0":`p^IFchOi/-nDTrMu"$;a*3ko3Q(iJs*m6Hg2]j:,hW&8IQA-!G`G`L_IBW$#=`q_>#L8/,JQT=&`gGhTseosZ+er>7Z70KWbb'PFRO*bHGaX8F"Jpo!=",,]GliQ,m]k4Y@Lm]s=>[Btm__,[E"b%6'*VAr0RKXe[/[BoB1_)]Q6VT0?HLUf'U&,TUDo;6%VBNiSkiYU%/PZ68j#PooSl+%Cs.^5J=majfP6&cdUY^>>mKiZ\B^CakuqC0+W=+0h9Yj>9eIGf0od(eqj*52*t?XN;)F@F6j*ClSZT"fc?a&od7(UPf,o4T#[<$F,?GCkRT_Y_I.;fdWT-,D$Z%Sn^rVIi[DgR&p,QGua&o(FR"Wit;u8)Q4p#;mnF3T.q=SBAl+%KLA@VkfKAPu?RQgR[Tci-5.EBK'JW7'm`M7>km'fbR<$Ll"AjCbt@%lN)rI4(#_)$SJM>DZ4YVMo:PA2(D37K[P6iJYnAPh@Cmi>JYiHsQDV(1cI#@0NY7T'-QTMr@D2=gC&6S5072!=PO003.r:cc6`e6S`$LY=cUJ)M=janhE[I(ed2_#/!PH0ZdiDn1&.fe<4O]X/#d"1(F$I-pSHlXUrkCROUk>TbKfp?'>C[;>&oYP*'pd#oZ8aTObl9.AJ5r4nQA+(9)'=+&71.j=8Uh"HMXuC:[r*3SeZ7)$a\_YCA?8?I`;VE$J*uC3$Ru`fB`m)OmV(2I&Ua0[6BqWld"+MOrP&^U"61FC.lCfA:3b!X`V%tC:IH3EF!Zg\2`9B2*ac/UON'2AR*CTJ1q043ZS)UbF*80+*Ws&9@b4Q=rTra`Z^mopb,k5J:$S*)K6(NdP&m0[(Ap7YoN?);0;00Z;J$FW3/UR\]pc@7.911+8a:TgrI`##'tr\D7i=)riNQA:j7:9rHOp?k"cHaq_PNFGg3U3.l!1!?+qHNg.XcBDakj@-H9SMi>,??=4On9>AfRS"@qhi1RZBkED5p?3W6IhH/m)S6'5-).$?QQ(*d;V4,T6TV%aWgG6$F(18K&i[-a5*9QqX#E[h'+G\^>?-jj;:gdonuA^QTZ%ppc$*Hi:Q"aX[=2W'4ZCM4Fs[A]4W5QfR-i29jM&3(kC5`J^AUUV"F6O4rc/R>Il`Jli_PqdC^qVJ_B?]4,Xj&f=#B[XCee8bl+&F,;W5lNWjIJG+rAi3<)HFQe>2o#4^%Sf0r!M"b7Hm-5,DUZ\\2gd1BPb0IC.[_H)Hcts%@V"pQlA:r6nJ;_kcUZqi+"T/_80MBZ]INJS/1,83WK67EnX7]mXA^bAEmKn:^4)098b*W?<1u-)q'!DLa/#SF["*,MEV[=So9e9)3L@#.Djpk%&0ff"$^kF[aUi--_K9S!DhtSX+6aNMJ166ke1'+.7AgXe<^+?s>E:R+:FiD8c.lj*J]3EGYB+>#AFsF4!2Xhb)SDSgRc56@ZBplLc@g"="VI_n25NPaE.J@^;LJf_S6$7Wijo.;QWW!RJ/FDV_eu[48c+k@c!daQ<9',7OR-(KMiYT\;eE'^o.]>r$77i3N"a`;4egg"]de\=o3_]%lhl-Xk*Y#j5iAY!sHk:[rlap5f3)bik-$l_('p_%a3%"ete3hqXpI=,?*4uD*=F8eAftTAob\XN;YZ:>mI>sV3JIAs2KV78&%qh'gDU,Iol+4'cuC/ZH>3q:S]dKD5OMI1e)e"^8?u+:BOjh044M45BmD+[+!LdGR@[>nh[*,GF<`%#q]VQ[+6#jNl>L[*#MJLUFNk3*Iq`o27YL1>r\&#_TC;Y_<%c%7Qa2Z;^EOT'(Y!"2$Ym'Q%S4BOik-`bq*HIHEVC/ick0JWp&Q2#a3:Z;q.nPJpL\D0\c>4Q4:b[L_o^rMm2.Z_)uE)#i+JZ5\q2Ub@sP5@G=)*%Z(%MLh+5+0ZuoM(9]M;=$DQ<*7NN1[EmgXnSf]:YVO8,/n,H[^jY`0Ye#FFrq4?^Jr^V+3f*W.;j*us%?YX^WB!6%EZ29q9Iq^31`^3@Y&G(1#,/%W7)3;I_;cm'5El?B)Hl&>X>bhb(A[5NmNf61Rg>^K(=;qneir&KV[,R8GMXXJDp4=>oI?E(fh?S#k%i\I4Vr"\H<`Uk6A^jL8;XO54!dF3Z)3[g^&N4a"H!GDE7?BRq"~>endstream endobj 45 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1512 +/Dest [ 26 0 R /Fit ] /Parent 30 0 R /Prev 44 0 R /Title (Related docs) +>> +endobj +46 0 obj +<< +/Count 5 /Kids [ 20 0 R 24 0 R 25 0 R 26 0 R 27 0 R ] /Type /Pages +>> +endobj +47 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 871 >> stream -GatU3;0/I&&:X@To[?i![iurNg`)*;G\*TMP,k]W'fCP>f`4BI0^J]6CX@UT\N(:[;IHA7qO=\'mMR8]nWM%E0*8Y&?ndj2P/1:PU^.;eF"j0A92@TNNlHZ1PZ6#:+[5)s%P)ma.p_mY1m1A%W4F*Eps>@^OA]2OpB5miC1@F9pVS,\4#'8W]qWULQr`40s+>%l#cU444@6&KLEiO>!kfG#B%,D1.6>URR.GjIE'Ju2q)!`E1S8Rb"Ig_`-(OfcJ2pmKDheji/:%Y4l9Bar$I00$c3knAGh$KgTe)/@d:Y^==A7+*V\t!)_%=Y9E+13>.JPn0q\`0YM1ESeXqc@6@kiO!5a1`("]6=]&lY*/D&2nT(_aKTBhJ=;;u_Qd+$F$`CPjT-o57k,";P69Xb[PUAJTlW%@(ggp?JL>(4U0;X(H`>D3+6LlG0lWLFhcg<>Y7,08FOk%>kj"(l_Nm_*ek=9p;r9WRD%r:!e[aoFKK]rg-suQDB+@\P.dp<%d'0n.ptOCfG8g1>XBeZ6?slC@+i`"QdneWR,)Uar$()FN7rfMF@UOpefHrR)PSm1$+d-+ka[MF6JnU>'S&$oM2!K\IFqJ*m$D.L#0\I.!AkaP%5HfUb#kk?lSm*6'5C.s19>3oo9EOP:%J5B+ZRc=C0Rc16'Jo's]B+!CPQ9"i1W'2<4AUn+Otu#e,_68ppbE3I4*@A(K\K0Z?LI3=5,Wd\Cm8C";Wd.Ka\bD"jp7@-%a[b.6s9>&QB$\lAamMm[HKeT54dLH8h(1o#tHj!DbO-[e@nOm/CU!AQpTlOUF^1WqCI/^[V#jjO(cc3JdP=N57\Nbg!5[AK&Jo#Wr)X!N6;Fg3,@">2*el3\gK]Niog'EQ?o*rI[\XWlb&ZHmW?2<]3=G[V)3)LN_gcRD1Wfcc!pO52i[C_jdXqWIX>.%/EHcAhp_!nRu_cCT)!'f!pdW'7kRnqgr\XY=ef32_k-&&6Z#lS[1)R#9p!phisI7_M'YR9'!$[e\sQ(^joYch?U>V7SJZl26R&IUVl:!pbMkejPc?!51s\2E&Yk-EugOh'eB,b&Y4\Oj#:r+N:MjpZXUpV]Vf6eU8sj*94A.p8,,dmTF_Xp[j\\Fo?%nN^I@it3-(!&9E=0YUBU`M,UGPBC%D:>q+Mq\T,nUN\,#m7oZqQM-J=4RVd/"rM^g#%_m;!@ic!7gp6~>endstream +Gau1->u03/'Sc)R/'c[E-=Q6dcH#?F9*H4Q>na#>oYrjCadU/9m/m1ZF4*st%Rr^O+sg6:h"Sp;I#^o$gP`F_?36WDE&&_)*(ZA\4QJeK$"qBRK+LjES]E%KD?ol"a\F15UpLaE&DBZ7:*W$.LG6!>0dJ1V7ocR"T#-k/!t3-U+><%IPbKqWjK\h5=^&--#a-9kI"^NEk?H_[p3I5%-\sr0;bSC-^u=krfgTub9cA=n$Z99U_JlMc$1k4FjND4kb4H;/+8RfZ:)LA7cNgsj(XUu:Jp4(Q:SnU8G#J[.9Am*2eiV[gd&-a.qX1+A@;?BfeON'!0hB8=YOX#S*Q7L&`JFiF?@!1=,\W6heMZhin5YTdo/_OHr==]'``Jm,hZOS'IALC*8neD^c@siR(p]I,/Za4;T.c3ccihW8c/4['V4I,!\CQeMal!8WP3'Y-9IBuC4gjg]+YtFn2)e?)h6,#8]'Z.dEF8Ron`fQ\:>gNjjhb3*D.bP**!*CHU_u)-A3p<8>Mq-nXHQM!e/.=NQDID?B2,78"oAJ.O\.m@7,80g$%#$:9P7'Me$A4Q[A[Ii]BFc1K"_e*UBE+7a4k@gm7+H":=#@sIN=D4GObk5IDHWnc!C9U[BJl'[Z@p6io,"])UZ8+3Dlm+[8]WMEc+R5>S\><)&i4Waijo)q44XO'@=2&g)nr(3rfDs[FU1:$Dg.*!GjPHa)*d$5i=a&=\pe^!E.%OGY`bKPEVrs7jRieL_S%?io8GD.;?-Wk\U>8H*u(>0@^F#$>,JNgu[O%-k.M5#5#KE(76*u6<1H%Z&30lBlrj@0_c*;-%(4~>endstream +endobj +48 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 4055 +>> +stream +Gau0G>Bedl&q801W2Df(`!G0&n]PC#'@6Fh^r`*Pp2G]7JAspX8A^DMf5J(b#[L$a3/J5QWp[$mY57GLNW;3@#iWP<%cD@;rG)E:&a!j?A57mT/PPUQ%MmXcb?oCZWl5r@.0P15g593ntOjunW1o1Xn?-`)_e7rh[q5dKd^]oOgX9^d!a8XGH6AaQse:sNd-;TgCSV@na(nod>^YDT6h+?^L7$[)u(6I.36(F_+IDpT<[,qA!RA/:o6*bpMFLW>n]DD)hX5';1h&#agU51Q8KGbsY\:%Da2`*^/qp(gf4>J)@0'RCcugdA4cCb]6Wj8rS$>Q,D0rp4.moYWAn_%<)rLAZ"IB!53\`Z>$C&iUkOb\!I[41iuWEGbPAbb>n:bXZ&WFiPYKd>^=(AIVF*4(4j,IW-R&$p#'":(Mc;RF=N)%58G5\>VSBcDIr$kO[3;R"Lmu.YTaXqd\=k0JK*YHm;h6+qOI.+8J;LhrTF:oJEBAkV/9X%`T.Lj[ABj,O.b]bfq-[D["KXH`>66A>ZL9K]p8DK9ES2Ci'OC0E42P4@*\>@9pEhpCDRk5'Q(Fk^5!h#;D/CuNS'4Y^oF[R%^]C&1!U_feW0cnR`)gejkVe2kKp%!Ur'G$J^mlk_WiS19!2qSBN&Y?TPaW1o#;Mf)IKhBMj:tEBmkKR02%)A!4"Epq58L@^\paK=icZYZsbasX_oXu:?kMTP`Vao@KKIj=d(H1*"Dg7Qe/DaI_.`$\AXQ9ZCJ-,sdcj0aX3=cl;Z*luNJgcntFf:]E:=-'O)\jmW$$rF[9Iu(/>rTW)+Y$!nAD&\Y=dN;e[Ek_,p&t%%B7jb3+pM]/SBOaTJ#>AT")IHK?=.D@mu48OkQfdu+3T\@nK_`#KN#o;m1\&!U;Yd4AnYI#+2FRWjpG/J!Dp!n>DYr3Xl0V0!]%LoMIAOO%rQ%s48Vj>^r@Vpf>-@ZdE$G.QM"b&[hE4t^d&"s&bL@_l+R#AdF+#bffQ:=oeY4Yn@@>A:>l6X7boDk^jFdms7H?qqoMGm-ppifNCo:+i`17m9KSP5B6qpFNmjQ_Q?5eQg%[Hn%CV(PZ`gh!iXB2^Xs^">=IIC;iApSXL#ru5*8kMuQjm#jA6MX*>@\Q/+@!Cau?ApHNWc(Cksjn3^Nq9Pj*D;BK'9kN]2/>e7KqSD$5AmA@R/m(*s_eb>nmVY%6\A,cRPWAr%$#eEB96CIO.u[4?[Q]4QU*FP-4gs>_1;Rud+g6]%3Vb!^,8sZ5PAe=Y[#%$*)VAQJKOmcI[httO=IeFjn1!HN%RqRP!tZ!1%T0;R1pT]<1E&i3-)DdF-UFjpp5h]d'f`H8:C)jg"hDimS6FJoU9:8K>UWY6$UN8"^[OJ_9JbDsLeGY!gOJ0<8hH`@,0CLHT0:uimM\7f!P4&S?T3QlMQALNBrI;dX4tE7::,'*+\/m2LeGF3Gl?:17oNQ\q1KHIYT!!9a/GP\pj#^l!puB4$\C7G-aU]Ur=o]rB/SDW3S:fgMN(!9/EWL8(YZ,1#1c*@JIKl0kICRl1MF#*lRIm4e&;S-Sa8:61dg,fr%hI6;/)>hjnlFri4%4E8F"`/[!F[^'6qoAbD]=Z^6oIL3k9U):R7dGGVYa?^Q*2MY"&#PL\lnX$eHlhg#n]'XYtkak;Gb3m`fpqnJQ]=1'_&KqMK/N5,9K1#i>&L+6RctV@$/\qqbd-D@5\bgo"asXh6l0@?L*I"UE9"1]=4OcXA+$;51oS[2LS:LZsejr'r53!^5>49CiBW*dJitB37:h8)pSXA?s)_RHu8j:B=(u-]siNXET-=2K6o;[+T3_ju,\$E`a!gT>M*>t1aD=.eMB9n.01IN4n=VO)UkX[P!$jPb/DYWKX1ZGZmFHd9f!L8,`A*O169t^PMlgR.i171RJUAIARk@%b>&`31IZ1OjQn/rEl9@#][p_Upb()t.idT>]s?_fii"m_Re[GSdKT+L5CL':"XO62WC!FpJb<_Xqrr35_d@Aoc<>Tu*KQ\]3Y2]\5AO&o:VcmqYpH5k14L2D1;K/8CSJ5d?GU*Oar9i'16P;dnH[54_@n-)[:i)/8f[dK5e(fq4D[*l=,"SoELUP5*KJ^QPP01Y&DE;-jodd:Xk[-hZsC=%P&CIr%bX[qaeFq>Q-nPu.r_(2bn4'&r(+3d_V3.!/UrCDb=kg:3)+jCiqAieXJ8sN/V'[?sD=ktA=O@tYD)a(2Zi(n:R`'D&/TPZWm7bt%R3d\Cn'/k?MHDmV7\h`1-'tOYej%Q^sY_`r5d%d"mK#FEGG"1@OV=#RAR$,gA00LR<=>#Ma@9`(bmK]$n8T`U91X01>G#I/:d'QQ=kEM3U/=/$8Z9g4`gMO+hKsb=JWh&#=\&>`PCN^&8YJso+arc(pjZ65i;JK/pTLS%%)Ef3(Q/%Y-K@*:o+6#R.@b->Q2H=7,#%=ZP8%7cgdM%trak+XV2Hf#d?7*qrdbjM:](^q4[^]cmL]^Bc#M!l8WaU8I4cGiQlr\JK^Z+N1Y'\l%4.9"%j\Jr>qiu"9nX+FQ_MZ4Vh]e&u]R6:HC>h!i-"q>ib6Z+dJn5+"4L%!Ll&h^sW4ppOZ!,iG0?%*A/5'*]'@N2bn#uGND.X'33^0rCc2WAd07t^!"#cAdj`(1f`n!q;o2[m2"E<4'mn\Q,dX/[8=.8Sj$%#TH:F2?4)X+.MYsWj&&D4J8@Nj4m&*0esf8$!D>F"_B8l+[fjk6UU?]sM3V2\S_PVqCTrrMi[]<2~>endstream +endobj +49 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 3249 +>> +stream +Gb"/*D0+E%')q<+d@[R4@Us2+"Cu<6%YL>F`G]O2V65\*moUK9@\M1@K5LVdo`u_F&4g9tF*9'F#9usDBDQ2NTC,Pbd.,/45Fj7=nGnSE%58%r7US'["q8f9A8q%q"!aMH>FKFJ(,X8?IVCb1Z$97HK]<.VM/3P<%lq_rfA,'a^4i+odqi/=s2I.;(aM&m%`,`,9bYtE)l6;4Tr,p8nDl4XK%crSZ\_b6"d4$J5p`S_ZQ9\t\qn%u=&!=%GDe67sbHkE["S,\BN6)9M4k@[MBj+JafP7i([C*5;obd$i/hL$'J%@a+d[8ZGQk+:2r.M@@8)q[VoKLEd(<'Kr[GKUsRnAM!*Ion!K%j(-\Q/i$%!INP"9&]RK`6_%AYgjqr`l'4+3(WB##+FXqk'7LAbkVZ8-b(;4=XU,?$8]+B*,\.K68US2m-a,nrc5[.ALq)/ig`I=ca)2f!4(gTJmmj2^N&R!?*Q7cq.aE*]EALT].;Y]c-LA&eHW1$L1B#!57=rTSW3hY[hd+kTRCVMW//:U0V5G$\RDQ?(0UoVIb?k`eYGr\hpP/j!O@^ic6SkpK8f)Cqi]*)Pi5l]9X42PdQ2EXZH3!6g6B:iT"Mjl,Xn(L6"!5S';L@oR(1eTMH:''PAQgqC5rqnh-hQ6CA!8+Fs+iB>&6RIP@W4#S6[&S=idb"a.7KVg.s-^,P*U$LN%V6(=e`aH&:Ce%PS@QCBXG@[Hf,PCI12&%Mh@V_>P\V67kW9qtIM\bXT7r?]@9Fm-KChRQHJm&fpdI<0Y]]WeXh`N`kQ40-%I(!/%o+ot]CmW9oJD(r,=0Nn&!C='iOWb"*TXh`0/>FLrk<5tLm-<4dLGlrS_J;fe3C"CjH24CD.[sO%K@M]Cb(-3F6k-0$2_GOYDHpm6NfFcY8o31OoQr-tIF>/P^l4G`h_>SbL=u)@r"t+k)6ODY>X0O&CE%aqC&X*j:@?`?T'ZB0`!Y6Q7'[_*30Cc2Z[sg["T2>:^!HhIJnWZn)!2=9N&V0eeKBN\PMgiq]b)@,G`96#Yj^^D%76m8;kR48!"H;RR%3Li2h!Z&`jhmJ%Z!g@h+t6cXl&cj4RNT*I"D*en.,L?U2ZF&GOQ?I>'\FA[M#?fXAG_b*[2F:i2ug`+;kFO2nLa%`Z4Ni5GRT%0D6_3.$8&oiW/PX+T:2EN>kYKo(gtL1Mg(ocj<[:XR'H5Wu&`^WKJRMB?C:[J)o%M"b@[plY%[!/Ojq&_\Tptd/9#6cU&79RoIhM9OLF(hGpn3ZG5_beheT&r<`S2i-5J3!mC2_=8_`3JL5)o=nEt/%"g&pQWn[+0&X(.33"E?-T*nJf_AYZdh-VSPNq=(mn([V+j%]T[RfGnjgTd;sp_CHb3%`ZFDT`2+&p,dksX^GZefLb>u_9S+#Bp,l@`AGN?21r9j>qK(N*?&CcLO^9:a1Z,Ab_6oY901:*l@Hm8k(6\bgV"YP04b=e?hh7703lnC^o7^1s>.>^lA!8iD(g)(s]*_ss*T%o:?3A*>2uWd]URW)DTuQeY5&;D/\+4B7/1,Q,?\WKSOr;*ic5\`G],^R#To28_;pfsZ2d'J5aHcpa+>D+"aj)6+VODJIG@hN?P+a;r:V3GOjXg]TA-r[^YS5A.e?Me7_PAU'pU;63;fMY)0d-rD;<]c*lM_k\f.TQLDD6n)'?E-q/1-PkA#I34Y=d@[XA%L0_B\]d:d)`V:D.7=t0>::/T>iXY@[AA$^jVf-:%m14tMZ~>endstream +endobj +50 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 3081 +>> +stream +Gb"/)=`^?/&q9SYW2BqtG7/DCk"V.\[q92UXeTNQ_\%b=PU*,W@Q8c%YMUlB#U(?Vnj^j@L;TLnk*HAUio*BurgGH+GB;9I(Q]c6q&0Pp-_pJS)=s?4DM0%,WcPfu-qMfepnoLWD94:)#rX^`,Ee.ioJf\Z>p2tY!%$ltQ_*fl&N9,,b!H/k&Y(OuW7iM-RMWZ6hk&X:q[bLYk=73*k$E76ot\l.6kL6*H.4GiY_;FO1H1aR.'_cS+j#rpY7uru*:\d>MHen$?gO)nQRj:1S>n*P%'GmU9bT?qOOF?$`/-R-ZJRo'0U9G[cZ*DLrQohELK=sO[YK%/'2seC)2TQlgP"8SEr(4(lLn@[eAGM],rCM]JmIFB1a4:<3B2LCSF$0F96#ktAOW@G$AiDX!+F3*E&ELN6@pj=g(KHP;U5Ft>\2+n%)I"jFN..?Sun@G,RDUfU6lij9Wj:SEpV>7s+\,KacAgaR/)EJ<=4s2;O&p%a76_h.2(X9T#th0i\X*jano!EJTBc&*hM`I8"3-gb,G6+h8SunS9=3#`gU/U5]@+Y@@)cfk,D&Nfh>;O6d9l?rIs:H_Ut=@.>;7Pga>fcZqrfJ0AkgQ2_cqX!k3SU:p6eU5(_&Van*qbc!O`O99!4<-X"lXX&MQ>UV=!Ad/.bVGX@ehYI5`+)D]5nE`=e;6Np/Q[L?D$Xe*ld];MVAC*IQ.:MAmW!(3-aAaD7AYJm3b*%6JOhAR_Dp6@LM[W<,h<96Yha;PKQ469-@mDHMV!ARRWpuZk/4j7n86F`.s@o4"'22'RX?+JW@8&(B.+t/a=tJS"C/MeVESFpK-0D4a\N*O"=;hD-XOTmgIWC0!O+]3nUa?7P1T@)Qts!.lj&TtBh!#sf:`QEsI7T'0C5\XMV%d\2*,t"n(KAtL/g\K=fl)O`;6(_V!!l$UpbcW25#qK2R![FDgr=ms3#"=GCr-f_QTS4,jaqatg()GgX>6&JB=FdGg!UsT-!Nr^35`LI$\@SbKgZ_npYQ5IdW'.,c%oIZBhi=AXrFtG\pI]^P2'W6Lf$Ed6kRSaDcF"QI7S6J;F@fN6,uXQ/TOhmq&$QlH1l+Kj9@E?-P<.71TCk[ZVAFEWBo]RsN^-SskVO!Y^=2:O^pclgG@`S)2Q7;o/OSs)"sB^!o.0\MV0,77$n"mB/@*:#'/`)m+'g]G4PQb%_5(D_p2'&PS)0IipVgeG`eD.J,LI;qh>g%"0(GZjUV#(r:2gctfP,k=Re-H0XJYSTK+@Mf)ENe;B2Q<&7`&uVa;b2u4$729TM>C7ogjtZljcH0g7CXmEO]2LCR:]pet>%.LF?oa1;'86Ri>8OH+s]fBErZbe!iXgSF/43.BlY[i./Co"&Ka?$ZM@5Lu&/_kiE2-Yc7t`g)u/aNO&L_!N;!_IC;#EP*3%DEqi:Gr*Ap_6n+pYf!]Q#dAaT?bIi:dsEJ+cTM8F>X>G^e[?e\f6#If-H12bD^6mQPrWd0PaQVW997>600V_*>8].4tqcTBQD;eAP,TL@&AVX'&D;%/'\bcL0a'&:StBNq?Kc(cODu(8C+PHJP/0"o$'$DuJe2d@W(h`q"Rn)#lMJPkY[?24&)P2VH$Mcr^n\(1@:??iDu,e0r,f%n35!Zs81ee;9BeTo0M'WC;!qr.\h%a$C>c?*s3K(Cd=TZ9;YSdC-_b]9&'r8=JJb(/7O8'4f)7l%J;"i,gd[Hc\>;6NY\(p]t^Q:,_eONPiO"lMq?U*6bJEL?#S0Y[Uj<@+77TA+fP*X@i9b\&5>93gIK\Op"N\cci",;""KklQ^le2%(j3%bq5\sV-L)d&j?&q?4MW2,;n?U-e'Q0o8,t4u*.>U@Lm=<.I&\PDDlj/lUuU3C2s`1\I&&bm*#;FIj.AD6u7DR9-!ngZ2l:'[W(NJc[[d";)p!Z6?%#b-*K^hdZ)!$)00E]:XUhR@68aK`3th5'pOXY57PY<-tMPg0Dt-U\XJ0r:EL^kp@OnO4.A?O2B7W-Zlgkk)m-O9E[V837MTl6KYoEY^i>Smi/7Hl:eA;AZ>CnIbN+j.g5X\dCh>47"+@I9nV4OaEk\P(WjO6h,72U`2Dh&fJ\4G1<*rRn9Zl8rS#=fg6GorV!=X23;l&ErIVcX+Z4/M%Q$6VKFtV-_[fYlP%DPmTKeq/64H3nUVZq]G&;U8SfSr3l!W!_mk&"R8%>>n"TG]05Qa#kp"J&c9V7XLr\PFCjl;@mTfdGGrulIb0h6EJ&9a9V,XF[b@%;<::9@Jk'B=)cT8`JQ"ZY:(^=to"J>0j!)ed,9UO$?jKj!V#@HRHcAb-f'Se/De]issC2g>X.\_8$IMpZdZ*#b%2e%bU]8jS[A,qh!#@E3Tpk$c,UJ^ijEoGt%SqH>PE.UR%d_6cH([[\8"Vs3!(QCJ_C/GN3!;j'M1YI;p3C&bpIfO)@G9?PUM\D.5>hnIb%d^NRs<2_\DFbhT1DaeYRTk(]?7Tm=V<['1`O2?f_XrkZ*Ah7S&3]LZs$=G5dL9\okI(?'&lWD)"ha;R$6kD7NEWdB0c$ocT%e6"~>endstream +endobj +51 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 210 +>> +stream +GapXOb6l*?'L_]oMHNMn&X/U9\;X7X:mB`!^/+iBk+C-uLcuJk0m3~>endstream endobj xref -0 46 +0 52 0000000000 65535 f 0000000061 00000 n 0000000146 00000 n @@ -275,45 +312,51 @@ xref 0000001841 00000 n 0000001989 00000 n 0000002137 00000 n -0000002286 00000 n -0000002402 00000 n -0000002708 00000 n -0000002786 00000 n -0000002892 00000 n -0000002976 00000 n -0000003182 00000 n -0000003388 00000 n -0000003594 00000 n -0000003681 00000 n -0000003996 00000 n -0000004071 00000 n -0000004208 00000 n -0000004309 00000 n -0000004414 00000 n -0000004528 00000 n -0000004683 00000 n -0000004808 00000 n -0000004925 00000 n -0000005052 00000 n -0000005168 00000 n -0000005285 00000 n -0000005400 00000 n -0000005516 00000 n -0000005610 00000 n -0000005692 00000 n -0000006604 00000 n -0000010425 00000 n -0000013534 00000 n +0000002285 00000 n +0000002433 00000 n +0000002582 00000 n +0000002698 00000 n +0000003018 00000 n +0000003096 00000 n +0000003202 00000 n +0000003286 00000 n +0000003492 00000 n +0000003698 00000 n +0000003904 00000 n +0000004110 00000 n +0000004197 00000 n +0000004512 00000 n +0000004587 00000 n +0000004724 00000 n +0000004825 00000 n +0000004930 00000 n +0000005058 00000 n +0000005175 00000 n +0000005289 00000 n +0000005444 00000 n +0000005569 00000 n +0000005686 00000 n +0000005813 00000 n +0000005929 00000 n +0000006046 00000 n +0000006161 00000 n +0000006277 00000 n +0000006371 00000 n +0000006460 00000 n +0000007422 00000 n +0000011569 00000 n +0000014910 00000 n +0000018083 00000 n trailer << /ID -[<604792abc998f7bf7e113332cf4628d4><604792abc998f7bf7e113332cf4628d4>] +[] % ReportLab generated PDF document -- digest (opensource) -/Info 26 0 R -/Root 25 0 R -/Size 46 +/Info 29 0 R +/Root 28 0 R +/Size 52 >> startxref -15138 +18384 %%EOF diff --git a/docs/TICKETS_ROADMAP.md b/docs/TICKETS_ROADMAP.md index 8b41824..582b51d 100644 --- a/docs/TICKETS_ROADMAP.md +++ b/docs/TICKETS_ROADMAP.md @@ -13,6 +13,7 @@ - [Attachments (deploy)](#attachments-deploy) - [Google Workspace — phase 2 (not implemented)](#google-workspace-phase-2-not-implemented) - [Agent / engineering queue (from your list)](#agent-engineering-queue-from-your-list) +- [Open tasks dependency graph](#open-tasks-dependency-graph) **Documentation index:** [README.md](README.md) @@ -107,3 +108,11 @@ mysql -u root -p androidcast_crashes < examples/crash_reporter/backend/sql/migra 5. **NDK** — Only if crash console shows native reports for these sessions. Use ticket **comments** for findings; set **workflow** + **assignees** when you hand off. + +--- + +## Open tasks dependency graph + +**Master plan:** [20260608_ALPHA_PRIORITIES.md](20260608_ALPHA_PRIORITIES.md) +**Diagram:** [OPEN_TASKS_GRAPH.md](OPEN_TASKS_GRAPH.md) +**Dev DNS help:** [tmp/20260608_help_request.md](../tmp/20260608_help_request.md) diff --git a/docs/TICKETS_ROADMAP.pdf b/docs/TICKETS_ROADMAP.pdf index 88d7fdc..025c0be 100644 --- a/docs/TICKETS_ROADMAP.pdf +++ b/docs/TICKETS_ROADMAP.pdf @@ -2,7 +2,7 @@ % ReportLab Generated PDF document (opensource) 1 0 obj << -/F1 2 0 R /F2 3 0 R /F3 5 0 R /F4 11 0 R /F5 13 0 R +/F1 2 0 R /F2 3 0 R /F3 5 0 R /F4 12 0 R /F5 14 0 R >> endobj 2 0 obj @@ -17,7 +17,7 @@ endobj endobj 4 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 45.68504 716.2874 172.853 727.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 716.2874 172.853 727.2874 ] /Subtype /Link /Type /Annot >> endobj 5 0 obj @@ -27,37 +27,42 @@ endobj endobj 6 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 45.68504 705.2874 199.845 716.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 705.2874 199.845 716.2874 ] /Subtype /Link /Type /Annot >> endobj 7 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 45.68504 694.2874 167.949 705.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 694.2874 167.949 705.2874 ] /Subtype /Link /Type /Annot >> endobj 8 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 59.68504 683.8499 130.545 693.8499 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 59.68504 683.8499 130.545 693.8499 ] /Subtype /Link /Type /Annot >> endobj 9 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 59.68504 673.8499 224.3325 683.8499 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 59.68504 673.8499 224.3325 683.8499 ] /Subtype /Link /Type /Annot >> endobj 10 0 obj << -/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 663.2874 192.861 674.2874 ] /Subtype /Link /Type /Annot +/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 663.2874 192.861 674.2874 ] /Subtype /Link /Type /Annot >> endobj 11 0 obj << -/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font +/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 652.2874 154.637 663.2874 ] /Subtype /Link /Type /Annot >> endobj 12 0 obj << -/Annots [ 4 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R ] /Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 25 0 R /Resources << +/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font +>> +endobj +13 0 obj +<< +/Annots [ 4 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] >> /Rotate 0 /Trans << @@ -65,24 +70,14 @@ endobj >> /Type /Page >> endobj -13 0 obj +14 0 obj << /BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font >> endobj -14 0 obj -<< -/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 25 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj 15 0 obj << -/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 25 0 R /Resources << +/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] >> /Rotate 0 /Trans << @@ -92,79 +87,94 @@ endobj endobj 16 0 obj << -/Outlines 18 0 R /PageMode /UseNone /Pages 25 0 R /Type /Catalog +/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page >> endobj 17 0 obj << -/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\)) - /Subject (\(unspecified\)) /Title (Tickets, Gitea, and alpha smoke delegation) /Trapped /False +/Outlines 19 0 R /PageMode /UseNone /Pages 27 0 R /Type /Catalog >> endobj 18 0 obj << -/Count 7 /First 19 0 R /Last 24 0 R /Type /Outlines +/Author (Android Cast project) /CreationDate (D:20260608122522+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122522+02'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Tickets, Gitea, and alpha smoke delegation) /Trapped /False >> endobj 19 0 obj << -/Dest [ 14 0 R /Fit ] /Next 20 0 R /Parent 18 0 R /Title (Gitea \(/app/androidcast_project/git/\)) +/Count 8 /First 20 0 R /Last 26 0 R /Type /Outlines >> endobj 20 0 obj << -/Dest [ 14 0 R /Fit ] /Next 21 0 R /Parent 18 0 R /Prev 19 0 R /Title (\376\377\000A\000l\000p\000h\000a\000 \000s\000m\000o\000k\000e\000 \000t\000i\000c\000k\000e\000t\000s\000 \000\(\000t\000y\000p\000i\000c\000a\000l\000 \000D\000B\000 \000i\000d\000 !\222\000 \000b\000l\000o\000c\000k\000\)) +/Dest [ 15 0 R /Fit ] /Next 21 0 R /Parent 19 0 R /Title (Gitea \(/app/androidcast_project/git/\)) >> endobj 21 0 obj << -/Count 2 /Dest [ 14 0 R /Fit ] /First 22 0 R /Last 23 0 R /Next 24 0 R /Parent 18 0 R - /Prev 20 0 R /Title (Console features \(implementation\)) +/Dest [ 15 0 R /Fit ] /Next 22 0 R /Parent 19 0 R /Prev 20 0 R /Title (\376\377\000A\000l\000p\000h\000a\000 \000s\000m\000o\000k\000e\000 \000t\000i\000c\000k\000e\000t\000s\000 \000\(\000t\000y\000p\000i\000c\000a\000l\000 \000D\000B\000 \000i\000d\000 !\222\000 \000b\000l\000o\000c\000k\000\)) >> endobj 22 0 obj << -/Dest [ 14 0 R /Fit ] /Next 23 0 R /Parent 21 0 R /Title (Attachments \(deploy\)) +/Count 2 /Dest [ 15 0 R /Fit ] /First 23 0 R /Last 24 0 R /Next 25 0 R /Parent 19 0 R + /Prev 21 0 R /Title (Console features \(implementation\)) >> endobj 23 0 obj << -/Dest [ 14 0 R /Fit ] /Parent 21 0 R /Prev 22 0 R /Title (Google Workspace \204 phase 2 \(not implemented\)) +/Dest [ 15 0 R /Fit ] /Next 24 0 R /Parent 22 0 R /Title (Attachments \(deploy\)) >> endobj 24 0 obj << -/Dest [ 15 0 R /Fit ] /Parent 18 0 R /Prev 21 0 R /Title (Agent / engineering queue \(from your list\)) +/Dest [ 15 0 R /Fit ] /Parent 22 0 R /Prev 23 0 R /Title (Google Workspace \204 phase 2 \(not implemented\)) >> endobj 25 0 obj << -/Count 3 /Kids [ 12 0 R 14 0 R 15 0 R ] /Type /Pages +/Dest [ 16 0 R /Fit ] /Next 26 0 R /Parent 19 0 R /Prev 22 0 R /Title (Agent / engineering queue \(from your list\)) >> endobj 26 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 711 +/Dest [ 16 0 R /Fit ] /Parent 19 0 R /Prev 25 0 R /Title (Open tasks dependency graph) >> -stream -Gatn#?#Q2d'Sc)J/'_]eFTrcnA9Vb:<7EO(BWH,fuDSW.%=d=KO"%grgasL:\87:D;oD!a[3>?jgB_Te%J;Y,S.0[j:SHBZDD[:*3PQjiZ*Ws6'V%/J?@$n67to6#cEVFKCfc(:[iZ0"9e^\cS=X>')i6_I6*66*.AEl"2["L[,jedr:\/?%&])a+T6W^m4,f]s7Kkaq*)M=*QGafanHAsL/a0Ajolou4=*]S#GUOl*3FkT@WL`DWLuuH:IdeO[d`FnhkadVc)g\6]4nQ$7.aNcjpN9seA`gWl3H')lK?4X!`AE%-:2Z858EeX4b!ZQ"$W&j*5i+?MgGi\*g.q",#S3cRQ@5nJNS(55EG;d;B^K%3[q]H##?BoO`jK^D_\edka>)mB.$In4IBpQ1CR=lGf0qdu9!^e_[*:21I[gPCHpqh['GDC.*r,SV-D,drGf9h$.YEp#rd/m,_DC8BXUZAat/@^CQ?rqJf)RKMSBJ`b]d+Q8mQ5nZs#(HXr8XHF~>endstream endobj 27 0 obj << +/Count 3 /Kids [ 13 0 R 15 0 R 16 0 R ] /Type /Pages +>> +endobj +28 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 744 +>> +stream +Gatn#?#Q2d'Sc)J/'_]eFTrcnA9[uE26Bfh'koUh.Mj)a#&J$9S+?#l+Doh%fgMm^&g2Sgo:5Q&:^D'gr`M&XV`=lY$C!h?#9&dCngh5k7unnkA/THOA3>eo9iDZoP8MX)2@2/TnH_ZB8]!:dP&`I;.G/BgGLEU_'_2H60VH("_-#l.OqF:i1r9.'KQ4/PV=a0m/sVd_(kaM-eEoSQCWo[C.;N&mYDbYsquQ@anG2G\#lFeV53RY4rW:>33_:ad^3?7q_Dg,Sj%!Opm`-'$Vq*\dIm*[POl,$o]KS@UIW@fEnepaX*'>Mu97)]t"qfZ0$s/dM>C"n[]]38Pf+Q:I$DY;ah@0J'jM0T%oINqlb=$^GI266L%Rf%UQqM3o=U=3Z>&,0iiIabu@!4AW.?"bUFRlH%r2/82d'$YRL`4Q76']e!"k2\[_&;:Te&L,]iP%4hE_%%hKH2P$1S^MWeBXo[.kA4K<7BF(_'Jaa15I<)57`a)]kRnk2f5hIepC7F3eWNkP\k9gK/h;sEa0M]Tf0ZE?+O&N&n[*!J9c>O(m8Un@B5\F#9bE9l,rr>=J8e%5jCsU.BV#54Kd`P0urm'XK9[h6EQb""NJ>HJR,`_K,8FiE7!Tg.endstream +endobj +29 0 obj +<< /Filter [ /ASCII85Decode /FlateDecode ] /Length 3969 >> stream Gb!#_>Ar<,'n5n\+A`c>mFiJ8]%ahK*';HCS&F19l1VrD5td^c`Yq_%-"E]`IsV^*b@i`D:9m;s,\%:__fJ'*,A@Udn>",*]er-[ma1l(6Q/F<$`"YtJaVBgf69_lNjN8m@4IRQWWob79>RW:_g=ng[85-ah,^qT$*%J"VU7QCFK,XSNds'j@TV%*h&9]QpmYW!q7Mfa.Z0R+p4fE.T_mCQkjC6#0Tds,B6+\SEt\=(B>-m!oHB+fBBl>b,tmF?glg=VrWi8Dh-Yg>Id:Ch0GLpc7BDl.7nj_jn"a9\>paKqa+/I!e&QY\=n+'4;,#Fi%T1OM06cmBJI!MmBUYSFoP'8]aHO@u^W3?W]N/tOOsqVDe+K1'9kSQAQI'nE-YT%s.:SteA:Dg8P!)8[>ob/i/B?sI_d2-!2u,#k\)h[rVGV@p(\UOeL&1_Kn9?t[Xg=Yj&&&?aSH&H;r*sq5V@h]'X/V6hK[:IK6#*WiGK4o*@"T>]%iWp!T=feLGM:G^Y#7+N!b`FJ,?(H#p?]RO@ZLH2[a?m0DQcLSGH>>&'C@21<9IqOVE)?8i>G0O2+uiNqP6E7(3N+4np/0t(?=3c2S7H@10eL`hmQ`='B"WMb?cUbP_U&u@]+i_$\ITC"^;n:EP\IS(/U'Np\I88uD2=\O7Ni%`/28]tKHO*fhg>cM]hF7>WOCdA^hBt8mmTE^f.)3mWB_sPSR_]`RWl1n=Xc_lJ!14'0U8SKS^VkHU__cATedBM(85p/&BX"E6Nihns!c;;RoI0M2EBj%B>?SWrbRY:bGF+G*hZlXf9!#HXDfS2u"eUn!Ln.dl^5L7J#3AIWO`7c'#K:I8QhaAd0$RbfH'pXlirH)u`-c23TMcd61GT0W&c\$q,hC?VM<2biCM@NI/JO9&&jfN=Hj!uCLD9JfE\JXS\+K`g4aVBKWqU4!kKS@T)[RSs#<,>QW3a4oaR`]Zf`0TmTLfOX)CQK@/b^0ssebq%IM3pd7FSQ7uIk,dCE_`\g2r5Q@Nrf!H`\`RMm6\G?b4oosGT2OIK6SqN^dS[WgMiO%%J<64pE8REJ7o==9hCu+rCB_U=Hn@-`"YMkeO=M4:BSR`POt,D%g>:PWm,GIm#70)P+C3G[J777Re-*e)91sA]Mu&M/TG`!Vmdt\gAZm^8\C-#Gl)PrjnRG;"'aMs4a:b[EN-%t;3`[f^3f*UD'j]9`b.;"3L?]\4:qi_+?':HBYX,uu6A/@"0sEZIn&3l5DR5ODqBN&B^UU/R:QM3EidTtZe^<9nlc8QOXe5T\LSC)E#9gN9IY`j":Sm`gkHZi,OPPdf=^&X,(@!+M\`(sr'_dSFmgWGM[i]_a((T-Q622PAf_B"IC(l1/F]6]VU"I`Y-rd[)bsjro@PJ*TFf^ZN3mFS-eZpFA\\W\b;9Lm#9g]T+o#%c-)B=a)pSF*$!D-.REQ?+)oBI)^6GZY/-F/=/"<&Y6bBq$ms,=RZ1c!_"Z7!J<"FN]sd+"Znn[MZrLSM)?VEn#Y(\)O4$o'0WgZD).[I#G/4)PZO7_+f'@E[FCHW)jp'S&@u0Rr!0JW;]t*TKFcRV$(s5Yes_B@X=$1(?:RJ'!cA[YgS69g9IG.(&)0L]-RZabPXfRN:a.I+d0o6QGQFqXB/qG!G4X]U1&m^7rNGPLq.89(kNQl9^gIXH2>fR?aBiq4)LM8oVPas"/VT^I30egAtFF:](@\ip7XeI]OYq@GcT6\(J<,OeYBfK1)-gN!AF`#88A+08>N%t$%B.p0:*G`Fc-0MOU+/uDDe4cI?cqS2]VJ+NZ_sf^h&'%Aqh,o6Jtfc^53XDVJ:YmEFFnAXk[ZbnDPVBF=>UUt)&sC#J\O>I.IU"5k?&U4apfBFM"o[pfq7>L7a[7dB[L^REZ!2ZTj9jPb/XTYhd1cYrX_2(!Ne(\Q'#VSj:k5P_k2(]fVi]nnlQF4q.97l=:54;iCY?5"+8..%1p)\@#CZO=$l'%7FVi3/?/T<-+8M?;58$8a?sJE$&>###&qX]h!55?k>lI.^![XPm)"#i0[b6gIO)7s!RF\XE?4&l2ND)pXesFPLIZh+Q/iZg73V?IN%A;Z@S>X@PC%:P"99kMp[4M]US'I^:4)Kop_@D3RkS*\!>>Do2bDDEG&%,B6",Ylbfl#WY!F]!LeUXdspsg@jjp:;:[a0nI&k$1oer^cTmFf8td%Mb3bLqh".ZefEM2U?cRgog(.M8oJdi(EFY(roMTWi%JS8H*OMRMuNN!V@+HK8X`QWL;]n*^YJ5$*\6D@nhjXd,[UUpP+e97'V.XjI_hu(4F,8M)+qp0BO:'4c@bqjWK]qJAg2&Yp5"Wk1q9/8:.?DY)W`2ruRP[H8\^P^=:mImGR1=PZ@G#_+fllu@bWCq-:Rcka!_lSbbMD"!XgepeL5Igoio5t/aWRFXA@\MqM"HjWcir4-+KpUIJGEZli%enOkqNh(-;QbEkR(!ImV3:;+k5s0jcubJHVt3?aEop!pI?0GS*t^Dr*REebl@FM62F^?I1c35d9!c#Z#j4@ZWRM2d)i\2f9^,BnDFnNDFu6m"[iIO@ctu;%N@Q@5L7LuM3?I,8`8&\OmQ7l3"/X,8hh$3@*>tXrE\NBBNfHLC"JhH`61KlWKB(q;KFUXA/V+5*J`T#CaEADh;'$#qOWa*%&X,`?h\AW&$8a0G0&8WDKI4!:j^a`kk[Z-*H;%i%9^.1=(dVhE+8p+L>sAZ!PX"8;/3MM=Y!AW'W+iWfU*CEgJr$ZlF6P(rP/JtS3C:eDd!rS>XthT:6pF=EmuWW3Cu@IHe3aQ9I4E?$I$caZ4.d'l]-"^]p_(jHoEn1'J:l_pil.Tq:dY\dS"2kh<]F0Sr>@A0Ir![Wj`)?~>endstream endobj -28 0 obj +30 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 735 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 921 >> stream -Gat=h:J^")&B4*cMC3Bm$ab&F[\05'@Za]qKj=jr&&!4O8`D&(KC)Lsg[@-"O_C`&WZiak`\W(Y2e*4(q%;FE>le:G%'Bb%KHu>/q_=7WpOEZ;QCr>5=>2etp<43ga:B6QD$%q`!4U*X,;A'J1TZQlVte8@leRcX:qabrL8MEPHVLEpn*>p9!cH-1+DoD]+N^+"P'\n7m]K_)IhPVoa!41k9@a\rRLLEHps4D'pNN,5I5@3(IkA&%AFO_ppmW=h@f:0W^nKX";Ao`SJe^1/$H=LJgr/fY!MsXnRC(L536^]EP^Q](qTN:02:/et#$s@-pWVE2?]!BHGl?5-7d'lQ("UiCe22-KfWDh*)[Wo&/@en6TFtO5r6>;R9P\oendstream +Gat=)gJ6Kg&:N^l7cG^.E,'BA_UGt#-TX-i*E?Lj(l^Hc"NSAhnNR\6$6RU$=CZkij]]=mSj>jluEK9PDaeYl=^A2#\V;s2dV'N#B1j2$*9F]\m?>rREAXJJJN$%`k[ku)I[Kj*K1`$H[lWT"7!ek@iYd.!IYNM'mpYkDJb]=VVPRZ6'6j-.d/Tt(?FX"#o!<_8!,W($Et5Cef2fa7-0!fY$)LJccs\>CGj8'IB=5<+"9d[2kphN@?,:dTjsI?IG]OR`)3B(nW\r[hP\V@I?GApDe/rMg1$7$/+D^,#3g_Ep+fI^Y'l[pA"\gMu6>?[lOIut\A4EDoS^fn\#j&n1H(nQT9O:P6dRhAnB'*lmPJNWO4JgT,bVj/=/qn\p.^@b;4Pb*i6ntSkgIe_%OIJrP"f(2r=gj@uG%eFp(E+$s3Wu.kG50Cu_RN(A=O`7\'PfLWAf,U2lJcSe6"!pUp:RNu\=;TjB1gXCeeR6&2cXm82mendstream endobj xref -0 29 +0 31 0000000000 65535 f 0000000061 00000 n 0000000134 00000 n @@ -177,33 +187,35 @@ xref 0000001018 00000 n 0000001166 00000 n 0000001314 00000 n -0000001430 00000 n -0000001685 00000 n -0000001791 00000 n -0000001997 00000 n -0000002203 00000 n -0000002290 00000 n -0000002607 00000 n -0000002681 00000 n -0000002802 00000 n -0000003122 00000 n -0000003291 00000 n -0000003395 00000 n -0000003526 00000 n -0000003652 00000 n -0000003727 00000 n -0000004529 00000 n -0000008590 00000 n +0000001462 00000 n +0000001578 00000 n +0000001840 00000 n +0000001946 00000 n +0000002152 00000 n +0000002358 00000 n +0000002445 00000 n +0000002762 00000 n +0000002836 00000 n +0000002957 00000 n +0000003277 00000 n +0000003446 00000 n +0000003550 00000 n +0000003681 00000 n +0000003820 00000 n +0000003929 00000 n +0000004004 00000 n +0000004839 00000 n +0000008900 00000 n trailer << /ID -[<7f14806cee8a14988600c30857cea804><7f14806cee8a14988600c30857cea804>] +[<8fe3abb3b6ae298d8e49cb9ed97a806a><8fe3abb3b6ae298d8e49cb9ed97a806a>] % ReportLab generated PDF document -- digest (opensource) -/Info 17 0 R -/Root 16 0 R -/Size 29 +/Info 18 0 R +/Root 17 0 R +/Size 31 >> startxref -9416 +9912 %%EOF diff --git a/docs/USB_HDMI_CAST.pdf b/docs/USB_HDMI_CAST.pdf index 9352db2..6ef350a 100644 --- a/docs/USB_HDMI_CAST.pdf +++ b/docs/USB_HDMI_CAST.pdf @@ -82,7 +82,7 @@ endobj endobj 15 0 obj << -/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\)) +/Author (Android Cast project) /CreationDate (D:20260608122522+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122522+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Subject (\(unspecified\)) /Title (USB-C / HDMI and USB-tether cast \(roadmap D & E\)) /Trapped /False >> endobj @@ -159,7 +159,7 @@ xref trailer << /ID -[<46e22eb997e0bc715c5afd1f1d8a9577><46e22eb997e0bc715c5afd1f1d8a9577>] +[<2f3e1c2548fcb7ec850b1fea338ffefa><2f3e1c2548fcb7ec850b1fea338ffefa>] % ReportLab generated PDF document -- digest (opensource) /Info 15 0 R diff --git a/docs/_pdf_build/build_all_docs_pdf.py b/docs/_pdf_build/build_all_docs_pdf.py index 70203d4..f80a11f 100644 --- a/docs/_pdf_build/build_all_docs_pdf.py +++ b/docs/_pdf_build/build_all_docs_pdf.py @@ -14,6 +14,10 @@ from md_to_pdf import build_pdf_from_markdown # noqa: E402 SUBTITLE = { "20260602_REVERSE_SSH_proposals_summary.md": "Generated from markdown — rev. 1–3 + appendices WG test/production", + "20260607-2FA-email-mobile-auth-flow.md": "Email forwarding + registration/2FA — alpha must-have", + "20260608_ALPHA_PRIORITIES.md": "Master alpha priority plan — 2026-06-08 renumber", + "20260608_STREAM_DUMP_DEBUG.md": "FR 0.1 stream dump while casting", + "OPEN_TASKS_GRAPH.md": "Open tasks dependency graph — alpha critical path", "GIT_FLOW.md": "Branches, next, release — from markdown", "REMOTE_ACCESS_IMPL.md": "WireGuard v1 — implementation and deploy", "REMOTE_ACCESS_VALIDATION.md": "Linux CLI validation — device sim + E2E", diff --git a/examples/crash_reporter/ROADMAP.md b/examples/crash_reporter/ROADMAP.md index 5459eaf..da94dfb 100644 --- a/examples/crash_reporter/ROADMAP.md +++ b/examples/crash_reporter/ROADMAP.md @@ -10,7 +10,8 @@ ## Next -- [ ] User registration + email verification +- [ ] User registration + email verification + TOTP 2FA — **alpha must-have** — [docs/20260607-2FA-email-mobile-auth-flow.md](../../docs/20260607-2FA-email-mobile-auth-flow.md) +- [ ] Email infra: `info@` / `admin@` / `root@` @ `apps.f0xx.org` → Gmail forward (§9) - [ ] RBAC editor UI (delegate roles per module) - [ ] Symbolication (ndk-stack / retrace) for native/Java stacks - [ ] MQTT ingest endpoint (parallel to HTTP POST) diff --git a/examples/crash_reporter/backend/README.md b/examples/crash_reporter/backend/README.md index bb01676..f93612e 100644 --- a/examples/crash_reporter/backend/README.md +++ b/examples/crash_reporter/backend/README.md @@ -311,7 +311,13 @@ BASE=https://apps.f0xx.org/app/androidcast_project/crashes ./scripts/ra_e2e_cli. **CLI validation (Linux device simulator):** [docs/REMOTE_ACCESS_VALIDATION.md](../../../docs/REMOTE_ACCESS_VALIDATION.md). -Android: Developer settings → **Remote access** → WireGuard (RSSH reserved). Requires VPN permission; polls BE every 1–7 min while enabled. +Android: Developer settings → **Remote access** → **RSSH** (alpha target; WireGuard lab-only). RSSH avoids VPN permission. Polls BE every 1–7 min while enabled. See [REMOTE_ACCESS_IMPL.md § VPN consent](../../../docs/REMOTE_ACCESS_IMPL.md#android-vpn-consent-wireguard) and [ROADMAP § RSSH](../../../docs/ROADMAP.md#remote-access-alpha--rssh). + +## Third-party licenses (BE / landing) + +- [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) — server stack, optional WireGuard tools, link to mobile app list. +- Mobile APK list: in-app **Licenses** button → `app/src/main/assets/licenses/combined.html`. +- Suggested public URL after sync: `…/crashes/THIRD_PARTY_LICENSES.md` (or HTML excerpt on hub legal page). ## Default accounts diff --git a/examples/crash_reporter/backend/THIRD_PARTY_LICENSES.md b/examples/crash_reporter/backend/THIRD_PARTY_LICENSES.md new file mode 100644 index 0000000..660d175 --- /dev/null +++ b/examples/crash_reporter/backend/THIRD_PARTY_LICENSES.md @@ -0,0 +1,46 @@ +# Third-party licenses — crash reporter backend & apps.f0xx.org stack + +Prepared for landing / legal disclosure. **Verify against production deploy** before publishing. + +## Android Cast application code (PHP/JS in this tree) + +- **License:** same BSD-style terms as the Android Cast mobile app (see repo root / mobile `licenses/combined.html`). +- **Scope:** `examples/crash_reporter/backend/` PHP, views, static JS/CSS under `public/assets/`. + +## Runtime infrastructure (operator-installed) + +| Component | Typical license | Notes | +|-----------|-----------------|--------| +| Alpine Linux | Various (GPL-2.0 kernel; mixed userspace) | BE VM base | +| nginx | BSD-2-Clause | Reverse proxy / static | +| PHP 8.x | PHP License v3.01 | FPM | +| MariaDB / SQLite | GPL-2.0 (MariaDB); public domain (SQLite) | DB engine | +| OpenSSH / OpenSSL | Various | Admin access, TLS | + +These are **server-side**; not bundled in the mobile APK. + +## Optional remote-access tooling + +| Component | License | When used | +|-----------|---------|-----------| +| **wireguard-tools** (`wg`, `wg-quick`) | GPLv2 (userspace) | BE dynamic peer provisioning (`WireGuardPeerProvisioner.php`) | +| Linux **WireGuard** kernel module | GPLv2 | UDP tunnel termination on BE | + +Mobile WireGuard mode uses outbound tunnel from device; see mobile app licenses for optional `wireguard-android` (Apache-2.0). + +## Planned: reverse SSH (RSSH) + +**Alpha essential** (owner agreement). Device opt-in via existing `heartbeat.php` RA flow; outbound SSH reverse tunnel to bastion; no `VpnService`. Stack may include OpenSSH-compatible client on device and `sshd`/SFTP on bastion — licenses TBD (typically OpenSSH license + OpenSSL). + +## Frontend assets (this backend) + +- **Vanilla JavaScript/CSS** in `public/assets/` — project-authored unless noted. +- No npm bundle in production tree at time of writing; if CDN libraries are added later, list them here. + +## Publishing on landing + +Suggested public URL after deploy sync: + +`https://apps.f0xx.org/app/androidcast_project/crashes/THIRD_PARTY_LICENSES.md` + +(or render an HTML excerpt on the hub legal page linking mobile + BE lists). diff --git a/examples/crash_reporter/backend/config/config.example.php b/examples/crash_reporter/backend/config/config.example.php index 57e49c8..468be0d 100644 --- a/examples/crash_reporter/backend/config/config.example.php +++ b/examples/crash_reporter/backend/config/config.example.php @@ -58,5 +58,27 @@ return [ 'require_wg_tools' => false, 'session_ttl_s' => 3600, 'min_poll_interval_s' => 45, + // Reverse SSH (alpha): outbound -R from device; no VpnService on phone + 'rssh' => [ + 'bastion_host' => 'ra.apps.f0xx.org', + 'bastion_port' => 443, + 'username_prefix' => 'ra-', + 'local_forward_port' => 8022, + 'remote_port_min' => 18000, + 'remote_port_max' => 18999, + ], + ], + // Outbound mail (registration verify, password reset) — see docs/20260607-2FA-email-mobile-auth-flow.md + 'mail' => [ + 'transport' => 'smtp', // smtp | sendmail + 'from' => 'Android Cast Issues ', + 'reply_to' => 'info@apps.f0xx.org', + 'smtp' => [ + 'host' => '127.0.0.1', + 'port' => 587, + 'encryption' => 'tls', // tls | ssl | '' + 'username' => '', + 'password' => '', // set in prod config.php only — not in git + ], ], ]; diff --git a/examples/crash_reporter/backend/public/api/auth_register.php b/examples/crash_reporter/backend/public/api/auth_register.php new file mode 100644 index 0000000..b8b6e6c --- /dev/null +++ b/examples/crash_reporter/backend/public/api/auth_register.php @@ -0,0 +1,26 @@ + false, 'error' => 'method_not_allowed'], 405); +} + +$raw = file_get_contents('php://input'); +$data = json_decode($raw ?: '', true); +if (!is_array($data)) { + json_out(['ok' => false, 'error' => 'invalid_json'], 400); +} + +$action = (string) ($data['action'] ?? 'register'); +if ($action === 'verify') { + $result = AuthRegistration::verifyEmailToken((string) ($data['token'] ?? '')); + json_out($result, $result['ok'] ? 200 : 400); +} + +$email = (string) ($data['email'] ?? ''); +$password = (string) ($data['password'] ?? ''); +$username = (string) ($data['username'] ?? ''); +$result = AuthRegistration::register($email, $password, $username); +json_out($result, $result['ok'] ? 200 : 400); diff --git a/examples/crash_reporter/backend/public/assets/js/remote_access.js b/examples/crash_reporter/backend/public/assets/js/remote_access.js index b1608f2..45c4a77 100644 --- a/examples/crash_reporter/backend/public/assets/js/remote_access.js +++ b/examples/crash_reporter/backend/public/assets/js/remote_access.js @@ -50,7 +50,7 @@ (devices || []).forEach((d) => { const tr = document.createElement('tr'); const wl = Number(d.whitelisted) === 1; - const canOpen = canOperate() && wl && (d.opt_in_mode === 'wireguard'); + const canOpen = canOperate() && wl && (d.opt_in_mode === 'wireguard' || d.opt_in_mode === 'rssh'); const openDisabled = canOpen ? '' : ' disabled title="Whitelist device and wait for WireGuard opt-in poll"'; const wlDisabled = canAdmin() ? '' : ' disabled'; tr.innerHTML = diff --git a/examples/crash_reporter/backend/scripts/ingest_priorities_20260608.php b/examples/crash_reporter/backend/scripts/ingest_priorities_20260608.php new file mode 100644 index 0000000..17e673f --- /dev/null +++ b/examples/crash_reporter/backend/scripts/ingest_priorities_20260608.php @@ -0,0 +1,104 @@ +#!/usr/bin/env php + 'ticket', 'label' => 'ticket', 'bg' => '#6366f1'], + ['id' => '20260608', 'label' => '20260608', 'bg' => '#0ea5e9'], + ['id' => 'open', 'label' => 'open', 'bg' => '#22c55e'], + ['id' => $ownerTag, 'label' => $ownerTag, 'bg' => $ownerTag === 'dev' ? '#f59e0b' : '#3b82f6'], + ]; +} + +$tasks = [ + ['key' => '0.1', 'owner' => 'agent', 'text' => 'P0 FR: Stream dump while casting — zip + meta.json + raw A/V; see docs/20260608_STREAM_DUMP_DEBUG.md. Cyclic with 6.6.'], + ['key' => '1', 'owner' => 'agent', 'text' => 'P1 Graphs dashboard parent — fixes + FRs.'], + ['key' => '1.1', 'owner' => 'agent', 'text' => 'P1.1 Graphs: all pending fixes (drill-down, perf, permalinks).'], + ['key' => '1.2', 'owner' => 'agent', 'text' => 'P1.2 Graphs: all pending FRs (columns 1/2/4, full-width grid).'], + ['key' => '2.1', 'owner' => 'dev', 'text' => 'P2.1 DEV: DNS MX/SPF/DKIM/DMARC for apps.f0xx.org — tmp/20260608_help_request.md'], + ['key' => '2.2', 'owner' => 'dev', 'text' => 'P2.2 DEV: Email forward info@/admin@/root@ → Gmail.'], + ['key' => '2.3', 'owner' => 'dev', 'text' => 'P2.3 DEV: Verify inbound mail to info@apps.f0xx.org.'], + ['key' => '2.4', 'owner' => 'dev', 'text' => 'P2.4 DEV: Gmail filters / reply-as optional.'], + ['key' => '2.5', 'owner' => 'agent', 'text' => 'P2.5 AGENT: BE outbound SMTP for verification emails (blocked on 2.1).'], + ['key' => '3', 'owner' => 'agent', 'text' => 'P3 AGENT: Auth register + verify + TOTP + migration 008 — docs/20260607-2FA-email-mobile-auth-flow.md'], + ['key' => '4', 'owner' => 'agent', 'text' => 'P4 AGENT: Reverse SSH alpha — replace WG for prod; mobile + BE; hidden no VPN.'], + ['key' => '5.1', 'owner' => 'shared', 'text' => 'P5.1 OTA channel + artifacts — 50% dev / 50% agent.'], + ['key' => '5.2', 'owner' => 'shared', 'text' => 'P5.2 Device crash/OTA URLs — 50% dev / 50% agent.'], + ['key' => '5.3', 'owner' => 'agent', 'text' => 'P5.3 RSSH BE validation — depends on 4.x.'], + ['key' => '5.4', 'owner' => 'agent', 'text' => 'P5.4 auth-2fa validation ALPHA G2–G6.'], + ['key' => '5.5', 'owner' => 'agent', 'text' => 'P5.5 RBAC validation.'], + ['key' => '6.1', 'owner' => 'dev', 'text' => 'P6.1 Opus/Speex E2E — dev + agent JNI.'], + ['key' => '6.2', 'owner' => 'dev', 'text' => 'P6.2 LAN soak A/B/C.'], + ['key' => '6.3', 'owner' => 'dev', 'text' => 'P6.3 App lifecycle notifications/recents.'], + ['key' => '6.4', 'owner' => 'dev', 'text' => 'P6.4 Rotation + preview crash — cyclic with 6.3.'], + ['key' => '6.5', 'owner' => 'dev', 'text' => 'P6.5 Audio lag — use 0.1 dumps for analysis.'], + ['key' => '6.6', 'owner' => 'dev', 'text' => 'P6.6 Stream debug analysis — same as 0.1.'], + ['key' => '9', 'owner' => 'agent', 'text' => 'P9 Landing hub — LOW priority.'], + ['key' => '9.1', 'owner' => 'agent', 'text' => 'P9.1 Hub cards graphs/builder/RA.'], + ['key' => '9.2', 'owner' => 'agent', 'text' => 'P9.2 Globe animation branch cleanup.'], + ['key' => '9.3', 'owner' => 'agent', 'text' => 'P9.3 Mobile chrome validation LOW.'], +]; + +$now = (int) round(microtime(true) * 1000); +$ok = 0; +$fail = 0; + +foreach ($tasks as $task) { + $text = $task['text']; + $ownerTag = $task['owner']; + $payload = [ + 'schema_version' => 1, + 'ingest_kind' => 'ticket', + 'ticket_id' => 'priority-20260608-' . preg_replace('/[^a-z0-9.-]+/i', '-', $task['key']), + 'title' => mb_substr($text, 0, 256), + 'brief' => mb_substr($text, 0, 512), + 'body' => $text . "\n\nRef: docs/20260608_ALPHA_PRIORITIES.md", + 'opened_at_epoch_ms' => $now, + 'rating' => 3, + 'owner' => $owner, + 'tags' => priority_tags($ownerTag), + 'device' => ['manufacturer' => 'roadmap', 'model' => 'priorities', 'sdk_int' => 34, 'release' => '14'], + 'app' => ['package' => 'com.foxx.androidcast', 'version_name' => 'roadmap', 'version_code' => 1], + ]; + if ($dryRun) { + echo "DRY {$task['key']} [{$ownerTag}]\n"; + $ok++; + continue; + } + $ctx = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\n", + 'content' => json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), + 'ignore_errors' => true, + ], + ]); + $resp = @file_get_contents($uploadUrl, false, $ctx); + $code = 0; + if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) { + $code = (int) $m[1]; + } + if ($code === 200 || $code === 409) { + echo ($code === 409 ? 'DUP ' : 'OK ') . "{$task['key']}\n"; + $ok++; + } else { + echo "FAIL {$task['key']} HTTP {$code} — {$resp}\n"; + $fail++; + } +} + +echo "Done: ok={$ok} fail={$fail}\n"; +exit($fail > 0 ? 1 : 0); diff --git a/examples/crash_reporter/backend/scripts/ingest_roadmap_tickets_20260604.php b/examples/crash_reporter/backend/scripts/ingest_roadmap_tickets_20260604.php index 6c42ec5..4a35499 100755 --- a/examples/crash_reporter/backend/scripts/ingest_roadmap_tickets_20260604.php +++ b/examples/crash_reporter/backend/scripts/ingest_roadmap_tickets_20260604.php @@ -35,7 +35,9 @@ $tasks = [ ['key' => '1', 'status' => 'open', 'text' => 'BE/builder deployment phases — dedicated services on BE; WIP verifying (Docker, out/builds, runner_script).'], ['key' => '2', 'status' => 'open', 'text' => 'OTA checks — part of builder/deploy path; verify artifacts and device OTA flow.'], ['key' => '3', 'status' => 'open', 'text' => 'Opus/Speex validation — E2E, encryption, FEC, NACK, negotiations; user and developer controls.'], - ['key' => '4', 'status' => 'open', 'text' => 'RSSH/WG (WireGuard v1) — finish deploy, prod validations, minimal tests; RA E2E CLI + Android poll.'], + ['key' => '4', 'status' => 'open', 'text' => 'Alpha: RSSH remote access (prod/hidden, no VPN) — BE bastion + Android reverse SSH client; WG v1 lab only. Opus/Speex JNI sinks TODO.'], + ['key' => 'auth-2fa', 'status' => 'open', 'text' => 'Alpha: registration + email verify + TOTP 2FA — mobile-friendly; depends on email DNS/SMTP. See docs/20260607-2FA-email-mobile-auth-flow.md.'], + ['key' => 'email-dns', 'status' => 'open', 'text' => 'Alpha: email infra — info@/admin@/root@ @ apps.f0xx.org forward to Gmail; SPF/DKIM/DMARC. Blocks auth mail.'], ['key' => '5', 'status' => 'open', 'text' => 'Data visualization / graphs — WIP; issues found on drill-down and layout.'], ['key' => '5.1', 'status' => 'open', 'text' => 'ISSUE 5.1: Graph brick click must open full-browser detailed chart (axis, legend, hover per point like Java visualizer).'], ['key' => '5.2', 'status' => 'open', 'text' => 'REQUIREMENT 5.2: All graph preview bricks clickable; Columns combo (1/2/4) after Window default 2; grid uses full width.'], diff --git a/examples/crash_reporter/backend/sql/migrations/008_auth_email_2fa.sql b/examples/crash_reporter/backend/sql/migrations/008_auth_email_2fa.sql new file mode 100644 index 0000000..ecb4977 --- /dev/null +++ b/examples/crash_reporter/backend/sql/migrations/008_auth_email_2fa.sql @@ -0,0 +1,41 @@ +-- Email registration + 2FA tables (MariaDB). Run as DB root once; SQLite dev auto-migrates via AuthEmailSchema. + +ALTER TABLE users + ADD COLUMN IF NOT EXISTS email_normalized VARCHAR(254) NULL, + ADD COLUMN IF NOT EXISTS email_verified_at TIMESTAMP NULL, + ADD COLUMN IF NOT EXISTS status ENUM('pending','active','locked','disabled') NOT NULL DEFAULT 'active', + ADD COLUMN IF NOT EXISTS recovery_email_normalized VARCHAR(254) NULL; + +CREATE UNIQUE INDEX IF NOT EXISTS uq_users_email_normalized ON users (email_normalized); + +CREATE TABLE IF NOT EXISTS auth_tokens ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id INT UNSIGNED NULL, + purpose ENUM('verify_email','reset_password','login_magic') NOT NULL, + token_hash CHAR(64) NOT NULL, + email_normalized VARCHAR(254) NULL, + expires_at TIMESTAMP NOT NULL, + used_at TIMESTAMP NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + KEY idx_auth_tokens_hash (token_hash), + KEY idx_auth_tokens_email (email_normalized) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS auth_factors ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id INT UNSIGNED NOT NULL, + type ENUM('totp','webauthn','backup_code') NOT NULL, + secret_encrypted TEXT NULL, + label VARCHAR(64) NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + KEY idx_auth_factors_user (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS auth_attempts ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + ip_hash CHAR(64) NOT NULL, + username_hash CHAR(64) NULL, + outcome VARCHAR(32) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + KEY idx_auth_attempts_ip (ip_hash, created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/examples/crash_reporter/backend/sql/migrations/009_rssh_sessions.sql b/examples/crash_reporter/backend/sql/migrations/009_rssh_sessions.sql new file mode 100644 index 0000000..86f03c5 --- /dev/null +++ b/examples/crash_reporter/backend/sql/migrations/009_rssh_sessions.sql @@ -0,0 +1,8 @@ +-- RSSH session credentials (MariaDB). Idempotent ALTERs. +ALTER TABLE remote_access_sessions + ADD COLUMN IF NOT EXISTS rssh_bastion_host VARCHAR(255) NULL, + ADD COLUMN IF NOT EXISTS rssh_bastion_port INT UNSIGNED NULL, + ADD COLUMN IF NOT EXISTS rssh_username VARCHAR(64) NULL, + ADD COLUMN IF NOT EXISTS rssh_secret VARCHAR(128) NULL, + ADD COLUMN IF NOT EXISTS rssh_remote_port INT UNSIGNED NULL, + ADD COLUMN IF NOT EXISTS rssh_local_port INT UNSIGNED NULL DEFAULT 8022; diff --git a/examples/crash_reporter/backend/sql/migrations/009_rssh_sessions.sqlite.sql b/examples/crash_reporter/backend/sql/migrations/009_rssh_sessions.sqlite.sql new file mode 100644 index 0000000..1c33681 --- /dev/null +++ b/examples/crash_reporter/backend/sql/migrations/009_rssh_sessions.sqlite.sql @@ -0,0 +1 @@ +-- RSSH columns for SQLite (applied via Database::ensureSchema). diff --git a/examples/crash_reporter/backend/src/AuthEmailSchema.php b/examples/crash_reporter/backend/src/AuthEmailSchema.php new file mode 100644 index 0000000..b6e3b1d --- /dev/null +++ b/examples/crash_reporter/backend/src/AuthEmailSchema.php @@ -0,0 +1,133 @@ + Database::isMysql() ? 'VARCHAR(254) NULL' : 'TEXT NULL', + 'email_verified_at' => Database::isMysql() ? 'TIMESTAMP NULL' : 'TEXT NULL', + 'status' => Database::isMysql() + ? "ENUM('pending','active','locked','disabled') NOT NULL DEFAULT 'active'" + : "TEXT NOT NULL DEFAULT 'active'", + 'recovery_email_normalized' => Database::isMysql() ? 'VARCHAR(254) NULL' : 'TEXT NULL', + ]; + foreach ($cols as $name => $ddl) { + try { + $pdo->exec('ALTER TABLE users ADD COLUMN ' . $name . ' ' . $ddl); + } catch (PDOException $e) { + $msg = strtolower($e->getMessage()); + if (!str_contains($msg, 'duplicate') && !str_contains($msg, 'already exists')) { + throw $e; + } + } + } + } + + private static function ensureAuthTokens(PDO $pdo): void { + if (Database::tableExists($pdo, 'auth_tokens')) { + return; + } + if (Database::isMysql()) { + $pdo->exec( + "CREATE TABLE auth_tokens ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id INT UNSIGNED NULL, + purpose ENUM('verify_email','reset_password','login_magic') NOT NULL, + token_hash CHAR(64) NOT NULL, + email_normalized VARCHAR(254) NULL, + expires_at TIMESTAMP NOT NULL, + used_at TIMESTAMP NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + KEY idx_auth_tokens_hash (token_hash), + KEY idx_auth_tokens_email (email_normalized) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ); + return; + } + $pdo->exec( + "CREATE TABLE auth_tokens ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NULL, + purpose TEXT NOT NULL, + token_hash TEXT NOT NULL, + email_normalized TEXT NULL, + expires_at TEXT NOT NULL, + used_at TEXT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + )" + ); + $pdo->exec('CREATE INDEX IF NOT EXISTS idx_auth_tokens_hash ON auth_tokens (token_hash)'); + } + + private static function ensureAuthFactors(PDO $pdo): void { + if (Database::tableExists($pdo, 'auth_factors')) { + return; + } + if (Database::isMysql()) { + $pdo->exec( + "CREATE TABLE auth_factors ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id INT UNSIGNED NOT NULL, + type ENUM('totp','webauthn','backup_code') NOT NULL, + secret_encrypted TEXT NULL, + label VARCHAR(64) NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + KEY idx_auth_factors_user (user_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ); + return; + } + $pdo->exec( + "CREATE TABLE auth_factors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + type TEXT NOT NULL, + secret_encrypted TEXT NULL, + label TEXT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + )" + ); + } + + private static function ensureAuthAttempts(PDO $pdo): void { + if (Database::tableExists($pdo, 'auth_attempts')) { + return; + } + if (Database::isMysql()) { + $pdo->exec( + "CREATE TABLE auth_attempts ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + ip_hash CHAR(64) NOT NULL, + username_hash CHAR(64) NULL, + outcome VARCHAR(32) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + KEY idx_auth_attempts_ip (ip_hash, created_at) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ); + return; + } + $pdo->exec( + "CREATE TABLE auth_attempts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ip_hash TEXT NOT NULL, + username_hash TEXT NULL, + outcome TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + )" + ); + } +} diff --git a/examples/crash_reporter/backend/src/AuthMailer.php b/examples/crash_reporter/backend/src/AuthMailer.php new file mode 100644 index 0000000..c6d3168 --- /dev/null +++ b/examples/crash_reporter/backend/src/AuthMailer.php @@ -0,0 +1,142 @@ +'); + $replyTo = (string) cfg('mail.reply_to', ''); + $transport = strtolower((string) cfg('mail.transport', 'smtp')); + if ($transport === 'sendmail') { + return self::sendMail($to, $subject, $bodyText, $from, $replyTo); + } + return self::sendSmtp($to, $subject, $bodyText, $from, $replyTo); + } + + public static function sendVerifyEmail(string $to, string $verifyUrl): bool { + $subject = 'Verify your Android Cast account'; + $body = "Open this link to verify your email (valid 24h):\n\n" . $verifyUrl . "\n"; + return self::send($to, $subject, $body); + } + + private static function sendMail(string $to, string $subject, string $body, string $from, string $replyTo): bool { + $headers = "From: {$from}\r\n"; + if ($replyTo !== '') { + $headers .= "Reply-To: {$replyTo}\r\n"; + } + $headers .= "Content-Type: text/plain; charset=UTF-8\r\n"; + return @mail($to, $subject, $body, $headers); + } + + private static function sendSmtp(string $to, string $subject, string $body, string $from, string $replyTo): bool { + $host = (string) cfg('mail.smtp.host', '127.0.0.1'); + $port = (int) cfg('mail.smtp.port', 587); + $enc = strtolower((string) cfg('mail.smtp.encryption', 'tls')); + $user = (string) cfg('mail.smtp.username', ''); + $pass = (string) cfg('mail.smtp.password', ''); + $remote = ($enc === 'ssl' ? 'ssl://' : '') . $host; + $errno = 0; + $errstr = ''; + $fp = @stream_socket_client($remote . ':' . $port, $errno, $errstr, 15); + if ($fp === false) { + error_log('AuthMailer SMTP connect failed: ' . $errstr); + return self::sendMail($to, $subject, $body, $from, $replyTo); + } + stream_set_timeout($fp, 15); + if (!self::smtpExpect($fp, [220])) { + fclose($fp); + return false; + } + $ehloHost = 'localhost'; + fwrite($fp, "EHLO {$ehloHost}\r\n"); + if (!self::smtpExpect($fp, [250])) { + fclose($fp); + return false; + } + if ($enc === 'tls') { + fwrite($fp, "STARTTLS\r\n"); + if (!self::smtpExpect($fp, [220])) { + fclose($fp); + return false; + } + if (!stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { + fclose($fp); + return false; + } + fwrite($fp, "EHLO {$ehloHost}\r\n"); + if (!self::smtpExpect($fp, [250])) { + fclose($fp); + return false; + } + } + if ($user !== '') { + fwrite($fp, "AUTH LOGIN\r\n"); + if (!self::smtpExpect($fp, [334])) { + fclose($fp); + return false; + } + fwrite($fp, base64_encode($user) . "\r\n"); + if (!self::smtpExpect($fp, [334])) { + fclose($fp); + return false; + } + fwrite($fp, base64_encode($pass) . "\r\n"); + if (!self::smtpExpect($fp, [235])) { + fclose($fp); + return false; + } + } + $fromAddr = self::extractAddress($from); + fwrite($fp, "MAIL FROM:<{$fromAddr}>\r\n"); + if (!self::smtpExpect($fp, [250])) { + fclose($fp); + return false; + } + fwrite($fp, "RCPT TO:<{$to}>\r\n"); + if (!self::smtpExpect($fp, [250, 251])) { + fclose($fp); + return false; + } + fwrite($fp, "DATA\r\n"); + if (!self::smtpExpect($fp, [354])) { + fclose($fp); + return false; + } + $msg = "From: {$from}\r\n"; + if ($replyTo !== '') { + $msg .= "Reply-To: {$replyTo}\r\n"; + } + $msg .= "To: {$to}\r\nSubject: {$subject}\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n{$body}\r\n.\r\n"; + fwrite($fp, $msg); + if (!self::smtpExpect($fp, [250])) { + fclose($fp); + return false; + } + fwrite($fp, "QUIT\r\n"); + fclose($fp); + return true; + } + + /** @param resource $fp @param list $codes */ + private static function smtpExpect($fp, array $codes): bool { + $line = ''; + while (($chunk = fgets($fp, 512)) !== false) { + $line .= $chunk; + if (strlen($chunk) >= 4 && $chunk[3] === ' ') { + break; + } + } + $code = (int) substr(trim($line), 0, 3); + return in_array($code, $codes, true); + } + + private static function extractAddress(string $from): string { + if (preg_match('/<([^>]+)>/', $from, $m)) { + return trim($m[1]); + } + return trim($from); + } +} diff --git a/examples/crash_reporter/backend/src/AuthRegistration.php b/examples/crash_reporter/backend/src/AuthRegistration.php new file mode 100644 index 0000000..168a25d --- /dev/null +++ b/examples/crash_reporter/backend/src/AuthRegistration.php @@ -0,0 +1,94 @@ + false, 'error' => 'invalid_email']; + } + if (strlen($password) < 10) { + return ['ok' => false, 'error' => 'weak_password']; + } + $username = trim($username); + if ($username === '') { + $username = strstr($emailNorm, '@', true) ?: $emailNorm; + } + $pdo = Database::pdo(); + $st = $pdo->prepare('SELECT id FROM users WHERE email_normalized = ? OR username = ? LIMIT 1'); + $st->execute([$emailNorm, $username]); + if ($st->fetchColumn()) { + return ['ok' => false, 'error' => 'already_registered']; + } + $hash = password_hash($password, PASSWORD_DEFAULT); + $pdo->prepare( + 'INSERT INTO users (username, password_hash, role, email_normalized, status) VALUES (?, ?, ?, ?, ?)' + )->execute([ + $username, + $hash, + 'viewer', + $emailNorm, + 'pending', + ]); + $userId = (int) $pdo->lastInsertId(); + $token = bin2hex(random_bytes(32)); + $tokenHash = hash('sha256', $token); + $expires = date('Y-m-d H:i:s', time() + self::TOKEN_TTL_S); + $pdo->prepare( + 'INSERT INTO auth_tokens (user_id, purpose, token_hash, email_normalized, expires_at, created_at) + VALUES (?, ?, ?, ?, ?, ?)' + )->execute([$userId, 'verify_email', $tokenHash, $emailNorm, $expires, date('Y-m-d H:i:s')]); + $base = rtrim((string) cfg('base_path', ''), '/'); + $verifyUrl = 'https://apps.f0xx.org' . $base . '/?view=verify_email&token=' . urlencode($token); + if (!AuthMailer::sendVerifyEmail($emailNorm, $verifyUrl)) { + error_log('AuthRegistration: verify mail failed for ' . $emailNorm); + } + return ['ok' => true]; + } + + /** @return array{ok:bool,error?:string} */ + public static function verifyEmailToken(string $token): array { + self::ensureSchema(); + $token = trim($token); + if ($token === '') { + return ['ok' => false, 'error' => 'missing_token']; + } + $hash = hash('sha256', $token); + $pdo = Database::pdo(); + $st = $pdo->prepare( + "SELECT * FROM auth_tokens WHERE token_hash = ? AND purpose = 'verify_email' AND used_at IS NULL LIMIT 1" + ); + $st->execute([$hash]); + $row = $st->fetch(PDO::FETCH_ASSOC); + if (!$row) { + return ['ok' => false, 'error' => 'invalid_token']; + } + $exp = strtotime((string) ($row['expires_at'] ?? '')); + if ($exp !== false && $exp < time()) { + return ['ok' => false, 'error' => 'expired_token']; + } + $userId = (int) ($row['user_id'] ?? 0); + $now = date('Y-m-d H:i:s'); + $pdo->prepare('UPDATE auth_tokens SET used_at = ? WHERE id = ?')->execute([$now, (int) $row['id']]); + $pdo->prepare( + "UPDATE users SET email_verified_at = ?, status = 'active' WHERE id = ?" + )->execute([$now, $userId]); + return ['ok' => true]; + } + + private static function normalizeEmail(string $email): string { + return strtolower(trim($email)); + } +} diff --git a/examples/crash_reporter/backend/src/Database.php b/examples/crash_reporter/backend/src/Database.php index 5990b98..c5c76ee 100644 --- a/examples/crash_reporter/backend/src/Database.php +++ b/examples/crash_reporter/backend/src/Database.php @@ -99,6 +99,7 @@ final class Database { self::ensureReportColumns($pdo); self::ensureReportViewsTable($pdo); self::ensureRbacSchema($pdo); + AuthEmailSchema::ensure($pdo); return; } $schemaFile = __DIR__ . '/../sql/schema.sqlite.sql'; @@ -112,6 +113,7 @@ final class Database { self::ensureReportColumns($pdo); self::ensureReportViewsTable($pdo); self::ensureRbacSchema($pdo); + AuthEmailSchema::ensure($pdo); } private static function ensureMysqlSchema(PDO $pdo): void { @@ -156,6 +158,7 @@ final class Database { } self::ensureMysqlReportViewsTable($pdo); self::ensureRbacSchema($pdo); + AuthEmailSchema::ensure($pdo); self::ensureTicketsTable($pdo); } diff --git a/examples/crash_reporter/backend/src/RemoteAccessRepository.php b/examples/crash_reporter/backend/src/RemoteAccessRepository.php index 2519c09..3a8183c 100644 --- a/examples/crash_reporter/backend/src/RemoteAccessRepository.php +++ b/examples/crash_reporter/backend/src/RemoteAccessRepository.php @@ -3,7 +3,7 @@ declare(strict_types=1); /** * On-demand remote access control plane (heartbeat type: ra). - * v1: WireGuard opt-in from device; reverse SSH (RSSH) reserved for future use. + * v1: WireGuard + reverse SSH (RSSH) opt-in from device (lab / alpha). */ final class RemoteAccessRepository { public const OPT_NONE = 'none'; @@ -23,6 +23,7 @@ final class RemoteAccessRepository { return; } self::ensureSessionColumns($pdo); + self::ensureRsshColumns($pdo); } /** @param list $where @param list $params */ @@ -67,6 +68,30 @@ final class RemoteAccessRepository { return $row ?: null; } + private static function ensureRsshColumns(PDO $pdo): void { + if (!Database::tableExists($pdo, 'remote_access_sessions')) { + return; + } + $cols = [ + 'rssh_bastion_host' => Database::isMysql() ? 'VARCHAR(255) NULL' : 'TEXT NULL', + 'rssh_bastion_port' => Database::isMysql() ? 'INT UNSIGNED NULL' : 'INTEGER NULL', + 'rssh_username' => Database::isMysql() ? 'VARCHAR(64) NULL' : 'TEXT NULL', + 'rssh_secret' => Database::isMysql() ? 'VARCHAR(128) NULL' : 'TEXT NULL', + 'rssh_remote_port' => Database::isMysql() ? 'INT UNSIGNED NULL' : 'INTEGER NULL', + 'rssh_local_port' => Database::isMysql() ? 'INT UNSIGNED NULL' : 'INTEGER NULL', + ]; + foreach ($cols as $name => $ddl) { + try { + $pdo->exec('ALTER TABLE remote_access_sessions ADD COLUMN ' . $name . ' ' . $ddl); + } catch (PDOException $e) { + $msg = strtolower($e->getMessage()); + if (!str_contains($msg, 'duplicate') && !str_contains($msg, 'already exists')) { + throw $e; + } + } + } + } + private static function ensureSessionColumns(PDO $pdo): void { if (!Database::tableExists($pdo, 'remote_access_sessions')) { return; @@ -142,7 +167,13 @@ final class RemoteAccessRepository { } if ($tunnelMode === self::OPT_RSSH) { - return self::raEnvelope('deny', ['reason' => 'rssh_not_implemented']); + $session = $pendingSession ?? self::findConnectableSession($deviceId, $random); + if ($session === null) { + self::logEvent($deviceId, null, null, 'poll_wait', 'no_pending_session', null, $clientIp); + return self::raEnvelope('wait', ['reason' => 'no_pending_session']); + } + self::touchSessionActivity((string) ($session['session_id'] ?? '')); + return self::buildConnectResponse($session, $deviceId, $clientIp); } $session = $pendingSession ?? self::findConnectableSession($deviceId, $random); @@ -279,6 +310,15 @@ final class RemoteAccessRepository { /** @param array $session */ private static function buildConnectResponse(array $session, string $deviceId, string $clientIp): array { + $tunnel = (string) ($session['tunnel'] ?? 'wireguard'); + if ($tunnel === 'ssh_reverse') { + return self::buildRsshConnectResponse($session, $deviceId, $clientIp); + } + return self::buildWireGuardConnectResponse($session, $deviceId, $clientIp); + } + + /** @param array $session */ + private static function buildWireGuardConnectResponse(array $session, string $deviceId, string $clientIp): array { $sessionId = (string) ($session['session_id'] ?? ''); $tunnel = (string) ($session['tunnel'] ?? 'wireguard'); if ($tunnel !== 'wireguard') { @@ -318,6 +358,67 @@ final class RemoteAccessRepository { ]); } + /** @param array $session */ + private static function buildRsshConnectResponse(array $session, string $deviceId, string $clientIp): array { + $sessionId = (string) ($session['session_id'] ?? ''); + $creds = self::ensureSessionRsshCredentials($sessionId, $session); + $expiresAt = (int) ($session['expires_at'] ?? (time() + 3600)); + $endpoint = RsshSessionProvisioner::endpointFromSession($session); + + self::markSessionActive($sessionId); + self::logEvent($deviceId, $sessionId, null, 'tunnel_connect', 'ssh_reverse', null, $clientIp); + + return self::raEnvelope('connect', [ + 'session_id' => $sessionId, + 'endpoint' => $endpoint, + 'tunnel' => 'ssh_reverse', + 'credentials' => [ + 'mode' => 'password', + 'bastion_host' => $creds['bastion_host'], + 'bastion_port' => $creds['bastion_port'], + 'username' => $creds['username'], + 'password' => $creds['password'], + 'remote_bind_port' => $creds['remote_bind_port'], + 'local_forward_host' => '127.0.0.1', + 'local_forward_port' => $creds['local_forward_port'], + ], + 'expires_at' => $expiresAt, + ]); + } + + /** @param array $session @return array{bastion_host:string,bastion_port:int,username:string,password:string,remote_bind_port:int,local_forward_port:int} */ + private static function ensureSessionRsshCredentials(string $sessionId, array $session): array { + $user = trim((string) ($session['rssh_username'] ?? '')); + $secret = trim((string) ($session['rssh_secret'] ?? '')); + $remote = (int) ($session['rssh_remote_port'] ?? 0); + $local = (int) ($session['rssh_local_port'] ?? 0); + $host = trim((string) ($session['rssh_bastion_host'] ?? '')); + $port = (int) ($session['rssh_bastion_port'] ?? 0); + if ($user !== '' && $secret !== '' && $remote > 0) { + return [ + 'bastion_host' => $host !== '' ? $host : (string) cfg('remote_access.rssh.bastion_host', 'ra.apps.f0xx.org'), + 'bastion_port' => $port > 0 ? $port : (int) cfg('remote_access.rssh.bastion_port', 443), + 'username' => $user, + 'password' => $secret, + 'remote_bind_port' => $remote, + 'local_forward_port' => $local > 0 ? $local : (int) cfg('remote_access.rssh.local_forward_port', 8022), + ]; + } + $alloc = RsshSessionProvisioner::allocate($sessionId); + Database::pdo()->prepare( + 'UPDATE remote_access_sessions SET rssh_bastion_host = ?, rssh_bastion_port = ?, rssh_username = ?, rssh_secret = ?, rssh_remote_port = ?, rssh_local_port = ? WHERE session_id = ?' + )->execute([ + $alloc['bastion_host'], + $alloc['bastion_port'], + $alloc['username'], + $alloc['password'], + $alloc['remote_bind_port'], + $alloc['local_forward_port'], + $sessionId, + ]); + return $alloc; + } + /** @param array $session @return array{client_private:string,client_address:string,server_public:string,allowed_ips:string,client_public:string} */ private static function ensureSessionWireGuardKeys(string $sessionId, array $session): array { $priv = trim((string) ($session['wg_client_private_key'] ?? '')); @@ -408,12 +509,15 @@ final class RemoteAccessRepository { public static function closeSession(string $sessionId, string $status, string $reason): void { $st = Database::pdo()->prepare( - 'SELECT wg_client_public_key FROM remote_access_sessions WHERE session_id = ? LIMIT 1' + 'SELECT tunnel, wg_client_public_key FROM remote_access_sessions WHERE session_id = ? LIMIT 1' ); $st->execute([$sessionId]); - $pub = $st->fetchColumn(); - if (is_string($pub) && $pub !== '') { - WireGuardPeerProvisioner::removeClientPeer($pub); + $row = $st->fetch(PDO::FETCH_ASSOC); + if (is_array($row) && ($row['tunnel'] ?? 'wireguard') === 'wireguard') { + $pub = $row['wg_client_public_key'] ?? ''; + if (is_string($pub) && $pub !== '') { + WireGuardPeerProvisioner::removeClientPeer($pub); + } } $now = self::nowSql(); Database::pdo()->prepare( @@ -432,13 +536,24 @@ final class RemoteAccessRepository { if (!Rbac::canAccessCompany($deviceCompanyId) || !Rbac::can('remote_access_operate', $deviceCompanyId)) { return ['ok' => false, 'error' => 'forbidden']; } - if ((string) ($device['opt_in_mode'] ?? self::OPT_NONE) !== self::OPT_WIREGUARD) { - return ['ok' => false, 'error' => 'device_not_opted_in_wg']; + if ((string) ($device['opt_in_mode'] ?? self::OPT_NONE) === self::OPT_NONE) { + return ['ok' => false, 'error' => 'device_not_opted_in']; + } + $optIn = (string) ($device['opt_in_mode'] ?? self::OPT_NONE); + if (!in_array($optIn, [self::OPT_WIREGUARD, self::OPT_RSSH], true)) { + return ['ok' => false, 'error' => 'device_not_opted_in']; } self::closeActiveSessionsForDevice($deviceId, 'superseded'); $sessionId = self::uuid4(); $companyId = $companyId ?? (int) ($device['company_id'] ?? Rbac::defaultCompanyId()); - $endpoint = (string) cfg('remote_access.wg_endpoint', 'ra.apps.f0xx.org:51820'); + $tunnel = $optIn === self::OPT_RSSH ? 'ssh_reverse' : 'wireguard'; + if ($tunnel === 'ssh_reverse') { + $host = (string) cfg('remote_access.rssh.bastion_host', 'ra.apps.f0xx.org'); + $port = (int) cfg('remote_access.rssh.bastion_port', 443); + $endpoint = $port === 443 ? $host : $host . ':' . $port; + } else { + $endpoint = (string) cfg('remote_access.wg_endpoint', 'ra.apps.f0xx.org:51820'); + } $expiresAt = time() + (int) cfg('remote_access.session_ttl_s', 3600); $now = self::nowSql(); Database::pdo()->prepare( @@ -450,7 +565,7 @@ final class RemoteAccessRepository { $companyId, $operatorUserId, self::STATUS_PENDING, - 'wireguard', + $tunnel, $endpoint, $expiresAt, $now, diff --git a/examples/crash_reporter/backend/src/RsshSessionProvisioner.php b/examples/crash_reporter/backend/src/RsshSessionProvisioner.php new file mode 100644 index 0000000..b097162 --- /dev/null +++ b/examples/crash_reporter/backend/src/RsshSessionProvisioner.php @@ -0,0 +1,50 @@ + 32) { + $username = substr($username, 0, 32); + } + $password = bin2hex(random_bytes(16)); + $remotePort = self::allocateRemotePort($sessionId); + $localPort = max(1, min(65535, (int) cfg('remote_access.rssh.local_forward_port', 8022))); + return [ + 'bastion_host' => $host, + 'bastion_port' => $port, + 'username' => $username, + 'password' => $password, + 'remote_bind_port' => $remotePort, + 'local_forward_port' => $localPort, + ]; + } + + public static function allocateRemotePort(string $sessionId): int { + $min = (int) cfg('remote_access.rssh.remote_port_min', 18000); + $max = (int) cfg('remote_access.rssh.remote_port_max', 18999); + if ($max <= $min) { + $max = $min + 999; + } + $span = $max - $min + 1; + return $min + (abs(crc32($sessionId . '|' . time())) % $span); + } + + /** @param array $session */ + public static function endpointFromSession(array $session): string { + $host = (string) ($session['rssh_bastion_host'] ?? cfg('remote_access.rssh.bastion_host', 'ra.apps.f0xx.org')); + $port = (int) ($session['rssh_bastion_port'] ?? cfg('remote_access.rssh.bastion_port', 443)); + if ($port === 443) { + return $host; + } + return $host . ':' . $port; + } +} diff --git a/examples/crash_reporter/backend/src/bootstrap.php b/examples/crash_reporter/backend/src/bootstrap.php index 2b9cbb1..a57e519 100644 --- a/examples/crash_reporter/backend/src/bootstrap.php +++ b/examples/crash_reporter/backend/src/bootstrap.php @@ -46,6 +46,9 @@ require_once __DIR__ . '/Database.php'; require_once __DIR__ . '/Rbac.php'; require_once __DIR__ . '/DeviceRepository.php'; require_once __DIR__ . '/Auth.php'; +require_once __DIR__ . '/AuthEmailSchema.php'; +require_once __DIR__ . '/AuthMailer.php'; +require_once __DIR__ . '/AuthRegistration.php'; require_once __DIR__ . '/ReportRepository.php'; require_once __DIR__ . '/TagCatalog.php'; require_once __DIR__ . '/TicketTagCatalog.php'; @@ -58,6 +61,7 @@ require_once __DIR__ . '/TicketRepository.php'; require_once __DIR__ . '/GraphRepository.php'; require_once __DIR__ . '/RemoteAccessRepository.php'; require_once __DIR__ . '/WireGuardPeerProvisioner.php'; +require_once __DIR__ . '/RsshSessionProvisioner.php'; require_once __DIR__ . '/AnalyticsHead.php'; function cfg(string $key, $default = null) { diff --git a/examples/settings.json b/examples/settings.json index 19af3cc..5b438eb 100644 --- a/examples/settings.json +++ b/examples/settings.json @@ -11,7 +11,7 @@ }, "crash": { "enabled": "true", - "upload_url": "https://f0xx.org/app/androidcast_project/crashes/api/upload.php", + "upload_url": "https://apps.f0xx.org/app/androidcast_project/crashes/api/upload.php", "prune_after_upload": "true", "upload_interval": "5m", "max_pending": 32 diff --git a/ndk/CMakeLists.txt b/ndk/CMakeLists.txt index 236a81f..2ef806d 100644 --- a/ndk/CMakeLists.txt +++ b/ndk/CMakeLists.txt @@ -46,6 +46,56 @@ else() message(STATUS "No libvpx.a for ${ANDROID_ABI} — VPx JNI probe off (build with scripts/build-native-codecs.sh ${ANDROID_ABI})") endif() +# libopus.a — same autodetect pattern (scripts/build-native-codecs.sh). +set(LIBOPUS_LIB "") +if(ANDROID_ABI) + set(LIBOPUS_PER_ABI "${NATIVE_LIBS_ROOT}/${ANDROID_ABI}/libopus.a") + if(EXISTS "${LIBOPUS_PER_ABI}") + set(LIBOPUS_LIB "${LIBOPUS_PER_ABI}") + endif() +endif() +if(NOT LIBOPUS_LIB AND DEFINED ENV{ANDROIDCAST_LIBOPUS_LIB}) + string(FIND "$ENV{ANDROIDCAST_LIBOPUS_LIB}" "${ANDROID_ABI}" _opus_abi_match) + if(_opus_abi_match GREATER -1 AND EXISTS "$ENV{ANDROIDCAST_LIBOPUS_LIB}") + set(LIBOPUS_LIB "$ENV{ANDROIDCAST_LIBOPUS_LIB}") + endif() +endif() +if(LIBOPUS_LIB) + message(STATUS "Linking libopus for ${ANDROID_ABI}: ${LIBOPUS_LIB}") + target_compile_definitions(androidcast_codecs PRIVATE ANDROIDCAST_HAVE_OPUS=1) + target_link_libraries(androidcast_codecs "${LIBOPUS_LIB}") + if(EXISTS "${THIRD_PARTY_ROOT}/opus/include") + target_include_directories(androidcast_codecs PRIVATE "${THIRD_PARTY_ROOT}/opus/include") + endif() +else() + message(STATUS "No libopus.a for ${ANDROID_ABI} — Opus JNI probe off (build with scripts/build-native-codecs.sh ${ANDROID_ABI})") +endif() + +# libspeex.a +set(LIBSPEEX_LIB "") +if(ANDROID_ABI) + set(LIBSPEEX_PER_ABI "${NATIVE_LIBS_ROOT}/${ANDROID_ABI}/libspeex.a") + if(EXISTS "${LIBSPEEX_PER_ABI}") + set(LIBSPEEX_LIB "${LIBSPEEX_PER_ABI}") + endif() +endif() +if(NOT LIBSPEEX_LIB AND DEFINED ENV{ANDROIDCAST_LIBSPEEX_LIB}) + string(FIND "$ENV{ANDROIDCAST_LIBSPEEX_LIB}" "${ANDROID_ABI}" _speex_abi_match) + if(_speex_abi_match GREATER -1 AND EXISTS "$ENV{ANDROIDCAST_LIBSPEEX_LIB}") + set(LIBSPEEX_LIB "$ENV{ANDROIDCAST_LIBSPEEX_LIB}") + endif() +endif() +if(LIBSPEEX_LIB) + message(STATUS "Linking libspeex for ${ANDROID_ABI}: ${LIBSPEEX_LIB}") + target_compile_definitions(androidcast_codecs PRIVATE ANDROIDCAST_HAVE_SPEEX=1) + target_link_libraries(androidcast_codecs "${LIBSPEEX_LIB}") + if(EXISTS "${THIRD_PARTY_ROOT}/speex/include") + target_include_directories(androidcast_codecs PRIVATE "${THIRD_PARTY_ROOT}/speex/include") + endif() +else() + message(STATUS "No libspeex.a for ${ANDROID_ABI} — Speex JNI probe off (build with scripts/build-native-codecs.sh ${ANDROID_ABI})") +endif() + find_library(log-lib log) find_library(android-lib android) find_library(dl-lib dl) diff --git a/ndk/README.md b/ndk/README.md index 1cb2bfa..9ffd3aa 100644 --- a/ndk/README.md +++ b/ndk/README.md @@ -9,27 +9,34 @@ The app loads `libandroidcast_codecs.so` for availability probes and future soft | FEC 3/4 + RS | Yes | — | v2 **per-shard** on wire (`FecShardWire`); v1 monolithic decode still accepted | | NACK + retransmit cache | Yes | — | UDP `MSG_NACK`, combined with FEC when enabled | | libvpx VP8/VP9 | Native | Encode + decode | Screen cast via ImageReader; receiver libvpx (decoder uses `VPX_CODEC_USE_ERROR_CONCEALMENT` when supported) or MediaCodec fallback | -| Opus / Speex | Stub | Probe only | Submodules under `third-party/` | +| Opus / Speex | Probe + static link when `lib*.a` built | Encode/decode JNI sinks | `scripts/build-native-codecs.sh` → CMake autolink | -## Enable libvpx (developer) +## Enable native codecs (developer) -1. `git submodule update --init third-party/libvpx` -2. Build `libvpx.a` per ABI (NDK is auto-detected; see `scripts/android-ndk.sh`): +1. Initialize submodules as needed: - `./scripts/build-native-codecs.sh arm64-v8a` + ```bash + git submodule update --init third-party/libvpx + git submodule update --init third-party/opus + git submodule update --init third-party/speex + ``` - (If the repo path has **spaces**, the script uses `/tmp/androidcast-libvpx-$USER` automatically.) -3. Build **each ABI** you ship (Gradle links `build/native//libvpx.a` automatically): +2. Build static libs per ABI (NDK auto-detected via `scripts/android-ndk.sh`): -```bash -./scripts/build-native-codecs.sh arm64-v8a -./scripts/build-native-codecs.sh armeabi-v7a # if armeabi-v7a is in your APK -./gradlew assembleDebug -``` + ```bash + ./scripts/build-native-codecs.sh arm64-v8a + ./scripts/build-native-codecs.sh armeabi-v7a # if shipped in APK + ``` + + If the repo path has **spaces**, the script stages under `/tmp/androidcast-libvpx-$USER`. + +3. Gradle / CMake link `build/native//lib*.a` when present and set `ANDROIDCAST_HAVE_LIBVPX`, `ANDROIDCAST_HAVE_OPUS`, `ANDROIDCAST_HAVE_SPEEX` accordingly. + +4. `./gradlew assembleDebug` Optional override: `ANDROIDCAST_LIBVPX_LIB` only applies when its path contains the current ABI name (e.g. `.../arm64-v8a/libvpx.a`). -ABIs without a matching `libvpx.a` still build; software VPx is disabled on those architectures. +ABIs without matching `.a` files still build; missing codecs are disabled on those architectures. ## Stream protection defaults diff --git a/scripts/build-native-codecs.sh b/scripts/build-native-codecs.sh index 64ca2ac..de4edfb 100755 --- a/scripts/build-native-codecs.sh +++ b/scripts/build-native-codecs.sh @@ -1,8 +1,6 @@ #!/usr/bin/env bash -# Build libvpx for Android ABIs and produce libvpx.a for ANDROIDCAST_LIBVPX_LIB. -# -# With --enable-external-build, libvpx does not provide a libvpx.a make target; -# this script uses the NDK (ndk-build) + generated Android.mk, as upstream intends. +# Build libvpx, libopus, and libspeex for Android ABIs → build/native//*.a +# CMake in ndk/ autolinks when those archives exist (ANDROIDCAST_HAVE_* = 1). # # Prerequisites: # git submodule update --init third-party/libvpx @@ -12,6 +10,7 @@ # ./scripts/build-native-codecs.sh arm64-v8a # ANDROIDCAST_LIBVPX_LIB=$PWD/build/native/arm64-v8a/libvpx.a ./gradlew assembleDebug # +# Produces (when sources present): libvpx.a, libopus.a, libspeex.a under build/native// set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" @@ -226,6 +225,112 @@ if [[ "$OUT" != "$PROJECT_OUT" ]]; then cp -f "$OUT/libvpx.a" "$PROJECT_OUT/libvpx.a" fi +androidcast_ndk_api() { echo 29; } + +androidcast_ndk_clang() { + local abi="$1" + local api + api="$(androidcast_ndk_api)" + case "$abi" in + arm64-v8a) echo "$NDK_HOST_BIN/aarch64-linux-android${api}-clang" ;; + armeabi-v7a) echo "$NDK_HOST_BIN/armv7a-linux-androideabi${api}-clang" ;; + x86_64) echo "$NDK_HOST_BIN/x86_64-linux-android${api}-clang" ;; + x86) echo "$NDK_HOST_BIN/i686-linux-android${api}-clang" ;; + *) echo "ERROR: unsupported ABI $abi" >&2; return 1 ;; + esac +} + +androidcast_ndk_configure_host() { + case "$1" in + arm64-v8a) echo aarch64-linux-android ;; + armeabi-v7a) echo armv7a-linux-androideabi ;; + x86_64) echo x86_64-linux-android ;; + x86) echo i686-linux-android ;; + *) echo "$1" ;; + esac +} + +build_opus_for_abi() { + local opus_src="$ROOT/third-party/opus" + if [[ ! -f "$opus_src/CMakeLists.txt" ]]; then + echo "Skip libopus: $opus_src missing (git submodule update --init third-party/opus)" + return 0 + fi + local opus_build="$BUILD_DIR/opus-cmake" + rm -rf "$opus_build" + echo "Building libopus for $ABI (CMake) ..." + cmake -DCMAKE_TOOLCHAIN_FILE="$NDK/build/cmake/android.toolchain.cmake" \ + -DANDROID_ABI="$ABI" \ + -DANDROID_PLATFORM="android-$(androidcast_ndk_api)" \ + -DBUILD_SHARED_LIBS=OFF \ + -DOPUS_BUILD_SHARED_LIBRARY=OFF \ + -DOPUS_BUILD_TESTING=OFF \ + -DOPUS_BUILD_PROGRAMS=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + -B "$opus_build" -S "$opus_src" + cmake --build "$opus_build" --target opus -j"$(nproc)" + local opus_a="$opus_build/libopus.a" + if [[ ! -f "$opus_a" ]]; then + opus_a="$(find "$opus_build" -name 'libopus.a' | head -1)" + fi + if [[ ! -f "$opus_a" ]]; then + echo "ERROR: libopus.a not found after Opus build" + exit 1 + fi + cp -f "$opus_a" "$PROJECT_OUT/libopus.a" + echo "Built: $PROJECT_OUT/libopus.a (size $(ls -lah "${PROJECT_OUT}/libopus.a" | awk '{print $5}'))" +} + +build_speex_for_abi() { + local speex_src_real="$ROOT/third-party/speex" + if [[ ! -d "$speex_src_real" ]]; then + echo "Skip libspeex: $speex_src_real missing (git submodule update --init third-party/speex)" + return 0 + fi + local speex_src="$speex_src_real" + local speex_build="$BUILD_DIR/speex-autotools" + if path_has_space "$speex_src_real"; then + speex_src="$STAGING/speex-src" + ln -sfn "$speex_src_real" "$speex_src" + fi + if [[ ! -f "$speex_src/configure" ]]; then + echo "Running speex autogen.sh ..." + (cd "$speex_src" && ./autogen.sh) + fi + rm -rf "$speex_build" + mkdir -p "$speex_build" + local cc host + cc="$(androidcast_ndk_clang "$ABI")" + host="$(androidcast_ndk_configure_host "$ABI")" + echo "Building libspeex for $ABI (autotools) ..." + ( + cd "$speex_build" + "$speex_src/configure" \ + --host="$host" \ + --prefix="$speex_build/install" \ + --disable-shared \ + --enable-static \ + --disable-binaries \ + --with-pic \ + CC="$cc" \ + CFLAGS="-fPIC -O2" + make -j"$(nproc)" + ) + local speex_a="$speex_build/libspeex/.libs/libspeex.a" + if [[ ! -f "$speex_a" ]]; then + speex_a="$(find "$speex_build" -name 'libspeex.a' | head -1)" + fi + if [[ ! -f "$speex_a" ]]; then + echo "ERROR: libspeex.a not found after Speex build" + exit 1 + fi + cp -f "$speex_a" "$PROJECT_OUT/libspeex.a" + echo "Built: $PROJECT_OUT/libspeex.a (size $(ls -lah "${PROJECT_OUT}/libspeex.a" | awk '{print $5}'))" +} + +build_opus_for_abi +build_speex_for_abi + echo "" echo "Built: $PROJECT_OUT/libvpx.a (size $(ls -lah "${PROJECT_OUT}/libvpx.a" | awk '{print $5}'))" if [[ -n "$STAGING" ]]; then @@ -233,5 +338,5 @@ if [[ -n "$STAGING" ]]; then fi echo "Build other ABIs as needed, e.g.:" echo " ./scripts/build-native-codecs.sh armeabi-v7a" -echo "Then rebuild the APK (CMake picks build/native//libvpx.a per ABI):" +echo "Then rebuild the APK (CMake autolinks build/native//*.a per ABI):" echo " ./gradlew assembleDebug"