Files
amethyst/.claude/skills/android-expert/SKILL.md
Claude 5d83b163eb refactor: Marmot leave, permissions, timeline and edit rules from marmot-kmp
- Leaving uses MarmotManager.leave in the app and amy: the heir is chosen
  among the members (a listed admin who already left no longer counts), the
  last member of a group can leave (it used to throw), and amy no longer
  publishes the step-down commit a second time.
- The member menu and revokeMarmotGroupAdmin use MarmotGroupPermissions, so
  demotion is checked against the members rather than the list's length.
- The feed asks MarmotAppEvent.isTimelineRow and the edit overlay asks
  MarmotMessageEdit instead of keeping their own copies of those rules.
- The media policy is built with EncryptedMediaPolicyV2.forBlossom, so amy's
  set-policy accepts a base URL without a trailing slash too.
- Group creation moves from CreateGroupScreen into
  BaseAccountViewModel.createMarmotGroup, and drops the profile commit that
  repeated the name already set at epoch 0.
- Removed: the unused commons viewmodels.MarmotGroupFeedViewModel and the
  MIP-04 send path nothing reached.
- cordn: the exposure card and amy stop calling the deprecated
  differsFromMarmot; CordnRuntimeTest moves to commons/src/jvmTest.
- AGENTS.md keeps a short version of the two-process rules; the detail moves
  to android-expert's references (31.6 KB of Codex's 32 KB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSSNWvwXdNnNwYnLoEvNcq
2026-10-25 01:26:36 +00:00

7.4 KiB


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 references/two-processes.md 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:

./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.