1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 05:17:39 +03:00
This commit is contained in:
Anton Afanasyeu
2026-06-08 12:38:57 +02:00
parent bfb41f06d9
commit c1591fa849
86 changed files with 5081 additions and 637 deletions

View File

@@ -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. 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. 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. 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 <hash>`. See [GIT_FLOW.md §4.1](docs/GIT_FLOW.md#41-fast-track-on-next-alpha-sprint).
## Quick commands ## Quick commands

View File

@@ -131,6 +131,8 @@ dependencies {
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20240303' testImplementation 'org.json:json:20240303'
implementation 'com.github.mwiede:jsch:0.2.21'
if (project.findProject(':tunnel') != null) { if (project.findProject(':tunnel') != null) {
implementation project(':tunnel') implementation project(':tunnel')
} }

View File

@@ -1,4 +1,4 @@
<h2>Android Cast</h2> <h2>Android Cast (application)</h2>
<p>Copyright &copy; Android Cast contributors.</p> <p>Copyright &copy; Android Cast contributors.</p>
<p>Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:</p> <p>Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:</p>
<ol> <ol>
@@ -8,25 +8,29 @@
</ol> </ol>
<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS &quot;AS IS&quot; 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.</p> <p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS &quot;AS IS&quot; 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.</p>
<h2>AndroidX AppCompat</h2> <h2>Android Open Source components (Gradle)</h2>
<p>Licensed under the <b>Apache License, Version 2.0</b>.</p> <ul>
<p>Copyright The Android Open Source Project.</p> <li><b>AndroidX AppCompat</b> — Apache License 2.0</li>
<li><b>AndroidX ConstraintLayout</b> — Apache License 2.0</li>
<h2>Google Material Components for Android</h2> <li><b>AndroidX RecyclerView</b> — Apache License 2.0</li>
<p>Licensed under the <b>Apache License, Version 2.0</b>.</p> <li><b>Google Material Components for Android</b>Apache License 2.0</li>
<li><b>Google Play services Cronet</b> — BSD 3-Clause (Chromium Cronet)</li>
<h2>AndroidX ConstraintLayout</h2> <li><b>Google Play Billing Library</b> — Google Play SDK terms (proprietary; see Google Play Console / developer policies)</li>
<p>Licensed under the <b>Apache License, Version 2.0</b>.</p> <li><b>Google Play In-App Review</b> — Google Play SDK terms</li>
<li><b>Eclipse Paho MQTT client</b> — Eclipse Public License 1.0 and Eclipse Distribution License 1.0 (dual-licensed)</li>
<h2>Google Play services Cronet</h2> <li><b>JSch (mwiede fork)</b> — BSD 3-Clause License (reverse SSH remote access)</li>
<p>Licensed under the <b>BSD 3-Clause License</b> (Chromium Cronet).</p> </ul>
<h2>JUnit 4</h2> <h2>JUnit 4</h2>
<p>Licensed under the <b>Eclipse Public License 1.0</b> (test dependency only).</p> <p>Licensed under the <b>Eclipse Public License 1.0</b> (test dependency only; not shipped in release APK).</p>
<h2>Future components (not yet bundled)</h2> <h2>Native codecs (optional; linked when built)</h2>
<p>Built from git submodules under <code>third-party/</code> via <code>scripts/build-native-codecs.sh</code>. CMake sets <code>ANDROIDCAST_HAVE_*</code> when <code>build/native/&lt;abi&gt;/lib*.a</code> exists for each ABI in the APK. JNI encode/decode sinks may still be partial while libraries are linked.</p>
<ul> <ul>
<li><b>libvpx</b> — BSD-style license (VP8/VP9 software codec, planned).</li> <li><b>libvpx</b> (VP8/VP9) — BSD-style license (Xiph.org). See <code>third-party/libvpx/LICENSE</code>.</li>
<li><b>Speex</b>revised BSD license (speech codec, planned).</li> <li><b>Opus</b>BSD-style license (Xiph.org et al.). See <code>third-party/opus/COPYING</code>.</li>
<li><b>Opus</b>BSD-style license (speech codec with in-band FEC, planned).</li> <li><b>Speex</b>Revised BSD license (Xiph.org Foundation). See <code>third-party/speex/COPYING</code>.</li>
</ul> </ul>
<h2>WireGuard tunnel module (optional Gradle module)</h2>
<p>When <code>third-party/wireguard-android</code> is initialized and the <code>:tunnel</code> module is included, the app may link the WireGuard Android tunnel library (Apache License 2.0; see upstream <code>wireguard-android</code>). Remote access still uses Android <code>VpnService</code> (system VPN permission).</p>

View File

@@ -36,6 +36,7 @@ public final class AppPreferences {
private static final String KEY_PLAY_INCOMING_AUDIO = "play_incoming_audio"; 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_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_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_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_SHOW_CODEC_PRIORITY_SCORES = "dev_show_codec_priority_scores";
private static final String KEY_DEV_RECEIVER_VIDEO_PRESET = "dev_receiver_video_preset"; 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(); 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). */ /** Send anonymous crash reports via the standalone crash watcher process (default on). */
public static boolean isSendAnonymousCrashLogs(Context context) { public static boolean isSendAnonymousCrashLogs(Context context) {
return prefs(context).getBoolean(KEY_SEND_ANONYMOUS_CRASH_LOGS, true); return prefs(context).getBoolean(KEY_SEND_ANONYMOUS_CRASH_LOGS, true);

View File

@@ -83,10 +83,11 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
CheckBox debugOverlay = findViewById(R.id.check_receiver_debug_overlay); CheckBox debugOverlay = findViewById(R.id.check_receiver_debug_overlay);
CheckBox codecScores = findViewById(R.id.check_show_codec_priority_scores); CheckBox codecScores = findViewById(R.id.check_show_codec_priority_scores);
CheckBox sessionStats = findViewById(R.id.check_grab_session_stats); 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 adaptiveDisplay = findViewById(R.id.check_receiver_adaptive_display);
CheckBox backendNtpSync = findViewById(R.id.check_dev_backend_ntp_sync); 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 -> findViewById(R.id.btn_open_session_stats).setOnClickListener(v ->
startActivity(new android.content.Intent(this, SessionStatsAnalyzerActivity.class))); 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_receiver_debug_overlay),
findViewById(R.id.check_show_codec_priority_scores), findViewById(R.id.check_show_codec_priority_scores),
findViewById(R.id.check_grab_session_stats), 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_receiver_adaptive_display),
findViewById(R.id.check_dev_backend_ntp_sync)); findViewById(R.id.check_dev_backend_ntp_sync));
bindDiagPingSection(); bindDiagPingSection();
@@ -167,7 +169,7 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
String[] labels = new String[]{ String[] labels = new String[]{
getString(R.string.dev_remote_access_disabled), getString(R.string.dev_remote_access_disabled),
getString(R.string.dev_remote_access_wireguard), getString(R.string.dev_remote_access_wireguard),
getString(R.string.dev_remote_access_rssh_unavailable), getString(R.string.dev_remote_access_rssh),
}; };
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, labels); android.R.layout.simple_spinner_item, labels);
@@ -186,15 +188,6 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
return; return;
} }
RemoteAccessMode picked = modes[position]; 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); applyRemoteAccessMode(picked);
} }
@@ -518,13 +511,15 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
} }
private void refreshFromPreferences(CheckBox debugOverlay, CheckBox codecScores, CheckBox sessionStats, private void refreshFromPreferences(CheckBox debugOverlay, CheckBox codecScores, CheckBox sessionStats,
CheckBox adaptiveDisplay, CheckBox backendNtpSync) { CheckBox streamDump, CheckBox adaptiveDisplay, CheckBox backendNtpSync) {
bindDeveloperCheckbox(debugOverlay, AppPreferences.isShowReceiverDebugOverlay(this), bindDeveloperCheckbox(debugOverlay, AppPreferences.isShowReceiverDebugOverlay(this),
AppPreferences::setShowReceiverDebugOverlay); AppPreferences::setShowReceiverDebugOverlay);
bindDeveloperCheckbox(codecScores, AppPreferences.isShowCodecPriorityScores(this), bindDeveloperCheckbox(codecScores, AppPreferences.isShowCodecPriorityScores(this),
AppPreferences::setShowCodecPriorityScores); AppPreferences::setShowCodecPriorityScores);
bindDeveloperCheckbox(sessionStats, AppPreferences.isGrabSessionStats(this), bindDeveloperCheckbox(sessionStats, AppPreferences.isGrabSessionStats(this),
AppPreferences::setGrabSessionStats); AppPreferences::setGrabSessionStats);
bindDeveloperCheckbox(streamDump, AppPreferences.isDevStreamDumpEnabled(this),
AppPreferences::setDevStreamDumpEnabled);
bindDeveloperCheckbox(adaptiveDisplay, AppPreferences.isDevReceiverAdaptiveDisplay(this), bindDeveloperCheckbox(adaptiveDisplay, AppPreferences.isDevReceiverAdaptiveDisplay(this),
AppPreferences::setDevReceiverAdaptiveDisplay); AppPreferences::setDevReceiverAdaptiveDisplay);
bindDeveloperCheckbox(backendNtpSync, AppPreferences.isDevBackendNtpSyncEnabled(this), bindDeveloperCheckbox(backendNtpSync, AppPreferences.isDevBackendNtpSyncEnabled(this),

View File

@@ -14,6 +14,7 @@ package com.foxx.androidcast.crash;
import android.content.Context; import android.content.Context;
import com.foxx.androidcast.AppPreferences; import com.foxx.androidcast.AppPreferences;
import com.foxx.androidcast.network.BackendEndpoints;
import com.foxx.androidcast.settings.AppSettingsJson; import com.foxx.androidcast.settings.AppSettingsJson;
import org.json.JSONObject; import org.json.JSONObject;
@@ -39,7 +40,7 @@ public final class CrashSettingsStore {
} }
String url = crash.optString("upload_url", "").trim(); String url = crash.optString("upload_url", "").trim();
if (!url.isEmpty()) { if (!url.isEmpty()) {
uploadUrl = url; uploadUrl = BackendEndpoints.normalizeCrashesUrl(url);
} }
if (crash.has("prune_after_upload")) { if (crash.has("prune_after_upload")) {
prune = parseBool(crash.optString("prune_after_upload", "true"), prune); prune = parseBool(crash.optString("prune_after_upload", "true"), prune);

View File

@@ -26,7 +26,8 @@ public final class DevDiagnosticsController {
AUDIO(R.id.dev_diag_audio, R.string.dev_diag_audio_title), 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), 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), 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 includeId;
final int titleRes; final int titleRes;
@@ -107,6 +108,8 @@ public final class DevDiagnosticsController {
DevDiagnosticsProbe.probeNetwork(activity, cancel), sectionText); DevDiagnosticsProbe.probeNetwork(activity, cancel), sectionText);
runSection(Section.CASTING, () -> runSection(Section.CASTING, () ->
DevDiagnosticsProbe.probeCastingExtras(activity), sectionText); DevDiagnosticsProbe.probeCastingExtras(activity), sectionText);
runSection(Section.ADVERTISEMENT, () ->
DevDiagnosticsProbe.probeFullAdvertisementInfo(activity), sectionText);
} finally { } finally {
if (!cancel.get()) { if (!cancel.get()) {
lastReport = new DevDiagnosticsReport(System.currentTimeMillis(), sectionText); lastReport = new DevDiagnosticsReport(System.currentTimeMillis(), sectionText);

View File

@@ -29,6 +29,7 @@ import com.foxx.androidcast.media.CodecPriorityCatalog;
import com.foxx.androidcast.media.CodecTier; import com.foxx.androidcast.media.CodecTier;
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge; import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
import com.foxx.androidcast.network.CastCodecFlags; import com.foxx.androidcast.network.CastCodecFlags;
import com.foxx.androidcast.network.transport.StreamProtectionCapability;
import com.foxx.androidcast.sender.CodecCatalog; import com.foxx.androidcast.sender.CodecCatalog;
import java.io.IOException; import java.io.IOException;
@@ -53,9 +54,9 @@ public final class DevDiagnosticsProbe {
sb.append("API ").append(Build.VERSION.SDK_INT).append('\n'); sb.append("API ").append(Build.VERSION.SDK_INT).append('\n');
sb.append("Native JNI probes:\n"); sb.append("Native JNI probes:\n");
sb.append(" libvpx: ").append(NativeCodecBridge.isLibvpxAvailable() ? "yes" : "no").append('\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'); .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'); .append('\n');
sb.append('\n').append("Video encoders (MediaCodec):\n"); sb.append('\n').append("Video encoders (MediaCodec):\n");
appendVideoCodecLines(sb, true, settings); appendVideoCodecLines(sb, true, settings);
@@ -173,6 +174,27 @@ public final class DevDiagnosticsProbe {
return sb.toString().trim(); 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<String> videoMimes = CodecCatalog.localAdvertisedVideoMimesInOrder();
sb.append("[video adv] ").append(join(videoMimes));
List<String> 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) { public static String probeCastingExtras(Context context) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
PackageManager pm = context.getPackageManager(); PackageManager pm = context.getPackageManager();

View File

@@ -12,8 +12,10 @@ import android.os.PowerManager;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import com.foxx.androidcast.AppPreferences; 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.crash.CrashSettingsStore;
import com.foxx.androidcast.discovery.CastLanPeerProbe;
import com.foxx.androidcast.network.BackendEndpoints;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.InputStream; import java.io.InputStream;
@@ -51,7 +53,7 @@ public final class DevNetworkSelfTestProbe {
probeOverall(context, caps, cancel, report); probeOverall(context, caps, cancel, report);
probeLink(context, caps, lp, report); probeLink(context, caps, lp, report);
probeCaptive(caps, cancel, report); probeCaptive(caps, cancel, report);
probeIntraLan(lp, cancel, report); probeIntraLan(context, lp, cancel, report);
probeBackend(context, cancel, report); probeBackend(context, cancel, report);
probeNtp(context, cancel, report); probeNtp(context, cancel, report);
probeNatExternal(caps, 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"); r.section("LAN", "Intra-network");
String ip = firstIpv4Host(lp); String ip = firstIpv4Host(lp);
if (ip == null) { if (ip == null) {
@@ -203,62 +206,36 @@ public final class DevNetworkSelfTestProbe {
return; return;
} }
String prefix = parts[0] + "." + parts[1] + "." + parts[2] + "."; String prefix = parts[0] + "." + parts[1] + "." + parts[2] + ".";
int self = Integer.parseInt(parts[3]);
List<Long> 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 + ")"); r.itemPass("subnet", prefix + "x (self " + ip + ")");
if (peers == 0) { CastLanPeerProbe.Result scan = CastLanPeerProbe.scan(context, cancel);
r.itemWarn("peers", "0 reachable (+/-8 quick scan)"); r.itemPass("scan-ms", Long.toString(scan.elapsedMs));
r.itemNa("avg-rtt", "n/a"); if (scan.peers == 0) {
r.itemWarn("cast-peers",
"0 on LAN (UDP :" + CastConfig.DISCOVERY_PORT + ", need peer listening/Receive)");
} else { } else {
long sum = 0; r.itemPass("cast-peers", scan.peers + " · " + scan.summary);
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("scan-ms", Long.toString(elapsed));
} }
private static void probeBackend(Context context, AtomicBoolean cancel, DevNetworkSelfTestReport r) { private static void probeBackend(Context context, AtomicBoolean cancel, DevNetworkSelfTestReport r) {
r.section("BACKEND", "Backend reachability"); r.section("BACKEND", "Backend reachability");
String url = resolveBackendUrl(context); String heartbeat = BackendEndpoints.HEARTBEAT_URL;
if (url.isEmpty()) { r.itemPass("url", shortenUrl(heartbeat));
r.itemWarn("url", "not configured"); String configured = BackendEndpoints.normalizeCrashesUrl(
return; 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()) { if (cancel != null && cancel.get()) {
r.itemWarn("probe", "stopped"); r.itemWarn("probe", "stopped");
return; return;
} }
HttpResult head = httpHead(url); HttpResult head = httpHead(BackendEndpoints.CRASHES_BASE + "/");
reportHttp(r, "head", head.code, head.summary); 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); 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); reportHttp(r, "uplink", uplink.code, uplink.summary);
} }
@@ -273,12 +250,7 @@ public final class DevNetworkSelfTestProbe {
if (!enabled) { if (!enabled) {
return; return;
} }
String backend = resolveBackendUrl(context); String backend = BackendEndpoints.HEARTBEAT_URL;
if (backend.isEmpty()) {
r.itemWarn("be-ntp", "backend URL missing");
return;
}
String heartbeatUrl = toHeartbeatUrl(backend);
if (cancel != null && cancel.get()) { if (cancel != null && cancel.get()) {
r.itemWarn("be-ntp", "stopped"); r.itemWarn("be-ntp", "stopped");
return; return;
@@ -286,7 +258,7 @@ public final class DevNetworkSelfTestProbe {
long now = System.currentTimeMillis() / 1000L; long now = System.currentTimeMillis() / 1000L;
String tz = new java.text.SimpleDateFormat("Z", Locale.US).format(new java.util.Date()); String tz = new java.text.SimpleDateFormat("Z", Locale.US).format(new java.util.Date());
String body = "{\"heartbeat\":{\"type\":\"ntp\",\"date\":" + now + ",\"tz\":\"" + tz + "\"}}"; String body = "{\"heartbeat\":{\"type\":\"ntp\",\"date\":" + now + ",\"tz\":\"" + tz + "\"}}";
HttpTextResult text = httpPostJsonText(heartbeatUrl, body); HttpTextResult text = httpPostJsonText(backend, body);
if (httpSuccess(text.code)) { if (httpSuccess(text.code)) {
r.itemPass("be-ntp", text.summary); r.itemPass("be-ntp", text.summary);
if (text.body != null && !text.body.isEmpty()) { if (text.body != null && !text.body.isEmpty()) {
@@ -319,29 +291,17 @@ public final class DevNetworkSelfTestProbe {
r.itemWarn("external-ip", "stopped"); r.itemWarn("external-ip", "stopped");
return; return;
} }
String backend = resolveBackendUrl(context); HttpTextResult viaBackend = httpPostJsonText(BackendEndpoints.HEARTBEAT_URL,
if (!backend.isEmpty()) { "{\"heartbeat\":{\"type\":\"ip\"}}");
HttpTextResult viaBackend = httpPostJsonText(toHeartbeatUrl(backend), if (viaBackend.code < 0) {
"{\"heartbeat\":{\"type\":\"ip\"}}"); r.itemFail("external-ip", viaBackend.summary);
if (viaBackend.code < 0) { } else if (!httpSuccess(viaBackend.code)) {
r.itemFail("external-ip", viaBackend.summary); 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);
} else { } 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) + "..."; 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) { private static boolean hasTransport(NetworkCapabilities caps, int t) {
return caps != null && caps.hasTransport(t); return caps != null && caps.hasTransport(t);
} }

View File

@@ -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();
}
}

View File

@@ -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<String, String> 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<String, String> 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<String, String> 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) {
}
}
}

View File

@@ -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;
}
}

View File

@@ -13,7 +13,9 @@ package com.foxx.androidcast.network.transport;
**********************************************************************/ **********************************************************************/
import com.foxx.androidcast.CastSettings; import com.foxx.androidcast.CastSettings;
import java.util.ArrayList;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List;
/** Runtime FEC/NACK implementations on this device (not a user setting). */ /** Runtime FEC/NACK implementations on this device (not a user setting). */
public final class StreamProtectionCapability { public final class StreamProtectionCapability {
@@ -54,4 +56,14 @@ public final class StreamProtectionCapability {
} }
return EnumSet.of(CastSettings.StreamProtection.NONE); return EnumSet.of(CastSettings.StreamProtection.NONE);
} }
/** Modes this device advertises on the wire (handshake uses AUTO → full set). */
public static List<String> advertisedProtectionInOrder() {
List<String> out = new ArrayList<>();
out.add("AUTO (handshake)");
for (CastSettings.StreamProtection mode : receiverImplementations()) {
out.add(mode.name());
}
return out;
}
} }

View File

@@ -21,7 +21,7 @@ public final class RemoteAccessCoordinator {
AppPreferences.clearDevRemoteAccessSession(app); AppPreferences.clearDevRemoteAccessSession(app);
return; return;
} }
if (mode == RemoteAccessMode.WIREGUARD) { if (mode == RemoteAccessMode.WIREGUARD || mode == RemoteAccessMode.RSSH) {
RemoteAccessService.start(app, mode); RemoteAccessService.start(app, mode);
return; return;
} }

View File

@@ -8,7 +8,7 @@ public enum RemoteAccessMode {
DISABLED, DISABLED,
/** WireGuard outbound tunnel (v1). */ /** WireGuard outbound tunnel (v1). */
WIREGUARD, WIREGUARD,
/** Reverse SSH — reserved; not selectable in UI until implemented. */ /** Reverse SSH — alpha essential; selectable when implemented. */
RSSH; RSSH;
public static RemoteAccessMode fromStored(String name) { public static RemoteAccessMode fromStored(String name) {
@@ -39,6 +39,6 @@ public enum RemoteAccessMode {
} }
public boolean isActive() { public boolean isActive() {
return this == WIREGUARD; return this == WIREGUARD || this == RSSH;
} }
} }

View File

@@ -62,7 +62,7 @@ public final class RemoteAccessService extends Service {
long exp = AppPreferences.getDevRemoteAccessExpiresAt(this); long exp = AppPreferences.getDevRemoteAccessExpiresAt(this);
if (exp > 0 && exp < System.currentTimeMillis() / 1000L) { if (exp > 0 && exp < System.currentTimeMillis() / 1000L) {
Log.i(TAG, "session expired — tearing down tunnel"); Log.i(TAG, "session expired — tearing down tunnel");
WireGuardTunnelBridge.tearDown(this); RemoteAccessTunnelHelper.tearDownAll(this);
AppPreferences.clearDevRemoteAccessSession(this); AppPreferences.clearDevRemoteAccessSession(this);
} }
} }
@@ -95,7 +95,7 @@ public final class RemoteAccessService extends Service {
String action = intent != null ? intent.getAction() : ACTION_POLL; String action = intent != null ? intent.getAction() : ACTION_POLL;
if (ACTION_STOP.equals(action)) { if (ACTION_STOP.equals(action)) {
cancelPoll(this); cancelPoll(this);
WireGuardTunnelBridge.tearDown(this); RemoteAccessTunnelHelper.tearDownAll(this);
stopForeground(true); stopForeground(true);
stopSelf(); stopSelf();
return START_NOT_STICKY; 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("app_version", com.foxx.androidcast.BuildConfig.VERSION_NAME);
hb.put("tunnel_mode", mode.toApiValue()); hb.put("tunnel_mode", mode.toApiValue());
JSONArray caps = new JSONArray(); 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"); caps.put("files_app_home");
hb.put("capabilities", caps); hb.put("capabilities", caps);
@@ -147,7 +152,7 @@ public final class RemoteAccessService extends Service {
if ("connect".equals(raAction)) { if ("connect".equals(raAction)) {
handleConnect(resp); handleConnect(resp);
} else if ("disabled".equals(raAction) || "deny".equals(raAction)) { } else if ("disabled".equals(raAction) || "deny".equals(raAction)) {
WireGuardTunnelBridge.tearDown(this); RemoteAccessTunnelHelper.tearDownAll(this);
} }
} catch (Exception e) { } catch (Exception e) {
Log.w(TAG, "poll failed: " + e.getMessage()); Log.w(TAG, "poll failed: " + e.getMessage());
@@ -156,7 +161,7 @@ public final class RemoteAccessService extends Service {
private void postDisableHeartbeat() { private void postDisableHeartbeat() {
try { try {
WireGuardTunnelBridge.tearDown(this); RemoteAccessTunnelHelper.tearDownAll(this);
AppPreferences.clearDevRemoteAccessSession(this); AppPreferences.clearDevRemoteAccessSession(this);
JSONObject hb = new JSONObject(); JSONObject hb = new JSONObject();
hb.put("type", "ra"); hb.put("type", "ra");
@@ -170,26 +175,34 @@ public final class RemoteAccessService extends Service {
} }
private void handleConnect(JSONObject resp) { private void handleConnect(JSONObject resp) {
if (!"wireguard".equals(resp.optString("tunnel", ""))) { String tunnel = resp.optString("tunnel", "");
Log.w(TAG, "unsupported tunnel in connect");
return;
}
String sessionId = resp.optString("session_id", ""); String sessionId = resp.optString("session_id", "");
long expiresAt = resp.optLong("expires_at", 0L); long expiresAt = resp.optLong("expires_at", 0L);
if (!sessionId.isEmpty()) { if (!sessionId.isEmpty()) {
AppPreferences.setDevRemoteAccessSession(this, sessionId, expiresAt); AppPreferences.setDevRemoteAccessSession(this, sessionId, expiresAt);
} }
JSONObject creds = resp.optJSONObject("credentials"); JSONObject creds = resp.optJSONObject("credentials");
String wgConfig = WireGuardConfigBuilder.fromConnectCredentials(creds); if (creds == null) {
if (wgConfig.isEmpty()) {
return; return;
} }
if (WireGuardTunnelBridge.applyConfig(this, wgConfig)) { boolean ok = false;
updateNotificationConnected(sessionId); 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); NotificationManager nm = getSystemService(NotificationManager.class);
if (nm == null) { if (nm == null) {
return; return;
@@ -197,6 +210,9 @@ public final class RemoteAccessService extends Service {
String text = sessionId.isEmpty() String text = sessionId.isEmpty()
? getString(R.string.dev_remote_access_notification_connected) ? getString(R.string.dev_remote_access_notification_connected)
: getString(R.string.dev_remote_access_notification_session, sessionId); : 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) nm.notify(NOTIF_ID, new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification) .setSmallIcon(R.drawable.ic_notification)
.setContentTitle(getString(R.string.dev_remote_access_notification_title)) .setContentTitle(getString(R.string.dev_remote_access_notification_title))

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -150,6 +150,7 @@ public final class CastSendPump {
} }
byte[] payload = item.buildPayload(); byte[] payload = item.buildPayload();
session.send(item.type, payload); session.send(item.type, payload);
com.foxx.androidcast.dev.StreamDumpWriter.recordFrame(item.type, payload);
int payloadSize = payload != null ? payload.length : 0; int payloadSize = payload != null ? payload.length : 0;
SessionStatsContext.recordOutboundMessage(item.type, item.keyFrame, payloadSize); SessionStatsContext.recordOutboundMessage(item.type, item.keyFrame, payloadSize);
if (statsPlane != null) { if (statsPlane != null) {

View File

@@ -118,6 +118,35 @@ public final class CodecCatalog {
return localAdvertisedAudioEncoderLines(settings); return localAdvertisedAudioEncoderLines(settings);
} }
/** Audio codec names advertised on wire (priority order; excludes AUTO). */
public static List<String> localAdvertisedAudioCodecsInOrder(CastSettings settings) {
List<String> 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<String> localAdvertisedVideoMimesInOrder() {
return localEncoderMimes();
}
/** Video codec labels for diagnostics (priority order; excludes AUTO). */
public static List<String> localAdvertisedVideoCodecsInOrder(CastSettings settings) {
List<String> 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) { private static String audioAdvertisedLine(CastSettings.AudioCodec codec, String mime, CastSettings settings) {
CodecTier tier = com.foxx.androidcast.media.CodecPriorityCatalog.tierForAudioPreference(codec); CodecTier tier = com.foxx.androidcast.media.CodecPriorityCatalog.tierForAudioPreference(codec);
int score = com.foxx.androidcast.media.CodecPriorityCatalog.priorityScore(codec, settings); int score = com.foxx.androidcast.media.CodecPriorityCatalog.priorityScore(codec, settings);

View File

@@ -294,6 +294,7 @@ public class ScreenCastService extends Service implements
sendPump = new CastSendPump(); sendPump = new CastSendPump();
sendPump.start(session, () -> stopping.set(true), plane); sendPump.start(session, () -> stopping.set(true), plane);
} }
com.foxx.androidcast.dev.StreamDumpWriter.startIfEnabled(this);
casting.set(true); casting.set(true);
CastActiveState.setSenderCasting(true); CastActiveState.setSenderCasting(true);
CastTrayNotifier.refresh(this); CastTrayNotifier.refresh(this);
@@ -989,6 +990,7 @@ public class ScreenCastService extends Service implements
sendPump.stop(); sendPump.stop();
sendPump = null; sendPump = null;
} }
com.foxx.androidcast.dev.StreamDumpWriter.finalizeIfActive(this);
if (fanoutPump != null) { if (fanoutPump != null) {
fanoutPump.stop(); fanoutPump.stop();
fanoutPump = null; fanoutPump = null;

View File

@@ -56,6 +56,10 @@
<include <include
android:id="@+id/dev_diag_casting" android:id="@+id/dev_diag_casting"
layout="@layout/view_dev_diag_card" /> layout="@layout/view_dev_diag_card" />
<include
android:id="@+id/dev_diag_advertisement"
layout="@layout/view_dev_diag_card" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
</LinearLayout> </LinearLayout>

View File

@@ -30,6 +30,20 @@
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
android:text="@string/dev_grab_session_stats" /> android:text="@string/dev_grab_session_stats" />
<CheckBox
android:id="@+id/check_dev_stream_dump"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_stream_dump" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/dev_stream_dump_hint"
android:textSize="12sp" />
<CheckBox <CheckBox
android:id="@+id/check_dev_diag_ping_pong" android:id="@+id/check_dev_diag_ping_pong"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -212,6 +212,7 @@
<string name="dev_diag_audio_codecs_title">Аудиокодеки (сжатие)</string> <string name="dev_diag_audio_codecs_title">Аудиокодеки (сжатие)</string>
<string name="dev_diag_network_title">Сеть</string> <string name="dev_diag_network_title">Сеть</string>
<string name="dev_diag_casting_title">Другой вывод / трансляция</string> <string name="dev_diag_casting_title">Другой вывод / трансляция</string>
<string name="dev_diag_advertisement_title">Полная реклама возможностей</string>
<string name="dev_sender_resolution_ui_section">Разрешение отправителя (тест)</string> <string name="dev_sender_resolution_ui_section">Разрешение отправителя (тест)</string>
<string name="dev_sender_resolution_ui_mode">Режим списка разрешений</string> <string name="dev_sender_resolution_ui_mode">Режим списка разрешений</string>
<string name="dev_sender_resolution_ui_separated">Раздельный (Оригинал / 720p / 480p / Авто)</string> <string name="dev_sender_resolution_ui_separated">Раздельный (Оригинал / 720p / 480p / Авто)</string>

View File

@@ -223,6 +223,7 @@
<string name="dev_diag_audio_codecs_title">Audio codecs (compression)</string> <string name="dev_diag_audio_codecs_title">Audio codecs (compression)</string>
<string name="dev_diag_network_title">Network</string> <string name="dev_diag_network_title">Network</string>
<string name="dev_diag_casting_title">Other casting / display</string> <string name="dev_diag_casting_title">Other casting / display</string>
<string name="dev_diag_advertisement_title">Full advertisement info</string>
<string name="dev_sender_resolution_ui_section">Sender resolution UI (test)</string> <string name="dev_sender_resolution_ui_section">Sender resolution UI (test)</string>
<string name="dev_sender_resolution_ui_mode">Resolution preset mode</string> <string name="dev_sender_resolution_ui_mode">Resolution preset mode</string>
<string name="dev_sender_resolution_ui_separated">Separated (Original / 720p / 480p / Auto)</string> <string name="dev_sender_resolution_ui_separated">Separated (Original / 720p / 480p / Auto)</string>
@@ -254,6 +255,8 @@
<string name="dev_show_receiver_debug_overlay">Show debug overlay on receiver</string> <string name="dev_show_receiver_debug_overlay">Show debug overlay on receiver</string>
<string name="dev_show_codec_priority_scores">Show codec priority scores in settings</string> <string name="dev_show_codec_priority_scores">Show codec priority scores in settings</string>
<string name="dev_grab_session_stats">Grab anonymous session stats</string> <string name="dev_grab_session_stats">Grab anonymous session stats</string>
<string name="dev_stream_dump">Record stream dump while casting (FR 0.1)</string>
<string name="dev_stream_dump_hint">Writes zip under files/stream_dumps/ (max 5 min or 100 MB). Pull via adb.</string>
<string name="dev_diag_ping_pong">Diagnostic cast ping-pong (~1/s)</string> <string name="dev_diag_ping_pong">Diagnostic cast ping-pong (~1/s)</string>
<string name="dev_diag_ping_response_mode">Ping response mode (this device)</string> <string name="dev_diag_ping_response_mode">Ping response mode (this device)</string>
<string name="dev_diag_ping_response_mirror">Mirror as-is (dummy pong)</string> <string name="dev_diag_ping_response_mirror">Mirror as-is (dummy pong)</string>
@@ -265,8 +268,9 @@
<string name="dev_remote_access_mode">Remote access (on-demand debug)</string> <string name="dev_remote_access_mode">Remote access (on-demand debug)</string>
<string name="dev_remote_access_disabled">Disabled</string> <string name="dev_remote_access_disabled">Disabled</string>
<string name="dev_remote_access_wireguard">WireGuard</string> <string name="dev_remote_access_wireguard">WireGuard</string>
<string name="dev_remote_access_rssh_unavailable">RSSH (reverse SSH — not available yet)</string> <string name="dev_remote_access_rssh">RSSH (reverse SSH)</string>
<string name="dev_remote_access_hint">When enabled, device polls BE every 17 min (type: ra). Disabled sends teardown + BE notification. RSSH is reserved for a future release.</string> <string name="dev_remote_access_rssh_unavailable">RSSH (reverse SSH — alpha; not available yet)</string>
<string name="dev_remote_access_hint">When enabled, device polls BE every 17 min (type:ra). WireGuard needs Android VPN permission once. RSSH uses outbound SSH — no VPN prompt.</string>
<string name="dev_remote_access_rssh_toast">Reverse SSH (RSSH) is not implemented yet.</string> <string name="dev_remote_access_rssh_toast">Reverse SSH (RSSH) is not implemented yet.</string>
<string name="dev_remote_access_applied">Remote access: %1$s</string> <string name="dev_remote_access_applied">Remote access: %1$s</string>
<string name="dev_remote_access_vpn_denied">VPN permission required for WireGuard remote access.</string> <string name="dev_remote_access_vpn_denied">VPN permission required for WireGuard remote access.</string>
@@ -277,7 +281,7 @@
<string name="dev_remote_access_notification_session">Connected · session %1$s</string> <string name="dev_remote_access_notification_session">Connected · session %1$s</string>
<string name="dev_tab_network_self_test">Network self-test</string> <string name="dev_tab_network_self_test">Network self-test</string>
<string name="dev_network_selftest_unlocked">Network self-test tab unlocked</string> <string name="dev_network_selftest_unlocked">Network self-test tab unlocked</string>
<string name="dev_network_selftest_intro">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.</string> <string name="dev_network_selftest_intro">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.</string>
<string name="dev_network_selftest_run">Run self-test</string> <string name="dev_network_selftest_run">Run self-test</string>
<string name="dev_network_selftest_force">Force re-check</string> <string name="dev_network_selftest_force">Force re-check</string>
<string name="dev_network_selftest_pending">Waiting for self-test run…</string> <string name="dev_network_selftest_pending">Waiting for self-test run…</string>

View File

@@ -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());
}
}

View File

@@ -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));
}
}

View File

@@ -29,9 +29,14 @@ public class RemoteAccessModeTest {
} }
@Test @Test
public void isActive_onlyWireGuard() { public void toApiValue_rssh() {
assertEquals("rssh", RemoteAccessMode.RSSH.toApiValue());
}
@Test
public void isActive_wireguardAndRssh() {
assertFalse(RemoteAccessMode.DISABLED.isActive()); assertFalse(RemoteAccessMode.DISABLED.isActive());
assertTrue(RemoteAccessMode.WIREGUARD.isActive()); assertTrue(RemoteAccessMode.WIREGUARD.isActive());
assertFalse(RemoteAccessMode.RSSH.isActive()); assertTrue(RemoteAccessMode.RSSH.isActive());
} }
} }

View File

@@ -453,7 +453,7 @@ endobj
endobj endobj
78 0 obj 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 /Subject (\(unspecified\)) /Title (Android Cast \204 On-Demand Remote Access \(Reverse Tunnel\) Proposals) /Trapped /False
>> >>
endobj endobj
@@ -995,7 +995,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<27be2a803248e97757fa9331ed0b01e2><27be2a803248e97757fa9331ed0b01e2>] [<0bc2f0c306f87efd0a3d63157383ae9d><0bc2f0c306f87efd0a3d63157383ae9d>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 78 0 R /Info 78 0 R

View File

@@ -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
<!-- toc -->
- [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)
<!-- /toc -->
**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:<forwarder-or-smtp-provider> ~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 <noreply@apps.f0xx.org>
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 <noreply@apps.f0xx.org>',
'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 `<img>` or SVG; “Cant 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.51 day ops |
| **P1 — Register + verify** | Pending table, mailer, verify link, captcha | 23 days |
| **P2 — TOTP login** | Enroll QR + challenge on login | 12 days |
| **P3 — Admin tools** | Recovery email, clear lockouts, audit | 1 day |
| **P4 — WebAuthn** | Passkey enroll/login (Chrome/Android) | 12 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) |

View File

@@ -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<eg`Z*M)#qo;n9oB+E9[O^bh9-Xnq/=j2]0SA1'0Ckr,G7m(,37u1F[PQn#=_hp\b4A($-agK%1c;;<PTB5JWmt_%8*$i$AKZl6YMfG^KV"Y;#heS7DH[_e8m0k=85$B^^]qH"7m.GkrqK@i#5+1/8anP)_'=4SSo/*bm=g,F'.M$[<&B'jPHE2&Gr4T^I3%A[A1/[7WMR\n9i=rrQku\Gdo@J3L^k+SuW3a\Nd<p4R\(,Q9=\IA@''\j0FfDYVf(]<VEm$1iK?_)EE**KR#&IO!:L1=9DPF$aarN-iC?N))>_D)GZV:i030)sd%H5F(p9Xd$hHV5\&\+%Xt=q&9\IUVl<qBW"(DDs^U03;:=$HrPAo-CIb*@G)Z%T`qIWof5T1n.3[bNVW_Bf]f'>"?oC(fgEptf?>Erj7jTJI/)-T2Nn@Sbm1diXCq"ahn)O[7AjV2U>:E0F!3G8iIfK,WmNhW#;GNK$3`8%S!'Zrf<g(C]5Z($]["JXkhloJ_%10HnS[UUnL/dSNG<-\:3u@9F./SUb0&3&*1hm%+p<F"P-GE6.5E=9<j^RO1h`*ioC+8'[0=G)d+=Tr7W@,IKkTc\qUQfY"kOCr_O=&fS'YR#!+m"3'p64S.P3C]&T!fW?Oj["V.mZ1LV;:@(@e!JVsb>m]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^DW<D\hT-:L,94$-(pc0G#%E?fF/l"N$Pftb5rod=ffH&83P3X\L)bL=Cl7VrgC,Y>mOlTKc(d3M,Q73Ia)q/,@.arUTMO(n]Tr?_5`b["Io$`LpLiQ+2)h-hi(I#D#p%\Lfht(=gh/fe\3aKLDq@;bW%dBA8WEWq^2cVhWQjboK.<lPI"XmuTDPjZ#m*UWp?;eE=ZqVd@#,?ONVDsFaku((65%+IErWgf4`Rb~>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/<jT[0&UnQm_FLs[4IARu42-hsa\u[&r16H[$Wkj$9$nj(J03-HJEfUp&IsqT^5g:0=\u\R7jleMQU<sF,kW=H/Cg>*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_<biKc2T0+KALs+,P7JnPD^_)CON]&G5-@tQ/>BH"Q>WVFc3:WLGM0&V:3Sl)*i$(A?_QUliAFFA?p)H[^)u'sAs)fY_Yc6mFPU`<sO([CjT$meHcg;aE>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<bu*7=g%Fe"OWdD4Epn7#iE(FZ'po5(cW(-"M=KW1k6XC[XlG[V\`fQ@iPWNKk;0nFW)d'5$g0,B$PltjulMMPik0O1/Akk"/a(%^3^YgP3l2@Q?DGsu0&!:TF)nMr1Z_&G>%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):e<q!D\Vt,g*gPJNAX8eb)?>q;<^@<$/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&<BM$bY)$TM([%jVK&=dA)]VFE]iNIc_*WVqA8PN':9'rrp;%Hns&%N,bgI"4bE#'pd\c2dB++78H_!d[eX<#!kP$AaQ_d<osX*$Z/LPG70H/eBcA3_Ai#6NW?FpquD(5+4AHEnAYbU:g+D=Es'L'#*NO1bO8:0<RMMi9XX-k=('3]&:ZpIPk,)dtn&KYd^O,m+u['+OM4A.RoS)A0G2S!%3e'E[4-1f<@H8?S@?,8^[kYN$b`Kn:u[M\h'\T<6C#0qerDcY)U]KKLdL^k>X]Nhd$qWh"N3q#j6eF^nNR5@M6DKppK4:p2Y_.-^Fb_tbpQ9raUKJ"SgLLK5<9EHg*en[eIXnig[=j?:7F=RgXo\Yrsn*o<Ml@/o#EE-I6I+lt6*'%=<L1m%X+hZg7;X!'>%QkqO8DBYBi0r/(\T9)<>46G6RGh3D1l?4dZe?O.E3cs]F7G&EDVgIU<g_Du3lIP!Jrjsq<2J]6_,MXcd6t@?38em]j04(O8hkp:nh[^l8mth/R\jPhl'`N!qY5lK5aV.<&r>J1A(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<V7"[k@)o#QrDO08N5-g7&fI?AE$/g!QH"n5fe5\\`4_r'TNN3SUVlij_-J2s4rB,<Lu`@>=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;<VaSuBh[qQko>&liaf0,*kjC.J`;P^+$52d[=KXj8&'e*4P/,Z9[dDF-'YRZoZiF;%AH>tt,27mKF>&4?hEit4=L9Dp`?'0q7P5:<UW0I^!0q(IX,,ucOg4Vh%OkOI;Ea:$Sh/,I?>(/f`++Ht18<u_2kmN!fc+koCU2>$P7L5$?#1K<#]<FlO*h.Qr"X9lPlX,:P3=5+GfP]C"(cM4nR876B-"NG'@4nJ]iQ*K0n4I*_'^\Rq^_-DmGUCS+W4Md*7kuC&=*F()ZdH.1&SJ@lo=pF_2i4u+<\^a"NcN3mukS?<igHBSh$ImWRX^iC$C<&Q*B^p%cM/N=lF:Z.TBCfp"nd^c"D[SM;@prE!)!1^[!I$ua/rT+V96b8g^D%VlLVc1g2S9FWL0D6,-dUd?RV=SQBJ,lg17h5G[jfP#`V,B1g&p%3'&2pYB#dq)qW.5HOl'R?26/FuLDu.(K;:3gCbh;7%D+7ie'<pOMGq,%f8F?7DkF\<.*8Y,]Cf.\K%=,I])N"_[D`2/b[FSPK;!h+$K%UQ5\V_0j,6qR<-8gOGB:`a<1]M<*oM%u]V&4qkjEWliO2H2`o,:?eub3D9M8R2C;jF0!-:B[[TjAUg<nW^?e=crMST=9af@B>`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<hes_+D3p@RqM+IKT>_cchmU/kQ1(qF$^eD0AK9Tcp3rm8n>1V1Nlq;e[a&"Te>f!\(u-EWQ!^X[pj@6=O!.>ldGc+ip!2K7s91GpDtS2'<gpTl9:Fk".in[5ZU,AbV.fU$Q)n`06)<OIe7Dko>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<jS9MNc_EI]T7(b1QW]^RHDhG^%0d$WTET8=is/7_$b^naIA3:e8L8,tI6HSh>,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&BIP<V?.eo_.+8+[1;V4\o!$/F$YSLQmRbIe#rnFR'o*)9Xpp?QH61bkJ]8Lcqoq_/>a+.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(#<hdhUgkZY.;=E=m1;h`\$#&U,3HE=CnV'a;q;F@*k)"&Jf1!Cgsnl?(!76nTW<)`BO9f-TM`#dsd()@0(.Cptc6kW3^uta_^o;Rk3],r6Gn.nUoL>62[Jb'p&Q_7QVo79A_s[m'Yo6Z')oqB-8Wm%OR#],.HY?9R4"H7i-cp\fMV<'JPG"k/nmp-2<JIr^kD_&=u9,rCttSc!LE:PU3BlP,Ie?;!+VtD7b)A95&ud)Y8sJUMpCpjtZNGQ=FpQ6\!c@mHLDEER-X@`,T(m5O7XC*u%D,HkLT6`u,?$&#_L(">s837/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<LeYY8O*`@/qlEP\L5;iB'o2E16B*_Y0/KheHK5Z<!:9-"I$;N[QQop&CH5;E=dTAZ3H";<QV+NY'GV%[JoOdWE]+fmC'cV19.a$59WB$?2;L#-YT0Q].SV"RQ=GJXN<ZsU"O2)5O_r0E'?YA+;&tHQ%<DM>(e,,\reN,eDbf:Q'b]!>8MnE3LAKg*-JWT`P@ZT@790kF9+j+"2mZVO1\]V)h#d?H>[@ij'n_Xskqs1`m^4Ok0O/IN.bo[J<JY6b9i3Ebdk\=^51iBn3uWE8f`,6s[5;(KDqV:*lQ^`ul*!l_d)\nPUWhFM<F.F-gTBHp:&3J(]8hbNEn;P8L`k@sPu6uc-,7m>oZJdeJ<jibTsWt?q8Y0X`F%li#SS3_2dU`^hV[F2>CmU?M'qQd(Mr=3pDdTA@e$i<KJ%1V[sOg:AiVU]XUERp7b1ofWW"p$-Wq'5eM-+DN6P;f1Uk!8c>)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(<f->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+Eo<MPWP;q-o7#k0#*XoQ>L9GQHSBVI/160Uh;]bk#qkn[CNLmUEnL"\No>2b?H@piC#hZ<96Gc`4$*)X+F3m"0%VM#<f47hoM,dSpV/U#J^ah6HIXO6Hb:<SA`K/,F^ItkB_J2jr7]TJU*d*-ON4:0k\au0-B2RL*0tT]7J3@-M[,^V\C00[UgAG=mC6)$XS<5U<$NKTUG!aqJOK\b"Xt4rm9):UIq5Z\Lu;.)Kc5=fD#@@jIZ4gdPG@X]I?Fj;)uL>VFS'k*@(L7m`)i@ff?mo':Ot%,SKJ$I(HR:Z,R=<)go,?,Pl)Dotd4</r9a<\\.=f*2Yf2PItZaAn)@cHM3<ud6g'lYD\YoGZ!J0%cZ\A.8EsR)75B=A9X,E&t&<s?GH'f%f/NP\0b:Y]?Vf5RiQ6,@r0rMEn@ba%RB&Nkl4#W`mN6W.%5Mj,Z?m*SKRd`.m`<up-_LN.2@O]S6`qTcSt0M+PXM<36cIFS7>HIX!C9A!Vn2*Z"sTp&,`kVDq6?o-`@B?`0nXg@M1"uRO;&;+CqGm@aViC3OQ;q+[1hsQ??IT[Up^n37Q`R@-:5USki8n@fb$^7plef94=<N5H[1VhO\Ju7^\I._:qO@V`p`R(CrdF=4\T1)$@KFoWG17J^W+P\WSNag,sj46;EBu*1Y0jUs&PeOH8T!<Mpi6,?Cs]/o)LcqF#qVVcpH`2WsI$/kJ]HnjX"Mcq\L"3fXp+*UT"SLSL15Gpq3b#]JS:5:XAUE2XaZ:/'3tTmXuOn29k8+@!]W<Durc#dV(<0sVaALpb.PlV&#;mt,222M8/-Y@QBMJm+Aa4B<gX/V7-%`^NakkB2Ms__M\!nPXq7N2?$--8$__GklAk7b"pY8WlRbHNf,Q.h*Sn0:T,3MF<%MR_:C6B4?k:5?Ib`+'U,mVDb_24b(!T%fQha8Cie~>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/Z<E;`\sObKs]a#`H=V*1.i%bR<:BL#l[Gr(1l+"+P@:L+16515:Wt'GsKKWG5oP,].;;',.1`=In`7&B2Q=`[XSCPuL10@&RbS.;XH"P2[jWU.nEXOWng,<.?8KiS6?&pheKE8C]1`,/'pFHH);:7b+AnN<i3&Xj/qR[b$bqW_h5FacML'B`H@E)H6tYh\Kg&^D>Ad^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^<Xd4&s1@6mGm8,iOQ"CjmK:J/uB[lO%_JDR`?e&:d#Uq4A]iN+++)i&mO=/_c_Ij*Qa*?'_S`oFb;[uYIno8-.Qh:,>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?#Z<F35R2nlq[Rj]"sZ,7UK_U&hMO%,rt(eKlIMCJNd,4huNPuLm.DO)_Y<%`rAN'!D:/BJ7Z4FTPaVaNuTn6ibT#h6b:jTr>G"IAeL)?'(*`*8:afUm7C6PiU*6$]6T%2gH[&d#><!""I@HD605$"fLBp1Pek$LMJp*`/tTqN'poa>/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+<!Q'+8Of;]h[Z9IW<M2@2LXjKZ;75'h/SFYOiT8n'H@/K!W2pLN)lV+n7@T&rLUrJCJ+Men/S,H+adoMY93#SBT]U&1!hXDb^#<MB?!bMgQ9"A/-q1^j:d7g=mjp_e@N5;+;dr5IjW!Mg2L$HsdB35nKV/EN3jfFQ9<YRE-:!72Q:@IWiF1#-@3o*S)UKLk`K+qVuG>ZZq,XP<dm:+.\da<^(^u.'hG1/`\G%//]ZFjZ^/eYS)gg.U=uJEQg8Y%i&#6;odPGX5!U`2Z%HS*a9U+eAZoSq?$@;;n]-7cj"\"RatR1f5Y9QY%G)HJpGe]AtPFA+(m=J]YH)7faN'51LSFIKiR4p/25C1!6K<8*6L*U^_nnUnt,56+JI2Qr>D@RO/44Se*G[laIERBVBUdbT[Ic`"f%YmJ&%!LPG\oAh&+a=Y4.<N:O>;ui<$Bb'V;+')R$B*Yt12mg\[@pF$p/pA0)bG2S/WRW65=7`%^7P<?mXB'oOoKeu`hS:HVoB.M<@3?u\`ZM.)iS(r<+l^aKq_N4c*B$!^m=T<^4OWW&`kKe$`9`HEA7a<#Bl1td\o7uV'kl$.&*8fNhfWN6rEFW+!'ZO'V:Vj6Q]C:-7q^`8g8-OPP5%p`?!!`Bko=bsjA!NbO?m%"]/P%oMp7fR[;Z[.tO@3OJPh>pmHY)q)Wp5@;WiG*Q7pf;tcLH`c*pQu%c]*[E<)2p%d3+HAs"tiZ1Qc6a,&Om<O$@c*%V*jcH6`&?^]lRIOKmWGo_gq-b7:EU5SYuUoEgZM6TQ\9DG?s1VedWcTKi>!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<Jo:rFsT@AgZZ-9o-=t6CoCVMO7>=pErZT6$Xl)-1T"c:&WpB3Z3<pZAT^ZK7MN+p:@e8f,migqBs;M\9(k\IEL'sTDg%;NFLgL1DaK>a1L$4k`Pjbe_sh+Df2<Ccs\/Fj+#IFV&XJ>q6m2X[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@cQ14IZYBe<dqVIN1h-F@&-3.gM'kGY!n?oYuiOr(g+4$HLrmKh/9SGCs;+[Ug[E\*:>T0a/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]<eC.c5FaM]VSd(MKCFaN%tq"o!<2\bIEs5P+*e-I5DZ2c2\gs2O"Kma\dM\eF.))C0N@XaL"P*U+I"2_?^=t42)_70_HuW,[HlN*S.^5/IKUR*a.:f7d0Db.S&ishpeM?NS.#amrWUA5L!/&B-N0<[>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*C</l$e]A>i$X#*Q!b-9>'^6j]=^F<V/fg$7n%rjdGK)`578!uFGFiPN6^QfJi"R'K059(>pUY_AF/%)_?L8jP=Z@g)kV%)@D,h]jZ<H8@dY/*shH--gioq1^4\0u./TY6;!FVBfC:J8Ymn]#O3Vn`?\-GhQ(*IJP"5I_<?_O.U@)5)p,UVTAp3d1tu'eS^!sTf.o:8oZr*"B,-;;<rp4GO?)VL?!/lE@nJK^uXWqoIq0>h5?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<lgLVi>^hi!8!=Pj!^/q/Nmc1Ab=<!M80++U93!;r/oJ@Vil^f*.a(g=p![Y*S5=@n_$N2g^(Gh<LUXg(,M,dVF13qu'JZ7M@KS7nLel&rWKE0+'bb`l6[lDG[pAj8!@6ImPsEZLif&,Upm=TqBMcI!7p9B)W;P(7$MK.(bKde*LW6('1q@h-Z[r-2#ZS4RiC'B^E8HshGmQ>b,<-Moi:qh4p=SKXNAbLoqTB9Xt&.&NA'=`f37ifTTN4HBe;WNG'NJ>1/]OR3+d*/m0KiLM5<FQPW^.,KU[0afl+]o.M&I!>F)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%$K6Q<i!<P"QJ>ELuL;'\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(Rl<Z@Nq%UKmhDn$NAAccqbm,UPG8F/e%-rf_s(2%uLEqWP<h@m;NiLZ%>G\B=mhbW\DKn/FKIO,<lrt;>H3u,/^3Q`Y(5e8/$n+pgN#dK%DrnuPmD_dq<Zl.9>p&DV-moO_g>K)<s+<ECR:/L[nH4CV<J@tk8SK7%4dl:GT,f7S&CJdR]6iQ+HQ*M2#YodQ;hpF%lCaQ51'aNa?Z03m[6n7Qd)r1!*(aZo_(I[fh%^-t$&QjE"mBPba[8+oNe6FX6Yc3"*fKQ'r,HrfJ/;GY`Ij1e*($R<7Alb$US:':2Cj/RFUPG(d22i,J^$J;`b[C35NBf&a1b98cZ`E?h$;;<Dgo]ZdBtL%I;`nHQ>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+<dXeq1-m*<e'cQW?-XMs%JJ5j_5cXPWQmph&-#Y-*Vf&'Ymgfa+rX0lNN4\,*og8*601dgE/<ka^Fom#$)0ro!33*@hnHl=>T5X2"@#NCqf&#EDiMRe,&GXXD9+\u6<fTR%=L4(.5$$OTrb0]@opkt+_<issHA@\bDR$B&BM9Y_8.NjOi.jOPJ%mW6])1\W4!2A,g!'K?F&?/\7i9@GI?'`*?/md]##HO3f?m&U@W?/.uk^D+*G(OtY7\U.i7BjeJZcam&Lc=CQS#EKtk;Wmr+TnHile"1SB-@_u&\F_l&M5_KPma&c;$>Q)X3<up:PnBtY`'Y-*f7C);;F&=+i^(b)<a^a+C0Xo'_4oof`Q-tH]0Dl"KWfV@(#Znd&l)^TbM;SVD:OF-sjJ^<[^EI7$3SU\uX1&.gUpY=<2Qr#N5m+!K($4.]sSAIMdSYs$;c_'Q:$#kIII!<]AouCT<$T>Z8&1aeks)-#]f$G%ZN/hDD,<ZE,tOB*Y_O.7ZX(oKbEHV(p`lJ37MhIs4if`b`+7V=/#gED3>sDe)P$H*("+ZZFY0c?=[sm",sFl@\jR"*K3jkC\:b9pJE*4DMK8ep^i/TNdR3&+j?S?NkmA>N!j%<Dtm=*[r(nf:0X!<Gj<4N;V0p?@"osbWTnkBZ-nTBFJm$hQUUco<\`\rFQ3kr8m@ml$F3*k$sZm+q,]10#,+m!F9Xu<1FA(_S[g$a\H9SkLWaW'u(ok3"XYUi^XDV%Sj#u[?I/7CQF\2!F93V2JSA-ld(35Fe1[&d<k%-(+[u`Ac1^ab'n+X8!Qsq:/X9T&;GPE<%,V;Rut]6'sc[#2A;-t<TF&sI6^m6\[c2lGJiqs.geRC>#;<`j[W)"fclFi+'XEW*mGI3+FN*_FLY(BYXPfR@C&$=&/`%Y;ipe""cA-)T[`>J_7ioD20e#c<YZ?J$;<!Rb,K,G&;5t>ic'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&[<fjBbdPZdHC,kj*pV6L6B55o2d]ZV+a`A;4I]89GE5.1_]c'dFOC(<"XnSp>*/?\.;`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^<scq"GJTL/&JPEDq4c4BVB=d&;NU3M/noVq3"#pc&Z='5U\V=Z1e#\:@Aq3OAA[TdYg:-4@4kghG/?9l`IV7_jYrg8a29Z_c4@dT=3ULr`IZ\8E(_$4^1chNcW9kr\Z?fNm=Pm.gUZFak&tVW&8NXnLTIeTbBcY.+hd6[Z;shF81hHeX<1C^P0:HeX:8-GhcaSAR.Yd*6[F$?Ksc0*6V?#Z*pd]^;!tO%djOR!AWqFq&6@nEbmkVMOp_$qVKrEn>s*0b'h-GJ-1+.a%jf=tFcZn`)_W8TED>%@8PFK]q*R>pYZIsB7W^Eo;4AqH7PWlSQEH'"RsR&k0(Bs#K-(%dmomZnj65t5L2<F?oW;HQ7n5qrl?>/99]Lq`>4Z=\*Os?qDnDI3h2TC_*htT<ZGE+*e2TMjaGb)I=VV0kZdVQ3\drD["osER"gM89*!*8rI&>J)B6*^+aK8_q?N/pqG0p.<IAg5$p.sFiGQVA`X&J&k:q`>C#Dp,^0r>?,EceMo3u(U*Hq`NZn=p)@m7aXeYG:FB.m%g@7!hucUbqR%ZSCj[XsuTRI\!]WXgFp,o7j<t4".U2T-HC549X>0cI$FOlG#1bLbH#;AT_P0pY'IU3Ua.4W0fGdfu7b;q%3l3i;*fecU7!#0\rD"<1L8CQn7$fX/mqoc<k1ML:11W*lkbSopNSc!24^%6smtSC_c'sQp`)GM>OS+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#fb<F<[qlaNL[r]l]7f">I%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*(?Hup<N9^4`!d4e;.2bt46*adJ?N-%8G?`d*h2k=ROEE[dMU_)FIBgcA'=<3PTk*@&/fFCiYnC8B@3AnC>5OkjpD$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@cN<JpO2Tt;KT:+p4P)EjK;$QVVOA9R/PT.P(et__rZcc*k;23,f`p9LlG=)NI$'S(Y9>q*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<DskaB#[7U>&9[=&N.IeiSV@%Sd!AiX%OO0oA)(H`\#s,`;N>B'=RE<fc'3"7p<?VB*1*kR=RI@'1Rl3(UTn]c]Mg(gM-4N@8]M!9;N?,b4r+HsSZQh][U`EG#9DZ2q93aiuC_2[YZ%:q%Qr:]U$</Vl=n+Pt<I&?Y=.i);D((g=5S6Kn=j(L!$IA4+:Srf@WAJq$^#,Qfjq>4?I1E^$0"r_YH+h6&<N"4b8fdjIu1k,,,.<D3N.r6O2QhduC>SlC<5iU5N)T#+W%dd,@0=EBGf2XqBeVI<fl$;9O14SkpM1-,;Q.-l]1Jrg>2gh7#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%<P6K-X8MLHRX]66e,G.f@WjnLjWgO`]kCPn3o`"\GMMd-6njm"8I7>@`::$A7R7I&RYJ6@<PU3Y>K&_2u_(qjaAq'$OW2r^8T-8:,2S/;.W#[@H+8?$LH6j_d74USaRf)H:\Kg:"jL;<79dWT2Wfm(6-E3ln2P:O?VtD/^YBRB%4<i2D`He9,,]t])f:i]mqXK8M.(\DoV=dEd>b;GU1XEIU6thJPCsIA54!a`0/l0L.Ug2!"EEG.B$X0"UWL7L2DH`X#Xm8N/FJ%W/*RR#b'oZuTf*tu,FQ%"6Ku19G,o_dr^mERBNc.Z^^UA<u[l9@#IH!CCjrsuPTQ3\Qo%8NTH3Q^Dkd38S)&KKqpP!1iMH,`%oAsP*RkLsYH4RY9(eG6bQM3K=0tFj7ogK"Ck[:jN^+<$l:#t"JQAj>rm;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<GF&5O;C#BC5/ujs-.188U5u\]F.`,T)G6bR5=I6Q'>'kM4(@/s?5dT-OfDCHV[72<N%k?Mdn-fbKG'*H`<pIj,\B(fNoOAbICg&,&qGnn"^eRCk5]ou+MPj<CddDdR8b>_i6kkZI[4*GfZQ@$q&<`r&_kJ<j7!gOs!/tK%1>;mOZ#a&FN'COK[R7<5XVK@9k]rZ9N&3K^S>7>J`dXfEBbcR#;FktDdSdj._a00lk4F\g9dB8KHbL0gm&iKZ7=Nc\aC^hffZYP.2Ve%-<AP]!i(N2FRRBYQ@ra;fg*rRWU/iRb)pQX[dQfJomF_H4mUc&R>VEf29\Mfh%?&<LrT3>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\ERgH1<Y>sndMp]$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<i4O@qn"EMd:=i#Z.ZDS8T^rrQ[quODSa7S)q-ECKlX\\$(OXa.C]Z+!t'+6j5_a%_/PSF^0%gLt5[7rBQV@kXMqYT!bTA5R6s%qm[&SPZ-DJ@\7hlc`82[r[/l\%bqA95u$B?q\`\^$)2)A@\;*7*]*M[)Rh)5/E)gPd.YjRb2+/Vs%jd.n^.^;_#9cd5P'E,:`0$,,0nFE[$DDRW,p\,WcD<nC<A-O()JE65f/E8Ll\2Tn2BaY-6j4f?%H^_p"#$88l#e5/AH;e;sI3B*%%F2&81=\/D]NDG,T;lE=XiATt6#)XKnTN3e@XN^9WAH8;]I,chnJMc6p!Dr7G0u?D8sBe[uE$[t,8M;O_+(b);-Gm.QUseC>?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-Z<IT08CA$0&O%V1]B*+fY"]BS8M3bUm$:if1Sq8EXQAC%Mbl7)E9=iVq2PB:qC6-7Q=k!E<W\VldZYcp0SS5g?(#)DMN*@nQno(M[6HN0Um7gB.i^pCU[m1Y.IDM3,VrUj7`MlV50PdN-.*h%h-Q8<AfWRE:Q3o57\;~>endstream
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
[<f1800686020f8e59d4708315a4e73c6f><f1800686020f8e59d4708315a4e73c6f>]
% ReportLab generated PDF document -- digest (opensource)
/Info 37 0 R
/Root 36 0 R
/Size 67
>>
startxref
29629
%%EOF

View File

@@ -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
<!-- toc -->
- [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)
<!-- /toc -->
**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.12.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.35.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 (G2G6) | 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.12.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.12.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 |

View File

@@ -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>5i<F3X^7e#)1i:c*/%$'?KN4JY3f6cm!U;npQ5sX!+-GneB2Nd%="X!AW$PZH7!=*i8RLtgM3Wl1BOEJ`k2,[!EI5$rIh$'$roDQbCc"9IaRj4lh8(8#f9\iuFNC[9,Daf2OXXnH-tANZe%(ZU9t)c6(:'?#9]ACUCj+DW:^73Kl&,SGrR%LE^oYdIGSWjj2GWL%K\ChdZ$/T(T#5gJXTLWCIA)"D4r.:WjT23>W3n0;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^%^u6B1<fJ]s*Sa;rqam$<SglJiJD)bm<V!!Qu"<OXQp%nm(Dlf5OL/6inP?,,G\-^ih^7"6!=bb5~>endstream
endobj
48 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3569
>>
stream
Gatm>>Ar7U(4P_U+M&E*R3o:/h4'0gg8%FJ8aArQlkV9_ck:22W`sU;l<WcUp-#3&=`SWW[R$BTLHHj.rB$7&,l!M@^UB3M]/c=/OHE5`I#VYOX(Zs;DW\-O]TW"6<=S8k-E5'?9roe0$M/e,2+b5u)'/a;2m7b0>V;dZ%F'<'";lqP,lZd59ti^p/A@dHQn\iB2#30KfO=;1E`g6]LBY-0P&j"n#J#<E0qSUu:#.Kg;9U5C*j^[A$$04#ggD/B54qa6q(!5m_$45>ci\G>%]SL;#c4^&SYZWV6hA(>r-*V05N/O1S/O:2e_oV,)*kE?892`N#tp/Q[LZ3ZEkmaI]O[FgM\%M9km&E(f7GK'Csh<LR&6R<4?$Y]0qfPJ$ApX#-&])17oMYnjgkWEq?BKa0H$99KtUt=AL8,NA4"AeMmi7K4)\>IU%:(`)U_<eZ3`(M7*u^%'hal%'4F6QQ7D5Li6tX5[VoRu'-1B"$A';"Ig;SCV\l2mPqaDmHjgc!<HS/NQmJg3S0dt6cA'k+WQ&$r:>i0d]c./V>tYQD-[S?/03hc`;4=&aO^#hsK-eb$!K=3^k<pIqQh6IY?eO5+N`]A\Etdq#$IQkrGTGM[J>: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(:<XuYBS5lfnQ]Uch9[CBQcI)qoI;28^^nN1e@0XO+R=n,Y^]EW<cB`(?FG1&=o6(nsDDqn]BuUL%7$MYk5l2+2nY!DeZH?E"/CD7OKe]"A'o^dK*u$D)C^o&4.pIi".2t6J"hPq$#1<qU+p/BGJ-q%;q?)%66*7UZj`4F+8[f^"b@r>/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*a2<XA1o&>4WV&E'N15cIj/^f*uN`6'Ze+0']:%eXHRbg6>4&`%+j:mtg$<DhH^PboA\J<9hG(;"#AVE5JUZ1uF(jLeMYU+02TYs(Dsotpu,DAprY)U)O%H6WFRiN&)I^.KWilJYD.!PkdX81tmd7b=JXQc6*'Q=U/fT^`]jI_$S^PfL*F4V.StT%b="(%)ElV4PIdYL6JrL<"d.)+g6klf4CdS2)I8WaZP.>%3Vu4J;6dc1XbO;KskT&eYS+XI^OTCJu8>rMgaMgukoBkVbm<]Nlk\W!@=ED,SA<qaEZ>XUoWi: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<B"ksdTUB3N'K#B?lf6m!W<nSJ5@=hjK'eh>\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<<XtLceA+/F@!AH-lLtFsn@9k8j-1(0j0V);P-1`uY";AA*t!<EoMu\/bBh4ZUIq2ReXMbNIe-WGpi3Jll`'#Y]k00@B]XHG`<G>^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!n<Q,[*&iNrYX)4K\_npPR,=6/D[4hMibX.a.&t^mMLI:-%&rtf"TK$ZaL;A#Hu^5C$FM!85u]i83=7![Gm8H*K6,`(gRpa!U,BE$P]#M8"$*5_#0>F,*B$W:lZFo_IP@;^#.MK0!W8ISr^%O^H7_<Y.*g_!qP"KjFL?/aSj&[ls':/(b%CaTg,/&\?F#X,;gZ_hm3R[fA''h6'A2/ob1]Jpahk>5)^k-gAffSmbaU".2k@>'75MG^7r,%;dicZ<dKm]nAuAeeKq\r`WP.0n]XU`emm1i0n*_>?Qi'$WRCaXAQQ+J[S[\p0_]Ydn^onoBmT"Y#;Z'V%^C@0uZR$eTGMs6IFG#^6]VA%rLjTcoqV=Aa7R&Z[rn]<u`XBHI>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<h_u,=OOh[[.l>:*7`,32,Ah8?kl!;+MtDY>#7VD![DfQ5D2SH/q0;/9RPBYVTbG/NbI14<btFhrHc141[hYB"<`t?/nH6+KJ\E`@=3BL?4[6N:ot=+h)`U$/U&EhZ"4t4E"$/Mr:q;8bqPtPp^.M+/3sl`/&'sAZ^*$5>1VCo[0;K9+CIDe<\KaP3bm*)Cgb0/3XeS80\I!#X'f;1_INTnPp6,f9C;*h.oO=rnh/cq1ZEl7Ce+LX5<hEsFWQ=(:u(R0pA-A(cYMT(ln)KJ8?\(*n9&S]*.!PSih\JiO(cC&QTgXHHjFD'H,qr;Tb(UA38'Il9,<6TG3LA.7B2@+.s-g`L&u:oB8+sOl\c2Xnru>k[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;SA<N;_YR>h\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><g!UTe)Pb*LC6Q.?TH>lgf"/DJb.\Gna0*P="m:ZofJ&XVg%qrup)I"'gEK6LCU`>,R(gIhi9\=sf;rS48Y$5O]unc%'`"&G'6gNC)+W!=ND:><=f-LXRAV&[`g<q;UM6c^uP^n7O(%q[!l6V&EiH1(;,5(ftoXAVob7I&d:=UmMer2K#QH?6JI@?Jn_BqN33R#TLc)3@rGeM.#l9h;6qEb4QqG]7M!#Ns`HEGt?G9$BdHuARc"/0d%]8&[V"IQnB_Fr=u'EKq^n)@Jp:c'YeitZscrN.2JgIq;:!NF\ioC8<mEoeYG5aGlgGD%6:Y:'aA\'\gb(mJ5HaAg^G<5NZZe\W!mc>2WW6t_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>Kiu<NBj3pa)b>hpIaDfofYEmB#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%<dO#/12:I3H,n<u@:ke4/h-;2(U6nB^PNdZ>="5_8L@duPjnQCcQ5iT36BT3Rc&%<D!P]_>UTq\=EVMnqq_BY^OQ!M`5[!M@dE$lam)HqG<AJXYr#d$-\(RT*Ec0GVjc_lig%dKX5:?.TNW7V5X>oQ@O4_6-/YG*5t"LA3=^k6?$&TA[PEke#&TL5FSk"*IasVO[4He;i#kDci*P^-=Yo8r)c_$?KdP%1*OI48#NU[J8RZf<BQi]31V-2d3k>%i3/\mef@(m;3\]PP=;6S;>$?p/E@0(>OsX-7Y<J"huT;O_V!^a&LMDd!K7`$`JdNO%dSWJ2Li4Zh\\u^0=on\[u3T:Hj1%.!^!&H)Zt$O48Jc:mA'![jj%)831PV>b&5H]td-s-6!F1o/UUE'e/;bW(''W$g0>,<mEWXNo5;Rf#mgXD,ZdEc8Y#]T5LhoOM0LKk9b]F5k]NIrVZ"(`.IDf:Xf4+;qf(P,]k^/ltk93q.@T>F>F:k$jb(=2DFS.[/TpA%ul0,<I;V-FT<a73>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.iI<!CVNAQWLFIs7`%=2CCa0N:.ZohR+#Vq*-<>X^1XMtWZstA2?U@P&nRE:@8J@T8Ya;C'B$HJ3qU=dOh#.Fq@<R-QIp`6=p(#qk+nPPJ4M?>(4#e+H6,9>)r:2&:n=dbfoj;`_B^W*W7kFQg?,;k3N!\q<H_<$h8D&!&r:30!0=W9Jr%^`PGN@OU2cth,gQqE/l*RG,W(S>:[+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<j1GNo]+bJ??\4;HZM_JJr2XdT1rG;s^`^+[*7Mtq/O5mfO-I4_n:**%=L^h/T9E(O@T\P0XrYP?$51N%5.C-8rAeV`Sj(-Lbgf>"\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-(EocgO8<qTm2gH2'$\AROUt*;aK(K6A;3#O@W?u^.,\7TD9`W,O<]4ObaBr0tM^Np8cSaco"Xb>l.?_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%<l5(t"illct;cVrJ7UZ:,h[Z&a_pt&CBX;c$Kc:\ZYMG4>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/]<l&K&lOaE>tD8E*Z:DDiJNR=M01Ia2oo`2eh15*D=q/DrP);P!ILX"aF4>NW9$-Gp801_<Z4htYS/Rl3't`VL3p?'_n6a,$!sl_?iQ%B_=C`1=+d@:8ZE1orWi9RpS.~>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<crq<\V9r%hsTHmQI$/4JnYQ/j]&$Q3a^9$[FYQD>)^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=<ruGU2YHeIX(R0W304VYkiuORf1g+?Z(%r\PYOd`Xj\H*/b*Z!`-8`Jd5Mjq9'drkDtSsAs4,@oB!qn-gcV`MX-oI2NkXg^;4@O";`1FELGh@h4TdW"A+(GEG)alS$imB:O'Ihi5ngpp[0Ad*KD?Q7-SSG*?>8=(#*RB5GZ<NZq*`QiR)odKi2E,efX3DZ1s>]'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]G<pUlnuVQ3S2Q<oqq2!gpb/m@q5NA0RJ"(I]p5jIT.GEj&JJe25#f.*akGsgIb8$DeiqrY1*U,.Kp)LC'&4:!X'])!ObT$[V>mAG8+/^qPFJ?Kt%jK48?p<I+jH5*C^H0RFA<h@(B7``1]q[6a;$Uc1k3jpgh*CF4Pt-/Q7K,)fX)(,/,+ck]LCNCn6qCK\Bq&H+eC#I>$GF`N6dsL$QUWQ>:_)R7CBH`2nV)__I]lHCJ?op2hVfO6Ntq$mSnKO%-sWG"Hee:iI<G6"V'=q=A3b?JKIPG_$Cd6iR;QHalDHoem%YnAhHaqkp010c[X?=L4U;+`')^8(i$ml`S\?U<<i[A@<'kb`%Xdq'1hO/hc)U:Op.aS:LU3l^O/aY`#@Qq>p"Lc=MO<!InF?%Y4l9T.!s#i&-;nik9Sr<%eY8S<KOG9I18O0$u<&;9cqMZ0BSciuJnE$KY]T:)H'sEuDt`At&d?^!-TL>M(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(+_oH9qhM<!p(7os$RihAS8U[8@OH6/)W.C?BI;[#kcN#Zu/S:"/Z]<$n#[#OOon[kmsP%Hc;<JLRKC$_5dDrX7@11b1g9+\.)[1hDrICRK-P,V6_,*cL,7](<]cQ`n\_V^=S&5q+OG,e_&%oA,2X+e?3VPnlBYKI+p=qa)@elb[ZB5.($.1r'mKn\L=XNEj#DE\C\<B?':njO*APhf6$Ygok&M.i1>RHQJk`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^<C:5GlNN3%<sX(EGt$MjW(O"(sPKi%X6s9_qH=FSG`q<<t.LrnFntIcNGRk_3=7!;*h_7=iZSV1O]RG3Z_o2\XnMrQ@^D03RKrB^\Rf:1A\nnK%$['N]de"<-i;P:c!a'YU70:%mR6t=7r9qX[S^?0':99o2b//L.2%6W_P"F'K"!Erl5Hl%UL=5:BLs#TD?X\cC<QVaBHWn>;<nS$#a$@/hK_oNE+AlIIONW[D3+i/9)oMjK`5?J3W:9Q?<INbqH!(-Hmd!55ehU8,h;5esB=W#RZYVhlSdAC,j#7XY<Pt499^6PZ+EhGZ)&^,3XcSS#]?Z8hlH;+tN[LjsQu4Pq!K>6kV+Y+cL("?$rU(doV"Fg$A]5iG;2(l-+nZ5:<oi3VJ.N3[#Tc$E:]3q/Rs7C\80ShGTGZOt)sfb:,f>M88YZ!l!4M2NEjUZKeVp\@b1*Ge(2$RW,aWb@dZ=8286SV3Q'NjnU%Ar$IYc9Id>q.U!Ou#D<b4CR<Wq>t],&i0YKEVThldl*3UOS7DkZ8i`/W\j,U\.P[oU:ou_);<le3#<Q340!l-Kb9(]oJ&T.FHN~>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+ZZDBES6egZ<ikp`#Uo5JENY_)>QIEpmB5Es;/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.)<A'E2'&b,2<$5cSiPaTK^QZWFAjX_6pZ3:2&OkZD3.o?gG_PdDC:ViX1AR8'U@E5hiQ5JFFiQ<VY9?P2BMY]EF$g_[cOd?8$=U>NX3\3h7RrB^H^/rFm^rZ8m2qi2Y"&XX'I*0\:?_dV:b;0Wik%/X[pG<qK^r)s%PL)_@/U]i`h-4!ka/91`1.+PCH;cE0A4=Vk>[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%_<!X4\#8*Z]DRn:/e@3WnslQ<16ZMua%dW@f@Bj8dMqGPpAFXWCoC.[g&,c:o(po#SMnoP@ofa*!6jGWqk-@Y1E8*XNXq&Vl[d5#^gqm"ef+H<-J'3sX0K1G]G\tB0.9iG"@J[4Rq^S.$q:f2!m?YA)X#`%!ueX)J2+`;M2+%bpSgT-<"T_PMq?WV$[^o$"sM9cP&Y#bQ3b"uII?;WGL4NIPaau:aU)pN$#l+Bi,9r-0ZaW?ZkH9S`FQSi:]!HCl$ZuY%hMJN,0PD.M")A:MDF#20>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<t96Ba]sB9*W;C-Yr5n]K-bqf>#.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.<Jh<kI,PQ++AqREB^LKDT&fKZZJ,arD!R?ES\,&LE-%fEnSnW3fW>pbXg<?^^<W/:\29@tM1s^"$b-K'1"7[lorB#OacN\8q%[pW1[%nTD<khQd*CK1rj<I&EZR8pDV/f_*5&eb),BHFj=7@!`2A\>oMf)E_+-,QV+pJI1\jE."./f/;.&EE0VnZ<k8LhX4Luh8rWXU.Y"H>5+bmGg`8>OlLX77klI&SfVgZ*j@;%/C09\kJCN3Vo/&t#sqWqnBH*l7%L(dqHWIP.UXcRD,Uj]C,7)FEU1Z)Nu75E:Om$S8r/%<ceJenCFo!d^^iPK]8A.%_lkVqf%.u7#&shmV_9X)8<#M$'S;Ek65pFBP-e;(=:L_.%9<lT]_`SuIdqgd<J6m?$Odd=>[ilX,k_;'r#`[eOHl\RF'/90<ToXkc-AK0.N5Hc?>FQCUUIq@@;S1IWGhIgh:nnm*"D=Rc98j",O%].3PpC`f;XB;\LY;=B@VA)*(HM(V0(p'mBF+#4-$eWCt=G'<G-/q&%/-YL1g&_>nG*;G9u@#:.OdF\Z5V%kr1D@H<Y1Tc/$Sqm'fj4NZ0\u0>YCRgJ1OulEelFUR=$gOjND"`P*-,G,U1#HuKIbC;ncZ\Tn*E)F$7#DlW+D6Xq<Xh7F\G]nUP$k*oI(_L.@^ii!WNh<U?4fUS@%QMb+;gN7:toTgsHFqug/\'XsfdC\RJ~>endstream
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

View File

@@ -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
<!-- toc -->
- [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)
<!-- /toc -->
---
## 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

View File

@@ -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)<g^tr6@kt<7D%Gb>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<sXbmI_S;33*[E;4?/C'hg$a;*<2UgahVZ"me:r9)2=-1'[nccc,HPs,=a)QrC0G9oN"3P)hi;A#Kmu_!j!Sa&p5,-dBpTWf\pD<^=WH/6BF>]d6ptc#Y6GfM%!P5q[A&f&G4jN9=jo"31INkBrcHIFhAZa,/[/e-upj[_\i30*L(MhOhN6L$);QhUiZ,r6bL'WF=hG*dc!$^Q2!Sl=LWRhL>`YPjt+WYtXg)$Rn<D2oB9F3=WW^=-*%r#,)5IEqTXXjsZA'nJjJN4L$44W^[/4&&o%*Hd4SUiDn>SWY!ZI\6).[O]S%R969j+he.RHr`lq<K@H]bU`WfG-]H>aplmfQ%+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_^<!0d`\BQY'\-]/d,@PD'aKY-1Ai:oY5Su=gBUEKlVc,*,mVF7j^*`)%L;NR3;SG5:$9\%09e]iVG,,U;;3\`)$l0It50,Mr.j&rCk+.XGNnp?r&,,WD<tN%u1lu,u]^eo:)pj+AG`cUFR\86X3"a4't>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@fj<YZgrRAjl"@11@A->89SX/()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#@<Gd/;D1^g:LKXGYL$:ID@!M4$#8W3]'jQ!WG9#ATHcUTgGa,:U&X=%XW+]XW9ErqPQ!"JlCn3[3!.oNfCcDL4_B*-Am<Qn@HC-Z-["^bkn!)elTSS&dW*p[8*b^iUR-nkkH!3R%6(cfa/_&OmG1sViW])4:$G?1K#C+!19,^qj(N]/`X=ohCrJAK'3==Nesn(V'G!tPcs?Bd6I_.P-H,q$lardDG>3tiaf]d`WPbn3d4KOF626RskXU"/7$kbTq]WsLYoT0eaP_8,WV0&>.D?<enZ%Pe_](L?f!cL@)$Y2AMAqM(ODYPEeCOF:s*f?h9JkKX&VW-W+21+8$I>P=%M-8eT)s8]#LrV=lI!rC@:!e?3uN/'[/kY)#LH,q#4ST/+:,Gt3tMOqLPhb%9oIgA_pq+3X;aX&=YX/Em"^)WMQ'-6Yq<R,>+/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]B<td(hQU#*[Qsmh5&FI=op1]t=OX?*4JWq?n%d83"KZhf,6!!W8=jC+[PGt$M,82dX52s_YjH2B8F*:a#?#c`699p,F[N@721SnfXptM7N*pLD#*:?4NBt]cEX#qd@Yh0uSjCuX#3kF%1['_[0Vi-jGKM&N-Y,7R\@`OdZl?B>QD3*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(<h'Tp`f]f\JMMWE#cHWF_*s'uX"]d[TG(K9AnHG9i>*Y)rcW/TQS2VKs_R2An#A%W7&UO<T&t7(iH=h!FLIEf;A?Q`36UrX*]@T%A)]WZL;)V;u/5,,#G(o9;#%oKar%Q4>@?\#@`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-<kjbo"2F.K`N"mC.`DX/=R*jbD48s!O67Yp<6TG`Tp#X8;L6_I"N$*09b[5O:kGPr+,Lr1o%0[nIakW]WaVdjMU*5qc$V=s=YfLP\=#!h+gM'uc*Ziip5YiVSdI-l)tq5>"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&+<H^O_WnO*Q^45Ef>iG1KsV6ek7XUioQM8c>M"\O#a7t*LQGJ'g9!L(e\j0d@mETMENi/iM!+\!Cj09$"Pfn-B)bir9OfU_XhqnmaTWD3.3E*f<#8*>h_.OUODOg<mi"22uD/(W-K;]:=l2+e;\;Ur)e-]O)49GFL;$r0jqQ8=:28#TEn6+rfYW6&6#bLpiWMK7k@R^;f17%Ff(j7q[):EEI2$k#'i/;i0C067D?UVH%u-,m)CEt\H:UJmU3dAbLkR+gRtU+nKKhr]Mr8sIm!@XEN_dZ$j="X.2O#=eQ,qWJL:mbN-\AB@q$'^@SXN%SrdYD@;NtDnp'MJi/8_Gf,=hYO9Dr&^JLdtVCGWsjP"tJ3Qb3&d%6@.U`8\]aq<J'N+Al$pk:_c'NT&*]40USo)pMbdsVmb8K=:-9p8@L`e-7-)d_QUk=q*RQ`4*JZ;gY?h7oK7]72l&o7srIHft+#qj@S+Pa=\u^=-aPqNt>,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"<A=br87*n(&g8V3hnfPmDOb`33@XCa/4jl/K"5s"Z,nC'ds`%r!D-/GAR~>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+%<Pk\`dm:"pruV\V7pNB]N=c"^tcP`&C>.<.$nO"e^mR,F\Q&06dig&\4\B/oPrm)8uF=Q?`+5eDB;6+XJu.eBof!<*W/_og@Q5$MZY1L_<s#CN,"2+XE?S1Q!kIm3t!0,BCp,FC4*Ihs;]q'msA7!UO(-#T.R\%'6"E^X0pQkfl(IJ%GF/@_lL1E^P-943Vr*Iu[h\*1cMo6mCg7ZAR>`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<dq9,NOQJOYr)W.6F1T'80pJuB)U!F`^bPh^PX%s64jQ!.VljAD/[=[hm87moUsAR+R63I<FYNFR7h*-<E*.>%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"<j+O26gLI-Ydb]0GT@\8H9+3&-sLjfOG%[dt9"0$ei'(07P>Q8173-<k^XN#ABCHQ*mrB/A#"N;i^lDQmS],"rt4mFHIXlZEt/NUD6_9c#1d6BT$Bn6~>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

View File

@@ -1,6 +1,6 @@
# Android Cast — alpha release & QA # 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) **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) - [E. Known limitations (not alpha blockers)](#e-known-limitations-not-alpha-blockers)
- [Log capture (when something fails)](#log-capture-when-something-fails) - [Log capture (when something fails)](#log-capture-when-something-fails)
- [Alpha backend (optional for LAN-only alpha)](#alpha-backend-optional-for-lan-only-alpha) - [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) - [Ending feature freeze](#ending-feature-freeze)
<!-- /toc --> <!-- /toc -->
@@ -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 | | 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 | | Camera cast (API 29+, best-effort) | Live sender mirror while casting |
| Receiver playback + notification stop | Play Store / AdMob production | | 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. 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 WiFi subnet. Record build: About / `ver
- Some apps silent under playback capture policy. - Some apps silent under playback capture policy.
- Heavy rotation on some OEMs may still stress system WFD/WM (watchdog) — document device + lock state. - Heavy rotation on some OEMs may still stress system WFD/WM (watchdog) — document device + lock state.
**Alpha approved when:** all **A**, **B1B4**, and **C1** pass on your reference sender + receiver pair. **LAN alpha approved when:** all **A**, **B1B4**, and **C1** pass on your reference sender + receiver pair. **Full alpha** additionally requires backend deploy and **F1F5** (RSSH).
--- ---
@@ -162,13 +165,38 @@ adb -s RECEIVER logcat -s ReceiverCastService:* AndroidCast:*
## Alpha backend (optional for LAN-only alpha) ## 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 | | Item | Notes |
|------|--------| |------|--------|
| Host `examples/ota/v0/` | See [OTA.md](OTA.md) | | Host `examples/ota/v0/` | See [OTA.md](OTA.md) |
| Deploy crash reporter | See [CRASH_REPORTER.md](CRASH_REPORTER.md) | | Deploy crash reporter | See [CRASH_REPORTER.md](CRASH_REPORTER.md) |
| Configure URLs | Developer settings or `local.properties` | | 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**, **B1B4**, **C1**) **and** backend items above **and** **F1F5** **and** **G1G6** 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) | ☐ |
--- ---

View File

@@ -2,7 +2,7 @@
%“Œ‹ž ReportLab Generated PDF document (opensource) %“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj 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 endobj
2 0 obj 2 0 obj
@@ -17,73 +17,83 @@ endobj
endobj endobj
4 0 obj 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 endobj
5 0 obj 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 endobj
6 0 obj 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 endobj
7 0 obj 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 endobj
8 0 obj 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 endobj
9 0 obj 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 endobj
10 0 obj 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 endobj
11 0 obj 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 endobj
12 0 obj 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 endobj
13 0 obj 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 endobj
14 0 obj 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 endobj
15 0 obj 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 endobj
16 0 obj 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 endobj
17 0 obj 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 /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 ] /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 >> /Rotate 0
/Trans << /Trans <<
@@ -91,44 +101,24 @@ endobj
>> /Type /Page >> /Type /Page
>> >>
endobj endobj
18 0 obj 20 0 obj
<< <<
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font /BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
>> >>
endobj endobj
19 0 obj 21 0 obj
<< <<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font /BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
>> >>
endobj endobj
20 0 obj 22 0 obj
<< <<
/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font /BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font
>> >>
endobj 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 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 ] /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans << >> /Rotate 0 /Trans <<
@@ -138,116 +128,147 @@ endobj
endobj endobj
24 0 obj 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 endobj
25 0 obj 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\)) /Contents 47 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 43 0 R /Resources <<
/Subject (\(unspecified\)) /Title (Android Cast \204 alpha release & QA) /Trapped /False /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>> >>
endobj endobj
26 0 obj 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 endobj
27 0 obj 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 endobj
28 0 obj 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 endobj
29 0 obj 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 endobj
30 0 obj 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 /Dest [ 23 0 R /Fit ] /Next 31 0 R /Parent 28 0 R /Prev 29 0 R /Title (Recommended defaults \(both devices\))
/Prev 29 0 R /Title (Alpha app sign-off \(manual QA\))
>> >>
endobj endobj
31 0 obj 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 endobj
32 0 obj 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 endobj
33 0 obj 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 endobj
34 0 obj 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 endobj
35 0 obj 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 endobj
36 0 obj 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 endobj
37 0 obj 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 endobj
38 0 obj 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 endobj
39 0 obj 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 endobj
40 0 obj 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=<OI&4B&jsBdlhoi&K^NKm$(5Bs&4;n`,`$';Hd</5([5A1'_98G&X+b<,'TXq&;GY1o0ipAnS==2$iM3)B%%mIJ>>arM^dis-?E=',mq.m`km-@VSMJ?bs5cubTM:aS,"Mo^ENUaB?+F#lA`D^_js\g*,nI^d+OZ#bC<e<mj'*6hi$)3m3N):K-a2,=uA!@rq\f+:%8AX:@oeX@@$W7?_7nW_ZYbhVE2bB1t^30qi'oAT&a]f,H)6bOqslO!ruW459<%8&h!"aTmcQYT.Il[Mf:o6Y*jtqJm"$*rcA-2&<-j6Md/+KaoVr!Y[h$VN?_UU]6<q0Jtj2M1nB>1&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\TB<p[E^H1)J6DPj+(=L2kVFJ<a-u;G$UaT>e!c$H>%kh)?_"?$[^uhX#(_>P_?&H](!,keDm#a4J1hun*sWrp`\0g67j*aR<;K#7k2+eC/ldu5s<G'@5(_:/l`(F[=+D2bnDB?eY$3[1dfAQ4U`a+NS>ISb_[KK'i%`o[;p`ek$$&[SgdhBh.\d#/=C'ZO^6/)(ZhTn%Nu,"P\0R4:N;POK^L48ihR.!3%<X^:36>V3%XDWR6g7`FS:n;"'_T\+=-lr>/6oKXn*Q8s5>e?gF@'rQppCBB9KH`2WVh@iT*5R.T7,~>endstream
endobj endobj
41 0 obj 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%<g7,9M_$trVgWH"UA5!KA43_\-s7"TnVM`aqGhQBbephoH$I\GB+eLKH$9SK#ciM=mkjM^'S6e"Z/^InXR&u^7.BDk8["AnS8rjTs*nUa^P94pHH9-kO\1O[(WeE-sog(X6FS9;B.[t?\M@9>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-<oOV*pbV3-.:_?`Bk_`G/id#JD$)^XE[LCR[KejR)DEp/+g[+hD*p>-`>UOj`U2$dV;<I4ei"M$=<(U6>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:=2<sK8smTDN!!EJcYnR=C+_49ocFM)[/dr.$3!F?+(0gHfW84^o&L$I*&c"!Hdb+ZEXkL?Vca)lK^T#m'i^8PS8=r9nr'=-M?OhRMLnWm\%77iH4)\B*G65j+*FF!%4t<S>3$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/h<pkF"#TGH%lEi;KdSWp8WV%Rtbd:3VT^r/JbHhd!s'3.$DW]BW:3&ddGTB+^3Vq9tVHq*-$/U;MiOMMgsU6_b7sEh2&<F+8UD,oML-JjGae%R?]a'ILX)nTlIDQZKT#?eHW]@&SLmYc[cOMVb[s$'JGEl`Kj_FJX*Qm'/CX8V70S-l!q?;2Zcu=lSBf1B74po,BNk+&[lcT[bCeRuqdZrFo=$c[CYgS\bk+!e>7_@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@R<m*GIAr`3Z1O&\d\aZ!$Z1WGoH_2s6)Jm_QhHr(sa\tH7"[iYSEjKrS9,O>2NH_Rq(La[ZE0k2l9&OS.Kg#o7^NO#$0\?YZT>_f^=K_n.ds?2.eG<s[p%'uG`J^ic;deU?cba<29g<>[.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^%JUN<XcD1]^;_Ci7)/&j"g"FM/-Z1Uk`4\\Xs8(2VJZY`Wb<7RA"CD+6mhbp[!d3;FXr6#CW~>endstream
endobj endobj
42 0 obj 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<IhNe'#]Nr/$-<WFDIL35dAD5G6/?5M9eG&9I.*8cr8f$Z-M_,EVSPZ,:_\mLWR2H2R'0`r$Ng9sAhkSRV7bC'%F+bRO620iqec!nY,@$6#!lMCMMgW^$?U>!(.sg<409/pIjG1fr6c6d,LJBE/EJim+7n2^1Ic-mmcfuh.ms8]'Y3d8q(Yaq9TM`F6(9<!0eUma/^"-8c[:-c1$Cbtl>&*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'&,e9<YjI*>jQg8-<kA5uCLS1]!:6VjdbOmo^I>gS>.9Ri;,Mk8a&^g@kk]3,qGUcP8:cqD7Y\pfoG-Gj87p@pgY\q6Yr+3I=V>(DbPj/u;KrDBm-'Y4IH<;'=,!&sHg:@QFFeMNO8gBYI:]qQ2YV'aWK+d0Cj<R=D_suC-&cUEQA3o5Ii?ne#Iqq@M$NL,c1'5XC>`@B$-iZ4_>kCqh&/-Wg\!E[94K/48HsSbsDKGQ_=[.WQh;c?G2*.D<kl?drk^_6]!utI="'T7@G3r2RP(U>CcX`MO.9kJ22lc<'>)I9(_[B]_oSS(7LG,Wfc2h6bf)o3s49-7NVJrb"kb3CN4R2)6RSXSD0hil")iE+jX-hAhSh77!+Kjldju(AuZ<\CeFo_T)iO`.SeDL@3SNpQf?smeoN>Xtui@o<B%m_hN4cG29@gFnp6OIdBm/]-_X.1rGH*7.?@.<ig:32j5RtLkLh[q#8K[m>7\/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<<!oJ93N0%,];_nK<uJa@dP>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#?,<4f<k9biYl@#e`_O56O>U9PhI\Dgnmp^[&gK2MsRScFgp`tV0E@q\7sRtq)'Pl%EnIH[L]H&N>pCGs$rYM+jm)]<%ZF<J9uQ9RRuDr>puM:%PmqFB-[r-R>c2u^-aU&Du-nf3Sg[K?W#lN+]5%0o\oSR$B6/.!],:meG_o@Nk_,&mYj'jrH4&b46sIce,m*<eo5G1&($=o/nH&5_5Ru$`N`MW4qeKEHsT+KTb'uONn]qJ^rQbuV%!Bc9#gV=K;%]qr,5H/cmPjU4S7mPb'E?g8^_1`/esn351NH!V$hD>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<VW6n:]R"WOGsSpJbp5h+bA[31/=HOqBA^WjUkjkie/(++Q@L:_Mmrr<\.0p6%`^95Q^'PX'a!p9?:<!&:p6#?NKrH=;",>'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%lhV61p<p:2oh&&efJ&W>oGM2JDKO%.&V./MgK9j*rL;1@5T8a<t^X0"J1Bq9pgWeH9c+ohQf$@PjP=Nhm5bb"[TU?^g\t<HOYlGp8M.HbB+0)At[0`p2.q;ne,l\?q7O#Cj7iXHM)+S]E80j^6HoiG"4OVK5-n/?0%8?]J/Wk`^iK[c>kWhm.KUUEgj4$6ukXWU$:+PjjiGdl.+kNpcY'N?!P)?:YjmdZ]e&&0Z1]jeJ\p6_lX,V$gj7t8PN9j\^r;DoDEna<JcDm&gO_QF=JcnTMen2K8<Hpb!.tT;D'Q;fa6A;_qBH!:;KH=1\hScj_]\AmCUeUT:OKJ8i>?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%=6<UWcf(eKt53PO\;4musfagWBEEAH,R"")aFMj+Tl1+"-OSDa1[Gf'8^=UsCl+RJa+Uc#oSZA_Cl'5airQ!<'aD4T9).l35OB+I[;OV%u>Jj15?V#+=>"MJa`9]Ad)?e/J[?f^Q#YmOr^m!$XeBEe`fAIlPjh0`~>endstream
endobj endobj
43 0 obj 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 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#<SD0#td6PKC`RL+c$!c5jO6nB`ZoOk,>_[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<8<h!&r":k)_b9Pri&csjEbIsacgF'mQUd1!e=gTVL`:r/kPtkX(m?l,X1>L2GJRRqLP@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+fgR</>Zc\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<TlkL:[TV#]HSGij1/^i7@Spa<cuOm:K2m8)h/Lc[P`)dG+-V6BWbt&RKfSOa-6kgM&MoKd-mpW:^bfK",^.NVSfMY0f4%3_Xp,Enl%6oG5_*hkk<o/AlHdVe]Z(+X5bQ%W\Y\^8BQ!tb_`Cdld$*Z,I#`dHe/CE3B%8SQl#N2K?K;?#_1<KVP3@cU\WgLilud%k.I(^r0Ou5SbM?/CC3AOQFu.*4RZh(R.8XU<uf8D-c`K-1K]M&mS@@`)`X(W[]Ru)#7"c5TJV&n\:Iuo&(KCm_s,)KI;c3bB-(oo[a9&-XCG'"$"s8?3'OF>~>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--_U<joM?=]5Qm<#+1Q"bPgQ(RU-E$a[B"985qG_#]H%U1.X?gc,2DiVpi,?'>kai[\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<FA8TsY22BpY%G&h79.tM_+&94C:.$F;PU)VQn/[gDRMXYU"UCFl;g`c3J#YK+B,L6I8Vq&11g&*iX\ThiM+]W725_D_2drLl1&hE@"#!*m49Ys(2aBOi)8XoPn`@\0+W;*%C7`KV<9PH\JKU$cAfNa)_;KN*<\%0lC/nC;*\U?+O>?%iNjtd\C5Fc*5?J_S4ZiVfks!+SQdPDC8;n8NN6hVL%7Zu4dlh(#/u]<Y]Uf%W#0u8P4P5h^>`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?RnKm<O+2&M9j5h"&kXS`>Fa?0l9-[NNmeQATGbh;6s*K?Rg8SN/&XIIJA#+?i!mEkApjCDm'/HN"N[Ej3`GdI0s/5Yc*2HkLb><E.8kQ2PrK286J2u%5YnYC2#nB;qUUoK>J2Ta8cQg:t6ImMO"KJ?cZ\W\Ll\(a]+i\B?2C!8<U,n/S@n<k@W2bB(bTd;V.kY66g+h2KJc_4?,Hp#bh9V7LpDWt[mrR-<5Fkspl.3E2&[J0nq]TAUL7Udut#3S!fV]SE7g]C?hI)BIm3H68;+q2-YS7cJppFP?+6$CmdipgmGL2me<s2MhB-W4<ZF@1WUpZ1oPik)Z$;tsRHQ8^soVHn5Q6`rP;9*7gS)@5kV\I8Fp/p+AA?.^^>j=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)#<oW(VAH)Uab]!!,F\/3__f@lI$2\Gtgj";ep%rf;+E;=!*T?;FCcI/+^eKG*[b`V@EB+!_0H(YG`cotCAs'2H&oep)`57_HX?mmJVO85:G`)GTbHR>=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<qtBFa,h1MG__8/G-D`J*'9-)nk."C\l9!r3K),7g17$_<l:&GHK-FOSjhs+e`-okd^G:To^^dPRcqD9ld;pA=j>:$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<h5&B_d]:e/`4OnV#*k2b"h=SQ5X]:6r3pIcq:^&m(Gsl4+^6JTI8)RsmEKtjaZeM"o)&hnK$$"bpZOV7+;*c;;V;e>)"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<jG8o?#4LOl\$H>,\P2dB3oBs*I`t,Eu?3EG))r9!uVs3XcQt*(:sBn0(4h7*#[9ta4#.ZY`[t(5C<iF.sf/F?QUS'ODZ2ap,-Yo#G#7]3(9n.,L(FSf<\<JGP.9fpV#'#CqBr!2=48PL=mCFNH0h]lkkIV9Q:H3i'25"7(uuJU%sF?Eu*VgG6E<`ra]WU6$L)=]44.l(\l[G1MfS1?/8#B;,V6C1CQRO+B"EY:((Sk6-'Ugc\I=Nqan:a1a2@4J4L<Gg!(8KrnI</kW-j/_dpB6ko?%S-<Ndki6"aLf*oiAeb;.r43cTJ'=Wbh:'dalRBt0uB5N,?eWRllZ4!*(PbWoni@8)q4Cec@*nbUhNmC:_LWTD&@-Vq+$)\Z!2IbH$5F$G<0L%-ZR@in,5>?@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(:r<F?u6%o_&nS9VGbE-3k]I8X>Pg!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`GA7X<ZQp3QA7^VY&<UW@-UM/H-otX[f*s_jCHr4j4V!I0[/*Qf-h>PrrCZL#B[>'#:p'F)W"L*pu4S<':`e#Q=aP>dUXfr"K,Tb+fE/J'=>`6[UhD[eT;=k[YZiI!,"iDD<tIg!/>h4=<"%RHJb)%o]Ps8`YN!l(Eeab[EVR[AeP>8UGJ]2Q$<h>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][(4G<eNU09<S8;7sO"a6$HP00jggi$*1P%UYFhddf]1sU-);oF>4;%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+=,<JZcgu5o33i#^N1RRQ4)[>>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<FkLk31*Ald"a8J#q.?2,!\G"/u6gFZCZ7A/KY*:Z#uplC>#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*nP<e)e>Vlk^\.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\3rG!_(7"mjF_Wf*E?=:ud=Nm;J6hptMe,?>j>OB7$2/;*A]`T[Z)Nd+887>n9d*2(h9<A'J_RM%I^R6[4]D\++#3mY6BV@:HPtpP>",``!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<bPZ<:XlX+0^5Sl\ePOH$pL_<!#1&X,O6GaN9*AMoroOWoZH6b`0:oLZ_/e%gLMLLI9K$!_o`[DY]A8>:m&9*bo$0c=g;ujO_[dKF<h28GaV1F.Ia<E^X<uq8Y/kP#VWW5AUXotk-\T\-Q-aop;`U@ADC4@]-Hs=CK8%GiFHjPRVZG#F*6:X7!R@.lN,-6mnuOQ:=sW>U+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(%%Qs<Tp86@g__=Ne_YdU:_mcX,c/n>tJg(6EYIqbr8LbH2>ubJflV>$kWcL/;q]/OqaSEaN14jHg+c!]\(cimaAe@gb'B2n))1lbj[I<EK!2;C3WK:TXS/+j/kF4]-U:^XtKpJ1lu9g1.@JI\ce]^HIQmCc)VX7@%2p(aLnT6:)VgtQCgdn"q#--gJ9aNCW9ib-%[)QaUiFHc>Fl)1b"-8R:t&cahos]VM4Y<^6"P7Xd:<bZ:D:3]Y:ngrQjKop1j:%h-Q[[?N!p6>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:6<O6Ofhj@2kp8Ed@XEg[,>Z5EG?h=4Z2\1uYo]sd'2m:FI<f3\Uk97.iTP%&&!ZIsrPqlFf`PX4jBQ^(GWu&L1aL["L!<q*&@S'Q,dSO>:/ju?"B%%r1Yhb4lk-0IJJ^pEFKq1FUU=aAn-U=',l<?npP6PFAZ?Kk:!h']G6*E-G9,)3"Zne^4AiX_ljbM*l1ZRtQWH13tPF!M&_#?`^`7N9K$A=\.doQMl^/=I27J@^fH[(dV[r?(hh:-e;eP<_^Psj$',YfSQ9`[p^TChCC2@oR@7WGN;39VM<miN%ZgIk&@IaINhG/j9)c(X$olVERBj@ZTb*W6M+]cR^~>endstream
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@`<HVA+:H8PWr#s2>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*7l<GA`AR7@fC_is8t=(DXdH9qgipji94IUDQ1&32-[#<8_t-d.,*j*kW'R,7*(-O0daR(9#GOXGZp%lg_q\!Siil[YaV;ldU2$?=,c)&'m1AmV`VS:XV7FcJ%pn>cTb$K"Hk@/HWC/DE.)lLg+GkqJ="J<KXhjaLL*\B2p,>c<'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<Bhkk'?,XV,b:s>:,@7t%sXj?Xn$p\;%0M;pXS7]9'hnb1/Jn)X*ao2Iq\ndnc<T?5g'BhD30KWY+W;GB)5Q]+,6asI8`#=<r7bkP9^n3'sRO(+]-i^I<6*>,gt"_dQlY;n"l-JPB*CK-V^oj8O-(LK)W<sUZNlcC9\(sb2IY-X52WX9f@tsBlT$af5spr78GBW.(6)aN1uaR?#-OOKFIQSc\@TEYkL;fUNi]jl_L(X_e@Q3^Y0JJ8[[.[<-t?^:."uZ%1=mB7CC[^0j5Z.)V4(DFeB9Jj*ZO>HhUfsCCc/Ug@#@l7\n^%K0`F>DM&n6KPH`aJ'q#+2pnMsF5b>##mdIVA6FWHD>DqX^UA2k&K`c(E8O<G3A,LB-Ueb4X<`2H,/0WHI5/%MCLDK)[_M\4Xj3'ZRNlV:99bEu/H["%6`#1MD^#&b_QCaLJtsCI]LOeU%\Q!N$CESr"$T0VJbk=8LmnLpfh4iUUH;R"l-8`P%^06qUNVe$b'8QK&W>("p0VJ>:C?C4=#BYiiH^L02G9^e$b=Sm5Yl#h'E]8U.$cdu+.%T%MMDb>GVF7"eU^n*KVGf*NXgcGG7K>S*"))kqU77kNprllQMq/<nHKes7j<-m?u2bl0EV0;ARW0gP>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]?[<GYftHM=baUUnAaGba+"NHF<Bh6j]PNSP?L2;ZuCVsY8]E=:Vo87OmQ*PJskBqI4BLb[fA^V_lJ.1>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<tentS;LF-k#J+M)fXGSDS_:#E#tDdY1#kHP^M4?F(87ji5<En@eimGF<fB)q]SF66&[=Y>;: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@6GIaru5Ph5<s\bad2VJO&:?4CDoY0P."c$\uO;+kZs*u<*L'*!`PU`:eZr_Pm"Z"S(DPBQ`)[o`Y:PLR6H[L^](C;]b[ZuqP6$0HG^qL>U7?Ip*`Qr8Uo[r^*NCYj`dO_&N[L>^A=3rKo->kBLDWM(M.i<s*=B%Ee`&`M=Wiu36m!npa6j4+JMj\h;h3F;9T3GCAdXI42#p237lI#DpIFs\Jnl23hP7XfE*BhWCL1gaX5's^3^p?]sP0Y$D.]5I])<Q#Tb?q]GID+Og;Oq15bG=T4\rAimhX(@hKZ"QJd!;cLDP]^?:B_T'&TeVKJu?@<.g.:'J[Il\=onl)\V9SGWoY/cma~>endstream
endobj endobj
xref xref
0 44 0 48
0000000000 65535 f 0000000000 65535 f
0000000061 00000 n 0000000061 00000 n
0000000146 00000 n 0000000146 00000 n
@@ -264,44 +285,48 @@ xref
0000001690 00000 n 0000001690 00000 n
0000001838 00000 n 0000001838 00000 n
0000001986 00000 n 0000001986 00000 n
0000002134 00000 n 0000002133 00000 n
0000002250 00000 n 0000002279 00000 n
0000002549 00000 n 0000002427 00000 n
0000002627 00000 n 0000002543 00000 n
0000002733 00000 n 0000002856 00000 n
0000002817 00000 n 0000002934 00000 n
0000003023 00000 n 0000003040 00000 n
0000003229 00000 n 0000003124 00000 n
0000003435 00000 n 0000003330 00000 n
0000003522 00000 n 0000003536 00000 n
0000003833 00000 n 0000003742 00000 n
0000003908 00000 n 0000003829 00000 n
0000004018 00000 n 0000004140 00000 n
0000004150 00000 n 0000004215 00000 n
0000004285 00000 n 0000004325 00000 n
0000004451 00000 n 0000004457 00000 n
0000004573 00000 n 0000004592 00000 n
0000004694 00000 n 0000004758 00000 n
0000004848 00000 n 0000004880 00000 n
0000004973 00000 n 0000005001 00000 n
0000005098 00000 n 0000005155 00000 n
0000005229 00000 n 0000005280 00000 n
0000005369 00000 n 0000005405 00000 n
0000005472 00000 n 0000005536 00000 n
0000005554 00000 n 0000005715 00000 n
0000006516 00000 n 0000005860 00000 n
0000010064 00000 n 0000006008 00000 n
0000013475 00000 n 0000006111 00000 n
0000006193 00000 n
0000007236 00000 n
0000010970 00000 n
0000014423 00000 n
trailer trailer
<< <<
/ID /ID
[<735e427b950d10110ca3f1f9e20deac2><735e427b950d10110ca3f1f9e20deac2>] [<f56de612b28393ad9c1197e5e2601682><f56de612b28393ad9c1197e5e2601682>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 25 0 R /Info 27 0 R
/Root 24 0 R /Root 26 0 R
/Size 44 /Size 48
>> >>
startxref startxref
14170 17346
%%EOF %%EOF

View File

@@ -69,4 +69,4 @@ PIN default: `1234` (change in settings if needed).
- QUIC / WebRTC / USB-tether transports - QUIC / WebRTC / USB-tether transports
- FEC / NACK stream protection - FEC / NACK stream protection
- 1:N multi-receiver (unless explicitly testing) - 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)

View File

@@ -92,7 +92,7 @@ endobj
endobj endobj
17 0 obj 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 /Subject (\(unspecified\)) /Title (Alpha soak checklist \(LAN demo\)) /Trapped /False
>> >>
endobj 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 endobj
27 0 obj 27 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2427 /Filter [ /ASCII85Decode /FlateDecode ] /Length 2525
>> >>
stream stream
Gau0FgL&^F&q9#IR)bGtFY3D)_c+]q\"$>g+`(NV+rl;ME@`3T0'-[Ep+u7kcGd%ufO$&+/U9h^k+Ze6&2<WAo&%7ji8YM8Hm1g(9G]s1_hRCjr%UA1h[(`Eg5b-P*Rb424>n2&\+)Hu`Wo'eM]*(!=$TIXI?dGFCOp*s8KM=4PslPuiVNag"Rfap2bp*dUf$t>@Es@[J?*^dI!h_;Vb7MWXZ64ijeuID<n^$jk#:sO:VqWH,n&$aV<f?n0h_<N0h[l&/CLB=;W,<!Sqn,B;I!IqYXl_g8JT;LT1M"H6(H#\#H<fIOfOnAC3[JQB.lZ5iL.-P+_mE1NNm,Z!u"AGZlH]+Jj$7aCfu<#D$3H='@?ZgBnMIo+VP>=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#Z9<O-34#-j"D5k6K;\G3TZ].#uMB0/kRLE0>cROY48W-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@qA<o2FZ9Z?m>IqIL!F2rn.Xc/>G8sM;24N*WMo,`qe7QC>_s8@@TN2^GO"Lc'`s#`S>p0?!uebW7aWt=^i!OI(/gph4(k;?l<(P<^&0n<?U!:krc!+&Y-a;u6%GZ>42<FZ#YWWAmsN<-[A#)Hb(`mee)0C#rJVH\SRL[0k=pj3sVFC!)Z>d'\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,=XK<ME$S)*n+g%TU!R(jW,[@$5$5H*RN0b\S@M(`)c]nM4Yd27RFda3;0SXq+9*Z1WS4=-p36`)ma53E:Lohm(hDX:Y7S#I=:p;*VHJf[2?8/2h+8cf]OOV/V<Pa`'T25?cbdE4QU9e1Zc^`@fBFZS_$`W_nD5Kac<95=t_<tKrF4RSYGr86=\hSWAkLd\'G7D'I.8+r?]8Qf?Ni]oN7T%Rk2RPW54W52J)U>r4&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&gtj'&>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</cVs45ff307eD"VlFn</OFW35!mXT+c:TD8)qY9-#r.@D/2W*5<7RSHJE'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,@]<p0,9k0#bYU3Rs%*2E6m<B:A9F]Hrjp1$A.nmX'@fd;hJCCh#i"Bc\RUMEH0]9]og`BlC/DHgu8l,Cs&Oc=tp.E_c)GsL?p=IQ%4M<Y[i8:.+]B_Q=1g5"FECGD4MRr(^ep>&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\ERGj<NnNYO<Gu^r-c*IetXG:K.&`R/ip7>l_,=)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<eR@(;q7'TFqT*Q;QfeV#T%F97rm"NM84Oa!\6k'g.1%\.[@"N*[Q=DjLY_Ub^/M.O.PsB&d!h*EHFmt5@ABa"$VAbOUBfqHWQQQdJY$5,R0Se.u/24/fCO/?%4GSr5Jtc%-HjoJrcH)Z3]'t8QJ1E3H;5F1*;d<tm'I+9(_tJ<EO@*76>,*-ZEGh7N]R+<ajuX9ibin8Nj&k5c3HCr'[[]g&WtI,/9Qn::7"gV\RNW/U`%sos7#,h)2bLO&O[3gCooORAI4g!:aK>otcIn.TBjh75,kWI]V5q$LeOfPUH1(]ZgCD\=&'Ui/9)nK@h6.Ms*G#5W_(&FkXB=I8BCD9*"e-Kict#4c[3O49OU\+(AB<N!H>C&VLW?.Z5;D=6Ho^A=3t@h"Ds[5HTWX:.*8aQ,*R\GSaM51cT&Njgm^SEGW_EWXX"6j_=J8'\>c>7:Ggq\\4TH.4`<Pm;j"unQZS6=66H&(;&Q>\Mhr@"i;<f1-\7=WK9nPi]M"aEZI5Z:#R,85]WM*:o#)WZLo6fE2&Dmbk^[U.<-cV6o[R4I"qsMcDUh%dt9j"O(.UE94aTfucI<4GW\XlllZ!Ro[N7n8<Plo)PC'/]\%Np+s%H(5HJ60,Zr$_l4$If:L.N]dnPq,f%Xh(J)AsmNH\fS",MW+"uPgX\+_fNM'9'P>%[$Yeh/\f!160c-kBWYpD,<CNA&Sg"RI!djAQRmOF+@<H@N2n?<$OAbboD.`Y#EJ.Kd^7mi!di%`kTqVGZb^R%ns'IkKW=^.#e!edfjO/,<kD?71#ONL^)R:A;In&pd`7D'\f"Ed`j];sU%0=G?UZ!a!=*8up=C6@[Yq)IKUlt\lmi`L`T;!.D+5PTTm1=6^FfEWJg\,DJO$/eLF)WLYddLV@Ng,bALLj$.6,!nVk8.<3Ch\4IBg5(Yg06?^P0%\rWY'sHkCFR(+GJ$d4eo'#`467[*C0b"CPf#&*bjtj'd'((q1!tO#[`eB2Oi3]SjKeVrh2R3kT=FKaa)]X<;.C:q+4r35&]l#<qo3(a,f5)(l(WIU/OgfpQXM[mXlBZ_&,/A`4V23Yt]7T1Vh93iU7im"=-%2Wq/Mcf/].luZaK*7(ne"7I.I-PMg8&pG5ir#AjdTo?XY_..5$/p]?0_D%L&1Vh%E*!S'%G%k1ZQ/EO!MbC)+3$tD;\((Lpf7_H$Werf?FJp<J>s:',R?c23F?'LJ9rDYtUq/P('?gY9dMlZ3Hh^=ZY3FFWGqDY.._ej(1P/DO@fFhd/5rq5![...b]#.4]e<ao9f>YPUD4n4.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`-^E<H`YQkR&/O3<E3Fu_T$m6CGn*s^e![\:?PJW$7bk6<S_g?6E0D4<3$A?#M34fjk;RD[Y9VdgjHr4,/CM_cQIV5S_0;\`;5Gd]4mg%*q4EUIQB9!5Znm4f+iXZ^S`Wh[qlo=!D#hP<%5GM9#T]t^8S+a8G5\jc%f)qrlj?0@PSPfQiLm*qa6Q=kV6?S&bfeA(0_*o$P,i/k4Hp9P`VGihKMeBlL1_)7*H<CV6=ll9QSCLlo5.Shmg43NTm<#RD,hIB-or8jD+;TC54Y4eXnh5(I6j2GN%E5(qPY;X+DeQ9BY=e'8Dpp87^(gX;b2fICm4e6E=+#%=66`u*Q($j1h'`)o^b<.r%rGiD.E#qY,8+6CP)9tNU>no94rTMY6]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./Q<P*()kqaFQ=?8@kWsRFh4cYhj.nnUp`g%6*t0iES2"klklVRF#uN!L0No^p\V!!p]s+Z_qe;lkC3!d6c(Y5[XOS#@2%5538gR6g\C]%?MnD_nR_HU#!/h\8$?$aSDCtqj';aJ$<A@^]]6c$5klNhLXmLkF?S,^5@h:E#.daaDppG8OjiMN$7),$NjROb0@Xb,T4[Q_b)ZMmH'IYP)CL_A9Q)FL&=ALmEgtn!LZbN'QQUHDn3r3X1H.XJ%-+*j_/;J7nVG/qbM[.;U!p'Nh=:1*jk*\oAuUb>h*DBCrW+Ih9j%~>endstream
endobj endobj
xref xref
0 28 0 28
@@ -183,7 +183,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<e968e22228343a774971f6b322897513><e968e22228343a774971f6b322897513>] [<bbd8ab8f4762cf64769bb7aaada579ad><bbd8ab8f4762cf64769bb7aaada579ad>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 17 0 R /Info 17 0 R
@@ -191,5 +191,5 @@ trailer
/Size 28 /Size 28
>> >>
startxref startxref
6495 6593
%%EOF %%EOF

View File

@@ -214,7 +214,7 @@ endobj
endobj endobj
37 0 obj 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 /Subject (\(unspecified\)) /Title (AV quality enhancement \204 Q&A session notes) /Trapped /False
>> >>
endobj endobj
@@ -455,7 +455,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<876852817b61089766fa64afae9b20cf><876852817b61089766fa64afae9b20cf>] [<bd17772f8f9b3b80a960f225dbde379d><bd17772f8f9b3b80a960f225dbde379d>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 37 0 R /Info 37 0 R

View File

@@ -92,7 +92,7 @@ endobj
endobj endobj
17 0 obj 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 /Subject (\(unspecified\)) /Title (Android Cast build ecosystem) /Trapped /False
>> >>
endobj endobj
@@ -190,7 +190,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<0da9c3b30d0111b270b2be75732a529b><0da9c3b30d0111b270b2be75732a529b>] [<a6c94f5fe79a0c13e711e1e90825077b><a6c94f5fe79a0c13e711e1e90825077b>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 17 0 R /Info 17 0 R

View File

@@ -67,7 +67,7 @@ endobj
endobj endobj
12 0 obj 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 /Subject (\(unspecified\)) /Title (Commercial features \(developer-gated\)) /Trapped /False
>> >>
endobj endobj
@@ -135,7 +135,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<bb21d75fe21ec421f3c0f308926be035><bb21d75fe21ec421f3c0f308926be035>] [<0c23c7846cb813e7f6634f931914a505><0c23c7846cb813e7f6634f931914a505>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 12 0 R /Info 12 0 R

View File

@@ -72,7 +72,7 @@ endobj
endobj endobj
13 0 obj 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 /Subject (\(unspecified\)) /Title (Anonymous crash reporter) /Trapped /False
>> >>
endobj endobj
@@ -141,7 +141,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<791687390028bc46a9df797aa4d2bb7c><791687390028bc46a9df797aa4d2bb7c>] [<a454a236c417c49668dff25f892582be><a454a236c417c49668dff25f892582be>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 13 0 R /Info 13 0 R

View File

@@ -4,8 +4,8 @@ This document defines how we develop, integrate, release, and hotfix. **Cursor a
| Field | Value | | Field | Value |
|-------|-------| |-------|-------|
| Version | 1.3 | | Version | 1.4 |
| Published | 2026-05-23 | | Published | 2026-06-08 |
| Author(s) | Anton Afanaasyeu | | Author(s) | Anton Afanaasyeu |
| Markdown source | `docs/GIT_FLOW.md` | | Markdown source | `docs/GIT_FLOW.md` |
| PDF | `docs/GIT_FLOW.pdf` | | 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) - [2. Branches](#2-branches)
- [3. Branch diagram (steady state)](#3-branch-diagram-steady-state) - [3. Branch diagram (steady state)](#3-branch-diagram-steady-state)
- [4. Daily development](#4-daily-development) - [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) - [5. Release: next → master](#5-release-next-master)
- [6. Hotfixes — chosen strategy](#6-hotfixes-chosen-strategy) - [6. Hotfixes — chosen strategy](#6-hotfixes-chosen-strategy)
- [Default: hotfix from master (option 1) ✓](#default-hotfix-from-master-option-1) - [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? | | Branch | Role | Direct commits? |
|--------|------|-----------------| |--------|------|-----------------|
| **`master`** | Production line; **green** (build + tests pass); release tags (`v0.1.2`) | **No** — merge only | | **`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) | | **`feature/*`**, **`fix/*`**, **`topic/*`** | Short-lived work; fork **`next`** | Yes (normal dev) |
| **`hotfix/*`** | Urgent fix for **released** code | Yes; merge target **`master`** first | | **`hotfix/*`** | Urgent fix for **released** code | Yes; merge target **`master`** first |
@@ -124,6 +125,43 @@ sequenceDiagram
Dev->>F: delete branch 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 <hash>` 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 <hash> # 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` ## 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`**. 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). 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. 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 <hash>` can drop a bad feature without touching others ([§4.1](GIT_FLOW.md#41-fast-track-on-next-alpha-sprint)).
--- ---

View File

@@ -219,7 +219,7 @@ endobj
endobj endobj
38 0 obj 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 /Subject (\(unspecified\)) /Title (Android Cast \204 Git flow \(green `master`\)) /Trapped /False
>> >>
endobj endobj
@@ -462,7 +462,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<068b2b541b532a647e185c962518956e><068b2b541b532a647e185c962518956e>] [<b859d7af6a4738a9ecce4f8413b8de77><b859d7af6a4738a9ecce4f8413b8de77>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 38 0 R /Info 38 0 R

View File

@@ -123,7 +123,7 @@ endobj
endobj endobj
22 0 obj 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 /Subject (\(unspecified\)) /Title (Android Cast \204 Graph Visualization Pivot) /Trapped /False
>> >>
endobj endobj
@@ -258,7 +258,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<b885b64a468bda1dd9ec9c5a881415e5><b885b64a468bda1dd9ec9c5a881415e5>] [<ceaa9bb32a6eaf264a3a717043768037><ceaa9bb32a6eaf264a3a717043768037>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 22 0 R /Info 22 0 R

View File

@@ -153,7 +153,7 @@ endobj
endobj endobj
27 0 obj 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 /Subject (\(unspecified\)) /Title (Android Cast \204 production infrastructure) /Trapped /False
>> >>
endobj endobj
@@ -319,7 +319,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<12469c2730c47a48460adf663f7f8cb3><12469c2730c47a48460adf663f7f8cb3>] [<87a8fa5b1e63c3bbf3a0dcf885fa3e7d><87a8fa5b1e63c3bbf3a0dcf885fa3e7d>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 27 0 R /Info 27 0 R

View File

@@ -112,7 +112,7 @@ endobj
endobj endobj
20 0 obj 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 /Subject (\(unspecified\)) /Title (Crash console \204 open ingest API \(design\)) /Trapped /False
>> >>
endobj endobj
@@ -232,7 +232,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<02d61126cd7bac88ae3ba3f80797fee3><02d61126cd7bac88ae3ba3f80797fee3>] [<a715c7d986b017c524e6219400d53a60><a715c7d986b017c524e6219400d53a60>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 20 0 R /Info 20 0 R

121
docs/OPEN_TASKS_GRAPH.md Normal file
View File

@@ -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
<!-- toc -->
- [Priority tiers](#priority-tiers)
- [Dependency graph](#dependency-graph)
- [Task index](#task-index)
- [Critical path](#critical-path)
<!-- /toc -->
---
## Priority tiers
| Tier | IDs | Alpha blocker |
|------|-----|---------------|
| P0 debug | **0.1** stream dump | No (feeds 6.5/6.6) |
| P1 | **1** graphs | No |
| P2 | **2.12.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<br/>agent · P0"]
G1["1.x graphs<br/>agent · P1"]
D21["2.1 DNS<br/>DEV"]
D22["2.2 forward<br/>DEV"]
D23["2.3 inbound test<br/>DEV"]
A25["2.5 SMTP<br/>agent"]
A3["3.x 2FA<br/>agent"]
RBAC["RBAC<br/>agent"]
R4["4.x RSSH<br/>agent · P3"]
B51["5.1 OTA<br/>50/50"]
B52["5.2 URLs<br/>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<br/>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.12.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.15.2** | 50/50 | deploy |
| **5.3** | Agent | 4.x |
| **5.45.5** | Agent | 3.x, RBAC |
| **6.16.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.36.5, 9.x
```
**Developer blocked checklist:** [tmp/20260608_help_request.md](../tmp/20260608_help_request.md)

190
docs/OPEN_TASKS_GRAPH.pdf Normal file
View File

@@ -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<o#MT23`Y805-cQ4)W/_l".L.Ca.i+qhCa*4sUj&Ns/A7\lH7$*:O$l9+$R]I[Tj[*]FG:Sl;YXAO70#WEtlPL_K)DSZqmA9nQYH3)Gt:s-Bqg_>*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></N%D-\B=S]1Z[++OjIC(:ogVqWH]pH7hd%FkDIlk_`Du&3.AIi.7`74>oA[F=OBkdc6(;-?5oUqg'c#"m-B:OsBIpBO:anNp-f8G:O2mJaQqPj'tXI:(!WofnbCI2q``^_EO>PU?U&E9dIk\f.`$'Ei6%lI_'W68<W3\e-"7Ls"^."2~>endstream
endobj
24 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2075
>>
stream
Gb!SlCN%rc'`FV1EKO]tg*I.LU1>"X!,"mW$sqrImY2l&$JCRU\$kD@:=K.<?0Yka/MYA]/9WR@p[F>.]Hf-@",\09r_=4/-R1T7B>?KcVSEUL^o.KNUV1Sm/4LP@QlSa-YW<dKKl(3\1_WHKD)00sKA/Q]agTG",%7jo7Kch%)cXN7_*QCMDd`ORS9XI+i09^$'YbXi")iJ"(jghs_TE6Q'/aLeF19d$d/D`A>[.^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$nkU<m$])7_AP;kL^=>LR(bkYl4[ZD<X'_@56LK&\.U^MjO]Vln1/dZ?XZ1804LFKinHLb\-R=!T=hB0"P=oeP%812;8Aq,Nuck]FH:CX/*EBnP>I1k:[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#Wtk9<Qd)&aNre'gUCPMZ^sOLKD@nh=]\#YZk0DDG/ZTE;?*(][hYoV9J(Z%eDAa'?$+]6W4l.FA@@PRqG%mBpc=%QYB\G^-YA"?2JlF_HHoP]R;I[G32eBd&@_bYrA\_2Q^hbo?^f"XW*&_BD;I[F>b(/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"</2K,]+V'rXC<ghT(R0SKbce#KOT6k;:t5b]Wc(s.;u]pK335+*#"oJ?p8D>NM)Z*DA0FoPtdL3'I-pkOk'\"lXu!I;f7s&ge;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<<mT-M.X,(+NI8TC<*93Sg&QX;rK\W_tQ,jiDXlu&;r80Q:,+s''5B5i3;Ma;<;D[[46fZG@@\!UJ>Y3&MH:/;l=7uYL=`O)V:J!\JM[Ve(K-Jp(d1P,H$;1J*mP[?VRHIN3<Mi.#cM_6fO6aK]ZHOTn,\),Hs&a0&uW_Y%Rll#=7T/?kHOe,&q?/e!/Bq)RK2STQt(-SFot=@pSOnUOFSYC.--dqOP4#2!J'&)IJ+><j):1*1(Gg%=ZK#^Pq&jNN^0:\a#OO>H[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<q4$Gom`VE<mmY7&7W7cTaZi>.<,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>C<Oa<6h\X%P\Vs3]EHn^T(^pJ#7uG$'h*hEe)q.qe(qolil.)\aUI+_LX$V:`ke8KP=1#[LRrF4==gf.%\\J'Ybd<iY!cHFRO`]mk%LPM8ECK0\;"oq5SV$+bTa3e[NhJlcUbq/7OmXHdTdUU[ZG1^%@Uf;gPN]C5Au\.;@eZoQ:DB%1DJcHJ8=p37DpYMDr@6*XZO0RH;&$>D<`/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<!&#*m$-/4X),aleM'#+-c`3:Th7N?l32N)0@.S20_Ll;ldUd)UT5Wg*?;hoSU8VF=o;Q/*4nSW4?@QqSjY=B^nV4a#/daE0iHoEhi:&GeYA0E'odejENVpM7$`h8AIG_*V8L8C(qnb#m]7B:\%Et>[=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
[<c8bcc4e5aa667718b0d822771282595d><c8bcc4e5aa667718b0d822771282595d>]
% ReportLab generated PDF document -- digest (opensource)
/Info 16 0 R
/Root 15 0 R
/Size 26
>>
startxref
6705
%%EOF

View File

@@ -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_<c'#s*OBAjC;Xd"$S\'@NA6Yj&mKi>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%<Q#<5!klICZ6c%+$$o?h#]%bcIko.?@G8oq5UP_69M;u(oTb4MA78<Q[XET+:n6%TE?&FmMquVkjeP9tn:_,I3=>iYP-=*T-<c':D*r+-epr<MtZ!S>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'\&=<Kjo98otd=nIj;,lRI!eBNlPX.)<?_RZ$X3CD(#B>,6U"pmM7o%."FSBPC570ZnGf`T;6d9q+uB<uf3^QZ!dRSF8aQS^BjMb'kMiB6JHM/kmT<aiE<=X(GE/dPX4Ss4W>"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*?>t<Kh)&1gCpPd?@!"i"%r0i`K.Z)"ksV+,&X(P0kJLY0W]u1L71KURp*FQ&pgbe`Q/*B%]RNS'QUn)8qUNXa,H"'a"X]P49m]-qEF:S4:`s!eqIP3JW_N?s$9=M3%'7Hg!t`Lg7$T?6>0(FL)9]/l[_RgLIr*&Z:\<N']E0VL=Fj2B/dXPmW5i-L/2P]Qf/?3'%\u,'`SG'm\^j08-3Xh4a&"!u=,mn<(X4%AG]j@kpb&aE8([0!cD26UC-gPO/ci]OJ$6a]Ls@ho[*&*ot69:32_$@]>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_;<BXpXlc#HT+PbU:<#6F<DGZogXnk""sD7E3Ks?&uHtFUq^Tm4q[&7&j3T3kh.j<Fkj'.US==FH&jiR@QY(nY?0822hJ^rr2SN=Gj*B10";2I!pdtl8]@fX0&SFQ4MH?6Rk<e?9OU_j%:Q]a^BcNF^Q`g^fIE:J+='X(SqUXV&ckEVRdP5MFPERQ&J[>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)%uuGKQME0</.1a*5^%`cGD2Xp54B.&Pa!SSa^Uk3tIR%X"#Obe.+XW`=jY9=t]/B!G%8@8Z2Yb)"jb:c6JF:Atb)qa3UOcGYg'I7R^`_DRCST([@0[Hf.Il/#_oVUDKLl?YR`cG&+>h1#<GIsQ9Yqm?d/ehg^I.=`$gC^2q/`6`R0.]C=`L1?[Wmk'-uCfkh<K)()>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<DO?d($iO2)A-,iH3NC<Q$T6VK+H`U#kM,_g8>@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_B<!HL4Y[T)YIR7e'\s&D0o+(ME@(Rle8FF>U`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<EuNbi@biLI/6hF$tKJ'/k^-ULELPG`G:P"S9D<q79#X7f.UVh0RRgSI_XP>+*VMrl/IVD=8X$?J<t7Xf1q-hlu2<I-&>H8dddOR-aV7BMB08<AoFIX_Pa(g!,;Hd->SMSMg.HZbM'5U"qkbd7_*O3`s!-^7&Ppj2.<<l''0*@gMl'W&!Z7gj*_Zg\\qcf6boW'g%q#?Pbkqo&\E`)5]iOO79c:0R>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:<D<+7gT-]fC.(W^-#[Vs70Pb-L?,?]DrgkV,o9_70OqJl`qk7Fq0>/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(<I*Kof.J8k=0^)QhDgni2\'G$E(A5[HB,+ffIG'JC+r6KbSp_XL?jZ.>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<r#>]>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(<j7/_l3j?$\0G$880IhE\UO8,aaZ^]E]53_fVIf.#WOg8BdJ[^qn!:(9Sul4Q8%RioL9'+e5/;3]!FLo*ih=7D0SZJ,1;5(h;%Jt3#U54>)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_*eq<Sl?>d!*\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(<hC_]T<,n",(!uAe/D]&BR1^ePeT9YUh,e_N!e,.U>p]L+*b**N'rgN:M!"J`R_O]4$*RY*!JM&7/_k76D9trfO57rZdr._<`U>5?W3NeokCM"-rCE;%gF<FE33jA9V.W4G'@L4F(e8ZMR7Wj3D9HBUe!sXtRPm2Ff`>aP2)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<mDfLK6fn$!>[%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<iP"3]0FboH"OKi^)g/f_DH$';Yem%WD3#ULhI<@7oYg`hP>(m+<3]IS'(LMhfWq&ks7P:"HR_tnd+m:'5fRoC<c_W(:<fK!q9Vd_*1Ef+j[s@2;AT':1Uo9od?O=W)#j9RE8AHo70pgTbt5[Tl?u74\`RI+\_U<i"[gM"0uEDsZdMPl@CO74*ah\Kbokk3X-<f^oS])Nf-8cmS>mSn^: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.%#"@'8<W002_CuGFZh"<<Fp90qAZLjDYOLp:#7kAc,r&`I-M5QUeEsqD]"'`uWi[>9ks5ZB'$qKJWhe1&^m=%lO;&,nMHM5*MB<:_KLkK8UQO4#.:`:[rZZ\-rjoC7]GZM\*ZL2opi@DsV-[bs/nmO9%ab8!I$q*GJF?X?+R*QKL"tSbMQ*><Q18M-*YN=!=P.dU4r#7ahD!-J_"\DciED(D*]@<<bjsBfDl#EN5RoRfkc@MlbNc=M!o;9(9~>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

View File

@@ -112,7 +112,7 @@ endobj
endobj endobj
20 0 obj 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 /Subject (\(unspecified\)) /Title (Over-the-air \(OTA\) updates \204 deployment schema **v0**) /Trapped /False
>> >>
endobj endobj
@@ -233,7 +233,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<fb1766001c59f41c7bc88a0fec7828d9><fb1766001c59f41c7bc88a0fec7828d9>] [<8ce728cd48920bf00138be4ae62e19b8><8ce728cd48920bf00138be4ae62e19b8>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 20 0 R /Info 20 0 R

View File

@@ -97,7 +97,7 @@ endobj
endobj endobj
17 0 obj 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 /Subject (\(unspecified\)) /Title (Pro / OSS split \(cast-core + cast-pro AAR\)) /Trapped /False
>> >>
endobj endobj
@@ -202,7 +202,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<64e7459a461b61dd1fa8670aa52b9dab><64e7459a461b61dd1fa8670aa52b9dab>] [<cca35018774facb6253a5fa1087adfbd><cca35018774facb6253a5fa1087adfbd>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 17 0 R /Info 17 0 R

View File

@@ -25,6 +25,7 @@ Click a link to open a guide. Section links jump within the document (GitHub, Cu
| Document | Summary | | Document | Summary |
|----------|---------| |----------|---------|
| [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md) | On-demand remote access (WG/SSH/file API) proposals. | | [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.md](ALPHA.md) | Alpha feature freeze, QA sign-off, defaults. |
| [ALPHA_SOAK.md](ALPHA_SOAK.md) | Short LAN soak checklist for demo validation. | | [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. | | [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 | | [REMOTE_ACCESS_VALIDATION.md](REMOTE_ACCESS_VALIDATION.md) | Linux CLI suite + mobile parity |
| [ROADMAP.md](ROADMAP.md) | Implementation roadmap, alpha definition, deferred work. | | [ROADMAP.md](ROADMAP.md) | Implementation roadmap, alpha definition, deferred work. |
| [TICKETS_ROADMAP.md](TICKETS_ROADMAP.md) | Tickets console, Gitea, attachments, agent queue. | | [TICKETS_ROADMAP.md](TICKETS_ROADMAP.md) | Tickets console, Gitea, attachments, agent queue. |
| [20260608_ALPHA_PRIORITIES.md](20260608_ALPHA_PRIORITIES.md) | **Master priority plan** (0.19.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. | | [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 - [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)) - [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 - [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)) - [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 ### Product and codecs

View File

@@ -52,7 +52,7 @@ endobj
endobj endobj
11 0 obj 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 endobj
12 0 obj 12 0 obj
@@ -112,7 +112,7 @@ endobj
endobj endobj
20 0 obj 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 /Subject (\(unspecified\)) /Title (Android Cast \204 documentation index) /Trapped /False
>> >>
endobj endobj
@@ -159,7 +159,7 @@ endobj
endobj endobj
29 0 obj 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 endobj
30 0 obj 30 0 obj
@@ -181,17 +181,17 @@ Gatn$;/_pX&BE]".IJp[1m^!8_qmqJ8<[5Zm<IEECRg'3fZ]_+rUr%C9suJ+BS@u<_D]1/rU2]j"Faa.
endobj endobj
33 0 obj 33 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2889 /Filter [ /ASCII85Decode /FlateDecode ] /Length 3156
>> >>
stream 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&<bE<AFh`pbciL%+LW9@nr0a^>lDl&,9C]NdY1ak-0m"e@K-K;\t'T><"5'6p>#gFRUD+cP(-Aaf04([2<$I_t/!%d+^Z5)#e*D-:Xq8-^IQa_]/Ci<d)#:A@hKKqe'5&AmsEhihk=LE^VTBmX.m/O&C!@7mi*kb))0NZgtPM5mU!WKn^:9p"'iYr%0t_rk`!8=FEf6m2b>.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<WU-'!1-ohi1*(MPG=U]]F?KO`%AP$S@d:q]3;5sIble#*[m';bAo,G1J;C=ZKE638(<<pXr-5_@:Til`a1rbi?G8\heDX+&I8K"%51ohm='H*k$-Kr#8R]nXP4Fqq@dJTG"j]*Fm_Z^Gk49A4fUSgF;@?.&g*a_*NX,8B6J,&;0O=r\!e'qPideaoZXh@NkMerp%jFEL$=HRkG=FSEW!-hbo9%A7I@i]QZhqGX^XHfufc?-#K-rCDKs<jZ=SXSa0sn:/Ig>*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_22j<KG8JpB37Z<(rG2"8hm7CXH$,P=d:g$qp:g+n,%_Z@^$4<i_/_u_I^2eHpc!I?f,YpPfjf$',GF+DE`9$M;4T_Zerm]UmE_d>c8^=0im:FONcP<=G0W<D>'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*8DHcE1NdB3<f)(k(-#eJi&%dg]D#EmMqoEVT7lA"S7to,3r@Jg^E!)YSiE,c>YV-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<AUJ211NED2t]-KuB-u<<Xol`ojPn(3oqjN54"T(4r*;5TY^WF1WlADVn.1@n<8+$4+UI2pP._QB^H\<ljn%B^N-8&_MG08]WR?j\O0=DNNJ)B3W09@\13sMACIYJ@pf2fe9qhPt8hg&2Kq%aQC%TWH>[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[%[str<qj2CeZQloEn3F@).jC3h=Z`0&D46#Y'lnmDEs,O6@O^K;Z>r0l#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:j<KoJWQo%DWRI6s/B$P`D[g2=hIRTCf&d.luX%[lA&?0>i"ps](MRqmisobOdM&bq6S@@e(K&IS\sn3%$RU&-Yao0A%]lf0%F<MHA4[Pb6]\n,Vk^'ru[s(I"HaT~>endstream 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;<D_"_PMWp(p`l&UuuE8a`OIUf*J<9Fs7Rb43M"aBUIYGL[Hk:JDu<<<)BWOH#U2ZQ!-gY$7h?+-fjR3h4:7+Gj!Q;2<U';&5iMl3aG(cr"@?Q[%d++(#]/T9?:""C=otc&3,F=T4"id\#nH6.\>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=>e<hl`Vb_>SNr`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%%8UJO6<qQQ=5Aapfqq[Y6f<NP[YmgC%>LDqe$NFaN?WDf"^jB5LOaYRI6b4'FWC$N<LVXIM8,Cl/h9-kVobG/L]s?5b3Y[Cm`]DL7U7&aDts%*%052AI/^SdE.WG*:;&)%lF(dXjsk@V=f3KB9BB$aR-<2`^8,8T1Dj2'!RlflX:@@CcrC"9ujJ`.@joo/2rN]')<B;:N_r?t6go,4jnfj[>rRE^-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\O5<Taa)4W8NL6j*]p&"9f:F!1,hbH;^3>Qs4IG8ppZ,`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<e]'=DI;0h9qWnEHT>![2d"aT'0N7ohngkA>4*$K_W[Y?lb[b8>%ZN3,-M:I/UJ!?fT-Yfq+\qUpYQ]<DM:n9V4-Yn>AgE6F,JF?/l`&\gf*5!VIdam/Rp'r6D8=+VfU'N<aPMMqFIeDk-AalmB@UZ8XCtZ+4!j,_kTeVI+PY%jB'$#UGg[[.qQmmrA^XWUXO(8XMo$W>*+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_<f(T.0OGrqgigd0*I4GrY^b\\IFNn5coR]R@bd%UGH6,d'i!.Z_-g`6skp$di!RM+!+&8Q.)a*#9f"XH7C'OH^)*3\3gb=s;k/+Gj!MKYrBen.cQG$PK@;83#CeBPXM6U;MSY<Qn'j"\G[qK+89P>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+<Bk99":+cW5j8&U957FC:8S05%q7ebg1L379)Ii7*BrE]XHjpdg'GBI=I)F?kQms9FK)Z8tWqiHbKj=&IQ7E75$Led;Jt?O)OI7I__FQ`E^'&@hpM>O]c=B2MUO14K)qh/c#2MK7oCoZ:lJL%go5FTVY9DY.0mX<tLap'D5#cQY2So:c&X:E4ZN4Qfi'<fKN[ULmLa9e_E^Mnn;(g`q54uUDPt2r9%:.YPCrP_?,(MR0cKN`7tj#l[9[E]hdBk5&*FcG/Zib)[\WUWDiQ<5$,hCCai*\TBm&=EKG,naebr\I@E$gZ/O)<?mU_tT"83C/N5rOrQ=p(\mH1F`:G)s9Lb,^9:]#GNof78NN?B^DfutHCf:?=Oa+RM?Z,H"4<R.K_^9?nV1/68-]qW+gc@DJ#@a?aj5.L(RS-JW@;[;shT*]@i!^fDd\Wi:1GR3%9=Chlhg#^F'sEjhAD%6POK*E%h&(GF\^]C!J+esi1?AcC@1VDBhc&V,*Id]ID)ri0%OPusD,r",aqr)%[]^ipiA36,M#9%4._6,C-OibT$5Wi(D'pS:V=S(\jpY[Nk:[>bjlUAsK;*_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 endobj
34 0 obj 34 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 389 /Filter [ /ASCII85Decode /FlateDecode ] /Length 722
>> >>
stream stream
GasbUhbM'3'ZTV='OS/5<9%Y#m5hr0`Tb@#>N>slV-A(r$1q\F0@[R$[gPQR_th4=j$8b'62S'+JU<Df5sZB5%@WCfI#/(LI7A>"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#J<mW&S=\":^!(!<A"o~>endstream 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<leU)1C@VKVq'ei1S0`8B\`AKT@p;KqN"A"eueZY[?6f'kBKg$'/')nI>,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..<l,2&\n`2aFNb4L>:TN[lk]lkKU5O7[*F.J2B&t$&8DjXmM<E:\BYK;^`Srd.P`<+`>93$8Shmc-9t<hlNC+9Gj"[(q6@uLg%YL87Sh=7j$^h<riINLc#EM4dH@G)I-OjmA1jEXNhD)(L5+1%'SITH5d3-A`S;.!W"r9<UR1umM`1r'Xk&?NPgYJkegaOgT;k*`OgUOK;$$(kLPt"T8Dd(H?)Gj\Cgl5YTc2%1YMJ9a^H"Bq5[(QunU;ebr"RT\%50XeN<LGXQGWK>l]oaB2O<3!/%G))-Z_Q0B#:\B"'9s;%\*U6<'f3Ljb!\1J9Mi!DVWafs&,"XVHGJ2U50%E9~>endstream
endobj endobj
xref xref
0 35 0 35
@@ -229,11 +229,11 @@ xref
0000004122 00000 n 0000004122 00000 n
0000004197 00000 n 0000004197 00000 n
0000004919 00000 n 0000004919 00000 n
0000007900 00000 n 0000008167 00000 n
trailer trailer
<< <<
/ID /ID
[<9df3a579e257b333c2cd9123c54c5e70><9df3a579e257b333c2cd9123c54c5e70>] [<ef3c83d8c0a3227f9e77021a833ebd39><ef3c83d8c0a3227f9e77021a833ebd39>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 20 0 R /Info 20 0 R
@@ -241,5 +241,5 @@ trailer
/Size 35 /Size 35
>> >>
startxref startxref
8380 8980
%%EOF %%EOF

View File

@@ -19,7 +19,7 @@ See the full proposal: [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVE
## App (developer settings) ## 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`) - Pref key: `dev_remote_access_mode` (`AppPreferences`)
- **Disabled:** notifies BE (`status:disable`), tears down VPN, stops `RemoteAccessService` - **Disabled:** notifies BE (`status:disable`), tears down VPN, stops `RemoteAccessService`
- **WireGuard:** foreground service; jittered poll **17 min**`heartbeat.php` with `type: ra` - **WireGuard:** foreground service; jittered poll **17 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 - 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 - 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. Uses the **existing crashes vhost** — no new nginx `location` blocks for HTTP.

View File

@@ -22,57 +22,57 @@ endobj
endobj endobj
5 0 obj 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 endobj
6 0 obj 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 endobj
7 0 obj 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 endobj
8 0 obj 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 endobj
9 0 obj 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 endobj
10 0 obj 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 endobj
11 0 obj 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 endobj
12 0 obj 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 endobj
13 0 obj 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 endobj
14 0 obj 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 endobj
15 0 obj 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 endobj
16 0 obj 16 0 obj
@@ -128,29 +128,30 @@ endobj
endobj endobj
23 0 obj 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 /Subject (\(unspecified\)) /Title (Remote access \204 implementation notes \(v1 WireGuard\)) /Trapped /False
>> >>
endobj endobj
24 0 obj 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 endobj
25 0 obj 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 endobj
26 0 obj 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 endobj
27 0 obj 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 /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 endobj
28 0 obj 28 0 obj
@@ -165,7 +166,7 @@ endobj
endobj endobj
30 0 obj 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 endobj
31 0 obj 31 0 obj
@@ -205,24 +206,24 @@ endobj
endobj endobj
38 0 obj 38 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 807 /Filter [ /ASCII85Decode /FlateDecode ] /Length 815
>> >>
stream 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';J<M\L?3!h:&^O5Sr#Zsn04mEBJbVh#759m[ZQu2Ok9G@%h8i!G<[be7nj\597Y!;SdjPRn4O%;^,qE7o&6U?NK,_fS-n80<8uc(EKiXl9*)FQb'Rg`sITd&Ih]*V$q@h;$E;92)&H=7Eqa(Q;iS+pR8!B%Ome:T)52fcY[##"9j>B/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"5<p-Nb[7P!Cb_X]?j;QO\:qtq0RSL\THI45p@JkF,)?BZPgOse=DXf-+;c/H7iVK>r)HNAjVD,>c:<^RsSRpp]$JR.fV0qW<ehA)0a<uLU0d9hZ3DQ<GZ-g']i>`W;%hBbJ-QmA;d5j3r-X-er8h&94o^YU./.<oq?.rc=A'0N)YgO]G`Tjjp[YHbfb5gAQ]\CQ#A!;#`")IGoFS``)<F#k(.KqGg_3sRW@Ili2Cu#Mb,F)J=jF(]dOpjdC9%R^TnAH[tNTi[:*fk_9T.[cbAc"P>qc^1>-9oAtjPhqcg`E#V9WkR;^KaIY8X4L``lW*JQ\AU(=DPO%6G`<d$m#bdQI]Jn71F-GN4(U8e^>pEi_7E/g,cMQ1lQkcFK.-r~>endstream Gatn$Df=>`&B<W!.IJLQ<\JXU,j\h:ctu[mYr:q)<3"a7J;dm6G(B8?mm:41&FImqM?FQ)o).pIo,S's*kqVIk5`O/HoU0%"XIU9!=L#1LLJWCn+"G^JRi1?]`=cT@\,dik^O:Y?7^Kg"7lbk(WF06.0]eX46b-Sq*l]_A53a=\?%!0W!/u)5*)G"i^j@Q9:VIY6)?"k#<+t;OO))OkGTn2j[Om"3(<[:%R5#mT,,R]2[9SLr.<uM*<,h!_Z*BW^(`oZj>I2+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<a%B@c,]>`ZVM`Gecb@b]@CcQe,G^T5M9XN;7B\p9aa+Vno[isPE(f>$9cP)Q&5e[/>]_YEe70,9C$3)u6T;\!#f8)9%1<Z<m@-`ii(c$cpC^EBF0R,LDj((S6"M0oNJ6ZOEBW<phIqRTTecZA9j^c"<\>FpA]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:JNp<YpE/V"Y[\?lX<i^ga697!TZ(7\apBR013bA~>endstream
endobj endobj
39 0 obj 39 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3647 /Filter [ /ASCII85Decode /FlateDecode ] /Length 4044
>> >>
stream 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?<pp`Y-1(6WpXtpAucYT)eJ+]5JYN&\G9"kp'#qinn0D3C-$s]h#f4'*q6kf\#'J?F^=JtHDfrn3E.cNaa'&bh2+oLBc&4^5tX-JU.'D&_Z7E0Z(g\j(6r4kX&;lu\*_=^RcD0eC;c1bd00%%"GcfWPD^P0pEB4QcfOL,pocgLO+>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-<Poi`3.to3?juPW]3l*M<JaH0Md2fk!YYo9do/MmLJpgWTCSK5UeR\:Hgh0np9sP_QV]40T&9R8hkJUmQ8%>BY@,h'HiPM(7W]P].pO-8PC>^mFTp^.IMk(ceZrHgJDpX?KV=_<ocCn0n_d;+G[CYJ=Wn>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.=S<PfX54JOqTZ<r`dXaJHmX)l8'j#c9]=-q$ajCG*&lk2MRl9%\gR>I;MF>f2W<notjN^]n<m*r93Q;g',bS.?*l"2%Ac@/?Tr'JmeJ$PR1\hKU\0ZD=<Si/bng]>'%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+</9$0TcE%kFHM5$T7&FrpqF*d#u2L_fn.-p2NpV"!P2K'd!P%jKl9l%\PAkM1"'Q?oVH[fb9dW".^rs#^L-rH!ngm]K">&Uo=CFq5Wtr8-6aS>0T>g[eb^931/+*AFTk[.`H_a=0](.KcC<$upnd30bjk]_5&EIW`$`$-PhO)5/4paheT6W"qAH#S8n<p0M.I72XqBaT0^@6Hm%T(<9[Mhn=9"OD%R?5D96$F$W>5<5c6E\;OR:XSfi;+UM163fJD:89d0f1:Ej^h9ObqK4URX&ET`:q2.X*h<bQaiP0<7'N]KP9*>"Yeb`F(o@Gi&?>\YfBSLJsGH&gs[Im[>$u?@:+XGdd0)Sdp+l$jV@C,B"gB?%Y$]'?Q4kQbq&G\4FYZ5fg\nuOsX6MrDgZZFRe^<Nia`<m2MI@N$=1JnKY3SQRL+dt11=L@\e*+Q'2'#1)+,FU\9sQBWc/_XCZGL0dKSLt:qG?7H4V>%b8nn-FklW:^gH(/]/c?QbOoYfpkFKq*GiZ+&hsKkn(JqOIniN<A3V7"Mf,gfHB-03S+-2'p4c+$Z<.e4c7GX"jb/)DD@ld5?)m=?O+)/Yl@E0W%j#*9IOTklu2rkK0amG4qR>R&\$-b#oa][QrKLSdqG6V@jVi>JX,4rqA859AS5$j]@pW0uh7h!`;*Jseu#+UF-h<bdnF*iu#@f+@8s%heQpBAhjONn+7c?OWgmtcH7L0L-1b?"`0#ADU=S?q8,;h@Tec2&9[S*A9<QLjE^]2O02%*&-5#I^Xm2VMhPKRY?)Q7VZ@>Ki)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<LM\5^B7NC2E.[in!#_o`?A4[$]GhBd?<5G]Fr6\IZQ-@g&1s=Vidk3\;j8H)5[^J,c.:o-*D*gkPZcPYG4I5rD<^89q*Ug,@,0Jp1s%N:MorS]PX[FNO\S\Z-bT5q:J'elB0B<prW@6]mdH-;VPJ\9s\tStSBtH+n,4$@G<U!b?Wii.`%ZfH-:lr\d&DM5]$'c;-]h-!d4'TdYUP$@ZVsO'&hXa8+h'Vtdj!SPYGJf7pX0Wq'fJ1:a2<6rf!5HrDiKNJ[dc;26GhndgtYpQkfN6U/U=[5k(]+M?WpVSsSE&1-Ra_k)ksB=Cqa77P,\o!@+TL6S)8c/-,c7m*]c(k*Qb7gGSG5NbU!)3&UI;J!!p4Xi*iLt8N+_0RBgidoN:[-:Fj*?=b.p$J)5[<?oV1[=r[3V&nn`SU%Kj1W\$A*]s`XtPP@1H55\GKZXroJu*.)Y:<o2[mu'(iR>]CpT!LC9ip4^)ZdX9/k\c*[!Q&q4*ApA=LXRe`Q+Ol0,TlJP\a/)T*\1#W]330@@mj5)eHgZ#aJX49F2EioRY;pc,!DA<e9l0/dagBG*,^IjCThd)[;#!GO<>C!H!Sg+A_9X*ktG%l.*-SD9<M^cMR6XS*RV+UhUW,NWI;W_W:c"pl.nr^MS=0Yh`l:(n474CkE"/jaVLj&4f)`:4O4`oB"F"7[clL0oH+LtFsXLGS]n=?nLq6Fr,^ckd3`EoX+AQS:pONft#aO=acRLT^a+>I/Zckn*M4Rd*01BHA]CGJ*UMo"pmDmo^diD`uIlJ.O`l"F#/u8,S]u]GImhi<YgJKTR<WZ`VC%9X'GoEtM/R\"_)^q"`h:B<<i&T@`DJ%';LGK8A^HY8L2@M-se,Y-,K&`PJ;]'Eotr]pOCr*>4JLSHt!9'6n^-(SB#l[McWp8L#BO4JL/?6VbmjGlC1cXB9CqW[kRm5+D/lr^"g4;Hh*+U(3X<H@rPWAVp/7@O&$.^YJg7Fa;A;$Y?5P:-;^]JJ;F_[KfC^.N^#<<gT!S2jB6N9.W'F;+I*jBc/^;J]DbTJ1OT0<5b\l,Qr1nc@SMPd/6QS/_V_>(h^j63%N&h[]2s:@EP#F:g</BO]4\BCDS)0#]^&!r&+sa!AC"Vq3pm8YLAf6n$$lb<Q;%Nm<Q8/.V:Qr:i+bFkk,>$o9`%bc3qf*^@#0n4CseS*7+r(AKHQBi3DmJ$r'3_<?%2)Q`GdX!#:u3$mQ9"ql1ZQ^A5sHXME+p1`-t.;q)@DC72c83ZQS,r.(/B3[5"EQD6QW='#fc(H7?@i^3g6ee>5Xnc6W&@J^HeG7;%\B"_mJ"g@n9nY"73~>endstream Gb"/*lZ:gj&c\H;@_];5gt<A`+t5Nn]lD=mdMjQ2r4'+TmV!^MQB4og"&P.<D?'>_^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`5<u;`'4K*2]^`%c10'/moMHW=6&&iGN5c-N5]hB+FKq9VV4\QqZ:t+b-cZ7T:P;S2-97YEdbP]fN-kf>C#%Z+d[&7Eim#6VL!]EN(5k<La%('RF2iC_h`=E1lb]m'+Z`XXNMMsk,rDqkc)3M1$*H1cMV([/mP,=VVsQq#;r&WU$?fAaTS%,FIKp=8"[T3WZ=;[:>[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;<P!D6aV[-a`uFaJ]Xmk\N7fGN!a]6a)%eT[Qf<C]%'D=*$8Ei3JCH.FIIuJ46l\[j.`E>Q_$_R)>KU$kUE)aq]?f<a-4R-"i%a;7`ku@A7:fHaHNdbY*9.5u+UZWT<(In*\;gq)(0!l%cZ[9R4d`ZVm[?(7V):c*`Y1sR/Xf10nnT#eWrq?U7*gFJG*tBYpUP&3/mB@Ohd"'`Eje:P4n<Om*[bD,bK(49<41<,l+tm:*f4!;+-0^QLGNn@WuSUIN%>BVeT?\WL7u'V1sCOXZobM12h"cRoI3*A&n;PUQM_(he#%@0iaiZt+8jJ1^;&pJP3Y5SDa$WFE^]iD7kRZ(7796FTARMCOdb"J)5!Kh\.P4e)YoiDUN&bT.1b)A,@;=:T(#h9]2oQ<Qf`'1kWUflHP@/G6"-Y&q[`1P,+B/4CBpilk&.0_0M$mUaB\:4^*rg<KNDlu^^C6qC4M9*AtkJ5RLOuis'%(%Q;7X^L=o`[L1[\)<Fq@5>8;"]3$1luX`_D&@*!2B68#p<X]W73rmknt1+hK"[>d@\-bQB9)ViT*!%+]I5kbClKp(-JJ>CuIiGA0TqJ2TmS2Y>U1XL6eYebLa_S0a\^pInc1i)==mJo)q2MZ]JK$s^T3Atb7686<!i!d)[,7=A]8]m[4Mf%nIU+(,2W;]Kr=i4:OfQ&rWm'i%Y6L8<i5DE<nlPA.M&UK&T;Z]=olh>mEPdk-`RdTIY,H,JjaMZ-B3!G!l)^hr`Tk<p+2kR$DJ!cW*[sSf7;U%m?$;f,>-!ha?=dunAL"+1UBqcZVo5,3;rYX"gQn9_B%6C6f9Q:s/XBi\uch)G"I0tVe0C-LL_mM=U^Yi&*?-\4$pE<ME((JTA.IZ"R$(-?4WS]20H8ZpO@X2A<*r<Fk%\H55CcuDJXe[Ru)7@ZQn7b(>H\@"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<Wus@0ei81)L*Z;n*]U5[n65`'8.6edSf'c-:GeacgPfIs2tde@(Ld@JEl$][kA:@X>@>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<<H,@R[MAB^?#j64LQA[UB;2hNl0j.?sNBPockjqgfD:0rs62rd7Ul4.!.2gDKL)\Y:PKB"ti@mQrT"u7Z?cW7[fXB3P'8OAJ3=M%N:;Dn]cD=j.sL?=1RQ`]jIQf^A5H22M:5X\m%pgCY3&;@+>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/p2Y<Nh8(r^ELM^r4cs"+M]Q0O+BHTp,&],2V\;kAk[(=I-(*H)A'l5;NYM3LGs=bTJ<[`Ki;'&p,[(iH+0L6r5KMpU>Z\[qkP\5O]>Lglr-?Rsl5+WQJSfMt%;[D9#mpZ[:KRQSJJnjIl%+L+chI>pp+LfCX0]"Z/3p*AQ+@*@FqempOOF/H)Lf09D9K8T_kB!<%B%V4RdM<eE+j+/W!#BO_.NLZa\n[%^9]+,fUg)l'nsji=,*R7@_A@l$0>XA+p=.@q[qjAU7Q+jE>"oW5/j#lnPm@qhYC,!-PWUkH+;/%cq.Et8^l*ARI<UVL7>6%bP,(Pjr/eBLP3'l8>MmH`IZt1%=3a=;qQ!:G./j<%1UamL:jAp46uP<WA4mPS>P!=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^IG<nYWfA%o$NP][S'ko?mAI69tjX`8BN:86r6e=c32@QoO\_C)lTM,TM6?q!MIk3X?<__10%J`c=uk$!'Fgofm>BhOdZ78p[>Dp&1=&>0K)KX-'<<jE^cOZ;h&E4^esW'<FZ+eQlS?<U+h&J:)QX4:O&5M0aDp[.([)"r0G`$7mACOE)WjD9+R1W]G02s<J-aE`6EI<"(A]\<VDB%,^Tqg`7BWuDu?a:C3b(L+PmQ++%1Oa,aIRhin^]:XW=!PEZ9jX2mS^m"N;UN`KiDmp@QaBN@8K/#m;YmdKY3n_7XZb'kOAaPY,p)PWS^)P7^4o.S-(G-ia]J6=al@Sb@Kej18]o#*j/md?d>.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 endobj
40 0 obj 40 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1666 /Filter [ /ASCII85Decode /FlateDecode ] /Length 2140
>> >>
stream 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<j"-`BmhGJ9B"M,UIOdfIS<OQ1#ORXo%DgHu+kiH4J$R^^GOHY[V?6pRa2:j`/^='BbrAoRP0Krgi;j3R5e"@!1+kb[ej4+h/-h["N3.dZ-j+5:RO,DRHG.I82;#nA:,'a>: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:#&5<TfFqPe&JLVRWE:?WdY(952r*+$eFAh1*IN+336(4LKSX44q(Y+L0'Qss:]0T&ab6aLcnr[s.M'q\CflC`Nc-;;QVs\iUhMY["UXh!-^*tQqP;Nkk=Ola'[u'iHfiH0TM3/I9S]\F=4/4#b[`L_g>9DT!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#(>V<!\5\^W5d2W:@QG]u8PjUT&FS:o0ZD"s`ed]CI$eB>8"[53/&H)GsJ`C$4p^js.g$KQn*K:Mh^B]AM8>(APH.%<tGqGnQB"#YCWlTJ0_H8jM`PeRk;re?kcPXK%mf-[!p/"20`\-D[/_/LVmB5nJA-*^CkALi8`1<GW4!16+1EmZH48(D@'n^GUupmJM:sei"tm%H@NI;%%W>@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;&<q6ig(a-.Oi"M=LGpaD;eu0Wc[<g<5(QL8\r<MYo9%HX,?=XTUriRCt1^LCK35dk.U3`hDb%Z+u&CYUd#)iSA\%_Lb]N@VZfh-V$dF=7_6=Z[&tZJ7!9f]I<BV+X<BuY#4.+X1]XpDR-PkJ&b@@,c7^T)Yn(VN%1LWKAHWTrn+ZYS'!qGku5"4bn"j;6[ur;6pfVpH.m34]eds;SaU9?bIF;151eW&CXh>,\I^re.;92&)o.J#7-C0V=92`RFic4sh;^>p;J%NP,l6r/@n!Vh*k*9#AITSE4[<g"*M$l+6+<m'D`6'%1+H4j'CcQhXQmWO?g9]Rn%E7#JcnMTb;YU$+-cKWjF)_a4qn6,HHjRk[rhfM%sIc0r:'`nP#n&q$h[u]lPY\.d2SOm.K^_1Lh:OL<jt67[Qi1g6n>27dOqiPB<O)KE:aqb[-//~>endstream 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/l0A3<jD61ZSt1ap"LO,=*Y)Oq)eC5,A!3\14JPls_G5rXmUibe%95H8O<t8CPSrsidVZ1>hW@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,bKai58psr26<GhY(7LV[J'rlCZl,I8FW0$0K##HqEbQNV2=OMHskRB]CmM^XXB7t4-2.L("U>J-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>\,68F92FWA<ciik=jHh(b=AuS`Va/6BnJ+P*%H_O&6(_@+6O5:TB>TX[Wj^Y+b=oBjZ&2EIsH,)@E[hYS)gVsNV[MGP)fnNQG4F>4WYU$u=du3g4<e4e$cA>.^Z^@uqTi?[k,skm9Jsq6Qn<AScDe)^JGlHCd(cC%EO425KjRdQbjidSfai4)/an5Mk#9s_j<"I9>QG`'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'8rtI<SI"BY!jP+?]m4"k&R4;tBjT)IL,3)*,4r>gS==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_<<UIHml%('f+8=Lb"u+sH>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&LTTTO5%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 endobj
xref xref
0 41 0 41
@@ -232,45 +233,45 @@ xref
0000000242 00000 n 0000000242 00000 n
0000000354 00000 n 0000000354 00000 n
0000000501 00000 n 0000000501 00000 n
0000000648 00000 n 0000000647 00000 n
0000000795 00000 n 0000000794 00000 n
0000000942 00000 n 0000000941 00000 n
0000001089 00000 n 0000001088 00000 n
0000001234 00000 n 0000001233 00000 n
0000001383 00000 n 0000001382 00000 n
0000001532 00000 n 0000001531 00000 n
0000001681 00000 n 0000001680 00000 n
0000001829 00000 n 0000001828 00000 n
0000001977 00000 n 0000001976 00000 n
0000002126 00000 n 0000002125 00000 n
0000002242 00000 n 0000002241 00000 n
0000002541 00000 n 0000002540 00000 n
0000002647 00000 n 0000002646 00000 n
0000002725 00000 n 0000002724 00000 n
0000002931 00000 n 0000002930 00000 n
0000003137 00000 n 0000003136 00000 n
0000003224 00000 n 0000003223 00000 n
0000003555 00000 n 0000003554 00000 n
0000003630 00000 n 0000003629 00000 n
0000003738 00000 n 0000003776 00000 n
0000003849 00000 n 0000003878 00000 n
0000004028 00000 n 0000004057 00000 n
0000004121 00000 n 0000004150 00000 n
0000004248 00000 n 0000004277 00000 n
0000004373 00000 n 0000004402 00000 n
0000004495 00000 n 0000004524 00000 n
0000004599 00000 n 0000004628 00000 n
0000004691 00000 n 0000004720 00000 n
0000004825 00000 n 0000004854 00000 n
0000004953 00000 n 0000004982 00000 n
0000005040 00000 n 0000005069 00000 n
0000005115 00000 n 0000005144 00000 n
0000006013 00000 n 0000006050 00000 n
0000009752 00000 n 0000010186 00000 n
trailer trailer
<< <<
/ID /ID
[<3c7062d12426f42b4b67ec68dbf63f96><3c7062d12426f42b4b67ec68dbf63f96>] [<0ea4a9599ca2b4815b3cdd11dc5a5773><0ea4a9599ca2b4815b3cdd11dc5a5773>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 23 0 R /Info 23 0 R
@@ -278,5 +279,5 @@ trailer
/Size 41 /Size 41
>> >>
startxref startxref
11510 12418
%%EOF %%EOF

View File

@@ -2,7 +2,7 @@
%“Œ‹ž ReportLab Generated PDF document (opensource) %“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj 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 endobj
2 0 obj 2 0 obj
@@ -17,47 +17,52 @@ endobj
endobj endobj
4 0 obj 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 endobj
5 0 obj 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 endobj
6 0 obj 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 endobj
7 0 obj 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 endobj
8 0 obj 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 endobj
9 0 obj 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 endobj
10 0 obj 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 endobj
11 0 obj 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 endobj
12 0 obj 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 ] /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 >> /Rotate 0
/Trans << /Trans <<
@@ -65,29 +70,19 @@ endobj
>> /Type /Page >> /Type /Page
>> >>
endobj endobj
13 0 obj 14 0 obj
<< <<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font /BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>> >>
endobj endobj
14 0 obj 15 0 obj
<< <<
/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font /BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font
>> >>
endobj 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 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 ] /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans << >> /Rotate 0 /Trans <<
@@ -97,83 +92,98 @@ endobj
endobj endobj
17 0 obj 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 endobj
18 0 obj 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\)) /Outlines 20 0 R /PageMode /UseNone /Pages 29 0 R /Type /Catalog
/Subject (\(unspecified\)) /Title (Remote access \204 validation \(Linux CLI + mobile parity\)) /Trapped /False
>> >>
endobj endobj
19 0 obj 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 endobj
20 0 obj 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 endobj
21 0 obj 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 endobj
22 0 obj 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 endobj
23 0 obj 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 endobj
24 0 obj 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 endobj
25 0 obj 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 endobj
26 0 obj 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 endobj
27 0 obj 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 endobj
28 0 obj 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/_9cM<!C;GO!J>G6aptu-a_5D<b3#OPXr(>o/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,<A<a"AQAb^iHPKtTi?)9#Zt_Cg>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]XUI<B]`M?QmF=(1Uj*[/\8L5.JuR@;jfX;Z4\<7F5Z[EOdKU^oLYa+:MOI0~>endstream
endobj endobj
29 0 obj 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@Cj6<jZG.64Ya\TIT<jnfrf8;*%gr,8bM3hN,)B*h-bROsYt$7_KMUWMldf9*E\WG7$&C8-Ro_\>LMUr#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<Q%q@FoP(U"lk4D(3CPoHdbPqL=?ep`UL_[:"$5:`YtN\'$piph@FT@+d'I?u+WCJ`q[T6%b@uPZWWoq;r;]j>#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%(;<gX&G=L5<4]O>59CgC+jMnhrs]MblEq2ckqaJf84E[@2'E)TG+-$,lFgMs.7<uCH2NlU'_$f^/YO02XGfuo7(kbA'R*I.(8?Yf*Jq]ptj>83F_Y'WDmYr7d?L8-;UN;Xm1,:rD[(e$/cp]Qj+-quF^kB,(^F`4\#p\iaUSG[GN0sRl6o>!3*3Acq20M&#X*0<WU[IrK!(1<5fl)H9T,_D,?ATNULc!kFI8[V-_V,nB69Q`T!G'SFEID3:aPf9'.5qVW\cTPo"E>Ymr+\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_!-<N=)a.M[K"eXEdlp,<38]MhHk0Dl^Dmhl&bPhip)?e<Q%WfGh?'cc2Y=4c,lE<P.Mo1\OlYBp(of0<Y+K(rJM0/O@hfT6SoD1d[3jcl+5At\:l=WfCWYe_C2geGLk[Vq$?3LIS\eWa_<.HVWhlDYLI&4ENq1e1T>]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'_*<VF5\gW8#=VXA;rmWMJ$2GnXE8BSZjgBY#H9$`poP[@rNI;e+bg.2lVV3t_+L719o9AMVO/$Wdoad>pIKg#trj;S@e;ut;G!pZGtCWb\A:BMf*f=AgSY$Y^gr8[@.pBiANCQZC=W>ELL.DR2H8+6pBqrP<Wm'JfRXRlgN4m&@_'Zb`Yo#0AdH04#mXg%2=!C7,$8_-p5$S(?#*?#*M+i/`-.D(%Z1<KjoC_TQ[k(Rf1Sq&/'JlKKTHV\#XJ70HXUr"1o$XSNNM2!q+O_ZZa;?hY[\A7eO*9f.L`>b+: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`W5JYSRi3<n7kR&i/`O]%!RNpkEc[_^6a^'LiXD"R>3[ko4U_3cYH?bl-mmP]j<g>K>"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`pWnrO3<G@[.q"Gfhn%Vm_#-<6.)/_G&;$u'fKB`<[,mRLjk-fnG?s*tXD0JDho^N)r3"9or#MpZIXr@Ht+48c8CUH&':"Mn[Ae>J<g^\!"X`>;d?4t>J%LHs46t^(i?g`=Bmk#!J:lWM&<BdbL!=@AFaHQEkAKoIW;,gjDAuAA^q2'W>\&!8hi@%jHC61i"H&ZB@6bh%t8I'HI.]TESODL0$"&TIe+4-4NI-4Ie^CFX+W:!S';@R3G$Qd;AfD\%0h<hB`Js#NZO9`,%fIEK'aRN%ZFqaS86j58:&-bP^jd7=;DTa$+QK=][kf<XE1MteO.#r<K]*>Pj2+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-<Up/R1W^XsgtGMTEl_aXJ"m+::HDTa1^0_):K_D6-i\/KpYa\8;:Ysrr2;KTVc#G8i>P"p6@_PB;LEa%.!t.lY(^abbq"C7k3l?*&4[<<3R@QtLllr,\@XMJo/B@T`BlnY's8@eB3S0m[8I17c<O?T@8EjY9etgN1QL(q476I;@RIN>Y5gK^@';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'<W9Z2Ek5N?)b(M%;MUhY*P[D[nkga'qW>'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 endobj
30 0 obj 30 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2296 /Filter [ /ASCII85Decode /FlateDecode ] /Length 732
>> >>
stream 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+_<cnnC)!Id?;MM>FchWHf'+(_$ni28;2[ES<!8O=4UM7b[A_8K!ZM^#:@7?Hm\A9sR>FWj\nKn47nr.M1NELD&h5bFH??,7^RXIs8.3;1qhn7B+4*ou"=V*.^5S(KL>Acp4Wm%(sk9A3R3p("B;'*`YeS9a'R?kK;qE\:H`)d<!rl/_"dhp2OVQ68(p!HUsmkAT5X^iq(o#o.4s$X+f7&3NU5$5K@(p4*^@#5HGTkrr\&IrlJ5O5.V&S*sq02t/JV/DuUf$C>J67Mit,MGumR`BK5;fdqoh+3<"\9\1h%_U'YC:s5#O#iodr3(=E.GX6k\&AT'Cn-m]Z#nl`%g`E?cBFnB&<S_@R)QKp<4+lD!EOK<X6qZ(8n5u']iQmubrH][E1_/*,`CIBM+-3tf7aGFe%j?Z&i6rN\I_2&^K*=p3`XB][>6R4:AeQ"sA+7/K"#@MACJP5tN'jQ_$F(Sa'`cG.8qS#^KZNh%g33-m5-?O<N^#5Aq,tN+m2,7ne:.mp86_:`7Z+2Z'28=8+&Ys+RVqD`k`2Ln,>`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'=<nFqJ.)`nfg>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<CSoHrLNqJpX2][!dI#9-R4>]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+qlFqjg2<dX0*fJo+)U#QZqSG9M:F'c9[ct"SO%1:,Xk#%=n$)M>Fd$ibVdX#^l&<!5YgCok/14gR961s+R0]Dp*QH,T<S6>;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(<nJZlAU*YC*rKl1bs@a+cBG0o0Mc.6jsqA)S*@i7+D\$qH9E)rK+AX]XoIH9jYQ!BHKdeY>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:jB<o"92-*:R$"gQUfQ7r7kukK'+SCj02T.L/C_XW5tY*Mb]Uf#e5?B;F0AKAQWR1o7.f6G_W0I/!MSls<t)O/GqNbMD5[M#qKsL(0?"#?fp%nJ\Msr_dC6E:G"l6*mf~>endstream 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'<Hq_2+q4S>;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<N=&IW<DBbIerofG:X8&FaBce.H_&l!JoC94rqk&AutYu0j.MrB#$N8jTSaKH=;);nBO;!,EPBT_9&.P%EbS5jW>(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<JtWJ<JmUu^?$r4P.[`NC-g$N,FUGsfe<*/cY59\('.0F8ks3\FS@encglLD,UC5sMZ,QC#^osP_']O\10SRWU^AST?D72cjQDAa8?/+4*,D:+7\d42.MEQ:_.+<1ATLEM>&KHJ$5t#3-QMaR`9>)[;"'!R4m^nP70,jOoBY/m>b>>-bSLZco(qcHfU;m<jqAG2[Z>SIER?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<Z60l8l+&Y7!Y&=F<Cm`.bQZ4g-_Za?rqTO>\!<lk77AXCqqRPfZ`ANKGpCjG>@;&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<VD2Tn,S*09XdIXTFc@,D!ENUk*E8Q>?'o&g=Tso^fC-XS([2:rorug?;tP@)n!MUP=<cGi#pZu!utB6"8bBViI3'&qbR,?nRh7)nLpTai77'(Q]L410#+81fsf%IM4*aPs-lI3P%uYA/h:'D*m2IrMdD#f%7:*MN2LRMXONE.M6#c?A6;WJ"M12_>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<WY^;G0(BC.ZJ1L:AbUr&I*m10[&D]MAUQmU!+*q3"DjtKaORKY"@5reqhH9j.*T$J!*C)T3JB2+mZV-d&I?d0V9Q\1W.q/ibfjZlk4-;tY?,mP#(Heb^(M&3CWG91t_nX5*@4[I=9gZhVpRb63:@'ag^"m0iId*LooLR1j&.`Z:3Ph4VbsWP[k9%NOp'@mJrXiD4NrY@+T"nA&@!&X"BWmhMY`-/#76+I<frlPJLH6dnA9egV1LDTC858qaa5aO`-E'O"_:1qhqRe>$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#)slk<eq\EkN"3)$FGou)]k@4C&qn/$$89X=;]9UJehu#R]Hfnobu(f!`K(-Tkff(HQXtdlT)m*(_$;-cA4t.!/A<.)c*]7"$]h):iLD1eQ8SU:>I7s02<Fbrabd.i`saf&*C"Vlj`PeZhT/u%leo?I:<(a7)+l':W?ZhFrZ"V%BfTPpU#VJgJdg1+*O@u3Io\<I'uNN$O_#:?kLV728L7S?ul](d39'N#9PFj:IA_*o*i/di2%bQY,rQ#gNh=fibf"0cJmjeI`?T'nhrbD1$O6R,5*bDj1HJ+>Hk\6-5M,/.m?tH5[8^F#(?oqJ;lp?Hh0;'_]?1<N[$;afkFOk5s(&s:jL)I%eVooJ2c%k6D*8+p05kRV1&DaFmQLDK3!mNR!ih`p(kMhH?Y5D/U\h)_[k,oGQOY\T>qh(g=<S*]h-%"O&<6KFfj4VYjUO63f.qE#:hc0.,u3!!ViA<n^#SK,Dse>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+7Qj<BYP&SO`_E=JE4k=+*)4H9c:;\1C_^W2sXMRfWs&cJ3Bo@7Ph+E*7R?^r3-a63)q;ZY]$FmZi+qrnu-5:VE,3Asn+=A!4-Z#C*AW/P9glC1\d+e"c)F<loSGmWMLBV=puH4Dg*MGn+h&7Gugl]6.mrVDBuN3`+lML1d.M;`0)oR>GD'bT^Z.tH]518#9#3[>p($(4RbOUnfdhsR]5R:N1f4<A&@[S::4Wl\fB'(q*>qs!+`I%c%[0'47W+RmY]"%aO?,(-3XYrEL(YiY[>6p(JlTpH0#9=':IAt7/L05pK$Mu!=,Ql<n%m\Yn2Hm9XC%2&kNP!(2LLll_`oh`6JbF5(6O47+l=MRsIJ^/8@J66crA!`!<6sopYY;AW_IO5ZZ"cA#_1KT#0cQhW-AXa7Y`gL_Z>3hCNbeJl1Y^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$_hpD<OdV[F>B/_pYh'_LYSIRP^=gkG,]!h=:8l''l5CIK^u-?oR`Q_<aCdjs.'Wc3O#4k<Z)FmfW6W7t_.9>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.k<iiB'T24]Dp)]16:V4`e]DarC;2L!CA%hm^6"!bm_dl[T;s#/)OGfJ?i9U_Q/RUnaMZDfDe@/-TG)mr]orhN(S1R,Xb_0TNpMFG[Fg_;\mN&9VUU/o^_i`b#736jriZmM+o"?mM"S7f#l."l<@J\K-iX9k]InX~>endstream
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*<JK$O=T"uXK*(I/UitYNO0i+1'5KX.O/uit6I^I@=gu[B,&i^FlYo<ncX3P(IJX=6hh)Gg+?<MW#K]18[$H)gTbig6]5)Ee7N!U_AKoL7-W[jK/Kc6i`FWoi%TFDm6cbtl,b'S2!pr[2*$5m$VG>I@^1iNeU3qQPdi9G2>q)k+u+,6!,qc+V+r3Fp!;8f;br;8+"Cahh:1][tiB23\f7kB%\o/ICKH'RFZqY&*6qtjoGpRX@Y)rqOL_o_!<D_kBOIOeHpX/$f/1H12:YMt8?oB-q"r]7RiiRPdB,4cQ;LQD'/k$\go4r:G0MVf3hKpl.g]=9<YV-m"[*ocJQO2RSR65%+=LtlAe>h1aN?Nm(?'Mg_o#<PFZH_@C7p6E?Od+Sf'V/.JXK3E&IPY"R=K]).]n9+s+'h*7\U0621^kto'*c,BI6<Ilt)WpJ;%1,#a.1CdsWr!kN1cp2H-um*=+bhaKjaU\T't,hcB4fX(@qq5'S8rM2aPH!nT+SJ;\M<b^*i%bK&Im[c`[dh2o*V+<'l/Q*X*a*MIY;%a75+,,Us=Ir%l]FAcMt0\BbMWDrCL*;p=!^s,(%H$Smc/KL_JQo-XoMTa!c;CU@;RoIshCX3"51'[AQ61+A;\0?tc__P=IRS;?<QK//m(i1cB5,AtbQX]/@1e6\7hu7%(Z+WH24ul_-__;+=V47]<f;0mi91V1>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?HFNNRgd<mpRG-O8[RKs70o]R^6+]lXcB:]TmB=Y1!:?nWpJGn>jC,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!4ArTq<CJD5_77Z]a3pSLi"dV,9S'3/n!G*l-#+J?;2ij'$PiXC^Ful2Y21;2?,&mgW`c2>e2qY`!D$P<PT#7tZ78c._cq!>tQ^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<!A<KY__I[o,BU03LlM>%u<82MuTEpDWDS.9>qf4r)h9J3M"&eimJI<Bo"F7_<e=BDUfd.tV#uEWh`r7TF6-E"<h/]`c%Vf2X'gZ9,EnEXtquaY+&4=:5_gM&S8*X`Z\R.kftjn3PhtSs06pW$@8QM#<_$2SV`oM#88]'^LsmOa<`3!oQCOWB"PKP:I?R`Dg0=d/WXn_jN>XqmrIdp%.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=#(<JC:H5mH&kt[GZ_2*<XA@iKbZ*-BdgJ,K"<m/LhITuWJLQI;HPa,URt+E:K^"hZm`EYQe.E.P2UE%<lcgHDL2D;miP^IaMB+7f*G1U6^pV`_8Cb,2W7FG:[VON0/?IPHTreoi%lP3J4\Z=AqCZE9n'C\aehMki\#4NAUH%lbO%.Z!act'%YeG*!WV:urHGZ`XjnRD:TY)$^3&GlZf!DhCG7j($5F^%B#m0*ube#St^UZUOAP(HQ9TdeHUO'Hn]9'Z1@1_hBI1l!a$;2:"d_.eT="OdgrUK&jpZ)#ad,$=c5[LsilW74;eNo;Z]4kF/=;TeDa2C()I:r^VYIc,AGWH:I9PcZo@>/ugYK130hVB/=/\Kr>dkr8g7a#2"[3J@<mbFtM-<eG;nU-V:VSn3[gchDd<Tc]Ef/3rFM6-MJlS):&!LM>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^_<ek?FY*0/:"?'hiG%bCL6PEKV&Cn`\-C2NWm`u$[CM`Z?G`<V/F-@cB>p>mpfm.[Bbu4Ou@qZ\OI"/lAB+U#;4(t9p$.54F@msqBkiPFSfo9ZA2L_UWLIRE]_"WhKnI8(phR\dNX]['RAY1'ue&C5:-IJ;Z~>endstream
endobj endobj
xref xref
0 31 0 33
0000000000 65535 f 0000000000 65535 f
0000000061 00000 n 0000000061 00000 n
0000000135 00000 n 0000000135 00000 n
@@ -186,35 +196,37 @@ xref
0000001090 00000 n 0000001090 00000 n
0000001237 00000 n 0000001237 00000 n
0000001385 00000 n 0000001385 00000 n
0000001501 00000 n 0000001533 00000 n
0000001762 00000 n 0000001649 00000 n
0000001868 00000 n 0000001917 00000 n
0000001946 00000 n 0000002023 00000 n
0000002152 00000 n 0000002101 00000 n
0000002358 00000 n 0000002307 00000 n
0000002445 00000 n 0000002513 00000 n
0000002779 00000 n 0000002600 00000 n
0000002853 00000 n 0000002934 00000 n
0000002963 00000 n 0000003008 00000 n
0000003065 00000 n 0000003118 00000 n
0000003195 00000 n 0000003220 00000 n
0000003313 00000 n 0000003350 00000 n
0000003445 00000 n 0000003468 00000 n
0000003563 00000 n 0000003609 00000 n
0000003660 00000 n 0000003741 00000 n
0000003735 00000 n 0000003859 00000 n
0000004517 00000 n 0000003956 00000 n
0000008009 00000 n 0000004031 00000 n
0000004854 00000 n
0000008387 00000 n
trailer trailer
<< <<
/ID /ID
[<c0e727411e9547578b92662630584baf><c0e727411e9547578b92662630584baf>] [<4aefba77718541cc2646cdb6a66281c0><4aefba77718541cc2646cdb6a66281c0>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 18 0 R /Info 19 0 R
/Root 17 0 R /Root 18 0 R
/Size 31 /Size 33
>> >>
startxref startxref
10397 11533
%%EOF %%EOF

View File

@@ -1,6 +1,6 @@
# Android Cast — implementation roadmap # 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) - [libvpx / Opus / Speex](#libvpx-opus-speex)
- [Transports & protocols](#transports-protocols) - [Transports & protocols](#transports-protocols)
- [Receiver / sender UX](#receiver-sender-ux) - [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) - [Deferred / post-alpha](#deferred-post-alpha)
- [Related docs](#related-docs) - [Related docs](#related-docs)
<!-- /toc --> <!-- /toc -->
@@ -35,7 +37,7 @@ Last updated: 2026-05-23. Living document — refine as alpha and commercial tra
| Track | Goal | Status | | 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 | | **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`. **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 - [ ] Host `examples/ota/v0/` channel JSON + APK artifacts
- [ ] Deploy `examples/crash_reporter/backend/` (see `nginx.conf`, `README.md`) - [ ] Deploy `examples/crash_reporter/backend/` (see `nginx.conf`, `README.md`)
- [ ] Set `ota.channel.url` and crash upload URL in dev settings / `local.properties` - [ ] 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 | | `LibvpxVideoEncoderSink` (surface + buffer) | Done |
| libvpx decoder on receiver (`LibvpxCapableVideoDecoder`) | Done | | libvpx decoder on receiver (`LibvpxCapableVideoDecoder`) | Done |
| Surface → libvpx without MediaCodec passthrough on cast | Done (ImageReader) | | 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). See [ndk/README.md](../ndk/README.md).

View File

@@ -2,7 +2,7 @@
%“Œ‹ž ReportLab Generated PDF document (opensource) %“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj 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 endobj
2 0 obj 2 0 obj
@@ -17,78 +17,88 @@ endobj
endobj endobj
4 0 obj 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 endobj
5 0 obj 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 endobj
6 0 obj 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 endobj
7 0 obj 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 endobj
8 0 obj 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 endobj
9 0 obj 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 endobj
10 0 obj 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 endobj
11 0 obj 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 endobj
12 0 obj 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 endobj
13 0 obj 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 endobj
14 0 obj 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 endobj
15 0 obj 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 endobj
16 0 obj 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 endobj
17 0 obj 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 endobj
18 0 obj 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 /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 ] /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 >> /Rotate 0
/Trans << /Trans <<
@@ -96,44 +106,24 @@ endobj
>> /Type /Page >> /Type /Page
>> >>
endobj endobj
19 0 obj 21 0 obj
<< <<
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font /BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
>> >>
endobj endobj
20 0 obj 22 0 obj
<< <<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font /BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
>> >>
endobj endobj
21 0 obj 23 0 obj
<< <<
/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font /BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font
>> >>
endobj 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 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 ] /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans << >> /Rotate 0 /Trans <<
@@ -143,121 +133,168 @@ endobj
endobj endobj
25 0 obj 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 endobj
26 0 obj 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\)) /Contents 50 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
/Subject (\(unspecified\)) /Title (Android Cast \204 implementation roadmap) /Trapped /False /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>> >>
endobj endobj
27 0 obj 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 endobj
28 0 obj 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 /Outlines 30 0 R /PageMode /UseNone /Pages 46 0 R /Type /Catalog
/Title (Alpha definition)
>> >>
endobj endobj
29 0 obj 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 endobj
30 0 obj 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 endobj
31 0 obj 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 endobj
32 0 obj 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 endobj
33 0 obj 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 endobj
34 0 obj 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 endobj
35 0 obj 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 endobj
36 0 obj 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 endobj
37 0 obj 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 endobj
38 0 obj 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 endobj
39 0 obj 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 endobj
40 0 obj 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 endobj
41 0 obj 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 endobj
42 0 obj 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<CUDQfq]d+_nDgUoCA1&$V=uls##d#C8Y8/6?5a](7(t&ihK.>>*cX=hQKt6A8OI)sYV5PrO\ZA%hQBb,a62C]3O)Y$[I#%UD*R>ma"OST-MFeEn34p:VqhT-_8PSYi_=`Egk`<,&@<sdJ>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/PY<hb2^jLbYJT6,c@ZWaT]ICuq=2goo,+.q)?r&P[DC#t*dG6Lq+2g=Q'Bj[jIEL3(+ih6MVM=q5SQJ&p!-h]<iCT!V$7S.G+HtZ39>S\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#k<q3$9"s&(?oR6lsr'#l~>endstream
endobj endobj
43 0 obj 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.N<E6&/+=Q9Mc$gqGm=tX9ILWTZJ%_k8!-1O?X/`".jG>AhQr0&=W[qbWAY=<!L3h/AO"r;L036dn!G&W#>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]iQSS0P<Uif'ql<Xo0?=C:dY)Ogb*L@I:-N3lLdRWEpjYJ\FH,C00G`/`L^q1"V:(V]g\6VN#.8&d\p(SHcS"[/]:;KI3K_VlsNKSs#u,2)+7nR"!KRl%K#*=ho</5;3Q>D3WcVL4pe[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`WZVr8bOBET<Wl52?`)Qm%#gi-\8sN#CHt-`&n>Z*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$<rVdjeS=W!2kSA3J",=Yc>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<OSm/GHeSM*AIiok',m`U2Y\B?q?S6aDogmdk<YHMdJ8qJ2#sN\n%f>+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'2r<CB!T6O#bM>WCIrQUX)'jLWghe$9hY9_cacW=U<T/56Rd*9&7h:4_)iFA2Zqpb\!^WH*$uR"%$!aSe[Ve88KR<IAaa?k\)ZEYkF_V_a5\#@B]8"2Co6<,ct5c8;30Wet4)-R[A6]H+\@eF),]K`8a.*][p,dkMn"[+BOQS?/m6JnCG'mLAuQfCjiXR2d5HFf0/Jg]t^q:Q&H5R?FJ'0%g;B30[3?s%_jS8\Bn.&kLoO&4LZ+GfuflQqZ('U:urfkM6prike[+Omrjf$&]jPaMoiF^BO]b;4j.TJ>u0&C%?KFr@"S,Ad-FgsdeUb6NUFZ$n<me!_?rmFN5n%6`p4l9LrXAT*u29O3D=+C$]_<=Zj/P#F&$0ILe$b;.>ateDLdkGM__57E4QrE;0N=S$,HpAs+kiGS5,A#/1#96Yn8QLn8PMU_+>]'lg1t[.O\f/T$sTI1$d2-+%Lbh_$1\m'pjZ_K<f+_n3.1XfGq29$]$Pn9&=(7&7ST'=+U^Ic`;cr*D<MVVd;q)\'V'W4,p8g'BI<rG'^UfQ9lNf/N:ghj`F!u3/[l6F:khT!eAEmcp[78\So,0TN1)"g/Ju?_bE.LpZ+3mUbFh,.A!'h_5,&3:aPH5"t[Y`[rHggc'4lD+e3lj9r&E02D0$AbW8nieF;t_k*A*;H#u"(6fCH"IaPkl[6IRubCcbO7DgA3";^b8Yp,VN4PPm[7I<Lf9rH9eWr)'kC&3e!0gFtK9ib:])Pji)*-fbX"+Th/*GhkMYBZOR:&?\NOF@04>6&UHkp!9uAK(W`r^]ce&l#j=VZ5ST0=*<+*.-3:8>E$LK;V_69@-eA1kh:@'nlI^fPq.P(p#jQ'>+7L7^c'kbCug"<pM7HRAqNF%`r7)!>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<=<SY6,(mRp&IZbK@gqb=Uf-WjApoc[S*pnQZ(ag@QWPM'0pA`\ShVUrUF1-]e8=q3Ci8W`VYf_)pA1ZBXVk-Sp.g@c)hS^.HeEV]1*M`O2K$BJ42RFeqG1TETE,S/7MXb(Z84''0XFJ/LIq<+?kqLn+QrmQH?SjZ'aF)<,(*lBsfuK<hgiGURr3dPDOG,T?)'H4*bCG(Tm>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 endobj
44 0 obj 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+'<?N2u)EmJO<b:la`_P;"+S5>+.!q&,=[]&b?0Rc`t`]G;0":`p^IFchOi/-nDTrMu"$;a*3ko3Q(iJs*m6Hg2]j:,hW&8IQA-!G`G`L_IBW$#=`q_<?&]F?DPj*p3e[/NBQk/bHeBZ(k4*MLmpWtBL1/O]rd,@m]7V>>#L8/,JQT=&`gGhTseosZ+er>7Z70KWbb'PFRO*bHGaX8F"Jpo!=",,]Gli<JK'@`)#B9;IY>Q,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)rI<fnIsR2F/J?V/@Jta9+#.*KT878G4HqKXR8!;8HGOM)(+6Ra(8pOPc?<hWA4#K3"-Z2!VZIgaKOF&]JhgdI7_j2`-SLKMMY4SQ>4(#_)$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+<Nh<%J(s5el[T-bn3P%HDe,3m#>(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`##<k=-_oP&fWYi8u3ELiQc/i;n/[(l"7Y`!mM@lOn9QB*iq`Dap??q+)59/_3\r-&ZX[[7_`NaNirSP&[Oo.(FB]0q3.20;!`BBdM4m9Y&`/]eeXO0qY&J/Otn\7AYM`)BhnM9:+=,\g2%h!ta%04DNk/r0j''G&EUVgN:gprKS1i&?Y5@H:b";.:*cG`&8IEHfOhf[^Y_Y+X7YUJk8:P3X5AZKWji"3X[-Q"GZ7k/<3NppO^jha*Z"uil]<sR5oQ2#KJgbc=9ig;kGS+.&K0s]-\0s4n=7aDP=I"?H?.<o7%Im8klh*ejWH;`'.p/k4F;h52n2OF$RYu">'tr\D7i=)riNQA:j7:9rHOp?k"cHaq_PNFGg3U3.l!1!?+qHNg.XcBDakj@-H9SMi>,??=4On9>AfRS"@qhi1RZBkED5p?3W6IhH/m)S6'5-).$?QQ(*d;V<uJlS<mcUe96QVC>4,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=,?<prbF+rLUPRC>*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%EZ<o3Q0fbBSAOEop&(N8g&g0_<r`2:UET/'b$%>29q9Iq^31`^3@Y&G(1#,/%W7)3;I_;cm'5El?B)H<DGge*(cZ9GSSa4,<S]@d;B7)r14^JSE^&RN7g9:,a[edY>l&>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 endobj
45 0 obj 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 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+!CP<t1,-tj&7,GQ>Q9"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[<k5XUhOCk9(e_F!_7agB=`<.0-.`m7Z<gSna`CHE1(e/V_\^feN@[Vercea`Ejl;(h#KL%t*.8:YljFR&aKp\[K"Un,kWbc1Z]WN\4pH)Jcd/Tg/"mudMN9e""<i[!DE*)HU5:f*<;%28R_fdM+'1?<<n?7k`lJlo)F(8C?1Lj`Iib,4VRBa^1A_:Tf<g?N`D(MfCCD^(UU8Z+\O5]L\fKZ-QnMnYn*mu6t=qj7;cU8f,=Y.g%f3$=5==M?M=RXgcp'.)mA5*+%>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<F]:"Ml"0Zif/J:s>[j\\Fo?%nN^I@it3-(!&9E=0YUBU`M,U</r6p4I^9Dp2k>GPBC%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$"qB<kC@@n4c90!@Ln&55hM&Y43nB]*',]p!P+>RK+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`bKPE<G%!hl3Ha>Vrs7jRieL_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/PP<q^:t=j8Qk$%J\oU'%0i=H%:sntS,';AgUol_6E4#G'U\>UQ%MmXcb?oCZWl5r@.0P15g593ntOjunW1o1Xn?-`)_e7rh[q5dKd^]oOgX9^d!a8XGH6AaQse:sNd-;TgCSV@na(nod>^YDT6h+?^L7$[)u(6I.36(<BtUOP_,gnugd'nR)?HGi,t4sPcnnMYq*DCgIS'aW+50F#R+XlifrBNRL<jX+C^asOcjD,+MA97CWLU1R^d/;7W2c<PMrP&:Ue&9m6^?7-^hJZcPQeLC%_eeh3636j$DIkj5sjnTqDVBqIUWmhb_\kW/Ad,+A17G;?]p)JQVtMjmtm]kNZoN)ScifAY=5#u.i="Y2J8%IBcKE93F:a$-Res^Fh:PT`"+jo(:4bIc2p/]qZY7?f@a3)[ZD\Tm;V-R*jn\g?+8K^P#UGA,1"aK,?Kj0Q[(&UBP!BK=;.3l7s^b<CsO9k.NQX!8TqK(&`%hm)e([;5TR?Th'XF/>F_+IDpT<[,qA!RA/:o6*bpMFLW>n]DD)hX5';1h&#agU51Q8KGbsY\:%Da2`*^/qp(gf4>J)@0'RCcugdA4cCb]6Wj8rS$>Q,D0rp4.moYWAn_%<)rLAZ"IB<et(;3SRBFSN);*r&t9>!53\`Z>$C&iUkOb\!I[41iuWEGbPAbb>n:bXZ&WFiPYKd>^=(AIVF*4(4j,IW-R&$p#'":(Mc;RF=N)%58G5\>VSBcDIr$kO[3;R"<K\ehTtZZruqeF8pFT(hH,+6f5F4nR:mRfksXGnKtC:_V?clOaaW!nnG:S-G0Ull`7Z7YrSY4(/kon%.b5k=diqqk4^t:rTbI_&2CX#;#EKToFB@MCQ`1N=Z4&C)eNL\TDtKIpmTOiEY@JK0L=e=n0r(e]T[-HVl2mXUrXXG_Xo!O..M&ZA?7e>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=icZY<PrN!bg*>ZsbasX_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"<r[uT+$&M;5l+@W\WgOh"#Alf)#_K=-!V!5'*GJpICu5IeId(G(uJK@Lrs=hDn5X!AVGj=F_+[QoeNRK%-YE2un5*n`Il#7Ibm3=+csl]k)!-\[VRGRd\u[`O]3J&ol;hk:dZd^n\]3Pn!De1S'K-mO#0e",tBO&`/HYS^6;c;FGI%is`iDa'i^tYs(K+$85\W\qSIrU:Xk=oP3_RW%I^8l\0-?#OF#!`_q_W/:QN2BmF]1kW@egJ12>)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<h<4nc\ou^U*aHj$F7iH,.40UG.Ck>-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]r<C(V(\85E_7blk,MfTAh&jm"AO!`@N`am'T<L2P=7AZiV<W*%8V0\i3p+n7]FH!7`,,M&%;Z'4Y^s_Vhq]i6m`j!r%H8sLm9.s%Z9@/Ms"Q?)e4^o6EB*IPXbp=IJ.h[P+*eO>B/SDW3S:fgMN(!9/EWL8(YZ,1#1c*@JIKl0kICRl1MF#*lRIm4e&;S-Sa8:61dg,fr%hI6;/)>hjnlFri4%4E8F"`/[!F[^<OT6Y)J;k`BI5ukVRgYU>'6qoAbD]=Z^6oIL3k9U):R7dGGVYa?^Q*2MY"&#PL\lnX$eHlhg#n]'XYtkak;Gb3m`fpqnJQ]=1'_&KqMK/N5,9K1#i>&L+6RctV@$/\qqbd-D@5\bg<j)Z`riOj,k-B!3XBZ.cG6Af<J/&&6KmZ6]=^[Z\)%0"S</`rZc3'TNUI[2&mqI70$5/NGhq.X::$<XBlG=dO%M4aFa\/Ps7jE-Dc8AS\[\1*PK1Qfp_9.sQg5Afq#Y/hfDVa7DXR@&GGFC>o"asXh6l0@?L*I"UE9"1]=4OcXA+$;51oS[2LS:LZsejr'r53!^5>49CiBW*dJitB37:h8)pSXA?s)_RHu8j:B=(u-]<u9[b-TtB@;\)R0C/b22`sfEZPE6ne)`S]iiB8BLtWsdk]j"8=rP>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(pjZ65<r:#fX*4DYB;o&f525^`=9!/>i;JK/pTLS%%)Ef3(Q/%Y-K@*:o+6#R.@b-<RaOqo)XXFZ(PI",a&':b#3Di'X3BdefjmA5H.1K^sWeeDM]RGVaQM;Q\OCu)6Hb>>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<<a-dQZ(Z'&&2G)N4ODfjhJ>%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+Jaf<uZUED[\h0"oXHGX$mVaBXVf^f![2*As"O[GdXrq<UXZQ?dt<;<S.[C%]SO+LQ%2'hR]I<3OrWub.TklU;c[SVggRL8?45X)`Hi@A0H>P7i([C*5;obd$i/hL$'J%@a+d[8ZGQk+:2r.M@@8)q[VoKLEd(<'Kr[GKUsRnAM!*Ion!K%j(-\Q/i$%!IN<XTrQhup*J$,:6gR[*i-:38i!JQIG?T(oFQRUH_DJGAcG/o,pB.NdRLRGjm,+9c#FYflE<t!$\d4fIo^PBhNn"%"+s#6AhUg628<H]/d<0:+YEf8-;qD6CC,pfGCBO@D*5DQb2Lnke!CGKJjf,)Q(Wd;=cY0muBm.\J)jj&&Xk>P"9&]RK`6_%AYgjqr`l'4+3(WB##+FXqk'7LAbkVZ8-b(;4=XU,<HL)[FWOV+4X/cM*+*lj;S%@<ShcMJOBP@osqh"4$]c>?$8]+B*,\.K68US2m-a,nrc5[.ALq)/ig`I=ca)2f!4(gTJ</FeEg.9c#RW3!tS'.Hu(5'+(5,I69F.-k[\)A9%marC'q_'C"b3*a#\HI[W&tUGocVAT:5H]3oH]?7U6Xn_%ajZs@H5C5I[6=&A\M3,R-pecED'QhL4G&ts+7YNNHOL"G,GnD`6MMLV.0HPeb@p[EK[jqoaL1<iC;(Ap3%i,]!MU$.mA%ChnhVAoN6`c(236$A0uB;Mp-e(fZd?=RE*m4K++?2f"ne.nYn;Ra8Dsg_M\u2,#$ml6\84QGScfp/570e,bNQgk="[Qo8+mf&2#NoMia'%B+5j#S`F#"Zj\_[=I>mmj2^N&R!?*Q7cq.aE*]EALT].;Y]c-LA&eHW1$L1B#!57=rTSW3hY[hd+kTRCVMW//:U0V5G$\RDQ?(0UoVIb?k`eYG<VFB3u'\0XO6K!b4<<q%Lkl(bCHn8X?2]pkji1#eh$i&kZqil#Xm`>r\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`a&#2H&:Ce%PS@QCBXG@[Hf,PCI12&%Mh@V_>P\V67kW9qtIM\bXT7r?]@9Fm-KChRQHJm&fpdI<0Y]]WeXh`N</#YY_#JQ,cVS3c(K"7Bqo(n@Vbi;A7C\f'p\+5irNU@$k/lBV,/2MP%>`kQ40-%I(!/%o+ot]CmW9oJD<g=ch9Ls3d&Ti?>(r<JsMjSP'UtkhMUQA5p!TnU-m4]-U'hijP:rFr#_3?Re(0k@%=aY&dYC>,=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<QXhc!GKgW:(U[V6>&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&_\T<q'3jV0N]'c5On,J']5@fV):tVD?Z3#ASED5fW2L8]t1^5\`o].c5!Jc/8UBMqL+a*4/#keaiidY\A#MblZ@#DlZH<N=iA@0W@#,h&HlLdmI8mVH:h!K$AUI,J<Tam@S'qGU-aN(bgtL,"Y_YQ"9&a:>ptd/9#6cU&79RoIhM9OLF(hGpn3ZG5_beheT&r<`S2i-5J3!mC2_=8_`3JL5)o=nEt<gn7sdmjUme8af-co/kkhkQ?hH:85JEY\&o#.BYdtOBN3lt*r"B84F\$U)SuVLF`?2a<LEV<K8ptnCHjfQk_EV?Ar#hfOU/HmP_VU9,jUm/%_ua'?]2V[^$N`B`">/%"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><r9E9oZN^m@]6u7S6Jh2=tJ$kVPKV:AX_&g?FGTJ%b2j^QpiVD7%jCUg0D2BG:OS_qYJ_(h=XRi'e]"?/L9UX'+3rCZNb=pVf6a_7L)%K:Y4Cs%1^MjM%Cn'*`hE*S-$X@og?Qbf)DMpOt!EHSM.umq5t)gZl^a1jH?pp!LPaLX0FH.OX"#or]Fj$QJKUT/tXD3nhcuOjV#j!DGBDBL5'&0OeiV8i!t;APQSEX!@-CnKB=If'2&b&7\+j_NHC.h=R%LY/SkS;NC;$IABp3$,J5!;3>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;*ic<Q$9i%'\.o??I&2+QQXk"IsNTPE5B_ubEL2!*Q>5\`G],^R#To28_<gLn^1LH4Z\#H#Yc2R>;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>::<fUhpko2d<CJ@0]fnhbFd0bplU1N++bJpc&\L_?du>/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;T<S-!>Lnk*HAUio*BurgGH+GB;9I(Q]c6q&0Pp-_pJS)=s?4DM0%,WcPfu-qMfepnoLWD94:)#rX^`,Ee.ioJf\Z<RcSXJ=uS5)a7[1,eH4HA..p@!ssf5:S8AAnUSnR4\"5?=42FOL?S>>p2tY!%$ltQ_*<B;o0<!IDpdhjD#tXQ,7?Zl;EqIdLRmU8'=.E][e4k>fl&N9,,b!H/k&Y(OuW7iM-RMWZ6hk&X:q[bLYk=73*k$E76ot\l.6kL6*H.4GiY_;FO1H1aR.'_cS+j#rpY7uru*:\d>MHe<K>n$?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/U<u!@G/*jTj-)Ej.DJNj?PLdqQ-AbS^P8FIBBmiklMb.Us&[ssU?,1\#i@#1/XIJBtJT%+@i<[6h%&e$23/j.M[>5]@+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=t<pJq2/mr@ld.dbC;ND!/&Co+]a_JEPi3['SAb*GoL1Ta,&[GeaeQno5L9>JS"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<ECmDe&el5BFi)!RrAPM/@8ZYB&Z7P&>/aN<klN+WN[!(ucg+"aI;IR>O&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&)P2V<C1bE2sI<T)RPEj98\8X!jeHarkcpfm*]"[=+^I3FCNkg^/,(5.]QQNSJqM>H$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",;<Lo`=,h%8Ssg>""KklQ^le2%(j3%bq5\sV-L)d&j?&q?4MW2,;n?U-e'Q0o8,t4u*.>U@Lm=<.I&\PDDlj<LAM;b`5+Z=]*)Lqk)f2P^f4mDs4RdT(:P2J!kART6_I'A7(pB*X&2$^uCn)d,,gRa(eY-FE<,sXIb[5.<qq,nOEbKU0%3J't"$_YFQ2h#E/Jn:tL`cadK#BI5D_D\aA_0M\R,)n.UOja6r.%sjqHf2CUMW:PUR=M"VtH;>/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<T#_c/kI:3JJ](#/_U,$]6@NJbYlGUPV/[..#D\s=H3J.V^?j^Ek)C"lJMM*Nl63U798[;G)f5nANrY)HY8HND<W+Q\hV-g#acO8`iV('ofepCEXk#cD!QW[JR].L98<1J:O_lJ(<`](L.g@T5p]7,F)_V>`!^/+iBk+C-uLcuJk0m3~>endstream
endobj endobj
xref xref
0 46 0 52
0000000000 65535 f 0000000000 65535 f
0000000061 00000 n 0000000061 00000 n
0000000146 00000 n 0000000146 00000 n
@@ -275,45 +312,51 @@ xref
0000001841 00000 n 0000001841 00000 n
0000001989 00000 n 0000001989 00000 n
0000002137 00000 n 0000002137 00000 n
0000002286 00000 n 0000002285 00000 n
0000002402 00000 n 0000002433 00000 n
0000002708 00000 n 0000002582 00000 n
0000002786 00000 n 0000002698 00000 n
0000002892 00000 n 0000003018 00000 n
0000002976 00000 n 0000003096 00000 n
0000003182 00000 n 0000003202 00000 n
0000003388 00000 n 0000003286 00000 n
0000003594 00000 n 0000003492 00000 n
0000003681 00000 n 0000003698 00000 n
0000003996 00000 n 0000003904 00000 n
0000004071 00000 n 0000004110 00000 n
0000004208 00000 n 0000004197 00000 n
0000004309 00000 n 0000004512 00000 n
0000004414 00000 n 0000004587 00000 n
0000004528 00000 n 0000004724 00000 n
0000004683 00000 n 0000004825 00000 n
0000004808 00000 n 0000004930 00000 n
0000004925 00000 n 0000005058 00000 n
0000005052 00000 n 0000005175 00000 n
0000005168 00000 n 0000005289 00000 n
0000005285 00000 n 0000005444 00000 n
0000005400 00000 n 0000005569 00000 n
0000005516 00000 n 0000005686 00000 n
0000005610 00000 n 0000005813 00000 n
0000005692 00000 n 0000005929 00000 n
0000006604 00000 n 0000006046 00000 n
0000010425 00000 n 0000006161 00000 n
0000013534 00000 n 0000006277 00000 n
0000006371 00000 n
0000006460 00000 n
0000007422 00000 n
0000011569 00000 n
0000014910 00000 n
0000018083 00000 n
trailer trailer
<< <<
/ID /ID
[<604792abc998f7bf7e113332cf4628d4><604792abc998f7bf7e113332cf4628d4>] [<b10a0e61bbf4903077fd80a61c513273><b10a0e61bbf4903077fd80a61c513273>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 26 0 R /Info 29 0 R
/Root 25 0 R /Root 28 0 R
/Size 46 /Size 52
>> >>
startxref startxref
15138 18384
%%EOF %%EOF

View File

@@ -13,6 +13,7 @@
- [Attachments (deploy)](#attachments-deploy) - [Attachments (deploy)](#attachments-deploy)
- [Google Workspace — phase 2 (not implemented)](#google-workspace-phase-2-not-implemented) - [Google Workspace — phase 2 (not implemented)](#google-workspace-phase-2-not-implemented)
- [Agent / engineering queue (from your list)](#agent-engineering-queue-from-your-list) - [Agent / engineering queue (from your list)](#agent-engineering-queue-from-your-list)
- [Open tasks dependency graph](#open-tasks-dependency-graph)
<!-- /toc --> <!-- /toc -->
**Documentation index:** [README.md](README.md) **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. 5. **NDK** — Only if crash console shows native reports for these sessions.
Use ticket **comments** for findings; set **workflow** + **assignees** when you hand off. 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)

View File

@@ -2,7 +2,7 @@
%“Œ‹ž ReportLab Generated PDF document (opensource) %“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj 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 endobj
2 0 obj 2 0 obj
@@ -17,7 +17,7 @@ endobj
endobj endobj
4 0 obj 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 endobj
5 0 obj 5 0 obj
@@ -27,37 +27,42 @@ endobj
endobj endobj
6 0 obj 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 endobj
7 0 obj 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 endobj
8 0 obj 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 endobj
9 0 obj 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 endobj
10 0 obj 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 endobj
11 0 obj 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 endobj
12 0 obj 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 ] /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 >> /Rotate 0
/Trans << /Trans <<
@@ -65,24 +70,14 @@ endobj
>> /Type /Page >> /Type /Page
>> >>
endobj endobj
13 0 obj 14 0 obj
<< <<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font /BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
>> >>
endobj 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 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 ] /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans << >> /Rotate 0 /Trans <<
@@ -92,79 +87,94 @@ endobj
endobj endobj
16 0 obj 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 endobj
17 0 obj 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\)) /Outlines 19 0 R /PageMode /UseNone /Pages 27 0 R /Type /Catalog
/Subject (\(unspecified\)) /Title (Tickets, Gitea, and alpha smoke delegation) /Trapped /False
>> >>
endobj endobj
18 0 obj 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 endobj
19 0 obj 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 endobj
20 0 obj 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 endobj
21 0 obj 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 /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\))
/Prev 20 0 R /Title (Console features \(implementation\))
>> >>
endobj endobj
22 0 obj 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 endobj
23 0 obj 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 endobj
24 0 obj 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 endobj
25 0 obj 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 endobj
26 0 obj 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:<7<n1>EO(BWH,fuDSW.%=d=KO"%grgasL:\87:D;oD!a[3>?jgB_Te%J;Y,S.0[j:SHB<m#jDe'V1YZHh6%/HM%]63&B?NqK2II%E16P.$[;Xs(e&nD!/W]TKbTtbJ7m;9Ql+6Y6SP1G%Dc-c@$D3DYrKVr1l#M>ZDD[:*3PQjiZ*Ws6'V%/J?@$n67to6#cEVFKCfc(:[iZ0"9e^\cS=X>')i6_I6*66*.AEl"2["L[,jedr:\/?%&])a+T6W^m4,f]s7Kkaq*)M=<W7UC*tB>*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##?B<NXO-%25^MrGn)pAfYW#!:pnfT#g,^Ue7>oO`jK^D_\edka>)mB.$In4IBpQ1CR=lGf0qdu9!^e_[*:21<Gq[,",?9"O!t1%p*_9Qb/S1]!!Db5V6HnE$D(BD.C7nPWo`F/5U'^d25Da>I[gPCHpqh['GDC.*r,SV-D,drGf9h$.YEp#rd/m,_DC8BXUZAat/@^CQ?rqJf)RKMSBJ`b]d+Q8mQ5nZs#(HXr8XHF~>endstream
endobj endobj
27 0 obj 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.<VnMb)1?sJ0U(G68SJa\X#W<Thbk7(r5[e!5:ma$Me8Z%,POLiI&I"5TM`%o*7nhP_P\!.K!6;O0fDp6?mIM[P\iD!=Q_YS,bkl$Y(41M&8^3!W56mp]~>endstream
endobj
29 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3969 /Filter [ /ASCII85Decode /FlateDecode ] /Length 3969
>> >>
stream 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'9<MK1'M=RR94;IO(WZu/8EWj9XO)R'+@2J3$ReVK'LcP(;8A$\S;uM0l\DNC;c%&V>kSQAQI'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]'<e/*oO]6_<lLCJ2F16%s0Z^2sO>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!#<O:>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!<pR'(]a.XSb,JnN>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,<bCOu5RE3b*RO!dU@,9=pU:Z_+4%QVNll)=k?_M_hE'X^O\qVeY!jN'40)bo/`D8Ka[q]tLZhYS0&i<GVZ[>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/@"0sEZI<NX!82F@GmWLiE#Pcn5\.Fg#V;?O8\]_sN#h(H;<ok`B\j9[9tWRN!c&\7u=Q-i<+'g>n&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!G<Z0KAnu]$#^C.;?<rj$4b-<G<K1Vj!c%-<O6KPcq6r3[b.IqVZfAWn\*h:'n;e:3HV0GKPbb+Q1"-a&htq8q<d+OD/hij[\NB-Z\93G#:U;,l'T*Uq+aMd;Q0n:;+gNA07-%+0nQ(l\P+Y$^#E].c'?PD&caGQr9_<eB-=PI5(M\B[iLMnAO=9649h)K/G]KgsJ=u!)4U%r8C`-.7F_'H95k`u&O2ojf&MX>4X]U1&m^7rNGPLq.89(kNQl9^gIXH2>fR?aBiq4)LM8oVPas"/VT^I30egAtFF:](@\ip7XeI]OYq@GcT6\(J<,OeYBfK1)-gN<K$f*:04*!!'E)h=i,!:R"?b;Fg0g`(r<e\bE5N8<9]`-CsZ*S1`FpB*-V4d>!AF`#8<fN)6_mlmI&+A.T%_W5!l$aIqrY7^@>8A+08>N%t$%B.p0:*G`Fc-0MOU+/uDD<lqRShb',f`22EY1%g'W]R_Z0Q@o2#TL.$jI&:["I[n'$E/an)7;V1U"hrN[SpCZBEq+/#PUr_6hY#kiVaVbOSpDPGfL1r#LJ0MI<5]QLRd4EZ7QeujYe.\<$mgC"_YrGH+j3's5&Vj]p&<+aR,_f8a65r@N%&.gai0ML2aj*e[NA1h`%<uL#O(k)4(Nn4->e4cI?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<QTC-ohgS>(.M8oJdi(EFY(roMTWi%JS8H*OMRMuNN!V@+HK8X`QWL;]n*^YJ5$*\6D@nhjXd,[UUpP+e97'V.XjI_hu<mf!O?B-R3@A1kWC..c6lO['n@rZ$a<f4>(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#<A][SK_V<mffls)ZAce.F1Z*33(N@q*#'Y.t'06p-ncn#4Ik@\XW;:mUrf_HjM<ek\B-7iC.-)`&T-$WBX]F;Fu(HrO>j4@ZWRM2d)i\2f9^,BnDFnN<Csi[Tb#9_2u9G;`\.pr'<s$?<sB6NFP83tD>DFu6m"[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&$8<MRN@-9?Gl]ljOCUd&M4aQP20=<;m^iNH;83#8>a0G0&8WDKI4!:j^a`k<c@e=n"?f(bPAe!fl8p'cN2QP7I2"Y-A6cBmoLO7C36+$^"H?:9Ko#!Oif1aUIfO/m/q]jJ`79mc2rPON(A5QGB,kZck-]i-)O^UeBN"?NM+h0U@+W?QG?>k[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 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'9<MK1'M=RR94;IO(WZu/8EWj9XO)R'+@2J3$ReVK'LcP(;8A$\S;uM0l\DNC;c%&V>kSQAQI'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]'<e/*oO]6_<lLCJ2F16%s0Z^2sO>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!#<O:>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!<pR'(]a.XSb,JnN>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,<bCOu5RE3b*RO!dU@,9=pU:Z_+4%QVNll)=k?_M_hE'X^O\qVeY!jN'40)bo/`D8Ka[q]tLZhYS0&i<GVZ[>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/@"0sEZI<NX!82F@GmWLiE#Pcn5\.Fg#V;?O8\]_sN#h(H;<ok`B\j9[9tWRN!c&\7u=Q-i<+'g>n&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!G<Z0KAnu]$#^C.;?<rj$4b-<G<K1Vj!c%-<O6KPcq6r3[b.IqVZfAWn\*h:'n;e:3HV0GKPbb+Q1"-a&htq8q<d+OD/hij[\NB-Z\93G#:U;,l'T*Uq+aMd;Q0n:;+gNA07-%+0nQ(l\P+Y$^#E].c'?PD&caGQr9_<eB-=PI5(M\B[iLMnAO=9649h)K/G]KgsJ=u!)4U%r8C`-.7F_'H95k`u&O2ojf&MX>4X]U1&m^7rNGPLq.89(kNQl9^gIXH2>fR?aBiq4)LM8oVPas"/VT^I30egAtFF:](@\ip7XeI]OYq@GcT6\(J<,OeYBfK1)-gN<K$f*:04*!!'E)h=i,!:R"?b;Fg0g`(r<e\bE5N8<9]`-CsZ*S1`FpB*-V4d>!AF`#8<fN)6_mlmI&+A.T%_W5!l$aIqrY7^@>8A+08>N%t$%B.p0:*G`Fc-0MOU+/uDD<lqRShb',f`22EY1%g'W]R_Z0Q@o2#TL.$jI&:["I[n'$E/an)7;V1U"hrN[SpCZBEq+/#PUr_6hY#kiVaVbOSpDPGfL1r#LJ0MI<5]QLRd4EZ7QeujYe.\<$mgC"_YrGH+j3's5&Vj]p&<+aR,_f8a65r@N%&.gai0ML2aj*e[NA1h`%<uL#O(k)4(Nn4->e4cI?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<QTC-ohgS>(.M8oJdi(EFY(roMTWi%JS8H*OMRMuNN!V@+HK8X`QWL;]n*^YJ5$*\6D@nhjXd,[UUpP+e97'V.XjI_hu<mf!O?B-R3@A1kWC..c6lO['n@rZ$a<f4>(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#<A][SK_V<mffls)ZAce.F1Z*33(N@q*#'Y.t'06p-ncn#4Ik@\XW;:mUrf_HjM<ek\B-7iC.-)`&T-$WBX]F;Fu(HrO>j4@ZWRM2d)i\2f9^,BnDFnN<Csi[Tb#9_2u9G;`\.pr'<s$?<sB6NFP83tD>DFu6m"[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&$8<MRN@-9?Gl]ljOCUd&M4aQP20=<;m^iNH;83#8>a0G0&8WDKI4!:j^a`k<c@e=n"?f(bPAe!fl8p'cN2QP7I2"Y-A6cBmoLO7C36+$^"H?:9Ko#!Oif1aUIfO/m/q]jJ`79mc2rPON(A5QGB,kZck-]i-)O^UeBN"?NM+h0U@+W?QG?>k[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 endobj
28 0 obj 30 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 735 /Filter [ /ASCII85Decode /FlateDecode ] /Length 921
>> >>
stream 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_=7<HqTKkNl:8+GheorN/'-bdqO:k\r8Y1_ji.3CC,Q&Kd[;MKBoK5T6J[+Ck:HU+RLl`ksB).X)rojL)Rq7QWpd_CDr+'3PR[:61Pi6QQY985G&7M7/cZ=69#O@Lgg)8G%_[6V2.=H*J2Kfe2nGI@QqZgmcJZdpTu!-;)H6FWiaC3P[KK;?5\`$e<Z[@XcZfOar)2LdRm=!Gj`=":eP0hpRiBo]57F19sdVNAtP-^iAG&H9');GBgk,1k"V^8;H%.G?<Vg_9.;gCSO"3uEg=B.H<Hm1$<%I%>WpOEZ;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\o<l[S2!.DCHo8KSRqsCRNIB#$ZeeA<h-;?GZ/Ai0CQ!=,3o%(j,nIc4o1^$?oFE=qNLT~>endstream Gat=)gJ6Kg&:N^l7cG^.E,<Q2-r6uI"XPEC(o\$:dFK@GMR.)A2i7,"SM^t83"]k8aVW4Gc8fMiG/0uuC$s(Oi("43)[@?!iX?gQnfT!jFaWh-jLfhc^(joJ[Kg4X>'BA_UGt#-TX-i*E?Lj(l^Hc"NSAhnNR\6$6RU$=CZkij]]=mSj>jluEK9PDaeY<?9$AS\V:*[;-P6<3pmV<SMlpn^!n["P"!N:_in3BRCoCWeL>l=^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($<be<#J9"f7X:B-8q&@];0A6C1iC]^aOR4sLS8GE':@i"`JrbW.4I?-m@m0SmS`FL_olcT/Xe]f+oBiQ:Y<"cV)i*DGpaWOYF^_=UujF*^)?p#=)NC<'8;+6W@S_EKYqSm=SUI,`i_mpa?gcuY6:B:`[V:`%W=-i5=>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&2cXm82m<D]@N#\'\']<gE&W6UMsmejX]`4?/Do;1a4IQl,pW,i(8UK]H8LVVa2-g.k<h):h%**Gno00V\:FSB&IPW)(*#K<]5*[ja&F8]c'<2,Jtmf2:)CP~>endstream
endobj endobj
xref xref
0 29 0 31
0000000000 65535 f 0000000000 65535 f
0000000061 00000 n 0000000061 00000 n
0000000134 00000 n 0000000134 00000 n
@@ -177,33 +187,35 @@ xref
0000001018 00000 n 0000001018 00000 n
0000001166 00000 n 0000001166 00000 n
0000001314 00000 n 0000001314 00000 n
0000001430 00000 n 0000001462 00000 n
0000001685 00000 n 0000001578 00000 n
0000001791 00000 n 0000001840 00000 n
0000001997 00000 n 0000001946 00000 n
0000002203 00000 n 0000002152 00000 n
0000002290 00000 n 0000002358 00000 n
0000002607 00000 n 0000002445 00000 n
0000002681 00000 n 0000002762 00000 n
0000002802 00000 n 0000002836 00000 n
0000003122 00000 n 0000002957 00000 n
0000003291 00000 n 0000003277 00000 n
0000003395 00000 n 0000003446 00000 n
0000003526 00000 n 0000003550 00000 n
0000003652 00000 n 0000003681 00000 n
0000003727 00000 n 0000003820 00000 n
0000004529 00000 n 0000003929 00000 n
0000008590 00000 n 0000004004 00000 n
0000004839 00000 n
0000008900 00000 n
trailer trailer
<< <<
/ID /ID
[<7f14806cee8a14988600c30857cea804><7f14806cee8a14988600c30857cea804>] [<8fe3abb3b6ae298d8e49cb9ed97a806a><8fe3abb3b6ae298d8e49cb9ed97a806a>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 17 0 R /Info 18 0 R
/Root 16 0 R /Root 17 0 R
/Size 29 /Size 31
>> >>
startxref startxref
9416 9912
%%EOF %%EOF

View File

@@ -82,7 +82,7 @@ endobj
endobj endobj
15 0 obj 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 /Subject (\(unspecified\)) /Title (USB-C / HDMI and USB-tether cast \(roadmap D & E\)) /Trapped /False
>> >>
endobj endobj
@@ -159,7 +159,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<46e22eb997e0bc715c5afd1f1d8a9577><46e22eb997e0bc715c5afd1f1d8a9577>] [<2f3e1c2548fcb7ec850b1fea338ffefa><2f3e1c2548fcb7ec850b1fea338ffefa>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 15 0 R /Info 15 0 R

View File

@@ -14,6 +14,10 @@ from md_to_pdf import build_pdf_from_markdown # noqa: E402
SUBTITLE = { SUBTITLE = {
"20260602_REVERSE_SSH_proposals_summary.md": "Generated from markdown — rev. 13 + appendices WG test/production", "20260602_REVERSE_SSH_proposals_summary.md": "Generated from markdown — rev. 13 + 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", "GIT_FLOW.md": "Branches, next, release — from markdown",
"REMOTE_ACCESS_IMPL.md": "WireGuard v1 — implementation and deploy", "REMOTE_ACCESS_IMPL.md": "WireGuard v1 — implementation and deploy",
"REMOTE_ACCESS_VALIDATION.md": "Linux CLI validation — device sim + E2E", "REMOTE_ACCESS_VALIDATION.md": "Linux CLI validation — device sim + E2E",

View File

@@ -10,7 +10,8 @@
## Next ## 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) - [ ] RBAC editor UI (delegate roles per module)
- [ ] Symbolication (ndk-stack / retrace) for native/Java stacks - [ ] Symbolication (ndk-stack / retrace) for native/Java stacks
- [ ] MQTT ingest endpoint (parallel to HTTP POST) - [ ] MQTT ingest endpoint (parallel to HTTP POST)

View File

@@ -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). **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 17 min while enabled. Android: Developer settings → **Remote access** → **RSSH** (alpha target; WireGuard lab-only). RSSH avoids VPN permission. Polls BE every 17 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 ## Default accounts

View File

@@ -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).

View File

@@ -58,5 +58,27 @@ return [
'require_wg_tools' => false, 'require_wg_tools' => false,
'session_ttl_s' => 3600, 'session_ttl_s' => 3600,
'min_poll_interval_s' => 45, '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 <noreply@apps.f0xx.org>',
'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
],
], ],
]; ];

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../src/bootstrap.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
json_out(['ok' => 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);

View File

@@ -50,7 +50,7 @@
(devices || []).forEach((d) => { (devices || []).forEach((d) => {
const tr = document.createElement('tr'); const tr = document.createElement('tr');
const wl = Number(d.whitelisted) === 1; 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 openDisabled = canOpen ? '' : ' disabled title="Whitelist device and wait for WireGuard opt-in poll"';
const wlDisabled = canAdmin() ? '' : ' disabled'; const wlDisabled = canAdmin() ? '' : ' disabled';
tr.innerHTML = tr.innerHTML =

View File

@@ -0,0 +1,104 @@
#!/usr/bin/env php
<?php
/**
* Ingest 2026-06-08 priority renumber as tickets (tag 20260608).
*
* Usage:
* php scripts/ingest_priorities_20260608.php [--url=BASE] [--dry-run]
*/
declare(strict_types=1);
$opts = getopt('', ['url::', 'owner::', 'dry-run']);
$base = rtrim($opts['url'] ?? 'https://apps.f0xx.org/app/androidcast_project/crashes', '/');
$owner = $opts['owner'] ?? 'admin';
$dryRun = isset($opts['dry-run']);
$uploadUrl = $base . '/api/ticket_upload.php';
function priority_tags(string $ownerTag): array {
return [
['id' => '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 G2G6.'],
['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);

View File

@@ -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' => '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' => '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' => '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', '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.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.'], ['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.'],

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1 @@
-- RSSH columns for SQLite (applied via Database::ensureSchema).

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
/** Migration 008 — email registration + 2FA tables (SQLite dev + MariaDB ALTER). */
final class AuthEmailSchema {
private function __construct() {
}
public static function ensure(PDO $pdo): void {
if (!Database::tableExists($pdo, 'users')) {
return;
}
self::ensureUserColumns($pdo);
self::ensureAuthTokens($pdo);
self::ensureAuthFactors($pdo);
self::ensureAuthAttempts($pdo);
}
private static function ensureUserColumns(PDO $pdo): void {
$cols = [
'email_normalized' => 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'))
)"
);
}
}

View File

@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
/** Outbound mail (registration verify, password reset). Task 2.5 — uses config mail.* */
final class AuthMailer {
private function __construct() {
}
public static function send(string $to, string $subject, string $bodyText): bool {
$from = (string) cfg('mail.from', 'Android Cast <noreply@apps.f0xx.org>');
$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<int> $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);
}
}

View File

@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
/** Email self-registration (verify link). TOTP enrollment follows login — task 3.x. */
final class AuthRegistration {
private const TOKEN_TTL_S = 86400;
private function __construct() {
}
public static function ensureSchema(): void {
AuthEmailSchema::ensure(Database::pdo());
}
/** @return array{ok:bool,error?:string} */
public static function register(string $email, string $password, string $username = ''): array {
self::ensureSchema();
$emailNorm = self::normalizeEmail($email);
if ($emailNorm === '' || !filter_var($emailNorm, FILTER_VALIDATE_EMAIL)) {
return ['ok' => 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));
}
}

View File

@@ -99,6 +99,7 @@ final class Database {
self::ensureReportColumns($pdo); self::ensureReportColumns($pdo);
self::ensureReportViewsTable($pdo); self::ensureReportViewsTable($pdo);
self::ensureRbacSchema($pdo); self::ensureRbacSchema($pdo);
AuthEmailSchema::ensure($pdo);
return; return;
} }
$schemaFile = __DIR__ . '/../sql/schema.sqlite.sql'; $schemaFile = __DIR__ . '/../sql/schema.sqlite.sql';
@@ -112,6 +113,7 @@ final class Database {
self::ensureReportColumns($pdo); self::ensureReportColumns($pdo);
self::ensureReportViewsTable($pdo); self::ensureReportViewsTable($pdo);
self::ensureRbacSchema($pdo); self::ensureRbacSchema($pdo);
AuthEmailSchema::ensure($pdo);
} }
private static function ensureMysqlSchema(PDO $pdo): void { private static function ensureMysqlSchema(PDO $pdo): void {
@@ -156,6 +158,7 @@ final class Database {
} }
self::ensureMysqlReportViewsTable($pdo); self::ensureMysqlReportViewsTable($pdo);
self::ensureRbacSchema($pdo); self::ensureRbacSchema($pdo);
AuthEmailSchema::ensure($pdo);
self::ensureTicketsTable($pdo); self::ensureTicketsTable($pdo);
} }

View File

@@ -3,7 +3,7 @@ declare(strict_types=1);
/** /**
* On-demand remote access control plane (heartbeat type: ra). * 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 { final class RemoteAccessRepository {
public const OPT_NONE = 'none'; public const OPT_NONE = 'none';
@@ -23,6 +23,7 @@ final class RemoteAccessRepository {
return; return;
} }
self::ensureSessionColumns($pdo); self::ensureSessionColumns($pdo);
self::ensureRsshColumns($pdo);
} }
/** @param list<string> $where @param list<mixed> $params */ /** @param list<string> $where @param list<mixed> $params */
@@ -67,6 +68,30 @@ final class RemoteAccessRepository {
return $row ?: null; 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 { private static function ensureSessionColumns(PDO $pdo): void {
if (!Database::tableExists($pdo, 'remote_access_sessions')) { if (!Database::tableExists($pdo, 'remote_access_sessions')) {
return; return;
@@ -142,7 +167,13 @@ final class RemoteAccessRepository {
} }
if ($tunnelMode === self::OPT_RSSH) { 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); $session = $pendingSession ?? self::findConnectableSession($deviceId, $random);
@@ -279,6 +310,15 @@ final class RemoteAccessRepository {
/** @param array<string, mixed> $session */ /** @param array<string, mixed> $session */
private static function buildConnectResponse(array $session, string $deviceId, string $clientIp): array { 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<string, mixed> $session */
private static function buildWireGuardConnectResponse(array $session, string $deviceId, string $clientIp): array {
$sessionId = (string) ($session['session_id'] ?? ''); $sessionId = (string) ($session['session_id'] ?? '');
$tunnel = (string) ($session['tunnel'] ?? 'wireguard'); $tunnel = (string) ($session['tunnel'] ?? 'wireguard');
if ($tunnel !== 'wireguard') { if ($tunnel !== 'wireguard') {
@@ -318,6 +358,67 @@ final class RemoteAccessRepository {
]); ]);
} }
/** @param array<string, mixed> $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<string, mixed> $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<string, mixed> $session @return array{client_private:string,client_address:string,server_public:string,allowed_ips:string,client_public:string} */ /** @param array<string, mixed> $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 { private static function ensureSessionWireGuardKeys(string $sessionId, array $session): array {
$priv = trim((string) ($session['wg_client_private_key'] ?? '')); $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 { public static function closeSession(string $sessionId, string $status, string $reason): void {
$st = Database::pdo()->prepare( $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]); $st->execute([$sessionId]);
$pub = $st->fetchColumn(); $row = $st->fetch(PDO::FETCH_ASSOC);
if (is_string($pub) && $pub !== '') { if (is_array($row) && ($row['tunnel'] ?? 'wireguard') === 'wireguard') {
WireGuardPeerProvisioner::removeClientPeer($pub); $pub = $row['wg_client_public_key'] ?? '';
if (is_string($pub) && $pub !== '') {
WireGuardPeerProvisioner::removeClientPeer($pub);
}
} }
$now = self::nowSql(); $now = self::nowSql();
Database::pdo()->prepare( Database::pdo()->prepare(
@@ -432,13 +536,24 @@ final class RemoteAccessRepository {
if (!Rbac::canAccessCompany($deviceCompanyId) || !Rbac::can('remote_access_operate', $deviceCompanyId)) { if (!Rbac::canAccessCompany($deviceCompanyId) || !Rbac::can('remote_access_operate', $deviceCompanyId)) {
return ['ok' => false, 'error' => 'forbidden']; return ['ok' => false, 'error' => 'forbidden'];
} }
if ((string) ($device['opt_in_mode'] ?? self::OPT_NONE) !== self::OPT_WIREGUARD) { if ((string) ($device['opt_in_mode'] ?? self::OPT_NONE) === self::OPT_NONE) {
return ['ok' => false, 'error' => 'device_not_opted_in_wg']; 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'); self::closeActiveSessionsForDevice($deviceId, 'superseded');
$sessionId = self::uuid4(); $sessionId = self::uuid4();
$companyId = $companyId ?? (int) ($device['company_id'] ?? Rbac::defaultCompanyId()); $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); $expiresAt = time() + (int) cfg('remote_access.session_ttl_s', 3600);
$now = self::nowSql(); $now = self::nowSql();
Database::pdo()->prepare( Database::pdo()->prepare(
@@ -450,7 +565,7 @@ final class RemoteAccessRepository {
$companyId, $companyId,
$operatorUserId, $operatorUserId,
self::STATUS_PENDING, self::STATUS_PENDING,
'wireguard', $tunnel,
$endpoint, $endpoint,
$expiresAt, $expiresAt,
$now, $now,

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/** Ephemeral reverse-SSH session credentials (alpha). */
final class RsshSessionProvisioner {
private function __construct() {
}
/** @return array{bastion_host:string,bastion_port:int,username:string,password:string,remote_bind_port:int,local_forward_port:int} */
public static function allocate(string $sessionId): array {
$host = (string) cfg('remote_access.rssh.bastion_host', 'ra.apps.f0xx.org');
$port = max(1, min(65535, (int) cfg('remote_access.rssh.bastion_port', 443)));
$prefix = (string) cfg('remote_access.rssh.username_prefix', 'ra-');
$username = $prefix . preg_replace('/[^a-zA-Z0-9_-]/', '', $sessionId);
if (strlen($username) > 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<string, mixed> $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;
}
}

View File

@@ -46,6 +46,9 @@ require_once __DIR__ . '/Database.php';
require_once __DIR__ . '/Rbac.php'; require_once __DIR__ . '/Rbac.php';
require_once __DIR__ . '/DeviceRepository.php'; require_once __DIR__ . '/DeviceRepository.php';
require_once __DIR__ . '/Auth.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__ . '/ReportRepository.php';
require_once __DIR__ . '/TagCatalog.php'; require_once __DIR__ . '/TagCatalog.php';
require_once __DIR__ . '/TicketTagCatalog.php'; require_once __DIR__ . '/TicketTagCatalog.php';
@@ -58,6 +61,7 @@ require_once __DIR__ . '/TicketRepository.php';
require_once __DIR__ . '/GraphRepository.php'; require_once __DIR__ . '/GraphRepository.php';
require_once __DIR__ . '/RemoteAccessRepository.php'; require_once __DIR__ . '/RemoteAccessRepository.php';
require_once __DIR__ . '/WireGuardPeerProvisioner.php'; require_once __DIR__ . '/WireGuardPeerProvisioner.php';
require_once __DIR__ . '/RsshSessionProvisioner.php';
require_once __DIR__ . '/AnalyticsHead.php'; require_once __DIR__ . '/AnalyticsHead.php';
function cfg(string $key, $default = null) { function cfg(string $key, $default = null) {

View File

@@ -11,7 +11,7 @@
}, },
"crash": { "crash": {
"enabled": "true", "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", "prune_after_upload": "true",
"upload_interval": "5m", "upload_interval": "5m",
"max_pending": 32 "max_pending": 32

View File

@@ -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})") message(STATUS "No libvpx.a for ${ANDROID_ABI} — VPx JNI probe off (build with scripts/build-native-codecs.sh ${ANDROID_ABI})")
endif() 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(log-lib log)
find_library(android-lib android) find_library(android-lib android)
find_library(dl-lib dl) find_library(dl-lib dl)

View File

@@ -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 | | 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 | | 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 | | 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` 1. Initialize submodules as needed:
2. Build `libvpx.a` per ABI (NDK is auto-detected; see `scripts/android-ndk.sh`):
`./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.) 2. Build static libs per ABI (NDK auto-detected via `scripts/android-ndk.sh`):
3. Build **each ABI** you ship (Gradle links `build/native/<abi>/libvpx.a` automatically):
```bash ```bash
./scripts/build-native-codecs.sh arm64-v8a ./scripts/build-native-codecs.sh arm64-v8a
./scripts/build-native-codecs.sh armeabi-v7a # if armeabi-v7a is in your APK ./scripts/build-native-codecs.sh armeabi-v7a # if shipped in APK
./gradlew assembleDebug ```
```
If the repo path has **spaces**, the script stages under `/tmp/androidcast-libvpx-$USER`.
3. Gradle / CMake link `build/native/<abi>/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`). 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 ## Stream protection defaults

View File

@@ -1,8 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Build libvpx for Android ABIs and produce libvpx.a for ANDROIDCAST_LIBVPX_LIB. # Build libvpx, libopus, and libspeex for Android ABIs → build/native/<abi>/*.a
# # CMake in ndk/ autolinks when those archives exist (ANDROIDCAST_HAVE_* = 1).
# 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.
# #
# Prerequisites: # Prerequisites:
# git submodule update --init third-party/libvpx # git submodule update --init third-party/libvpx
@@ -12,6 +10,7 @@
# ./scripts/build-native-codecs.sh arm64-v8a # ./scripts/build-native-codecs.sh arm64-v8a
# ANDROIDCAST_LIBVPX_LIB=$PWD/build/native/arm64-v8a/libvpx.a ./gradlew assembleDebug # 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/<abi>/
set -euo pipefail set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" 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" cp -f "$OUT/libvpx.a" "$PROJECT_OUT/libvpx.a"
fi 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 ""
echo "Built: $PROJECT_OUT/libvpx.a (size $(ls -lah "${PROJECT_OUT}/libvpx.a" | awk '{print $5}'))" echo "Built: $PROJECT_OUT/libvpx.a (size $(ls -lah "${PROJECT_OUT}/libvpx.a" | awk '{print $5}'))"
if [[ -n "$STAGING" ]]; then if [[ -n "$STAGING" ]]; then
@@ -233,5 +338,5 @@ if [[ -n "$STAGING" ]]; then
fi fi
echo "Build other ABIs as needed, e.g.:" echo "Build other ABIs as needed, e.g.:"
echo " ./scripts/build-native-codecs.sh armeabi-v7a" echo " ./scripts/build-native-codecs.sh armeabi-v7a"
echo "Then rebuild the APK (CMake picks build/native/<abi>/libvpx.a per ABI):" echo "Then rebuild the APK (CMake autolinks build/native/<abi>/*.a per ABI):"
echo " ./gradlew assembleDebug" echo " ./gradlew assembleDebug"