mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-30 15:48:36 +00:00
The 33 descriptions (~16k chars, ~19k with paths) overran Codex's skill-list budget (2% of the context window, or 8,000 chars when unknown), and a review found most bodies stale: paths from before the commons/commonsUI moves, the deleted legacy desktop app, a callback signer API that no longer exists, and example APIs that were never in the tree. Kept, rewritten and checked against the code (every path, class and command): account-state (+feeds), amy-expert, android-expert, auth-signers, compose-expert (+API style, Navigation 3 back stacks), desktop-expert (+run/package), event-store-semantics (+NIP-50 search), find-missing- translations, kotlin-coroutines (+structured concurrency, flow modeling), kotlin-multiplatform (+Gradle, composable extraction), ngit-pr, nip85-trusted-assertions, nostr-expert (+NIP lookup, generated NIP catalog), relay-client. Dropped: textbook Compose/Kotlin technique skills, kotlin-expert, kotlin-types-value-class, gradle-expert, quartz-integration (external-facing; see the Quartz docs commit), find-non-lambda-logs (audit finished; the rule is now in AGENTS.md Kotlin Style), and the /nip, /desktop-run, /extract commands. The skill list is now ~4k chars. Also drops .claude/core-skills-plan.md (stale history) and repoints SearchableEvent's KDoc to event-store-semantics. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
7.4 KiB
Markdown
130 lines
7.4 KiB
Markdown
---
|
|
name: android-expert
|
|
description: Android shim in `amethyst/`: MainActivity, AndroidManifest/intent filters, :napplet process, play/fdroid flavors, R8 + tools/r8-verify, permissions, Intent/Context ports. Use when editing amethyst/. NOT for screens/navigation (compose-expert).
|
|
---
|
|
|
|
# android-expert
|
|
|
|
`amethyst/` is shrinking to an Android shim: entry points, services, system integrations and
|
|
the actuals of shared ports. Screens, the nav host and the bar / rail / drawer are shared UI in
|
|
`commonsUI` (see `compose-expert`). A new screen goes there even when only Android shows it.
|
|
|
|
Source root: `amethyst/src/main/java/com/vitorpamplona/amethyst/` (an Android app module, not
|
|
KMP: there is no `androidMain` here). Flavor code is in `amethyst/src/play/` and
|
|
`amethyst/src/fdroid/`.
|
|
|
|
## Entry points
|
|
|
|
- `Amethyst.kt`: the `Application`. Builds `AppModules` (`Amethyst.instance`) in the main
|
|
process only.
|
|
- `ui/MainActivity.kt`: the single main Activity (`launchMode="singleInstance"`).
|
|
`onCreate` calls `enableEdgeToEdge()` and sets
|
|
`AmethystTheme { ... AmethystApp(Amethyst.instance.sessionManager, AndroidAppRoot) }`.
|
|
Everything from there down is shared UI.
|
|
- `ui/AndroidAppRoot.kt`: `object AndroidAppRoot : AppRoot`, Android's implementation of the
|
|
shared shell port (`commonsUI/.../commons/ui/app/AppRoot.kt`). It registers the Android-only
|
|
destinations (`androidDestinations` in `ui/navigation/AppNavigation.kt`) and runs
|
|
`NavigateIfIntentRequested`.
|
|
- `ui/platform/AndroidAppPlatform.kt` (`AppPlatform`) and `ui/platform/AndroidAppServices.kt`
|
|
(`AppServices`, the interface is in `commons/.../commons/service/AppServices.kt`): the other two
|
|
ports the shared UI calls.
|
|
- Other activities have their own windows: `ui/call/CallActivity`,
|
|
`ui/screen/loggedIn/nests/room/activity/NestActivity`, `service/playback/pip/PipVideoActivity`,
|
|
the signer / napplet consent activities, and `HealthConnectRationaleActivity`.
|
|
|
|
## Two processes
|
|
|
|
The app runs in **main** and **`:napplet`**. Read `AGENTS.md` "Android Runtime Processes" before
|
|
touching `Amethyst.kt`, an Application callback, or anything under `napplet/`. The sandbox
|
|
activities and services (`NappletHostActivity`, `NappletBrowserActivity`, `NappletHostService`,
|
|
`NappletBrowserService`) live in the `:nappletHost` module and are declared with
|
|
`android:process=":napplet"`. The broker side (`napplet/NappletBrokerService.kt`,
|
|
`NappletLaunchRegistry`, consent) stays in `amethyst/`.
|
|
|
|
## Manifest and intents
|
|
|
|
`amethyst/src/main/AndroidManifest.xml`; each flavor merges its own `AndroidManifest.xml`.
|
|
|
|
- `MainActivity` takes `MAIN`, `VIEW` for `nostr:`, `amethyst:`, `nostrconnect:`, the
|
|
wallet-connect schemes and several `https` hosts, and `SEND`.
|
|
- **Share targets are `<activity-alias>`es of MainActivity** (`.ui.ShareAsDMAlias`,
|
|
`ShareAsPictureAlias`, `ShareAsShortVideoAlias`, `ShareAsVideoAlias`, `ShareAsHighlightAlias`,
|
|
`ScanQrCodeAlias`). Which one the user picked is visible only in the launching intent's
|
|
component class name, which `ShareIntentRouting.targetOf` in
|
|
`commonsUI/.../commons/ui/navigation/ShareIntentRouting.kt` maps to a `ShareTarget`. Renaming an
|
|
alias without updating its constant there silently routes the share to New Post. Adding a target
|
|
means an alias, a constant, and a `ShareTarget` entry.
|
|
- `NavigateIfIntentRequested` (`ui/navigation/AppNavigation.kt`) turns `VIEW` / `SEND` /
|
|
`SEND_MULTIPLE` / NFC intents into `nav.newStack(...)` calls.
|
|
- Foreground services (`CallForegroundService`, `NestForegroundService`, `PlaybackService`,
|
|
`NotificationRelayService`, ...) read their notification strings synchronously, which is the one
|
|
case where a string may stay in Android `res/`. See `AGENTS.md` "Strings" and
|
|
`amethyst/src/main/res/AGENTS.md`.
|
|
|
|
## Intent / Context access from shared UI
|
|
|
|
Shared composables never touch `Context` directly. Use the existing shape: a small interface plus
|
|
an `expect fun rememberX()` in `commonsUI` `commonMain`, with the Android actual in
|
|
`commonsUI/src/androidMain`. Examples: `TextSharer` / `rememberTextSharer()` (the actual builds the
|
|
`ACTION_SEND` chooser from `LocalContext`), `FileSharer`, `PlatformBackHandler`,
|
|
`HideFromScreenshots`, `rememberCoarseLocationPermission(): PlatformPermission` (the actual wraps
|
|
Accompanist's `rememberPermissionState`). When the behavior needs app-only state (an Activity
|
|
class, a service, `Amethyst.instance`), put a member on `AppPlatform` / `AppServices` and implement
|
|
it in `AndroidAppPlatform` / `AndroidAppServices`, as `openNestRoom` does with `NestActivity.launch`.
|
|
|
|
## Flavors: play vs fdroid
|
|
|
|
`amethyst/build.gradle.kts` declares `flavorDimensions += "channel"` with `play` (default) and
|
|
`fdroid`. They differ in `BuildConfig.IS_CASTING_AVAILABLE` and in their dependencies:
|
|
|
|
- `play`: Firebase messaging, ML Kit (language id, translate, GenAI proofreading / rewriting /
|
|
prompt / image description), Play Services Cast, androidx AppFunctions.
|
|
- `fdroid`: UnifiedPush and no Google services.
|
|
|
|
Classes that `main` calls exist in **both** `src/play/java/...` and `src/fdroid/java/...` with the
|
|
same name and signature (`PushNotificationUtils`, `SelectNotificationProvider`,
|
|
`LanguageTranslatorService`, `WritingAssistantFactory`, `ChromecastCaster`, `TermsGate`, ...). Add
|
|
or change one side and the other flavor stops compiling. Build both when you touch them. Play-only
|
|
code (`appfunctions/AmethystAppFunctions.kt`, `AmethystCastOptionsProvider`) needs no twin as long
|
|
as `main` never references it. Build types: `debug` (`.debug` suffix), `release`, and `benchmark`
|
|
(release config, debug-signed, profileable).
|
|
|
|
Gradle details beyond this (catalog, versions, KMP source sets) belong to `kotlin-multiplatform`.
|
|
|
|
## Images (Coil)
|
|
|
|
`service/images/ImageLoaderSetup.kt` installs the singleton Coil loader. It is called from
|
|
`AppModules.setImageLoader()` with a `callFactory` built from
|
|
`roleBasedHttpClientBuilder.okHttpClientForImage`, so Tor routing and the "image" traffic tag
|
|
apply. Change the client there, not inside the loader setup.
|
|
`BlossomFetcher` and `ProfilePictureFetcher` are Android-side. The shared fetchers
|
|
(`Base64Fetcher`, `BlurHashFetcher`, `ThumbHashFetcher`) are in
|
|
`commonsUI/.../commons/service/image/`, and the desktop counterpart is
|
|
`desktopApp/.../desktop/service/images/DesktopImageLoaderSetup.kt`. Never build an `ImageLoader`
|
|
inside a composable.
|
|
|
|
## R8 and release checks
|
|
|
|
The rules are `amethyst/proguard-rules.pro` plus `quartz/consumer-rules.pro`. The policy is to keep
|
|
only what is reached by name. Read `references/proguard-rules.md` before adding a keep.
|
|
|
|
R8 cannot see reflection, so a broken keep gives a green build and a crash on users' devices.
|
|
Before tagging a release, run the reflection-contract check on a minified build. A
|
|
`playBenchmark` build is enough, and `-PdisableAbiSplits=true` keeps it quick:
|
|
|
|
```bash
|
|
./gradlew :amethyst:assemblePlayBenchmark -PdisableAbiSplits=true
|
|
python3 tools/r8-verify/verify_reflection_contract.py amethyst/build/outputs/mapping/playBenchmark/
|
|
```
|
|
|
|
The release workflow (`.github/workflows/create-release.yml`) runs the same script on
|
|
`playRelease` and `fdroidRelease`. Adding reflection means adding a keep rule **and** a line in
|
|
`tools/r8-verify/reflection-contract.txt` (prefix it with `@play` / `@fdroid` for flavor-only
|
|
classes).
|
|
|
|
## Testing
|
|
|
|
Unit tests are in `amethyst/src/test` (plus `testPlay`), instrumented tests in
|
|
`amethyst/src/androidTest` (plus `androidTestPlay`). Before calling a UI change done, run it on a
|
|
device or emulator: `./gradlew :amethyst:installDebug`.
|