Skip to content

Commit

Permalink
Closes #309 Allow updating and deleting read-only groups when Contact…
Browse files Browse the repository at this point in the history
…sContract.CALLER_IS_SYNCADAPTER is set to true
  • Loading branch information
vestrel00 committed Sep 8, 2023
1 parent bacc888 commit d5d7762
Show file tree
Hide file tree
Showing 6 changed files with 48 additions and 49 deletions.
7 changes: 6 additions & 1 deletion core/src/main/java/contacts/core/Contacts.kt
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ interface Contacts {
* insertion or updating its value afterwards is typically only done in the context of sync
* adapters. This is not for general app use!
*
* If you are using this API in your sync adapter implementation, then you should set this value
* If you are using this API in your sync adapter implementation, then you may set this value
* to true. It will allow you to perform certain insert, update, and delete operations that
* may otherwise fail. For example, attempting to update read-only rows
* ([contacts.core.entities.NewDataEntity.isReadOnly]) in the Data table
Expand All @@ -177,6 +177,11 @@ interface Contacts {
* read-only data should result in actual changes to take effect. Updating read-only data is
* just one of the many different behaviors/side-effects that this value affects.
*
* Obviously, ONLY MODIFY RAW CONTACT DATA THAT IS ASSOCIATED WITH THE
* [android.accounts.Account] THAT YOUR SYNC ADAPTER WORKS WITH! The APIs in this library does
* NOT prevent you from modifying data that your sync adapter does not own, which may cause
* sync issues (unsaved changes, data loss) to occur.
*
* This library is not responsible for documenting all of the different behaviors/side-effects
* caused by setting this to true.
*
Expand Down
63 changes: 26 additions & 37 deletions core/src/main/java/contacts/core/groups/GroupsDelete.kt
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ interface GroupsDelete : CrudApi {
* Adds the given [groups] to the delete queue, which will be deleted on [commit] or
* [commitInOneTransaction].
*
* Attempting to delete a read-only group will result in a failed operation.
* Read-only groups will be ignored and result in a failed operation unless
* [contacts.core.Contacts.callerIsSyncAdapter] is set to true.
*/
fun groups(vararg groups: ExistingGroupEntity): GroupsDelete

Expand Down Expand Up @@ -260,28 +261,15 @@ private class GroupsDeleteImpl(
val results = mutableMapOf<Long, Boolean>()
for (groupId in groupsIds) {
results[groupId] = contactsApi.deleteGroupsWhere(
// Attempting to delete a read-only group will result in a "successful" result
// even though the group was not actually deleted. The group will be marked as
// "deleted" in the local Contacts Provider database but will cause sync adapter
// failures. Ultimately, deletion of read-only groups will not be propagated
// to the remote sync servers.
//
// Thus, we manually add the check to match only non-read-only groups.
(GroupsFields.Id equalTo groupId) and (GroupsFields.GroupIsReadOnly equalTo false)
(GroupsFields.Id equalTo groupId)
.forSyncAdapter(contactsApi.callerIsSyncAdapter)
)
}

val whereResultMap = mutableMapOf<String, Boolean>()
groupsWhere?.let {
whereResultMap[it.toString()] = contactsApi.deleteGroupsWhere(
// Attempting to delete a read-only group will result in a "successful" result
// even though the group was not actually deleted. The group will be marked as
// "deleted" in the local Contacts Provider database but will cause sync adapter
// failures. Ultimately, deletion of read-only groups will not be propagated
// to the remote sync servers.
//
// Thus, we manually add the check to match only non-read-only groups.
it and (GroupsFields.GroupIsReadOnly equalTo false)
it.forSyncAdapter(contactsApi.callerIsSyncAdapter)
)
}

Expand All @@ -300,29 +288,18 @@ private class GroupsDeleteImpl(
val operations = arrayListOf<ContentProviderOperation>()

if (groupsIds.isNotEmpty()) {
contactsApi.deleteOperationFor(
// Attempting to delete a read-only group will result in a "successful" result
// even though the group was not actually deleted. The group will be marked as
// "deleted" in the local Contacts Provider database but will cause sync adapter
// failures. Ultimately, deletion of read-only groups will not be propagated
// to the remote sync servers.
//
// Thus, we manually add the check to match only non-read-only groups.
(GroupsFields.Id `in` groupsIds) and (GroupsFields.GroupIsReadOnly equalTo false)
).let(operations::add)
contactsApi
.deleteOperationFor(
(GroupsFields.Id `in` groupsIds)
.forSyncAdapter(contactsApi.callerIsSyncAdapter)
)
.let(operations::add)
}

groupsWhere?.let {
contactsApi.deleteOperationFor(
// Attempting to delete a read-only group will result in a "successful" result
// even though the group was not actually deleted. The group will be marked as
// "deleted" in the local Contacts Provider database but will cause sync adapter
// failures. Ultimately, deletion of read-only groups will not be propagated
// to the remote sync servers.
//
// Thus, we manually add the check to match only non-read-only groups.
it and (GroupsFields.GroupIsReadOnly equalTo false)
).let(operations::add)
contactsApi
.deleteOperationFor(it.forSyncAdapter(contactsApi.callerIsSyncAdapter))
.let(operations::add)
}

GroupsDeleteAllResult(
Expand All @@ -334,6 +311,18 @@ private class GroupsDeleteImpl(
}
}

private fun Where<GroupsField>.forSyncAdapter(callerIsSyncAdapter: Boolean): Where<GroupsField> =
if (callerIsSyncAdapter) {
this
} else {
// Attempting to delete a read-only group will result in a "successful" result even though
// the group was not actually deleted. The group will be marked as deleted" in the local
// Contacts Provider database but will cause sync adapter failures. Ultimately, deletion of
// read-only groups will not be propagated to the remote sync servers. Thus, we manually
// add the check to match only non-read-only groups.
this and (GroupsFields.GroupIsReadOnly equalTo false)
}

private fun Contacts.deleteGroupsWhere(where: Where<GroupsField>): Boolean =
contentResolver.applyBatch(deleteOperationFor(where)).deleteSuccess

Expand Down
12 changes: 6 additions & 6 deletions core/src/main/java/contacts/core/groups/GroupsUpdate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,8 @@ interface GroupsUpdate : CrudApi {
/**
* Adds the given [groups] to the update queue, which will be updated on [commit].
*
* ## Read-only [ExistingGroupEntity]s
*
* Read-only groups will be ignored and result in a failed operation.
* Read-only groups will be ignored and result in a failed operation unless
* [contacts.core.Contacts.callerIsSyncAdapter] is set to true.
*/
fun groups(vararg groups: ExistingGroupEntity): GroupsUpdate

Expand Down Expand Up @@ -152,8 +151,9 @@ interface GroupsUpdate : CrudApi {
TITLE_ALREADY_EXIST,

/**
* The [ExistingGroupEntity.isReadOnly] is true. System, read-only groups cannot be
* modified, except perhaps by the owning sync adapter.
* The [ExistingGroupEntity.isReadOnly] is true and
* [contacts.core.Contacts.callerIsSyncAdapter] is false. System, read-only groups
* cannot be modified, except perhaps by the owning sync adapter.
*/
GROUP_IS_READ_ONLY,

Expand Down Expand Up @@ -235,7 +235,7 @@ private class GroupsUpdateImpl(
// reason. Unlike other APIs in this library, this API will indicate success if there
// is no failure reason.

if (group.isReadOnly) {
if (group.isReadOnly && !contactsApi.callerIsSyncAdapter) {
failureReasons[group] = FailureReason.GROUP_IS_READ_ONLY
continue
}
Expand Down
2 changes: 1 addition & 1 deletion docs/entities/about-api-entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ newRawContact.setDataAsReadOnly(true)
Checking if an `ExistingDataEntity` is read-only requires a separate query. You can use extension
functions defined in `DataIsReadOnly.kt` for this purpose.

> ℹ️ For more info, read [Convenience functions | Check if Data is read-only](./../other/convenience-functions.md#getset-data-read-only).
> ℹ️ For more info, read [Convenience functions | Get/set Data read-only](./../other/convenience-functions.md#getset-data-read-only).
Modifying read-only data locally as a sync adapter is possible by using an instance of
`contacts.core.Contacts` with the `callerIsSyncAdapter` flag set to true.
Expand Down
7 changes: 4 additions & 3 deletions docs/groups/delete-groups.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,11 @@ Contacts Provider typically have the following system groups (for standard Googl
- systemId: Family, title: Family
- systemId: Coworkers, title: Coworkers

The above list may vary per account.
The above list may vary per Account and/or flavor of Android.

The `GroupsDelete` API will not attempt to delete a read-only group and will simply result in
failure.
If you are implementing a sync adapter, you may be able to delete read-only groups associated with
the Account that your sync adapter works with. For more info, read
[Contacts API Setup | Sync adapter operations](./../setup/setup-contacts-api.md#sync-adapter-operations).

## Group memberships are automatically deleted

Expand Down
6 changes: 5 additions & 1 deletion docs/groups/update-groups.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ Contacts Provider typically have the following system groups (for standard Googl
- systemId: Family, title: Family
- systemId: Coworkers, title: Coworkers

The above list may vary per account.
The above list may vary per Account and/or flavor of Android.

If you are implementing a sync adapter, you may be able to update read-only groups associated with
the Account that your sync adapter works with. For more info, read
[Contacts API Setup | Sync adapter operations](./../setup/setup-contacts-api.md#sync-adapter-operations).

## Groups and duplicate titles

Expand Down

0 comments on commit d5d7762

Please sign in to comment.