mirror of
git://f0xx.org/android_cast
synced 2026-07-29 03:57:50 +03:00
BE sync
This commit is contained in:
@@ -26,6 +26,7 @@ Follow [docs/GIT_FLOW.md](docs/GIT_FLOW.md) and [docs/GIT_FLOW.pdf](docs/GIT_FLO
|
||||
6. **Hotfix (next-only bug):** `fix/*` from **next** → merge to **next** only.
|
||||
7. **Never** force-push `master`; force-push `next` only when the user explicitly requests history rewrite on integration.
|
||||
8. When working on a named branch, call **SetActiveBranch** with that branch name.
|
||||
9. **Fast track on `next`:** if committing directly on `next` (alpha sprint, user asked), **one feature per atomic commit** — message names the feature; no mixed RSSH + graphs + docs in one commit. Revert with `git revert <hash>`. See [GIT_FLOW.md §4.1](docs/GIT_FLOW.md#41-fast-track-on-next-alpha-sprint).
|
||||
|
||||
## Quick commands
|
||||
|
||||
|
||||
@@ -131,6 +131,8 @@ dependencies {
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.json:json:20240303'
|
||||
|
||||
implementation 'com.github.mwiede:jsch:0.2.21'
|
||||
|
||||
if (project.findProject(':tunnel') != null) {
|
||||
implementation project(':tunnel')
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<h2>Android Cast</h2>
|
||||
<h2>Android Cast (application)</h2>
|
||||
<p>Copyright © 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>
|
||||
<ol>
|
||||
@@ -8,25 +8,29 @@
|
||||
</ol>
|
||||
<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</p>
|
||||
|
||||
<h2>AndroidX AppCompat</h2>
|
||||
<p>Licensed under the <b>Apache License, Version 2.0</b>.</p>
|
||||
<p>Copyright The Android Open Source Project.</p>
|
||||
|
||||
<h2>Google Material Components for Android</h2>
|
||||
<p>Licensed under the <b>Apache License, Version 2.0</b>.</p>
|
||||
|
||||
<h2>AndroidX ConstraintLayout</h2>
|
||||
<p>Licensed under the <b>Apache License, Version 2.0</b>.</p>
|
||||
|
||||
<h2>Google Play services Cronet</h2>
|
||||
<p>Licensed under the <b>BSD 3-Clause License</b> (Chromium Cronet).</p>
|
||||
<h2>Android Open Source components (Gradle)</h2>
|
||||
<ul>
|
||||
<li><b>AndroidX AppCompat</b> — Apache License 2.0</li>
|
||||
<li><b>AndroidX ConstraintLayout</b> — Apache License 2.0</li>
|
||||
<li><b>AndroidX RecyclerView</b> — Apache License 2.0</li>
|
||||
<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>
|
||||
<li><b>Google Play Billing Library</b> — Google Play SDK terms (proprietary; see Google Play Console / developer policies)</li>
|
||||
<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>
|
||||
<li><b>JSch (mwiede fork)</b> — BSD 3-Clause License (reverse SSH remote access)</li>
|
||||
</ul>
|
||||
|
||||
<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/<abi>/lib*.a</code> exists for each ABI in the APK. JNI encode/decode sinks may still be partial while libraries are linked.</p>
|
||||
<ul>
|
||||
<li><b>libvpx</b> — BSD-style license (VP8/VP9 software codec, planned).</li>
|
||||
<li><b>Speex</b> — revised BSD license (speech codec, planned).</li>
|
||||
<li><b>Opus</b> — BSD-style license (speech codec with in-band FEC, planned).</li>
|
||||
<li><b>libvpx</b> (VP8/VP9) — BSD-style license (Xiph.org). See <code>third-party/libvpx/LICENSE</code>.</li>
|
||||
<li><b>Opus</b> — BSD-style license (Xiph.org et al.). See <code>third-party/opus/COPYING</code>.</li>
|
||||
<li><b>Speex</b> — Revised BSD license (Xiph.org Foundation). See <code>third-party/speex/COPYING</code>.</li>
|
||||
</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>
|
||||
|
||||
@@ -36,6 +36,7 @@ public final class AppPreferences {
|
||||
private static final String KEY_PLAY_INCOMING_AUDIO = "play_incoming_audio";
|
||||
private static final String KEY_DEV_RECEIVER_DEBUG_OVERLAY = "dev_receiver_debug_overlay";
|
||||
private static final String KEY_DEV_GRAB_SESSION_STATS = "dev_grab_session_stats";
|
||||
private static final String KEY_DEV_STREAM_DUMP = "dev_stream_dump";
|
||||
private static final String KEY_SEND_ANONYMOUS_CRASH_LOGS = "send_anonymous_crash_logs";
|
||||
private static final String KEY_DEV_SHOW_CODEC_PRIORITY_SCORES = "dev_show_codec_priority_scores";
|
||||
private static final String KEY_DEV_RECEIVER_VIDEO_PRESET = "dev_receiver_video_preset";
|
||||
@@ -160,6 +161,15 @@ public final class AppPreferences {
|
||||
prefs(context).edit().putBoolean(KEY_DEV_GRAB_SESSION_STATS, grab).apply();
|
||||
}
|
||||
|
||||
/** Developer: record elementary cast streams to zip (FR 0.1; default off). */
|
||||
public static boolean isDevStreamDumpEnabled(Context context) {
|
||||
return prefs(context).getBoolean(KEY_DEV_STREAM_DUMP, false);
|
||||
}
|
||||
|
||||
public static void setDevStreamDumpEnabled(Context context, boolean enabled) {
|
||||
prefs(context).edit().putBoolean(KEY_DEV_STREAM_DUMP, enabled).apply();
|
||||
}
|
||||
|
||||
/** Send anonymous crash reports via the standalone crash watcher process (default on). */
|
||||
public static boolean isSendAnonymousCrashLogs(Context context) {
|
||||
return prefs(context).getBoolean(KEY_SEND_ANONYMOUS_CRASH_LOGS, true);
|
||||
|
||||
@@ -83,10 +83,11 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
|
||||
CheckBox debugOverlay = findViewById(R.id.check_receiver_debug_overlay);
|
||||
CheckBox codecScores = findViewById(R.id.check_show_codec_priority_scores);
|
||||
CheckBox sessionStats = findViewById(R.id.check_grab_session_stats);
|
||||
CheckBox streamDump = findViewById(R.id.check_dev_stream_dump);
|
||||
CheckBox adaptiveDisplay = findViewById(R.id.check_receiver_adaptive_display);
|
||||
CheckBox backendNtpSync = findViewById(R.id.check_dev_backend_ntp_sync);
|
||||
|
||||
refreshFromPreferences(debugOverlay, codecScores, sessionStats, adaptiveDisplay, backendNtpSync);
|
||||
refreshFromPreferences(debugOverlay, codecScores, sessionStats, streamDump, adaptiveDisplay, backendNtpSync);
|
||||
|
||||
findViewById(R.id.btn_open_session_stats).setOnClickListener(v ->
|
||||
startActivity(new android.content.Intent(this, SessionStatsAnalyzerActivity.class)));
|
||||
@@ -144,6 +145,7 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
|
||||
findViewById(R.id.check_receiver_debug_overlay),
|
||||
findViewById(R.id.check_show_codec_priority_scores),
|
||||
findViewById(R.id.check_grab_session_stats),
|
||||
findViewById(R.id.check_dev_stream_dump),
|
||||
findViewById(R.id.check_receiver_adaptive_display),
|
||||
findViewById(R.id.check_dev_backend_ntp_sync));
|
||||
bindDiagPingSection();
|
||||
@@ -167,7 +169,7 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
|
||||
String[] labels = new String[]{
|
||||
getString(R.string.dev_remote_access_disabled),
|
||||
getString(R.string.dev_remote_access_wireguard),
|
||||
getString(R.string.dev_remote_access_rssh_unavailable),
|
||||
getString(R.string.dev_remote_access_rssh),
|
||||
};
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
|
||||
android.R.layout.simple_spinner_item, labels);
|
||||
@@ -186,15 +188,6 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
|
||||
return;
|
||||
}
|
||||
RemoteAccessMode picked = modes[position];
|
||||
if (picked == RemoteAccessMode.RSSH) {
|
||||
Toast.makeText(DeveloperSettingsActivity.this,
|
||||
R.string.dev_remote_access_rssh_toast, Toast.LENGTH_SHORT).show();
|
||||
suppressRemoteAccessSpinnerEvents = true;
|
||||
remoteAccessSpinner.setSelection(
|
||||
AppPreferences.getDevRemoteAccessMode(DeveloperSettingsActivity.this).ordinal(), false);
|
||||
suppressRemoteAccessSpinnerEvents = false;
|
||||
return;
|
||||
}
|
||||
applyRemoteAccessMode(picked);
|
||||
}
|
||||
|
||||
@@ -518,13 +511,15 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void refreshFromPreferences(CheckBox debugOverlay, CheckBox codecScores, CheckBox sessionStats,
|
||||
CheckBox adaptiveDisplay, CheckBox backendNtpSync) {
|
||||
CheckBox streamDump, CheckBox adaptiveDisplay, CheckBox backendNtpSync) {
|
||||
bindDeveloperCheckbox(debugOverlay, AppPreferences.isShowReceiverDebugOverlay(this),
|
||||
AppPreferences::setShowReceiverDebugOverlay);
|
||||
bindDeveloperCheckbox(codecScores, AppPreferences.isShowCodecPriorityScores(this),
|
||||
AppPreferences::setShowCodecPriorityScores);
|
||||
bindDeveloperCheckbox(sessionStats, AppPreferences.isGrabSessionStats(this),
|
||||
AppPreferences::setGrabSessionStats);
|
||||
bindDeveloperCheckbox(streamDump, AppPreferences.isDevStreamDumpEnabled(this),
|
||||
AppPreferences::setDevStreamDumpEnabled);
|
||||
bindDeveloperCheckbox(adaptiveDisplay, AppPreferences.isDevReceiverAdaptiveDisplay(this),
|
||||
AppPreferences::setDevReceiverAdaptiveDisplay);
|
||||
bindDeveloperCheckbox(backendNtpSync, AppPreferences.isDevBackendNtpSyncEnabled(this),
|
||||
|
||||
@@ -14,6 +14,7 @@ package com.foxx.androidcast.crash;
|
||||
import android.content.Context;
|
||||
|
||||
import com.foxx.androidcast.AppPreferences;
|
||||
import com.foxx.androidcast.network.BackendEndpoints;
|
||||
import com.foxx.androidcast.settings.AppSettingsJson;
|
||||
|
||||
import org.json.JSONObject;
|
||||
@@ -39,7 +40,7 @@ public final class CrashSettingsStore {
|
||||
}
|
||||
String url = crash.optString("upload_url", "").trim();
|
||||
if (!url.isEmpty()) {
|
||||
uploadUrl = url;
|
||||
uploadUrl = BackendEndpoints.normalizeCrashesUrl(url);
|
||||
}
|
||||
if (crash.has("prune_after_upload")) {
|
||||
prune = parseBool(crash.optString("prune_after_upload", "true"), prune);
|
||||
|
||||
@@ -26,7 +26,8 @@ public final class DevDiagnosticsController {
|
||||
AUDIO(R.id.dev_diag_audio, R.string.dev_diag_audio_title),
|
||||
AUDIO_CODECS(R.id.dev_diag_audio_codecs, R.string.dev_diag_audio_codecs_title),
|
||||
NETWORK(R.id.dev_diag_network, R.string.dev_diag_network_title),
|
||||
CASTING(R.id.dev_diag_casting, R.string.dev_diag_casting_title);
|
||||
CASTING(R.id.dev_diag_casting, R.string.dev_diag_casting_title),
|
||||
ADVERTISEMENT(R.id.dev_diag_advertisement, R.string.dev_diag_advertisement_title);
|
||||
|
||||
final int includeId;
|
||||
final int titleRes;
|
||||
@@ -107,6 +108,8 @@ public final class DevDiagnosticsController {
|
||||
DevDiagnosticsProbe.probeNetwork(activity, cancel), sectionText);
|
||||
runSection(Section.CASTING, () ->
|
||||
DevDiagnosticsProbe.probeCastingExtras(activity), sectionText);
|
||||
runSection(Section.ADVERTISEMENT, () ->
|
||||
DevDiagnosticsProbe.probeFullAdvertisementInfo(activity), sectionText);
|
||||
} finally {
|
||||
if (!cancel.get()) {
|
||||
lastReport = new DevDiagnosticsReport(System.currentTimeMillis(), sectionText);
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.foxx.androidcast.media.CodecPriorityCatalog;
|
||||
import com.foxx.androidcast.media.CodecTier;
|
||||
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
|
||||
import com.foxx.androidcast.network.CastCodecFlags;
|
||||
import com.foxx.androidcast.network.transport.StreamProtectionCapability;
|
||||
import com.foxx.androidcast.sender.CodecCatalog;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -53,9 +54,9 @@ public final class DevDiagnosticsProbe {
|
||||
sb.append("API ").append(Build.VERSION.SDK_INT).append('\n');
|
||||
sb.append("Native JNI probes:\n");
|
||||
sb.append(" libvpx: ").append(NativeCodecBridge.isLibvpxAvailable() ? "yes" : "no").append('\n');
|
||||
sb.append(" Opus: ").append(NativeCodecBridge.isOpusAvailable() ? "yes (native)" : "no (stub→AAC)")
|
||||
sb.append(" Opus: ").append(NativeCodecBridge.isOpusAvailable() ? "yes (lib linked)" : "no (stub→AAC)")
|
||||
.append('\n');
|
||||
sb.append(" Speex: ").append(NativeCodecBridge.isSpeexAvailable() ? "yes (native)" : "no (stub→AAC)")
|
||||
sb.append(" Speex: ").append(NativeCodecBridge.isSpeexAvailable() ? "yes (lib linked)" : "no (stub→AAC)")
|
||||
.append('\n');
|
||||
sb.append('\n').append("Video encoders (MediaCodec):\n");
|
||||
appendVideoCodecLines(sb, true, settings);
|
||||
@@ -173,6 +174,27 @@ public final class DevDiagnosticsProbe {
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
/** Wire advertisement summary (last card on diagnostics tab). */
|
||||
public static String probeFullAdvertisementInfo(Context context) {
|
||||
CastSettings settings = AppPreferences.loadSenderDefaults(context);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("[audio adv] ").append(join(CodecCatalog.localAdvertisedAudioCodecsInOrder(settings)))
|
||||
.append(" · mask 0x")
|
||||
.append(Integer.toHexString(CastCodecFlags.localAudioMask()))
|
||||
.append('\n');
|
||||
List<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) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
PackageManager pm = context.getPackageManager();
|
||||
|
||||
@@ -12,8 +12,10 @@ import android.os.PowerManager;
|
||||
import android.telephony.TelephonyManager;
|
||||
|
||||
import com.foxx.androidcast.AppPreferences;
|
||||
import com.foxx.androidcast.BuildConfig;
|
||||
import com.foxx.androidcast.CastConfig;
|
||||
import com.foxx.androidcast.crash.CrashSettingsStore;
|
||||
import com.foxx.androidcast.discovery.CastLanPeerProbe;
|
||||
import com.foxx.androidcast.network.BackendEndpoints;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
@@ -51,7 +53,7 @@ public final class DevNetworkSelfTestProbe {
|
||||
probeOverall(context, caps, cancel, report);
|
||||
probeLink(context, caps, lp, report);
|
||||
probeCaptive(caps, cancel, report);
|
||||
probeIntraLan(lp, cancel, report);
|
||||
probeIntraLan(context, lp, cancel, report);
|
||||
probeBackend(context, cancel, report);
|
||||
probeNtp(context, cancel, report);
|
||||
probeNatExternal(caps, context, cancel, report);
|
||||
@@ -186,7 +188,8 @@ public final class DevNetworkSelfTestProbe {
|
||||
}
|
||||
}
|
||||
|
||||
private static void probeIntraLan(LinkProperties lp, AtomicBoolean cancel, DevNetworkSelfTestReport r) {
|
||||
private static void probeIntraLan(Context context, LinkProperties lp, AtomicBoolean cancel,
|
||||
DevNetworkSelfTestReport r) {
|
||||
r.section("LAN", "Intra-network");
|
||||
String ip = firstIpv4Host(lp);
|
||||
if (ip == null) {
|
||||
@@ -203,62 +206,36 @@ public final class DevNetworkSelfTestProbe {
|
||||
return;
|
||||
}
|
||||
String prefix = parts[0] + "." + parts[1] + "." + parts[2] + ".";
|
||||
int self = Integer.parseInt(parts[3]);
|
||||
List<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 + ")");
|
||||
if (peers == 0) {
|
||||
r.itemWarn("peers", "0 reachable (+/-8 quick scan)");
|
||||
r.itemNa("avg-rtt", "n/a");
|
||||
CastLanPeerProbe.Result scan = CastLanPeerProbe.scan(context, cancel);
|
||||
r.itemPass("scan-ms", Long.toString(scan.elapsedMs));
|
||||
if (scan.peers == 0) {
|
||||
r.itemWarn("cast-peers",
|
||||
"0 on LAN (UDP :" + CastConfig.DISCOVERY_PORT + ", need peer listening/Receive)");
|
||||
} else {
|
||||
long sum = 0;
|
||||
for (Long v : rtts) {
|
||||
sum += v;
|
||||
r.itemPass("cast-peers", scan.peers + " · " + scan.summary);
|
||||
}
|
||||
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) {
|
||||
r.section("BACKEND", "Backend reachability");
|
||||
String url = resolveBackendUrl(context);
|
||||
if (url.isEmpty()) {
|
||||
r.itemWarn("url", "not configured");
|
||||
return;
|
||||
String heartbeat = BackendEndpoints.HEARTBEAT_URL;
|
||||
r.itemPass("url", shortenUrl(heartbeat));
|
||||
String configured = BackendEndpoints.normalizeCrashesUrl(
|
||||
CrashSettingsStore.load(context).uploadUrl);
|
||||
if (!configured.isEmpty() && !configured.equals(BackendEndpoints.UPLOAD_URL)) {
|
||||
r.itemWarn("configured", shortenUrl(configured) + " (legacy host; probe uses apps.f0xx.org)");
|
||||
}
|
||||
r.itemPass("url", shortenUrl(url));
|
||||
if (cancel != null && cancel.get()) {
|
||||
r.itemWarn("probe", "stopped");
|
||||
return;
|
||||
}
|
||||
HttpResult head = httpHead(url);
|
||||
HttpResult head = httpHead(BackendEndpoints.CRASHES_BASE + "/");
|
||||
reportHttp(r, "head", head.code, head.summary);
|
||||
HttpReadResult read = httpRead(url, 32 * 1024);
|
||||
HttpReadResult read = httpRead(BackendEndpoints.CRASHES_BASE + "/", 32 * 1024);
|
||||
reportHttpRead(r, "downlink", read.code, read.summary);
|
||||
HttpResult uplink = httpPostJson(url, "{\"ping\":\"bandwidth\"}");
|
||||
HttpResult uplink = httpPostJson(BackendEndpoints.HEARTBEAT_URL,
|
||||
"{\"heartbeat\":{\"type\":\"ping\"}}");
|
||||
reportHttp(r, "uplink", uplink.code, uplink.summary);
|
||||
}
|
||||
|
||||
@@ -273,12 +250,7 @@ public final class DevNetworkSelfTestProbe {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
String backend = resolveBackendUrl(context);
|
||||
if (backend.isEmpty()) {
|
||||
r.itemWarn("be-ntp", "backend URL missing");
|
||||
return;
|
||||
}
|
||||
String heartbeatUrl = toHeartbeatUrl(backend);
|
||||
String backend = BackendEndpoints.HEARTBEAT_URL;
|
||||
if (cancel != null && cancel.get()) {
|
||||
r.itemWarn("be-ntp", "stopped");
|
||||
return;
|
||||
@@ -286,7 +258,7 @@ public final class DevNetworkSelfTestProbe {
|
||||
long now = System.currentTimeMillis() / 1000L;
|
||||
String tz = new java.text.SimpleDateFormat("Z", Locale.US).format(new java.util.Date());
|
||||
String body = "{\"heartbeat\":{\"type\":\"ntp\",\"date\":" + now + ",\"tz\":\"" + tz + "\"}}";
|
||||
HttpTextResult text = httpPostJsonText(heartbeatUrl, body);
|
||||
HttpTextResult text = httpPostJsonText(backend, body);
|
||||
if (httpSuccess(text.code)) {
|
||||
r.itemPass("be-ntp", text.summary);
|
||||
if (text.body != null && !text.body.isEmpty()) {
|
||||
@@ -319,9 +291,7 @@ public final class DevNetworkSelfTestProbe {
|
||||
r.itemWarn("external-ip", "stopped");
|
||||
return;
|
||||
}
|
||||
String backend = resolveBackendUrl(context);
|
||||
if (!backend.isEmpty()) {
|
||||
HttpTextResult viaBackend = httpPostJsonText(toHeartbeatUrl(backend),
|
||||
HttpTextResult viaBackend = httpPostJsonText(BackendEndpoints.HEARTBEAT_URL,
|
||||
"{\"heartbeat\":{\"type\":\"ip\"}}");
|
||||
if (viaBackend.code < 0) {
|
||||
r.itemFail("external-ip", viaBackend.summary);
|
||||
@@ -333,16 +303,6 @@ public final class DevNetworkSelfTestProbe {
|
||||
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 {
|
||||
r.itemPass("external-ip", truncate(web.summary, 100));
|
||||
}
|
||||
}
|
||||
|
||||
private static void probeMtu(LinkProperties lp, DevNetworkSelfTestReport r) {
|
||||
@@ -457,31 +417,6 @@ public final class DevNetworkSelfTestProbe {
|
||||
return oneLine.substring(0, max - 3) + "...";
|
||||
}
|
||||
|
||||
private static String toHeartbeatUrl(String backend) {
|
||||
if (backend.contains("/api/upload.php")) {
|
||||
return backend.replace("/api/upload.php", "/api/heartbeat.php");
|
||||
}
|
||||
if (backend.endsWith("/")) {
|
||||
return backend + "api/heartbeat.php";
|
||||
}
|
||||
return backend + "/api/heartbeat.php";
|
||||
}
|
||||
|
||||
private static String resolveBackendUrl(Context context) {
|
||||
String crash = CrashSettingsStore.load(context).uploadUrl;
|
||||
if (crash != null && !crash.trim().isEmpty()) {
|
||||
return crash.trim();
|
||||
}
|
||||
String ota = AppPreferences.getOtaChannelUrl(context);
|
||||
if (ota != null && !ota.trim().isEmpty()) {
|
||||
return ota.trim();
|
||||
}
|
||||
if (BuildConfig.OTA_CHANNEL_URL_DEFAULT != null && !BuildConfig.OTA_CHANNEL_URL_DEFAULT.trim().isEmpty()) {
|
||||
return BuildConfig.OTA_CHANNEL_URL_DEFAULT.trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static boolean hasTransport(NetworkCapabilities caps, int t) {
|
||||
return caps != null && caps.hasTransport(t);
|
||||
}
|
||||
|
||||
245
app/src/main/java/com/foxx/androidcast/dev/StreamDumpWriter.java
Normal file
245
app/src/main/java/com/foxx/androidcast/dev/StreamDumpWriter.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,9 @@ package com.foxx.androidcast.network.transport;
|
||||
**********************************************************************/
|
||||
import com.foxx.androidcast.CastSettings;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
/** Runtime FEC/NACK implementations on this device (not a user setting). */
|
||||
public final class StreamProtectionCapability {
|
||||
@@ -54,4 +56,14 @@ public final class StreamProtectionCapability {
|
||||
}
|
||||
return EnumSet.of(CastSettings.StreamProtection.NONE);
|
||||
}
|
||||
|
||||
/** Modes this device advertises on the wire (handshake uses AUTO → full set). */
|
||||
public static List<String> advertisedProtectionInOrder() {
|
||||
List<String> out = new ArrayList<>();
|
||||
out.add("AUTO (handshake)");
|
||||
for (CastSettings.StreamProtection mode : receiverImplementations()) {
|
||||
out.add(mode.name());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public final class RemoteAccessCoordinator {
|
||||
AppPreferences.clearDevRemoteAccessSession(app);
|
||||
return;
|
||||
}
|
||||
if (mode == RemoteAccessMode.WIREGUARD) {
|
||||
if (mode == RemoteAccessMode.WIREGUARD || mode == RemoteAccessMode.RSSH) {
|
||||
RemoteAccessService.start(app, mode);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ public enum RemoteAccessMode {
|
||||
DISABLED,
|
||||
/** WireGuard outbound tunnel (v1). */
|
||||
WIREGUARD,
|
||||
/** Reverse SSH — reserved; not selectable in UI until implemented. */
|
||||
/** Reverse SSH — alpha essential; selectable when implemented. */
|
||||
RSSH;
|
||||
|
||||
public static RemoteAccessMode fromStored(String name) {
|
||||
@@ -39,6 +39,6 @@ public enum RemoteAccessMode {
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return this == WIREGUARD;
|
||||
return this == WIREGUARD || this == RSSH;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ public final class RemoteAccessService extends Service {
|
||||
long exp = AppPreferences.getDevRemoteAccessExpiresAt(this);
|
||||
if (exp > 0 && exp < System.currentTimeMillis() / 1000L) {
|
||||
Log.i(TAG, "session expired — tearing down tunnel");
|
||||
WireGuardTunnelBridge.tearDown(this);
|
||||
RemoteAccessTunnelHelper.tearDownAll(this);
|
||||
AppPreferences.clearDevRemoteAccessSession(this);
|
||||
}
|
||||
}
|
||||
@@ -95,7 +95,7 @@ public final class RemoteAccessService extends Service {
|
||||
String action = intent != null ? intent.getAction() : ACTION_POLL;
|
||||
if (ACTION_STOP.equals(action)) {
|
||||
cancelPoll(this);
|
||||
WireGuardTunnelBridge.tearDown(this);
|
||||
RemoteAccessTunnelHelper.tearDownAll(this);
|
||||
stopForeground(true);
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
@@ -135,7 +135,12 @@ public final class RemoteAccessService extends Service {
|
||||
hb.put("app_version", com.foxx.androidcast.BuildConfig.VERSION_NAME);
|
||||
hb.put("tunnel_mode", mode.toApiValue());
|
||||
JSONArray caps = new JSONArray();
|
||||
if (mode == RemoteAccessMode.WIREGUARD) {
|
||||
caps.put("wireguard");
|
||||
} else if (mode == RemoteAccessMode.RSSH) {
|
||||
caps.put("rssh");
|
||||
caps.put("ssh_reverse");
|
||||
}
|
||||
caps.put("files_app_home");
|
||||
hb.put("capabilities", caps);
|
||||
|
||||
@@ -147,7 +152,7 @@ public final class RemoteAccessService extends Service {
|
||||
if ("connect".equals(raAction)) {
|
||||
handleConnect(resp);
|
||||
} else if ("disabled".equals(raAction) || "deny".equals(raAction)) {
|
||||
WireGuardTunnelBridge.tearDown(this);
|
||||
RemoteAccessTunnelHelper.tearDownAll(this);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "poll failed: " + e.getMessage());
|
||||
@@ -156,7 +161,7 @@ public final class RemoteAccessService extends Service {
|
||||
|
||||
private void postDisableHeartbeat() {
|
||||
try {
|
||||
WireGuardTunnelBridge.tearDown(this);
|
||||
RemoteAccessTunnelHelper.tearDownAll(this);
|
||||
AppPreferences.clearDevRemoteAccessSession(this);
|
||||
JSONObject hb = new JSONObject();
|
||||
hb.put("type", "ra");
|
||||
@@ -170,26 +175,34 @@ public final class RemoteAccessService extends Service {
|
||||
}
|
||||
|
||||
private void handleConnect(JSONObject resp) {
|
||||
if (!"wireguard".equals(resp.optString("tunnel", ""))) {
|
||||
Log.w(TAG, "unsupported tunnel in connect");
|
||||
return;
|
||||
}
|
||||
String tunnel = resp.optString("tunnel", "");
|
||||
String sessionId = resp.optString("session_id", "");
|
||||
long expiresAt = resp.optLong("expires_at", 0L);
|
||||
if (!sessionId.isEmpty()) {
|
||||
AppPreferences.setDevRemoteAccessSession(this, sessionId, expiresAt);
|
||||
}
|
||||
JSONObject creds = resp.optJSONObject("credentials");
|
||||
String wgConfig = WireGuardConfigBuilder.fromConnectCredentials(creds);
|
||||
if (wgConfig.isEmpty()) {
|
||||
if (creds == null) {
|
||||
return;
|
||||
}
|
||||
if (WireGuardTunnelBridge.applyConfig(this, wgConfig)) {
|
||||
updateNotificationConnected(sessionId);
|
||||
boolean ok = false;
|
||||
if ("wireguard".equals(tunnel)) {
|
||||
String wgConfig = WireGuardConfigBuilder.fromConnectCredentials(creds);
|
||||
if (!wgConfig.isEmpty()) {
|
||||
ok = WireGuardTunnelBridge.applyConfig(this, wgConfig);
|
||||
}
|
||||
} else if ("ssh_reverse".equals(tunnel)) {
|
||||
ok = ReverseSshTunnelBridge.applyConfig(this, creds);
|
||||
} else {
|
||||
Log.w(TAG, "unsupported tunnel in connect: " + tunnel);
|
||||
return;
|
||||
}
|
||||
if (ok) {
|
||||
updateNotificationConnected(sessionId, tunnel);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateNotificationConnected(String sessionId) {
|
||||
private void updateNotificationConnected(String sessionId, String tunnel) {
|
||||
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||
if (nm == null) {
|
||||
return;
|
||||
@@ -197,6 +210,9 @@ public final class RemoteAccessService extends Service {
|
||||
String text = sessionId.isEmpty()
|
||||
? getString(R.string.dev_remote_access_notification_connected)
|
||||
: getString(R.string.dev_remote_access_notification_session, sessionId);
|
||||
if ("ssh_reverse".equals(tunnel)) {
|
||||
text = text + " (RSSH)";
|
||||
}
|
||||
nm.notify(NOTIF_ID, new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(getString(R.string.dev_remote_access_notification_title))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -150,6 +150,7 @@ public final class CastSendPump {
|
||||
}
|
||||
byte[] payload = item.buildPayload();
|
||||
session.send(item.type, payload);
|
||||
com.foxx.androidcast.dev.StreamDumpWriter.recordFrame(item.type, payload);
|
||||
int payloadSize = payload != null ? payload.length : 0;
|
||||
SessionStatsContext.recordOutboundMessage(item.type, item.keyFrame, payloadSize);
|
||||
if (statsPlane != null) {
|
||||
|
||||
@@ -118,6 +118,35 @@ public final class CodecCatalog {
|
||||
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) {
|
||||
CodecTier tier = com.foxx.androidcast.media.CodecPriorityCatalog.tierForAudioPreference(codec);
|
||||
int score = com.foxx.androidcast.media.CodecPriorityCatalog.priorityScore(codec, settings);
|
||||
|
||||
@@ -294,6 +294,7 @@ public class ScreenCastService extends Service implements
|
||||
sendPump = new CastSendPump();
|
||||
sendPump.start(session, () -> stopping.set(true), plane);
|
||||
}
|
||||
com.foxx.androidcast.dev.StreamDumpWriter.startIfEnabled(this);
|
||||
casting.set(true);
|
||||
CastActiveState.setSenderCasting(true);
|
||||
CastTrayNotifier.refresh(this);
|
||||
@@ -989,6 +990,7 @@ public class ScreenCastService extends Service implements
|
||||
sendPump.stop();
|
||||
sendPump = null;
|
||||
}
|
||||
com.foxx.androidcast.dev.StreamDumpWriter.finalizeIfActive(this);
|
||||
if (fanoutPump != null) {
|
||||
fanoutPump.stop();
|
||||
fanoutPump = null;
|
||||
|
||||
@@ -56,6 +56,10 @@
|
||||
<include
|
||||
android:id="@+id/dev_diag_casting"
|
||||
layout="@layout/view_dev_diag_card" />
|
||||
|
||||
<include
|
||||
android:id="@+id/dev_diag_advertisement"
|
||||
layout="@layout/view_dev_diag_card" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -30,6 +30,20 @@
|
||||
android:layout_marginTop="12dp"
|
||||
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
|
||||
android:id="@+id/check_dev_diag_ping_pong"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
<string name="dev_diag_audio_codecs_title">Аудиокодеки (сжатие)</string>
|
||||
<string name="dev_diag_network_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_mode">Режим списка разрешений</string>
|
||||
<string name="dev_sender_resolution_ui_separated">Раздельный (Оригинал / 720p / 480p / Авто)</string>
|
||||
|
||||
@@ -223,6 +223,7 @@
|
||||
<string name="dev_diag_audio_codecs_title">Audio codecs (compression)</string>
|
||||
<string name="dev_diag_network_title">Network</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_mode">Resolution preset mode</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_codec_priority_scores">Show codec priority scores in settings</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_response_mode">Ping response mode (this device)</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_disabled">Disabled</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_hint">When enabled, device polls BE every 1–7 min (type: ra). Disabled sends teardown + BE notification. RSSH is reserved for a future release.</string>
|
||||
<string name="dev_remote_access_rssh">RSSH (reverse SSH)</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 1–7 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_applied">Remote access: %1$s</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_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_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_force">Force re-check</string>
|
||||
<string name="dev_network_selftest_pending">Waiting for self-test run…</string>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -29,9 +29,14 @@ public class RemoteAccessModeTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isActive_onlyWireGuard() {
|
||||
public void toApiValue_rssh() {
|
||||
assertEquals("rssh", RemoteAccessMode.RSSH.toApiValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isActive_wireguardAndRssh() {
|
||||
assertFalse(RemoteAccessMode.DISABLED.isActive());
|
||||
assertTrue(RemoteAccessMode.WIREGUARD.isActive());
|
||||
assertFalse(RemoteAccessMode.RSSH.isActive());
|
||||
assertTrue(RemoteAccessMode.RSSH.isActive());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,7 +453,7 @@ endobj
|
||||
endobj
|
||||
78 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260605123816+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123816+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Author (Android Cast project) /CreationDate (D:20260608122520+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122520+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Android Cast \204 On-Demand Remote Access \(Reverse Tunnel\) Proposals) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
@@ -995,7 +995,7 @@ xref
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<27be2a803248e97757fa9331ed0b01e2><27be2a803248e97757fa9331ed0b01e2>]
|
||||
[<0bc2f0c306f87efd0a3d63157383ae9d><0bc2f0c306f87efd0a3d63157383ae9d>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 78 0 R
|
||||
|
||||
418
docs/20260607-2FA-email-mobile-auth-flow.md
Normal file
418
docs/20260607-2FA-email-mobile-auth-flow.md
Normal 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; “Can’t scan?” reveals manual secret |
|
||||
| Errors | Same `.alert` pattern as login |
|
||||
| Android app | Settings → “Project account” → opens Custom Tab to account/security (post-alpha polish OK) |
|
||||
|
||||
### §10.7 Alpha implementation plan
|
||||
|
||||
| Phase | Scope | Estimate |
|
||||
|-------|--------|----------|
|
||||
| **P0 — Email infra** | §9 DNS + forward + outbound SMTP test | 0.5–1 day ops |
|
||||
| **P1 — Register + verify** | Pending table, mailer, verify link, captcha | 2–3 days |
|
||||
| **P2 — TOTP login** | Enroll QR + challenge on login | 1–2 days |
|
||||
| **P3 — Admin tools** | Recovery email, clear lockouts, audit | 1 day |
|
||||
| **P4 — WebAuthn** | Passkey enroll/login (Chrome/Android) | 1–2 days optional for alpha |
|
||||
| **P5 — Mobile app link** | Custom Tab entry points | 0.5 day |
|
||||
|
||||
**Alpha sign-off (auth):** new user can register on phone, verify email, enroll TOTP, log in with 2FA; admin can clear lockout; inbound `info@` mail reaches Gmail.
|
||||
|
||||
Update [`ALPHA.md`](ALPHA.md) § G (auth) and [`ROADMAP.md`](ROADMAP.md) alpha backend checklist when P1 starts.
|
||||
|
||||
---
|
||||
|
||||
## UI/UX alignment (existing consoles)
|
||||
|
||||
Do **not** introduce a new design system. Reuse:
|
||||
|
||||
| Asset | Path |
|
||||
|-------|------|
|
||||
| Styles | `public/assets/css/app.css` — `.login-page`, `.login-card`, `.alert`, `.muted`, `.locale-picker` |
|
||||
| Layout shell | `views/layout.php` — nav, hub card styling |
|
||||
| i18n | `public/assets/js/i18n.js` + JSON catalogs |
|
||||
| Analytics hook | `AnalyticsHead::render()` on auth pages (optional) |
|
||||
|
||||
New views (planned):
|
||||
|
||||
- `views/register.php`
|
||||
- `views/verify_email.php`
|
||||
- `views/account_security.php`
|
||||
- `views/two_factor_challenge.php`
|
||||
|
||||
Hub landing contact form (`landing-pages.inc.php`) should eventually use **`info@apps.f0xx.org`** as `mailto:` — align when email live.
|
||||
|
||||
---
|
||||
|
||||
## Security & compliance notes
|
||||
|
||||
- Enforce **rate limits** on register, login, verify resend (e.g. 5/min/IP hash).
|
||||
- Lock account after N failed 2FA attempts; unlock via email or admin.
|
||||
- Do **not** log passwords or TOTP codes.
|
||||
- Backup codes: generate 10 one-time codes, display once, store bcrypt hashes.
|
||||
- GDPR-style minimization: optional `auth_attempts` retention job (purge > 90 days).
|
||||
|
||||
---
|
||||
|
||||
## Open questions
|
||||
|
||||
| # | Question | Default proposal |
|
||||
|---|----------|------------------|
|
||||
| Q1 | Require WebAuthn for alpha or TOTP-only? | **TOTP required**; WebAuthn optional P4 |
|
||||
| Q2 | Allow registration on prod immediately or invite-only? | **Open register** with captcha; `viewer` role |
|
||||
| Q3 | Single Gmail inbox for all aliases? | Yes for now (`foxxspambox0@gmail.com`) |
|
||||
| Q4 | Separate `noreply@` for outbound? | Yes — no forward needed; SPF only |
|
||||
|
||||
---
|
||||
|
||||
## Source linkage
|
||||
|
||||
| Topic | Doc / code |
|
||||
|-------|------------|
|
||||
| Infra / DNS | [INFRA.md](INFRA.md) |
|
||||
| Crash console | [CRASH_REPORTER.md](CRASH_REPORTER.md), [backend README](../examples/crash_reporter/backend/README.md) |
|
||||
| RBAC | [backend README § RBAC](../examples/crash_reporter/backend/README.md#rbac-phase-1--admin-ui) |
|
||||
| Alpha gates | [ALPHA.md](ALPHA.md), [ROADMAP.md](ROADMAP.md) |
|
||||
| PDF build | [`scripts/build-all-docs-pdf.sh`](../scripts/build-all-docs-pdf.sh) |
|
||||
462
docs/20260607-2FA-email-mobile-auth-flow.pdf
Normal file
462
docs/20260607-2FA-email-mobile-auth-flow.pdf
Normal 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%iodPGX5!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
|
||||
333
docs/20260608_ALPHA_PRIORITIES.md
Normal file
333
docs/20260608_ALPHA_PRIORITIES.md
Normal 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.1–2.4 dev, 2.5+3.x agent | Mixed | **Yes** (auth path) |
|
||||
| **P3** | 4.x | Agent (+ deploy assist) | **Yes** (RSSH) |
|
||||
| **P4** | 5.x | 50/50 OTA/URLs; rest agent | **Yes** |
|
||||
| **P5** | 6.x | ~99% dev | LAN alpha |
|
||||
| **P9** | 9.x landing | Agent when idle | No |
|
||||
|
||||
**Effective order (agent):** 0.1 (parallel) → 4.x + 2.5/3.x + 5.3–5.5 → 1.x → 9.x when bandwidth allows.
|
||||
|
||||
---
|
||||
|
||||
## 0.x Emergency debug (not alpha blocker)
|
||||
|
||||
### 0.1 FR — Stream dump while casting
|
||||
|
||||
**Priority:** High emergency debug. **Cyclic with 6.6** (same capability; 6.6 references 0.1 for audio lag / lifecycle analysis).
|
||||
|
||||
**Goal:** While audio or video cast is active, optionally record what flows through the encode/send (or receive/decode) path for offline analysis.
|
||||
|
||||
| Sub | Mode | Deliverable |
|
||||
|-----|------|-------------|
|
||||
| **0.1.1** | Structured | `YYYYMMDD_HHMMSS_sess_record.zip` containing `meta.json` + streams |
|
||||
| **0.1.2** | Raw quick | Same archive layout; **uncompressed** elementary streams only |
|
||||
| **0.1.3** | Tooling | Update session stats / graphs ingest to read `meta.json` schema |
|
||||
|
||||
**Archive layout (both 0.1.1 and 0.1.2):**
|
||||
|
||||
```text
|
||||
20260608_120422_sess_record.zip
|
||||
├── meta.json
|
||||
├── 20260608_audio_stream.raw # or .aac / .opus per meta
|
||||
└── 20260608_video_stream.raw # or .h264 / annex-b per meta
|
||||
```
|
||||
|
||||
**`meta.json` (sketch):**
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": 1,
|
||||
"session_id": "20260608_120422",
|
||||
"role": "sender",
|
||||
"record_mode": "raw_elementary",
|
||||
"started_at_epoch_ms": 0,
|
||||
"ended_at_epoch_ms": 0,
|
||||
"video": {
|
||||
"mime": "video/avc",
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"frame_count": 1200,
|
||||
"file": "20260608_video_stream.raw",
|
||||
"format": "length_prefixed_avc",
|
||||
"csd0_hex": "…",
|
||||
"csd1_hex": "…"
|
||||
},
|
||||
"audio": {
|
||||
"mime": "audio/mp4a-latm",
|
||||
"sample_rate": 44100,
|
||||
"channels": 2,
|
||||
"frame_count": 800,
|
||||
"file": "20260608_audio_stream.raw",
|
||||
"format": "length_prefixed_aac"
|
||||
},
|
||||
"cast_settings_snapshot": { "transport": "udp", "protection": "NONE" }
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation hooks (prep):**
|
||||
|
||||
| Tap point | File | Notes |
|
||||
|-----------|------|-------|
|
||||
| Sender video frames | `ScreenCastService` / `CastSendPump` | After `onEncodedFrame`, before UDP wrap |
|
||||
| Sender audio frames | `AudioEncoderSink.Callback.onEncodedFrame` | Same |
|
||||
| Receiver (optional phase 2) | `ReceiverCastService` / `AudioDecoder` | Inbound before decode |
|
||||
|
||||
**Dev gate:** Developer settings → **Record stream dump** (off by default); cap size/duration; write under `files/stream_dumps/`.
|
||||
|
||||
**Spec detail:** [20260608_STREAM_DUMP_DEBUG.md](20260608_STREAM_DUMP_DEBUG.md)
|
||||
|
||||
---
|
||||
|
||||
## 1.x Graphs console
|
||||
|
||||
| ID | Task | Owner |
|
||||
|----|------|-------|
|
||||
| **1** | Graphs dashboard — parent | Agent |
|
||||
| **1.1** | All pending **fixes** (5.1 brick drill-down, perf, permalinks, etc.) | Agent |
|
||||
| **1.2** | All pending **FRs** (5.2 columns 1/2/4, full-width grid) | Agent |
|
||||
|
||||
Depends on BE deploy (soft). Not alpha blocker.
|
||||
|
||||
---
|
||||
|
||||
## 2.x Email → SMTP → 2FA → RBAC
|
||||
|
||||
| ID | Task | Owner | Notes |
|
||||
|----|------|-------|-------|
|
||||
| **2.1** | DNS: MX + SPF + DKIM + DMARC for `apps.f0xx.org` | **Developer** | Web UI only; examples in `tmp/20260608_help_request.md` |
|
||||
| **2.2** | Register forward addresses (`info@`, `admin@`, `root@` → Gmail) | **Developer** | Cloudflare Email Routing or ImprovMX |
|
||||
| **2.3** | Inbound forward verification | **Developer** | Send test mail |
|
||||
| **2.4** | Gmail filters / reply-as (optional) | **Developer** | |
|
||||
| **2.5** | Outbound SMTP from BE (`noreply@`, verification mail) | **Agent** | Symfony Mailer; `config.php` |
|
||||
| **3.x** | Registration + verify + TOTP + mobile UI + migration `008` | **Agent** | See [20260607-2FA-email-mobile-auth-flow.md](20260607-2FA-email-mobile-auth-flow.md) |
|
||||
| **→ RBAC** | Finish admin UI after pages stable | **Agent** | Former task 7 |
|
||||
|
||||
Chain: `2.1 → 2.2 → 2.3` (dev) ∥ `2.5` (agent after 2.1 DNS) → `3.x` → RBAC.
|
||||
|
||||
---
|
||||
|
||||
## 3.x Auth implementation (agent)
|
||||
|
||||
All items from [20260607-2FA-email-mobile-auth-flow.md §10.7](20260607-2FA-email-mobile-auth-flow.md#107-alpha-implementation-plan):
|
||||
|
||||
- P1 Register + verify + ALTCHA
|
||||
- P2 TOTP enroll + login challenge
|
||||
- P3 Admin lockout clear + audit
|
||||
- P4 WebAuthn (optional alpha)
|
||||
- P5 Android Custom Tab entry (optional)
|
||||
|
||||
**Tests:** PHP API smoke scripts; unit tests for token hash / TOTP verify helpers.
|
||||
|
||||
---
|
||||
|
||||
## 4.x Reverse SSH alpha
|
||||
|
||||
**Replace WireGuard as alpha deliverable** — WG stays for lab ([REMOTE_ACCESS_IMPL.md](REMOTE_ACCESS_IMPL.md)).
|
||||
|
||||
| ID | Task | Owner |
|
||||
|----|------|-------|
|
||||
| **4.1** | BE: `opt_in_mode: rssh`, bastion user, `authorized_keys` / `-R` port map | Agent |
|
||||
| **4.2** | BE: nginx `stream` or SSH on 443 per [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md) | Agent + deploy |
|
||||
| **4.3** | Android: `RemoteAccessMode.RSSH` selectable, no `VpnService` | Agent |
|
||||
| **4.4** | Android: outbound SSH client (libssh2 / dropbear JNI — TBD) + FG service | Agent |
|
||||
| **4.5** | E2E: whitelist → open session → operator shell/SFTP on bastion | Agent + QA |
|
||||
| **4.wg** | WireGuard fixes only if lab broken | Agent as needed |
|
||||
|
||||
Depends on: BE deploy (5.x). Blocks **5.3**.
|
||||
|
||||
---
|
||||
|
||||
## 5.x Alpha backend validation
|
||||
|
||||
| ID | Task | Owner split |
|
||||
|----|------|-------------|
|
||||
| **5.1** | OTA channel + artifacts on `apps.f0xx.org` | 50% dev / 50% agent |
|
||||
| **5.2** | Crash + OTA URLs on device (`settings.json`, dev settings) | 50% dev / 50% agent |
|
||||
| **5.3** | RSSH BE validation | Agent (deploy assist dev) — **depends 4.x** |
|
||||
| **5.4** | auth-2fa validation (G2–G6) | Agent |
|
||||
| **5.5** | RBAC validation | Agent |
|
||||
|
||||
---
|
||||
|
||||
## 6.x Alpha app LAN
|
||||
|
||||
| ID | Task | Owner | Deps |
|
||||
|----|------|-------|------|
|
||||
| **6.1** | Opus/Speex E2E | Dev + agent JNI | native `.a` |
|
||||
| **6.2** | LAN soak A/B/C | Dev | — |
|
||||
| **6.3** | App lifecycle (notifications, recents) | Dev | |
|
||||
| **6.4** | Rotation + preview crash | Dev | ↔ 6.3 cyclic |
|
||||
| **6.5** | Audio lag — buffers, handshake, graphs | Dev | **0.1 helps** |
|
||||
| **6.6** | Debug stream analysis | Dev | **= 0.1** |
|
||||
|
||||
Agent: fixes / urgent FRs only (~1%).
|
||||
|
||||
---
|
||||
|
||||
## 9.x Landing (lowest)
|
||||
|
||||
Renumbered from former 6.x landing track:
|
||||
|
||||
| ID | Task |
|
||||
|----|------|
|
||||
| **9** | Landing hub parent |
|
||||
| **9.1** | Hub cards (graphs, builder, RA) |
|
||||
| **9.2** | Globe / earth animations |
|
||||
| **9.3** | Mobile chrome validation (LOW) |
|
||||
|
||||
---
|
||||
|
||||
## Dependency graph
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph P0["P0 debug"]
|
||||
S01["0.1 stream dump"]
|
||||
end
|
||||
|
||||
subgraph P2["P2 auth — dev"]
|
||||
D21["2.1 DNS"]
|
||||
D22["2.2 forward"]
|
||||
D23["2.3 verify inbound"]
|
||||
end
|
||||
|
||||
subgraph P2a["P2 auth — agent"]
|
||||
A25["2.5 SMTP"]
|
||||
A3["3.x 2FA"]
|
||||
RBAC["RBAC"]
|
||||
end
|
||||
|
||||
subgraph P3["P3 RSSH"]
|
||||
R4["4.x reverse SSH"]
|
||||
end
|
||||
|
||||
subgraph P4["P4 backend alpha"]
|
||||
B51["5.1 OTA"]
|
||||
B52["5.2 URLs"]
|
||||
B53["5.3 RSSH validate"]
|
||||
B54["5.4 auth validate"]
|
||||
end
|
||||
|
||||
subgraph P5["P5 app LAN — dev"]
|
||||
A61["6.1 Opus"]
|
||||
A62["6.2 soak"]
|
||||
A63["6.3 lifecycle"]
|
||||
A64["6.4 rotation"]
|
||||
A65["6.5 audio lag"]
|
||||
end
|
||||
|
||||
subgraph P9["P9 landing"]
|
||||
L9["9.x"]
|
||||
end
|
||||
|
||||
ALPHA["FULL_ALPHA"]
|
||||
|
||||
S01 -.-> A65
|
||||
S01 -.-> A64
|
||||
|
||||
D21 --> D22 --> D23
|
||||
D21 --> A25 --> A3 --> RBAC --> B54
|
||||
A3 --> ALPHA
|
||||
|
||||
R4 --> B53 --> ALPHA
|
||||
B51 --> ALPHA
|
||||
B52 --> ALPHA
|
||||
|
||||
A62 --> ALPHA
|
||||
A63 -.-> A64
|
||||
A64 -.-> A63
|
||||
|
||||
L9 -.-> RBAC
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Licenses & tests checklist
|
||||
|
||||
| When adding… | Update |
|
||||
|--------------|--------|
|
||||
| libssh2 / dropbear / Symfony Mailer | `combined.html`, `THIRD_PARTY_LICENSES.md` |
|
||||
| Stream dump (no new deps if pure Java) | — |
|
||||
| RSSH BE scripts | BE licenses doc |
|
||||
|
||||
| Area | Test command |
|
||||
|------|----------------|
|
||||
| Android unit | `./gradlew :app:testDebugUnitTest` |
|
||||
| RSSH API | `BASE=… ./scripts/ra_e2e_cli.sh` (extend for RSSH) |
|
||||
| Auth API | `scripts/test_rbac_api.sh` + new `test_auth_api.sh` (TBD) |
|
||||
| Stream dump | Unit test `StreamDumpWriter` framing + `meta.json` schema |
|
||||
|
||||
---
|
||||
|
||||
## Developer help (blocked on you)
|
||||
|
||||
**Email/DNS (2.1–2.4):** hands-on checklist with record examples:
|
||||
|
||||
→ **[tmp/20260608_help_request.md](../tmp/20260608_help_request.md)** (PDF: `tmp/20260608_help_request.pdf`)
|
||||
|
||||
Please complete 2.1–2.3 before agent enables production verification mail (2.5).
|
||||
|
||||
---
|
||||
|
||||
## Source linkage
|
||||
|
||||
| Doc | Topic |
|
||||
|-----|--------|
|
||||
| [20260608_STREAM_DUMP_DEBUG.md](20260608_STREAM_DUMP_DEBUG.md) | 0.1 implementation spec |
|
||||
| [20260607-2FA-email-mobile-auth-flow.md](20260607-2FA-email-mobile-auth-flow.md) | 2.x / 3.x |
|
||||
| [OPEN_TASKS_GRAPH.md](OPEN_TASKS_GRAPH.md) | Ticket-level graph |
|
||||
| [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md) | 4.x |
|
||||
| [ALPHA.md](ALPHA.md) | Sign-off F/G |
|
||||
362
docs/20260608_ALPHA_PRIORITIES.pdf
Normal file
362
docs/20260608_ALPHA_PRIORITIES.pdf
Normal 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
|
||||
122
docs/20260608_STREAM_DUMP_DEBUG.md
Normal file
122
docs/20260608_STREAM_DUMP_DEBUG.md
Normal 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
|
||||
232
docs/20260608_STREAM_DUMP_DEBUG.pdf
Normal file
232
docs/20260608_STREAM_DUMP_DEBUG.pdf
Normal 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
|
||||
@@ -1,6 +1,6 @@
|
||||
# Android Cast — alpha release & QA
|
||||
|
||||
Last updated: 2026-05-23. Use this document for **feature freeze**, **alpha builds**, and **sign-off** before widening testing.
|
||||
Last updated: 2026-06-04. Use this document for **feature freeze**, **alpha builds**, and **sign-off** before widening testing.
|
||||
|
||||
**Related:** [ROADMAP.md](ROADMAP.md) · [README.md](../README.md)
|
||||
|
||||
@@ -24,6 +24,8 @@ Last updated: 2026-05-23. Use this document for **feature freeze**, **alpha buil
|
||||
- [E. Known limitations (not alpha blockers)](#e-known-limitations-not-alpha-blockers)
|
||||
- [Log capture (when something fails)](#log-capture-when-something-fails)
|
||||
- [Alpha backend (optional for LAN-only alpha)](#alpha-backend-optional-for-lan-only-alpha)
|
||||
- [F. Remote access — RSSH (required for full alpha sign-off)](#f-remote-access--rssh-required-for-full-alpha-sign-off)
|
||||
- [G. Email + registration / 2FA (required for full alpha sign-off)](#g-email--registration--2fa-required-for-full-alpha-sign-off)
|
||||
- [Ending feature freeze](#ending-feature-freeze)
|
||||
<!-- /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 |
|
||||
| Camera cast (API 29+, best-effort) | Live sender mirror while casting |
|
||||
| Receiver playback + notification stop | Play Store / AdMob production |
|
||||
| **On-demand remote debug (RSSH, hidden)** — developer settings + BE | WireGuard-only remote access as alpha deliverable |
|
||||
|
||||
Code gate: `CastConfig.ALPHA_FEATURE_FREEZE = true` hides experimental transports and turns off multi-receiver selection.
|
||||
|
||||
@@ -139,7 +142,7 @@ Run on **two physical devices**, same Wi‑Fi subnet. Record build: About / `ver
|
||||
- Some apps silent under playback capture policy.
|
||||
- Heavy rotation on some OEMs may still stress system WFD/WM (watchdog) — document device + lock state.
|
||||
|
||||
**Alpha approved when:** all **A**, **B1–B4**, and **C1** pass on your reference sender + receiver pair.
|
||||
**LAN alpha approved when:** all **A**, **B1–B4**, and **C1** pass on your reference sender + receiver pair. **Full alpha** additionally requires backend deploy and **F1–F5** (RSSH).
|
||||
|
||||
---
|
||||
|
||||
@@ -162,13 +165,38 @@ adb -s RECEIVER logcat -s ReceiverCastService:* AndroidCast:*
|
||||
|
||||
## Alpha backend (optional for LAN-only alpha)
|
||||
|
||||
LAN demo does **not** require OTA/crash backend. For **full alpha** with updates and crash upload:
|
||||
LAN cast demo does **not** require OTA/crash backend. For **full alpha** (owner agreement) with updates, crash upload, and **production remote debug**:
|
||||
|
||||
| Item | Notes |
|
||||
|------|--------|
|
||||
| Host `examples/ota/v0/` | See [OTA.md](OTA.md) |
|
||||
| Deploy crash reporter | See [CRASH_REPORTER.md](CRASH_REPORTER.md) |
|
||||
| Configure URLs | Developer settings or `local.properties` |
|
||||
| **RSSH remote access** | **Alpha essential:** hidden mode (no VPN consent); device polls BE, opens outbound reverse SSH to bastion; operator connects via BE UI whitelist + session. WireGuard v1 is lab-only for alpha sign-off. See [REMOTE_ACCESS_IMPL.md](REMOTE_ACCESS_IMPL.md) |
|
||||
| **Email + 2FA auth** | **Alpha essential:** `info@`/`admin@` forward to Gmail; user register + email verify + TOTP; see [20260607-2FA-email-mobile-auth-flow.md](20260607-2FA-email-mobile-auth-flow.md) |
|
||||
|
||||
### F. Remote access — RSSH (required for full alpha sign-off)
|
||||
|
||||
| # | Step | Pass |
|
||||
|---|------|------|
|
||||
| F1 | Developer settings → **RSSH** (not WireGuard) → no Android VPN permission dialog | ☐ |
|
||||
| F2 | Device polls BE (`type: ra`); operator whitelists device + opens session in dashboard | ☐ |
|
||||
| F3 | Reverse tunnel active; operator reaches device debug endpoint on bastion (shell or agreed file path) | ☐ |
|
||||
| F4 | Session expiry / disable tears down tunnel; BE notified | ☐ |
|
||||
| F5 | No persistent VPN key icon while remote debug is active | ☐ |
|
||||
|
||||
**Full alpha approved when:** LAN sign-off (**A**, **B1–B4**, **C1**) **and** backend items above **and** **F1–F5** **and** **G1–G6** pass on reference environment.
|
||||
|
||||
### G. Email + registration / 2FA (required for full alpha sign-off)
|
||||
|
||||
| # | Step | Pass |
|
||||
|---|------|------|
|
||||
| G1 | Inbound mail to `info@apps.f0xx.org` arrives at configured Gmail (forward) | ☐ |
|
||||
| G2 | BE sends verification email (SMTP); link opens on mobile browser | ☐ |
|
||||
| G3 | New user registers → verifies email → enrolls TOTP (QR) | ☐ |
|
||||
| G4 | Login requires password + TOTP; lockout after repeated failures | ☐ |
|
||||
| G5 | User can set recovery email; admin/root can clear auth lockouts | ☐ |
|
||||
| G6 | Register/login UI matches crashes console theme (EN/RU, mobile layout) | ☐ |
|
||||
|
||||
---
|
||||
|
||||
|
||||
229
docs/ALPHA.pdf
229
docs/ALPHA.pdf
@@ -2,7 +2,7 @@
|
||||
%“Œ‹ž ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 16 0 R /F4 18 0 R /F5 19 0 R /F6 20 0 R
|
||||
/F1 2 0 R /F2 3 0 R /F3 18 0 R /F4 20 0 R /F5 21 0 R /F6 22 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
@@ -17,73 +17,83 @@ endobj
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 716.2874 143.501 727.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 716.2874 143.501 727.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 705.2874 182.629 716.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 705.2874 182.629 716.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 694.2874 179.957 705.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 694.2874 179.957 705.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 683.2874 157.733 694.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 683.2874 157.733 694.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 59.68504 672.8499 182.22 682.8499 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 59.68504 672.8499 182.22 682.8499 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 662.8499 138.0375 672.8499 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 59.68504 662.8499 138.0375 672.8499 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 652.8499 243.495 662.8499 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 59.68504 652.8499 243.495 662.8499 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 642.8499 156.795 652.8499 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 59.68504 642.8499 156.795 652.8499 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 632.8499 195.15 642.8499 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 59.68504 632.8499 195.15 642.8499 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 622.2874 170.181 633.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 45.68504 622.2874 170.181 633.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 611.2874 202.645 622.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 45.68504 611.2874 202.645 622.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 600.2874 122.173 611.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 59.68504 600.8499 256.83 610.8499 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 59.68504 590.8499 258.3 600.8499 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 45.68504 580.2874 122.173 591.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R
|
||||
14 0 R 15 0 R ] /Contents 40 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 39 0 R /Resources <<
|
||||
14 0 R 15 0 R 16 0 R 17 0 R ] /Contents 44 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 43 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0
|
||||
/Trans <<
|
||||
@@ -91,44 +101,24 @@ endobj
|
||||
>> /Type /Page
|
||||
>>
|
||||
endobj
|
||||
18 0 obj
|
||||
20 0 obj
|
||||
<<
|
||||
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
19 0 obj
|
||||
21 0 obj
|
||||
<<
|
||||
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
20 0 obj
|
||||
22 0 obj
|
||||
<<
|
||||
/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
21 0 obj
|
||||
<<
|
||||
/Contents 41 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 39 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
22 0 obj
|
||||
<<
|
||||
/Contents 42 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 39 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
23 0 obj
|
||||
<<
|
||||
/Contents 43 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 39 0 R /Resources <<
|
||||
/Contents 45 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 43 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
@@ -138,116 +128,147 @@ endobj
|
||||
endobj
|
||||
24 0 obj
|
||||
<<
|
||||
/Outlines 26 0 R /PageMode /UseNone /Pages 39 0 R /Type /Catalog
|
||||
/Contents 46 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 43 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
25 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260605123816+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123816+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Android Cast \204 alpha release & QA) /Trapped /False
|
||||
/Contents 47 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 43 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
26 0 obj
|
||||
<<
|
||||
/Count 13 /First 27 0 R /Last 38 0 R /Type /Outlines
|
||||
/Outlines 28 0 R /PageMode /UseNone /Pages 43 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
27 0 obj
|
||||
<<
|
||||
/Dest [ 21 0 R /Fit ] /Next 28 0 R /Parent 26 0 R /Title (Alpha scope \(what we ship\))
|
||||
/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Android Cast \204 alpha release & QA) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
28 0 obj
|
||||
<<
|
||||
/Dest [ 21 0 R /Fit ] /Next 29 0 R /Parent 26 0 R /Prev 27 0 R /Title (Recommended defaults \(both devices\))
|
||||
/Count 16 /First 29 0 R /Last 42 0 R /Type /Outlines
|
||||
>>
|
||||
endobj
|
||||
29 0 obj
|
||||
<<
|
||||
/Dest [ 21 0 R /Fit ] /Next 30 0 R /Parent 26 0 R /Prev 28 0 R /Title (Feature freeze checklist \(engineering\))
|
||||
/Dest [ 23 0 R /Fit ] /Next 30 0 R /Parent 28 0 R /Title (Alpha scope \(what we ship\))
|
||||
>>
|
||||
endobj
|
||||
30 0 obj
|
||||
<<
|
||||
/Count 5 /Dest [ 21 0 R /Fit ] /First 31 0 R /Last 35 0 R /Next 36 0 R /Parent 26 0 R
|
||||
/Prev 29 0 R /Title (Alpha app sign-off \(manual QA\))
|
||||
/Dest [ 23 0 R /Fit ] /Next 31 0 R /Parent 28 0 R /Prev 29 0 R /Title (Recommended defaults \(both devices\))
|
||||
>>
|
||||
endobj
|
||||
31 0 obj
|
||||
<<
|
||||
/Dest [ 21 0 R /Fit ] /Next 32 0 R /Parent 30 0 R /Title (A. Smoke \(required \204 all must pass\))
|
||||
/Dest [ 23 0 R /Fit ] /Next 32 0 R /Parent 28 0 R /Prev 30 0 R /Title (Feature freeze checklist \(engineering\))
|
||||
>>
|
||||
endobj
|
||||
32 0 obj
|
||||
<<
|
||||
/Dest [ 22 0 R /Fit ] /Next 33 0 R /Parent 30 0 R /Prev 31 0 R /Title (B. Resilience \(required\))
|
||||
/Count 5 /Dest [ 23 0 R /Fit ] /First 33 0 R /Last 37 0 R /Next 38 0 R /Parent 28 0 R
|
||||
/Prev 31 0 R /Title (Alpha app sign-off \(manual QA\))
|
||||
>>
|
||||
endobj
|
||||
33 0 obj
|
||||
<<
|
||||
/Dest [ 22 0 R /Fit ] /Next 34 0 R /Parent 30 0 R /Prev 32 0 R /Title (C. Settings matrix \(pick one row for official alpha demo\))
|
||||
/Dest [ 23 0 R /Fit ] /Next 34 0 R /Parent 32 0 R /Title (A. Smoke \(required \204 all must pass\))
|
||||
>>
|
||||
endobj
|
||||
34 0 obj
|
||||
<<
|
||||
/Dest [ 22 0 R /Fit ] /Next 35 0 R /Parent 30 0 R /Prev 33 0 R /Title (D. Regression guards \(quick\))
|
||||
/Dest [ 24 0 R /Fit ] /Next 35 0 R /Parent 32 0 R /Prev 33 0 R /Title (B. Resilience \(required\))
|
||||
>>
|
||||
endobj
|
||||
35 0 obj
|
||||
<<
|
||||
/Dest [ 22 0 R /Fit ] /Parent 30 0 R /Prev 34 0 R /Title (E. Known limitations \(not alpha blockers\))
|
||||
/Dest [ 24 0 R /Fit ] /Next 36 0 R /Parent 32 0 R /Prev 34 0 R /Title (C. Settings matrix \(pick one row for official alpha demo\))
|
||||
>>
|
||||
endobj
|
||||
36 0 obj
|
||||
<<
|
||||
/Dest [ 22 0 R /Fit ] /Next 37 0 R /Parent 26 0 R /Prev 30 0 R /Title (Log capture \(when something fails\))
|
||||
/Dest [ 24 0 R /Fit ] /Next 37 0 R /Parent 32 0 R /Prev 35 0 R /Title (D. Regression guards \(quick\))
|
||||
>>
|
||||
endobj
|
||||
37 0 obj
|
||||
<<
|
||||
/Dest [ 22 0 R /Fit ] /Next 38 0 R /Parent 26 0 R /Prev 36 0 R /Title (Alpha backend \(optional for LAN-only alpha\))
|
||||
/Dest [ 24 0 R /Fit ] /Parent 32 0 R /Prev 36 0 R /Title (E. Known limitations \(not alpha blockers\))
|
||||
>>
|
||||
endobj
|
||||
38 0 obj
|
||||
<<
|
||||
/Dest [ 23 0 R /Fit ] /Parent 26 0 R /Prev 37 0 R /Title (Ending feature freeze)
|
||||
/Dest [ 24 0 R /Fit ] /Next 39 0 R /Parent 28 0 R /Prev 32 0 R /Title (Log capture \(when something fails\))
|
||||
>>
|
||||
endobj
|
||||
39 0 obj
|
||||
<<
|
||||
/Count 4 /Kids [ 17 0 R 21 0 R 22 0 R 23 0 R ] /Type /Pages
|
||||
/Count 2 /Dest [ 24 0 R /Fit ] /First 40 0 R /Last 41 0 R /Next 42 0 R /Parent 28 0 R
|
||||
/Prev 38 0 R /Title (Alpha backend \(optional for LAN-only alpha\))
|
||||
>>
|
||||
endobj
|
||||
40 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 871
|
||||
/Dest [ 25 0 R /Fit ] /Next 41 0 R /Parent 39 0 R /Title (F. Remote access \204 RSSH \(required for full alpha sign-off\))
|
||||
>>
|
||||
stream
|
||||
Gatn%?#Q2d'Sc)R/'c\0:9'6J#\8/D7l4r-['j65`\@_X"`.qCB%sM]:"#u>g58o:.1t5-4FYV'%]0fIMtXou!P$50HpN=<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
|
||||
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
|
||||
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
|
||||
43 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 604
|
||||
/Count 4 /Kids [ 19 0 R 23 0 R 24 0 R 25 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
44 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 952
|
||||
>>
|
||||
stream
|
||||
Gat='D/[lW&H:NnnU/Pg9p;qi`t0?(c@K^28sOs\-<4r(8W=?Ef^?]Qof/WsU+fFf7u;W-]B:L'&:n%opWdO33lN3O&Oo;!KHku"1]^6r?[L`aF#<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
|
||||
xref
|
||||
0 44
|
||||
0 48
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000146 00000 n
|
||||
@@ -264,44 +285,48 @@ xref
|
||||
0000001690 00000 n
|
||||
0000001838 00000 n
|
||||
0000001986 00000 n
|
||||
0000002134 00000 n
|
||||
0000002250 00000 n
|
||||
0000002549 00000 n
|
||||
0000002627 00000 n
|
||||
0000002733 00000 n
|
||||
0000002817 00000 n
|
||||
0000003023 00000 n
|
||||
0000003229 00000 n
|
||||
0000003435 00000 n
|
||||
0000003522 00000 n
|
||||
0000003833 00000 n
|
||||
0000003908 00000 n
|
||||
0000004018 00000 n
|
||||
0000004150 00000 n
|
||||
0000004285 00000 n
|
||||
0000004451 00000 n
|
||||
0000004573 00000 n
|
||||
0000004694 00000 n
|
||||
0000004848 00000 n
|
||||
0000004973 00000 n
|
||||
0000005098 00000 n
|
||||
0000005229 00000 n
|
||||
0000005369 00000 n
|
||||
0000005472 00000 n
|
||||
0000005554 00000 n
|
||||
0000006516 00000 n
|
||||
0000010064 00000 n
|
||||
0000013475 00000 n
|
||||
0000002133 00000 n
|
||||
0000002279 00000 n
|
||||
0000002427 00000 n
|
||||
0000002543 00000 n
|
||||
0000002856 00000 n
|
||||
0000002934 00000 n
|
||||
0000003040 00000 n
|
||||
0000003124 00000 n
|
||||
0000003330 00000 n
|
||||
0000003536 00000 n
|
||||
0000003742 00000 n
|
||||
0000003829 00000 n
|
||||
0000004140 00000 n
|
||||
0000004215 00000 n
|
||||
0000004325 00000 n
|
||||
0000004457 00000 n
|
||||
0000004592 00000 n
|
||||
0000004758 00000 n
|
||||
0000004880 00000 n
|
||||
0000005001 00000 n
|
||||
0000005155 00000 n
|
||||
0000005280 00000 n
|
||||
0000005405 00000 n
|
||||
0000005536 00000 n
|
||||
0000005715 00000 n
|
||||
0000005860 00000 n
|
||||
0000006008 00000 n
|
||||
0000006111 00000 n
|
||||
0000006193 00000 n
|
||||
0000007236 00000 n
|
||||
0000010970 00000 n
|
||||
0000014423 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<735e427b950d10110ca3f1f9e20deac2><735e427b950d10110ca3f1f9e20deac2>]
|
||||
[<f56de612b28393ad9c1197e5e2601682><f56de612b28393ad9c1197e5e2601682>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 25 0 R
|
||||
/Root 24 0 R
|
||||
/Size 44
|
||||
/Info 27 0 R
|
||||
/Root 26 0 R
|
||||
/Size 48
|
||||
>>
|
||||
startxref
|
||||
14170
|
||||
17346
|
||||
%%EOF
|
||||
|
||||
@@ -69,4 +69,4 @@ PIN default: `1234` (change in settings if needed).
|
||||
- QUIC / WebRTC / USB-tether transports
|
||||
- FEC / NACK stream protection
|
||||
- 1:N multi-receiver (unless explicitly testing)
|
||||
- OTA / crash upload (backend track)
|
||||
- OTA / crash upload (backend track) — except **RSSH remote access**, which is **full alpha essential** (separate checklist in [ALPHA.md](ALPHA.md) § F)
|
||||
|
||||
@@ -92,7 +92,7 @@ endobj
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260605123816+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123816+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Alpha soak checklist \(LAN demo\)) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
@@ -145,10 +145,10 @@ Gatn#?Z2Df'ZJu$.F)EaD4^b-L:sG;U/$WX9]7_Sj:\Ap!+r[GW\=>;1Kg3G;**)SZ31fqlgqa?a-V@3
|
||||
endobj
|
||||
27 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2427
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2525
|
||||
>>
|
||||
stream
|
||||
Gau0FgL&^F&q9#IR)bGtFY3D)_c+]q\"$>g+`(NV+rl;ME@`3T0'-[Ep+u7kcGd%ufO$&+/U9h^k+Ze6&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>j'&>VXb6j7frWDX?@_6M4Lb3>M,<;.]\MdDN`i7M_ZH&.1$aAP.D62rHecRM3k'/VK;T5_'Zt$reD:T)/C??b0P0lZMW_\hffKTQ6rba*BU.#$B%?9B\1!aR(4,2I6ZR2ZC[@0t?g4mCgE&u]DQq]E+;&jM#hD?Voj;LTCPhm)*h!?Q8'oV?"ei!`\?$Q]T</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
|
||||
xref
|
||||
0 28
|
||||
@@ -183,7 +183,7 @@ xref
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<e968e22228343a774971f6b322897513><e968e22228343a774971f6b322897513>]
|
||||
[<bbd8ab8f4762cf64769bb7aaada579ad><bbd8ab8f4762cf64769bb7aaada579ad>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 17 0 R
|
||||
@@ -191,5 +191,5 @@ trailer
|
||||
/Size 28
|
||||
>>
|
||||
startxref
|
||||
6495
|
||||
6593
|
||||
%%EOF
|
||||
|
||||
@@ -214,7 +214,7 @@ endobj
|
||||
endobj
|
||||
37 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260605123816+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123816+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (AV quality enhancement \204 Q&A session notes) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
@@ -455,7 +455,7 @@ xref
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<876852817b61089766fa64afae9b20cf><876852817b61089766fa64afae9b20cf>]
|
||||
[<bd17772f8f9b3b80a960f225dbde379d><bd17772f8f9b3b80a960f225dbde379d>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 37 0 R
|
||||
|
||||
@@ -92,7 +92,7 @@ endobj
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260605123816+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123816+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Android Cast build ecosystem) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
@@ -190,7 +190,7 @@ xref
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<0da9c3b30d0111b270b2be75732a529b><0da9c3b30d0111b270b2be75732a529b>]
|
||||
[<a6c94f5fe79a0c13e711e1e90825077b><a6c94f5fe79a0c13e711e1e90825077b>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 17 0 R
|
||||
|
||||
@@ -67,7 +67,7 @@ endobj
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260605123816+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123816+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Commercial features \(developer-gated\)) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
@@ -135,7 +135,7 @@ xref
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<bb21d75fe21ec421f3c0f308926be035><bb21d75fe21ec421f3c0f308926be035>]
|
||||
[<0c23c7846cb813e7f6634f931914a505><0c23c7846cb813e7f6634f931914a505>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 12 0 R
|
||||
|
||||
@@ -72,7 +72,7 @@ endobj
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260605123816+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123816+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Anonymous crash reporter) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
@@ -141,7 +141,7 @@ xref
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<791687390028bc46a9df797aa4d2bb7c><791687390028bc46a9df797aa4d2bb7c>]
|
||||
[<a454a236c417c49668dff25f892582be><a454a236c417c49668dff25f892582be>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 13 0 R
|
||||
|
||||
@@ -4,8 +4,8 @@ This document defines how we develop, integrate, release, and hotfix. **Cursor a
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Version | 1.3 |
|
||||
| Published | 2026-05-23 |
|
||||
| Version | 1.4 |
|
||||
| Published | 2026-06-08 |
|
||||
| Author(s) | Anton Afanaasyeu |
|
||||
| Markdown source | `docs/GIT_FLOW.md` |
|
||||
| PDF | `docs/GIT_FLOW.pdf` |
|
||||
@@ -24,6 +24,7 @@ This document defines how we develop, integrate, release, and hotfix. **Cursor a
|
||||
- [2. Branches](#2-branches)
|
||||
- [3. Branch diagram (steady state)](#3-branch-diagram-steady-state)
|
||||
- [4. Daily development](#4-daily-development)
|
||||
- [4.1 Fast track on `next` (alpha sprint)](#41-fast-track-on-next-alpha-sprint)
|
||||
- [5. Release: next → master](#5-release-next-master)
|
||||
- [6. Hotfixes — chosen strategy](#6-hotfixes-chosen-strategy)
|
||||
- [Default: hotfix from master (option 1) ✓](#default-hotfix-from-master-option-1)
|
||||
@@ -64,7 +65,7 @@ This document defines how we develop, integrate, release, and hotfix. **Cursor a
|
||||
| Branch | Role | Direct commits? |
|
||||
|--------|------|-----------------|
|
||||
| **`master`** | Production line; **green** (build + tests pass); release tags (`v0.1.2`) | **No** — merge only |
|
||||
| **`next`** | Integration / release candidate; absorbs features until a release is approved | **No** — merge only |
|
||||
| **`next`** | Integration / release candidate; absorbs features until a release is approved | **No** — merge only (exception: alpha sprint direct commits when maintainer/agent explicitly commits here — [§4.1](GIT_FLOW.md#41-fast-track-on-next-alpha-sprint)) |
|
||||
| **`feature/*`**, **`fix/*`**, **`topic/*`** | Short-lived work; fork **`next`** | Yes (normal dev) |
|
||||
| **`hotfix/*`** | Urgent fix for **released** code | Yes; merge target **`master`** first |
|
||||
|
||||
@@ -124,6 +125,43 @@ sequenceDiagram
|
||||
Dev->>F: delete branch
|
||||
```
|
||||
|
||||
### 4.1 Fast track on `next` (alpha sprint)
|
||||
|
||||
During intensive alpha work, the team may commit **directly on `next`** instead of short-lived `feature/*` branches — to move faster and avoid branch overhead.
|
||||
|
||||
**Trade-off:** integration history on `next` is harder to read; **mitigation: atomic, revert-friendly commits.**
|
||||
|
||||
| Rule | Why |
|
||||
|------|-----|
|
||||
| **One feature (or one logical slice) per commit** | `git revert <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`
|
||||
@@ -297,6 +335,7 @@ Agents working in this repo **must**:
|
||||
5. **Hotfixes on released code:** branch from `master`, merge back to `master`, tag, then **merge `master` into `next`**.
|
||||
6. **Set active branch** metadata when using a named topic branch (project convention).
|
||||
7. **Do not** force-push `master`; avoid force-push on `next` unless the user explicitly requests integration history rewrite.
|
||||
8. **Fast track on `next`:** when the user commits directly on `next` (alpha sprint), use **atomic commits** — one feature per commit so `git revert <hash>` can drop a bad feature without touching others ([§4.1](GIT_FLOW.md#41-fast-track-on-next-alpha-sprint)).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ endobj
|
||||
endobj
|
||||
38 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260605123816+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123816+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Android Cast \204 Git flow \(green `master`\)) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
@@ -462,7 +462,7 @@ xref
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<068b2b541b532a647e185c962518956e><068b2b541b532a647e185c962518956e>]
|
||||
[<b859d7af6a4738a9ecce4f8413b8de77><b859d7af6a4738a9ecce4f8413b8de77>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 38 0 R
|
||||
|
||||
@@ -123,7 +123,7 @@ endobj
|
||||
endobj
|
||||
22 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Android Cast \204 Graph Visualization Pivot) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
@@ -258,7 +258,7 @@ xref
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<b885b64a468bda1dd9ec9c5a881415e5><b885b64a468bda1dd9ec9c5a881415e5>]
|
||||
[<ceaa9bb32a6eaf264a3a717043768037><ceaa9bb32a6eaf264a3a717043768037>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 22 0 R
|
||||
|
||||
@@ -153,7 +153,7 @@ endobj
|
||||
endobj
|
||||
27 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Android Cast \204 production infrastructure) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
@@ -319,7 +319,7 @@ xref
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<12469c2730c47a48460adf663f7f8cb3><12469c2730c47a48460adf663f7f8cb3>]
|
||||
[<87a8fa5b1e63c3bbf3a0dcf885fa3e7d><87a8fa5b1e63c3bbf3a0dcf885fa3e7d>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 27 0 R
|
||||
|
||||
@@ -112,7 +112,7 @@ endobj
|
||||
endobj
|
||||
20 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Crash console \204 open ingest API \(design\)) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
@@ -232,7 +232,7 @@ xref
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<02d61126cd7bac88ae3ba3f80797fee3><02d61126cd7bac88ae3ba3f80797fee3>]
|
||||
[<a715c7d986b017c524e6219400d53a60><a715c7d986b017c524e6219400d53a60>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 20 0 R
|
||||
|
||||
121
docs/OPEN_TASKS_GRAPH.md
Normal file
121
docs/OPEN_TASKS_GRAPH.md
Normal 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.1–2.4** dev, **2.5** + **3.x** agent, RBAC | **Yes** (auth) |
|
||||
| P3 | **4.x** RSSH | **Yes** |
|
||||
| P4 | **5.x** backend validation | **Yes** |
|
||||
| P5 | **6.x** app LAN | LAN alpha |
|
||||
| P9 | **9.x** landing | No |
|
||||
|
||||
---
|
||||
|
||||
## Dependency graph
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
S01["0.1 stream dump<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.1–2.4** | **Developer** | DNS UI access |
|
||||
| **2.5** | Agent | 2.1 |
|
||||
| **3.x** | Agent | 2.5 |
|
||||
| **RBAC** | Agent | 3.x, stable pages |
|
||||
| **4.x** | Agent | BE deploy |
|
||||
| **5.1–5.2** | 50/50 | deploy |
|
||||
| **5.3** | Agent | 4.x |
|
||||
| **5.4–5.5** | Agent | 3.x, RBAC |
|
||||
| **6.1–6.6** | Developer (~99%) | 0.1 ↔ 6.6 |
|
||||
| **9.x** | Agent (low) | — |
|
||||
|
||||
---
|
||||
|
||||
## Critical path
|
||||
|
||||
```text
|
||||
DEV: 2.1 → 2.2 → 2.3
|
||||
AGENT: 2.5 → 3.x → 5.4 ──┐
|
||||
AGENT: 4.x → 5.3 ─────────┼──► FULL_ALPHA
|
||||
SHARED: 5.1 OTA, 5.2 URLs ─┤
|
||||
DEV: 6.2 LAN soak ────────┘
|
||||
|
||||
Parallel: 0.1, 1.x, 6.1/6.3–6.5, 9.x
|
||||
```
|
||||
|
||||
**Developer blocked checklist:** [tmp/20260608_help_request.md](../tmp/20260608_help_request.md)
|
||||
190
docs/OPEN_TASKS_GRAPH.pdf
Normal file
190
docs/OPEN_TASKS_GRAPH.pdf
Normal 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≥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
|
||||
288
docs/OPUS_SPEEX_VALIDATION.pdf
Normal file
288
docs/OPUS_SPEEX_VALIDATION.pdf
Normal 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
|
||||
@@ -112,7 +112,7 @@ endobj
|
||||
endobj
|
||||
20 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Over-the-air \(OTA\) updates \204 deployment schema **v0**) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
@@ -233,7 +233,7 @@ xref
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<fb1766001c59f41c7bc88a0fec7828d9><fb1766001c59f41c7bc88a0fec7828d9>]
|
||||
[<8ce728cd48920bf00138be4ae62e19b8><8ce728cd48920bf00138be4ae62e19b8>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 20 0 R
|
||||
|
||||
@@ -97,7 +97,7 @@ endobj
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Pro / OSS split \(cast-core + cast-pro AAR\)) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
@@ -202,7 +202,7 @@ xref
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<64e7459a461b61dd1fa8670aa52b9dab><64e7459a461b61dd1fa8670aa52b9dab>]
|
||||
[<cca35018774facb6253a5fa1087adfbd><cca35018774facb6253a5fa1087adfbd>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 17 0 R
|
||||
|
||||
@@ -25,6 +25,7 @@ Click a link to open a guide. Section links jump within the document (GitHub, Cu
|
||||
| Document | Summary |
|
||||
|----------|---------|
|
||||
| [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md) | On-demand remote access (WG/SSH/file API) proposals. |
|
||||
| [20260607-2FA-email-mobile-auth-flow.md](20260607-2FA-email-mobile-auth-flow.md) | Email forwarding + registration/2FA (alpha must-have). |
|
||||
| [ALPHA.md](ALPHA.md) | Alpha feature freeze, QA sign-off, defaults. |
|
||||
| [ALPHA_SOAK.md](ALPHA_SOAK.md) | Short LAN soak checklist for demo validation. |
|
||||
| [AV_QUALITY_QA_SESSION.md](AV_QUALITY_QA_SESSION.md) | AV enhancement Q&A: sender vs receiver pipelines. |
|
||||
@@ -42,6 +43,9 @@ Click a link to open a guide. Section links jump within the document (GitHub, Cu
|
||||
| [REMOTE_ACCESS_VALIDATION.md](REMOTE_ACCESS_VALIDATION.md) | Linux CLI suite + mobile parity |
|
||||
| [ROADMAP.md](ROADMAP.md) | Implementation roadmap, alpha definition, deferred work. |
|
||||
| [TICKETS_ROADMAP.md](TICKETS_ROADMAP.md) | Tickets console, Gitea, attachments, agent queue. |
|
||||
| [20260608_ALPHA_PRIORITIES.md](20260608_ALPHA_PRIORITIES.md) | **Master priority plan** (0.1–9.x renumber, owners, alpha path). |
|
||||
| [20260608_STREAM_DUMP_DEBUG.md](20260608_STREAM_DUMP_DEBUG.md) | FR 0.1 stream dump spec. |
|
||||
| [OPEN_TASKS_GRAPH.md](OPEN_TASKS_GRAPH.md) | Open tasks dependency graph (Mermaid). |
|
||||
| [USB_HDMI_CAST.md](USB_HDMI_CAST.md) | Roadmap D/E: HDMI awareness, USB-tether transport. |
|
||||
|
||||
---
|
||||
@@ -68,9 +72,10 @@ Click a link to open a guide. Section links jump within the document (GitHub, Cu
|
||||
|
||||
- [CRASH_REPORTER.md](CRASH_REPORTER.md) — crash upload
|
||||
- [OPEN_API.md](OPEN_API.md) — ingest API ([§Upload API](OPEN_API.md#upload-api-post-apiv1uploadphp))
|
||||
- [TICKETS_ROADMAP.md](TICKETS_ROADMAP.md) — tickets + Gitea
|
||||
- [TICKETS_ROADMAP.md](TICKETS_ROADMAP.md) — tickets + Gitea ([§Open tasks graph](OPEN_TASKS_GRAPH.md))
|
||||
- [GRAFANA_vs_others_graphvis_pivot.md](GRAFANA_vs_others_graphvis_pivot.md) — graphs pivot
|
||||
- [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md) — remote access ([§Rev. 3 Q&A](20260602_REVERSE_SSH_proposals_summary.md#rev-3--wg-vs-ssh-qa--bastion-deep-dive))
|
||||
- [20260607-2FA-email-mobile-auth-flow.md](20260607-2FA-email-mobile-auth-flow.md) — email + 2FA ([§9 Email](20260607-2FA-email-mobile-auth-flow.md#9-email-addresses-agent-config))
|
||||
|
||||
### Product and codecs
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ endobj
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 17 0 R /Fit ] /Rect [ 45.68504 644.2874 102.165 655.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 644.2874 102.165 655.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
@@ -112,7 +112,7 @@ endobj
|
||||
endobj
|
||||
20 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Android Cast \204 documentation index) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
@@ -159,7 +159,7 @@ endobj
|
||||
endobj
|
||||
29 0 obj
|
||||
<<
|
||||
/Dest [ 17 0 R /Fit ] /Next 30 0 R /Parent 21 0 R /Prev 23 0 R /Title (Agent bootstrap)
|
||||
/Dest [ 18 0 R /Fit ] /Next 30 0 R /Parent 21 0 R /Prev 23 0 R /Title (Agent bootstrap)
|
||||
>>
|
||||
endobj
|
||||
30 0 obj
|
||||
@@ -181,17 +181,17 @@ Gatn$;/_pX&BE]".IJp[1m^!8_qmqJ8<[5Zm<IEECRg'3fZ]_+rUr%C9suJ+BS@u<_D]1/rU2]j"Faa.
|
||||
endobj
|
||||
33 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2889
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3156
|
||||
>>
|
||||
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
|
||||
34 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 389
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 722
|
||||
>>
|
||||
stream
|
||||
GasbUhbM'3'ZTV='OS/5<9%Y#m5hr0`Tb@#>N>slV-A(r$1q\F0@[R$[gPQR_th4=j$8b'62S'+JU<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
|
||||
xref
|
||||
0 35
|
||||
@@ -229,11 +229,11 @@ xref
|
||||
0000004122 00000 n
|
||||
0000004197 00000 n
|
||||
0000004919 00000 n
|
||||
0000007900 00000 n
|
||||
0000008167 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<9df3a579e257b333c2cd9123c54c5e70><9df3a579e257b333c2cd9123c54c5e70>]
|
||||
[<ef3c83d8c0a3227f9e77021a833ebd39><ef3c83d8c0a3227f9e77021a833ebd39>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 20 0 R
|
||||
@@ -241,5 +241,5 @@ trailer
|
||||
/Size 35
|
||||
>>
|
||||
startxref
|
||||
8380
|
||||
8980
|
||||
%%EOF
|
||||
|
||||
@@ -19,7 +19,7 @@ See the full proposal: [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVE
|
||||
|
||||
## App (developer settings)
|
||||
|
||||
- Dropdown: **Disabled** | **WireGuard** | **RSSH (greyed — not selectable)**
|
||||
- Dropdown: **Disabled** | **WireGuard** (lab) | **RSSH** (alpha — outbound reverse SSH, no VPN)
|
||||
- Pref key: `dev_remote_access_mode` (`AppPreferences`)
|
||||
- **Disabled:** notifies BE (`status:disable`), tears down VPN, stops `RemoteAccessService`
|
||||
- **WireGuard:** foreground service; jittered poll **1–7 min** → `heartbeat.php` with `type: ra`
|
||||
@@ -27,7 +27,19 @@ See the full proposal: [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVE
|
||||
- Session credentials persisted until `expires_at`; expiry tears down tunnel locally
|
||||
- Optional full userspace WG via `third-party/wireguard-android` (`:tunnel`); otherwise `RemoteAccessVpnService` TUN fallback
|
||||
|
||||
## BE control plane
|
||||
### Android VPN consent (WireGuard)
|
||||
|
||||
WireGuard on Android **must** use `VpnService` (see `DeveloperSettingsActivity` → `VpnService.prepare()`). This is **not avoidable** on stock devices:
|
||||
|
||||
| UX | WireGuard (v1, lab) | RSSH (**alpha essential**) |
|
||||
|----|----------------|----------------|
|
||||
| One-time system dialog | Yes — “Allow VPN?” | No VPN permission |
|
||||
| Status-bar key icon | Yes while tunnel up | No |
|
||||
| Full-device routing | TUN interface (split routes in config, still a VPN) | No L3 tunnel |
|
||||
| Outbound from phone | UDP to BE `:51820` | HTTPS poll + outbound SSH to bastion |
|
||||
|
||||
There is no supported “hidden VPN” on non-root Android: any TUN creator is classified as a VPN app. **RSSH** (foreground service + existing HTTPS poll + outbound SSH reverse tunnel) is the **alpha deliverable** agreed by project owners — device initiates, operator reaches a forwarded port on the bastion, without VPN consent or status-bar key. See [Rev. 3 Q&A](20260602_REVERSE_SSH_proposals_summary.md#rev-3--wg-vs-ssh-qa--bastion-deep-dive) and [ROADMAP.md § Remote access](ROADMAP.md#remote-access-alpha--rssh).
|
||||
|
||||
|
||||
Uses the **existing crashes vhost** — no new nginx `location` blocks for HTTP.
|
||||
|
||||
|
||||
@@ -22,57 +22,57 @@ endobj
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 705.2874 104.381 716.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 59.68504 705.8499 174.72 715.8499 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 694.2874 209.309 705.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 695.2874 209.309 706.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 59.68504 683.8499 100.125 693.8499 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 59.68504 684.8499 100.125 694.8499 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 59.68504 673.8499 159.315 683.8499 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 59.68504 674.8499 159.315 684.8499 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 59.68504 663.8499 168.9 673.8499 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 59.68504 664.8499 168.9 674.8499 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 59.68504 653.8499 145.1325 663.8499 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 59.68504 654.8499 145.1325 664.8499 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 59.68504 643.8499 87.19504 653.8499 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 59.68504 644.8499 87.19504 654.8499 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 59.68504 633.8499 95.52754 643.8499 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 59.68504 634.8499 95.52754 644.8499 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 623.2874 185.293 634.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 624.2874 185.293 635.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 612.2874 157.709 623.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 613.2874 157.709 624.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 601.2874 65.24504 612.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 21 0 R /Fit ] /Rect [ 45.68504 602.2874 65.24504 613.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
16 0 obj
|
||||
@@ -128,29 +128,30 @@ endobj
|
||||
endobj
|
||||
23 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Remote access \204 implementation notes \(v1 WireGuard\)) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
24 0 obj
|
||||
<<
|
||||
/Count 13 /First 25 0 R /Last 36 0 R /Type /Outlines
|
||||
/Count 14 /First 25 0 R /Last 36 0 R /Type /Outlines
|
||||
>>
|
||||
endobj
|
||||
25 0 obj
|
||||
<<
|
||||
/Dest [ 20 0 R /Fit ] /Next 26 0 R /Parent 24 0 R /Title (App \(developer settings\))
|
||||
/Count 1 /Dest [ 20 0 R /Fit ] /First 26 0 R /Last 26 0 R /Next 27 0 R /Parent 24 0 R
|
||||
/Title (App \(developer settings\))
|
||||
>>
|
||||
endobj
|
||||
26 0 obj
|
||||
<<
|
||||
/Dest [ 20 0 R /Fit ] /Next 27 0 R /Parent 24 0 R /Prev 25 0 R /Title (BE control plane)
|
||||
/Dest [ 20 0 R /Fit ] /Parent 25 0 R /Title (Android VPN consent \(WireGuard\))
|
||||
>>
|
||||
endobj
|
||||
27 0 obj
|
||||
<<
|
||||
/Count 6 /Dest [ 20 0 R /Fit ] /First 28 0 R /Last 33 0 R /Next 34 0 R /Parent 24 0 R
|
||||
/Prev 26 0 R /Title (Production deploy \(no new nginx HTTP paths\))
|
||||
/Prev 25 0 R /Title (Production deploy \(no new nginx HTTP paths\))
|
||||
>>
|
||||
endobj
|
||||
28 0 obj
|
||||
@@ -165,7 +166,7 @@ endobj
|
||||
endobj
|
||||
30 0 obj
|
||||
<<
|
||||
/Dest [ 20 0 R /Fit ] /Next 31 0 R /Parent 27 0 R /Prev 29 0 R /Title (3. BE packages and permissions)
|
||||
/Dest [ 21 0 R /Fit ] /Next 31 0 R /Parent 27 0 R /Prev 29 0 R /Title (3. BE packages and permissions)
|
||||
>>
|
||||
endobj
|
||||
31 0 obj
|
||||
@@ -205,24 +206,24 @@ endobj
|
||||
endobj
|
||||
38 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 807
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 815
|
||||
>>
|
||||
stream
|
||||
Gatn$D,8n?&BE]&;r#PO%52na4R=(!AX`gsK5>meKM:C6=VKQB.37;pQNZ$El<$;KUfOMOr-rqb^H2cD5.(:4.Y-On5+GKC%+6$?J-:t*_^$8YO6G';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
|
||||
39 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3647
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 4044
|
||||
>>
|
||||
stream
|
||||
Gatm>>BAQ-&q801fU"C6>DM4/PWV*ASD$Qg8Q*NooV(\d?kSV>;^;HsQ8Lhg^Rk[J,!f)#m+RYJ5QH_u`Ek`9Tn/*9>5l:G!dK%oUN[Vi3o%qF1DLR@5/QF+X7*7!EhmCu[VMih33K;a`n84TfWF;lD".i]BJiXmZ/B5ZPB:l'Wqc&q:9H#L^8*I/_@R=q-ZMbq$0\C,X1#%SXtIIq?<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
|
||||
40 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1666
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2140
|
||||
>>
|
||||
stream
|
||||
Gb!;cgMYb8&:O:S$rAdCSS39\8R6p1aL9kBE#^g6P8=8:MHibq.?`On>5$itQD>@S1+TqR*sr%[cL/oJ3HOo8!$*.Nrp6`e:0l4!$nt@o"#)98JU7P6pZUSW-mL=53!3BoTKb8hhA]Y.mc`7Ac@U_QW0GN@Tq!1gGrT.NiJpBm`#ptJ.njB&UYZ2/:^K0,o+U>"Y[4NF5*Zi5$P:KlNN`15\G+pQW%.dkZ&ZYNo_V*bea[[1@oWO?]CcSiHAoKDmD=Nb;:VOikZ@u5GS7p$,FB#c^@NO<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<TTO5%r9+@U=c6h)bLnGgD$ml^rq5'L]=9?4A3=K6eAR>^=V'lS:P2B.tcRlU,d"\2._i7m<=4>P]D)H?0!Q4cP#/Tn2CNj1$J~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 41
|
||||
@@ -232,45 +233,45 @@ xref
|
||||
0000000242 00000 n
|
||||
0000000354 00000 n
|
||||
0000000501 00000 n
|
||||
0000000648 00000 n
|
||||
0000000795 00000 n
|
||||
0000000942 00000 n
|
||||
0000001089 00000 n
|
||||
0000001234 00000 n
|
||||
0000001383 00000 n
|
||||
0000001532 00000 n
|
||||
0000001681 00000 n
|
||||
0000001829 00000 n
|
||||
0000001977 00000 n
|
||||
0000002126 00000 n
|
||||
0000002242 00000 n
|
||||
0000002541 00000 n
|
||||
0000002647 00000 n
|
||||
0000002725 00000 n
|
||||
0000002931 00000 n
|
||||
0000003137 00000 n
|
||||
0000003224 00000 n
|
||||
0000003555 00000 n
|
||||
0000003630 00000 n
|
||||
0000003738 00000 n
|
||||
0000003849 00000 n
|
||||
0000004028 00000 n
|
||||
0000004121 00000 n
|
||||
0000004248 00000 n
|
||||
0000004373 00000 n
|
||||
0000004495 00000 n
|
||||
0000004599 00000 n
|
||||
0000004691 00000 n
|
||||
0000004825 00000 n
|
||||
0000004953 00000 n
|
||||
0000005040 00000 n
|
||||
0000005115 00000 n
|
||||
0000006013 00000 n
|
||||
0000009752 00000 n
|
||||
0000000647 00000 n
|
||||
0000000794 00000 n
|
||||
0000000941 00000 n
|
||||
0000001088 00000 n
|
||||
0000001233 00000 n
|
||||
0000001382 00000 n
|
||||
0000001531 00000 n
|
||||
0000001680 00000 n
|
||||
0000001828 00000 n
|
||||
0000001976 00000 n
|
||||
0000002125 00000 n
|
||||
0000002241 00000 n
|
||||
0000002540 00000 n
|
||||
0000002646 00000 n
|
||||
0000002724 00000 n
|
||||
0000002930 00000 n
|
||||
0000003136 00000 n
|
||||
0000003223 00000 n
|
||||
0000003554 00000 n
|
||||
0000003629 00000 n
|
||||
0000003776 00000 n
|
||||
0000003878 00000 n
|
||||
0000004057 00000 n
|
||||
0000004150 00000 n
|
||||
0000004277 00000 n
|
||||
0000004402 00000 n
|
||||
0000004524 00000 n
|
||||
0000004628 00000 n
|
||||
0000004720 00000 n
|
||||
0000004854 00000 n
|
||||
0000004982 00000 n
|
||||
0000005069 00000 n
|
||||
0000005144 00000 n
|
||||
0000006050 00000 n
|
||||
0000010186 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<3c7062d12426f42b4b67ec68dbf63f96><3c7062d12426f42b4b67ec68dbf63f96>]
|
||||
[<0ea4a9599ca2b4815b3cdd11dc5a5773><0ea4a9599ca2b4815b3cdd11dc5a5773>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 23 0 R
|
||||
@@ -278,5 +279,5 @@ trailer
|
||||
/Size 41
|
||||
>>
|
||||
startxref
|
||||
11510
|
||||
12418
|
||||
%%EOF
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
%“Œ‹ž ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 11 0 R /F4 13 0 R /F5 14 0 R
|
||||
/F1 2 0 R /F2 3 0 R /F3 12 0 R /F4 14 0 R /F5 15 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
@@ -17,47 +17,52 @@ endobj
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 716.2874 147.501 727.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 716.2874 147.501 727.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 705.2874 70.13304 716.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 705.2874 70.13304 716.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 694.2874 156.381 705.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 694.2874 156.381 705.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 683.2874 129.285 694.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 683.2874 129.285 694.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 672.2874 182.189 683.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 17 0 R /Fit ] /Rect [ 45.68504 672.2874 217.317 683.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 661.2874 127.037 672.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 17 0 R /Fit ] /Rect [ 45.68504 661.2874 182.189 672.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 650.2874 103.045 661.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 17 0 R /Fit ] /Rect [ 45.68504 650.2874 127.037 661.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 17 0 R /Fit ] /Rect [ 45.68504 639.2874 103.045 650.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R ] /Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources <<
|
||||
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 29 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0
|
||||
/Trans <<
|
||||
@@ -65,29 +70,19 @@ endobj
|
||||
>> /Type /Page
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
14 0 obj
|
||||
<<
|
||||
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
15 0 obj
|
||||
<<
|
||||
/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources <<
|
||||
/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 29 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
@@ -97,83 +92,98 @@ endobj
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Outlines 19 0 R /PageMode /UseNone /Pages 27 0 R /Type /Catalog
|
||||
/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 29 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Remote access \204 validation \(Linux CLI + mobile parity\)) /Trapped /False
|
||||
/Outlines 20 0 R /PageMode /UseNone /Pages 29 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Count 7 /First 20 0 R /Last 26 0 R /Type /Outlines
|
||||
/Author (Android Cast project) /CreationDate (D:20260608122521+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122521+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Remote access \204 validation \(Linux CLI + mobile parity\)) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
20 0 obj
|
||||
<<
|
||||
/Dest [ 15 0 R /Fit ] /Next 21 0 R /Parent 19 0 R /Title (What the CLI suite simulates)
|
||||
/Count 8 /First 21 0 R /Last 28 0 R /Type /Outlines
|
||||
>>
|
||||
endobj
|
||||
21 0 obj
|
||||
<<
|
||||
/Dest [ 15 0 R /Fit ] /Next 22 0 R /Parent 19 0 R /Prev 20 0 R /Title (Scripts)
|
||||
/Dest [ 16 0 R /Fit ] /Next 22 0 R /Parent 20 0 R /Title (What the CLI suite simulates)
|
||||
>>
|
||||
endobj
|
||||
22 0 obj
|
||||
<<
|
||||
/Dest [ 15 0 R /Fit ] /Next 23 0 R /Parent 19 0 R /Prev 21 0 R /Title (Quick start \(local orchestration\))
|
||||
/Dest [ 16 0 R /Fit ] /Next 23 0 R /Parent 20 0 R /Prev 21 0 R /Title (Scripts)
|
||||
>>
|
||||
endobj
|
||||
23 0 obj
|
||||
<<
|
||||
/Dest [ 15 0 R /Fit ] /Next 24 0 R /Parent 19 0 R /Prev 22 0 R /Title (Production / staging BE)
|
||||
/Dest [ 16 0 R /Fit ] /Next 24 0 R /Parent 20 0 R /Prev 22 0 R /Title (Quick start \(local orchestration\))
|
||||
>>
|
||||
endobj
|
||||
24 0 obj
|
||||
<<
|
||||
/Dest [ 15 0 R /Fit ] /Next 25 0 R /Parent 19 0 R /Prev 23 0 R /Title (Optional: bring up WireGuard on Linux)
|
||||
/Dest [ 16 0 R /Fit ] /Next 25 0 R /Parent 20 0 R /Prev 23 0 R /Title (Production / staging BE)
|
||||
>>
|
||||
endobj
|
||||
25 0 obj
|
||||
<<
|
||||
/Dest [ 16 0 R /Fit ] /Next 26 0 R /Parent 19 0 R /Prev 24 0 R /Title (Mobile vs CLI checklist)
|
||||
/Dest [ 17 0 R /Fit ] /Next 26 0 R /Parent 20 0 R /Prev 24 0 R /Title (UDP / WireGuard data plane \(ra_udp_debug.sh\))
|
||||
>>
|
||||
endobj
|
||||
26 0 obj
|
||||
<<
|
||||
/Dest [ 16 0 R /Fit ] /Parent 19 0 R /Prev 25 0 R /Title (Troubleshooting)
|
||||
/Dest [ 17 0 R /Fit ] /Next 27 0 R /Parent 20 0 R /Prev 25 0 R /Title (Optional: bring up WireGuard on Linux)
|
||||
>>
|
||||
endobj
|
||||
27 0 obj
|
||||
<<
|
||||
/Count 3 /Kids [ 12 0 R 15 0 R 16 0 R ] /Type /Pages
|
||||
/Dest [ 17 0 R /Fit ] /Next 28 0 R /Parent 20 0 R /Prev 26 0 R /Title (Mobile vs CLI checklist)
|
||||
>>
|
||||
endobj
|
||||
28 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 691
|
||||
/Dest [ 17 0 R /Fit ] /Parent 20 0 R /Prev 27 0 R /Title (Troubleshooting)
|
||||
>>
|
||||
stream
|
||||
Gatn#?Z4XP'ZJu*'_d`-RN">.rq)2uF@)^s/_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
|
||||
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
|
||||
30 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2296
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 732
|
||||
>>
|
||||
stream
|
||||
Gatm=D0)19&H;*)Z'2UP8fM3!=-60@34@82A%/J7[U1l/?oU^o""/.teMI0oh*V5e"@>'g-*^7Tm-[#\\,/:@J;PG3s6E/T+(oQgM#bq2[K'bb#0^sn4M,jL+?N66<324])ISUqMQ&ZA5)7V^+=V_QM@$TpLamo2jrR%@TpgIo,n-pl.8A_PbeE'fqNA1;2%N%k%^S-++o"0[WLrTPFq+_<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
|
||||
xref
|
||||
0 31
|
||||
0 33
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000135 00000 n
|
||||
@@ -186,35 +196,37 @@ xref
|
||||
0000001090 00000 n
|
||||
0000001237 00000 n
|
||||
0000001385 00000 n
|
||||
0000001501 00000 n
|
||||
0000001762 00000 n
|
||||
0000001868 00000 n
|
||||
0000001946 00000 n
|
||||
0000002152 00000 n
|
||||
0000002358 00000 n
|
||||
0000002445 00000 n
|
||||
0000002779 00000 n
|
||||
0000002853 00000 n
|
||||
0000002963 00000 n
|
||||
0000003065 00000 n
|
||||
0000003195 00000 n
|
||||
0000003313 00000 n
|
||||
0000003445 00000 n
|
||||
0000003563 00000 n
|
||||
0000003660 00000 n
|
||||
0000003735 00000 n
|
||||
0000004517 00000 n
|
||||
0000008009 00000 n
|
||||
0000001533 00000 n
|
||||
0000001649 00000 n
|
||||
0000001917 00000 n
|
||||
0000002023 00000 n
|
||||
0000002101 00000 n
|
||||
0000002307 00000 n
|
||||
0000002513 00000 n
|
||||
0000002600 00000 n
|
||||
0000002934 00000 n
|
||||
0000003008 00000 n
|
||||
0000003118 00000 n
|
||||
0000003220 00000 n
|
||||
0000003350 00000 n
|
||||
0000003468 00000 n
|
||||
0000003609 00000 n
|
||||
0000003741 00000 n
|
||||
0000003859 00000 n
|
||||
0000003956 00000 n
|
||||
0000004031 00000 n
|
||||
0000004854 00000 n
|
||||
0000008387 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<c0e727411e9547578b92662630584baf><c0e727411e9547578b92662630584baf>]
|
||||
[<4aefba77718541cc2646cdb6a66281c0><4aefba77718541cc2646cdb6a66281c0>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 18 0 R
|
||||
/Root 17 0 R
|
||||
/Size 31
|
||||
/Info 19 0 R
|
||||
/Root 18 0 R
|
||||
/Size 33
|
||||
>>
|
||||
startxref
|
||||
10397
|
||||
11533
|
||||
%%EOF
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Android Cast — implementation roadmap
|
||||
|
||||
Last updated: 2026-05-23. Living document — refine as alpha and commercial tracks progress.
|
||||
Last updated: 2026-06-04. Living document — refine as alpha and commercial tracks progress.
|
||||
|
||||
---
|
||||
|
||||
@@ -22,6 +22,8 @@ Last updated: 2026-05-23. Living document — refine as alpha and commercial tra
|
||||
- [libvpx / Opus / Speex](#libvpx-opus-speex)
|
||||
- [Transports & protocols](#transports-protocols)
|
||||
- [Receiver / sender UX](#receiver-sender-ux)
|
||||
- [Remote access (alpha — RSSH)](#remote-access-alpha--rssh)
|
||||
- [Auth & email (alpha)](#auth--email-alpha)
|
||||
- [Deferred / post-alpha](#deferred-post-alpha)
|
||||
- [Related docs](#related-docs)
|
||||
<!-- /toc -->
|
||||
@@ -35,7 +37,7 @@ Last updated: 2026-05-23. Living document — refine as alpha and commercial tra
|
||||
| Track | Goal | Status |
|
||||
|-------|------|--------|
|
||||
| **Android app** | Stable, feature-rich, **demo-ready** on LAN (discover → PIN → cast → stop; soak two devices) | Core path done; polish UX/docs and gate experimental options for show |
|
||||
| **Backend** | Deployed **OTA channel** + **crash ingest** (+ optional MQTT per `docs/OTA.md`) | WIP — examples under `examples/`; deployment blocked |
|
||||
| **Backend** | Deployed **OTA channel** + **crash ingest** + **on-demand remote access (RSSH, prod/hidden)** | WIP — examples under `examples/`; WireGuard v1 lab path done; RSSH implementation in progress |
|
||||
|
||||
**Alpha app** can ship before backend is live if the demo is LAN-only. **Full alpha** includes OTA/crash URLs configured in developer settings or `local.properties`.
|
||||
|
||||
@@ -58,6 +60,44 @@ Last updated: 2026-05-23. Living document — refine as alpha and commercial tra
|
||||
- [ ] Host `examples/ota/v0/` channel JSON + APK artifacts
|
||||
- [ ] Deploy `examples/crash_reporter/backend/` (see `nginx.conf`, `README.md`)
|
||||
- [ ] Set `ota.channel.url` and crash upload URL in dev settings / `local.properties`
|
||||
- [ ] **RSSH remote access (alpha essential):** device opt-in via `heartbeat.php` (`type: ra`, `opt_in_mode: rssh`); outbound SSH reverse tunnel to bastion; **no VPN permission / no status-bar key**; operator whitelist + session open in BE UI; E2E validation per [REMOTE_ACCESS_VALIDATION.md](REMOTE_ACCESS_VALIDATION.md) (RSSH path TBD)
|
||||
- [ ] WireGuard v1 remains available for lab/E2E; not required for alpha sign-off if RSSH meets owner criteria
|
||||
- [ ] **Email infra (alpha essential):** `info@` / `admin@` / `root@` on `apps.f0xx.org` → Gmail forward; SPF/DKIM/DMARC — [20260607-2FA-email-mobile-auth-flow.md §9](20260607-2FA-email-mobile-auth-flow.md#9-email-addresses-agent-config)
|
||||
- [ ] **Registration + 2FA (alpha essential):** email verify + TOTP + mobile-friendly login/register — [§10](20260607-2FA-email-mobile-auth-flow.md#10-registration-2fa-mobile-auth)
|
||||
|
||||
---
|
||||
|
||||
## Remote access (alpha — RSSH)
|
||||
|
||||
**Owner agreement:** production **RSSH** (reverse SSH, hidden — no `VpnService`) is an **essential part of the alpha release**, not post-alpha. WireGuard v1 is implemented for infra/E2E lab work; alpha remote-debug UX targets RSSH.
|
||||
|
||||
| Item | Status |
|
||||
|------|--------|
|
||||
| BE control plane (`heartbeat.php` `type: ra`, admin UI, RBAC, purge cron) | Done |
|
||||
| WireGuard v1 (device `VpnService`, UDP DNAT, `WireGuardPeerProvisioner`) | Done (lab) |
|
||||
| BE `opt_in_mode: rssh` + bastion/session provisioning | TODO |
|
||||
| Android RSSH mode selectable (no VPN prompt) | TODO |
|
||||
| Outbound reverse SSH client + foreground service | TODO |
|
||||
| Operator E2E (whitelist → open session → shell/file on bastion) | TODO |
|
||||
|
||||
Implementation notes: [REMOTE_ACCESS_IMPL.md](REMOTE_ACCESS_IMPL.md) · proposal: [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md).
|
||||
|
||||
---
|
||||
|
||||
## Auth & email (alpha)
|
||||
|
||||
**Owner agreement:** public email aliases + **registration with email verification and 2FA (TOTP)** are **alpha must-have** for the crashes/build consoles.
|
||||
|
||||
| Item | Status |
|
||||
|------|--------|
|
||||
| DNS forward `info@` / `admin@` / `root@` → Gmail | Ops TODO |
|
||||
| BE SMTP outbound (`noreply@apps.f0xx.org`) | TODO |
|
||||
| Migration `008_auth_email_2fa` | TODO |
|
||||
| Register + verify + TOTP enroll/login | TODO |
|
||||
| WebAuthn / passkeys (optional alpha) | TODO |
|
||||
| Admin: recovery email, clear lockouts, audit | TODO |
|
||||
|
||||
Full spec: [20260607-2FA-email-mobile-auth-flow.md](20260607-2FA-email-mobile-auth-flow.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -143,7 +183,8 @@ Enable in **Settings → Stream protection (UDP)** on both sender and receiver,
|
||||
| `LibvpxVideoEncoderSink` (surface + buffer) | Done |
|
||||
| libvpx decoder on receiver (`LibvpxCapableVideoDecoder`) | Done |
|
||||
| Surface → libvpx without MediaCodec passthrough on cast | Done (ImageReader) |
|
||||
| Opus / Speex build + sinks | TODO |
|
||||
| Opus / Speex cross-compile + CMake autolink (`build-native-codecs.sh`) | Done |
|
||||
| Opus / Speex JNI encode/decode sinks | TODO |
|
||||
|
||||
See [ndk/README.md](../ndk/README.md).
|
||||
|
||||
|
||||
253
docs/ROADMAP.pdf
253
docs/ROADMAP.pdf
@@ -2,7 +2,7 @@
|
||||
%“Œ‹ž ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 17 0 R /F4 19 0 R /F5 20 0 R /F6 21 0 R
|
||||
/F1 2 0 R /F2 3 0 R /F3 19 0 R /F4 21 0 R /F5 22 0 R /F6 23 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
@@ -17,78 +17,88 @@ endobj
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 716.2874 100.381 727.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 45.68504 716.2874 100.381 727.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 705.8499 124.2975 715.8499 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 59.68504 705.8499 124.2975 715.8499 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 695.8499 140.1375 705.8499 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 59.68504 695.8499 140.1375 705.8499 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 685.2874 113.717 696.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 45.68504 685.2874 162.605 696.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 674.2874 265.741 685.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 45.68504 673.6874 118.157 683.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 663.2874 151.037 674.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 45.68504 663.2874 113.717 674.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 652.2874 131.949 663.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 45.68504 652.2874 265.741 663.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 641.2874 161.269 652.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 45.68504 641.2874 151.037 652.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 630.2874 121.277 641.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 45.68504 630.2874 131.949 641.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 618.6874 126.157 628.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 25 0 R /Fit ] /Rect [ 45.68504 619.2874 161.269 630.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 608.2874 121.709 619.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 26 0 R /Fit ] /Rect [ 45.68504 608.2874 121.277 619.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 45.68504 597.2874 120.829 608.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 26 0 R /Fit ] /Rect [ 45.68504 596.6874 126.157 606.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 24 0 R /Fit ] /Rect [ 45.68504 586.2874 92.37304 597.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 26 0 R /Fit ] /Rect [ 45.68504 586.2874 121.709 597.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 26 0 R /Fit ] /Rect [ 45.68504 575.2874 120.829 586.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 26 0 R /Fit ] /Rect [ 45.68504 564.2874 92.37304 575.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
20 0 obj
|
||||
<<
|
||||
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R
|
||||
14 0 R 15 0 R 16 0 R ] /Contents 42 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 41 0 R /Resources <<
|
||||
14 0 R 15 0 R 16 0 R 17 0 R 18 0 R ] /Contents 47 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0
|
||||
/Trans <<
|
||||
@@ -96,44 +106,24 @@ endobj
|
||||
>> /Type /Page
|
||||
>>
|
||||
endobj
|
||||
19 0 obj
|
||||
21 0 obj
|
||||
<<
|
||||
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
20 0 obj
|
||||
22 0 obj
|
||||
<<
|
||||
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
21 0 obj
|
||||
23 0 obj
|
||||
<<
|
||||
/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
22 0 obj
|
||||
<<
|
||||
/Contents 43 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 41 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
23 0 obj
|
||||
<<
|
||||
/Contents 44 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 41 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
24 0 obj
|
||||
<<
|
||||
/Contents 45 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 41 0 R /Resources <<
|
||||
/Contents 48 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
@@ -143,121 +133,168 @@ endobj
|
||||
endobj
|
||||
25 0 obj
|
||||
<<
|
||||
/Outlines 27 0 R /PageMode /UseNone /Pages 41 0 R /Type /Catalog
|
||||
/Contents 49 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
26 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Android Cast \204 implementation roadmap) /Trapped /False
|
||||
/Contents 50 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
27 0 obj
|
||||
<<
|
||||
/Count 14 /First 28 0 R /Last 40 0 R /Type /Outlines
|
||||
/Contents 51 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
28 0 obj
|
||||
<<
|
||||
/Count 2 /Dest [ 22 0 R /Fit ] /First 29 0 R /Last 30 0 R /Next 31 0 R /Parent 27 0 R
|
||||
/Title (Alpha definition)
|
||||
/Outlines 30 0 R /PageMode /UseNone /Pages 46 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
29 0 obj
|
||||
<<
|
||||
/Dest [ 22 0 R /Fit ] /Next 30 0 R /Parent 28 0 R /Title (Alpha app checklist)
|
||||
/Author (Android Cast project) /CreationDate (D:20260608122522+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122522+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Android Cast \204 implementation roadmap) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
30 0 obj
|
||||
<<
|
||||
/Dest [ 22 0 R /Fit ] /Parent 28 0 R /Prev 29 0 R /Title (Alpha backend checklist)
|
||||
/Count 16 /First 31 0 R /Last 45 0 R /Type /Outlines
|
||||
>>
|
||||
endobj
|
||||
31 0 obj
|
||||
<<
|
||||
/Dest [ 22 0 R /Fit ] /Next 32 0 R /Parent 27 0 R /Prev 28 0 R /Title (Product positioning)
|
||||
/Count 2 /Dest [ 24 0 R /Fit ] /First 32 0 R /Last 33 0 R /Next 34 0 R /Parent 30 0 R
|
||||
/Title (Alpha definition)
|
||||
>>
|
||||
endobj
|
||||
32 0 obj
|
||||
<<
|
||||
/Dest [ 22 0 R /Fit ] /Next 33 0 R /Parent 27 0 R /Prev 31 0 R /Title (USB-C / HDMI \(roadmap D\) and USB-tether cast \(roadmap E\))
|
||||
/Dest [ 24 0 R /Fit ] /Next 33 0 R /Parent 31 0 R /Title (Alpha app checklist)
|
||||
>>
|
||||
endobj
|
||||
33 0 obj
|
||||
<<
|
||||
/Dest [ 22 0 R /Fit ] /Next 34 0 R /Parent 27 0 R /Prev 32 0 R /Title (Commercial \(Play Store prep\))
|
||||
/Dest [ 24 0 R /Fit ] /Parent 31 0 R /Prev 32 0 R /Title (Alpha backend checklist)
|
||||
>>
|
||||
endobj
|
||||
34 0 obj
|
||||
<<
|
||||
/Dest [ 23 0 R /Fit ] /Next 35 0 R /Parent 27 0 R /Prev 33 0 R /Title (Crash console open API)
|
||||
/Dest [ 24 0 R /Fit ] /Next 35 0 R /Parent 30 0 R /Prev 31 0 R /Title (Remote access \(alpha \204 RSSH\))
|
||||
>>
|
||||
endobj
|
||||
35 0 obj
|
||||
<<
|
||||
/Dest [ 23 0 R /Fit ] /Next 36 0 R /Parent 27 0 R /Prev 34 0 R /Title (Stream protection \(FEC / NACK\))
|
||||
/Dest [ 24 0 R /Fit ] /Next 36 0 R /Parent 30 0 R /Prev 34 0 R /Title (Auth & email \(alpha\))
|
||||
>>
|
||||
endobj
|
||||
36 0 obj
|
||||
<<
|
||||
/Dest [ 23 0 R /Fit ] /Next 37 0 R /Parent 27 0 R /Prev 35 0 R /Title (libvpx / Opus / Speex)
|
||||
/Dest [ 25 0 R /Fit ] /Next 37 0 R /Parent 30 0 R /Prev 35 0 R /Title (Product positioning)
|
||||
>>
|
||||
endobj
|
||||
37 0 obj
|
||||
<<
|
||||
/Dest [ 23 0 R /Fit ] /Next 38 0 R /Parent 27 0 R /Prev 36 0 R /Title (Transports & protocols)
|
||||
/Dest [ 25 0 R /Fit ] /Next 38 0 R /Parent 30 0 R /Prev 36 0 R /Title (USB-C / HDMI \(roadmap D\) and USB-tether cast \(roadmap E\))
|
||||
>>
|
||||
endobj
|
||||
38 0 obj
|
||||
<<
|
||||
/Dest [ 23 0 R /Fit ] /Next 39 0 R /Parent 27 0 R /Prev 37 0 R /Title (Receiver / sender UX)
|
||||
/Dest [ 25 0 R /Fit ] /Next 39 0 R /Parent 30 0 R /Prev 37 0 R /Title (Commercial \(Play Store prep\))
|
||||
>>
|
||||
endobj
|
||||
39 0 obj
|
||||
<<
|
||||
/Dest [ 24 0 R /Fit ] /Next 40 0 R /Parent 27 0 R /Prev 38 0 R /Title (Deferred / post-alpha)
|
||||
/Dest [ 25 0 R /Fit ] /Next 40 0 R /Parent 30 0 R /Prev 38 0 R /Title (Crash console open API)
|
||||
>>
|
||||
endobj
|
||||
40 0 obj
|
||||
<<
|
||||
/Dest [ 24 0 R /Fit ] /Parent 27 0 R /Prev 39 0 R /Title (Related docs)
|
||||
/Dest [ 25 0 R /Fit ] /Next 41 0 R /Parent 30 0 R /Prev 39 0 R /Title (Stream protection \(FEC / NACK\))
|
||||
>>
|
||||
endobj
|
||||
41 0 obj
|
||||
<<
|
||||
/Count 4 /Kids [ 18 0 R 22 0 R 23 0 R 24 0 R ] /Type /Pages
|
||||
/Dest [ 26 0 R /Fit ] /Next 42 0 R /Parent 30 0 R /Prev 40 0 R /Title (libvpx / Opus / Speex)
|
||||
>>
|
||||
endobj
|
||||
42 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 821
|
||||
/Dest [ 26 0 R /Fit ] /Next 43 0 R /Parent 30 0 R /Prev 41 0 R /Title (Transports & protocols)
|
||||
>>
|
||||
stream
|
||||
Gau1->u03/'Sc)R/'c[E-=Q6dcH#?F9/SOC>oTSFoYrjCc'lSBm/m1ZF4*st%Rr^O+sg*6SG1-PEfNio]7VqQ?36XoE&&b*4@g8R!YdnG!G?-8B9-jR",as_iW,]$6J,'82pVpX*.'mB!Vo<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
|
||||
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
|
||||
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
|
||||
45 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1512
|
||||
/Dest [ 26 0 R /Fit ] /Parent 30 0 R /Prev 44 0 R /Title (Related docs)
|
||||
>>
|
||||
endobj
|
||||
46 0 obj
|
||||
<<
|
||||
/Count 5 /Kids [ 20 0 R 24 0 R 25 0 R 26 0 R 27 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
47 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 871
|
||||
>>
|
||||
stream
|
||||
GatU3;0/I&&:X@To[?i![iurNg`)*;G\*TMP,k]W'fCP>f`4BI0^J]6CX@UT\N(:[;IHA7qO=\'mMR8]nWM%E0*8Y&?ndj2P/1:PU^.;eF"j0A92@TNNlHZ1PZ6#:+[5)s%P)ma.p_mY1m1A%W4F*Eps>@^OA]2OpB5miC1@F9pVS,\4#'8W]qWULQr`40s+>%l#cU444@6&KLEiO>!kfG#B%,D1.6>URR.GjIE'Ju2q)!`E1S8Rb"Ig_`-(OfcJ2pmKDheji/:%Y4l9Bar$I00$c3knAGh$KgTe)/@d:Y^==A7+*V\t!)_%=Y9E+13>.JPn0q\`0YM1ESeXqc@6@kiO!5a1`("]6=]&lY*/D&2nT(_aKTBhJ=;;u_Qd+$F$`CPjT-o57k,";P69Xb[PUAJTlW%@(ggp?JL>(4U0;X(H`>D3+6LlG0lWLFhcg<>Y7,08FOk%>kj"(l_Nm_*ek=9p;r9WRD%r:!e[aoFKK]rg-suQDB+@\P.dp<%d'0n.ptOCfG8g1>XBeZ6?slC@+i`"QdneWR,)Uar$()FN7rfMF@UOpefHrR)PSm1$+d-+ka[MF6JnU>'S&$oM2!K\IFqJ*m$D.L#0\I.!AkaP%5HfUb#kk?lSm*6'5C.s19>3oo9EOP:%J5B+ZRc=C0Rc16'Jo's]B+!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`aH&: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
|
||||
xref
|
||||
0 46
|
||||
0 52
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000146 00000 n
|
||||
@@ -275,45 +312,51 @@ xref
|
||||
0000001841 00000 n
|
||||
0000001989 00000 n
|
||||
0000002137 00000 n
|
||||
0000002286 00000 n
|
||||
0000002402 00000 n
|
||||
0000002708 00000 n
|
||||
0000002786 00000 n
|
||||
0000002892 00000 n
|
||||
0000002976 00000 n
|
||||
0000003182 00000 n
|
||||
0000003388 00000 n
|
||||
0000003594 00000 n
|
||||
0000003681 00000 n
|
||||
0000003996 00000 n
|
||||
0000004071 00000 n
|
||||
0000004208 00000 n
|
||||
0000004309 00000 n
|
||||
0000004414 00000 n
|
||||
0000004528 00000 n
|
||||
0000004683 00000 n
|
||||
0000004808 00000 n
|
||||
0000004925 00000 n
|
||||
0000005052 00000 n
|
||||
0000005168 00000 n
|
||||
0000005285 00000 n
|
||||
0000005400 00000 n
|
||||
0000005516 00000 n
|
||||
0000005610 00000 n
|
||||
0000005692 00000 n
|
||||
0000006604 00000 n
|
||||
0000010425 00000 n
|
||||
0000013534 00000 n
|
||||
0000002285 00000 n
|
||||
0000002433 00000 n
|
||||
0000002582 00000 n
|
||||
0000002698 00000 n
|
||||
0000003018 00000 n
|
||||
0000003096 00000 n
|
||||
0000003202 00000 n
|
||||
0000003286 00000 n
|
||||
0000003492 00000 n
|
||||
0000003698 00000 n
|
||||
0000003904 00000 n
|
||||
0000004110 00000 n
|
||||
0000004197 00000 n
|
||||
0000004512 00000 n
|
||||
0000004587 00000 n
|
||||
0000004724 00000 n
|
||||
0000004825 00000 n
|
||||
0000004930 00000 n
|
||||
0000005058 00000 n
|
||||
0000005175 00000 n
|
||||
0000005289 00000 n
|
||||
0000005444 00000 n
|
||||
0000005569 00000 n
|
||||
0000005686 00000 n
|
||||
0000005813 00000 n
|
||||
0000005929 00000 n
|
||||
0000006046 00000 n
|
||||
0000006161 00000 n
|
||||
0000006277 00000 n
|
||||
0000006371 00000 n
|
||||
0000006460 00000 n
|
||||
0000007422 00000 n
|
||||
0000011569 00000 n
|
||||
0000014910 00000 n
|
||||
0000018083 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<604792abc998f7bf7e113332cf4628d4><604792abc998f7bf7e113332cf4628d4>]
|
||||
[<b10a0e61bbf4903077fd80a61c513273><b10a0e61bbf4903077fd80a61c513273>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 26 0 R
|
||||
/Root 25 0 R
|
||||
/Size 46
|
||||
/Info 29 0 R
|
||||
/Root 28 0 R
|
||||
/Size 52
|
||||
>>
|
||||
startxref
|
||||
15138
|
||||
18384
|
||||
%%EOF
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
- [Attachments (deploy)](#attachments-deploy)
|
||||
- [Google Workspace — phase 2 (not implemented)](#google-workspace-phase-2-not-implemented)
|
||||
- [Agent / engineering queue (from your list)](#agent-engineering-queue-from-your-list)
|
||||
- [Open tasks dependency graph](#open-tasks-dependency-graph)
|
||||
<!-- /toc -->
|
||||
|
||||
**Documentation index:** [README.md](README.md)
|
||||
@@ -107,3 +108,11 @@ mysql -u root -p androidcast_crashes < examples/crash_reporter/backend/sql/migra
|
||||
5. **NDK** — Only if crash console shows native reports for these sessions.
|
||||
|
||||
Use ticket **comments** for findings; set **workflow** + **assignees** when you hand off.
|
||||
|
||||
---
|
||||
|
||||
## Open tasks dependency graph
|
||||
|
||||
**Master plan:** [20260608_ALPHA_PRIORITIES.md](20260608_ALPHA_PRIORITIES.md)
|
||||
**Diagram:** [OPEN_TASKS_GRAPH.md](OPEN_TASKS_GRAPH.md)
|
||||
**Dev DNS help:** [tmp/20260608_help_request.md](../tmp/20260608_help_request.md)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
%“Œ‹ž ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 5 0 R /F4 11 0 R /F5 13 0 R
|
||||
/F1 2 0 R /F2 3 0 R /F3 5 0 R /F4 12 0 R /F5 14 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
@@ -17,7 +17,7 @@ endobj
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 45.68504 716.2874 172.853 727.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 716.2874 172.853 727.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
@@ -27,37 +27,42 @@ endobj
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 45.68504 705.2874 199.845 716.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 705.2874 199.845 716.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 45.68504 694.2874 167.949 705.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 694.2874 167.949 705.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 59.68504 683.8499 130.545 693.8499 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 59.68504 683.8499 130.545 693.8499 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 59.68504 673.8499 224.3325 683.8499 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 59.68504 673.8499 224.3325 683.8499 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 663.2874 192.861 674.2874 ] /Subtype /Link /Type /Annot
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 663.2874 192.861 674.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
|
||||
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 652.2874 154.637 663.2874 ] /Subtype /Link /Type /Annot
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Annots [ 4 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R ] /Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 25 0 R /Resources <<
|
||||
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Annots [ 4 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0
|
||||
/Trans <<
|
||||
@@ -65,24 +70,14 @@ endobj
|
||||
>> /Type /Page
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
14 0 obj
|
||||
<<
|
||||
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 25 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 25 0 R /Resources <<
|
||||
/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
@@ -92,79 +87,94 @@ endobj
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Outlines 18 0 R /PageMode /UseNone /Pages 25 0 R /Type /Catalog
|
||||
/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 27 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Tickets, Gitea, and alpha smoke delegation) /Trapped /False
|
||||
/Outlines 19 0 R /PageMode /UseNone /Pages 27 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/Count 7 /First 19 0 R /Last 24 0 R /Type /Outlines
|
||||
/Author (Android Cast project) /CreationDate (D:20260608122522+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122522+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Tickets, Gitea, and alpha smoke delegation) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Dest [ 14 0 R /Fit ] /Next 20 0 R /Parent 18 0 R /Title (Gitea \(/app/androidcast_project/git/\))
|
||||
/Count 8 /First 20 0 R /Last 26 0 R /Type /Outlines
|
||||
>>
|
||||
endobj
|
||||
20 0 obj
|
||||
<<
|
||||
/Dest [ 14 0 R /Fit ] /Next 21 0 R /Parent 18 0 R /Prev 19 0 R /Title (\376\377\000A\000l\000p\000h\000a\000 \000s\000m\000o\000k\000e\000 \000t\000i\000c\000k\000e\000t\000s\000 \000\(\000t\000y\000p\000i\000c\000a\000l\000 \000D\000B\000 \000i\000d\000 !\222\000 \000b\000l\000o\000c\000k\000\))
|
||||
/Dest [ 15 0 R /Fit ] /Next 21 0 R /Parent 19 0 R /Title (Gitea \(/app/androidcast_project/git/\))
|
||||
>>
|
||||
endobj
|
||||
21 0 obj
|
||||
<<
|
||||
/Count 2 /Dest [ 14 0 R /Fit ] /First 22 0 R /Last 23 0 R /Next 24 0 R /Parent 18 0 R
|
||||
/Prev 20 0 R /Title (Console features \(implementation\))
|
||||
/Dest [ 15 0 R /Fit ] /Next 22 0 R /Parent 19 0 R /Prev 20 0 R /Title (\376\377\000A\000l\000p\000h\000a\000 \000s\000m\000o\000k\000e\000 \000t\000i\000c\000k\000e\000t\000s\000 \000\(\000t\000y\000p\000i\000c\000a\000l\000 \000D\000B\000 \000i\000d\000 !\222\000 \000b\000l\000o\000c\000k\000\))
|
||||
>>
|
||||
endobj
|
||||
22 0 obj
|
||||
<<
|
||||
/Dest [ 14 0 R /Fit ] /Next 23 0 R /Parent 21 0 R /Title (Attachments \(deploy\))
|
||||
/Count 2 /Dest [ 15 0 R /Fit ] /First 23 0 R /Last 24 0 R /Next 25 0 R /Parent 19 0 R
|
||||
/Prev 21 0 R /Title (Console features \(implementation\))
|
||||
>>
|
||||
endobj
|
||||
23 0 obj
|
||||
<<
|
||||
/Dest [ 14 0 R /Fit ] /Parent 21 0 R /Prev 22 0 R /Title (Google Workspace \204 phase 2 \(not implemented\))
|
||||
/Dest [ 15 0 R /Fit ] /Next 24 0 R /Parent 22 0 R /Title (Attachments \(deploy\))
|
||||
>>
|
||||
endobj
|
||||
24 0 obj
|
||||
<<
|
||||
/Dest [ 15 0 R /Fit ] /Parent 18 0 R /Prev 21 0 R /Title (Agent / engineering queue \(from your list\))
|
||||
/Dest [ 15 0 R /Fit ] /Parent 22 0 R /Prev 23 0 R /Title (Google Workspace \204 phase 2 \(not implemented\))
|
||||
>>
|
||||
endobj
|
||||
25 0 obj
|
||||
<<
|
||||
/Count 3 /Kids [ 12 0 R 14 0 R 15 0 R ] /Type /Pages
|
||||
/Dest [ 16 0 R /Fit ] /Next 26 0 R /Parent 19 0 R /Prev 22 0 R /Title (Agent / engineering queue \(from your list\))
|
||||
>>
|
||||
endobj
|
||||
26 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 711
|
||||
/Dest [ 16 0 R /Fit ] /Parent 19 0 R /Prev 25 0 R /Title (Open tasks dependency graph)
|
||||
>>
|
||||
stream
|
||||
Gatn#?#Q2d'Sc)J/'_]eFTrcnA9Vb:<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
|
||||
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
|
||||
>>
|
||||
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
|
||||
endobj
|
||||
28 0 obj
|
||||
30 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 735
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 921
|
||||
>>
|
||||
stream
|
||||
Gat=h:J^")&B4*cMC3Bm$ab&F[\05'@Za]qKj=jr&&!4O8`D&(KC)Lsg[@-"O_C`&WZiak`\W(Y2e*4(q%;FE>le:G%'Bb%KHu>/q_=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
|
||||
xref
|
||||
0 29
|
||||
0 31
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000134 00000 n
|
||||
@@ -177,33 +187,35 @@ xref
|
||||
0000001018 00000 n
|
||||
0000001166 00000 n
|
||||
0000001314 00000 n
|
||||
0000001430 00000 n
|
||||
0000001685 00000 n
|
||||
0000001791 00000 n
|
||||
0000001997 00000 n
|
||||
0000002203 00000 n
|
||||
0000002290 00000 n
|
||||
0000002607 00000 n
|
||||
0000002681 00000 n
|
||||
0000002802 00000 n
|
||||
0000003122 00000 n
|
||||
0000003291 00000 n
|
||||
0000003395 00000 n
|
||||
0000003526 00000 n
|
||||
0000003652 00000 n
|
||||
0000003727 00000 n
|
||||
0000004529 00000 n
|
||||
0000008590 00000 n
|
||||
0000001462 00000 n
|
||||
0000001578 00000 n
|
||||
0000001840 00000 n
|
||||
0000001946 00000 n
|
||||
0000002152 00000 n
|
||||
0000002358 00000 n
|
||||
0000002445 00000 n
|
||||
0000002762 00000 n
|
||||
0000002836 00000 n
|
||||
0000002957 00000 n
|
||||
0000003277 00000 n
|
||||
0000003446 00000 n
|
||||
0000003550 00000 n
|
||||
0000003681 00000 n
|
||||
0000003820 00000 n
|
||||
0000003929 00000 n
|
||||
0000004004 00000 n
|
||||
0000004839 00000 n
|
||||
0000008900 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<7f14806cee8a14988600c30857cea804><7f14806cee8a14988600c30857cea804>]
|
||||
[<8fe3abb3b6ae298d8e49cb9ed97a806a><8fe3abb3b6ae298d8e49cb9ed97a806a>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 17 0 R
|
||||
/Root 16 0 R
|
||||
/Size 29
|
||||
/Info 18 0 R
|
||||
/Root 17 0 R
|
||||
/Size 31
|
||||
>>
|
||||
startxref
|
||||
9416
|
||||
9912
|
||||
%%EOF
|
||||
|
||||
@@ -82,7 +82,7 @@ endobj
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Author (Android Cast project) /CreationDate (D:20260605123817+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260605123817+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Author (Android Cast project) /CreationDate (D:20260608122522+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260608122522+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (USB-C / HDMI and USB-tether cast \(roadmap D & E\)) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
@@ -159,7 +159,7 @@ xref
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<46e22eb997e0bc715c5afd1f1d8a9577><46e22eb997e0bc715c5afd1f1d8a9577>]
|
||||
[<2f3e1c2548fcb7ec850b1fea338ffefa><2f3e1c2548fcb7ec850b1fea338ffefa>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 15 0 R
|
||||
|
||||
@@ -14,6 +14,10 @@ from md_to_pdf import build_pdf_from_markdown # noqa: E402
|
||||
|
||||
SUBTITLE = {
|
||||
"20260602_REVERSE_SSH_proposals_summary.md": "Generated from markdown — rev. 1–3 + appendices WG test/production",
|
||||
"20260607-2FA-email-mobile-auth-flow.md": "Email forwarding + registration/2FA — alpha must-have",
|
||||
"20260608_ALPHA_PRIORITIES.md": "Master alpha priority plan — 2026-06-08 renumber",
|
||||
"20260608_STREAM_DUMP_DEBUG.md": "FR 0.1 stream dump while casting",
|
||||
"OPEN_TASKS_GRAPH.md": "Open tasks dependency graph — alpha critical path",
|
||||
"GIT_FLOW.md": "Branches, next, release — from markdown",
|
||||
"REMOTE_ACCESS_IMPL.md": "WireGuard v1 — implementation and deploy",
|
||||
"REMOTE_ACCESS_VALIDATION.md": "Linux CLI validation — device sim + E2E",
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
|
||||
## Next
|
||||
|
||||
- [ ] User registration + email verification
|
||||
- [ ] User registration + email verification + TOTP 2FA — **alpha must-have** — [docs/20260607-2FA-email-mobile-auth-flow.md](../../docs/20260607-2FA-email-mobile-auth-flow.md)
|
||||
- [ ] Email infra: `info@` / `admin@` / `root@` @ `apps.f0xx.org` → Gmail forward (§9)
|
||||
- [ ] RBAC editor UI (delegate roles per module)
|
||||
- [ ] Symbolication (ndk-stack / retrace) for native/Java stacks
|
||||
- [ ] MQTT ingest endpoint (parallel to HTTP POST)
|
||||
|
||||
@@ -311,7 +311,13 @@ BASE=https://apps.f0xx.org/app/androidcast_project/crashes ./scripts/ra_e2e_cli.
|
||||
|
||||
**CLI validation (Linux device simulator):** [docs/REMOTE_ACCESS_VALIDATION.md](../../../docs/REMOTE_ACCESS_VALIDATION.md).
|
||||
|
||||
Android: Developer settings → **Remote access** → WireGuard (RSSH reserved). Requires VPN permission; polls BE every 1–7 min while enabled.
|
||||
Android: Developer settings → **Remote access** → **RSSH** (alpha target; WireGuard lab-only). RSSH avoids VPN permission. Polls BE every 1–7 min while enabled. See [REMOTE_ACCESS_IMPL.md § VPN consent](../../../docs/REMOTE_ACCESS_IMPL.md#android-vpn-consent-wireguard) and [ROADMAP § RSSH](../../../docs/ROADMAP.md#remote-access-alpha--rssh).
|
||||
|
||||
## Third-party licenses (BE / landing)
|
||||
|
||||
- [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) — server stack, optional WireGuard tools, link to mobile app list.
|
||||
- Mobile APK list: in-app **Licenses** button → `app/src/main/assets/licenses/combined.html`.
|
||||
- Suggested public URL after sync: `…/crashes/THIRD_PARTY_LICENSES.md` (or HTML excerpt on hub legal page).
|
||||
|
||||
## Default accounts
|
||||
|
||||
|
||||
46
examples/crash_reporter/backend/THIRD_PARTY_LICENSES.md
Normal file
46
examples/crash_reporter/backend/THIRD_PARTY_LICENSES.md
Normal 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).
|
||||
@@ -58,5 +58,27 @@ return [
|
||||
'require_wg_tools' => false,
|
||||
'session_ttl_s' => 3600,
|
||||
'min_poll_interval_s' => 45,
|
||||
// Reverse SSH (alpha): outbound -R from device; no VpnService on phone
|
||||
'rssh' => [
|
||||
'bastion_host' => 'ra.apps.f0xx.org',
|
||||
'bastion_port' => 443,
|
||||
'username_prefix' => 'ra-',
|
||||
'local_forward_port' => 8022,
|
||||
'remote_port_min' => 18000,
|
||||
'remote_port_max' => 18999,
|
||||
],
|
||||
],
|
||||
// Outbound mail (registration verify, password reset) — see docs/20260607-2FA-email-mobile-auth-flow.md
|
||||
'mail' => [
|
||||
'transport' => 'smtp', // smtp | sendmail
|
||||
'from' => 'Android Cast Issues <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
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
26
examples/crash_reporter/backend/public/api/auth_register.php
Normal file
26
examples/crash_reporter/backend/public/api/auth_register.php
Normal 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);
|
||||
@@ -50,7 +50,7 @@
|
||||
(devices || []).forEach((d) => {
|
||||
const tr = document.createElement('tr');
|
||||
const wl = Number(d.whitelisted) === 1;
|
||||
const canOpen = canOperate() && wl && (d.opt_in_mode === 'wireguard');
|
||||
const canOpen = canOperate() && wl && (d.opt_in_mode === 'wireguard' || d.opt_in_mode === 'rssh');
|
||||
const openDisabled = canOpen ? '' : ' disabled title="Whitelist device and wait for WireGuard opt-in poll"';
|
||||
const wlDisabled = canAdmin() ? '' : ' disabled';
|
||||
tr.innerHTML =
|
||||
|
||||
@@ -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 G2–G6.'],
|
||||
['key' => '5.5', 'owner' => 'agent', 'text' => 'P5.5 RBAC validation.'],
|
||||
['key' => '6.1', 'owner' => 'dev', 'text' => 'P6.1 Opus/Speex E2E — dev + agent JNI.'],
|
||||
['key' => '6.2', 'owner' => 'dev', 'text' => 'P6.2 LAN soak A/B/C.'],
|
||||
['key' => '6.3', 'owner' => 'dev', 'text' => 'P6.3 App lifecycle notifications/recents.'],
|
||||
['key' => '6.4', 'owner' => 'dev', 'text' => 'P6.4 Rotation + preview crash — cyclic with 6.3.'],
|
||||
['key' => '6.5', 'owner' => 'dev', 'text' => 'P6.5 Audio lag — use 0.1 dumps for analysis.'],
|
||||
['key' => '6.6', 'owner' => 'dev', 'text' => 'P6.6 Stream debug analysis — same as 0.1.'],
|
||||
['key' => '9', 'owner' => 'agent', 'text' => 'P9 Landing hub — LOW priority.'],
|
||||
['key' => '9.1', 'owner' => 'agent', 'text' => 'P9.1 Hub cards graphs/builder/RA.'],
|
||||
['key' => '9.2', 'owner' => 'agent', 'text' => 'P9.2 Globe animation branch cleanup.'],
|
||||
['key' => '9.3', 'owner' => 'agent', 'text' => 'P9.3 Mobile chrome validation LOW.'],
|
||||
];
|
||||
|
||||
$now = (int) round(microtime(true) * 1000);
|
||||
$ok = 0;
|
||||
$fail = 0;
|
||||
|
||||
foreach ($tasks as $task) {
|
||||
$text = $task['text'];
|
||||
$ownerTag = $task['owner'];
|
||||
$payload = [
|
||||
'schema_version' => 1,
|
||||
'ingest_kind' => 'ticket',
|
||||
'ticket_id' => 'priority-20260608-' . preg_replace('/[^a-z0-9.-]+/i', '-', $task['key']),
|
||||
'title' => mb_substr($text, 0, 256),
|
||||
'brief' => mb_substr($text, 0, 512),
|
||||
'body' => $text . "\n\nRef: docs/20260608_ALPHA_PRIORITIES.md",
|
||||
'opened_at_epoch_ms' => $now,
|
||||
'rating' => 3,
|
||||
'owner' => $owner,
|
||||
'tags' => priority_tags($ownerTag),
|
||||
'device' => ['manufacturer' => 'roadmap', 'model' => 'priorities', 'sdk_int' => 34, 'release' => '14'],
|
||||
'app' => ['package' => 'com.foxx.androidcast', 'version_name' => 'roadmap', 'version_code' => 1],
|
||||
];
|
||||
if ($dryRun) {
|
||||
echo "DRY {$task['key']} [{$ownerTag}]\n";
|
||||
$ok++;
|
||||
continue;
|
||||
}
|
||||
$ctx = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => "Content-Type: application/json\r\n",
|
||||
'content' => json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
|
||||
'ignore_errors' => true,
|
||||
],
|
||||
]);
|
||||
$resp = @file_get_contents($uploadUrl, false, $ctx);
|
||||
$code = 0;
|
||||
if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) {
|
||||
$code = (int) $m[1];
|
||||
}
|
||||
if ($code === 200 || $code === 409) {
|
||||
echo ($code === 409 ? 'DUP ' : 'OK ') . "{$task['key']}\n";
|
||||
$ok++;
|
||||
} else {
|
||||
echo "FAIL {$task['key']} HTTP {$code} — {$resp}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
echo "Done: ok={$ok} fail={$fail}\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -35,7 +35,9 @@ $tasks = [
|
||||
['key' => '1', 'status' => 'open', 'text' => 'BE/builder deployment phases — dedicated services on BE; WIP verifying (Docker, out/builds, runner_script).'],
|
||||
['key' => '2', 'status' => 'open', 'text' => 'OTA checks — part of builder/deploy path; verify artifacts and device OTA flow.'],
|
||||
['key' => '3', 'status' => 'open', 'text' => 'Opus/Speex validation — E2E, encryption, FEC, NACK, negotiations; user and developer controls.'],
|
||||
['key' => '4', 'status' => 'open', 'text' => 'RSSH/WG (WireGuard v1) — finish deploy, prod validations, minimal tests; RA E2E CLI + Android poll.'],
|
||||
['key' => '4', 'status' => 'open', 'text' => 'Alpha: RSSH remote access (prod/hidden, no VPN) — BE bastion + Android reverse SSH client; WG v1 lab only. Opus/Speex JNI sinks TODO.'],
|
||||
['key' => 'auth-2fa', 'status' => 'open', 'text' => 'Alpha: registration + email verify + TOTP 2FA — mobile-friendly; depends on email DNS/SMTP. See docs/20260607-2FA-email-mobile-auth-flow.md.'],
|
||||
['key' => 'email-dns', 'status' => 'open', 'text' => 'Alpha: email infra — info@/admin@/root@ @ apps.f0xx.org forward to Gmail; SPF/DKIM/DMARC. Blocks auth mail.'],
|
||||
['key' => '5', 'status' => 'open', 'text' => 'Data visualization / graphs — WIP; issues found on drill-down and layout.'],
|
||||
['key' => '5.1', 'status' => 'open', 'text' => 'ISSUE 5.1: Graph brick click must open full-browser detailed chart (axis, legend, hover per point like Java visualizer).'],
|
||||
['key' => '5.2', 'status' => 'open', 'text' => 'REQUIREMENT 5.2: All graph preview bricks clickable; Columns combo (1/2/4) after Window default 2; grid uses full width.'],
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
-- RSSH columns for SQLite (applied via Database::ensureSchema).
|
||||
133
examples/crash_reporter/backend/src/AuthEmailSchema.php
Normal file
133
examples/crash_reporter/backend/src/AuthEmailSchema.php
Normal 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'))
|
||||
)"
|
||||
);
|
||||
}
|
||||
}
|
||||
142
examples/crash_reporter/backend/src/AuthMailer.php
Normal file
142
examples/crash_reporter/backend/src/AuthMailer.php
Normal 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);
|
||||
}
|
||||
}
|
||||
94
examples/crash_reporter/backend/src/AuthRegistration.php
Normal file
94
examples/crash_reporter/backend/src/AuthRegistration.php
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -99,6 +99,7 @@ final class Database {
|
||||
self::ensureReportColumns($pdo);
|
||||
self::ensureReportViewsTable($pdo);
|
||||
self::ensureRbacSchema($pdo);
|
||||
AuthEmailSchema::ensure($pdo);
|
||||
return;
|
||||
}
|
||||
$schemaFile = __DIR__ . '/../sql/schema.sqlite.sql';
|
||||
@@ -112,6 +113,7 @@ final class Database {
|
||||
self::ensureReportColumns($pdo);
|
||||
self::ensureReportViewsTable($pdo);
|
||||
self::ensureRbacSchema($pdo);
|
||||
AuthEmailSchema::ensure($pdo);
|
||||
}
|
||||
|
||||
private static function ensureMysqlSchema(PDO $pdo): void {
|
||||
@@ -156,6 +158,7 @@ final class Database {
|
||||
}
|
||||
self::ensureMysqlReportViewsTable($pdo);
|
||||
self::ensureRbacSchema($pdo);
|
||||
AuthEmailSchema::ensure($pdo);
|
||||
self::ensureTicketsTable($pdo);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* On-demand remote access control plane (heartbeat type: ra).
|
||||
* v1: WireGuard opt-in from device; reverse SSH (RSSH) reserved for future use.
|
||||
* v1: WireGuard + reverse SSH (RSSH) opt-in from device (lab / alpha).
|
||||
*/
|
||||
final class RemoteAccessRepository {
|
||||
public const OPT_NONE = 'none';
|
||||
@@ -23,6 +23,7 @@ final class RemoteAccessRepository {
|
||||
return;
|
||||
}
|
||||
self::ensureSessionColumns($pdo);
|
||||
self::ensureRsshColumns($pdo);
|
||||
}
|
||||
|
||||
/** @param list<string> $where @param list<mixed> $params */
|
||||
@@ -67,6 +68,30 @@ final class RemoteAccessRepository {
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
private static function ensureRsshColumns(PDO $pdo): void {
|
||||
if (!Database::tableExists($pdo, 'remote_access_sessions')) {
|
||||
return;
|
||||
}
|
||||
$cols = [
|
||||
'rssh_bastion_host' => Database::isMysql() ? 'VARCHAR(255) NULL' : 'TEXT NULL',
|
||||
'rssh_bastion_port' => Database::isMysql() ? 'INT UNSIGNED NULL' : 'INTEGER NULL',
|
||||
'rssh_username' => Database::isMysql() ? 'VARCHAR(64) NULL' : 'TEXT NULL',
|
||||
'rssh_secret' => Database::isMysql() ? 'VARCHAR(128) NULL' : 'TEXT NULL',
|
||||
'rssh_remote_port' => Database::isMysql() ? 'INT UNSIGNED NULL' : 'INTEGER NULL',
|
||||
'rssh_local_port' => Database::isMysql() ? 'INT UNSIGNED NULL' : 'INTEGER NULL',
|
||||
];
|
||||
foreach ($cols as $name => $ddl) {
|
||||
try {
|
||||
$pdo->exec('ALTER TABLE remote_access_sessions ADD COLUMN ' . $name . ' ' . $ddl);
|
||||
} catch (PDOException $e) {
|
||||
$msg = strtolower($e->getMessage());
|
||||
if (!str_contains($msg, 'duplicate') && !str_contains($msg, 'already exists')) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function ensureSessionColumns(PDO $pdo): void {
|
||||
if (!Database::tableExists($pdo, 'remote_access_sessions')) {
|
||||
return;
|
||||
@@ -142,7 +167,13 @@ final class RemoteAccessRepository {
|
||||
}
|
||||
|
||||
if ($tunnelMode === self::OPT_RSSH) {
|
||||
return self::raEnvelope('deny', ['reason' => 'rssh_not_implemented']);
|
||||
$session = $pendingSession ?? self::findConnectableSession($deviceId, $random);
|
||||
if ($session === null) {
|
||||
self::logEvent($deviceId, null, null, 'poll_wait', 'no_pending_session', null, $clientIp);
|
||||
return self::raEnvelope('wait', ['reason' => 'no_pending_session']);
|
||||
}
|
||||
self::touchSessionActivity((string) ($session['session_id'] ?? ''));
|
||||
return self::buildConnectResponse($session, $deviceId, $clientIp);
|
||||
}
|
||||
|
||||
$session = $pendingSession ?? self::findConnectableSession($deviceId, $random);
|
||||
@@ -279,6 +310,15 @@ final class RemoteAccessRepository {
|
||||
|
||||
/** @param array<string, mixed> $session */
|
||||
private static function buildConnectResponse(array $session, string $deviceId, string $clientIp): array {
|
||||
$tunnel = (string) ($session['tunnel'] ?? 'wireguard');
|
||||
if ($tunnel === 'ssh_reverse') {
|
||||
return self::buildRsshConnectResponse($session, $deviceId, $clientIp);
|
||||
}
|
||||
return self::buildWireGuardConnectResponse($session, $deviceId, $clientIp);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $session */
|
||||
private static function buildWireGuardConnectResponse(array $session, string $deviceId, string $clientIp): array {
|
||||
$sessionId = (string) ($session['session_id'] ?? '');
|
||||
$tunnel = (string) ($session['tunnel'] ?? 'wireguard');
|
||||
if ($tunnel !== 'wireguard') {
|
||||
@@ -318,6 +358,67 @@ final class RemoteAccessRepository {
|
||||
]);
|
||||
}
|
||||
|
||||
/** @param array<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} */
|
||||
private static function ensureSessionWireGuardKeys(string $sessionId, array $session): array {
|
||||
$priv = trim((string) ($session['wg_client_private_key'] ?? ''));
|
||||
@@ -408,13 +509,16 @@ final class RemoteAccessRepository {
|
||||
|
||||
public static function closeSession(string $sessionId, string $status, string $reason): void {
|
||||
$st = Database::pdo()->prepare(
|
||||
'SELECT wg_client_public_key FROM remote_access_sessions WHERE session_id = ? LIMIT 1'
|
||||
'SELECT tunnel, wg_client_public_key FROM remote_access_sessions WHERE session_id = ? LIMIT 1'
|
||||
);
|
||||
$st->execute([$sessionId]);
|
||||
$pub = $st->fetchColumn();
|
||||
$row = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (is_array($row) && ($row['tunnel'] ?? 'wireguard') === 'wireguard') {
|
||||
$pub = $row['wg_client_public_key'] ?? '';
|
||||
if (is_string($pub) && $pub !== '') {
|
||||
WireGuardPeerProvisioner::removeClientPeer($pub);
|
||||
}
|
||||
}
|
||||
$now = self::nowSql();
|
||||
Database::pdo()->prepare(
|
||||
'UPDATE remote_access_sessions SET status = ?, close_reason = ?, closed_at = ?, last_activity_at = ? WHERE session_id = ?'
|
||||
@@ -432,13 +536,24 @@ final class RemoteAccessRepository {
|
||||
if (!Rbac::canAccessCompany($deviceCompanyId) || !Rbac::can('remote_access_operate', $deviceCompanyId)) {
|
||||
return ['ok' => false, 'error' => 'forbidden'];
|
||||
}
|
||||
if ((string) ($device['opt_in_mode'] ?? self::OPT_NONE) !== self::OPT_WIREGUARD) {
|
||||
return ['ok' => false, 'error' => 'device_not_opted_in_wg'];
|
||||
if ((string) ($device['opt_in_mode'] ?? self::OPT_NONE) === self::OPT_NONE) {
|
||||
return ['ok' => false, 'error' => 'device_not_opted_in'];
|
||||
}
|
||||
$optIn = (string) ($device['opt_in_mode'] ?? self::OPT_NONE);
|
||||
if (!in_array($optIn, [self::OPT_WIREGUARD, self::OPT_RSSH], true)) {
|
||||
return ['ok' => false, 'error' => 'device_not_opted_in'];
|
||||
}
|
||||
self::closeActiveSessionsForDevice($deviceId, 'superseded');
|
||||
$sessionId = self::uuid4();
|
||||
$companyId = $companyId ?? (int) ($device['company_id'] ?? Rbac::defaultCompanyId());
|
||||
$tunnel = $optIn === self::OPT_RSSH ? 'ssh_reverse' : 'wireguard';
|
||||
if ($tunnel === 'ssh_reverse') {
|
||||
$host = (string) cfg('remote_access.rssh.bastion_host', 'ra.apps.f0xx.org');
|
||||
$port = (int) cfg('remote_access.rssh.bastion_port', 443);
|
||||
$endpoint = $port === 443 ? $host : $host . ':' . $port;
|
||||
} else {
|
||||
$endpoint = (string) cfg('remote_access.wg_endpoint', 'ra.apps.f0xx.org:51820');
|
||||
}
|
||||
$expiresAt = time() + (int) cfg('remote_access.session_ttl_s', 3600);
|
||||
$now = self::nowSql();
|
||||
Database::pdo()->prepare(
|
||||
@@ -450,7 +565,7 @@ final class RemoteAccessRepository {
|
||||
$companyId,
|
||||
$operatorUserId,
|
||||
self::STATUS_PENDING,
|
||||
'wireguard',
|
||||
$tunnel,
|
||||
$endpoint,
|
||||
$expiresAt,
|
||||
$now,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,9 @@ require_once __DIR__ . '/Database.php';
|
||||
require_once __DIR__ . '/Rbac.php';
|
||||
require_once __DIR__ . '/DeviceRepository.php';
|
||||
require_once __DIR__ . '/Auth.php';
|
||||
require_once __DIR__ . '/AuthEmailSchema.php';
|
||||
require_once __DIR__ . '/AuthMailer.php';
|
||||
require_once __DIR__ . '/AuthRegistration.php';
|
||||
require_once __DIR__ . '/ReportRepository.php';
|
||||
require_once __DIR__ . '/TagCatalog.php';
|
||||
require_once __DIR__ . '/TicketTagCatalog.php';
|
||||
@@ -58,6 +61,7 @@ require_once __DIR__ . '/TicketRepository.php';
|
||||
require_once __DIR__ . '/GraphRepository.php';
|
||||
require_once __DIR__ . '/RemoteAccessRepository.php';
|
||||
require_once __DIR__ . '/WireGuardPeerProvisioner.php';
|
||||
require_once __DIR__ . '/RsshSessionProvisioner.php';
|
||||
require_once __DIR__ . '/AnalyticsHead.php';
|
||||
|
||||
function cfg(string $key, $default = null) {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"crash": {
|
||||
"enabled": "true",
|
||||
"upload_url": "https://f0xx.org/app/androidcast_project/crashes/api/upload.php",
|
||||
"upload_url": "https://apps.f0xx.org/app/androidcast_project/crashes/api/upload.php",
|
||||
"prune_after_upload": "true",
|
||||
"upload_interval": "5m",
|
||||
"max_pending": 32
|
||||
|
||||
@@ -46,6 +46,56 @@ else()
|
||||
message(STATUS "No libvpx.a for ${ANDROID_ABI} — VPx JNI probe off (build with scripts/build-native-codecs.sh ${ANDROID_ABI})")
|
||||
endif()
|
||||
|
||||
# libopus.a — same autodetect pattern (scripts/build-native-codecs.sh).
|
||||
set(LIBOPUS_LIB "")
|
||||
if(ANDROID_ABI)
|
||||
set(LIBOPUS_PER_ABI "${NATIVE_LIBS_ROOT}/${ANDROID_ABI}/libopus.a")
|
||||
if(EXISTS "${LIBOPUS_PER_ABI}")
|
||||
set(LIBOPUS_LIB "${LIBOPUS_PER_ABI}")
|
||||
endif()
|
||||
endif()
|
||||
if(NOT LIBOPUS_LIB AND DEFINED ENV{ANDROIDCAST_LIBOPUS_LIB})
|
||||
string(FIND "$ENV{ANDROIDCAST_LIBOPUS_LIB}" "${ANDROID_ABI}" _opus_abi_match)
|
||||
if(_opus_abi_match GREATER -1 AND EXISTS "$ENV{ANDROIDCAST_LIBOPUS_LIB}")
|
||||
set(LIBOPUS_LIB "$ENV{ANDROIDCAST_LIBOPUS_LIB}")
|
||||
endif()
|
||||
endif()
|
||||
if(LIBOPUS_LIB)
|
||||
message(STATUS "Linking libopus for ${ANDROID_ABI}: ${LIBOPUS_LIB}")
|
||||
target_compile_definitions(androidcast_codecs PRIVATE ANDROIDCAST_HAVE_OPUS=1)
|
||||
target_link_libraries(androidcast_codecs "${LIBOPUS_LIB}")
|
||||
if(EXISTS "${THIRD_PARTY_ROOT}/opus/include")
|
||||
target_include_directories(androidcast_codecs PRIVATE "${THIRD_PARTY_ROOT}/opus/include")
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "No libopus.a for ${ANDROID_ABI} — Opus JNI probe off (build with scripts/build-native-codecs.sh ${ANDROID_ABI})")
|
||||
endif()
|
||||
|
||||
# libspeex.a
|
||||
set(LIBSPEEX_LIB "")
|
||||
if(ANDROID_ABI)
|
||||
set(LIBSPEEX_PER_ABI "${NATIVE_LIBS_ROOT}/${ANDROID_ABI}/libspeex.a")
|
||||
if(EXISTS "${LIBSPEEX_PER_ABI}")
|
||||
set(LIBSPEEX_LIB "${LIBSPEEX_PER_ABI}")
|
||||
endif()
|
||||
endif()
|
||||
if(NOT LIBSPEEX_LIB AND DEFINED ENV{ANDROIDCAST_LIBSPEEX_LIB})
|
||||
string(FIND "$ENV{ANDROIDCAST_LIBSPEEX_LIB}" "${ANDROID_ABI}" _speex_abi_match)
|
||||
if(_speex_abi_match GREATER -1 AND EXISTS "$ENV{ANDROIDCAST_LIBSPEEX_LIB}")
|
||||
set(LIBSPEEX_LIB "$ENV{ANDROIDCAST_LIBSPEEX_LIB}")
|
||||
endif()
|
||||
endif()
|
||||
if(LIBSPEEX_LIB)
|
||||
message(STATUS "Linking libspeex for ${ANDROID_ABI}: ${LIBSPEEX_LIB}")
|
||||
target_compile_definitions(androidcast_codecs PRIVATE ANDROIDCAST_HAVE_SPEEX=1)
|
||||
target_link_libraries(androidcast_codecs "${LIBSPEEX_LIB}")
|
||||
if(EXISTS "${THIRD_PARTY_ROOT}/speex/include")
|
||||
target_include_directories(androidcast_codecs PRIVATE "${THIRD_PARTY_ROOT}/speex/include")
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "No libspeex.a for ${ANDROID_ABI} — Speex JNI probe off (build with scripts/build-native-codecs.sh ${ANDROID_ABI})")
|
||||
endif()
|
||||
|
||||
find_library(log-lib log)
|
||||
find_library(android-lib android)
|
||||
find_library(dl-lib dl)
|
||||
|
||||
@@ -9,27 +9,34 @@ The app loads `libandroidcast_codecs.so` for availability probes and future soft
|
||||
| FEC 3/4 + RS | Yes | — | v2 **per-shard** on wire (`FecShardWire`); v1 monolithic decode still accepted |
|
||||
| NACK + retransmit cache | Yes | — | UDP `MSG_NACK`, combined with FEC when enabled |
|
||||
| libvpx VP8/VP9 | Native | Encode + decode | Screen cast via ImageReader; receiver libvpx (decoder uses `VPX_CODEC_USE_ERROR_CONCEALMENT` when supported) or MediaCodec fallback |
|
||||
| Opus / Speex | Stub | Probe only | Submodules under `third-party/` |
|
||||
| Opus / Speex | Probe + static link when `lib*.a` built | Encode/decode JNI sinks | `scripts/build-native-codecs.sh` → CMake autolink |
|
||||
|
||||
## Enable libvpx (developer)
|
||||
## Enable native codecs (developer)
|
||||
|
||||
1. `git submodule update --init third-party/libvpx`
|
||||
2. Build `libvpx.a` per ABI (NDK is auto-detected; see `scripts/android-ndk.sh`):
|
||||
1. Initialize submodules as needed:
|
||||
|
||||
`./scripts/build-native-codecs.sh arm64-v8a`
|
||||
```bash
|
||||
git submodule update --init third-party/libvpx
|
||||
git submodule update --init third-party/opus
|
||||
git submodule update --init third-party/speex
|
||||
```
|
||||
|
||||
(If the repo path has **spaces**, the script uses `/tmp/androidcast-libvpx-$USER` automatically.)
|
||||
3. Build **each ABI** you ship (Gradle links `build/native/<abi>/libvpx.a` automatically):
|
||||
2. Build static libs per ABI (NDK auto-detected via `scripts/android-ndk.sh`):
|
||||
|
||||
```bash
|
||||
./scripts/build-native-codecs.sh arm64-v8a
|
||||
./scripts/build-native-codecs.sh armeabi-v7a # if armeabi-v7a is in your APK
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
```bash
|
||||
./scripts/build-native-codecs.sh arm64-v8a
|
||||
./scripts/build-native-codecs.sh armeabi-v7a # if shipped in APK
|
||||
```
|
||||
|
||||
If the repo path has **spaces**, the script stages under `/tmp/androidcast-libvpx-$USER`.
|
||||
|
||||
3. Gradle / CMake link `build/native/<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`).
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build libvpx for Android ABIs and produce libvpx.a for ANDROIDCAST_LIBVPX_LIB.
|
||||
#
|
||||
# With --enable-external-build, libvpx does not provide a libvpx.a make target;
|
||||
# this script uses the NDK (ndk-build) + generated Android.mk, as upstream intends.
|
||||
# Build libvpx, libopus, and libspeex for Android ABIs → build/native/<abi>/*.a
|
||||
# CMake in ndk/ autolinks when those archives exist (ANDROIDCAST_HAVE_* = 1).
|
||||
#
|
||||
# Prerequisites:
|
||||
# git submodule update --init third-party/libvpx
|
||||
@@ -12,6 +10,7 @@
|
||||
# ./scripts/build-native-codecs.sh arm64-v8a
|
||||
# ANDROIDCAST_LIBVPX_LIB=$PWD/build/native/arm64-v8a/libvpx.a ./gradlew assembleDebug
|
||||
#
|
||||
# Produces (when sources present): libvpx.a, libopus.a, libspeex.a under build/native/<abi>/
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
@@ -226,6 +225,112 @@ if [[ "$OUT" != "$PROJECT_OUT" ]]; then
|
||||
cp -f "$OUT/libvpx.a" "$PROJECT_OUT/libvpx.a"
|
||||
fi
|
||||
|
||||
androidcast_ndk_api() { echo 29; }
|
||||
|
||||
androidcast_ndk_clang() {
|
||||
local abi="$1"
|
||||
local api
|
||||
api="$(androidcast_ndk_api)"
|
||||
case "$abi" in
|
||||
arm64-v8a) echo "$NDK_HOST_BIN/aarch64-linux-android${api}-clang" ;;
|
||||
armeabi-v7a) echo "$NDK_HOST_BIN/armv7a-linux-androideabi${api}-clang" ;;
|
||||
x86_64) echo "$NDK_HOST_BIN/x86_64-linux-android${api}-clang" ;;
|
||||
x86) echo "$NDK_HOST_BIN/i686-linux-android${api}-clang" ;;
|
||||
*) echo "ERROR: unsupported ABI $abi" >&2; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
androidcast_ndk_configure_host() {
|
||||
case "$1" in
|
||||
arm64-v8a) echo aarch64-linux-android ;;
|
||||
armeabi-v7a) echo armv7a-linux-androideabi ;;
|
||||
x86_64) echo x86_64-linux-android ;;
|
||||
x86) echo i686-linux-android ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
build_opus_for_abi() {
|
||||
local opus_src="$ROOT/third-party/opus"
|
||||
if [[ ! -f "$opus_src/CMakeLists.txt" ]]; then
|
||||
echo "Skip libopus: $opus_src missing (git submodule update --init third-party/opus)"
|
||||
return 0
|
||||
fi
|
||||
local opus_build="$BUILD_DIR/opus-cmake"
|
||||
rm -rf "$opus_build"
|
||||
echo "Building libopus for $ABI (CMake) ..."
|
||||
cmake -DCMAKE_TOOLCHAIN_FILE="$NDK/build/cmake/android.toolchain.cmake" \
|
||||
-DANDROID_ABI="$ABI" \
|
||||
-DANDROID_PLATFORM="android-$(androidcast_ndk_api)" \
|
||||
-DBUILD_SHARED_LIBS=OFF \
|
||||
-DOPUS_BUILD_SHARED_LIBRARY=OFF \
|
||||
-DOPUS_BUILD_TESTING=OFF \
|
||||
-DOPUS_BUILD_PROGRAMS=OFF \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-B "$opus_build" -S "$opus_src"
|
||||
cmake --build "$opus_build" --target opus -j"$(nproc)"
|
||||
local opus_a="$opus_build/libopus.a"
|
||||
if [[ ! -f "$opus_a" ]]; then
|
||||
opus_a="$(find "$opus_build" -name 'libopus.a' | head -1)"
|
||||
fi
|
||||
if [[ ! -f "$opus_a" ]]; then
|
||||
echo "ERROR: libopus.a not found after Opus build"
|
||||
exit 1
|
||||
fi
|
||||
cp -f "$opus_a" "$PROJECT_OUT/libopus.a"
|
||||
echo "Built: $PROJECT_OUT/libopus.a (size $(ls -lah "${PROJECT_OUT}/libopus.a" | awk '{print $5}'))"
|
||||
}
|
||||
|
||||
build_speex_for_abi() {
|
||||
local speex_src_real="$ROOT/third-party/speex"
|
||||
if [[ ! -d "$speex_src_real" ]]; then
|
||||
echo "Skip libspeex: $speex_src_real missing (git submodule update --init third-party/speex)"
|
||||
return 0
|
||||
fi
|
||||
local speex_src="$speex_src_real"
|
||||
local speex_build="$BUILD_DIR/speex-autotools"
|
||||
if path_has_space "$speex_src_real"; then
|
||||
speex_src="$STAGING/speex-src"
|
||||
ln -sfn "$speex_src_real" "$speex_src"
|
||||
fi
|
||||
if [[ ! -f "$speex_src/configure" ]]; then
|
||||
echo "Running speex autogen.sh ..."
|
||||
(cd "$speex_src" && ./autogen.sh)
|
||||
fi
|
||||
rm -rf "$speex_build"
|
||||
mkdir -p "$speex_build"
|
||||
local cc host
|
||||
cc="$(androidcast_ndk_clang "$ABI")"
|
||||
host="$(androidcast_ndk_configure_host "$ABI")"
|
||||
echo "Building libspeex for $ABI (autotools) ..."
|
||||
(
|
||||
cd "$speex_build"
|
||||
"$speex_src/configure" \
|
||||
--host="$host" \
|
||||
--prefix="$speex_build/install" \
|
||||
--disable-shared \
|
||||
--enable-static \
|
||||
--disable-binaries \
|
||||
--with-pic \
|
||||
CC="$cc" \
|
||||
CFLAGS="-fPIC -O2"
|
||||
make -j"$(nproc)"
|
||||
)
|
||||
local speex_a="$speex_build/libspeex/.libs/libspeex.a"
|
||||
if [[ ! -f "$speex_a" ]]; then
|
||||
speex_a="$(find "$speex_build" -name 'libspeex.a' | head -1)"
|
||||
fi
|
||||
if [[ ! -f "$speex_a" ]]; then
|
||||
echo "ERROR: libspeex.a not found after Speex build"
|
||||
exit 1
|
||||
fi
|
||||
cp -f "$speex_a" "$PROJECT_OUT/libspeex.a"
|
||||
echo "Built: $PROJECT_OUT/libspeex.a (size $(ls -lah "${PROJECT_OUT}/libspeex.a" | awk '{print $5}'))"
|
||||
}
|
||||
|
||||
build_opus_for_abi
|
||||
build_speex_for_abi
|
||||
|
||||
echo ""
|
||||
echo "Built: $PROJECT_OUT/libvpx.a (size $(ls -lah "${PROJECT_OUT}/libvpx.a" | awk '{print $5}'))"
|
||||
if [[ -n "$STAGING" ]]; then
|
||||
@@ -233,5 +338,5 @@ if [[ -n "$STAGING" ]]; then
|
||||
fi
|
||||
echo "Build other ABIs as needed, e.g.:"
|
||||
echo " ./scripts/build-native-codecs.sh armeabi-v7a"
|
||||
echo "Then rebuild the APK (CMake picks build/native/<abi>/libvpx.a per ABI):"
|
||||
echo "Then rebuild the APK (CMake autolinks build/native/<abi>/*.a per ABI):"
|
||||
echo " ./gradlew assembleDebug"
|
||||
|
||||
Reference in New Issue
Block a user