Commit graph

263 commits

Author SHA1 Message Date
Horizon
cf4e72fa01
fix(build): port build scripts to Python to allow Windows contributors to build subprojects (#83)
* Rewrite build-version and all build-zip bash scripts to python

* Add executable permissions to python build scripts

* Use python build script for kmod in CI

* Fix

* Enhance kmod build script, add/fix docs, CI edits

* Delete remaining build-zip bash scripts

* Delete remaining build-zip bash scripts
2026-04-25 19:53:15 +03:00
okhsunrog
4ad2ba8c2d ci: fall back to ephemeral keystore when secrets are unavailable
GitHub Actions does not expose secrets to workflows triggered by PRs from
forks, so the lsposed job's `assembleRelease` was failing with a corrupt
release.jks for every external contributor. Generate a throwaway keystore
on the fly in that case so fork PRs get a green CI; signed-for-release
artifacts (push/tag runs) keep using the real secrets unchanged.
2026-04-25 19:42:44 +03:00
Danila Gornushko
d6f6d62682
Merge pull request #69 from BlueGradientHorizon/main
Some checks failed
CI Image / build-push (push) Has been cancelled
CI / setup (push) Has been cancelled
CI / kmod (android12-5.10) (push) Has been cancelled
CI / kmod (android13-5.10) (push) Has been cancelled
CI / kmod (android13-5.15) (push) Has been cancelled
CI / kmod (android14-5.15) (push) Has been cancelled
CI / kmod (android14-6.1) (push) Has been cancelled
CI / kmod (android15-6.6) (push) Has been cancelled
CI / kmod (android16-6.12) (push) Has been cancelled
CI / portshide (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / zygisk (push) Has been cancelled
CI / lsposed (push) Has been cancelled
CI / release (push) Has been cancelled
fix(ci): fix docker image pull errors for forks with uppercase usernames
2026-04-21 19:06:49 +03:00
BlueGradientHorizon
e170a6e9df Simpler approach to lowercasing
Revert adding docker/metadata-action
2026-04-21 18:32:28 +03:00
okhsunrog
c207b5edd3 chore: update-json for v0.7.1 2026-04-21 16:51:32 +03:00
okhsunrog
7f22875e6b chore: release v0.7.1 2026-04-21 16:45:04 +03:00
Danila Gornushko
1da843c643
Merge pull request #75 from okhsunrog/test/lsposed-native-install-recommendation
test(lsposed): unit-test buildNativeInstallRecommendation
2026-04-21 16:10:16 +03:00
okhsunrog
7b1ce8ffac test(lsposed): cover buildNativeInstallRecommendation decision table
17 tests exercising every branch of the KMI × kernel-series matcher:

  - every row of the exact-match shipping matrix (android12-5.10
    through android16-6.12),
  - the reporter's regression scenario (5.10-android12 kernel on an
    Android 15 ROM — used to land in zygisk-recommended, now correctly
    resolves to android12-5.10 with androidVersion "Android 15"),
  - ambiguous series fallbacks (5.10 / 5.15 without a KMI tag return
    both candidates via variantAmbiguous),
  - deterministic series fallbacks (6.1 / 6.6 / 6.12 without KMI
    still unambiguous),
  - pre-GKI / unsupported series (4.14, 5.4, 6.3) all fall back to
    zygisk,
  - unparseable / blank / empty kernel strings (don't crash, return
    zygisk or null respectively),
  - parseKernelSeries + parseKernelAndroidBranch edge cases.

Zero new test infrastructure — plain JUnit 4 @Test like the existing
RussianAppFilterTest.
2026-04-21 16:00:36 +03:00
okhsunrog
acc9446952 refactor(lsposed): make buildNativeInstallRecommendation unit-testable
The KMI × kernel-series matching logic was buried as a local function
inside loadDashboardState and called suExec / Build.VERSION.RELEASE
inline, which made it impossible to exercise from JVM unit tests.

Extract buildNativeInstallRecommendation(kernelRaw, deviceAndroidLabel)
as a top-level internal pure function taking both inputs as strings,
plus the parseKernelSeries and parseKernelAndroidBranch helpers it
uses. The su and Build.VERSION probes now happen at the call site in
loadDashboardState, so the core matching logic is a pure String ->
NativeInstallRecommendation? mapping that tests can hit directly.

No behaviour change.
2026-04-21 15:59:08 +03:00
Danila Gornushko
6f5443f188
Merge pull request #71 from okhsunrog/fix/lsposed-kmod-active-overrides-unsupported-kernel
fix(lsposed): recommend kmod on custom kernels missing the GKI KMI tag
2026-04-21 15:54:58 +03:00
okhsunrog
7b3f013a2d docs(lsposed): RU string — "в эту сессию" -> "при текущей загрузке" 2026-04-21 15:47:12 +03:00
okhsunrog
137bedc385 refactor(lsposed): align kmodOnUnsupportedKernel gating with variant-mismatch
The freshForCurrentBoot gate was asymmetric: kmodVariantMismatch
fires the moment you install a wrong-variant zip (valid, since it's
decided from the module.prop-stamped gkiVariant vs the kernel-series
table — no boot required), but kmodOnUnsupportedKernel waited for
post-fs-data even though its predicate is identical in nature
(kernel-series ∉ GKI table ⇒ zygisk recommended). Same philosophy,
same gate.

Keep the boot gate on kmodAmbiguousLoadFailed — that one is literally
"the kmod you installed tried to load this boot and failed, pick the
other candidate", so it genuinely requires current-boot insmod
status.
2026-04-21 15:46:54 +03:00
okhsunrog
282a9bba25 feat(lsposed): explain the GKI KMI tag in the install-recommendation card
On a device where the Android OS release differs from the kernel's
GKI KMI tag (common on Pixel 4a/6a etc. — Android 14/15 ROM over an
android12-5.10 kernel) the kernel-version line shows "android12"
right next to "Your device: Android 15", which is genuinely
confusing if you don't know what a KMI is.

Adds a small italic note below the device/kernel line that spells
out what the "androidNN" fragment actually means. Only shown when
the KMI parses out of uname -r AND it differs from the OS release —
when they match the note would just be noise, and when no KMI is in
uname -r there's nothing to clarify.
2026-04-21 15:45:44 +03:00
okhsunrog
91b00c71d5 fix(lsposed): debug-logging hint says "below" not "above" 2026-04-21 15:12:29 +03:00
Danila Gornushko
b6081e9bae
Merge pull request #73 from okhsunrog/feat/lsposed-profile-names
feat(lsposed): show profile names instead of user IDs
2026-04-21 15:11:47 +03:00
okhsunrog
1a1e84a5fa docs(lsposed): rewrite multi-profile warning to match post-#66 reality
The existing dashboard_issue_self_multi_profile warning still said the
picker "only sees apps from its own profile" and that a Save would
"silently drop targets it can't see" — both of which stopped being
true after #66 switched every picker and resolver to `pm list packages
--user all`. Running VPN Hide in more than one profile is still not
recommended, but for different reasons: the main-profile copy is the
only one that matters (LSPosed hooks live in system_server, which is
main-profile only), extras in secondary profiles are redundant, and
they race each other's Saves to the shared target files.

Also spells out why "install only in Second Space" is not an option,
which was coming up in support threads.
2026-04-21 15:04:08 +03:00
okhsunrog
d24b73ebb1 docs: changelog for profile-name rendering 2026-04-21 14:55:09 +03:00
okhsunrog
5148da169e feat(lsposed): render profile names instead of user IDs in app rows
Chrome installed in three profiles used to read "Chrome (0, 10, 11)"
— cryptic unless you know Android's user-ID scheme. Every Protection
screen now threads AppListCache.userNames through the row renderer
and hands it to labelWithUsers, so the same row reads
"Chrome (Owner, Work, Test User 1)".

Applied to all three Protection screens (Tun targets, App hiding,
Ports). When the name map is missing (no root, parse fail) the helper
still falls back to numeric IDs, so nothing regresses.
2026-04-21 14:54:53 +03:00
okhsunrog
75ef3fe661 feat(lsposed): load user-profile names alongside the package scan
Prep work for rendering apps with "(Work, Second Space)" instead of
"(10, 11)" in the Protection list. Batches `pm list users` into the
same `su` invocation as the package scan via a sentinel-split output,
so the cost is still one root roundtrip per refresh.

Exposes the parsed user_id -> friendly_name map as a StateFlow on
AppListCache. Extends labelWithUsers(label, userIds, userNames =
emptyMap()) to substitute names when present; numeric fallback stays
when root isn't available or parsing fails, so no caller is forced to
pass the map.
2026-04-21 14:50:20 +03:00
Danila Gornushko
c25b267a6b
Merge pull request #72 from okhsunrog/fix/lsposed-multi-profile-polish
fix(lsposed): polish multi-profile app list (follow-up to #66)
2026-04-21 14:46:57 +03:00
okhsunrog
00f281fe3c fix(lsposed): drop "(0)" user-ID suffix when app is only in current profile
The multi-profile merge shipped with a labelWithUsers property that
appended the user-ID list to every row unconditionally — so single-
profile users (the large majority) saw every entry as
"Telegram (0)", "Chrome (0)", ... even though there was nothing
multi-profile to disambiguate.

Extract a top-level labelWithUsers(label, userIds) helper that
suppresses the suffix when the only user is the current one, and apply
it across all three Protection screens (Tun targets, App hiding,
Ports) — previously only the Tun screen rendered the suffix, which was
itself an inconsistency.
2026-04-21 14:33:20 +03:00
Danila Gornushko
04eef02d94
Merge pull request #67 from okhsunrog/feat/lsposed-russian-apps-batch-2026-04-20
feat(lsposed): detect 7 more Russian brands
2026-04-21 14:31:49 +03:00
okhsunrog
884fbf6dd4 fix(lsposed): classify secondary-only apps correctly as system or user
When a package exists only in a secondary profile (work profile, MIUI
Second Space, etc.) PM lookup fails for the main-user context and we
fall back to parsing ApplicationInfo from the APK via
getPackageArchiveInfo. The archive-parsed info doesn't carry
FLAG_SYSTEM — that bit is attached by PM at install time, not stored
in the manifest — so every secondary-only app was being classified as
user-installed and the "Show system apps" filter hid none of them.

Fall back to the APK path: /data/app/... is user-installed, everything
else (/system, /product, /vendor, /apex, ...) is baked into the system
image. Flag-based detection still wins whenever PM returns a real
ApplicationInfo.
2026-04-21 14:29:42 +03:00
okhsunrog
9a5d979ad1 refactor(lsposed): fold three package-enumeration su calls into one
AppListCache.reload() was spawning three separate `su -c pm list
packages ...` processes to collect package names, UIDs, and APK paths
respectively. Each `su` spin-up is ~50-100ms, so every refresh paid
~150-300ms for no reason — `pm list packages -U -f --user all` returns
all three fields in a single call.

Parser now merges comma-joined UIDs (AOSP `--user all`) and also
merges duplicate lines emitted per-user on ROMs that prefer that
format, so the output is stable across flavors. No behavior change
for callers.
2026-04-21 14:29:17 +03:00
Danila Gornushko
7d6ef30f1f
Merge pull request #66 from AndreyGubin/issue-65
fix(lsposed): show apps from both spaces
2026-04-21 14:23:18 +03:00
okhsunrog
24c3215c5d fix(lsposed): recommend kmod on custom kernels missing the GKI KMI tag
Reported: kmod loads fine (diagnostics pass, /proc/vpnhide_targets
exists) on a custom 5.10 kernel whose uname -r is "5.10.253-Glow-v4.7",
yet the dashboard shows a red "kernel not supported" card and
recommends replacing the kmod with zygisk.

Two orthogonal problems, both fixed here.

1. KMI matching conflated KMI with Android OS release. kernelBranch was
   parsed from an androidNN tag inside uname -r (the GKI Kernel Module
   Interface identifier), but when the tag was absent the code fell back
   to Build.VERSION.RELEASE — a ROM-level value that has nothing to do
   with KMI. An Android 15 HyperOS on an android12 GKI kernel is a
   common combo; the old fallback matched (Android 15, 5.10), which
   isn't in the GKI baseline table, and escalated to zygisk. Now: match
   only the parsed KMI. No Build.VERSION.RELEASE fallback for matching.

2. When the KMI tag is genuinely missing (custom kernels strip it), fall
   back on kernel series. Unique series (6.1 / 6.6 / 6.12) each ship a
   single KMI variant so the recommendation stays deterministic.
   Ambiguous series (5.10 / 5.15) each ship two variants — propagate
   both via new NativeInstallRecommendation.variantAmbiguous +
   alternativeArtifact fields, so the install card surfaces "install X;
   if it doesn't load after reboot, install Y instead" and the broken-
   kmod banner offers the alternative when one of the two candidates
   failed to insmod. kmodVariantMismatch accepts either candidate for
   ambiguous series so it doesn't fire on a legitimately-installed
   alternative variant.

Defense in depth on top of the smarter heuristic: every heuristic-
driven kmod warning is now gated on !kmodRaw.active (active kmod =
/proc/vpnhide_targets exists = empirical proof of working install), and
kmodOnUnsupportedKernel additionally requires freshForCurrentBoot so a
just-installed-but-not-rebooted module doesn't get prematurely flagged.

Also adds KmodBrokenReason.AmbiguousLoadFailed + matching card subtitle
and issue banner, plus EN/RU strings for all three new surface areas.
2026-04-21 12:52:37 +03:00
BlueGradientHorizon
1f662b0489 Proper fix instead of dirty hack
Use docker/metadata-action to perform username lowercasing
2026-04-20 21:05:03 +03:00
Andrey Gubin
b9b7bd340f fix lint 2026-04-20 22:04:33 +04:00
BlueGradientHorizon
0830cd82fc Try to fix CI 2026-04-20 20:42:44 +03:00
okhsunrog
f1e6a61970 feat(lsposed): add 7 Russian brand prefixes to app detection
Covers Anixart, ePN Cashback, TNT Premier, Swoo (Кошелёк),
Макси, Ростелеком Личный кабинет, Проверка чеков ФНС.
2026-04-20 19:29:53 +03:00
Andrey Gubin
329242877c fix(lsposed): show apps from both spaces 2026-04-20 19:19:54 +04:00
okhsunrog
07e4247749 update lockfile
Some checks are pending
CI / lint (push) Waiting to run
CI / kmod (android12-5.10) (push) Waiting to run
CI / kmod (android13-5.10) (push) Waiting to run
CI / kmod (android13-5.15) (push) Waiting to run
CI / kmod (android14-5.15) (push) Waiting to run
CI / kmod (android14-6.1) (push) Waiting to run
CI / kmod (android15-6.6) (push) Waiting to run
CI / kmod (android16-6.12) (push) Waiting to run
CI / zygisk (push) Waiting to run
CI / lsposed (push) Waiting to run
CI / portshide (push) Waiting to run
CI / release (push) Blocked by required conditions
2026-04-20 16:54:16 +03:00
Danila Gornushko
87037d5857
Merge pull request #60 from okhsunrog/feat/lsposed-apk-size-optimization
Some checks are pending
CI / lint (push) Waiting to run
CI / kmod (android12-5.10) (push) Waiting to run
CI / kmod (android13-5.10) (push) Waiting to run
CI / kmod (android13-5.15) (push) Waiting to run
CI / kmod (android14-5.15) (push) Waiting to run
CI / kmod (android14-6.1) (push) Waiting to run
CI / kmod (android15-6.6) (push) Waiting to run
CI / kmod (android16-6.12) (push) Waiting to run
CI / zygisk (push) Waiting to run
CI / lsposed (push) Waiting to run
CI / portshide (push) Waiting to run
CI / release (push) Blocked by required conditions
perf(lsposed): APK 47 MB → 3.37 MB (R8 + core-splashscreen)
2026-04-20 12:18:51 +03:00
okhsunrog
572b865e1a perf(lsposed): APK 47 MB -> 3.37 MB, polish splash screen
isMinifyEnabled was off in release, so Compose + material-icons-extended
+ kotlinx stdlib shipped un-shrunk. com.google.android.material was
pulled in solely for Theme.Material3.DayNight.NoActionBar, but Compose
handles DayNight itself via isSystemInDarkTheme() — swap for
androidx.core:core-splashscreen + a local theme parented from
Theme.DeviceDefault.NoActionBar. proguard-rules.pro already had the
Xposed keep rules, so enabling R8 + isShrinkResources was a flag flip.

While inside the splash path, hold it through the startup chain with
setKeepOnScreenCondition so the Dashboard appears already populated
instead of flashing three transient spinners (root check,
selfNeedsRestart, cache load). DashboardCache prewarm moved up into
MainScreen so it runs while the splash is still held.

Dead wizard-generated drawable/ic_launcher_{foreground,background}.xml
had no referrers — the live launcher icon is the adaptive-icon under
mipmap-*/.

APK: 47.22 MB -> 3.37 MB (-93%). Single classes.dex (multi-dex no
longer needed).
2026-04-20 05:54:41 +03:00
okhsunrog
ab7ccdbb82 fix(lsposed): remove Kmod load trace UI section
Some checks are pending
CI / lint (push) Waiting to run
CI / kmod (android12-5.10) (push) Waiting to run
CI / kmod (android13-5.10) (push) Waiting to run
CI / kmod (android13-5.15) (push) Waiting to run
CI / kmod (android14-5.15) (push) Waiting to run
CI / kmod (android14-6.1) (push) Waiting to run
CI / kmod (android15-6.6) (push) Waiting to run
CI / kmod (android16-6.12) (push) Waiting to run
CI / zygisk (push) Waiting to run
CI / lsposed (push) Waiting to run
CI / portshide (push) Waiting to run
CI / release (push) Blocked by required conditions
The Diagnostics screen rendered a raw key=value dump of
/data/adb/vpnhide_kmod/load_status + load_dmesg as a monospace blob,
asking users to "share this block" for bug reports. The same data is
already (a) parsed into proper Dashboard issue cards (wrong-variant
kmod, missing kretprobes, etc.) and (b) bundled into the Collect debug
log zip, so the on-screen dump was redundant and ugly.

- Drop KmodLoadTraceCard composable + its loader and state
- Drop diag_kmod_load_trace_{title,description} from EN+RU
- Repoint dashboard_issue_kmod_load_failed at "Collect debug log"
  instead of the now-deleted trace section
- Edit changelog.json history[0] for v0.7.0 to drop the trace mention
  and regenerate CHANGELOG.md + update-json/changelog.md
2026-04-20 03:34:31 +03:00
okhsunrog
10f5850193 chore(scripts): add stats.py for GitHub release download counts
Quick uv-shebang script that hits the GitHub Releases API and prints a
per-asset download table for every release, plus a grand total. No new
deps in the repo — script declares its own (httpx + rich) inline.
2026-04-20 03:05:33 +03:00
okhsunrog
cce7d758fc chore: update-json for v0.7.0 2026-04-20 03:01:52 +03:00
okhsunrog
4cf512c6ae chore: release v0.7.0 2026-04-20 02:54:26 +03:00
Danila Gornushko
38e5638ccb
Merge pull request #59 from okhsunrog/perf/prewarm-diagnostics-cache
perf(lsposed): pre-warm DiagnosticsCache at app launch
2026-04-20 02:35:52 +03:00
okhsunrog
57ab9f46ec perf(lsposed): pre-warm DiagnosticsCache at app launch
DiagnosticsCache only triggered runAllChecks the first time the user
opened the Diagnostics tab. That meant a brief spinner on first navigation
even though the same caches we added for Dashboard / Protection make
those tabs instant. The cache itself was already designed to survive tab
switches — we just weren't kicking the load early enough.

MainActivity now fires DiagnosticsCache.run as soon as selfNeedsRestart
is known to be false. By the time the user switches from Dashboard to
Diagnostics, runAllChecks has typically finished and the tab opens with
results already in place — no spinner.
2026-04-20 02:30:20 +03:00
Danila Gornushko
273cfd9554
Merge pull request #58 from okhsunrog/fix/tun-hint-restart-target
docs(lsposed): spell out target-restart rules in Tun help accordion
2026-04-20 02:30:17 +03:00
okhsunrog
487c507844 docs(lsposed): spell out target-restart rules in Tun help accordion
Audit of every "restart" mention surfaced one missing hint: the Tun
help accordion explained how to use the L/K/Z toggles but never said
which of them need the target app to be force-stopped + reopened
after Save. New apps_hint_restart_target paragraph fills the gap:

- L (LSPosed) and K (kmod) — UID lists are live-reloaded by the
  hooks; the target sees the new behavior on its next syscall, no
  restart.
- Z (Zygisk) — hooks are injected at zygote fork, so flipping Z for
  an already-running target only takes effect on its next launch.
2026-04-20 02:07:11 +03:00
Danila Gornushko
d8c195c0c8
Merge pull request #57 from okhsunrog/feat/multi-profile-support
feat: target apps across all user profiles (work profile, etc.)
2026-04-20 02:04:34 +03:00
okhsunrog
42dbdfd272 docs: disambiguate every restart/reboot mention in UI + README
Audit pass: every user-facing mention of "restart" or "reboot" now
states explicitly which thing — the device, the VPN Hide app, or the
target apps — needs the action. A few were ambiguous (especially the
banner text + module-card subtitles around `selfNeedsRestart`, which
just said "restart the app" without saying which one).

UI strings (EN + RU):
- banner_added_self / dashboard_needs_restart: "Restart VPN Hide
  (force-stop and reopen) … no device reboot needed".
- dashboard_installed_restart_app: "Installed, restart VPN Hide to
  activate" (the Zygisk module-card subtitle, shown when VPN Hide
  just added itself to its own targets).
- dashboard_reboot_needed: "Device reboot needed" (LSPosed module
  card subtitle).
- dashboard_issue_version_mismatch: "Reboot the device to apply…".
- vpn_off_prompt: "Results are cached until VPN Hide is restarted"
  (was "for the rest of this app session" — now names the app
  explicitly).

README.en.md:
- Step 1.3 / Step 2 final lines say "Reboot the device".
2026-04-20 01:58:23 +03:00
okhsunrog
7695e91300 docs(changelog): swap Telegram → Ozon in multi-profile entry 2026-04-20 01:51:11 +03:00
okhsunrog
eeda05509a feat(lsposed): warn when VPN Hide is installed in multiple profiles
The pkg→UID resolver fix in the previous commit makes targeting
multi-profile apps work transparently — but if the user also installs
the VPN Hide APK itself in more than one profile, there's a footgun:

Each APK instance shares the same target files (those live in
/data/adb/ and /data/system/, system-wide), but each instance's app
picker only sees apps from its own profile (PackageManager
.getInstalledApplications is per-user). A Save from a profile that
doesn't see all targets silently drops them from the saved list.

Detect this from the Dashboard: if `pm list packages -U --user all`
reports more than one UID for our own package, emit a Warning that
recommends uninstalling from secondary profiles and managing targets
only from the main one.
2026-04-20 01:49:12 +03:00
okhsunrog
16578a15d2 feat: target apps across all user profiles (work profile, etc.)
Android user profiles (work profile, MIUI Second Space, Private Space,
secondary users) each give the same package its own UID in the
namespace `<user>*100000 + <app_id>`. Previously every pkg→UID
resolver used plain `pm list packages -U`, which only emits UIDs for
the primary user, so the work-profile copy of Telegram kept seeing
the VPN even though the user had marked Telegram as a target.

Switch every resolver to `pm list packages -U --user all`. The pm
output format for multi-profile apps is comma-separated on one line:

    package:com.android.chrome uid:10187,1010187

Each call site now splits on `,` and emits one UID per line so every
profile's copy is individually matched by the hooks. No UI changes —
"mark Telegram as a target" just now means "in every profile it's
installed in".

Resolvers touched (all places found by an audit, no duplicates left):

  Shell (boot-time):
    kmod/module/service.sh
    zygisk/module/service.sh
    portshide/module/vpnhide_ports_apply.sh

  Kotlin (save-time via suExec):
    AppPickerScreen.kt       — buildUidResolver
    AppHidingScreen.kt       — buildHidingUidResolver
    ShellUtils.kt            — ensureSelfInTargets
    TargetsCache.kt          — PM_LIST batch script + parser

Verified on a Pixel 4a with a managed profile (user 10):
  - Chrome toggled in LZ on primary → both 10187 and 1010187 land in
    /data/system/vpnhide_uids.txt.
  - Primary-only apps (Ozon, etc.) still resolve to a single UID.
  - ensureSelfInTargets correctly adds both UIDs when vpnhide is
    installed across profiles.
2026-04-20 01:27:11 +03:00
Danila Gornushko
c6c408c25b
Merge pull request #56 from okhsunrog/feat/diagnostics-cache
perf(lsposed): cache Diagnostics + Protection state for the session
2026-04-20 00:58:33 +03:00
okhsunrog
c5d96546bd perf(lsposed): cache Diagnostics + Protection state for the session
Two new app-scoped caches complete the "nothing re-runs on tab switch"
story started by #53 / #54:

- DiagnosticsCache: state machine (NotRun / Running / VpnOff / Ready)
  around runAllChecks(). Once Ready, hook-probe results are fixed for
  the process lifetime — hooks don't change mid-session, so there's
  nothing to re-run. VpnOff exposes a shared retry UI. Dashboard's
  "Protection status" section and Diagnostics top section both read
  from this cache.
- TargetsCache: every root-shell read each Protection screen used to
  do on tab switch (target files, observer files, pm list packages -U,
  module.prop) is now a single batched suExec collected at startup and
  refreshed on Save / top-bar Refresh. Eight serial root roundtrips
  collapsed to one.

UX tweaks:
- Shared VpnOffPrompt composable (banner + Retry button) used on both
  Dashboard and Diagnostics — tapping Retry refreshes both caches so
  state stays consistent across screens.
- Diagnostics split into two visually independent sections: protection
  checks on top (state-dependent), log-capture tools below (always
  visible). Previously the bottom cards disappeared when VPN was off,
  cutting users off from the very tools they need to report a bug.
- CheckResults / isVpnActive / runAllChecks promoted from `private` to
  `internal` so the cache can call them.

Save handlers in AppPicker / AppHiding / PortsHiding now call
TargetsCache.refresh() alongside DashboardCache.invalidate().
2026-04-20 00:51:05 +03:00
Danila Gornushko
4d81b35c27
Merge pull request #54 from okhsunrog/feat/dashboard-cache
perf(lsposed): cache Dashboard state + update check
2026-04-20 00:24:02 +03:00