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.beginTransactionWithListenerNonExclusive(listener);
117             } else {
118                 db.beginTransactionNonExclusive();
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