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 */ ContactsTransaction(boolean batch)71 public ContactsTransaction(boolean batch) { 72 mBatch = batch; 73 mDatabasesForTransaction = Lists.newArrayList(); 74 mDatabaseTagMap = Maps.newHashMap(); 75 mIsDirty = false; 76 } 77 isBatch()78 public boolean isBatch() { 79 return mBatch; 80 } 81 isDirty()82 public boolean isDirty() { 83 return mIsDirty; 84 } 85 markDirty()86 public void markDirty() { 87 mIsDirty = true; 88 } 89 markYieldFailed()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 */ startTransactionForDb(SQLiteDatabase db, String tag, SQLiteTransactionListener listener)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 */ hasDbInTransaction(String tag)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 */ getDbForTag(String tag)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 */ removeDbForTag(String tag)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 */ markSuccessful(boolean callerIsBatch)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 */ getTagForDb(SQLiteDatabase db)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 */ finish(boolean callerIsBatch)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