Overview

ContentProvider is one of the core components of the Android system. It encapsulates the data access interface, and the underlying data is generally stored in the database or in the cloud.

In most cases, you don’t really need a ContentProvider, and if your application interacts with something else, you can just use the SQLite database (check the official documentation before using ContentProvider to decide if you really want to use it). But since there are some open source libraries for ORM, we don’t even do the database ourselves.

Contentproviders and ContentResolver provide a basic interface that meets most of our needs. However, when dealing with a large amount of data, we can choose to call the corresponding function of the ContentResolver multiple times or use the batch operation. Of course the latter performance will be better.

bulkInsert

If only a single table is involved in bulk inserts, we can simply use bulkInsert(Uri Uri, ContentValues[] values) for bulk inserts.

ContentProviderOperation

In order to make batch update, insert, delete data more convenient, Android system introduced the ContentProviderOperation class. In development of official document recommended ContentProviderOperations, there is a reason:

  1. All operations are performed in a single transaction to ensure data integrity
  2. Because batch operations are performed within a transaction, only one transaction needs to be opened and closed, which is better than opening and closing multiple transactions
  3. Using batch operations reduces context switching between applications and Content Providers compared to multiple single operations, which also improves application performance, reduces CPU time, and of course reduces battery consumption.

ContentProviderOperation.Builder

To create a ContentProviderOperation objects, you need to use ContentProviderOperation. Builder class, by calling the following several static function to obtain a Builder object:

function use
newInsert Create a Builder to perform insert operations (support multiple tables)
newUpdate Create a Builder to perform the update operation
newDelete Create a Builder to perform the delete operation
newAssertQuery Can be understood as a breakpoint query, namely query have qualified data, if not, throws a OperationApplicationException anomalies

This Buidler object uses the famous Builder design pattern. Since the Builder object’s functions all return themselves, the final ContentProviderOperation object is generated through a series of function chain calls.

    /* * Prepares the batch operation for inserting a new raw contact and its data. Even if * the Contacts Provider does not  have any data for this person, you can't add a Contact, * only a raw contact. The Contacts Provider will then add a Contact automatically. */

     // Creates a new array of ContentProviderOperation objects.
    ArrayList<ContentProviderOperation> ops =
            new ArrayList<ContentProviderOperation>();

    /* * Creates a new raw contact with its account type (server type) and account name * (user's account). Remember that the display name is not stored in this row, but in a * StructuredName data row. No other data is required. */
    ContentProviderOperation.Builder op =
            ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
            .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, mSelectedAccount.getType())
            .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, mSelectedAccount.getName());

    // Builds the operation and adds it to the array of operations
    ops.add(op.build());Copy the code

Of course, you can also use the familiar ContentValues object, withValues(values).

The core function of Builder

The core functions of the Builder object (see the API directly) :

function use
withSelection (String selection, String[] selectionArgs) Specify the data condition to operate on. Can only be used for Update, delete, or Assert.
withSelectionBackReference(int selectionArgIndex, int previousResult) Add a “backreference” as the query condition. The value of the selectionArgIndex position previously specified withSelection(String, String[]) is overwritten. Can only be used for Update, delete, or Assert.
withValue (String key, Object value) Defining a column of data values is similar to adding a column of data to ConetentValues. Can only be used for insert, update, or Assert.
withValues (ContentValues values) Define data values for multiple columns. Can only be used for insert, update, or Assert
withValueBackReference(String key, int previousResult) Add a “backward reference”. Set the value of the specified “key” column using the value in “backreference”,A backreference is the ContentProviderResult returned after the first previousResult of a set of operations is completed. The ID in the URI returned by ContentProviderOperation is used for insert, and the count returned by Update or Assert. This value overrides the previous withValues(ContentValues) setting. Can only be used for insert, update, or Assert.
withValueBackReferences(ContentValues backReferences) Add a “backward reference”. Use ContentValues to perform multiple withValueBackReference operations. The key and value in ContentValues correspond to “column name” and “index of previousResult”, seewithValueBackReferenceThe parameters. Value will be added as String. This value overrides the previous withValues(ContentValues) setting. Can only be used for insert, update, or Assert.
withExpectedCount(int count) Verify influence the number of rows, if the count is not the same, will be thrown OperationApplicationException. Can only be used for UPDATE, DELETE, or Assert operations.

You can refer to the website for instructions on “backreferencing”

Finally, the applyBatch() function of ContentResolver applies the batch operation:

try {
   getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
} catch (RemoteException e) {
   // do s.th.
} catch (OperationApplicationException e) {
   // do s.th.
}Copy the code

The working principle of

From the ContentProviderOperation. Builder build () method, you can see constructed a new ContentProviderOperation ().

        /** Create a ContentProviderOperation from this {@link Builder}. */
        public ContentProviderOperation build(a) {
            if (mType == TYPE_UPDATE) {
                if ((mValues == null || mValues.size() == 0)
                        && (mValuesBackReferences == null || mValuesBackReferences.size() == 0)) {
                    throw new IllegalArgumentException("Empty values"); }}if (mType == TYPE_ASSERT) {
                if ((mValues == null || mValues.size() == 0)
                        && (mValuesBackReferences == null || mValuesBackReferences.size() == 0)
                        && (mExpectedCount == null)) {
                    throw new IllegalArgumentException("Empty values"); }}return new ContentProviderOperation(this);
        }Copy the code

Batch operation from getContentResolver (). ApplyBatch (ContactsContract. AUTHORITY, operationList), Call will eventually go the ContentProvider. ApplyBatch (), this method did two things:

  1. Defines a ContentProviderResult[] array whose size is equal to the size of Operations. ContentProviderResult Is used to hold the execution result of each ContentProviderOperation. The ContentProviderResult is of two types, a concrete “URI” and the number of “count” rows affected by the operation, which is eventually used in “backreference”.
  2. Traversal operations, and invoke the corresponding ContentProviderOperation. Apply the operation, the results back to the corresponding ContentProviderResult [] stored in an array.
    public @NonNull ContentProviderResult[] applyBatch(
            @NonNull ArrayList<ContentProviderOperation> operations)
                    throws OperationApplicationException {
        final int numOperations = operations.size();
        final ContentProviderResult[] results = new ContentProviderResult[numOperations];
        for (int i = 0; i < numOperations; i++) {
            results[i] = operations.get(i).apply(this, results, i);
        }
        return results;
    }Copy the code

The ContentProviderOperation. Apply method, there are several important steps:

  1. First call resolveValueBackReferences (), ContentValue of dealing with the “reference” back.
  2. Call again resolveSelectionArgsBackReferences, dealing with “backward reference” query parameter.
  3. Insert is called directly and the URI returned by insert is assigned to new ContentProviderResult(newUri).
  4. Provider. Delete, provider. Update, and assign numRows to new ContentProviderResult(numRows).
  5. As opposed to AssertQuery, provider.query directly compares the queried data with the expected Values and returns the corresponding new ContentProviderResult(numRows) if the Values are the same. If not, declare Exception.
  6. If mExpectedCount isn’t empty (set up withExpectedCount (int count)), comparing with numRows, determine whether the expected value and the actual operation value, is not the same so OperationApplicationException.
    public ContentProviderResult apply(ContentProvider provider, ContentProviderResult[] backRefs,
            int numBackRefs) throws OperationApplicationException {
        ContentValues values = resolveValueBackReferences(backRefs, numBackRefs);
        String[] selectionArgs =
                resolveSelectionArgsBackReferences(backRefs, numBackRefs);

        if (mType == TYPE_INSERT) {
            Uri newUri = provider.insert(mUri, values);
            if (newUri == null) {
                throw new OperationApplicationException("insert failed");
            }
            return new ContentProviderResult(newUri);
        }

        int numRows;
        if (mType == TYPE_DELETE) {
            numRows = provider.delete(mUri, mSelection, selectionArgs);
        } else if (mType == TYPE_UPDATE) {
            numRows = provider.update(mUri, values, mSelection, selectionArgs);
        } else if (mType == TYPE_ASSERT) {
            // Assert that all rows match expected values
            String[] projection =  null;
            if(values ! =null) {
                // Build projection map from expected values
                final ArrayList<String> projectionList = new ArrayList<String>();
                for (Map.Entry<String, Object> entry : values.valueSet()) {
                    projectionList.add(entry.getKey());
                }
                projection = projectionList.toArray(new String[projectionList.size()]);
            }
            final Cursor cursor = provider.query(mUri, projection, mSelection, selectionArgs, null);
            try {
                numRows = cursor.getCount();
                if(projection ! =null) {
                    while (cursor.moveToNext()) {
                        for (int i = 0; i < projection.length; i++) {
                            final String cursorValue = cursor.getString(i);
                            final String expectedValue = values.getAsString(projection[i]);
                            if(! TextUtils.equals(cursorValue, expectedValue)) {// Throw exception when expected values don't match
                                Log.e(TAG, this.toString());
                                throw new OperationApplicationException("Found value " + cursorValue
                                        + " when expected " + expectedValue + " for column "
                                        + projection[i]);
                            }
                        }
                    }
                }
            } finally{ cursor.close(); }}else {
            Log.e(TAG, this.toString());
            throw new IllegalStateException("bad type, " + mType);
        }

        if(mExpectedCount ! =null&& mExpectedCount ! = numRows) { Log.e(TAG,this.toString());
            throw new OperationApplicationException("wrong number of rows: " + numRows);
        }

        return new ContentProviderResult(numRows);
    }Copy the code

In resolveValueBackReferences approach will determine whether mValuesBackReferences is empty, if the null is returned directly mValues, MValues are ContentValue objects that are assembled from values filled in withValue or withValues methods, such as values to update or insert. If mValuesBackReferences! = null (withValueBackReference or withValueBackReferences is used), then the “backreference” value needs to be handled, Find the value of the ContentProviderResult returned by the “previousResult” completed ContentProviderOperation and bind it to the corresponding key. Looking for “backreferences” is done in the backRefToValue function, as you can see below.

ResolveSelectionArgsBackReferences functions are similar.

    public ContentValues resolveValueBackReferences(
            ContentProviderResult[] backRefs, int numBackRefs) {
        if (mValuesBackReferences == null) {
            return mValues;
        }
        final ContentValues values;
        if (mValues == null) {
            values = new ContentValues();
        } else {
            values = new ContentValues(mValues);
        }
        for (Map.Entry<String, Object> entry : mValuesBackReferences.valueSet()) {
            String key = entry.getKey();
            Integer backRefIndex = mValuesBackReferences.getAsInteger(key);
            if (backRefIndex == null) {
                Log.e(TAG, this.toString());
                throw new IllegalArgumentException("values backref " + key + " is not an integer");
            }
            values.put(key, backRefToValue(backRefs, numBackRefs, backRefIndex));
        }
        return values;
    }Copy the code

BackRefToValue handles two cases: if the URI in ContentProviderResult is not empty, the ID of the URI is returned; Return the count value if it is null. So, as you can see from the apply function above, insert corresponds to ID; Delete, Update, and assertQuery return count.

    private long backRefToValue(ContentProviderResult[] backRefs, int numBackRefs,
            Integer backRefIndex) {
        if (backRefIndex >= numBackRefs) {
            Log.e(TAG, this.toString());
            throw new ArrayIndexOutOfBoundsException("asked for back ref " + backRefIndex
                    + " but there are only " + numBackRefs + " back refs");
        }
        ContentProviderResult backRef = backRefs[backRefIndex];
        long backRefValue;
        if(backRef.uri ! =null) {
            backRefValue = ContentUris.parseId(backRef.uri);
        } else {
            backRefValue = backRef.count;
        }
        return backRefValue;
    }Copy the code

With a transaction

Refer to the implementation of mediaprovider.java to use transactions in applyBatch:

  @NonNull
    @Override
    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) throws OperationApplicationException {
        SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
        db.beginTransaction();
        try {
            ContentProviderResult[] results = super.applyBatch(operations);
            db.setTransactionSuccessful();
            return results;
        } finally{ db.endTransaction(); }}Copy the code

reference

The Android contact provider synchronization adapter Gibhub Sample Code Stackoverflow On withValueBackReference solution Android ‘s ContentProviderOperation: “WithBackReference” explained Android uses ContentProviderOperation to add contacts