Giter Site home page Giter Site logo

alexstyl / contactstore Goto Github PK

View Code? Open in Web Editor NEW
434.0 4.0 15.0 1.06 MB

A modern, strongly-typed contacts API for Android.

Home Page: https://alexstyl.github.io/contactstore

License: Apache License 2.0

Kotlin 100.00%
android-library android contacts contacts-app contacts-api kotlin-android kotlin-library kotlin-coroutine-flow kotlin kotlin-coroutines

contactstore's Introduction

Introduction

Contact Store - a modern contacts Android API

Contact Store is a modern API that makes access to contacts on Android devices simple to use.

The default way of accessing contacts on Android is based off ContentProviders. Despite powerful, ContentProviders can be error-prone and frustrating to use.

Contact Store is a refreshed take on the Contacts API. It provides solutions to contacts' most frequent use cases and uses modern developer practices for an enjoyable developer experience.

Quick Start

Install the API using Gradle:

repositories {
  ...
  mavenCentral()
}

dependencies {
    implementation 'com.alexstyl:contactstore:1.7.0'
    
    // extension functions for kotlin coroutines
    implementation 'com.alexstyl:contactstore-coroutines:1.7.0'
    
    // extension functions for rxJava 3
    implementation 'com.alexstyl:contactstore-reactive:1.7.0'
    
    // optional dependency for tests
    testImplementation 'com.alexstyl:contactstore-test:1.7.0'
}

Fetch all contacts

val store = ContactStore.newInstance(application)

store.fetchContacts()
    .collect { contacts ->
        println("Contacts emitted: $contacts")
    }

Get details of a specific contact

val store = ContactStore.newInstance(application)

store.fetchContacts(
    predicate = ContactLookup(contactId),
    columnsToFetch = allContactColumns()
)
    .collect { contacts ->
        val contact = contacts.firstOrNull()
        if (contact == null) {
            println("Contact not found")
        } else {
            println("Contact found: $contact")

            // Use contact.phones, contact.mails, contact.customDataItems etc
        }
    }

Insert a new contact into a Gmail account

val store = ContactStore.newInstance(application)

store.execute {
    insert(InternetAcount("[email protected]", "gmail.com")) {
        firstName = "Paolo"
        lastName = "Melendez"
        phone(
            value = PhoneNumber("555"),
            label = Label.PhoneNumberMobile
        )
        mail(
            address = "[email protected]",
            label = Label.LocationWork
        )
        event(
            dayOfMonth = 23,
            month = 11,
            year = 2021,
            label = Label.DateBirthday
        )
        postalAddress(
            street = "85 Somewhere Str",
            label = Label.LocationHome
        )
        webAddress(
            address = "[email protected]",
            label = Label.LocationWork
        )
        groupMembership(groupId = 123)
    }
}

Update an existing Contact

val foundContacts = store.fetchContacts(
    predicate = ContactLookup(contactId = 5L),
    columnsToFetch = listOf(ContactColumn.Note)
).blockingGet()

if (foundContacts.isEmpty()) return // the contact was not found

val contact = foundContacts.first()

store.execute {
    update(contact.mutableCopy {
        note = Note("To infinity and beyond!")
    })
}

Delete a contact

store.execute {
    delete(contactId = 5L)
}

Does Contact Store support all features the default Contacts API does?

Probably not and this is not the aim of the project. The existing Contacts API has been out there for 10 years or so without much update. It is powerful given that you have access to an SQL-like syntax. I am assuming that a lot of the features it provides were introduced because the platform developers were coding against the ContactProvider interface instead of supporting the features app developers would eventually end up using.

Keeping the API lean allows for faster iterations/releases too as there is less things to maintain. I am not saying that the features of the default API are not important or that they will never make it to Contact Store. Instead, I would rather have the features and capabilities of the API to be driven by dev requirements.

If you believe you are missing a specific feature, open a new feature request on Github.

Can I use this API from Java?

You should be able to use Contact Store through Java as you can call Kotlin code from Java, but it won't be ideal. Check this Github issue and write your experience.

Getting Help

Checkout the project documentation to learn about about Contact Store features in detail. To report a specific problem or feature request, open a new issue on Github.

License

Apache 2.0. See the LICENSE file for details.

Author

Made by Alex Styl. Follow @alexstyl on Twitter for future updates.

contactstore's People

Contributors

alexstyl avatar rbenza avatar tatocaster avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

contactstore's Issues

Possible issues with LinkedAccountMimeType

I found that certain apps, for example Telegram can have different package types for different app stores but use same/original package name as account type when adding account mimes to Android system.

For example, Telegram's Play Store version has package name org.telegram.messenger but Web download version has org.telegram.messenger.web package name and org.telegram.messenger.web version add account mime with package name/account type "org.telegram.messenger" which prevents us from matching to LinkedAccountMimeType.packageName we extract with AccountInfoResolver.kt

Getting mime types and matching according to mime types account type would possibly be a solution

java.lang.IllegalStateException: Inline class has no underlying property name in metadata: deserialized class LookupKey

When someone tries to use the Contact#lookupKey property on ContactStore 0.7.0, they can see the following stacktrace in compilation time:

Caused by: java.lang.IllegalStateException: Inline class has no underlying property name in metadata: deserialized class LookupKey
	at org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedClassDescriptor.computeInlineClassRepresentation(DeserializedClassDescriptor.kt:185)
	at org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedClassDescriptor.access$computeInlineClassRepresentation(DeserializedClassDescriptor.kt:33)
	at org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedClassDescriptor$inlineClassRepresentation$1.invoke(DeserializedClassDescriptor.kt:68)
	at org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedClassDescriptor$inlineClassRepresentation$1.invoke(DeserializedClassDescriptor.kt:68)
	at org.jetbrains.kotlin.storage.LockBasedStorageManager$LockBasedLazyValue.invoke(LockBasedStorageManager.java:408)
	at org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedClassDescriptor.getInlineClassRepresentation(DeserializedClassDescriptor.kt:171)
	at org.jetbrains.kotlin.ir.declarations.lazy.IrLazyClass$inlineClassRepresentation$2.invoke(IrLazyClass.kt:100)
	at org.jetbrains.kotlin.ir.declarations.lazy.IrLazyClass$inlineClassRepresentation$2.invoke(IrLazyClass.kt:99)
	at org.jetbrains.kotlin.ir.declarations.lazy.SynchronizedLazyVar.getValue(lazyUtil.kt:29)
	at org.jetbrains.kotlin.ir.declarations.lazy.SynchronizedLazyVar.getValue(lazyUtil.kt:40)
	at org.jetbrains.kotlin.ir.declarations.lazy.IrLazyClass.getInlineClassRepresentation(IrLazyClass.kt:99)
	at org.jetbrains.kotlin.ir.types.IrTypeSystemContext$DefaultImpls.getUnsubstitutedUnderlyingType(IrTypeSystemContext.kt:451)
	at org.jetbrains.kotlin.backend.jvm.JvmIrTypeSystemContext.getUnsubstitutedUnderlyingType(JvmIrTypeSystemContext.kt:19)
	at org.jetbrains.kotlin.ir.types.IrTypeSystemContext$DefaultImpls.getSubstitutedUnderlyingType(IrTypeSystemContext.kt:454)
	at org.jetbrains.kotlin.backend.jvm.JvmIrTypeSystemContext.getSubstitutedUnderlyingType(JvmIrTypeSystemContext.kt:19)
	at org.jetbrains.kotlin.backend.jvm.codegen.IrTypeCheckerContextForTypeMapping.getSubstitutedUnderlyingType(IrTypeMapper.kt)
	at org.jetbrains.kotlin.types.ExpandedTypeUtilsKt.computeExpandedTypeInner(expandedTypeUtils.kt:36)
	at org.jetbrains.kotlin.types.ExpandedTypeUtilsKt.computeExpandedTypeForInlineClass(expandedTypeUtils.kt:13)
	at org.jetbrains.kotlin.types.AbstractTypeMapper.mapType(AbstractTypeMapper.kt:104)
	at org.jetbrains.kotlin.types.AbstractTypeMapper.mapType(AbstractTypeMapper.kt:47)
	at org.jetbrains.kotlin.backend.jvm.codegen.IrTypeMapper.mapType(IrTypeMapper.kt:134)
	at org.jetbrains.kotlin.backend.jvm.codegen.MethodSignatureMapper.mapReturnType(MethodSignatureMapper.kt:180)
	at org.jetbrains.kotlin.backend.jvm.codegen.MethodSignatureMapper.mapReturnType(MethodSignatureMapper.kt:173)
	at org.jetbrains.kotlin.backend.jvm.codegen.MethodSignatureMapper.mapSignature(MethodSignatureMapper.kt:263)
	at org.jetbrains.kotlin.backend.jvm.codegen.MethodSignatureMapper.mapSignature$default(MethodSignatureMapper.kt:228)
	at org.jetbrains.kotlin.backend.jvm.codegen.MethodSignatureMapper.mapSignatureSkipGeneric(MethodSignatureMapper.kt:223)
	at org.jetbrains.kotlin.backend.jvm.codegen.MethodSignatureMapper.mapAsmMethod(MethodSignatureMapper.kt:57)
	at org.jetbrains.kotlin.backend.jvm.codegen.IrCodegenUtilsKt.classFileContainsMethod(irCodegenUtils.kt:377)
	at org.jetbrains.kotlin.backend.jvm.MemoizedInlineClassReplacements.buildReplacement(MemoizedInlineClassReplacements.kt:240)
	at org.jetbrains.kotlin.backend.jvm.MemoizedInlineClassReplacements.buildReplacement$default(MemoizedInlineClassReplacements.kt:227)
	at org.jetbrains.kotlin.backend.jvm.MemoizedInlineClassReplacements.createMethodReplacement(MemoizedInlineClassReplacements.kt:183)
	at org.jetbrains.kotlin.backend.jvm.MemoizedInlineClassReplacements.access$createMethodReplacement(MemoizedInlineClassReplacements.kt:39)
	at org.jetbrains.kotlin.backend.jvm.MemoizedInlineClassReplacements$getReplacementFunction$1.invoke(MemoizedInlineClassReplacements.kt:85)
	at org.jetbrains.kotlin.backend.jvm.MemoizedInlineClassReplacements$getReplacementFunction$1.invoke(MemoizedInlineClassReplacements.kt:54)
	at 

Feature request: Add sorting to the ContactStore api

It would be really nice if users could sort the data by fields and ASC/DESC order. More specifically, support it on query level in content provider and not on the call site when the data is already fetched.
Currently ContactQueries.kt use Contacts.DISPLAY_NAME_PRIMARYas a sort order by default in multiple lookups.

I suggest adding the overloading method fetchContacts with the 3rd parameter sortOrder nullable by default, this will not break the existing code to the users and accept sorting order. Instead of String, there can be created some kind of data structure to predefined fields and ASC/DESC behavior. queryContacts function by default will accept Contacts.DISPLAY_NAME_PRIMARY as a parameter.

Publishing app to mavenCentral

I am currently in the process of uploading the library to mavenCentral. This might take a few days to become available.

It seemed like the repo had to be public for me to do so.

Hang tight.

Run unit tests and Android tests via Github Actions

There are more people contributing to the project lately, and it would be nice to have less moderation required on each PR.
This issue is about running all unit tests and Android tests of all modules of the project, using Github Actions.

Requirements:

  • Have all unit tests and Android tests to run when someone opens a PR.
  • If any of the tests fail, then the PR should be blocked.
  • Document is a way (i.e. on your PR) how this automation works.

I have to admit that I have yet to experiment with Github Actions so I am not entirely sure what it fully entails.

There is no need for this work to take place in a single PR.

If you would like to pick up this task, reply to this issue, so that I can assign it to you.

Discussion: Java support

This issue works as a discussion for checking if there is a need to provide Java support to consumers.

If you are interested in such a thing, react to this post. If you are brave enough to use ContactStore on your Java project, please write down your experience.

If you can think of a nice API and changes that need to happen in the project, reply with your suggestion.

Feature Request: Provide Paging-Support

When you have a lot of contacts, loading them all at once can take some time. For that reason, I would like to use paging.
Therefore it would be really nice if a method could be provided which may look a bit like this:

    public fun fetchContactsPaged(
        predicate: ContactPredicate? = null,
        columnsToFetch: List<ContactColumn> = emptyList(),
        displayNameStyle: DisplayNameStyle = DisplayNameStyle.Primary,
        pageSize: Int,
        offset: Int,
    ): FetchRequest<List<Contact>>

allowing the caller to pass a page size (how many records should be fetched) and an offset (assuming the contacts are sorted by displayName).

Crash: uri format is unknown when ensuring ContactUri

Received a crash from a user of the library. Stack trace:

image

Important thing to figure out whether this is due to user input or there is an issue with the library. Either way the library should not crash the app in either case.

Support for a Idiomatic Kotlin API

There are various opportunities for making developer life easier using the API.

For starters, a typical way a developer might create a new contact is:

        store.execute(SaveRequest().apply {
            insert(
                MutableContact().apply {
                    prefix = "A."
                    firstName = "Paolo"
                    middleName = "M."
                    lastName = "Melendez"
                    suffix = "Z."
                }
            )
        })

This relies on Kotlin's apply extension, which might hint towards embedding the lambda into the API itself.
An example:

        store.execute {
            insert(newContact {
                prefix = "A."
                firstName = "Paolo"
                middleName = "M."
                lastName = "Melendez"
                suffix = "Z."
            })
        }

In the same fashion, updating a contact could be expressed as:

        store.execute {
            update(contact.mutableCopy {
                note = Note("To infinity and beyond!")
            })
        }

This is an idea I am still exploring. Unless there is a need/interest for a more idiomatic API, it might be just overengineering.

Filter for duplicate numbers

Seems like the contact database might contain the same phone number multiple times.

Someone shared a screenshot of the sample app, where the contact details show the same number multiple times. The contact had WhatsApp linked to it. An other contact contained the same phone three times. That contact had both WhatsApp and Signal installed.

Whatsapp Whatsapp + Signal
Frame 1 Frame 2

URI formate exception

Faced the following issue while using the library in prod, any possible solution for

Fatal Exception: java.lang.IllegalArgumentException: uri format is unknown
       at com.alexstyl.contactstore.RawContactQueries.fetchRawContacts(:2)
       at com.alexstyl.contactstore.ContactQueries.access$fetchAdditionalColumns(:1)
       at com.alexstyl.contactstore.ContactQueries$queryContacts$$inlined$map$1$2.emit(:69)
       at com.alexstyl.contactstore.ContactQueries$queryAllContacts$$inlined$map$1$2.emit(:64)
       at com.alexstyl.contactstore.utils.ContentResolverKt$runQueryFlow$$inlined$map$1$2.emit(:74)
       at kotlinx.coroutines.flow.internal.SafeCollectorKt$emitFun$1.invoke(SafeCollector.kt:15)
       at kotlinx.coroutines.flow.internal.SafeCollectorKt$emitFun$1.invoke(SafeCollector.kt:15)
       at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:87)
       at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:66)
       at com.alexstyl.contactstore.utils.FlowExtensionsKt$a.invokeSuspend(:34)
       at com.alexstyl.contactstore.utils.FlowExtensionsKt$a.invoke(:1)
       at kotlinx.coroutines.flow.FlowKt__EmittersKt$onStart$$inlined$unsafeFlow$1.collect(SafeCollector.common.kt:116)
       at com.alexstyl.contactstore.utils.ContentResolverKt$runQueryFlow$$inlined$map$1.collect(:21)
       at com.alexstyl.contactstore.ContactQueries$queryAllContacts$$inlined$map$1.collect(:7)
       at com.alexstyl.contactstore.ContactQueries$queryContacts$$inlined$map$1.collect(:11)
       at kotlinx.coroutines.flow.FlowKt__ReduceKt.first(Reduce.kt:198)
       at kotlinx.coroutines.flow.FlowKt.first(:1)
       at com.alexstyl.contactstore.FetchRequest$a.invokeSuspend(:34)
       at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
       at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
       at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
       at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
       at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
       at kotlinx.coroutines.BuildersKt.runBlocking(:1)
       at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
       at kotlinx.coroutines.BuildersKt.runBlocking$default(:1)
       at com.alexstyl.contactstore.FetchRequest.blockingGet(:7)
       at com.jobget.onboarding.requestendorsements.repo.DefaultUserContactsProvider.get$lambda-1(UserContactsProvider.kt:45)
       at com.jobget.onboarding.requestendorsements.repo.DefaultUserContactsProvider.$r8$lambda$PCXMhjAq1fthczh8YCeZcJhq6FM()
       at com.jobget.onboarding.requestendorsements.repo.DefaultUserContactsProvider$$ExternalSyntheticLambda4.call(:2)
       at io.reactivex.rxjava3.internal.operators.observable.ObservableFromCallable.subscribeActual(ObservableFromCallable.java:46)
       at io.reactivex.rxjava3.core.Observable.subscribe(Observable.java:13176)
       at io.reactivex.rxjava3.internal.operators.observable.ObservableMap.subscribeActual(ObservableMap.java:33)
       at io.reactivex.rxjava3.core.Observable.subscribe(Observable.java:13176)
       at io.reactivex.rxjava3.internal.operators.observable.ObservableMap.subscribeActual(ObservableMap.java:33)
       at io.reactivex.rxjava3.core.Observable.subscribe(Observable.java:13176)
       at io.reactivex.rxjava3.internal.operators.observable.ObservableMap.subscribeActual(ObservableMap.java:33)
       at io.reactivex.rxjava3.core.Observable.subscribe(Observable.java:13176)
       at io.reactivex.rxjava3.internal.operators.observable.ObservableMap.subscribeActual(ObservableMap.java:33)
       at io.reactivex.rxjava3.core.Observable.subscribe(Observable.java:13176)
       at io.reactivex.rxjava3.internal.operators.observable.ObservableOnErrorReturn.subscribeActual(ObservableOnErrorReturn.java:31)
       at io.reactivex.rxjava3.core.Observable.subscribe(Observable.java:13176)
       at io.reactivex.rxjava3.internal.operators.observable.ObservableSubscribeOn$SubscribeTask.run(ObservableSubscribeOn.java:96)
       at io.reactivex.rxjava3.core.Scheduler$DisposeTask.run(Scheduler.java:644)
       at io.reactivex.rxjava3.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:65)
       at io.reactivex.rxjava3.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:56)
       at java.util.concurrent.FutureTask.run(FutureTask.java:266)
       at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301)
       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
       at java.lang.Thread.run(Thread.java:923)

We just fetch the contacts in our application to show them to our users.

Possibility of Supporting ContactsAccountType ?

See
https://developer.android.com/guide/topics/providers/contacts-provider#ContactsFile

https://cs.android.com/android/platform/superproject/+/master:packages/apps/Contacts/src/com/android/contacts/model/account/AccountTypeProvider.java

https://cs.android.com/android/platform/superproject/+/master:packages/apps/Contacts/src/com/android/contacts/model/account/ExternalAccountType.java

https://stackoverflow.com/questions/35992096/how-to-showhandle-contact-details-intents-of-apps

Android has ContactsFile concept where external apps such as WhatsApp can integrate in to Contacts database with deep links to contacts.

It is extremely complicated (at least to me) to get that data in a modern way as you need to read and parse the contacts file for each app.

Newer versions prevent scanning of packages and I am not sure if there is a way to find packages with ContactsFile but, an easier option to add some known package names and only scan contact database for those.

For example I currently do something like

enum class KnownAccountMimeProviders(val packageName: String, val title: String) {

    WHATS_APP("com.whatsapp", "WhatsApp"),
    SIGNAL("org.thoughtcrime.securesms", "Signal"),
    TELEGRAM("org.telegram.messenger", "Telegram"),
    VIBER("com.viber.voip", "Viber"),
    KIK("kik.android", "Kik"),
    DUO("com.google.android.apps.tachyon", "Duo"),
    THREEMA("ch.threema.app", "Threema");

    companion object {
        private val map = values().associateBy(KnownAccountMimeProviders::packageName)
        fun fromPackageName(packageName: String) = map[packageName]

    }

}
    private fun getAllKnownAccountMimes(): List<AccountMime> {
        val startTime = System.currentTimeMillis()
        val items = mutableListOf<AccountMime>()
        val projection = arrayOf(
            ContactsContract.Data.CONTACT_ID,
            ContactsContract.Data._ID,
            ContactsContract.Data.MIMETYPE,
            ContactsContract.RawContacts.ACCOUNT_TYPE,
            ContactsContract.Data.DATA1,
            ContactsContract.Data.DATA2,
            ContactsContract.Data.DATA3,
            //07/07/21 Adding DATA4 and DATA5 as Duo stores titles there as (DATA4 Voice call on Duo) (DATA5 Voice call on Duo +123456989)
            ContactsContract.Data.DATA4,
            ContactsContract.Data.DATA5
        )

        val selection = KnownAccountMimeProviders
            .values()
            //Spaces in the beginning and end are important!!"
            .joinToString(separator = " OR ") { "${ContactsContract.RawContacts.ACCOUNT_TYPE} = ?" }
            .ifEmpty { null }

        val selectionArgs = if (selection != null) {
            KnownAccountMimeProviders.values().map { it.packageName }.toTypedArray()
        } else {
            null
        }


        if (CLog.isDebug()) {
            CLog.log(logTag, "getAllAccountMimes() -> selection: $selection")
        }

        try {
            applicationContext.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection, selection, selectionArgs, null)?.use { cursor ->
                while (cursor.moveToNext()) {
                    try {
                        if (CLog.isDebug()) {
                            CLog.log(logTag, "getAllAccountMimes() -> ACCOUNT_TYPE/Package Name: ${cursor.getStringFromColumn(ContactsContract.RawContacts.ACCOUNT_TYPE)}")
                            CLog.log(logTag, "getAllAccountMimes() -> MIMETYPE: ${cursor.getStringFromColumn(ContactsContract.Data.MIMETYPE)}")
                        }

                        val accountPackage = cursor.getStringFromColumn(ContactsContract.RawContacts.ACCOUNT_TYPE).extNullIfEmptyOrValue()
                            ?: continue
                        val mime = cursor.getStringFromColumn(ContactsContract.Data.MIMETYPE).extNullIfEmptyOrValue()
                            ?: continue

                        val id = cursor.getLongFromColumn(ContactsContract.Data._ID)

                        val contactId = cursor.getLongFromColumn(ContactsContract.Data.CONTACT_ID)

                        val accountMimeDataSet = mutableListOf<AccountMimeData>()

                        cursor.getStringFromColumn(ContactsContract.Data.DATA1).extNullIfEmptyOrValue()?.let {
                            accountMimeDataSet.add(AccountMimeData(ContactsContract.Data.DATA1, it))
                        }

                        cursor.getStringFromColumn(ContactsContract.Data.DATA2).extNullIfEmptyOrValue()?.let {
                            accountMimeDataSet.add(AccountMimeData(ContactsContract.Data.DATA2, it))
                        }

                        cursor.getStringFromColumn(ContactsContract.Data.DATA3).extNullIfEmptyOrValue()?.let {
                            accountMimeDataSet.add(AccountMimeData(ContactsContract.Data.DATA3, it))
                        }

                        cursor.getStringFromColumn(ContactsContract.Data.DATA4).extNullIfEmptyOrValue()?.let {
                            accountMimeDataSet.add(AccountMimeData(ContactsContract.Data.DATA4, it))
                        }

                        cursor.getStringFromColumn(ContactsContract.Data.DATA5).extNullIfEmptyOrValue()?.let {
                            accountMimeDataSet.add(AccountMimeData(ContactsContract.Data.DATA5, it))
                        }
          

                        val accountMime = AccountMimeProvider.provideFromBaseAccountMime(contactId = contactId, id = id, accountType = accountPackage, mimeType = mime, accountMimeDataSet = accountMimeDataSet)
     
                        if (accountMime !is UnknownAccountMime) {
                            items.add(accountMime)
                        } else {
                    
                        }
                    } catch (e: Exception) {
                    
                    }

                }
            }
        } catch (e: Exception) {
         
        }


        return items
    }

As you see I rely on DATA2/3etc but that is eror prone and can fail if developer changes it. It can be extracted from ContactsFile and parsed that way.

Support lower Android API levels

Currently the library supports API level 23 and newer. It would be great if the minSdk could be lowered even more, if there are no technical limitations. Personally, I would like to use this library in an app with minSdk = 19.

java.lang.NullPointerException: item.getAsString(NameColumns.MIDDLE_NAME) must not be null

Hi,
Got this when I selected a contact with no middle name in sample app.

I think these properties should be null-safety in ContactQueries.

firstName = item.getAsString(NameColumns.GIVEN_NAME)
middleName = item.getAsString(NameColumns.MIDDLE_NAME)
lastName = item.getAsString(NameColumns.FAMILY_NAME)
prefix = item.getAsString(NameColumns.PREFIX)
suffix = item.getAsString(NameColumns.SUFFIX)
phoneticFirstName = item.getAsString(NameColumns.PHONETIC_GIVEN_NAME)
phoneticMiddleName = item.getAsString(NameColumns.PHONETIC_MIDDLE_NAME)
phoneticLastName = item.getAsString(NameColumns.PHONETIC_FAMILY_NAME)

Provide an error-prone way to use Labels

ContactStore expects the developer to provide an appropriate Label when they use a LabeledValue object. For example, you can represent a 'Mobile' entry as such:

val newMobile = LabeledValue(PhoneNumber("555"), PhoneNumberMobile) 

and the object can be used later on in ContactStore.execute(). If the label provided is not a PhoneXYZ one, execute() will throw an exception. For example:

val newMobile = LabeledValue(PhoneNumber("555"), DateBirthday)

will compile and the LabeledValue will be created without an issue. Passing the object to execute() though will cause an exception to be thrown.

An enhancement here would be to provide developers with direct feedback of this behavior. Compilation time would be ideal but hard to maintain. Alternatively failing during Runtime when an incompatible object gets created is also an approach.

ANR -> trySendBlocking is incorrectly called in callBackFlow

I think trySendBlocking is incorrectly called here: https://github.com/alexstyl/contactstore/blob/1addc3efad4b9abf6d9e8210a9aa98e2f4d9c255/library/src/main/java/com/alexstyl/contactstore/UriFlow.kt

I already saw couple of ANR's in production, probably when a user removes a group of contacts at once. Then onChanged gets() called frequently in a row. Tested it with a log statement.

To resolve this we should check if the Channel is open, and if so call trySend(). PR to fix this: #20

Crash -> Error java.lang.NumberFormatException

It seems you lib produces a crash when a user has an invalid phone number. See stack trace below. I made a PR for it to fix the issue: #17

Caused by java.lang.NumberFormatException: For input string: ""
       at java.lang.Integer.parseInt(Integer.java:627)
       at java.lang.Integer.parseInt(Integer.java:650)
       at a.a.invoke(a.java:10)
       at com.alexstyl.contactstore.utils.CursorKt.iterate(CursorKt.java:10)
       at com.alexstyl.contactstore.ContactQueries.access$fetchAdditionalColumns(ContactQueries.java:10)
       at com.alexstyl.contactstore.ContactQueries$queryContacts$$inlined$map$1$2.emit(ContactQueries.java:10)
       at com.alexstyl.contactstore.ContactQueries$queryAllContacts$$inlined$map$1$2.emit(ContactQueries.java:62)
       at com.alexstyl.contactstore.utils.ContentResolverKt$runQueryFlow$$inlined$map$1$2.emit(ContentResolverKt.java:72)
       at kotlinx.coroutines.flow.internal.SafeCollectorKt$emitFun$1.invoke(SafeCollectorKt.java:1)
       at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.java:9)
       at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.java)
       at com.alexstyl.contactstore.utils.FlowExtensionsKt$a.invokeSuspend(FlowExtensionsKt.java:32)
       at com.alexstyl.contactstore.utils.FlowExtensionsKt$a.invoke(FlowExtensionsKt.java:2)
       at kotlinx.coroutines.flow.FlowKt__EmittersKt$onStart$$inlined$unsafeFlow$1.collect(FlowKt__EmittersKt.java:90)
       at com.alexstyl.contactstore.utils.ContentResolverKt$runQueryFlow$$inlined$map$1.collect(ContentResolverKt.java:21)
       at com.alexstyl.contactstore.ContactQueries$queryAllContacts$$inlined$map$1.collect(ContactQueries.java:5)
       at com.alexstyl.contactstore.ContactQueries$lookupFromMail$$inlined$map$1.collect$bridge(ContactQueries.java:5)
       at com.alexstyl.contactstore.ContactQueries$queryContacts$$inlined$map$1.collect(ContactQueries.java:11)
       at kotlinx.coroutines.flow.FlowKt__ReduceKt.firstOrNull(FlowKt__ReduceKt.java:1)
       at kotlinx.coroutines.flow.FlowKt.firstOrNull(FlowKt.java:1)

Test implementation of ContactStore

I am working on an implementation of the ContactStore interface that can be used in unit tests. That way users of the api will be able to have a consistent behavior (that will work as close to the real implementation as possible) without having to create their own "Fake" of the store.

There are going to be some limitations. Each OEM might have modified the ContactProvider in the device in any way, which means that the test implementation can not match the behavior found in every single possible device/configuration.

Open to suggestions and ideas.

Problem with ContactGroups when moving a contact between two accounts

Hi

I have encountered an interesting problem this weekend:

  • My app can now move/copy a contact from my separate DB to the "normal" Contact-Store. But as that would require a deeper understanding of my usecase, let us instead assume I want to move the contact from one Account (as in InternetAccount) to another. The behavior should be the same.
  • Part of my new logic takes care of copying the GroupMemberships. To be safe - I check that all the GroupMemberships which I want to copy, reference a valid ContactGroup.
  • I use contactStore.fetchContactGroups() to load all existing ContactGroups to compare them by id (and name as fallback). But the problem is that - if I understood it correctly - that is an aggregated list over all accounts on the phone.
  • In my case, that is not what I want, because while a ContactGroup does exist on the phone, it may not exist in the account into which I am copying the contact. As a result, the GroupMembership is set correctly but in the new account, it does not reference any valid ContactGroup and is therefore not shown in the default Google Contacts app.
  • However, if I move the contact back, everything is fine again because the GroupMembership survived and now references a valid ContactGroup again.

Does that description make any sense?

If yes, I have two questions / wishes

  1. Would it be possible to add the option to filter contactStore.fetchContactGroups() by Account?
  2. When using contactStore.execute { insertGroup(myGroup) } where is it actually saved? Into the phone's local contacts? Into all the accounts? Into the "default" account?
    => would be great if it were able to specify exactly into which account to store it

Cannot instantiate MutableContact in tests

Sorry for bothering you, it is probably a simple mistake on my part
Since updating to version 1.7 (actually since updating to more than 1.4), I always get a crash when I try to create or mock a MutableContact in one of my unit-tests

Basically,

    @Test
    fun `should not crash`() {
        val realContact = MutableContact()
    }

would already crash. Mocking it (using the mockk library) would lead to the same result.
On the other hand, instantiating the class in production code works without issues.

The exception is the following...
Do you happen to have any clue what might be the cause?

'kotlin.reflect.KProperty1 a.p.a(java.lang.Class, java.lang.String, java.lang.String, int)'
java.lang.NoSuchMethodError: 'kotlin.reflect.KProperty1 a.p.a(java.lang.Class, java.lang.String, java.lang.String, int)'
	at com.alexstyl.contactstore.MutableContact.<clinit>(SourceFile:1)
	at ch.abwesend.privatecontacts.infrastructure.repository.androidcontacts.AndroidContactSaveServiceTest.should not crash(AndroidContactSaveServiceTest.kt:210)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
	at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
	at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
	at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:99)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:79)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:75)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:61)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
	at com.sun.proxy.$Proxy2.stop(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:193)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
	at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:133)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
	at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
	at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)

[Bug] Nickname is not stored

Hi

I have a little problem: it seems like the nickname is lost when I try to create a new contact.

My code looks like this:

contactStore.execute { insert(contact, internetAccount) }

in debugging, contact has the nickname set to the desired value. However, when I look at the contact afterward in Google's contacts app, the nickname has disappeared.

๐Ÿ“– Guest Book

This issue is here for you to write any nice things about the project you like. I would love to know how ContactStore has helped you in your projects. If you happen to know any companies/projects that use it, feel free to mention them.

If you have any issues, questions, suggestions, please open a new issue

Thanks,
Alex

Primary Phone/Email support?

Does this exist in the current API scheme with fetching? Im not seeing an easy way to check this data without a secondary cursor query on the phones and emails.

[Feature Request] Providing like the "Main" account of an existing contact

This is related to my previous feature-request (thanks again for that) about contact-groups and their associated accounts.

My scenario is as follows: I load an existing contact from Android using the ContactStore. Now I would like to add a group-membership. How do I know which ones to offer to the user? If the user wants to create a new one, how do I know in which account I need to store it?

All of that works for creating a new account, ofc, because there I (or the user) need to specify in which account to store the contact. But for an existing contact, that information is missing.

If I understood it correctly, there is no "one single" account on the Contact class because you potentially aggregate the information over multiple accounts. Is it possible to identify something like a "main" account? Or providing a list of the involved accounts (in that case I could do a best-effort guess and e.g. prefer a google account over whatsapp or something like that)?

Java version

Can any one share the java version to get the contact list, edit contact and add contact .? Thanks

fetchContacts does not return lookupKey when using TestContactStore

I am currently trying the test dependency out but observed a weird error when prepopulating the store. I am currently filtering out contacts without a lookup key. While this should technically never be the case, the property is still nullable. Now, when setting a prepopulated contact with a lookup key, the result using fetchContacts returns the exact same contact but without the lookup key, which is null:

    private val store = TestContactStore(
        contactsSnapshot = listOf(
            StoredContact(
                contactId = 0,
                lookupKey = LookupKey(""),
                firstName = "Max",
                lastName = "Mustermann",
                phones = listOf(
                    LabeledValue(
                        value = PhoneNumber("123456789"),
                        label = Label.PhoneNumberMobile,
                    )
                ),
                postalAddresses = listOf(
                    LabeledValue(
                        value = PostalAddress(
                            postCode = "12345",
                            street = "Musterweg 10",
                            city = "Musterstadt",
                            country = "Musterland"
                        ),
                        label = Label.LocationHome
                    )
                )
            )
        )
    )
[PartialContact(contactId=0, displayName='Max Mustermann' lookupKey=null, isStarred=false, columns=[com.alexstyl.contactstore.ContactColumn$Names@197180a5, com.alexstyl.contactstore.ContactColumn$Phones@31028e45, com.alexstyl.contactstore.ContactColumn$Mails@68f75a35, com.alexstyl.contactstore.ContactColumn$PostalAddresses@50e0b472], imageData=N/A, phones=[LabeledValue(value=PhoneNumber(raw=123456789), label=com.alexstyl.contactstore.Label$PhoneNumberMobile@4a86be01, id=null, account=null)], mails=[], events=N/A, postalAddresses=[LabeledValue(value=PostalAddress(street=Musterweg 10, poBox=, neighborhood=, city=Musterstadt, region=, postCode=12345, country=Musterland), label=com.alexstyl.contactstore.Label$LocationHome@50a1af86, id=null, account=null)], webAddresses=N/A, imAddresses=N/A, sipAddresses=N/A, customDataItems=N/A, note=N/A, groups=N/A, relations=N/A, organization=N/A, jobTitle=N/A, firstName=Max, lastName=Mustermann, middleName=, prefix=, suffix=, phoneticLastName=, phoneticFirstName=, phoneticMiddleName=, fullNameStyle=0, phoneticNameStyle=0, nickname=N/A)]

Create static analysis gradle task (detekt + lint)

There are more people contributing to the project lately, and it would be nice to have less moderation required on each PR.
This issue is about creating a gradle task to run static analysis in the project.

Requirements:

  • Introduce detekt in the project (link to Detekt)
  • Create a gradle task called runStaticAnalysis which would run Android lint and detekt.
  • Document is a way (i.e. on your PR) how the static analysis works (such as how to change the rules of each tools).
  • Static analysis should run on all modules (library, library-test, sample) with the same rules.

Keep in mind that there are going to be way more detekt and lint issues than the default thresholds. There is no need to fix anything in the code that the static analysis will pick up. Instead, increases the threshold of allowed issues to the number of issues detected.

There is no need for this work to take place in a single PR.

If you would like to pick up this task, reply to this issue, so that I can assign it to you.

Link to Detekt: https://detekt.github.io/detekt/
Example Project with static analysis set up: https://github.com/code-with-the-italians/bundel/

Investigation: How much RAM does Contact Store when fetching a lot of contacts?

Use the profiler that comes with Android Studio to investigate how much memory is required by Contact Store when fetching contacts.

More specifically, I would like to understand how much memory is required when doing a query for all contacts of the device.
Testing with 1000 contacts sould be enough for testing.

You can use an app such as this to populate the device with multiple contacts.

More info

Someone reported a crash from production from a low end device. The crash seems to be related to Cursor allocation.

crash

The user also mentioned that their app crashes with a similar crash from Room:

Fatal Exception: android.database.CursorWindowAllocationException
Cursor window allocation of 2048 kb failed.

android.database.CursorWindow.<init> (http://CursorWindow.java:108)
android.database.sqlite.SQLiteCursor.getCount (http://SQLiteCursor.java:132)
-LINE REMOVED-
http://androidx.room.CoroutinesRoom$Companion$createFlow$1$1$1.invokeSuspend (http://CoroutinesRoom.java:2)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (http://BaseContinuationImpl.java:4)
http://kotlinx.coroutines.DispatchedTask.run (http://DispatchedTask.java:86)
java.util.concurrent.ThreadPoolExecutor.runWorker (http://ThreadPoolExecutor.java:1162)
http://java.lang.Thread.run (http://Thread.java:764)

Both crashes seem to be related to memory allocation. I can think of ways of reducing the memory required by Contact Store, but we need to understand the memory required first.

Device Specs

https://www.gsmarena.com/tecno_pouvoir_3-9866.php

wfR8ooiu jpg large

[Question] Creating new contacts

Sorry, this is more of a question than an issue but I was not sure about the ideal channel of communications and maybe the question is also interesting for other users of the library.

As explained in the Readme, it is possible to create a new contact in the Android contact database, however for my real-world use-case I have two additional questions:

  1. I cant/don't have to pass a contactId along. Therefore, I assume that the ID will be assigned automatically: Is that correct (would make sense to me as Android is better able to guarantee its uniqueness).
  2. I have to pass the account into which the contact should be stored. Is there a way to get the information from Android (or ideally from this library), which accounts are available and which of them is the default?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.