Docs/agent guidelines (#6129)

* docs: refresh agent guidelines

* docs: add agent harness adapters

* Update local-only block markers in AGENTS.md

DO NOT COMMIT phrase was confusing most LLMs and preventing them from committing anything without an additional explicit permission.
This commit is contained in:
eliotcougar authored and GitHub committed 2026-08-29 15:59:49 +08:00
1 parent e2dc37ba26
commit c73498b064
11 files changed
+387 -84

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
# Agent-guide adapter
Before starting any repository task, open and follow the repository-root `AGENTS.md`.
That guide defines the scoped `AGENTS.md` files that must also be opened before editing
their paths. Treat the root and applicable scoped guides as mandatory. Do not duplicate
or replace their rules in this file.
+188
View File
@@ -0,0 +1,188 @@
# v2rayNG agent guide
## Scope and precedence
This file applies to the entire repository. Read the applicable scoped guide before editing any
listed path, including when the agent starts at the repository root:
- Read `V2rayNG/app/src/main/java/com/v2ray/ang/service/AGENTS.md` for
`V2rayNG/app/src/main/java/com/v2ray/ang/service/`,
`V2rayNG/app/src/test/java/com/v2ray/ang/service/`, and these paths under
`V2rayNG/app/src/main/java/com/v2ray/ang/`: `core/CoreServiceManager.kt`,
`core/LauncherManager.kt`, `root/`, `contracts/ServiceControl.kt`,
`contracts/Tun2SocksControl.kt`, `handler/NotificationManager.kt`,
`helper/MessageHelper.kt`, and `helper/NotificationHelper.kt`.
- Read `V2rayNG/app/src/main/java/com/v2ray/ang/ui/AGENTS.md` for
`V2rayNG/app/src/main/java/com/v2ray/ang/ui/` and
`V2rayNG/app/src/test/java/com/v2ray/ang/ui/`.
Follow both guides in scoped paths. A scoped rule overrides a root rule only when they require
incompatible actions; every other root rule remains active.
## Requirement language
- `must`, `must not`, `only`, and `never` are mandatory. Only the current user request or linked
acceptance criteria can state an exception; agent inference cannot.
- `Task scope` is the named files and behavior plus dependency, test, or build edits that a
compiler or required validation proves necessary. Every other change is outside scope.
- `Changed behavior` includes direct and downstream build-time or runtime effects through callers,
consumers, storage, IPC, and resources.
- `Supported` versions, ABIs, distributions, and locales are exactly those declared by
`V2rayNG/app/build.gradle.kts` and `.github/workflows/build.yml` inputs.
- A behavior is `verified` only when its named check passed on branch HEAD. Otherwise list the
exact unrun check and blocker, and do not call the behavior verified.
## Existing code is not a compliance baseline
- These guides define the target state; they do not claim that this checkout, an older commit, or
another worktree complies. Existing code shows current behavior and is not permission to repeat
a pattern.
- A `touched declaration` is any function, property, class, composable, resource entry, manifest
element, or Gradle block containing an added or removed line. Inspect it and every caller or
consumer whose contract changes against all applicable rules before editing.
- Every new declaration and changed behavior must comply. Inspect a helper or pattern before
reusing it. Tests prove only their assertions; names, green builds, and prior approval prove
nothing else.
- Do not repair unrelated violations. Fix a violation left on a task-changed execution path in the
same branch; if that would change behavior outside scope, request approval first. Report any
remaining violation that blocks required validation.
## Agent-guide synchronization and PR boundary
- `AGENTS.md` files are the only rule source. `CLAUDE.md`, `GEMINI.md`, and
`.github/copilot-instructions.md` are routing shims and must only load or point to applicable
`AGENTS.md` files.
- Feature, fix, and release branches use instruction files from fetched `upstream/master` as their
baseline. If `upstream` is absent, run
`git remote add upstream https://github.com/2dust/v2rayNG.git`; otherwise require
`git remote get-url upstream` to print exactly that URL. Successfully run
`git fetch upstream master` before comparison. An agent-instruction branch uses its HEAD and
working diff as the proposal.
- At task start and after each upstream merge or rebase, synchronize canonical instructions into
the worktree without deleting local-only text. Compare them with:
```sh
git diff upstream/master -- ':(glob)**/AGENTS.md' ':(glob)**/CLAUDE.md' ':(glob)**/GEMINI.md' .github/copilot-instructions.md
```
After synchronization, every displayed difference must be inside a local-only block. If a branch
predates the files, keep copied versions untracked or unstaged; acquire tracked copies only by
merging or rebasing their upstream commit.
- User-, machine-, worktree-, and temporary text is local-only. Put it in `AGENTS.md` between
visible lines `LOCAL INSTRUCTIONS - START` and `LOCAL INSTRUCTIONS - END`.
Unless the task targets agent instructions, treat every pre-existing instruction-file difference
as local-only.
- Keep local-only text unstaged and uncommitted. Never use `git add .`, `git add -A`, or
`git commit -a` in that worktree. Stage task paths explicitly; never delete, restore, or overwrite
local instructions to clean the worktree.
- Before each commit, push, or PR update outside agent-instruction scope, run both commands. Both
must print no paths; otherwise stop and remove the instruction change from the staged or branch
diff without altering its working copy.
```sh
git diff --cached --name-only -- ':(glob)**/AGENTS.md' ':(glob)**/CLAUDE.md' ':(glob)**/GEMINI.md' .github/copilot-instructions.md
git diff --name-only upstream/master...HEAD -- ':(glob)**/AGENTS.md' ':(glob)**/CLAUDE.md' ':(glob)**/GEMINI.md' .github/copilot-instructions.md
```
- Commit instruction files only for a task explicitly limited to agent instructions. The branch
and PR may contain only `AGENTS.md` and necessary routing-shim changes; name agent instructions
in the PR title and state that exclusive scope in the body.
## Project boundaries
- The Android project is under `V2rayNG/` and uses Kotlin, Gradle Kotlin DSL, Compose, Material 3,
coroutines, and AndroidX lifecycle. Read dependency versions from
`V2rayNG/gradle/libs.versions.toml`, SDK levels from `V2rayNG/app/build.gradle.kts`, and CI tools
from `.github/workflows/build.yml`; never copy those numbers into a guide.
- `AngApplication` initializes MMKV, app locales, WorkManager, defaults, and theme state. Persist
application data through `MmkvManager` or `SettingsManager`; do not create `SharedPreferences`
or another path for data they own.
- `core/` owns native configuration and lifecycle, `handler/` data and application operations,
`service/` Android services, and `ui/` Compose activities, components, and ViewModels. Put new
code in its owner unless a scoped guide names a narrower owner.
- Shared lifecycle and configuration code must retain VPN, proxy-only, and root-mode branches.
Change one mode only when task scope names it; gate the change and preserve the other modes.
- `AndroidLibXrayLite` is native-core source. `V2rayNG/app/libs/libv2ray.aar` and the HEV libraries
are generated inputs; do not modify or commit them unless task scope upgrades a native dependency.
Such a PR must identify the source revision and verify every app-declared ABI.
## Build and validation
Before building a clean checkout, initialize submodules recursively and reproduce the HEV and AAR
steps in `.github/workflows/build.yml`. The AAR revision must match the `AndroidLibXrayLite` gitlink
from `git ls-tree HEAD AndroidLibXrayLite`. Take build-tool versions from that workflow and
`V2rayNG/gradle/libs.versions.toml`.
Run Gradle from `V2rayNG/`, using `gradlew.bat` in Windows PowerShell or `./gradlew` on POSIX. Use
the Play Store debug variant unless task scope names another distribution.
Apply every matching validation rule:
- Documentation only: run `git diff --check`; no Gradle task is required.
- Kotlin/Java production code: run the test class that asserts each changed behavior, then
`:app:testPlaystoreDebugUnitTest` and `:app:compilePlaystoreDebugKotlin`.
- Resources, manifest, Gradle, dependencies, or native packaging: run
`:app:assemblePlaystoreDebug`.
- F-Droid only: replace `Playstore` with `Fdroid`. Run both variants when shared code branches on a
flavor, `BuildConfig.DISTRIBUTION`, or a flavor-specific resource or dependency.
- Android service lifecycle, framework callback, native interaction, permission, accessibility,
focus, or state restoration: also run an emulator or physical-device check.
Add or update JUnit tests under `V2rayNG/app/src/test/java/` for changed deterministic logic. Report
an unavailable required check under the exact label `Not run`, with its command or scenario and
reason. A compile or assembled APK does not verify runtime behavior.
There is no enforced Kotlin formatter. Preserve local indentation and import order. Do not change
whitespace outside touched declarations or reorder imports except as the diff requires.
## Repository-wide coding rules
- Keep the diff inside task scope; exclude unrelated refactors, upgrades, generated files,
translation cleanup, renames, and formatting-only changes.
- Use server GUIDs and group IDs for persistence, asynchronous work, and UI state. Never use a
list index, adapter position, or paging position as server or group identity.
- Run disk, network, package-manager, native, bitmap, and CPU-intensive work off the main thread.
Own each asynchronous operation with a named lifecycle or ViewModel scope and cancel it when the
owner ends or newer work supersedes it.
- Put all visible and accessibility text in Android resources. Update every locale in
`androidResources.localeFilters` for each changed key, preserving placeholders, plurals, and
formatting tags.
- Never rename or delete a persisted, serialized, routing, import, or export field without a
backward-compatible migration and a regression test reading the preceding released format.
Multi-record writes must complete together or restore/remove partial writes on every failure.
- Log recoverable failures through `LogUtil`, including operation, mode/component, non-secret
stable ID when available, and exception. Never log credentials, full proxy URLs, private keys,
or exported configurations.
- Before adding a constant, helper, manager, repository, or state holder, search its owner. Extend
an owner that controls the same data or lifecycle. Create a new abstraction only for different
ownership or lifetime, and state that distinction in code or the PR.
- A platform workaround needs a comment naming its Android/API or vendor boundary, prevented
failure, and exact removal condition.
## Deprecated, experimental, and version-gated APIs
- Introduce a deprecated API only for a supported-version fallback lacking the replacement, a
required deprecated callback/interface member, or a documented platform/vendor defect that
blocks acceptance criteria. The defect requires a test or linked upstream issue.
- Isolate each permitted deprecated call in one compatibility function or adapter. Comment the
reason and an exact removal trigger. Use the stable API wherever available.
- Scope `@Suppress("DEPRECATION")` or `@SuppressLint("<IssueId>")` to the containing expression or function.
Class scope is allowed only when every member implements the same required deprecated interface;
file, package, and module scope is forbidden. Never suppress permission, background-execution,
lifecycle, or security requirements.
- Guard every API above `minSdk` by SDK or extension version in the same function, or use
`@RequiresApi` only when every entry point is guarded. Mark reusable guards
`@ChecksSdkIntAtLeast`. Lower versions must use an
existing API, return explicit unsupported status, or hide/disable the feature; never crash or
report false success.
- Add or expand an experimental API only when no stable API meets named acceptance criteria, its
dependency is pinned in `libs.versions.toml`, a project-owned stable interface contains it and
excludes it from persisted data, IPC payloads, and public shared contracts, a regression test
covers it, and a comment names the opt-in marker and exact reevaluation condition.
- Put `@OptIn` on the direct function/property, or on a class only when multiple members need it.
File and module opt-ins are forbidden. Existing experimental use outside scope does not permit a
new call site.
- A preview/canary SDK or alpha dependency is allowed in production only when the request or linked
issue requires it. Keep a stable path for supported non-preview devices, gate by runtime API or
feature availability, and test both paths. After an API guard, opt-in, or lint suppression, run
`:app:lintPlaystoreDebug` and also `:app:lintFdroidDebug` when behavior differs by distribution.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
@@ -0,0 +1,92 @@
# Service code agent guide
These rules apply to the service and service-adjacent paths listed in the repository-root
`AGENTS.md`. Follow both guides; apply the precedence rule defined by the root guide.
## Process and lifecycle invariants
- The daemon process `:RunSoLibV2RayDaemon` is the only authority for whether the native
core is running. Do not use a UI-process singleton, cached Boolean, bound-service
assumption, or successful broadcast send as proof of daemon state.
- Route every app-initiated VPN, proxy-only, and root start through `LauncherManager`.
Route service commands and their results through `MessageHelper`. Put lifecycle state
shared by two or more run modes in `CoreServiceManager` or `ServiceControl`. Do not
load or query the native core from the UI process to decide start, stop, or restart.
- Keep Android-owned `VpnService` entry paths intact. `CoreVpnService` must accept a null
restart intent and the `VpnService.SERVICE_INTERFACE` action without requiring the
app-initiated `LauncherManager` path.
- A start path is idempotent only when a second equivalent command leaves exactly one
native core, tunnel, foreground notification update loop, worker set, and root rule
set. Every edit to start dispatch or `onStartCommand` must preserve that result.
- In each `onStartCommand`, enter foreground before reading configuration, opening a
file or socket, starting a coroutine that performs setup, invoking native code, or
executing a root command. `CoreVpnService`, `CoreProxyOnlyService`, and
`CoreRootService` must call `NotificationManager.ensureForeground()`;
`CoreTestService` and `SubscriptionUpdateService` must call
`NotificationHelper.startForeground()`.
- On setup failure, cancel the setup job, release every resource created by that attempt,
remove partial root or VPN state, and stop the failed service instance. Do not return a
sticky restart mode unless `CoreServiceManager.isRunning()` is true, the service owns
every tunnel or root resource required by its run mode, and the Android restart path
reconstructs configuration, native core, notification, and mode-owned resources
without UI-process state.
- Preserve these ownership boundaries: `CoreVpnService` owns the VPN interface and
socket protection; `CoreProxyOnlyService` owns local-proxy mode; `CoreRootService`
owns root routing. Code used by two or more of these services belongs in their common
lifecycle layer. Code used by one mode remains in that mode's service.
## Cleanup, concurrency, networking, and IPC
- Teardown must prevent new work before releasing dependencies. Mark the service as
stopping, reject or invalidate pending start/reload work, cancel and join setup jobs,
then release owned resources. Remove root routing before stopping its core listener.
Close each VPN descriptor and tun2socks resource once on setup failure, revoke, stop,
and destroy; make repeated teardown calls no-ops after the first close.
- Every new coroutine launched by a service must be a child of a job or scope stored by
that service or the shared lifecycle owner. Cancel that owner during stop and
`onDestroy`. Do not use `GlobalScope`, an anonymous standalone `CoroutineScope`, or a
job whose parent outlives the service and later recreates routes, notifications,
files, or native state.
- Do not add root commands, file I/O, network probes, or native calls directly to
`onCreate`, `onStartCommand`, `onRevoke`, or `onDestroy` on the main thread. Perform
the operation in the service-owned scope. If traffic-leak prevention requires the
callback to await completion, the call site must contain an explicit timeout and a
comment naming the leak-prevention ordering; an unbounded wait is prohibited.
- A handover belongs to the `NetworkMonitor` instance that scheduled it.
`NetworkMonitor.unregister()` must cancel pending handover work and prevent its
callback from entering after `unregister()` returns. Immediately before reload, the
handover handler must verify that its monitor instance is still the instance owned by
`CoreServiceManager` and that the core is running. Stop and destroy must unregister
the monitor before clearing it or stopping the core.
- Every app-internal broadcast intent must set its package to `AppConfig.ANG_PACKAGE`;
every app-internal service intent must use an explicit `ComponentName`. Do not add an
implicit broadcast or service intent. Each payload must implement the serialization
contract used by its receiver. When a caller must distinguish `handled by daemon`
from `daemon absent`, return an acknowledgement/result from the daemon. Broadcast
delivery, bind success, and command enqueue success are not acknowledgements.
## Native resources, logging, and validation
- `TProxyService` loads the JNI library `libhev-socks5-tunnel.so`; root mode executes
the separately packaged `libhevsockstun.so`. A native or packaging diff must inspect
the produced APK as a ZIP and confirm both HEV files and `libgojni.so` for every ABI
selected by `ABI_FILTERS` or, when that property is absent, every ABI in the app's
default `splits.abi` block.
- A recoverable start, stop, reload, handover, or cleanup failure log must include the
run mode, lifecycle phase, stable non-secret profile/group identifier when one exists,
failed operation, and exception. Apply the root guide's secret-redaction rule.
- Move deterministic lifecycle decisions into pure helpers and add a JVM regression test
for each changed decision. Apply this scenario mapping to service changes:
- Start dispatch, `onStartCommand`, or command deduplication: cold start and two
equivalent consecutive start commands.
- Foreground setup or setup-error handling: successful setup and failure after at
least one resource has been acquired.
- Stop or teardown: normal stop, repeated stop, and stop while setup is in flight.
- Restart or network handover: successful restart/handover and a stop racing the
pending restart/handover.
- Shared lifecycle code: run the mapped scenarios in VPN, proxy-only, and root modes.
Mode-owned code: run them in the owning mode.
- Every service lifecycle scenario above requires an emulator or physical device; a JVM
test or assembled APK is not a substitute. Record each scenario that did not run under
`Not run`, with the device requirement or blocker. Do not claim the service change is
runtime-verified when any mapped scenario is unrun.
@@ -0,0 +1 @@
@AGENTS.md
@@ -0,0 +1 @@
@AGENTS.md
@@ -0,0 +1,95 @@
# UI code agent guide
These rules apply to the UI paths listed in the repository-root `AGENTS.md`. Follow both
guides; apply the precedence rule defined by the root guide.
## Compose architecture and state
- Implement every new screen and component with the existing Compose and Material 3
stack. A new screen activity must extend `BaseComponentActivity` or
`HelperBaseComponentActivity` and place its screen, ViewModel, and action contract in
the package that owns its navigation entry. Do not add a fragment, ViewBinding layout,
XML screen layout, or RecyclerView adapter. Do not migrate existing legacy UI outside
task scope, and do not extend it or use it as the template for new UI.
- Keep durable screen state and business work in a ViewModel/repository and expose it as
immutable observable state. Collect flows with `collectAsStateWithLifecycle()`.
Compose-local state is only for transient presentation state; use `rememberSaveable`
when that state must survive activity recreation.
- Durable state includes loaded data, selection by domain ID, validation results,
progress, errors, and any value written to settings or a profile. Transient
presentation state is limited to visual expansion, animation, scroll/focus position,
and an uncommitted input value owned solely by the visible component. A composable
must not perform repository, MMKV, import/export, routing, or native-core work.
- Send user operations through the owning feature's action/event contract and
ViewModel. Main-screen operations must use `MainAction` and `MainViewModel`.
Composables must not mutate a repository, `MmkvManager`, or `SettingsManager`
directly.
- Use server GUIDs and group IDs for Compose keys, selection, saved state, and action
parameters. Do not persist or dispatch a visible index, adapter position, or paging
offset as identity. After filtering, sorting, subscription replacement, paging, or an
asynchronous result, resolve the item again by its domain ID.
- Run blocking file, network, package-manager, and bitmap-decoding operations on
`Dispatchers.IO`. Run CPU-bound parsing, sorting, filtering, and transformation on
`Dispatchers.Default`. Launch both from the owning ViewModel or lifecycle scope.
Publish results through a private `MutableStateFlow` exposed as `StateFlow`; do not
read or write Compose snapshot state on either background dispatcher.
## Interaction, accessibility, and layout
- Give every icon with a click, long-click, toggle, or custom accessibility action a
localized accessible name that states its action. Set `contentDescription = null` on
every icon with no user action. Do not expose a raw URL,
package ID, GUID, or duplicated descendant label when a row-level semantic node
supplies the name.
- A row with one activation behavior must expose one focusable semantic node, one
localized name, its current selected/on/off state, and one activation action. Merge or
clear descendant semantics so a label, icon, checkbox, and switch do not become
duplicate focus targets. Keep a descendant as a separate node only when it performs a
different user action; give that node its own name and role. TalkBack, touch, Enter,
Space, and D-pad center must invoke the same action for the same node.
- Key each focusable list item by its domain ID. Reordering or recomposition must keep
focus on the node with that ID if it still exists. If a selection change removes or
replaces the focused node, move focus exactly once to the final active item after the
state update; do not announce or focus an intermediate item.
- Use `stringResource` for text created inside a composable and localized
`Context.getString`/resources outside Compose. This rule covers visible labels,
semantic labels, state descriptions, validation messages, errors, dialogs, snackbars,
and toasts. Apply the root guide's locale and placeholder requirements to every key.
- Every form containing a text-editing control must use a vertically scrollable
container. Inside a `Scaffold`, apply its `innerPadding`, call
`consumeWindowInsets(innerPadding)`, and
apply `imePadding()` to that container. Outside a `Scaffold`, apply `imePadding()` and
consume the navigation-bar inset exactly once. With the IME visible, the user must be
able to focus, read, edit, and activate both the first and last form controls without
dismissing the IME.
- A shared component used by server screens must preserve edge-to-edge system-bar
insets, light and dark themes, dynamic color enabled and disabled, and both single-
and double-column layouts. A change to such a component requires rendering and
interaction checks in each of those configurations; dynamic-color checks require a
device or emulator that supports dynamic color.
## Validation mapping
Apply every row whose trigger matches the UI diff:
- ViewModel state, action mapping, validation, filtering, parsing, or selection logic:
add or update a JVM test that asserts the initial state and every success, invalid-
input, empty-input, and failure branch added or modified by the diff.
- `rememberSaveable`, `SavedStateHandle`, activity recreation, or restoration logic:
recreate the activity and verify the final state by domain ID, not list position.
- Loading or data presentation: exercise every state represented by the owning state
model among empty, loading, error, and populated; do not invent states absent from
that model.
- Click, toggle, selection, or navigation behavior: verify touch, keyboard focus and
activation, and D-pad focus and activation.
- Semantics, focus, accessible text, row merging, or icon meaning: inspect the semantics
tree and run TalkBack on an emulator or device. Record the spoken label, state, focus
order, and activation result.
- Form fields, scrolling, or insets: open the IME, traverse from the first control to the
last control, and activate the primary action with the IME still visible.
- A setting, profile, routing rule, or other persisted value: perform the action, close
and reopen the screen, and verify the value through the owning ViewModel/repository.
A screenshot verifies appearance only. It does not verify semantics, focus, activation,
state ownership, persistence, or recreation. Record every mapped check that did not run
under `Not run`; do not claim the corresponding behavior is verified.
@@ -0,0 +1 @@
@AGENTS.md
@@ -0,0 +1 @@
@AGENTS.md
-84
View File
@@ -1,84 +0,0 @@
# v2rayNG — Agent guide
## Build
```sh
cd V2rayNG
./gradlew assembleFdroidDebug # or assemblePlaystoreDebug
```
Kotlin 2.4.0, AGP 9.2.1, Gradle Kotlin DSL. No lint/format/typecheck tasks configured.
## Test
```sh
cd V2rayNG && ./gradlew test
```
JUnit 4 + Mockito. Unit tests only under `app/src/test/java/`. No instrumented tests beyond defaults.
## Project structure
```
V2rayNG/
app/
src/main/java/com/v2ray/ang/
AngApplication.kt # extends MultiDexApplication, inits MMKV + WorkManager
AppConfig.kt # all constants, pref keys, URLs, ports, tags
core/ # v2ray core integration
CoreServiceManager.kt # start/stop core, traffic stats, delay measurement
CoreConfigManager.kt # generates JSON config for v2ray core
CoreNativeManager.kt # JNI bridge to libv2ray AAR
CoreOutboundBuilder.kt # outbound config construction
CoreConfigContextBuilder.kt
service/ # Android foreground services
CoreVpnService.kt # VPN mode (VpnService)
CoreProxyOnlyService.kt # proxy-only mode (no VPN)
CoreTestService.kt # delay test
TProxyService.kt
DialerNativeService.kt / DialerWebviewService.kt # browser dialer
RealPingWorkerService.kt # WorkManager-based real ping
QSTileService.kt # quick settings tile
ProcessService.kt
handler/ # business logic
MmkvManager.kt # all MMKV CRUD (servers, subs, settings, routing)
SettingsManager.kt # preference defaults, config generation (633 lines)
AngConfigManager.kt # server config operations
NotificationManager.kt
SpeedtestManager.kt
SubscriptionUpdater.kt
WebDavManager.kt
UpdateCheckerManager.kt
CertificateFingerprintManager.kt
SettingsChangeManager.kt
ui/ # activities, adapters, fragments
MainActivity.kt # main screen with drawer + tabs
ServerActivity.kt # edit server config
ServerCustomConfigActivity.kt / ServerGroupActivity.kt / ServerProxyChainActivity.kt
SettingsActivity.kt / PerAppProxyActivity.kt / AppPickerActivity.kt
ScannerActivity.kt / LogcatActivity.kt
RoutingSettingActivity.kt / RoutingEditActivity.kt
SubSettingActivity.kt / SubEditActivity.kt
UserAssetActivity.kt / UserAssetUrlActivity.kt
TaskerActivity.kt / UrlSchemeActivity.kt
BackupActivity.kt / CheckUpdateActivity.kt / AboutActivity.kt
fmt/ # protocol URL parsers (VMESS, VLESS, TROJAN, SS, SOCKS, etc.)
dto/ # data classes + entities/
enums/ # EConfigType, Language, RoutingType, etc.
extension/_Ext.kt # extension functions (toast, traffic string, etc.)
util/ # Utils, JsonUtil, HttpUtil, LogUtil, etc.
receiver/ # BootReceiver, TaskerReceiver, WidgetProvider
contracts/ # interfaces (ServiceControl, Tun2SocksControl)
helper/ # QRCodeScannerHelper, PermissionHelper, FileChooserHelper etc.
```
## Key facts
- **Storage**: MMKV exclusively — never SharedPreferences. `MmkvManager` is the data layer.
- **Core**: Native AAR (`libv2ray`) from [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) or [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite). Prebuilt `.aar` files go in `app/libs/`.
- **Services** run in dedicated process `:RunSoLibV2RayDaemon`. `CoreServiceManager` controls start/stop lifecycle.
- **Two modes**: VPN (`CoreVpnService`, uses `VpnService.Builder`) or proxy-only (`CoreProxyOnlyService`, local SOCKS/HTTP).
- **Flavors**: `fdroid` (suffix `.fdroid`) and `playstore` (no suffix). ABI version codes differ per flavor.
- **hev-socks5-tunnel**: Optional tun2socks binary. Build with `./compile-hevtun.sh` (requires `NDK_HOME`).
- **ViewBinding** enabled, no DataBinding.
- **No CI**, no pre-commit hooks, no lint/format enforcement.