Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
eaf6044
chore: add project setup — CLAUDE.md, skills, plan, brainstorm, fix m…
nrobi144 Apr 14, 2026
3470fe1
feat(contacts): Phase 1 — contact creation screen with name/phone/ema…
nrobi144 Apr 14, 2026
3f81694
feat(contacts): Phase 2 — all 13 field types with full parity
nrobi144 Apr 14, 2026
97a8e69
feat(contacts): Phase 3 — photo support with Coil, gallery, camera
nrobi144 Apr 14, 2026
93cf1f3
feat(contacts): Phase 4 — M3 Expressive polish, edge cases, predictiv…
nrobi144 Apr 14, 2026
b74b790
test(contacts): Phase 5 — test hardening, 220 tests total
nrobi144 Apr 14, 2026
5e89ae0
fix(contacts): address P1/P2/P3 review findings
nrobi144 Apr 14, 2026
2a14ba1
refactor(contacts): address PR feedback — idioms, previews, DRY, acce…
nrobi144 Apr 14, 2026
1694231
refactor(contacts): eliminate delegate, add file_paths comment, type …
nrobi144 Apr 14, 2026
2c7a09b
style(contacts): Kotlin idioms — expression bodies, top-level constants
nrobi144 Apr 14, 2026
46ca522
test(contacts): comprehensive coverage — components, integration, E2E…
nrobi144 Apr 15, 2026
34ff99e
fix(tests): address review — remove duplicates, fix patterns, slim Te…
nrobi144 Apr 15, 2026
2d9ff5d
refactor(contacts): M3 UI redesign — section headers, FieldRow, prope…
nrobi144 Apr 15, 2026
7327b04
chore: trust javadoc/sources JARs in verification-metadata
nrobi144 Apr 15, 2026
c720ce8
fix(contacts): NPE in FieldTypeSelector dropdown — use indexed loop
nrobi144 Apr 15, 2026
bbceee9
fix(contacts): crash in FieldTypeSelector — label() was @Composable i…
nrobi144 Apr 15, 2026
63dadc3
feat(contacts): M3 Expressive Phase 1 — theme, buttons, visual cleanup
nrobi144 Apr 15, 2026
070a090
feat(contacts): M3 Expressive Phase 2 — remove buttons + photo bottom…
nrobi144 Apr 15, 2026
1050b65
feat(contacts): M3 Expressive Phase 3 — type-in-label with trailing d…
nrobi144 Apr 15, 2026
7d2aebf
feat(contacts): M3 Expressive Phase 4 — chip grid "Add more info"
nrobi144 Apr 15, 2026
a46fc6c
feat(contacts): M3 Expressive Phases 5-7 — footer, IME, polish
nrobi144 Apr 15, 2026
33c968c
fix(contacts): NPE in type label — compute outside Popup composition
nrobi144 Apr 15, 2026
394cf0d
fix(contacts): NPE in type label extensions — make receivers nullable
nrobi144 Apr 15, 2026
7ee7e6b
fix(contacts): padding/margin polish — 32dp column margin, larger typ…
nrobi144 Apr 15, 2026
b2e260e
feat(contacts): UI polish + account selector bottom sheet
nrobi144 Apr 15, 2026
903d5e7
fix(contacts): button reflow animation + remove save pulse
nrobi144 Apr 15, 2026
79eed53
fix(contacts): fix broken instrumented tests + remove AccountChipTest
nrobi144 Apr 15, 2026
6fdef84
test(contacts): remove stale tests for removed UI elements
nrobi144 Apr 15, 2026
12d57bc
refactor(contacts): fix detekt violations — extract helpers, split me…
nrobi144 Apr 16, 2026
ac7a18a
chore: restore .idea config files tracked on main
nrobi144 Apr 16, 2026
40e8e64
chore: revert .gitignore changes
nrobi144 Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -388,11 +388,12 @@

</activity-alias>

<!-- Edit or create a contact with only the most important fields displayed initially. -->
<!-- Compose-based contact creation screen (replaces ACTION_INSERT from ContactEditorActivity) -->
<activity
android:name=".activities.ContactEditorActivity"
android:name=".ui.contactcreation.ContactCreationActivity"
android:exported="true"
android:theme="@style/EditorActivityTheme">
android:launchMode="singleTop"
android:theme="@android:style/Theme.Material.Light.NoActionBar">

<intent-filter>
<action android:name="android.intent.action.INSERT"/>
Expand All @@ -404,6 +405,13 @@
</intent-filter>
</activity>

<!-- Edit a contact with only the most important fields displayed initially. -->
<activity
android:name=".activities.ContactEditorActivity"
android:exported="true"
android:theme="@style/EditorActivityTheme">
</activity>

<!-- Keep support for apps that expect the Compact editor -->
<activity-alias
android:name="com.android.contacts.activities.CompactContactEditorActivity"
Expand Down
6 changes: 6 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ plugins {
alias(libs.plugins.detekt)
alias(libs.plugins.hilt)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.ksp)
}

Expand Down Expand Up @@ -86,7 +87,10 @@ dependencies {
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.tooling.preview)

implementation(libs.coil.compose)

implementation(libs.hilt.android)
implementation(libs.androidx.hilt.navigation.compose)
ksp(libs.hilt.compiler)

implementation(libs.guava)
Expand All @@ -104,13 +108,15 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)

testImplementation(libs.junit4)
testImplementation(kotlin("test"))
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.mockk)
testImplementation(libs.mockk.agent)
testImplementation(libs.mockk.android)
testImplementation(libs.robolectric)
testImplementation(libs.turbine)

androidTestImplementation(kotlin("test"))
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
androidTestImplementation(libs.androidx.test.espresso.core)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package com.android.contacts.ui.contactcreation

import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import com.android.contacts.ui.contactcreation.model.ContactCreationAction
import com.android.contacts.ui.contactcreation.model.ContactCreationUiState
import com.android.contacts.ui.core.AppTheme
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class ContactCreationEditorScreenTest {

@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()

private val capturedActions = mutableListOf<ContactCreationAction>()

@Before
fun setup() {
capturedActions.clear()
}

@Test
fun initialState_showsSaveTextButton() {
setContent()
composeTestRule.onNodeWithTag(TestTags.SAVE_TEXT_BUTTON).assertIsDisplayed()
}

@Test
fun initialState_showsCloseButton() {
setContent()
composeTestRule.onNodeWithTag(TestTags.CLOSE_BUTTON).assertIsDisplayed()
}

@Test
fun initialState_showsNameField() {
setContent()
composeTestRule.onNodeWithTag(TestTags.NAME_FIRST).assertIsDisplayed()
}

@Test
fun initialState_showsPhoneField() {
setContent()
composeTestRule.onNodeWithTag(TestTags.phoneField(0)).assertIsDisplayed()
}

@Test
fun initialState_showsEmailField() {
setContent()
composeTestRule.onNodeWithTag(TestTags.emailField(0)).assertIsDisplayed()
}

@Test
fun tapSave_dispatchesSaveAction() {
setContent()
composeTestRule.onNodeWithTag(TestTags.SAVE_TEXT_BUTTON).performClick()
assertEquals(ContactCreationAction.Save, capturedActions.last())
}

@Test
fun tapClose_dispatchesNavigateBackAction() {
setContent()
composeTestRule.onNodeWithTag(TestTags.CLOSE_BUTTON).performClick()
assertEquals(ContactCreationAction.NavigateBack, capturedActions.last())
}

@Test
fun savingState_disablesSaveButton() {
setContent(state = ContactCreationUiState(isSaving = true))
composeTestRule.onNodeWithTag(TestTags.SAVE_TEXT_BUTTON).assertIsNotEnabled()
}

@Test
fun notSavingState_enablesSaveButton() {
setContent(state = ContactCreationUiState(isSaving = false))
composeTestRule.onNodeWithTag(TestTags.SAVE_TEXT_BUTTON).assertIsEnabled()
}

// --- Discard dialog ---

@Test
fun discardDialog_rendersWhenShowDiscardDialogTrue() {
setContent(state = ContactCreationUiState(showDiscardDialog = true))
composeTestRule.onNodeWithTag(TestTags.DISCARD_DIALOG).assertIsDisplayed()
}

@Test
fun discardDialog_notRenderedByDefault() {
setContent()
composeTestRule.onNodeWithTag(TestTags.DISCARD_DIALOG).assertDoesNotExist()
}

@Test
fun discardDialog_confirmDispatchesConfirmDiscard() {
setContent(state = ContactCreationUiState(showDiscardDialog = true))
composeTestRule.onNodeWithTag(TestTags.DISCARD_DIALOG_CONFIRM).performClick()
assertEquals(ContactCreationAction.ConfirmDiscard, capturedActions.last())
}

@Test
fun discardDialog_dismissDispatchesDismissDiscardDialog() {
setContent(state = ContactCreationUiState(showDiscardDialog = true))
composeTestRule.onNodeWithTag(TestTags.DISCARD_DIALOG_DISMISS).performClick()
assertEquals(ContactCreationAction.DismissDiscardDialog, capturedActions.last())
}

// --- Add more info chip grid ---

@Test
fun addMoreInfoSection_showsWhenChipsAvailable() {
setContent()
composeTestRule.onNodeWithTag(TestTags.ADD_MORE_INFO_SECTION).assertIsDisplayed()
}

@Test
fun addMoreInfoSection_addressChipAddsAddress() {
setContent()
composeTestRule.onNodeWithTag(TestTags.addMoreInfoChip("address")).performClick()
assertEquals(ContactCreationAction.AddAddress, capturedActions.last())
}

@Test
fun addMoreInfoSection_orgChipShowsOrganization() {
setContent()
composeTestRule.onNodeWithTag(TestTags.addMoreInfoChip("organization")).performClick()
assertEquals(ContactCreationAction.ShowOrganization, capturedActions.last())
}

@Test
fun addMoreInfoSection_noteChipShowsNote() {
setContent()
composeTestRule.onNodeWithTag(TestTags.addMoreInfoChip("note")).performClick()
assertEquals(ContactCreationAction.ShowNote, capturedActions.last())
}

private fun setContent(state: ContactCreationUiState = ContactCreationUiState()) {
composeTestRule.setContent {
AppTheme {
ContactCreationEditorScreen(
uiState = state,
accounts = emptyList(),
onAction = { capturedActions.add(it) },
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package com.android.contacts.ui.contactcreation

import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import com.android.contacts.ui.contactcreation.model.ContactCreationAction
import com.android.contacts.ui.contactcreation.model.ContactCreationUiState
import com.android.contacts.ui.contactcreation.model.NameState
import com.android.contacts.ui.contactcreation.model.PhoneFieldState
import com.android.contacts.ui.core.AppTheme
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test

/**
* End-to-end flow tests exercising the full [ContactCreationEditorScreen].
*
* Uses [createAndroidComposeRule] with [ComponentActivity] + the screen composable
* directly (avoids Hilt wiring for the Activity). Actions are captured via lambda
* to verify the full UI -> action pipeline.
*/
class ContactCreationFlowTest {

@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()

private val capturedActions = mutableListOf<ContactCreationAction>()

@Before
fun setup() {
capturedActions.clear()
}

// --- 1. Basic save flow ---

@Test
fun createBasicContact_endToEnd() {
setContent()
// Type first name
composeTestRule.onNodeWithTag(TestTags.NAME_FIRST).performTextInput("John")
assertTrue(
capturedActions.any { it is ContactCreationAction.UpdateFirstName },
)

// Type phone
composeTestRule.onNodeWithTag(TestTags.phoneField(0)).performTextInput("555-0100")
assertTrue(
capturedActions.any { it is ContactCreationAction.UpdatePhone },
)

// Tap save
composeTestRule.onNodeWithTag(TestTags.SAVE_TEXT_BUTTON).performClick()
assertEquals(ContactCreationAction.Save, capturedActions.last())
}

// --- 2. All fields save flow ---

@Test
fun createWithAllFields_endToEnd() {
val state = TestFactory.fullState()
setContent(state = state)

// Verify core sections are rendered
composeTestRule.onNodeWithTag(TestTags.NAME_FIRST).assertIsDisplayed()
composeTestRule.onNodeWithTag(TestTags.phoneField(0)).assertIsDisplayed()
composeTestRule.onNodeWithTag(TestTags.emailField(0)).assertIsDisplayed()

// Tap save
composeTestRule.onNodeWithTag(TestTags.SAVE_TEXT_BUTTON).performClick()
assertEquals(ContactCreationAction.Save, capturedActions.last())
}

// --- 3. Cancel with discard flow ---

@Test
fun cancelWithDiscard_endToEnd() {
setContent(state = ContactCreationUiState(showDiscardDialog = true))

// Discard dialog should be visible
composeTestRule.onNodeWithTag(TestTags.DISCARD_DIALOG).assertIsDisplayed()

// Tap discard (confirm button)
composeTestRule.onNodeWithTag(TestTags.DISCARD_DIALOG_CONFIRM).performClick()
assertEquals(ContactCreationAction.ConfirmDiscard, capturedActions.last())
}

// --- 4. Intent extras pre-fill ---

@Test
fun intentExtras_preFill_endToEnd() {
// Simulate pre-filled state (as Activity.applyIntentExtras would produce)
val preFilled = ContactCreationUiState(
nameState = NameState(first = "Jane"),
phoneNumbers = listOf(PhoneFieldState(id = "p1", number = "555-1234")),
)
setContent(state = preFilled)

// Fields should be displayed with pre-filled data
composeTestRule.onNodeWithTag(TestTags.NAME_FIRST).assertIsDisplayed()
composeTestRule.onNodeWithTag(TestTags.phoneField(0)).assertIsDisplayed()

// Save
composeTestRule.onNodeWithTag(TestTags.SAVE_TEXT_BUTTON).performClick()
assertEquals(ContactCreationAction.Save, capturedActions.last())
}

// --- 5. Zero-account local contact ---

@Test
fun zeroAccount_localContact_endToEnd() {
val state = ContactCreationUiState(
selectedAccount = null,
accountName = null,
)
setContent(state = state)

// Type a name and save
composeTestRule.onNodeWithTag(TestTags.NAME_FIRST).performTextInput("Local")
composeTestRule.onNodeWithTag(TestTags.SAVE_TEXT_BUTTON).performClick()
assertEquals(ContactCreationAction.Save, capturedActions.last())
}

// --- Helper ---

private fun setContent(state: ContactCreationUiState = ContactCreationUiState()) {
composeTestRule.setContent {
AppTheme {
ContactCreationEditorScreen(
uiState = state,
accounts = emptyList(),
onAction = { capturedActions.add(it) },
)
}
}
}
}
Loading