009: Magisk ship pack through US-S5-1 + US-S4 OpenSSH/Jelly/Gallery2.
Release-keys/AVB, CVE clean+hi batches, Yama/slabrand, Pro branding, bundled apps, and AXP-class product surface docs/scripts.
This commit is contained in:
1 parent
fd793b9471
commit
deaaf42caa
129 files changed
+11769
-141
No files matched your search
+222
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Attribute: AXP GOLDEN ConnectivityCheckPreferenceController (Settings.apk),
|
||||
* cloned for LOS 23.2 without axp.os / captiveportal.axpos.org endpoints.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
package com.android.settings.network;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.provider.Settings;
|
||||
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.core.BasePreferenceController;
|
||||
import com.android.settings.core.PreferenceControllerMixin;
|
||||
import com.android.settingslib.RestrictedLockUtils;
|
||||
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
|
||||
import com.android.settingslib.RestrictedLockUtilsInternal;
|
||||
import com.android.settingslib.core.lifecycle.events.OnResume;
|
||||
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
|
||||
/** Internet connectivity check provider list (captive portal URLs). */
|
||||
public class ConnectivityCheckPreferenceController extends BasePreferenceController
|
||||
implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener, OnResume {
|
||||
|
||||
private static final String KEY = "connectivity_check_settings";
|
||||
|
||||
private static final int DISABLED = 0;
|
||||
private static final int AMAZON = 1;
|
||||
private static final int CLOUDFLARE = 2;
|
||||
private static final int GOOGLE = 3;
|
||||
private static final int HUAWEI = 4;
|
||||
private static final int KUKETZ = 5;
|
||||
private static final int MICROSOFT = 6;
|
||||
private static final int OPENSUSE = 7;
|
||||
private static final int UBUNTU = 8;
|
||||
|
||||
private ListPreference mConnectivityPreference;
|
||||
|
||||
public ConnectivityCheckPreferenceController(Context context) {
|
||||
super(context, KEY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAvailabilityStatus() {
|
||||
return isDisabledByAdmin() ? DISABLED_FOR_USER : AVAILABLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPreferenceKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void displayPreference(PreferenceScreen screen) {
|
||||
ListPreference pref = new ListPreference(screen.getContext());
|
||||
pref.setKey(KEY);
|
||||
pref.setOrder(30);
|
||||
pref.setIcon(R.drawable.ic_settings_language);
|
||||
pref.setTitle(R.string.connectivity_check_title);
|
||||
pref.setSummary(R.string.connectivity_check_summary);
|
||||
pref.setEntries(R.array.connectivity_check_entries);
|
||||
pref.setEntryValues(R.array.connectivity_check_values);
|
||||
pref.setOnPreferenceChangeListener(this);
|
||||
if (mConnectivityPreference == null) {
|
||||
screen.addPreference(pref);
|
||||
mConnectivityPreference = pref;
|
||||
}
|
||||
super.displayPreference(screen);
|
||||
updatePreferenceState();
|
||||
}
|
||||
|
||||
private void updatePreferenceState() {
|
||||
if (mConnectivityPreference == null) {
|
||||
return;
|
||||
}
|
||||
ContentResolver cr = mContext.getContentResolver();
|
||||
int mode = Settings.Global.getInt(cr, Settings.Global.CAPTIVE_PORTAL_MODE, 1);
|
||||
if (mode == 0) {
|
||||
mConnectivityPreference.setValueIndex(DISABLED);
|
||||
return;
|
||||
}
|
||||
String http = Settings.Global.getString(cr, Settings.Global.CAPTIVE_PORTAL_HTTP_URL);
|
||||
int idx = GOOGLE;
|
||||
if ("http://fireoscaptiveportal.com/generate_204".equals(http)) {
|
||||
idx = AMAZON;
|
||||
} else if ("http://cp.cloudflare.com".equals(http)) {
|
||||
idx = CLOUDFLARE;
|
||||
} else if ("http://connectivitycheck.gstatic.com/generate_204".equals(http)) {
|
||||
idx = GOOGLE;
|
||||
} else if ("http://connectivitycheck.platform.hicloud.com/generate_204".equals(http)) {
|
||||
idx = HUAWEI;
|
||||
} else if ("http://captiveportal.kuketz.de".equals(http)) {
|
||||
idx = KUKETZ;
|
||||
} else if ("http://edge-http.microsoft.com/captiveportal/generate_204".equals(http)) {
|
||||
idx = MICROSOFT;
|
||||
} else if ("http://conncheck.opensuse.org".equals(http)) {
|
||||
idx = OPENSUSE;
|
||||
} else if ("http://connectivity-check.ubuntu.com".equals(http)) {
|
||||
idx = UBUNTU;
|
||||
}
|
||||
mConnectivityPreference.setValueIndex(idx);
|
||||
}
|
||||
|
||||
private void setCaptivePortalURLs(ContentResolver cr, int value) {
|
||||
final String httpKey = Settings.Global.CAPTIVE_PORTAL_HTTP_URL;
|
||||
final String httpsKey = Settings.Global.CAPTIVE_PORTAL_HTTPS_URL;
|
||||
final String fallbackKey = Settings.Global.CAPTIVE_PORTAL_FALLBACK_URL;
|
||||
final String otherKey = Settings.Global.CAPTIVE_PORTAL_OTHER_FALLBACK_URLS;
|
||||
final String modeKey = Settings.Global.CAPTIVE_PORTAL_MODE;
|
||||
|
||||
String http;
|
||||
String https;
|
||||
String fallback;
|
||||
String other;
|
||||
int mode = 1;
|
||||
|
||||
switch (value) {
|
||||
case DISABLED:
|
||||
// Match GOLDEN: write Google URLs then mode=0
|
||||
http = "http://connectivitycheck.gstatic.com/generate_204";
|
||||
https = "https://www.google.com/generate_204";
|
||||
fallback = "http://www.google.com/gen_204";
|
||||
other = "http://play.googleapis.com/generate_204";
|
||||
mode = 0;
|
||||
break;
|
||||
case AMAZON:
|
||||
http = "http://fireoscaptiveportal.com/generate_204";
|
||||
https = "https://fireoscaptiveportal.com/generate_204";
|
||||
fallback = http;
|
||||
other = http;
|
||||
break;
|
||||
case CLOUDFLARE:
|
||||
http = "http://cp.cloudflare.com";
|
||||
https = "https://cp.cloudflare.com";
|
||||
fallback = http;
|
||||
other = http;
|
||||
break;
|
||||
case HUAWEI:
|
||||
http = "http://connectivitycheck.platform.hicloud.com/generate_204";
|
||||
https = "https://connectivitycheck.platform.hicloud.com/generate_204";
|
||||
fallback = http;
|
||||
other = http;
|
||||
break;
|
||||
case KUKETZ:
|
||||
http = "http://captiveportal.kuketz.de";
|
||||
https = "https://captiveportal.kuketz.de";
|
||||
fallback = http;
|
||||
other = http;
|
||||
break;
|
||||
case MICROSOFT:
|
||||
http = "http://edge-http.microsoft.com/captiveportal/generate_204";
|
||||
https = http;
|
||||
fallback = http;
|
||||
other = http;
|
||||
break;
|
||||
case OPENSUSE:
|
||||
http = "http://conncheck.opensuse.org";
|
||||
https = "https://conncheck.opensuse.org";
|
||||
fallback = http;
|
||||
other = http;
|
||||
break;
|
||||
case UBUNTU:
|
||||
http = "http://connectivity-check.ubuntu.com";
|
||||
https = "https://connectivity-check.ubuntu.com";
|
||||
fallback = http;
|
||||
other = http;
|
||||
break;
|
||||
case GOOGLE:
|
||||
default:
|
||||
http = "http://connectivitycheck.gstatic.com/generate_204";
|
||||
https = "https://www.google.com/generate_204";
|
||||
fallback = "http://www.google.com/gen_204";
|
||||
other = "http://play.googleapis.com/generate_204";
|
||||
break;
|
||||
}
|
||||
|
||||
Settings.Global.putString(cr, httpKey, http);
|
||||
Settings.Global.putString(cr, httpsKey, https);
|
||||
Settings.Global.putString(cr, fallbackKey, fallback);
|
||||
Settings.Global.putString(cr, otherKey, other);
|
||||
Settings.Global.putInt(cr, modeKey, mode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
if (KEY.equals(preference.getKey())) {
|
||||
setCaptivePortalURLs(
|
||||
mContext.getContentResolver(), Integer.parseInt((String) newValue));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
updatePreferenceState();
|
||||
if (mConnectivityPreference != null && mConnectivityPreference.getValue() != null) {
|
||||
setCaptivePortalURLs(
|
||||
mContext.getContentResolver(),
|
||||
Integer.parseInt(mConnectivityPreference.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
private EnforcedAdmin getEnforcedAdmin() {
|
||||
return RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
|
||||
mContext, UserManager.DISALLOW_CONFIG_PRIVATE_DNS, UserHandle.myUserId());
|
||||
}
|
||||
|
||||
private boolean isDisabledByAdmin() {
|
||||
return getEnforcedAdmin() != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
# 009 AXP LOS23 port — patch / packaging README
|
||||
|
||||
**Track**: Magisk AXP resurrection on LOS 23.2 sunfish (`AXP_Resurection`).
|
||||
**Not ship**: KernelSU / SusFS (`007` standby / optional recycle only).
|
||||
|
||||
## Apply order (host tree under `/srv/rom/src/lineage/`)
|
||||
|
||||
1. **Google stack** — `vendor/sunfish-a16/gms.mk` + `Android.mk` (microG, GSF,
|
||||
Phonesky). Host APKs: `/srv/rom/prebuilts/{microg,phonesky}/` (never Git).
|
||||
2. **Signature spoofing** — `vendor/sunfish-a16/patches/005-sigspoof/`
|
||||
(`apply_005_sigspoof.py`). Attribute: LOS restricted spoofing + AXP/005.
|
||||
3. **Call recording unlock** — `vendor/sunfish-a16/patches/008-call-rec-flag-secure/`
|
||||
(`extendrom_call_recording`). Attribute: EXTENDROM `F_CALLREC`.
|
||||
4. **Magisk preroot** — EXTENDROM-class boot/`init_boot` patch using Magisk APK
|
||||
from `/srv/rom/prebuilts/magisk/` (see `docs/009-magisk-preinstall-runbook.md`).
|
||||
Stub: `vendor/sunfish-a16/magisk.mk`. Attribute: Magisk upstream + sfX
|
||||
extendrom Magisk patcher. **Host tree currently has no `vendor/extendrom`**
|
||||
— must vendor minimal patcher or checkout extendrom before first Magisk bacon.
|
||||
5. **Bundled apps** — `/srv/rom/prebuilts/axp-apps/` + `apps.mk` (Aurora,
|
||||
Droidify, Fossify Gallery). Symlink APKs into `vendor/sunfish-a16/prebuilt/`.
|
||||
6. **Private DNS presets** — `apply_009_private_dns_providers.py` (GOLDEN list).
|
||||
7. **Internet connectivity check** — `apply_009_connectivity_check.py` (GOLDEN
|
||||
list **minus** AXP.OS / `axpos.org`).
|
||||
8. **Disable for user apps** — `apply_009_disable_user_apps.py` (SPA gate).
|
||||
9. **Userspace hardening / signing** — staged: see `signing/README.md`,
|
||||
`hardening/README.md` (US-S4-1 OpenSSH trim), and
|
||||
`docs/009-hardening-signing-checklist.md` (US-S1 keys → US-S2 wire →
|
||||
US-S3 AVB → US-S4+ harden). Bootloader lock = wont.
|
||||
10. **Kernel hardening** — LOS23 `msm-4.14` + map in
|
||||
`docs/009-kernel-hardening-map.md` (FR-017). Stage 1:
|
||||
`vendor/sunfish-a16/patches/009-kernel-hardening/apply_stage1.sh`
|
||||
(AXP Graphene/AH allowlist). Priority: AXP → LOS → Iodé → optional
|
||||
Graphene; no Divest hunt; no extra location lockdown.
|
||||
|
||||
## Product includes
|
||||
|
||||
Wire Magisk-resurrection product from `vendor/sunfish-a16/` into
|
||||
`lineage_sunfish` (same pattern as `004`/`005`). Keep Phonesky priv-app path.
|
||||
**No FakeStore. No KernelSU product packages.**
|
||||
|
||||
## Deferred
|
||||
|
||||
- Banking Gate E / Island validation (owner future).
|
||||
- Localization AXP-vs-LOS compare (`docs/009-localization-axp-vs-los.md` stub).
|
||||
|
||||
## Host paths (rom-dev01)
|
||||
|
||||
| What | Path |
|
||||
|------|------|
|
||||
| Magisk APK | `/srv/rom/prebuilts/magisk/Magisk-v30.7.apk` |
|
||||
| EXTENDROM Magisk patcher | `/srv/rom/references/extendrom/config/magisk/` |
|
||||
| Kernel source | `/srv/rom/src/lineage/kernel/google/msm-4.14` |
|
||||
| Product include (gms) | `device/google/sunfish/lineage_sunfish.mk` → `vendor/sunfish-a16/gms.mk` |
|
||||
| Vanilla baseline | `/srv/rom/artifacts/003-vanilla/20260914-lineage-23.2-sunfish/` |
|
||||
| Optional KSU boots | `/srv/rom/artifacts/007-kernelsu/` (non-ship) |
|
||||
|
||||
**Gap:** `vendor/extendrom` is **not** checked out in the lineage tree yet.
|
||||
Magisk bacon blocked on T011 (symlink/checkout + PREROOT or slim wire).
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""009: port GOLDEN Internet connectivity check UI (sans axp.os).
|
||||
|
||||
Copies ConnectivityCheckPreferenceController, adds string/array resources,
|
||||
wires into NetworkDashboardFragment. Idempotent.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
MARKER = "sunfish-a16-009-connectivity-check"
|
||||
|
||||
ENTRIES = """ <!-- {m}: GOLDEN connectivity providers minus AXP.OS -->
|
||||
<string-array name="connectivity_check_entries" translatable="false">
|
||||
<item>Disabled</item>
|
||||
<item>Amazon Fire OS (US)</item>
|
||||
<item>Cloudflare (US)</item>
|
||||
<item>Google (US) [default]</item>
|
||||
<item>Huawei (CN)</item>
|
||||
<item>Kuketz (DE)</item>
|
||||
<item>Microsoft Edge (US)</item>
|
||||
<item>openSUSE (DE)</item>
|
||||
<item>Ubuntu (UK)</item>
|
||||
</string-array>
|
||||
<string-array name="connectivity_check_values" translatable="false">
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<item>3</item>
|
||||
<item>4</item>
|
||||
<item>5</item>
|
||||
<item>6</item>
|
||||
<item>7</item>
|
||||
<item>8</item>
|
||||
</string-array>
|
||||
""".format(
|
||||
m=MARKER
|
||||
)
|
||||
|
||||
STRINGS = f""" <!-- {MARKER} -->
|
||||
<string name="connectivity_check_title" translatable="false">Internet connectivity check</string>
|
||||
<string name="connectivity_check_summary" translatable="false">HTTP endpoints to use for performing internet connectivity checks.</string>
|
||||
"""
|
||||
|
||||
|
||||
def ensure_arrays(path: pathlib.Path) -> None:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if "connectivity_check_entries" in text:
|
||||
return
|
||||
if "</resources>" not in text:
|
||||
raise SystemExit(f"FAIL: no </resources> in {path}")
|
||||
path.write_text(text.replace("</resources>", ENTRIES + "</resources>", 1), encoding="utf-8")
|
||||
|
||||
|
||||
def ensure_strings(path: pathlib.Path) -> None:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if "connectivity_check_title" in text:
|
||||
return
|
||||
path.write_text(text.replace("</resources>", STRINGS + "</resources>", 1), encoding="utf-8")
|
||||
|
||||
|
||||
def wire_fragment(path: pathlib.Path) -> None:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if "ConnectivityCheckPreferenceController" in text:
|
||||
return
|
||||
if "controllers.add(privateDnsPreferenceController);" not in text:
|
||||
raise SystemExit("FAIL: expected PrivateDns add site missing")
|
||||
text = text.replace(
|
||||
"controllers.add(privateDnsPreferenceController);",
|
||||
"controllers.add(privateDnsPreferenceController);\n"
|
||||
f" // {MARKER}\n"
|
||||
" controllers.add(new ConnectivityCheckPreferenceController(context));",
|
||||
1,
|
||||
)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else ".")
|
||||
here = pathlib.Path(__file__).resolve().parent
|
||||
src = here / "ConnectivityCheckPreferenceController.java"
|
||||
dst = root / (
|
||||
"packages/apps/Settings/src/com/android/settings/network/"
|
||||
"ConnectivityCheckPreferenceController.java"
|
||||
)
|
||||
arrays = root / "packages/apps/Settings/res/values/arrays.xml"
|
||||
# Prefer cm_strings for non-translated titles; fall back to strings.xml end
|
||||
cm = root / "packages/apps/Settings/res/values/cm_strings.xml"
|
||||
frag = root / (
|
||||
"packages/apps/Settings/src/com/android/settings/network/NetworkDashboardFragment.java"
|
||||
)
|
||||
for p in (src, arrays, cm, frag):
|
||||
if not p.is_file():
|
||||
print(f"FAIL: missing {p}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if dst.is_file() and MARKER.split("-")[0] and "sans axp" in dst.read_text(encoding="utf-8"):
|
||||
# already copied
|
||||
pass
|
||||
else:
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
|
||||
ensure_arrays(arrays)
|
||||
ensure_strings(cm)
|
||||
wire_fragment(frag)
|
||||
print(f"OK: connectivity check wired -> {dst}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
"""009: allow classic Disable for non-system apps (A16 Archive era).
|
||||
|
||||
LOS AppDisableButton returns null for !isSystemApp. Graphene/AXP-class UX
|
||||
keeps Disable usable for user apps. Attribute: AXP/owner preference over Archive.
|
||||
Idempotent.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
MARKER = "sunfish-a16-009-disable-user-apps"
|
||||
|
||||
CANDIDATES = [
|
||||
pathlib.Path("packages/apps/Settings/src/com/android/settings/spa/app/appinfo/AppDisableButton.kt"),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else ".")
|
||||
path = None
|
||||
for rel in CANDIDATES:
|
||||
p = root / rel
|
||||
if p.is_file():
|
||||
path = p
|
||||
break
|
||||
if path is None:
|
||||
print("FAIL: AppDisableButton.kt not found", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if MARKER in text:
|
||||
print(f"OK: already applied ({path})")
|
||||
return 0
|
||||
|
||||
old = " if (!app.isSystemApp) return null\n"
|
||||
if old not in text:
|
||||
print("FAIL: expected isSystemApp gate not found", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
new = (
|
||||
f" // {MARKER}: allow Disable for user apps (not only system)\n"
|
||||
f" // if (!app.isSystemApp) return null\n"
|
||||
)
|
||||
path.write_text(text.replace(old, new, 1), encoding="utf-8")
|
||||
print(f"OK: patched {path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
"""US-S4-2 — drop Lineage Jelly + Gallery2 to match AXP.OS PRO product surface.
|
||||
|
||||
Oracle: AXP.OS-22.2-20260324-PRO-sunfish.zip product/app has neither Jelly nor
|
||||
Gallery2 (browser stub + FossifyGallery_FD live on system_ext). See
|
||||
docs/009-axp-pro-product-apps-20260324.md.
|
||||
|
||||
FossifyGallery already declares LOCAL_OVERRIDES_PACKAGES := Gallery Gallery2
|
||||
Glimpse, but Gallery2 still landed in product/app on the 162840Z ship — same
|
||||
class of failure as OpenSSH filter-out. Patch the Lineage PRODUCT_PACKAGES
|
||||
add sites and add belt-and-suspenders stubs.
|
||||
|
||||
Usage (from ANDROID_BUILD_TOP):
|
||||
python3 vendor/sunfish-a16/patches/009-axp-los23-port/apply_009_drop_jelly_gallery2.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
MARKER = "sunfish-a16-009-uss4-2-jelly-gallery2"
|
||||
|
||||
# Common Lineage shapes (tolerant).
|
||||
JELLY_BLOCK = re.compile(
|
||||
r"# Browser\n"
|
||||
r"PRODUCT_PACKAGES \+= \\\n"
|
||||
r" Jelly\n",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
# ExactCalculator + Jelly list (common Lineage shape) → keep ExactCalculator only.
|
||||
JELLY_WITH_CALC = re.compile(
|
||||
r"(PRODUCT_PACKAGES \+= \\\n"
|
||||
r" ExactCalculator) \\\n"
|
||||
r" Jelly\n",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
GALLERY2_IN_LIST = re.compile(
|
||||
r"( DeskClock \\\n)"
|
||||
r" Gallery2 \\\n",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
|
||||
def patch_text(text: str, path: Path) -> tuple[str, list[str]]:
|
||||
notes: list[str] = []
|
||||
bad = f"# Jelly stripped ({MARKER})" in text or f"# Gallery2 stripped ({MARKER})" in text
|
||||
has_pkg = bool(re.search(r"(?m)^[ \t]*Jelly( \\)?$", text)) or bool(
|
||||
re.search(r"(?m)^[ \t]*Gallery2( \\)?$", text)
|
||||
)
|
||||
if MARKER in text and not bad and not has_pkg:
|
||||
notes.append("already")
|
||||
return text, notes
|
||||
|
||||
orig = text
|
||||
|
||||
text, n = JELLY_BLOCK.subn(
|
||||
f"# Browser (Jelly) omitted ({MARKER}) — AXP PRO uses noDefaultBrowserWebView\n",
|
||||
text,
|
||||
count=1,
|
||||
)
|
||||
if n:
|
||||
notes.append(f"jelly_block={n}")
|
||||
|
||||
text, n = JELLY_WITH_CALC.subn(
|
||||
rf"\1\n",
|
||||
text,
|
||||
count=1,
|
||||
)
|
||||
if n:
|
||||
notes.append(f"jelly_calc={n}")
|
||||
|
||||
text, n = GALLERY2_IN_LIST.subn(
|
||||
rf"\1",
|
||||
text,
|
||||
count=1,
|
||||
)
|
||||
if n:
|
||||
notes.append(f"gallery2_list={n}")
|
||||
|
||||
# Remove leftover broken comment-only continuations from older applicator.
|
||||
text2 = re.sub(
|
||||
r"^[ \t]*# (?:Jelly|Gallery2) stripped \(" + re.escape(MARKER) + r"\)\n",
|
||||
"",
|
||||
text,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
if text2 != text:
|
||||
notes.append("scrub_bad_comments")
|
||||
text = text2
|
||||
|
||||
if text == orig and not notes:
|
||||
return orig, notes
|
||||
if MARKER not in text and text != orig:
|
||||
text = f"# {MARKER}\n" + text
|
||||
return text, notes
|
||||
|
||||
|
||||
def main() -> int:
|
||||
top = Path(os.environ.get("ANDROID_BUILD_TOP", ".")).resolve()
|
||||
candidates = [
|
||||
top / "vendor/lineage/config/common.mk",
|
||||
top / "vendor/lineage/config/common_mobile.mk",
|
||||
top / "vendor/lineage/config/common_full.mk",
|
||||
top / "vendor/lineage/config/telephony.mk",
|
||||
top / "build/make/target/product/handheld_product.mk",
|
||||
top / "build/make/target/product/handheld_system.mk",
|
||||
top / "build/make/target/product/generic_system.mk",
|
||||
]
|
||||
# Also scan device trees that often force Gallery2
|
||||
for p in (top / "device").rglob("*.mk") if (top / "device").is_dir() else []:
|
||||
if p.name in ("device.mk", "lineage.mk", "lineage_sunfish.mk", "aosp_sunfish.mk"):
|
||||
candidates.append(p)
|
||||
|
||||
touched = 0
|
||||
still_present = []
|
||||
for mk in candidates:
|
||||
if not mk.is_file():
|
||||
continue
|
||||
text = mk.read_text(encoding="utf-8", errors="replace")
|
||||
if "Jelly" not in text and "Gallery2" not in text:
|
||||
continue
|
||||
# Ignore mention-only in comments/docs after a clean strip.
|
||||
has_pkg = bool(re.search(r"(?m)^[ \t]*Jelly( \\)?$", text)) or bool(
|
||||
re.search(r"(?m)^[ \t]*Gallery2( \\)?$", text)
|
||||
)
|
||||
bad = f"# Jelly stripped ({MARKER})" in text or f"# Gallery2 stripped ({MARKER})" in text
|
||||
if not has_pkg and not bad:
|
||||
continue
|
||||
new, notes = patch_text(text, mk)
|
||||
if notes == ["already"]:
|
||||
print(f"OK: already applied ({mk})")
|
||||
touched += 1
|
||||
continue
|
||||
if new == text:
|
||||
print(f"WARN: {mk} mentions Jelly/Gallery2 but no pattern matched: {notes}")
|
||||
still_present.append(str(mk))
|
||||
continue
|
||||
mk.write_text(new, encoding="utf-8")
|
||||
print(f"OK: patched {mk} ({', '.join(notes)})")
|
||||
touched += 1
|
||||
|
||||
if still_present:
|
||||
print(
|
||||
"FAIL: Jelly/Gallery2 still present, patterns missed: "
|
||||
+ ", ".join(still_present),
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if touched == 0:
|
||||
print("OK: Jelly/Gallery2 already absent from PRODUCT_PACKAGES sites")
|
||||
return 0
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
"""US-S4-1 — strip OpenSSH/rsync/procmem packages from Lineage common.mk.
|
||||
|
||||
AXP.OS PRO sunfish (A15) product/system lack sshd/ssh/rsync/procmem binaries but
|
||||
still ship init.openssh.rc. PRODUCT_PACKAGES filter-out via inherit-product did
|
||||
*not* remove them from the 20261002T125259Z ship image — patch the source block
|
||||
instead (same place Lineage adds them).
|
||||
|
||||
Keeps PRODUCT_COPY_FILES for init.openssh.rc (AXP surface).
|
||||
|
||||
Usage:
|
||||
python3 vendor/sunfish-a16/patches/009-axp-los23-port/apply_009_drop_openssh_packages.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
MARKER = "sunfish-a16-009-uss4-openssh"
|
||||
|
||||
OPENSSH_PACKAGES = re.compile(
|
||||
r"# Openssh\n"
|
||||
r"PRODUCT_PACKAGES \+= \\\n"
|
||||
r"(?: \S+ \\\n)*"
|
||||
r" start-ssh\n",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
RSYNC_PACKAGES = re.compile(
|
||||
r"# rsync\n"
|
||||
r"PRODUCT_PACKAGES \+= \\\n"
|
||||
r" rsync\n",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
PROCMEM_DEBUG = re.compile(
|
||||
r"# These packages are excluded from user builds\n"
|
||||
r"PRODUCT_PACKAGES_DEBUG \+= \\\n"
|
||||
r" procmem\n"
|
||||
r"\n"
|
||||
r"ifneq \(\$\(TARGET_BUILD_VARIANT\),user\)\n"
|
||||
r"PRODUCT_ARTIFACT_PATH_REQUIREMENT_ALLOWED_LIST \+= \\\n"
|
||||
r" system/bin/procmem\n"
|
||||
r"endif\n",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
top = Path(os.environ.get("ANDROID_BUILD_TOP", ".")).resolve()
|
||||
mk = top / "vendor/lineage/config/common.mk"
|
||||
if not mk.is_file():
|
||||
print(f"FAIL: missing {mk}", file=sys.stderr)
|
||||
return 1
|
||||
text = mk.read_text()
|
||||
if MARKER in text:
|
||||
print(f"OK: already applied ({mk})")
|
||||
return 0
|
||||
|
||||
orig = text
|
||||
text, n1 = OPENSSH_PACKAGES.subn(
|
||||
f"# Openssh packages stripped ({MARKER}) — keep init.openssh.rc below\n",
|
||||
text,
|
||||
count=1,
|
||||
)
|
||||
text, n2 = RSYNC_PACKAGES.subn(
|
||||
f"# rsync stripped ({MARKER})\n",
|
||||
text,
|
||||
count=1,
|
||||
)
|
||||
text, n3 = PROCMEM_DEBUG.subn(
|
||||
f"# procmem debug package stripped ({MARKER})\n",
|
||||
text,
|
||||
count=1,
|
||||
)
|
||||
if n1 != 1 or n2 != 1:
|
||||
print(
|
||||
f"FAIL: Openssh/rsync block match n1={n1} n2={n2} n3={n3}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
# procmem block optional if layout drifted
|
||||
if n3 == 0:
|
||||
print("WARN: procmem debug block not matched (layout drift)")
|
||||
|
||||
if "init.openssh.rc" not in text:
|
||||
print("FAIL: init.openssh.rc COPY_FILES missing after patch", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
mk.write_text(text)
|
||||
print(f"OK: patched {mk} (openssh_pkgs={n1} rsync={n2} procmem={n3})")
|
||||
return 0 if text != orig else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+498
@@ -0,0 +1,498 @@
|
||||
#!/usr/bin/env python3
|
||||
"""009: expand Private DNS presets to AXP GOLDEN list (no axp.os).
|
||||
|
||||
Clones GOLDEN Settings radios/hostnames into LOS PrivateDnsModeDialogPreference
|
||||
(which already has a Cloudflare stub). Idempotent via marker comments.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
MARKER = "sunfish-a16-009-private-dns"
|
||||
|
||||
# key, UI label, DoT hostname — GOLDEN order (cloudflare hostname = security.*)
|
||||
PROVIDERS = [
|
||||
("adguard", "AdGuard (CT)", "dns.adguard.com"),
|
||||
("appliedprivacy", "AppliedPrivacy (AT)", "dot1.applied-privacy.net"),
|
||||
("cira", "CIRA (CA)", "protected.canadianshield.cira.ca"),
|
||||
("controld", "Control D (CA)", "p2.freedns.controld.com"),
|
||||
("cleanbrowsing", "CleanBrowsing (US)", "security-filter-dns.cleanbrowsing.org"),
|
||||
("cznic", "CZ.NIC (CZ)", "odvr.nic.cz"),
|
||||
("cloudflare", "Cloudflare (US)", "security.cloudflare-dns.com"),
|
||||
("dnszero", "DNS0 (FR)", "zero.dns0.eu"),
|
||||
("dnssb", "DNS.SB (DE)", "dot.sb"),
|
||||
("google", "Google (US)", "dns.google"),
|
||||
("mullvad", "Mullvad (SE)", "base.dns.mullvad.net"),
|
||||
("quadnine", "Quad9 (CH)", "dns.quad9.net"),
|
||||
("restena", "Restena (LU)", "kaitain.restena.lu"),
|
||||
("switch", "SWITCH (CH)", "dns.switch.ch"),
|
||||
("uncensoreddns", "UncensoredDNS (DK)", "anycast.censurfridns.dk"),
|
||||
]
|
||||
|
||||
|
||||
def patch_layout(path: pathlib.Path) -> bool:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if MARKER in text:
|
||||
return False
|
||||
# Insert provider radios after off, before opportunistic (keep provider+hostname)
|
||||
includes = []
|
||||
for key, _, _ in PROVIDERS:
|
||||
includes.append(
|
||||
f' <!-- {MARKER} -->\n'
|
||||
f' <include\n'
|
||||
f' android:id="@+id/private_dns_mode_{key}"\n'
|
||||
f' layout="@layout/preference_widget_dialog_radiobutton"/>\n'
|
||||
)
|
||||
block = "".join(includes)
|
||||
# Remove ALL existing cloudflare includes (re-added once via PROVIDERS)
|
||||
text2 = re.sub(
|
||||
r'\s*(?:<!--.*?-->\s*)?<include\s*\n\s*android:id="@\+id/private_dns_mode_cloudflare"\s*\n\s*layout="@layout/preference_widget_dialog_radiobutton"/>\s*',
|
||||
"\n",
|
||||
text,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
text2 = re.sub(
|
||||
r'\s*<include\s+android:id="@\+id/private_dns_mode_cloudflare"[^/]*/>\s*',
|
||||
"\n",
|
||||
text2,
|
||||
)
|
||||
needle = (
|
||||
' <include\n'
|
||||
' android:id="@+id/private_dns_mode_opportunistic"\n'
|
||||
' layout="@layout/preference_widget_dialog_radiobutton"/>'
|
||||
)
|
||||
if needle not in text2:
|
||||
# alternate formatting
|
||||
needle = 'android:id="@+id/private_dns_mode_opportunistic"'
|
||||
idx = text2.find(needle)
|
||||
if idx < 0:
|
||||
raise SystemExit(f"FAIL: opportunistic radio not in {path}")
|
||||
# find start of include element
|
||||
start = text2.rfind("<include", 0, idx)
|
||||
text2 = text2[:start] + block + text2[start:]
|
||||
else:
|
||||
text2 = text2.replace(needle, block + needle, 1)
|
||||
path.write_text(text2, encoding="utf-8")
|
||||
return True
|
||||
|
||||
|
||||
def patch_cm_strings(path: pathlib.Path) -> bool:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if MARKER in text and "private_dns_hostname_quadnine" in text:
|
||||
return False
|
||||
# Replace/extend cloudflare hostname to GOLDEN security.cloudflare-dns.com
|
||||
text = re.sub(
|
||||
r'<string name="private_dns_hostname_cloudflare"[^>]*>.*?</string>',
|
||||
'<string name="private_dns_hostname_cloudflare" translatable="false">'
|
||||
"security.cloudflare-dns.com</string>",
|
||||
text,
|
||||
count=1,
|
||||
)
|
||||
text = re.sub(
|
||||
r'<string name="private_dns_mode_cloudflare"[^>]*>.*?</string>',
|
||||
'<string name="private_dns_mode_cloudflare" translatable="false">'
|
||||
"Cloudflare (US)</string>",
|
||||
text,
|
||||
count=1,
|
||||
)
|
||||
lines = [f" <!-- {MARKER}: AXP GOLDEN Private DNS presets -->"]
|
||||
for key, label, host in PROVIDERS:
|
||||
if key == "cloudflare":
|
||||
continue # already present
|
||||
lines.append(
|
||||
f' <string name="private_dns_mode_{key}" translatable="false">{label}</string>'
|
||||
)
|
||||
lines.append(
|
||||
f' <string name="private_dns_hostname_{key}" translatable="false">{host}</string>'
|
||||
)
|
||||
insert = "\n".join(lines) + "\n"
|
||||
if "</resources>" not in text:
|
||||
raise SystemExit(f"FAIL: no </resources> in {path}")
|
||||
text = text.replace("</resources>", insert + "</resources>", 1)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
return True
|
||||
|
||||
|
||||
def write_preference_java(path: pathlib.Path) -> None:
|
||||
"""Rewrite PrivateDnsModeDialogPreference with data-driven GOLDEN presets."""
|
||||
# Keep copyright / package; replace body after class declaration via full file.
|
||||
providers_java = ",\n".join(
|
||||
f' new Provider("{key}", R.id.private_dns_mode_{key}, '
|
||||
f"R.string.private_dns_mode_{key}, R.string.private_dns_hostname_{key})"
|
||||
for key, _, _ in PROVIDERS
|
||||
)
|
||||
# Assign custom mode ints starting at 4 (LOS cloudflare used 4)
|
||||
mode_consts = "\n".join(
|
||||
f" private static final int PRIVATE_DNS_MODE_{key.upper()} = {4 + i};"
|
||||
for i, (key, _, _) in enumerate(PROVIDERS)
|
||||
)
|
||||
map_puts = "\n".join(
|
||||
f" PRIVATE_DNS_MAP.put(PRIVATE_DNS_MODE_{key.upper()}, "
|
||||
f"R.id.private_dns_mode_{key});"
|
||||
for key, _, _ in PROVIDERS
|
||||
)
|
||||
content = f'''/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.network;
|
||||
|
||||
import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF;
|
||||
import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
|
||||
import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
|
||||
|
||||
import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.ConnectivitySettingsManager;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.overlay.FeatureFactory;
|
||||
import com.android.settings.utils.AnnotationSpan;
|
||||
import com.android.settingslib.CustomDialogPreferenceCompat;
|
||||
import com.android.settingslib.HelpUtils;
|
||||
import com.android.settingslib.RestrictedLockUtils;
|
||||
import com.android.settingslib.RestrictedLockUtilsInternal;
|
||||
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.google.common.net.InternetDomainName;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Dialog to set the Private DNS.
|
||||
* {MARKER}: AXP GOLDEN provider presets (hostnames from GOLDEN Settings.apk).
|
||||
*/
|
||||
public class PrivateDnsModeDialogPreference extends CustomDialogPreferenceCompat implements
|
||||
RadioGroup.OnCheckedChangeListener, TextWatcher {{
|
||||
|
||||
public static final String ANNOTATION_URL = "url";
|
||||
|
||||
private static final String TAG = "PrivateDnsModeDialog";
|
||||
private static final Map<Integer, Integer> PRIVATE_DNS_MAP;
|
||||
|
||||
{mode_consts}
|
||||
|
||||
private static final Provider[] PROVIDERS = new Provider[] {{
|
||||
{providers_java}
|
||||
}};
|
||||
|
||||
static {{
|
||||
PRIVATE_DNS_MAP = new HashMap<>();
|
||||
PRIVATE_DNS_MAP.put(PRIVATE_DNS_MODE_OFF, R.id.private_dns_mode_off);
|
||||
PRIVATE_DNS_MAP.put(PRIVATE_DNS_MODE_OPPORTUNISTIC, R.id.private_dns_mode_opportunistic);
|
||||
PRIVATE_DNS_MAP.put(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, R.id.private_dns_mode_provider);
|
||||
{map_puts}
|
||||
}}
|
||||
|
||||
private static final class Provider {{
|
||||
final String key;
|
||||
final int radioId;
|
||||
final int labelRes;
|
||||
final int hostnameRes;
|
||||
|
||||
Provider(String key, int radioId, int labelRes, int hostnameRes) {{
|
||||
this.key = key;
|
||||
this.radioId = radioId;
|
||||
this.labelRes = labelRes;
|
||||
this.hostnameRes = hostnameRes;
|
||||
}}
|
||||
}}
|
||||
|
||||
@VisibleForTesting
|
||||
TextInputLayout mHostnameLayout;
|
||||
@VisibleForTesting
|
||||
EditText mHostnameText;
|
||||
@VisibleForTesting
|
||||
RadioGroup mRadioGroup;
|
||||
@VisibleForTesting
|
||||
int mMode;
|
||||
|
||||
public PrivateDnsModeDialogPreference(Context context) {{
|
||||
super(context);
|
||||
}}
|
||||
|
||||
public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs) {{
|
||||
super(context, attrs);
|
||||
}}
|
||||
|
||||
public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr) {{
|
||||
super(context, attrs, defStyleAttr);
|
||||
}}
|
||||
|
||||
public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {{
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}}
|
||||
|
||||
private final AnnotationSpan.LinkInfo mUrlLinkInfo = new AnnotationSpan.LinkInfo(
|
||||
ANNOTATION_URL, (widget) -> {{
|
||||
final Context context = widget.getContext();
|
||||
final Intent intent = HelpUtils.getHelpIntent(context,
|
||||
context.getString(R.string.help_uri_private_dns),
|
||||
context.getClass().getName());
|
||||
if (intent != null) {{
|
||||
try {{
|
||||
widget.startActivityForResult(intent, 0);
|
||||
}} catch (ActivityNotFoundException e) {{
|
||||
Log.w(TAG, "Activity was not found for intent, " + intent.toString());
|
||||
}}
|
||||
}}
|
||||
}});
|
||||
|
||||
private static int modeForRadioId(int radioId) {{
|
||||
if (radioId == R.id.private_dns_mode_off) return PRIVATE_DNS_MODE_OFF;
|
||||
if (radioId == R.id.private_dns_mode_opportunistic) return PRIVATE_DNS_MODE_OPPORTUNISTIC;
|
||||
if (radioId == R.id.private_dns_mode_provider) return PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
|
||||
for (int i = 0; i < PROVIDERS.length; i++) {{
|
||||
if (PROVIDERS[i].radioId == radioId) {{
|
||||
return 4 + i;
|
||||
}}
|
||||
}}
|
||||
return PRIVATE_DNS_MODE_OPPORTUNISTIC;
|
||||
}}
|
||||
|
||||
private static Provider providerForMode(int mode) {{
|
||||
int idx = mode - 4;
|
||||
if (idx >= 0 && idx < PROVIDERS.length) {{
|
||||
return PROVIDERS[idx];
|
||||
}}
|
||||
return null;
|
||||
}}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {{
|
||||
super.onBindViewHolder(holder);
|
||||
if (isDisabledByAdmin()) {{
|
||||
holder.itemView.setEnabled(true);
|
||||
}}
|
||||
setSaveButtonListener();
|
||||
}}
|
||||
|
||||
@Override
|
||||
protected void onBindDialogView(View view) {{
|
||||
final Context context = getContext();
|
||||
mMode = ConnectivitySettingsManager.getPrivateDnsMode(context);
|
||||
if (mMode == PRIVATE_DNS_MODE_PROVIDER_HOSTNAME) {{
|
||||
final String host = ConnectivitySettingsManager.getPrivateDnsHostname(context);
|
||||
for (int i = 0; i < PROVIDERS.length; i++) {{
|
||||
if (host != null && host.equals(context.getString(PROVIDERS[i].hostnameRes))) {{
|
||||
mMode = 4 + i;
|
||||
break;
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
mRadioGroup = view.findViewById(R.id.private_dns_radio_group);
|
||||
mRadioGroup.check(PRIVATE_DNS_MAP.getOrDefault(mMode, R.id.private_dns_mode_opportunistic));
|
||||
mRadioGroup.setOnCheckedChangeListener(this);
|
||||
|
||||
final RadioButton offRadioButton = view.findViewById(R.id.private_dns_mode_off);
|
||||
offRadioButton.setText(com.android.settingslib.R.string.private_dns_mode_off);
|
||||
for (Provider p : PROVIDERS) {{
|
||||
final RadioButton rb = view.findViewById(p.radioId);
|
||||
if (rb != null) {{
|
||||
rb.setText(p.labelRes);
|
||||
}}
|
||||
}}
|
||||
final RadioButton opportunisticRadioButton =
|
||||
view.findViewById(R.id.private_dns_mode_opportunistic);
|
||||
opportunisticRadioButton.setText(
|
||||
com.android.settingslib.R.string.private_dns_mode_opportunistic);
|
||||
final RadioButton providerRadioButton = view.findViewById(R.id.private_dns_mode_provider);
|
||||
providerRadioButton.setText(com.android.settingslib.R.string.private_dns_mode_provider);
|
||||
|
||||
mHostnameLayout = view.findViewById(R.id.private_dns_mode_provider_hostname_layout);
|
||||
mHostnameText = view.findViewById(R.id.private_dns_mode_provider_hostname);
|
||||
if (mHostnameText != null) {{
|
||||
mHostnameText.setText(ConnectivitySettingsManager.getPrivateDnsHostname(context));
|
||||
mHostnameText.addTextChangedListener(this);
|
||||
}}
|
||||
|
||||
final TextView helpTextView = view.findViewById(R.id.private_dns_help_info);
|
||||
helpTextView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
final Intent helpIntent = HelpUtils.getHelpIntent(context,
|
||||
context.getString(R.string.help_uri_private_dns),
|
||||
context.getClass().getName());
|
||||
final AnnotationSpan.LinkInfo linkInfo = new AnnotationSpan.LinkInfo(context,
|
||||
ANNOTATION_URL, helpIntent);
|
||||
if (linkInfo.isActionable()) {{
|
||||
helpTextView.setText(AnnotationSpan.linkify(
|
||||
context.getText(R.string.private_dns_help_message), linkInfo));
|
||||
}} else {{
|
||||
helpTextView.setText("");
|
||||
}}
|
||||
|
||||
updateDialogInfo();
|
||||
}}
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(RadioGroup group, int checkedId) {{
|
||||
mMode = modeForRadioId(checkedId);
|
||||
updateDialogInfo();
|
||||
}}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {{
|
||||
}}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {{
|
||||
}}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {{
|
||||
updateDialogInfo();
|
||||
}}
|
||||
|
||||
@Override
|
||||
public void performClick() {{
|
||||
EnforcedAdmin enforcedAdmin = getEnforcedAdmin();
|
||||
if (enforcedAdmin == null) {{
|
||||
super.performClick();
|
||||
}} else {{
|
||||
RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), enforcedAdmin);
|
||||
}}
|
||||
}}
|
||||
|
||||
private EnforcedAdmin getEnforcedAdmin() {{
|
||||
return RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
|
||||
getContext(), UserManager.DISALLOW_CONFIG_PRIVATE_DNS, UserHandle.myUserId());
|
||||
}}
|
||||
|
||||
private boolean isDisabledByAdmin() {{
|
||||
return getEnforcedAdmin() != null;
|
||||
}}
|
||||
|
||||
private void updateDialogInfo() {{
|
||||
final boolean modeProvider = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME == mMode;
|
||||
if (mHostnameLayout != null) {{
|
||||
mHostnameLayout.setEnabled(modeProvider);
|
||||
mHostnameLayout.setErrorEnabled(false);
|
||||
}}
|
||||
}}
|
||||
|
||||
private void setSaveButtonListener() {{
|
||||
View.OnClickListener onClickListener = v -> doSaveButton();
|
||||
DialogInterface.OnShowListener onShowListener = dialog -> {{
|
||||
if (dialog == null) {{
|
||||
Log.e(TAG, "The DialogInterface is null!");
|
||||
return;
|
||||
}}
|
||||
Button saveButton = ((AlertDialog) dialog).getButton(DialogInterface.BUTTON_POSITIVE);
|
||||
if (saveButton == null) {{
|
||||
Log.e(TAG, "Can't get the save button!");
|
||||
return;
|
||||
}}
|
||||
saveButton.setOnClickListener(onClickListener);
|
||||
}};
|
||||
setOnShowListener(onShowListener);
|
||||
}}
|
||||
|
||||
@VisibleForTesting
|
||||
void doSaveButton() {{
|
||||
Context context = getContext();
|
||||
int modeToSet = mMode;
|
||||
Provider preset = providerForMode(mMode);
|
||||
if (preset != null) {{
|
||||
ConnectivitySettingsManager.setPrivateDnsHostname(context,
|
||||
context.getString(preset.hostnameRes));
|
||||
modeToSet = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
|
||||
}} else if (mMode == PRIVATE_DNS_MODE_PROVIDER_HOSTNAME) {{
|
||||
if (mHostnameLayout == null || mHostnameText == null) {{
|
||||
Log.e(TAG, "Can't find hostname resources!");
|
||||
return;
|
||||
}}
|
||||
if (mHostnameText.getText().isEmpty()) {{
|
||||
mHostnameLayout.setError(context.getString(R.string.private_dns_field_require));
|
||||
Log.w(TAG, "The hostname is empty!");
|
||||
return;
|
||||
}}
|
||||
if (!InternetDomainName.isValid(mHostnameText.getText().toString())) {{
|
||||
mHostnameLayout.setError(context.getString(R.string.private_dns_hostname_invalid));
|
||||
Log.w(TAG, "The hostname is invalid!");
|
||||
return;
|
||||
}}
|
||||
ConnectivitySettingsManager.setPrivateDnsHostname(context,
|
||||
mHostnameText.getText().toString());
|
||||
}}
|
||||
|
||||
ConnectivitySettingsManager.setPrivateDnsMode(context, modeToSet);
|
||||
FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
|
||||
.action(context, SettingsEnums.ACTION_PRIVATE_DNS_MODE, modeToSet);
|
||||
Dialog dialog = getDialog();
|
||||
if (dialog != null) {{
|
||||
dialog.dismiss();
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
'''
|
||||
# Fix f-string doubled braces - we used {{ for literal braces in the template
|
||||
# But MARKER was intended as comment - replace
|
||||
content = content.replace("{MARKER}", MARKER)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else ".")
|
||||
layout = root / "packages/apps/Settings/res/layout/private_dns_mode_dialog.xml"
|
||||
cm = root / "packages/apps/Settings/res/values/cm_strings.xml"
|
||||
java = root / (
|
||||
"packages/apps/Settings/src/com/android/settings/network/"
|
||||
"PrivateDnsModeDialogPreference.java"
|
||||
)
|
||||
for p in (layout, cm, java):
|
||||
if not p.is_file():
|
||||
print(f"FAIL: missing {p}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if MARKER in java.read_text(encoding="utf-8") and "quadnine" in layout.read_text(
|
||||
encoding="utf-8"
|
||||
):
|
||||
print("OK: private DNS already applied")
|
||||
return 0
|
||||
|
||||
patch_layout(layout)
|
||||
patch_cm_strings(cm)
|
||||
write_preference_java(java)
|
||||
print(f"OK: private DNS providers applied ({len(PROVIDERS)} presets)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# Apply early TARGET_UNOFFICIAL_BUILD_ID to device product makefile.
|
||||
# Must run before breakfast: inherit-product of vendor/lineage parses version.mk
|
||||
# immediately; vendor/extra inherit-product is too late.
|
||||
#
|
||||
# Usage (on rom-dev01 tree):
|
||||
# python3 vendor/sunfish-a16/patches/009-axp-los23-port/apply_009_unofficial_build_id.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
import sys
|
||||
|
||||
MARKER = "TARGET_UNOFFICIAL_BUILD_ID := AXP-Resurection-Pro"
|
||||
NEEDLE = "# Inherit some common Lineage stuff.\n"
|
||||
INSERT = (
|
||||
"# sunfish-a16: must precede vendor/lineage version.mk (via inherit below)\n"
|
||||
f"{MARKER}\n\n"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
top = Path(os.environ.get("ANDROID_BUILD_TOP", ".")).resolve()
|
||||
mk = top / "device/google/sunfish/lineage_sunfish.mk"
|
||||
if not mk.is_file():
|
||||
print(f"FAIL: missing {mk}", file=sys.stderr)
|
||||
return 1
|
||||
text = mk.read_text()
|
||||
if MARKER in text:
|
||||
print(f"OK: already applied ({mk})")
|
||||
return 0
|
||||
if NEEDLE not in text:
|
||||
print(f"FAIL: needle not found in {mk}", file=sys.stderr)
|
||||
return 1
|
||||
mk.write_text(text.replace(NEEDLE, INSERT + NEEDLE, 1))
|
||||
print(f"OK: patched {mk}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,67 @@
|
||||
# US-S4-1 — drop Lineage product OpenSSH binaries / rsync / procmem
|
||||
|
||||
**Track**: AXP Resurection Pro (Magisk), unlocked sunfish.
|
||||
**Not**: slim variant; not bootloader lock; not Yama (kernel).
|
||||
|
||||
## AXP reference (mandatory)
|
||||
|
||||
Oracle: `AXP.OS-22.2-20260324-PRO-sunfish.zip` → `payload.bin` → `product.img`
|
||||
(+ GOLDEN `ro.lineage.version=22.2-2026.003-sunfish-pro`, Android 15):
|
||||
|
||||
| Path | AXP PRO (A15) |
|
||||
|------|----------------|
|
||||
| `/bin/sshd`, `ssh`, `scp`, `sftp`, `ssh-keygen`, `start-ssh` | **absent** |
|
||||
| `/bin/rsync`, `/bin/procmem` | **absent** |
|
||||
| `/etc/init/init.openssh.rc` | **present** (`mkdir /data/ssh*`) |
|
||||
|
||||
### Was this an A16 Lineage add?
|
||||
|
||||
**No.** Lineage `vendor/lineage/config/common.mk` has shipped the Openssh
|
||||
block since 2012; lineage-20 / 21 / 22.2 all still include it. AXP PRO on
|
||||
A15 therefore **actively omitted** the binaries (same lineage generation
|
||||
still lists them upstream). Public `AXP.OS/android_vendor_lineage`
|
||||
`upstream_*` mirrors keep the Lineage Openssh block — use the **PRO zip /
|
||||
GOLDEN**, not that mirror, as product oracle.
|
||||
|
||||
## What we ship
|
||||
|
||||
Match AXP PRO product surface by stripping Lineage `common.mk` package
|
||||
adds (see `apply_009_drop_openssh_packages.py`) — a plain
|
||||
`PRODUCT_PACKAGES := $(filter-out …)` inherit did **not** drop `sshd` from
|
||||
ship `20261002T125259Z`:
|
||||
|
||||
- Remove packages: `scp`, `sftp`, `ssh`, `sshd`, `sshd_config`, `ssh-keygen`,
|
||||
`start-ssh`, `rsync`, `procmem` (debug)
|
||||
- **Keep** `init.openssh.rc` copy (AXP has it)
|
||||
|
||||
Remote shell / file transfer on this track remains **adb** (+ Magisk); the
|
||||
rc only creates `/data/ssh*` and does not start `sshd` without the binary.
|
||||
|
||||
## Verify (LAB after flash)
|
||||
|
||||
```text
|
||||
adb shell ls /product/bin/sshd # expect: No such file
|
||||
adb shell ls /product/bin/rsync # expect: No such file
|
||||
adb shell ls /product/etc/init/init.openssh.rc # expect: present (AXP)
|
||||
adb shell pidof magiskd # expect: non-empty
|
||||
```
|
||||
|
||||
## US-S4-2 — drop Jelly + Gallery2 (AXP PRO product)
|
||||
|
||||
**Oracle**: same PRO zip — `product/app` has **no** Jelly / Gallery2;
|
||||
`system_ext` ships Fossify + `noDefaultBrowserWebView`. Inventory:
|
||||
`docs/009-axp-pro-product-apps-20260324.md`.
|
||||
|
||||
- `apply_009_drop_jelly_gallery2.py` — remove Lineage `PRODUCT_PACKAGES`
|
||||
adds for Jelly / Gallery2
|
||||
- `NoLineageJelly` uninstallable override module (AXP stub class)
|
||||
- Keep `FossifyGallery` (already overrides Gallery/Gallery2/Glimpse)
|
||||
|
||||
LAB after flash: no `/product/app/Jelly`, no `/product/app/Gallery2`,
|
||||
FossifyGallery present.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- `CONFIG_SECURITY_YAMA` (kernel / US-S5 — **done** on `162840Z`)
|
||||
- Switching lunch to `user` (US-S6)
|
||||
- NeoLauncher / Aosmium (wont / exception)
|
||||
@@ -0,0 +1,89 @@
|
||||
# 009 signing / AVB (userspace Gate G)
|
||||
|
||||
Clone AXP/GOLDEN posture: **`user` + `release-keys`**, device **unlocked**
|
||||
(`orange`). Not IOdes locked-bootloader.
|
||||
|
||||
Full checklist: `docs/009-hardening-signing-checklist.md`.
|
||||
|
||||
## Stages
|
||||
|
||||
| Stage | What |
|
||||
|-------|------|
|
||||
| US-S1 | Generate keys → `/srv/rom/secrets/android-certs/` |
|
||||
| US-S2 | Wire `PRODUCT_DEFAULT_DEV_CERTIFICATE` into bacon |
|
||||
| US-S3 | Project AVB key (`AVB_CUSTOM_KEY_PATH`); decide `WITH_AVB` |
|
||||
| US-S4+ | Userspace harden / CVE — separate patches |
|
||||
|
||||
## Generate (rom-dev01 only)
|
||||
|
||||
```bash
|
||||
# from synced repo copy or laptop scp of scripts/
|
||||
sudo -u android-build bash /srv/rom/src/lineage/vendor/sunfish-a16/../../../../\
|
||||
# prefer:
|
||||
bash /path/to/AXP_Resurection/scripts/009-generate-release-keys.sh
|
||||
```
|
||||
|
||||
Script writes under `/srv/rom/secrets/android-certs/` when that parent is
|
||||
writable, otherwise `$HOME/.android-certs` (Lineage default). On rom-dev01
|
||||
(2026-09-29) secrets/ is root-owned without group write → keys live in
|
||||
`/home/android-build/.android-certs/`. Refuses overwrite. Reminds about
|
||||
encrypted offline backup before first signed ship bacon.
|
||||
|
||||
## Wire (US-S2 — after keys exist)
|
||||
|
||||
In the bacon environment (not committed secrets):
|
||||
|
||||
```bash
|
||||
export PRODUCT_DEFAULT_DEV_CERTIFICATE=$HOME/.android-certs/releasekey
|
||||
# after root relocate: /srv/rom/secrets/android-certs/releasekey
|
||||
# lunch lineage_sunfish-userdebug # first signed build
|
||||
# later: lineage_sunfish-user
|
||||
```
|
||||
|
||||
`009-bacon-axp-stage1-combo4.sh` auto-exports when `$HOME/.android-certs/releasekey.pk8` exists.
|
||||
## AVB (US-S3)
|
||||
|
||||
Default tree falls back to AOSP `external/avb/test/data/testkey_rsa2048.pem`
|
||||
(and LAB has been observed with `testkey_rsa4096`) plus
|
||||
`BOARD_AVB_MAKE_VBMETA_IMAGE_ARGS += --flags 3` unless `WITH_AVB=true`
|
||||
(`device/google/sunfish/BoardConfigLineage.mk`).
|
||||
|
||||
Project key generation is in `009-generate-release-keys.sh`
|
||||
(`avb.pem` + `avb_pkmd.bin`). **Wire in tree** (soong-safe path), not only env:
|
||||
|
||||
`vendor/sunfish-a16/BoardConfig.mk` sets `BOARD_AVB_KEY_PATH` /
|
||||
`BOARD_AVB_ALGORITHM=SHA256_RSA4096` when `certs/avb.pem` exists. Keep
|
||||
`WITH_AVB` unset → vbmeta **flags 3** =
|
||||
`HASHTREE_DISABLED|VERIFICATION_DISABLED` (libavb) → Magisk preroot on
|
||||
unlocked/orange. That is **signing infrastructure**, not runtime AVB harden.
|
||||
|
||||
Validate after bacon (script fail-closed) on **target_files `IMAGES/`** (the
|
||||
OTA pair). Do not use `out/target/product/$DEVICE/{boot,vbmeta}.img` alone —
|
||||
those make images can be stock and disagree with the Magisk payload in the zip.
|
||||
Gates: pubkey sha1 == `avb_pkmd.bin` (not AOSP testkey); flags 3;
|
||||
`verify_image --key` + `--expected_chain_partition vbmeta_system:1:avb_pkmd.bin`;
|
||||
`magiskboot cpio test` ≠ 0; LAB stays `flash.locked=0` + Magisk works.
|
||||
|
||||
**OEM unlocking:** leave enabled as a best-effort escape hatch (not a recovery
|
||||
guarantee). Do not relock the bootloader on this Magisk track.
|
||||
|
||||
## Branding
|
||||
|
||||
`TARGET_UNOFFICIAL_BUILD_ID=AXP-Resurection-Pro` is set at the **top** of
|
||||
`device/google/sunfish/lineage_sunfish.mk` (before inheriting
|
||||
`vendor/lineage` → `version.mk`). Helper:
|
||||
`apply_009_unofficial_build_id.py`. **Pro** = full Magisk resurrection track
|
||||
(not a future slim/minimal variant). Mirrors GOLDEN’s `…-sunfish-pro` /
|
||||
releasetype `PRO` intent without patching `vendor/lineage/config/version.mk`.
|
||||
|
||||
`branding.mk` adds `ro.axp.resurection.track=pro`. Ship scripts also publish
|
||||
`AXP-Resurection-Pro-<ver>-<date>-sunfish.zip` as a hardlink/copy of the
|
||||
Lineage `bacon` zip (no rebuild solely to rename).
|
||||
|
||||
|
||||
## Never
|
||||
|
||||
- Commit `*.pk8`, private `*.pem`, or pass files.
|
||||
- Relock LAB/GOLDEN as a project requirement (OEM unlock-always-on does not
|
||||
make Magisk+relock safe).
|
||||
- Flash old `kh-cve` / `kh-full` zips from 2026-09-18.
|
||||
Reference in new issue
Block a user