/* Copyright © 2013-2014, Silent Circle, LLC. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Any redistribution, use, or modification is done solely for personal benefit and not for any commercial purpose or for monetary gain * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Silent Circle nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SILENT CIRCLE, LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * This implementation is edited version of original Android sources. */ /* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.silentcircle.contacts.providers; import android.content.ContentProvider; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentValues; import android.content.Context; import android.content.OperationApplicationException; import android.net.Uri; import android.util.Log; import net.sqlcipher.database.SQLiteTransactionListener; import java.util.ArrayList; /** * A common base class for the contacts and profile providers. This handles much of the same * logic that SQLiteContentProvider does (i.e. starting transactions on the appropriate database), * but exposes awareness of batch operations to the subclass so that cross-database operations * can be supported. */ public abstract class AbstractContactsProvider extends ContentProvider implements SQLiteTransactionListener { public static final String TAG = "ContactsProvider"; public static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); /** Set true to enable detailed transaction logging. */ public static final boolean ENABLE_TRANSACTION_LOG = false; // Don't submit with true. /** * Duration in ms to sleep after successfully yielding the lock during a batch operation. */ protected static final int SLEEP_AFTER_YIELD_DELAY = 4000; /** * Maximum number of operations allowed in a batch between yield points. */ private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500; /** * Number of inserts performed in bulk to allow before yielding the transaction. */ private static final int BULK_INSERTS_PER_YIELD_POINT = 50; /** * The contacts transaction that is active in this thread. */ private ThreadLocal<ContactsTransaction> mTransactionHolder; /** * The DB helper to use for this content provider. */ private ScContactsDatabaseHelper mDbHelper; /** * The database helper to serialize all transactions on. If non-null, any new transaction * created by this provider will automatically retrieve a writable database from this helper * and initiate a transaction on that database. This should be used to ensure that operations * across multiple databases are all blocked on a single DB lock (to prevent deadlock cases). * * Hint: It's always {@link ScContactsDatabaseHelper}. * * TODO Change the structure to make it obvious that it's actually always set, and is the * {@link ScContactsDatabaseHelper}. */ private ScContactsDatabaseHelper mSerializeOnDbHelper; /** * The tag corresponding to the database used for serializing transactions. * * Hint: It's always the contacts db helper tag. * * See also the TODO on {@link #mSerializeOnDbHelper}. */ private String mSerializeDbTag; /** * The transaction listener used with {@link #mSerializeOnDbHelper}. * * Hint: It's always {@link ScContactsDatabaseHelper}. * * See also the TODO on {@link #mSerializeOnDbHelper}. */ private SQLiteTransactionListener mSerializedDbTransactionListener; @Override public boolean onCreate() { Context context = getContext(); mDbHelper = getDatabaseHelper(context); mTransactionHolder = getTransactionHolder(); return true; } public ScContactsDatabaseHelper getDatabaseHelper() { return mDbHelper; } /** * Specifies a database helper (and corresponding tag) to serialize all transactions on. * * See also the TODO on {@link #mSerializeOnDbHelper}. */ public void setDbHelperToSerializeOn(ScContactsDatabaseHelper serializeOnDbHelper, String tag, SQLiteTransactionListener listener) { mSerializeOnDbHelper = serializeOnDbHelper; mSerializeDbTag = tag; mSerializedDbTransactionListener = listener; } public ContactsTransaction getCurrentTransaction() { return mTransactionHolder.get(); } @Override public Uri insert(Uri uri, ContentValues values) { ContactsTransaction transaction = startTransaction(false); try { Uri result = insertInTransaction(uri, values); if (result != null) { transaction.markDirty(); } transaction.markSuccessful(false); return result; } finally { endTransaction(false); } } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { ContactsTransaction transaction = startTransaction(false); try { int deleted = deleteInTransaction(uri, selection, selectionArgs); if (deleted > 0) { transaction.markDirty(); } transaction.markSuccessful(false); return deleted; } finally { endTransaction(false); } } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { ContactsTransaction transaction = startTransaction(false); try { int updated = updateInTransaction(uri, values, selection, selectionArgs); if (updated > 0) { transaction.markDirty(); } transaction.markSuccessful(false); return updated; } finally { endTransaction(false); } } @Override public int bulkInsert(Uri uri, ContentValues[] values) { ContactsTransaction transaction = startTransaction(true); int numValues = values.length; int opCount = 0; try { for (int i = 0; i < numValues; i++) { insert(uri, values[i]); if (++opCount >= BULK_INSERTS_PER_YIELD_POINT) { opCount = 0; try { yield(transaction); } catch (RuntimeException re) { transaction.markYieldFailed(); throw re; } } } transaction.markSuccessful(true); } finally { endTransaction(true); } return numValues; } @Override public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) throws OperationApplicationException { if (VERBOSE_LOGGING) { Log.v(TAG, "applyBatch: " + operations.size() + " ops"); } int ypCount = 0; int opCount = 0; ContactsTransaction transaction = startTransaction(true); try { final int numOperations = operations.size(); final ContentProviderResult[] results = new ContentProviderResult[numOperations]; for (int i = 0; i < numOperations; i++) { if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) { throw new OperationApplicationException( "Too many content provider operations between yield points. " + "The maximum number of operations per yield point is " + MAX_OPERATIONS_PER_YIELD_POINT, ypCount); } final ContentProviderOperation operation = operations.get(i); if (i > 0 && operation.isYieldAllowed()) { if (VERBOSE_LOGGING) { Log.v(TAG, "applyBatch: " + opCount + " ops finished; about to yield..."); } opCount = 0; try { if (yield(transaction)) { ypCount++; } } catch (RuntimeException re) { transaction.markYieldFailed(); throw re; } } results[i] = operation.apply(this, results, i); } transaction.markSuccessful(true); return results; } finally { endTransaction(true); } } /** * If we are not yet already in a transaction, this starts one (on the DB to serialize on, if * present) and sets the thread-local transaction variable for tracking. If we are already in * a transaction, this returns that transaction, and the batch parameter is ignored. * @param callerIsBatch Whether the caller is operating in batch mode. */ private ContactsTransaction startTransaction(boolean callerIsBatch) { if (ENABLE_TRANSACTION_LOG) { Log.i(TAG, "startTransaction " + ((Object)this).getClass().getSimpleName() + " callerIsBatch=" + callerIsBatch, new RuntimeException("startTransaction")); } ContactsTransaction transaction = mTransactionHolder.get(); if (transaction == null) { transaction = new ContactsTransaction(callerIsBatch); if (mSerializeOnDbHelper != null) { transaction.startTransactionForDb(mSerializeOnDbHelper.getDatabase(true), mSerializeDbTag, mSerializedDbTransactionListener); } mTransactionHolder.set(transaction); } return transaction; } /** * Ends the current transaction and clears out the member variable. This does not set the * transaction as being successful. * @param callerIsBatch Whether the caller is operating in batch mode. */ private void endTransaction(boolean callerIsBatch) { if (ENABLE_TRANSACTION_LOG) { Log.i(TAG, "endTransaction " + ((Object)this).getClass().getSimpleName() + " callerIsBatch=" + callerIsBatch, new RuntimeException("endTransaction")); } ContactsTransaction transaction = mTransactionHolder.get(); if (transaction != null && (!transaction.isBatch() || callerIsBatch)) { try { if (transaction.isDirty()) { notifyChange(); } transaction.finish(callerIsBatch); } finally { // No matter what, make sure we clear out the thread-local transaction reference. mTransactionHolder.set(null); } } } /** * Gets the database helper for this contacts provider. This is called once, during onCreate(). */ protected abstract ScContactsDatabaseHelper getDatabaseHelper(Context context); /** * Gets the thread-local transaction holder to use for keeping track of the transaction. This * is called once, in onCreate(). If multiple classes are inheriting from this class that need * to be kept in sync on the same transaction, they must all return the same thread-local. */ protected abstract ThreadLocal<ContactsTransaction> getTransactionHolder(); protected abstract Uri insertInTransaction(Uri uri, ContentValues values); protected abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs); protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs); protected abstract boolean yield(ContactsTransaction transaction); protected abstract void notifyChange(); }