- 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
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: theApplication. BuildsAppModules(Amethyst.instance) in the main process only.ui/MainActivity.kt: the single main Activity (launchMode="singleInstance").onCreatecallsenableEdgeToEdge()and setsAmethystTheme { ... 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 (androidDestinationsinui/navigation/AppNavigation.kt) and runsNavigateIfIntentRequested.ui/platform/AndroidAppPlatform.kt(AppPlatform) andui/platform/AndroidAppServices.kt(AppServices, the interface is incommons/.../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, andHealthConnectRationaleActivity.
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.
MainActivitytakesMAIN,VIEWfornostr:,amethyst:,nostrconnect:, the wallet-connect schemes and severalhttpshosts, andSEND.- 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, whichShareIntentRouting.targetOfincommonsUI/.../commons/ui/navigation/ShareIntentRouting.ktmaps to aShareTarget. Renaming an alias without updating its constant there silently routes the share to New Post. Adding a target means an alias, a constant, and aShareTargetentry. NavigateIfIntentRequested(ui/navigation/AppNavigation.kt) turnsVIEW/SEND/SEND_MULTIPLE/ NFC intents intonav.newStack(...)calls.- Foreground services (
CallForegroundService,NestForegroundService,PlaybackService,NotificationRelayService, ...) read their notification strings synchronously, which is the one case where a string may stay in Androidres/. SeeAGENTS.md"Strings" andamethyst/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.