Home | History | Annotate | Download | only in contacts
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.providers.contacts;
     18 
     19 import android.database.sqlite.SQLiteDatabase;
     20 import android.database.sqlite.SQLiteTransactionListener;
     21 import android.util.Log;
     22 
     23 import com.google.android.collect.Lists;
     24 import com.google.android.collect.Maps;
     25 
     26 import java.util.List;
     27 import java.util.Map;
     28 
     29 /**
     30  * A transaction for interacting with a Contacts provider.  This is used to pass state around
     31  * throughout the operations comprising the transaction, including which databases the overall
     32  * transaction is involved in, and whether the operation being performed is a batch operation.
     33  */
     34 public class ContactsTransaction {
     35 
     36     /**
     37      * Whether this transaction is encompassing a batch of operations.  If we're in batch mode,
     38      * transactional operations from non-batch callers are ignored.
     39      */
     40     private final boolean mBatch;
     41 
     42     /**
     43      * The list of databases that have been enlisted in this transaction.
     44      *
     45      * Note we insert elements to the head of the list, so that we endTransaction() in the reverse
     46      * order.
     47      */
     48     private final List<SQLiteDatabase> mDatabasesForTransaction;
     49 
     50     /**
     51      * The mapping of tags to databases involved in this transaction.
     52      */
     53     private final Map<String, SQLiteDatabase> mDatabaseTagMap;
     54 
     55     /**
     56      * Whether any actual changes have been made successfully in this transaction.
     57      */
     58     private boolean mIsDirty;
     59 
     60     /**
     61      * Whether a yield operation failed with an exception.  If this occurred, we may not have a
     62      * lock on one of the databases that we started the transaction with (the yield code cleans
     63      * that up itself), so we should do an extra check before ending transactions.
     64      */
     65     private boolean mYieldFailed;
     66 
     67     /**
     68      * Creates a new transaction object, optionally marked as a batch transaction.
     69      * @param batch Whether the transaction is in batch mode.
     70      */
     71     public ContactsTransaction(boolean batch) {
     72         mBatch = batch;
     73         mDatabasesForTransaction = Lists.newArrayList();
     74         mDatabaseTagMap = Maps.newHashMap();
     75         mIsDirty = false;
     76     }
     77 
     78     public boolean isBatch() {
     79         return mBatch;
     80     }
     81 
     82     public boolean isDirty() {
     83         return mIsDirty;
     84     }
     85 
     86     public void markDirty() {
     87         mIsDirty = true;
     88     }
     89 
     90     public void markYieldFailed() {
     91         mYieldFailed = true;
     92     }
     93 
     94     /**
     95      * If the given database has not already been enlisted in this transaction, adds it to our
     96      * list of affected databases and starts a transaction on it.  If we already have the given
     97      * database in this transaction, this is a no-op.
     98      * @param db The database to start a transaction on, if necessary.
     99      * @param tag A constant that can be used to retrieve the DB instance in this transaction.
    100      * @param listener A transaction listener to attach to this transaction.  May be null.
    101      */
    102     public void startTransactionForDb(SQLiteDatabase db, String tag,
    103             SQLiteTransactionListener listener) {
    104         if (AbstractContactsProvider.ENABLE_TRANSACTION_LOG) {
    105             Log.i(AbstractContactsProvider.TAG, "startTransactionForDb: db=" + db.getPath() +
    106                     "  tag=" + tag + "  listener=" + listener +
    107                     "  startTransaction=" + !hasDbInTransaction(tag),
    108                     new RuntimeException("startTransactionForDb"));
    109         }
    110         if (!hasDbInTransaction(tag)) {
    111             // Insert a new db into the head of the list, so that we'll endTransaction() in
    112             // the reverse order.
    113             mDatabasesForTransaction.add(0, db);
    114             mDatabaseTagMap.put(tag, db);
    115             if (listener != null) {
    116                 db.beginTransactionWithListener(listener);
    117             } else {
    118                 db.beginTransaction();
    119             }
    120         }
    121     }
    122 
    123     /**
    124      * Returns whether DB corresponding to the given tag is currently enlisted in this transaction.
    125      */
    126     public boolean hasDbInTransaction(String tag) {
    127         return mDatabaseTagMap.containsKey(tag);
    128     }
    129 
    130     /**
    131      * Retrieves the database enlisted in the transaction corresponding to the given tag.
    132      * @param tag The tag of the database to look up.
    133      * @return The database corresponding to the tag, or null if no database with that tag has been
    134      *     enlisted in this transaction.
    135      */
    136     public SQLiteDatabase getDbForTag(String tag) {
    137         return mDatabaseTagMap.get(tag);
    138     }
    139 
    140     /**
    141      * Removes the database corresponding to the given tag from this transaction.  It is now the
    142      * caller's responsibility to do whatever needs to happen with this database - it is no longer
    143      * a part of this transaction.
    144      * @param tag The tag of the database to remove.
    145      * @return The database corresponding to the tag, or null if no database with that tag has been
    146      *     enlisted in this transaction.
    147      */
    148     public SQLiteDatabase removeDbForTag(String tag) {
    149         SQLiteDatabase db = mDatabaseTagMap.get(tag);
    150         mDatabaseTagMap.remove(tag);
    151         mDatabasesForTransaction.remove(db);
    152         return db;
    153     }
    154 
    155     /**
    156      * Marks all active DB transactions as successful.
    157      * @param callerIsBatch Whether this is being performed in the context of a batch operation.
    158      *     If it is not, and the transaction is marked as batch, this call is a no-op.
    159      */
    160     public void markSuccessful(boolean callerIsBatch) {
    161         if (!mBatch || callerIsBatch) {
    162             for (SQLiteDatabase db : mDatabasesForTransaction) {
    163                 db.setTransactionSuccessful();
    164             }
    165         }
    166     }
    167 
    168     /**
    169      * @return the tag for a database.  Only intended to be used for logging.
    170      */
    171     private String getTagForDb(SQLiteDatabase db) {
    172         for (String tag : mDatabaseTagMap.keySet()) {
    173             if (db == mDatabaseTagMap.get(tag)) {
    174                 return tag;
    175             }
    176         }
    177         return null;
    178     }
    179 
    180     /**
    181      * Completes the transaction, ending the DB transactions for all associated databases.
    182      * @param callerIsBatch Whether this is being performed in the context of a batch operation.
    183      *     If it is not, and the transaction is marked as batch, this call is a no-op.
    184      */
    185     public void finish(boolean callerIsBatch) {
    186         if (AbstractContactsProvider.ENABLE_TRANSACTION_LOG) {
    187             Log.i(AbstractContactsProvider.TAG, "ContactsTransaction.finish  callerIsBatch=" +
    188                     callerIsBatch, new RuntimeException("ContactsTransaction.finish"));
    189         }
    190         if (!mBatch || callerIsBatch) {
    191             for (SQLiteDatabase db : mDatabasesForTransaction) {
    192                 if (AbstractContactsProvider.ENABLE_TRANSACTION_LOG) {
    193                     Log.i(AbstractContactsProvider.TAG, "ContactsTransaction.finish: " +
    194                             "endTransaction for " + getTagForDb(db));
    195                 }
    196                 // If an exception was thrown while yielding, it's possible that we no longer have
    197                 // a lock on this database, so we need to check before attempting to end its
    198                 // transaction.  Otherwise, we should always expect to be in a transaction (and will
    199                 // throw an exception if this is not the case).
    200                 if (mYieldFailed && !db.isDbLockedByCurrentThread()) {
    201                     // We no longer hold the lock, so don't do anything with this database.
    202                     continue;
    203                 }
    204                 db.endTransaction();
    205             }
    206             mDatabasesForTransaction.clear();
    207             mDatabaseTagMap.clear();
    208             mIsDirty = false;
    209         }
    210     }
    211 }
    212