diff options
author | Ricki Hirner <hirner@bitfire.at> | 2020-10-15 18:35:08 +0300 |
---|---|---|
committer | Ricki Hirner <hirner@bitfire.at> | 2020-10-15 18:37:37 +0300 |
commit | ce2f39525d572047a5d49709e48ead87adc186ca (patch) | |
tree | 002166b76dcfa3a1a6a2137329e2d39da2c267c9 | |
parent | b840ecbff99b629f3558ca49a858a2c9fcf2632d (diff) |
Update to Android gradle plugin 4.1.0; increase target SDK level to 30; update BatchOperation to new API definition
-rw-r--r-- | build.gradle | 8 | ||||
-rw-r--r-- | gradle/wrapper/gradle-wrapper.properties | 2 | ||||
-rw-r--r-- | src/main/java/at/bitfire/vcard4android/AndroidContact.kt | 217 | ||||
-rw-r--r-- | src/main/java/at/bitfire/vcard4android/BatchOperation.kt | 137 |
4 files changed, 175 insertions, 189 deletions
diff --git a/build.gradle b/build.gradle index 78f914a..981d36f 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.0.1' + classpath 'com.android.tools.build:gradle:4.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}" } @@ -30,12 +30,12 @@ apply plugin: 'kotlin-android' apply plugin: 'org.jetbrains.dokka' android { - compileSdkVersion 29 + compileSdkVersion 30 buildToolsVersion '30.0.2' defaultConfig { minSdkVersion 16 // Android 4.1 - targetSdkVersion 29 // Android 10 + targetSdkVersion 30 // Android 11 } compileOptions { @@ -81,7 +81,7 @@ dependencies { // noinspection GradleDependency implementation "org.apache.commons:commons-text:${versions.commonsText}" - // ez-vcard to parse/generate VCards + // ez-vcard to parse/generate vCards api('com.googlecode.ez-vcard:ez-vcard:0.11.0') { // hCard functionality not needed exclude group: 'org.jsoup' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1e2b225..d042ef0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip diff --git a/src/main/java/at/bitfire/vcard4android/AndroidContact.kt b/src/main/java/at/bitfire/vcard4android/AndroidContact.kt index 98bc509..8ab9a32 100644 --- a/src/main/java/at/bitfire/vcard4android/AndroidContact.kt +++ b/src/main/java/at/bitfire/vcard4android/AndroidContact.kt @@ -8,7 +8,6 @@ package at.bitfire.vcard4android -import android.content.ContentProviderOperation import android.content.ContentUris import android.content.ContentValues import android.content.EntityIterator @@ -549,20 +548,20 @@ open class AndroidContact( fun add(): Uri { val batch = BatchOperation(addressBook.provider!!) - val builder = ContentProviderOperation.newInsert(addressBook.syncAdapterURI(RawContacts.CONTENT_URI)) + val builder = BatchOperation.CpoBuilder.newInsert(addressBook.syncAdapterURI(RawContacts.CONTENT_URI)) buildContact(builder, false) - batch.enqueue(BatchOperation.Operation(builder)) + batch.enqueue(builder) insertDataRows(batch) batch.commit() - val result = batch.getResult(0) ?: throw ContactsStorageException("Empty result from content provider when adding contact") - id = ContentUris.parseId(result.uri) + val resultUri = batch.getResult(0)?.uri ?: throw ContactsStorageException("Empty result from content provider when adding contact") + id = ContentUris.parseId(resultUri) // we need a raw contact ID to insert the photo insertPhoto(contact!!.photo) - return result.uri + return resultUri } fun update(contact: Contact): Uri { @@ -570,16 +569,16 @@ open class AndroidContact( val batch = BatchOperation(addressBook.provider!!) val uri = rawContactSyncURI() - val builder = ContentProviderOperation.newUpdate(uri) + val builder = BatchOperation.CpoBuilder.newUpdate(uri) buildContact(builder, true) - batch.enqueue(BatchOperation.Operation(builder)) + batch.enqueue(builder) // Delete known data rows before adding the new ones. // - We don't delete group memberships. // - We'll only delete rows we have inserted so that unknown rows like // vnd.android.cursor.item/important_people (= contact is in Samsung "edge panel") remain untouched. - batch.enqueue(BatchOperation.Operation( - ContentProviderOperation.newDelete(dataSyncURI()) + batch.enqueue(BatchOperation.CpoBuilder + .newDelete(dataSyncURI()) .withSelection(Data.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + " IN (?,?,?,?,?,?,?,?,?,?,?,?,?)", arrayOf(id.toString(), @@ -596,7 +595,7 @@ open class AndroidContact( Event.CONTENT_ITEM_TYPE, Relation.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE)) - )) + ) insertDataRows(batch) batch.commit() @@ -609,7 +608,7 @@ open class AndroidContact( @CallSuper - protected open fun buildContact(builder: ContentProviderOperation.Builder, update: Boolean) { + protected open fun buildContact(builder: BatchOperation.CpoBuilder, update: Boolean) { if (!update) builder .withValue(RawContacts.ACCOUNT_NAME, addressBook.account.name) .withValue(RawContacts.ACCOUNT_TYPE, addressBook.account.type) @@ -664,15 +663,8 @@ open class AndroidContact( contact.phoneticGivenName == null && contact.phoneticMiddleName == null && contact.phoneticFamilyName == null) return - val op: BatchOperation.Operation - val builder = ContentProviderOperation.newInsert(dataSyncURI()) - if (id == null) - op = BatchOperation.Operation(builder, StructuredName.RAW_CONTACT_ID, 0) - else { - op = BatchOperation.Operation(builder) - builder.withValue(StructuredName.RAW_CONTACT_ID, id) - } - builder .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE) + val builder = insertDataBuilder(StructuredName.RAW_CONTACT_ID) + .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE) .withValue(StructuredName.DISPLAY_NAME, contact.displayName) .withValue(StructuredName.PREFIX, contact.prefix) .withValue(StructuredName.GIVEN_NAME, contact.givenName) @@ -683,11 +675,8 @@ open class AndroidContact( .withValue(StructuredName.PHONETIC_MIDDLE_NAME, contact.phoneticMiddleName) .withValue(StructuredName.PHONETIC_FAMILY_NAME, contact.phoneticFamilyName) - if (addressBook.readOnly) - builder.withValue(Data.IS_READ_ONLY, 1) - Constants.log.log(Level.FINER, "Built StructuredName data row", builder.build()) - batch.enqueue(op) + batch.enqueue(builder) } protected open fun insertPhoneNumber(batch: BatchOperation, labeledNumber: LabeledProperty<Telephone>) { @@ -765,26 +754,16 @@ open class AndroidContact( } } - val op: BatchOperation.Operation - val builder = ContentProviderOperation.newInsert(dataSyncURI()) - if (id == null) - op = BatchOperation.Operation(builder, Phone.RAW_CONTACT_ID, 0) - else { - op = BatchOperation.Operation(builder) - builder.withValue(Phone.RAW_CONTACT_ID, id) - } - builder .withValue(Phone.MIMETYPE, Phone.CONTENT_ITEM_TYPE) + val builder = insertDataBuilder(Phone.RAW_CONTACT_ID) + .withValue(Phone.MIMETYPE, Phone.CONTENT_ITEM_TYPE) .withValue(Phone.NUMBER, number.text) .withValue(Phone.TYPE, typeCode) .withValue(Phone.LABEL, typeLabel) .withValue(Phone.IS_PRIMARY, if (isPrimary) 1 else 0) .withValue(Phone.IS_SUPER_PRIMARY, if (isPrimary) 1 else 0) - if (addressBook.readOnly) - builder.withValue(Data.IS_READ_ONLY, 1) - Constants.log.log(Level.FINER, "Built Phone data row", builder.build()) - batch.enqueue(op) + batch.enqueue(builder) } protected open fun insertEmail(batch: BatchOperation, labeledEmail: LabeledProperty<ezvcard.property.Email>) { @@ -830,26 +809,16 @@ open class AndroidContact( } } - val op: BatchOperation.Operation - val builder = ContentProviderOperation.newInsert(dataSyncURI()) - if (id == null) - op = BatchOperation.Operation(builder, Email.RAW_CONTACT_ID, 0) - else { - op = BatchOperation.Operation(builder) - builder.withValue(Email.RAW_CONTACT_ID, id) - } - builder .withValue(Email.MIMETYPE, Email.CONTENT_ITEM_TYPE) + val builder = insertDataBuilder(Email.RAW_CONTACT_ID) + .withValue(Email.MIMETYPE, Email.CONTENT_ITEM_TYPE) .withValue(Email.ADDRESS, email.value) .withValue(Email.TYPE, typeCode) .withValue(Email.LABEL, typeLabel) .withValue(Email.IS_PRIMARY, if (isPrimary) 1 else 0) .withValue(Phone.IS_SUPER_PRIMARY, if (isPrimary) 1 else 0) - if (addressBook.readOnly) - builder.withValue(Data.IS_READ_ONLY, 1) - Constants.log.log(Level.FINER, "Built Email data row", builder.build()) - batch.enqueue(op) + batch.enqueue(builder) } protected open fun insertOrganization(batch: BatchOperation) { @@ -868,25 +837,15 @@ open class AndroidContact( department = org.next() } - val op: BatchOperation.Operation - val builder = ContentProviderOperation.newInsert(dataSyncURI()) - if (id == null) - op = BatchOperation.Operation(builder, Organization.RAW_CONTACT_ID, 0) - else { - op = BatchOperation.Operation(builder) - builder.withValue(Organization.RAW_CONTACT_ID, id) - } - builder .withValue(Organization.MIMETYPE, Organization.CONTENT_ITEM_TYPE) + val builder = insertDataBuilder(Organization.RAW_CONTACT_ID) + .withValue(Organization.MIMETYPE, Organization.CONTENT_ITEM_TYPE) .withValue(Organization.COMPANY, company) .withValue(Organization.DEPARTMENT, department) .withValue(Organization.TITLE, contact.jobTitle) .withValue(Organization.JOB_DESCRIPTION, contact.jobDescription) - if (addressBook.readOnly) - builder.withValue(Data.IS_READ_ONLY, 1) - Constants.log.log(Level.FINER, "Built Organization data row", builder.build()) - batch.enqueue(op) + batch.enqueue(builder) } protected open fun insertIMPP(batch: BatchOperation, labeledImpp: LabeledProperty<Impp>) { @@ -940,28 +899,19 @@ open class AndroidContact( } } - val op: BatchOperation.Operation - val builder = ContentProviderOperation.newInsert(dataSyncURI()) - if (id == null) - op = BatchOperation.Operation(builder, Im.RAW_CONTACT_ID, 0) - else { - op = BatchOperation.Operation(builder) - builder.withValue(Im.RAW_CONTACT_ID, id) - } - - if (addressBook.readOnly) - builder.withValue(Data.IS_READ_ONLY, 1) - + val builder: BatchOperation.CpoBuilder if (sipAddress) { // save as SIP address - builder .withValue(SipAddress.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE) + builder = insertDataBuilder(SipAddress.RAW_CONTACT_ID) + .withValue(SipAddress.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE) .withValue(SipAddress.DATA, impp.handle) .withValue(SipAddress.TYPE, typeCode) .withValue(SipAddress.LABEL, typeLabel) Constants.log.log(Level.FINER, "Built SipAddress data row", builder.build()) } else { // save as IM address - builder .withValue(Im.MIMETYPE, Im.CONTENT_ITEM_TYPE) + builder = insertDataBuilder(Im.RAW_CONTACT_ID) + .withValue(Im.MIMETYPE, Im.CONTENT_ITEM_TYPE) .withValue(Im.DATA, impp.handle) .withValue(Im.TYPE, typeCode) .withValue(Im.LABEL, typeLabel) @@ -969,7 +919,7 @@ open class AndroidContact( .withValue(Im.CUSTOM_PROTOCOL, protocolLabel) Constants.log.log(Level.FINER, "Built Im data row", builder.build()) } - batch.enqueue(op) + batch.enqueue(builder) } protected open fun insertNickname(batch: BatchOperation) { @@ -993,24 +943,14 @@ open class AndroidContact( } } - val op: BatchOperation.Operation - val builder = ContentProviderOperation.newInsert(dataSyncURI()) - if (id == null) - op = BatchOperation.Operation(builder, Nickname.RAW_CONTACT_ID, 0) - else { - op = BatchOperation.Operation(builder) - builder.withValue(Nickname.RAW_CONTACT_ID, id) - } - builder .withValue(Nickname.MIMETYPE, Nickname.CONTENT_ITEM_TYPE) + val builder = insertDataBuilder(Nickname.RAW_CONTACT_ID) + .withValue(Nickname.MIMETYPE, Nickname.CONTENT_ITEM_TYPE) .withValue(Nickname.NAME, nick.values.first()) .withValue(Nickname.TYPE, typeCode) .withValue(Nickname.LABEL, typeLabel) - if (addressBook.readOnly) - builder.withValue(Data.IS_READ_ONLY, 1) - Constants.log.log(Level.FINER, "Built Nickname data row", builder.build()) - batch.enqueue(op) + batch.enqueue(builder) } protected open fun insertNote(batch: BatchOperation) { @@ -1018,22 +958,12 @@ open class AndroidContact( if (contact.note.isNullOrEmpty()) return - val op: BatchOperation.Operation - val builder = ContentProviderOperation.newInsert(dataSyncURI()) - if (id == null) - op = BatchOperation.Operation(builder, Note.RAW_CONTACT_ID, 0) - else { - op = BatchOperation.Operation(builder) - builder.withValue(Note.RAW_CONTACT_ID, id) - } - builder .withValue(Note.MIMETYPE, Note.CONTENT_ITEM_TYPE) + val builder = insertDataBuilder(Note.RAW_CONTACT_ID) + .withValue(Note.MIMETYPE, Note.CONTENT_ITEM_TYPE) .withValue(Note.NOTE, contact.note) - if (addressBook.readOnly) - builder.withValue(Data.IS_READ_ONLY, 1) - Constants.log.log(Level.FINER, "Built Note data row", builder.build()) - batch.enqueue(op) + batch.enqueue(builder) } protected open fun insertStructuredPostal(batch: BatchOperation, labeledAddress: LabeledProperty<Address>) { @@ -1083,15 +1013,8 @@ open class AndroidContact( } } - val op: BatchOperation.Operation - val builder = ContentProviderOperation.newInsert(dataSyncURI()) - if (id == null) - op = BatchOperation.Operation(builder, StructuredPostal.RAW_CONTACT_ID, 0) - else { - op = BatchOperation.Operation(builder) - builder.withValue(StructuredPostal.RAW_CONTACT_ID, id) - } - builder .withValue(StructuredPostal.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE) + val builder = insertDataBuilder(StructuredPostal.RAW_CONTACT_ID) + .withValue(StructuredPostal.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE) .withValue(StructuredPostal.FORMATTED_ADDRESS, formattedAddress) .withValue(StructuredPostal.TYPE, typeCode) .withValue(StructuredPostal.LABEL, typeLabel) @@ -1103,11 +1026,8 @@ open class AndroidContact( .withValue(StructuredPostal.POSTCODE, address.postalCode) .withValue(StructuredPostal.COUNTRY, address.country) - if (addressBook.readOnly) - builder.withValue(Data.IS_READ_ONLY, 1) - Constants.log.log(Level.FINER, "Built StructuredPostal data row", builder.build()) - batch.enqueue(op) + batch.enqueue(builder) } protected open fun insertWebsite(batch: BatchOperation, labeledUrl: LabeledProperty<Url>) { @@ -1135,24 +1055,14 @@ open class AndroidContact( } } - val op: BatchOperation.Operation - val builder = ContentProviderOperation.newInsert(dataSyncURI()) - if (id == null) - op = BatchOperation.Operation(builder, Website.RAW_CONTACT_ID, 0) - else { - op = BatchOperation.Operation(builder) - builder.withValue(Website.RAW_CONTACT_ID, id) - } - builder .withValue(Website.MIMETYPE, Website.CONTENT_ITEM_TYPE) + val builder = insertDataBuilder(Website.RAW_CONTACT_ID) + .withValue(Website.MIMETYPE, Website.CONTENT_ITEM_TYPE) .withValue(Website.URL, url.value) .withValue(Website.TYPE, typeCode) .withValue(Website.LABEL, typeLabel) - if (addressBook.readOnly) - builder.withValue(Data.IS_READ_ONLY, 1) - Constants.log.log(Level.FINER, "Built Website data row", builder.build()) - batch.enqueue(op) + batch.enqueue(builder) } protected open fun insertEvent(batch: BatchOperation, type: Int, dateOrTime: DateOrTimeProperty) { @@ -1170,23 +1080,13 @@ open class AndroidContact( } } - val op: BatchOperation.Operation - val builder = ContentProviderOperation.newInsert(dataSyncURI()) - if (id == null) - op = BatchOperation.Operation(builder, Event.RAW_CONTACT_ID, 0) - else { - op = BatchOperation.Operation(builder) - builder.withValue(Event.RAW_CONTACT_ID, id) - } - builder .withValue(Event.MIMETYPE, Event.CONTENT_ITEM_TYPE) + val builder = insertDataBuilder(Event.RAW_CONTACT_ID) + .withValue(Event.MIMETYPE, Event.CONTENT_ITEM_TYPE) .withValue(Event.TYPE, type) .withValue(Event.START_DATE, dateStr) - if (addressBook.readOnly) - builder.withValue(Data.IS_READ_ONLY, 1) - - batch.enqueue(op) Constants.log.log(Level.FINER, "Built Event data row", builder.build()) + batch.enqueue(builder) } protected open fun insertRelation(batch: BatchOperation, related: Related) { @@ -1206,24 +1106,14 @@ open class AndroidContact( else -> labels += type.value } - val op: BatchOperation.Operation - val builder = ContentProviderOperation.newInsert(dataSyncURI()) - if (id == null) - op = BatchOperation.Operation(builder, Relation.RAW_CONTACT_ID, 0) - else { - op = BatchOperation.Operation(builder) - builder.withValue(Relation.RAW_CONTACT_ID, id) - } - builder .withValue(Relation.MIMETYPE, Relation.CONTENT_ITEM_TYPE) + val builder = insertDataBuilder(Relation.RAW_CONTACT_ID) + .withValue(Relation.MIMETYPE, Relation.CONTENT_ITEM_TYPE) .withValue(Relation.NAME, related.text) .withValue(Relation.TYPE, typeCode) .withValue(Relation.LABEL, StringUtils.trimToNull(labels.joinToString(", "))) - if (addressBook.readOnly) - builder.withValue(Data.IS_READ_ONLY, 1) - Constants.log.log(Level.FINER, "Built Relation data row", builder.build()) - batch.enqueue(op) + batch.enqueue(builder) } protected open fun insertPhoto(orig: ByteArray?) { @@ -1307,6 +1197,19 @@ open class AndroidContact( // helpers + protected fun insertDataBuilder(rawContactKeyName: String): BatchOperation.CpoBuilder { + val builder = BatchOperation.CpoBuilder.newInsert(dataSyncURI()) + if (id == null) + builder.withValueBackReference(rawContactKeyName, 0) + else + builder.withValue(rawContactKeyName, id) + + if (addressBook.readOnly) + builder.withValue(Data.IS_READ_ONLY, 1) + + return builder + } + protected open fun queryPhotoMaxDimensions(): Int { try { addressBook.provider?.query(ContactsContract.DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, diff --git a/src/main/java/at/bitfire/vcard4android/BatchOperation.kt b/src/main/java/at/bitfire/vcard4android/BatchOperation.kt index da09a35..8228230 100644 --- a/src/main/java/at/bitfire/vcard4android/BatchOperation.kt +++ b/src/main/java/at/bitfire/vcard4android/BatchOperation.kt @@ -9,6 +9,7 @@ package at.bitfire.vcard4android import android.content.* +import android.net.Uri import android.os.RemoteException import android.os.TransactionTooLargeException import java.util.* @@ -17,13 +18,24 @@ class BatchOperation( private val providerClient: ContentProviderClient ) { - private val queue = LinkedList<Operation>() + companion object { + + /** + * See https://android.googlesource.com/platform/packages/providers/ContactsProvider.git/+/refs/heads/android11-release/src/com/android/providers/contacts/AbstractContactsProvider.java#70 + * + * Some operations may count more than one operation, so use a safe value of 450 instead of 500. + */ + const val MAX_OPERATIONS_PER_YIELD_POINT = 450 + + } + + private val queue = LinkedList<CpoBuilder>() private var results = arrayOfNulls<ContentProviderResult?>(0) fun nextBackrefIdx() = queue.size - fun enqueue(operation: Operation) = queue.add(operation) + fun enqueue(operation: CpoBuilder) = queue.add(operation) fun commit(): Int { var affected = 0 @@ -36,7 +48,7 @@ class BatchOperation( for (result in results.filterNotNull()) when { - result.count != null -> affected += result.count + result.count != null -> affected += result.count ?: 0 result.uri != null -> affected += 1 } Constants.log.fine("… $affected record(s) affected") @@ -57,9 +69,9 @@ class BatchOperation( * Catches [TransactionTooLargeException] and splits the operations accordingly. * @param start index of first operation which will be run (inclusive) * @param end index of last operation which will be run (exclusive!) - * @throws RemoteException on contact provider errors + * @throws RemoteException on calendar provider errors * @throws OperationApplicationException when the batch can't be processed - * @throws ContactsStorageException if the transaction is too large + * @throws CalendarStorageException if the transaction is too large */ private fun runBatch(start: Int, end: Int) { if (end == start) @@ -90,34 +102,105 @@ class BatchOperation( private fun toCPO(start: Int, end: Int): ArrayList<ContentProviderOperation> { val cpo = ArrayList<ContentProviderOperation>(end - start) - for ((i, op) in queue.subList(start, end).withIndex()) { - val builder = op.builder - op.backrefKey?.let { key -> - if (op.backrefIdx < start) - // back reference is outside of the current batch - results[op.backrefIdx]?.let { result -> - builder .withValueBackReferences(null) - .withValue(key, ContentUris.parseId(result.uri)) - } - else - // back reference is in current batch, apply offset - builder.withValueBackReference(key, op.backrefIdx - start) + for ((i, cpoBuilder) in queue.subList(start, end).withIndex()) { + for ((backrefKey, backrefIdx) in cpoBuilder.valueBackrefs) { + if (backrefIdx < start) { + // back reference is outside of the current batch, get result from previous execution ... + val resultUri = results[backrefIdx]?.uri ?: throw ContactsStorageException("Referenced operation didn't produce a valid result") + val resultId = ContentUris.parseId(resultUri) + // ... and use result directly instead of using a back reference + cpoBuilder .removeValueBackReference(backrefKey) + .withValue(backrefKey, resultId) + } else + // back reference is in current batch, shift index + cpoBuilder.withValueBackReference(backrefKey, backrefIdx - start) } - // set a yield point at least every 300 operations - if (i % 300 == 0) - builder.withYieldAllowed(true) + if (i % MAX_OPERATIONS_PER_YIELD_POINT == MAX_OPERATIONS_PER_YIELD_POINT - 1) + cpoBuilder.yieldAllowed = true - cpo += builder.build() + cpo += cpoBuilder.build() } return cpo } - class Operation constructor( - val builder: ContentProviderOperation.Builder, - val backrefKey: String? = null, - val backrefIdx: Int = -1 - ) + /** + * Wrapper for [ContentProviderOperation.Builder] that allows to reset previously-set + * value back references. + */ + class CpoBuilder private constructor( + val uri: Uri, + val type: Type + ) { + + enum class Type { INSERT, UPDATE, DELETE } + + companion object { + + fun newInsert(uri: Uri) = CpoBuilder(uri, Type.INSERT) + fun newUpdate(uri: Uri) = CpoBuilder(uri, Type.UPDATE) + fun newDelete(uri: Uri) = CpoBuilder(uri, Type.DELETE) + + } + + + var selection: String? = null + var selectionArguments: Array<String>? = null + + val values = mutableMapOf<String, Any>() + val valueBackrefs = mutableMapOf<String, Int>() + + var yieldAllowed = false + + + fun withSelection(select: String, args: Array<String>): CpoBuilder { + selection = select + selectionArguments = args + return this + } + + fun withValueBackReference(key: String, index: Int): CpoBuilder { + valueBackrefs[key] = index + return this + } + + fun removeValueBackReference(key: String): CpoBuilder { + if (valueBackrefs.remove(key) == null) + throw IllegalArgumentException("$key was not set as value back reference") + return this + } + + fun withValue(key: String, value: Any?): CpoBuilder { + if (value != null) + values[key] = value + else + values -= key + return this + } + + + fun build(): ContentProviderOperation { + val builder = when (type) { + Type.INSERT -> ContentProviderOperation.newInsert(uri) + Type.UPDATE -> ContentProviderOperation.newUpdate(uri) + Type.DELETE -> ContentProviderOperation.newDelete(uri) + } + + if (selection != null) + builder.withSelection(selection, selectionArguments) + + for ((key, value) in values) + builder.withValue(key, value) + for ((key, index) in valueBackrefs) + builder.withValueBackReference(key, index) + + if (yieldAllowed) + builder.withYieldAllowed(true) + + return builder.build() + } + + } -} +}
\ No newline at end of file |