/* * 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.android.providers.contacts; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteTransactionListener; import android.util.Log; import com.google.android.collect.Lists; import com.google.android.collect.Maps; import java.util.List; import java.util.Map; /** * A transaction for interacting with a Contacts provider. This is used to pass state around * throughout the operations comprising the transaction, including which databases the overall * transaction is involved in, and whether the operation being performed is a batch operation. */ public class ContactsTransaction { /** * Whether this transaction is encompassing a batch of operations. If we're in batch mode, * transactional operations from non-batch callers are ignored. */ private final boolean mBatch; /** * The list of databases that have been enlisted in this transaction. * * Note we insert elements to the head of the list, so that we endTransaction() in the reverse * order. */ private final List mDatabasesForTransaction; /** * The mapping of tags to databases involved in this transaction. */ private final Map mDatabaseTagMap; /** * Whether any actual changes have been made successfully in this transaction. */ private boolean mIsDirty; /** * Whether a yield operation failed with an exception. If this occurred, we may not have a * lock on one of the databases that we started the transaction with (the yield code cleans * that up itself), so we should do an extra check before ending transactions. */ private boolean mYieldFailed; /** * Creates a new transaction object, optionally marked as a batch transaction. * @param batch Whether the transaction is in batch mode. */ public ContactsTransaction(boolean batch) { mBatch = batch; mDatabasesForTransaction = Lists.newArrayList(); mDatabaseTagMap = Maps.newHashMap(); mIsDirty = false; } public boolean isBatch() { return mBatch; } public boolean isDirty() { return mIsDirty; } public void markDirty() { mIsDirty = true; } public void markYieldFailed() { mYieldFailed = true; } /** * If the given database has not already been enlisted in this transaction, adds it to our * list of affected databases and starts a transaction on it. If we already have the given * database in this transaction, this is a no-op. * @param db The database to start a transaction on, if necessary. * @param tag A constant that can be used to retrieve the DB instance in this transaction. * @param listener A transaction listener to attach to this transaction. May be null. */ public void startTransactionForDb(SQLiteDatabase db, String tag, SQLiteTransactionListener listener) { if (AbstractContactsProvider.ENABLE_TRANSACTION_LOG) { Log.i(AbstractContactsProvider.TAG, "startTransactionForDb: db=" + db.getPath() + " tag=" + tag + " listener=" + listener + " startTransaction=" + !hasDbInTransaction(tag), new RuntimeException("startTransactionForDb")); } if (!hasDbInTransaction(tag)) { // Insert a new db into the head of the list, so that we'll endTransaction() in // the reverse order. mDatabasesForTransaction.add(0, db); mDatabaseTagMap.put(tag, db); if (listener != null) { db.beginTransactionWithListener(listener); } else { db.beginTransaction(); } } } /** * Returns whether DB corresponding to the given tag is currently enlisted in this transaction. */ public boolean hasDbInTransaction(String tag) { return mDatabaseTagMap.containsKey(tag); } /** * Retrieves the database enlisted in the transaction corresponding to the given tag. * @param tag The tag of the database to look up. * @return The database corresponding to the tag, or null if no database with that tag has been * enlisted in this transaction. */ public SQLiteDatabase getDbForTag(String tag) { return mDatabaseTagMap.get(tag); } /** * Removes the database corresponding to the given tag from this transaction. It is now the * caller's responsibility to do whatever needs to happen with this database - it is no longer * a part of this transaction. * @param tag The tag of the database to remove. * @return The database corresponding to the tag, or null if no database with that tag has been * enlisted in this transaction. */ public SQLiteDatabase removeDbForTag(String tag) { SQLiteDatabase db = mDatabaseTagMap.get(tag); mDatabaseTagMap.remove(tag); mDatabasesForTransaction.remove(db); return db; } /** * Marks all active DB transactions as successful. * @param callerIsBatch Whether this is being performed in the context of a batch operation. * If it is not, and the transaction is marked as batch, this call is a no-op. */ public void markSuccessful(boolean callerIsBatch) { if (!mBatch || callerIsBatch) { for (SQLiteDatabase db : mDatabasesForTransaction) { db.setTransactionSuccessful(); } } } /** * @return the tag for a database. Only intended to be used for logging. */ private String getTagForDb(SQLiteDatabase db) { for (String tag : mDatabaseTagMap.keySet()) { if (db == mDatabaseTagMap.get(tag)) { return tag; } } return null; } /** * Completes the transaction, ending the DB transactions for all associated databases. * @param callerIsBatch Whether this is being performed in the context of a batch operation. * If it is not, and the transaction is marked as batch, this call is a no-op. */ public void finish(boolean callerIsBatch) { if (AbstractContactsProvider.ENABLE_TRANSACTION_LOG) { Log.i(AbstractContactsProvider.TAG, "ContactsTransaction.finish callerIsBatch=" + callerIsBatch, new RuntimeException("ContactsTransaction.finish")); } if (!mBatch || callerIsBatch) { for (SQLiteDatabase db : mDatabasesForTransaction) { if (AbstractContactsProvider.ENABLE_TRANSACTION_LOG) { Log.i(AbstractContactsProvider.TAG, "ContactsTransaction.finish: " + "endTransaction for " + getTagForDb(db)); } // If an exception was thrown while yielding, it's possible that we no longer have // a lock on this database, so we need to check before attempting to end its // transaction. Otherwise, we should always expect to be in a transaction (and will // throw an exception if this is not the case). if (mYieldFailed && !db.isDbLockedByCurrentThread()) { // We no longer hold the lock, so don't do anything with this database. continue; } db.endTransaction(); } mDatabasesForTransaction.clear(); mDatabaseTagMap.clear(); mIsDirty = false; } } }