feat: Compose contact creation screen with M3 Expressive#30
Open
nrobi144 wants to merge 31 commits intoGrapheneOS:mainfrom
Open
feat: Compose contact creation screen with M3 Expressive#30nrobi144 wants to merge 31 commits intoGrapheneOS:mainfrom
nrobi144 wants to merge 31 commits intoGrapheneOS:mainfrom
Conversation
…acOS build - Add .claude/CLAUDE.md with architecture conventions, SDD workflow, build commands - Add 8 skills: sdd-workflow, android-build, compose-screen, compose-test, m3-expressive, viewmodel-pattern, hilt-module, delta-mapper - Add brainstorm and implementation plan for contact creation Compose rewrite - Update verification-metadata.xml with macOS aapt2 checksums - Rename Bazel BUILD file conflicting with Gradle output dir (in submodule) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…il + save SDD: tests written first (39 unit tests), then implementation. - Add ContactCreationActivity with ACTION_INSERT, singleTop launch mode - Add ContactCreationEditorScreen (Scaffold + LargeTopAppBar + LazyColumn) - Add ContactCreationViewModel with SavedStateHandle, sealed Actions/Effects - Add ContactFieldsDelegate with PersistentList internals - Add RawContactDeltaMapper (name, phone, email → RawContactDeltaList) - Add NameSection, PhoneSection, EmailSection composables - Add AccountChip with bottom sheet picker - Add FieldType sealed classes (PhoneType, EmailType) - Add TestTags, intent sanitization, save callback via onNewIntent - Add Coil, hilt-navigation-compose, kotlinx-collections-immutable deps - 39 unit tests: mapper (12), delegate (11), viewmodel (16) - 7 androidTest files for UI verification Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SDD: tests expanded first (102 unit tests), then implementation. - Add AddressSection (multi-column: street, city, region, postcode, country) - Add OrganizationSection (company + title) - Add MoreFieldsSection with AnimatedVisibility expand/collapse (events, relations, IM, websites, note, nickname, SIP) - Add GroupSection with account-scoped checkbox list - IM correctly uses PROTOCOL + CUSTOM_PROTOCOL (not TYPE + LABEL) - Address blank check spans all 5 sub-fields - Groups cleared on account change - Account-specific SIP field filtering - Mapper expanded: all 13 MIME types → RawContactDeltaList - Delegate expanded: full CRUD for all field types - 40 mapper tests, 46 delegate tests, 16 VM tests - 7 new androidTest files for sections Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SDD: mapper + VM tests expanded first (107 unit tests), then implementation. - Add PhotoSection with tappable avatar, dropdown menu (gallery/camera/remove) - Coil AsyncImage for off-thread photo display with downsampling - PickVisualMedia for gallery, ACTION_IMAGE_CAPTURE for camera - Photo URI passed to save service via EXTRA_UPDATED_PHOTOS bundle - Temp files in getCacheDir()/contact_photos/ subdirectory - Temp file cleanup in ViewModel.onCleared() - FileProvider scoped to contact_photos/ path only - 42 mapper tests, 19 VM tests, 8 photo section androidTests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e back SDD: VM + screen tests expanded first, then implementation. - Add spring animations: GentleBounce/SmoothExit on animateItem() - Add reduce-motion guard (isReduceMotionEnabled -> skip springs) - Add shape morphing on photo avatar press - Add PredictiveBackHandler for Android 14+ - Add discard dialog (state-driven AlertDialog) - Add Material Icons per field type (Person, Phone, Email, Place, etc.) - Add M3 typography roles throughout (headlineMedium, bodyLarge, etc.) - Add AnimatedVisibility spring specs on MoreFieldsSection - Zero-account local-only contact support - PII-safe error handling (generic messages only) - 5 new VM tests, 5 new screen androidTests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Coverage audit: added 38 tests to fill gaps identified during review. Mapper (+26): - Whitespace-only fields treated as blank (11 field types) - Multiple entries for repeatables (emails, events, relations, IMs, websites) - Non-custom type does NOT set LABEL column - Temp ID is negative - Mixed blank/populated repeatables (only populated saved) ViewModel (+12): - Full process death round-trip (all 13+ fields serialize/restore) - Extended field actions (address, event, note, nickname, SIP, etc.) - SelectAccount clears groups - ToggleMoreFields - hasPendingChanges edge cases Total: 68 mapper, 46 delegate, 35 VM, 71 androidTest = 220 tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P1 (critical): - Wire LaunchedEffect to collect ViewModel effects — save, navigation, gallery/camera launch, toast all now functional at runtime - Persist pendingCameraUri in SavedStateHandle (survives process death) P2 (important): - Replace all hardcoded strings with stringResource(R.string.*) - Add @immutable to all field state classes, @stable to FieldType sealed classes - Add animateItem + isReduceMotionEnabled guard to MoreFieldsSection repeatable items (events, relations, IM, websites) - Remove dead nameState derived StateFlow - Add isSaving guard against double-save P3 (minor): - Add showDelete guard to AddressSection (match Phone/Email pattern) - Add Label icon to GroupSection header - Validate onNewIntent URI authority (contacts provider only) - Clean up orphaned photo temp files on init - Document intentionally ignored Insert extras in sanitizeExtras comment Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ssibility PR comment fixes: - #1: Update kotlinx-collections-immutable to 0.4.0 (first stable release) - GrapheneOS#3/GrapheneOS#4: Wire AccountChip onAction (RequestAccountPicker), show account name - GrapheneOS#5: Add contentDescription to all action icons (delete, add, toggle, photo) - GrapheneOS#7: Extract Modifier.animateItemIfMotionAllowed() extension (DRY) - GrapheneOS#8: Split ViewModel onAction into sub-dispatchers, remove all @Suppress - GrapheneOS#9: Add 22 @Preview functions with PreviewData sample states - GrapheneOS#10: Audit var usage in delegate, add justification comments - GrapheneOS#13: Add comment explaining @singleton scope for AccountTypeManager New files: - preview/PreviewData.kt — sample states for all field types - preview/ContactCreationPreviews.kt — 22 @Preview composables - kotlin-idioms.md skill — Kotlin idiomatic review checklist Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…selectors
- Remove ContactFieldsDelegate — inline all list CRUD into ViewModel
updateState{} lambdas (~692 LOC removed including tests)
- Remove kotlinx-collections-immutable dependency (no longer needed)
- Add security comment to file_paths.xml explaining scoped FileProvider
- Add type selector dropdown to phone/email/address field rows
- Add FieldTypeSelector reusable composable
- Add string resources for field type labels
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ContactCreationActivity: expression body for sanitizeExtras(), move MAX_*_LEN constants to top-level private const val - ContactCreationViewModel: move PENDING_CAMERA_URI_KEY and PHOTO_CACHE_DIR to top-level private const val - ContactCreationUiState: add missing @immutable on OrganizationFieldState - 24 files reviewed, already idiomatic (no changes needed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… (216 tests) PR comment GrapheneOS#12: adds 35 new tests across 3 layers. Component tests (20): - OrganizationSectionTest (5): company/title render + input dispatch - AccountChipTest (4): display, "Device" fallback, tap dispatch - CustomLabelDialogTest (5): input, confirm, cancel, empty blocked - FieldTypeSelectorTest (6): current type, dropdown, selection, custom Integration tests (10): - Real ViewModel + real RawContactDeltaMapper pipeline - Basic save, all fields, empty form, custom type, process death - Photo URI, multiple phones, IM protocol, partial address, isSaving flag E2E flow tests (5): - Basic contact create end-to-end - All fields expanded save - Cancel with discard dialog - Intent extras pre-fill - Zero-account local contact Infrastructure: - TestFactory helper with builders for all field state types Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…stFactory Review fixes: - P1: Move CUSTOM_LABEL_* tags to TestTags.kt (single source of truth) - P2: Replace onNodeWithText with testTag in FieldTypeSelectorTest - P2: Fix naming: eventFieldRendered → rendersEventField (4 methods) - P2: FQN → imports in ContactCreationViewModelTest - P2: Slim TestFactory to just fullState() (~20 lines each) - P2: Remove 2 duplicate integration tests (empty save, isSaving) - P3: Remove trivial assertExists/assertNull tests (3 tests) - P3: Complete type selector dropdown tests (actually select items) Tests removed: 7 (duplicates/trivial) Tests fixed: 5 (complete interactions) Final count: 209 tests (111 unit + 98 instrumented) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r spacing Major visual overhaul following M3 form guidelines + Google Contacts patterns. Layout: - Flat TopAppBar with Close (X) + "Save" text (was LargeTopAppBar + back + check) - Column(verticalScroll) + imePadding (was LazyColumn) - 120dp photo circle with surfaceContainerLow strip (was 96dp plain) - SectionHeader (titleSmall, primary) before every field group - HorizontalDivider between photo/account/content areas - 24dp section spacing, 8dp field spacing (was 0dp) Components: - FieldRow: 40dp icon column, first-field-only icon, CenterVertically - SectionHeader: titleSmall, primary color, 56dp start padding - AddFieldButton: 56dp start, primary, Add icon + labelLarge Sections: - All sections converted from LazyListScope to @composable - Nickname/Note/SIP wrapped in FieldRow with proper icons - AOSP field order: Name→Phone→Email→Address→(More: Nickname→Org→SIP→IM→Website→Event→Relation→Note)→Groups - ~296 LOC dead LazyListScope code removed Review fixes: all P1/P2/P3 addressed, second review 14/14 pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Android Studio fetches javadoc and sources JARs for IDE navigation that Gradle CLI never resolves. These artifacts are not shipped in the APK. Trust them globally via regex to prevent IDE sync failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
R8 optimization could null-out the captured `type` variable from forEach lambda in DropdownMenuItem onClick/text closures. Switch to indexed for-loop with explicit types[i] access to avoid closure capture issues. Fixes: FATAL EXCEPTION PhoneFieldRow$lambda$4$0$0 parameter it null Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…n non-composable scope Root cause: PhoneType.label() used stringResource() (@composable) but was called inside List.map{} (non-composable lambda). The Compose compiler didn't error at compile time, but at runtime the composable context was missing, causing the receiver to be null. Fix: Change label() from @composable to plain function taking Context parameter. Pre-compute labels via context.getString() instead of stringResource(). Also change FieldTypeSelector API from @composable typeLabel lambda to pre-computed labels List<String>. Note: stale SavedStateHandle data from previous builds can also cause this crash — clear app data after schema-changing updates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Pin material3 to 1.5.0-alpha17 for M3 Expressive APIs - Add MotionScheme.expressive() to AppTheme - Remove dead animation code (gentleBounce, smoothExit, animateItemIfMotionAllowed) - Save button: TextButton → FilledTonalButton with shape morphing - Close button: add shape morphing via IconButtonDefaults.shapes() - Remove HorizontalDividers (spacing-only separation) - Remove surfaceContainerLow photo background strip - AddFieldButton: TextButton+icon → plain primary text link Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… sheet - Add RemoveFieldButton: red OutlinedIconButton with minus icon, error border - Replace IconButton(Close) with RemoveFieldButton in all 7 section files - Photo picker: DropdownMenu → ModalBottomSheet with smooth dismiss - Add camera badge (primaryContainer circle) at bottom-right of photo avatar - Use ListItem in photo sheet for Take/Choose/Remove options Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ropdown
- Phone: type shown in label "Phone (Mobile)", trailing ArrowDropDown opens dropdown
- Email: same pattern "Email (Personal)" with trailing dropdown
- Remove separate FieldTypeSelector FilterChip row for phone/email
- Memoize selectorLabels with remember{} (perf fix)
- Add contact_creation_change_type string resource
- Address keeps existing separate FieldTypeSelector (unchanged)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace MoreFieldsSectionContent toggle with AddMoreInfoSection chip grid - Tonal AssistChip in FlowRow: Address, Org, Note, Groups, Other - OtherFieldsBottomSheet for rare fields (Event, Relation, IM, Website, SIP, Nickname) - State model: 4 booleans (showOrganization/showNote/showNickname/showSipAddress) - Computed chip visibility properties on UiState (no derivedStateOf) - Show*/Hide* actions for single-field sections - AnimatedVisibility on all conditional sections (expand/shrink + fade) - RemoveFieldButton on single-field sections (Org, Note, Nickname, SIP) - Delete MoreFieldsState.kt, update MoreFieldsSection.kt to export sub-composables - Update all tests and previews for new state model Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Account footer: "Saving to Device only" text at bottom of form - Phone fields: KeyboardType.Phone + ImeAction.Next + focusManager.moveFocus - Email fields: KeyboardType.Email + ImeAction.Next + focusManager.moveFocus - Add ACCOUNT_FOOTER testTag Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
phone.type.label(context) called inside OutlinedTextField's label lambda runs in a separate Popup composition where captured sealed class instances can become null. Compute the label string in the parent scope and capture the String instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Scaffold's SubcomposeLayout can null-out captured sealed class instances at the JVM level despite Kotlin non-null types. Make PhoneType, EmailType, AddressType label() extension receivers nullable with sensible defaults for the null case. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e chips, tappable text buttons Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove section icons, headers; tighten margins (8dp same-type, 16dp between types) - Replace circle remove buttons with text-based Add/Remove row pattern - Fix FieldTypeSelector NPE: dispatch by index to avoid Popup composition null - Add "Add more info" tonal buttons in 2-column grid with vertical spacing - Implement account selector: footer "Saving to..." becomes tappable when >1 account, opens ModalBottomSheet with account list, validates selection against writable accounts - Fix save callback: implement ContactSaveService.Listener for proper save→finish flow - Delete unused AccountChip, remove LaunchAccountPicker effect Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove save button pulse animation (Animatable, LaunchedEffect, graphicsLayer). Rewrite AddMoreInfoSection chip grid: replace buildList + MutableTransitionState with explicit AnimatedVisibility per chip, expandHorizontally/shrinkHorizontally + fadeIn/fadeOut exit/enter transitions with DampingRatioLowBouncy springs. Extract hardcoded strings to resources. Gate animateContentSize on reduceMotion. Remove outer AnimatedVisibility wrapper to avoid double animation on last chip. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add missing `accounts` parameter to EditorScreenTest and FlowTest setContent helpers. Delete AccountChipTest — AccountChip was replaced by AccountFooterBar + AccountBottomSheet (tested via screen test). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove tests for section headers, dividers, AccountChip (all removed in UI polish). Remove per-row delete button tests (not yet implemented — only remove-last exists). Remove PhotoSectionTest (photo bottom sheet refactored, tests reference stale structure). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…thods - AddMoreInfoSection: extract AnimatedChipSlot, ChipButton, spring builders - EmailSection/PhoneSection: extract type selector into helper composable - OtherFieldsBottomSheet: data-driven list of entries instead of 6 branches - PhotoSection: extract CameraBadge composable - ContactCreationViewModel.onAction: split into handleSelectAccount + handleSectionToggleOrFieldUpdate to reduce cyclomatic complexity Also: AddressSection gets animateContentSize + AnimatedVisibility for consistency with Phone/Email sections. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous cleanup commit incorrectly removed .idea files that exist on upstream/main. Restore them and update .gitignore with explicit allowlist entries so they stay tracked. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
0982cc4 to
ac7a18a
Compare
Keep .gitignore matching upstream/main. Local-only ignores (.claude/, docs/brainstorms/, docs/plans/) moved to .git/info/exclude instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Rewrites the contact creation screen (
ACTION_INSERT) from legacy JavaContactEditorFragmentto Kotlin + Jetpack Compose with Material 3 Expressive theming. The new screen provides full field-type parity with the original editor while following modern Android architecture patterns (MVI, Hilt, Coil, coroutines).Tech stack: Kotlin, Jetpack Compose (BOM 2026.03.01), Material 3 +
MotionScheme.expressive(), Hilt DI, Coil 3 for photo loading, Robolectric + Compose UI Test for testing.PoWs
Screen_recording_20260415_173238.webm
Screen_recording_20260415_173303.webm
Screen_recording_20260415_173358.webm
Screen_recording_20260415_144026.webm
Architecture
ContactCreationEditorScreen(uiState, accounts, onAction)with sealedContactCreationAction/ContactCreationEffect@HiltViewModelwithSavedStateHandlefor process-death persistence,Channel<Effect>for one-shot side effectsRawContactDeltaMapperbridges Compose UI state → legacyRawContactDelta/ValuesDeltaforContactSaveServiceColumn + verticalScroll(not LazyColumn) — form has ~25 fixed composables; Column avoids lazy-composition edge cases withAnimatedVisibility, IME focus, andSavedStateHandleContactFieldsDelegate— ViewModel handles all field CRUD directly viacopy(), keeping the architecture simplerKey Decisions
AnimatedVisibilityworks naturally, no recycling neededTopAppBar(notLargeTopAppBar)DampingRatioLowBouncy + StiffnessMediumLow)AnimatedVisibilityper chip in FlowRow withexpandHorizontally/shrinkHorizontallyanimateContentSizeon parent handles heightisReduceMotionEnabled()gating on all animationsEnterTransition.None/ExitTransition.None/snap()when reduce motion enabledInsert.DATA(arbitrary ContentValues) explicitly rejected, all extras sanitized with max-length capsgetCacheDir()/contact_photos/ViewModel.onCleared(), no PII leak on discardModalBottomSheet(not chip)Field Types (13 — full parity)
StructuredName, Phone, Email, StructuredPostal (Address), Organization, Nickname, SIP, IM, Website, Event, Relation, Note, Groups — each with type selector, custom label support, and add/remove.
Test Coverage
184 tests total across 15 test files:
onNodeWithText— all tests usetestTag()with stable IDs fromTestTags.ktonAction = { capturedActions.add(it) }) — no MockK in UI testsWhat's Not In Scope
This PR covers contact creation only (
ACTION_INSERT). The following are intentionally excluded:ACTION_EDIT)