Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
ij_kotlin_packages_to_use_import_on_demand = unset
ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 1
ij_kotlin_line_break_after_multiline_when_entry = false
ktlint_code_style = android_studio
ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 1
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_filename = disabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_function-signature = disabled
ktlint_standard_trailing-comma-on-call-site = disabled
Expand Down
4 changes: 2 additions & 2 deletions AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -253,11 +253,11 @@

<!-- Used to set options -->
<activity
android:name=".preference.ContactsPreferenceActivity"
android:name=".ui.settings.SettingsActivity"
android:exported="false"
android:label="@string/activity_title_settings"
android:launchMode="singleTop"
android:theme="@style/ContactsPreferencesTheme"/>
android:theme="@style/Theme.Compose"/>

<activity
android:name=".activities.LicenseActivity"
Expand Down
19 changes: 19 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ java {
}
}

tasks.withType<Test>().configureEach {
javaLauncher.set(
javaToolchains.launcherFor {
languageVersion.set(JavaLanguageVersion.of(21))
},
)
}

detekt {
basePath.set(rootDir)
buildUponDefaultConfig = true
Expand Down Expand Up @@ -66,6 +74,13 @@ android {
res.directories.add("../res")
}

testOptions {
unitTests {
isIncludeAndroidResources = true
isReturnDefaultValues = true
}
}

lint {
abortOnError = false
disable += setOf("UnusedResources", "UnusedIds")
Expand All @@ -85,12 +100,14 @@ dependencies {
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.lifecycle.viewmodel.compose)

implementation(libs.hilt.android)
ksp(libs.hilt.compiler)

implementation(libs.guava)

implementation(libs.kotlinx.immutable)
implementation(libs.kotlinx.coroutines.android)

implementation(libs.material)
Expand All @@ -103,13 +120,15 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.test.manifest)
debugImplementation(libs.androidx.compose.ui.tooling)

testImplementation(platform(libs.androidx.compose.bom))
testImplementation(libs.junit4)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.mockk)
testImplementation(libs.mockk.agent)
testImplementation(libs.mockk.android)
testImplementation(libs.robolectric)
testImplementation(libs.turbine)
testImplementation(libs.androidx.compose.ui.test.junit4)

androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.android.contacts.data.accounts.repository

import com.android.contacts.model.AccountTypeManager
import com.android.contacts.model.account.AccountInfo
import com.android.contacts.model.account.AccountWithDataSet
import com.android.contacts.preference.ContactsPreferences
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
class AccountsRepositoryImplTest {

private val contactsPreferences = mockk<ContactsPreferences>()
private val accountTypeManager = mockk<AccountTypeManager>()

private val repository = AccountsRepositoryImpl(
contactsPreferences = contactsPreferences,
accountTypeManager = accountTypeManager,
ioDispatcher = UnconfinedTestDispatcher(),
)

@Test
fun getDefaultAccountLabel_whenDefaultAccountIsWritable_returnsItsNameLabel() = runTest {
givenDefaultAccount(DEFAULT_ACCOUNT)
givenWritableAccounts(
accountInfo(OTHER_ACCOUNT, label = "other@example.org"),
accountInfo(DEFAULT_ACCOUNT, label = "Device"),
)

assertEquals("Device", repository.getDefaultAccountLabel())
}

@Test
fun getDefaultAccountLabel_whenThereIsNoDefaultAccount_returnsNull() = runTest {
givenDefaultAccount(null)

assertNull(repository.getDefaultAccountLabel())
}

@Test
fun getDefaultAccountLabel_whenDefaultAccountIsNotWritable_returnsNull() = runTest {
givenDefaultAccount(DEFAULT_ACCOUNT)
givenWritableAccounts(accountInfo(OTHER_ACCOUNT, label = "other@example.org"))

assertNull(repository.getDefaultAccountLabel())
}

@Test
fun getDefaultAccountLabel_whenThereAreNoWritableAccounts_returnsNull() = runTest {
givenDefaultAccount(DEFAULT_ACCOUNT)
givenWritableAccounts()

assertNull(repository.getDefaultAccountLabel())
}

@Test
fun getDefaultAccountLabel_whenLoadingAccountsFails_returnsNull() = runTest {
givenDefaultAccount(DEFAULT_ACCOUNT)
every { accountTypeManager.filterAccountsAsync(any()) } returns
Futures.immediateFailedFuture(IllegalStateException("accounts unavailable"))

assertNull(repository.getDefaultAccountLabel())
}

@Test
fun getDefaultAccountLabel_whenLoadingAccountsIsInterrupted_returnsNull() = runTest {
val future = mockk<ListenableFuture<List<AccountInfo>>>()
every { future.get() } throws InterruptedException()
givenDefaultAccount(DEFAULT_ACCOUNT)
every { accountTypeManager.filterAccountsAsync(any()) } returns future

assertNull(repository.getDefaultAccountLabel())
}

private fun givenDefaultAccount(account: AccountWithDataSet?) {
every { contactsPreferences.defaultAccount } returns account
}

private fun givenWritableAccounts(vararg accounts: AccountInfo) {
every { accountTypeManager.filterAccountsAsync(any()) } returns
Futures.immediateFuture(accounts.toList())
}

private fun accountInfo(
account: AccountWithDataSet,
label: String,
): AccountInfo {
val accountInfo = mockk<AccountInfo>()
every { accountInfo.account } returns account
every { accountInfo.nameLabel } returns label
return accountInfo
}

private companion object {
val DEFAULT_ACCOUNT = AccountWithDataSet("default@example.org", "com.example", null)
val OTHER_ACCOUNT = AccountWithDataSet("other@example.org", "com.example", null)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.android.contacts.data.appinfo.repository

import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
class AppInfoRepositoryImplTest {

private val context = mockk<Context>()
private val packageManager = mockk<PackageManager>()

private val repository = AppInfoRepositoryImpl(
context = context,
packageManager = packageManager,
ioDispatcher = UnconfinedTestDispatcher(),
)

@Before
fun setUp() {
every { context.packageName } returns PACKAGE_NAME
}

@Test
fun getBuildVersion_returnsTheVersionOfTheInstalledPackage() = runTest {
val packageInfo = PackageInfo().apply { versionName = "1.7.40" }
givenPackageInfo { packageInfo }

assertEquals("1.7.40", repository.getBuildVersion())
}

@Test
fun getBuildVersion_whenPackageHasNoVersion_returnsNull() = runTest {
givenPackageInfo { PackageInfo() }

assertNull(repository.getBuildVersion())
}

@Test
fun getBuildVersion_whenPackageIsNotFound_returnsNull() = runTest {
givenPackageInfo { throw PackageManager.NameNotFoundException() }

assertNull(repository.getBuildVersion())
}

private fun givenPackageInfo(packageInfo: () -> PackageInfo) {
every {
packageManager.getPackageInfo(
PACKAGE_NAME,
any<PackageManager.PackageInfoFlags>(),
)
} answers { packageInfo() }
}

private companion object {
const val PACKAGE_NAME = "com.android.contacts"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.android.contacts.data.contactsfilter.repository

import com.android.contacts.data.contactsfilter.model.ContactsFilter
import com.android.contacts.list.ContactListFilter
import com.android.contacts.list.ContactListFilterController
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
class ContactsFilterRepositoryImplTest {

private val contactListFilterController = mockk<ContactListFilterController>()

private val repository = ContactsFilterRepositoryImpl(
contactListFilterController = contactListFilterController,
ioDispatcher = UnconfinedTestDispatcher(),
)

@Test
fun getContactsFilter_whenDefaultFilterIsPersisted_returnsAllAccounts() = runTest {
givenPersistedFilter(ContactListFilter.FILTER_TYPE_DEFAULT)

assertEquals(ContactsFilter.ALL_ACCOUNTS, repository.getContactsFilter())
}

@Test
fun getContactsFilter_whenAllAccountsFilterIsPersisted_returnsAllAccounts() = runTest {
givenPersistedFilter(ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS)

assertEquals(ContactsFilter.ALL_ACCOUNTS, repository.getContactsFilter())
}

@Test
fun getContactsFilter_whenCustomFilterIsPersisted_returnsCustom() = runTest {
givenPersistedFilter(ContactListFilter.FILTER_TYPE_CUSTOM)

assertEquals(ContactsFilter.CUSTOM, repository.getContactsFilter())
}

@Test
fun getContactsFilter_whenAnotherFilterTypeIsPersisted_returnsNull() = runTest {
givenPersistedFilter(ContactListFilter.FILTER_TYPE_ACCOUNT)

assertNull(repository.getContactsFilter())
}

@Test
fun getContactsFilter_whenNothingIsPersisted_returnsNull() = runTest {
every { contactListFilterController.persistedFilter } returns null

assertNull(repository.getContactsFilter())
}

private fun givenPersistedFilter(filterType: Int) {
every {
contactListFilterController.persistedFilter
} returns ContactListFilter.createFilterWithType(filterType)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.android.contacts.data.permissions.repository

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
internal class PermissionsRepositoryImplTest {

private val context = mockk<Context>()

private val repository = PermissionsRepositoryImpl(context = context)

@Test
fun isCallLogGranted_whenPermissionIsGranted_isTrue() = runTest {
givenCallLogPermission(PackageManager.PERMISSION_GRANTED)

assertTrue(repository.isCallLogGranted())
}

@Test
fun isCallLogGranted_whenPermissionIsDenied_isFalse() = runTest {
givenCallLogPermission(PackageManager.PERMISSION_DENIED)

assertFalse(repository.isCallLogGranted())
}

private fun givenCallLogPermission(result: Int) {
every { context.checkSelfPermission(Manifest.permission.READ_CALL_LOG) } returns result
}
}
Loading