summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/db/AbstractTransactionalProvider.java
blob: 7e289b76fd31e8c8565098da3870959b0a35bdab (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.db;

import org.mozilla.gecko.AppConstants.Versions;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;

/**
 * This abstract class exists to capture some of the transaction-handling
 * commonalities in Fennec's DB layer.
 *
 * In particular, this abstracts DB access, batching, and a particular
 * transaction approach.
 *
 * That approach is: subclasses implement the abstract methods
 * {@link #insertInTransaction(android.net.Uri, android.content.ContentValues)},
 * {@link #deleteInTransaction(android.net.Uri, String, String[])}, and
 * {@link #updateInTransaction(android.net.Uri, android.content.ContentValues, String, String[])}.
 *
 * These are all called expecting a transaction to be established, so failed
 * modifications can be rolled-back, and work batched.
 *
 * If no transaction is established, that's not a problem. Transaction nesting
 * can be avoided by using {@link #beginWrite(SQLiteDatabase)}.
 *
 * The decision of when to begin a transaction is left to the subclasses,
 * primarily to avoid the pattern of a transaction being begun, a read occurring,
 * and then a write being necessary. This lock upgrade can result in SQLITE_BUSY,
 * which we don't handle well. Better to avoid starting a transaction too soon!
 *
 * You are probably interested in some subclasses:
 *
 * * {@link AbstractPerProfileDatabaseProvider} provides a simple abstraction for
 *   querying databases that are stored in the user's profile directory.
 * * {@link PerProfileDatabaseProvider} is a simple version that only allows a
 *   single ContentProvider to access each per-profile database.
 * * {@link SharedBrowserDatabaseProvider} is an example of a per-profile provider
 *   that allows for multiple providers to safely work with the same databases.
 */
@SuppressWarnings("javadoc")
public abstract class AbstractTransactionalProvider extends ContentProvider {
    private static final String LOGTAG = "GeckoTransProvider";

    private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
    private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);

    protected abstract SQLiteDatabase getReadableDatabase(Uri uri);
    protected abstract SQLiteDatabase getWritableDatabase(Uri uri);

    public abstract SQLiteDatabase getWritableDatabaseForTesting(Uri uri);

    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);

    /**
     * Track whether we're in a batch operation.
     *
     * When we're in a batch operation, individual write steps won't even try
     * to start a transaction... and neither will they attempt to finish one.
     *
     * Set this to <code>Boolean.TRUE</code> when you're entering a batch --
     * a section of code in which {@link ContentProvider} methods will be
     * called, but nested transactions should not be started. Callers are
     * responsible for beginning and ending the enclosing transaction, and
     * for setting this to <code>Boolean.FALSE</code> when done.
     *
     * This is a ThreadLocal separate from `db.inTransaction` because batched
     * operations start transactions independent of individual ContentProvider
     * operations. This doesn't work well with the entire concept of this
     * abstract class -- that is, automatically beginning and ending transactions
     * for each insert/delete/update operation -- and doing so without
     * causing arbitrary nesting requires external tracking.
     *
     * Note that beginWrite takes a DB argument, but we don't differentiate
     * between databases in this tracking flag. If your ContentProvider manages
     * multiple database transactions within the same thread, you'll need to
     * amend this scheme -- but then, you're already doing some serious wizardry,
     * so rock on.
     */
    final ThreadLocal<Boolean> isInBatchOperation = new ThreadLocal<Boolean>();

    private boolean isInBatch() {
        final Boolean isInBatch = isInBatchOperation.get();
        if (isInBatch == null) {
            return false;
        }

        return isInBatch;
    }

    /**
     * If we're not currently in a transaction, and we should be, start one.
     */
    protected void beginWrite(final SQLiteDatabase db) {
        if (isInBatch()) {
            trace("Not bothering with an intermediate write transaction: inside batch operation.");
            return;
        }

        if (!db.inTransaction()) {
            trace("beginWrite: beginning transaction.");
            db.beginTransaction();
        }
    }

    /**
     * If we're not in a batch, but we are in a write transaction, mark it as
     * successful.
     */
    protected void markWriteSuccessful(final SQLiteDatabase db) {
        if (isInBatch()) {
            trace("Not marking write successful: inside batch operation.");
            return;
        }

        if (db.inTransaction()) {
            trace("Marking write transaction successful.");
            db.setTransactionSuccessful();
        }
    }

    /**
     * If we're not in a batch, but we are in a write transaction,
     * end it.
     *
     * @see PerProfileDatabaseProvider#markWriteSuccessful(SQLiteDatabase)
     */
    protected void endWrite(final SQLiteDatabase db) {
        if (isInBatch()) {
            trace("Not ending write: inside batch operation.");
            return;
        }

        if (db.inTransaction()) {
            trace("endWrite: ending transaction.");
            db.endTransaction();
        }
    }

    protected void beginBatch(final SQLiteDatabase db) {
        trace("Beginning batch.");
        isInBatchOperation.set(Boolean.TRUE);
        db.beginTransaction();
    }

    protected void markBatchSuccessful(final SQLiteDatabase db) {
        if (isInBatch()) {
            trace("Marking batch successful.");
            db.setTransactionSuccessful();
            return;
        }
        Log.w(LOGTAG, "Unexpectedly asked to mark batch successful, but not in batch!");
        throw new IllegalStateException("Not in batch.");
    }

    protected void endBatch(final SQLiteDatabase db) {
        trace("Ending batch.");
        db.endTransaction();
        isInBatchOperation.set(Boolean.FALSE);
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        trace("Calling delete on URI: " + uri + ", " + selection + ", " + selectionArgs);

        final SQLiteDatabase db = getWritableDatabase(uri);
        int deleted = 0;

        try {
            deleted = deleteInTransaction(uri, selection, selectionArgs);
            markWriteSuccessful(db);
        } finally {
            endWrite(db);
        }

        if (deleted > 0) {
            final boolean shouldSyncToNetwork = !isCallerSync(uri);
            getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
        }

        return deleted;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        trace("Calling insert on URI: " + uri);

        final SQLiteDatabase db = getWritableDatabase(uri);
        Uri result = null;
        try {
            result = insertInTransaction(uri, values);
            markWriteSuccessful(db);
        } catch (SQLException sqle) {
            Log.e(LOGTAG, "exception in DB operation", sqle);
        } catch (UnsupportedOperationException uoe) {
            Log.e(LOGTAG, "don't know how to perform that insert", uoe);
        } finally {
            endWrite(db);
        }

        if (result != null) {
            final boolean shouldSyncToNetwork = !isCallerSync(uri);
            getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
        }

        return result;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        trace("Calling update on URI: " + uri + ", " + selection + ", " + selectionArgs);

        final SQLiteDatabase db = getWritableDatabase(uri);
        int updated = 0;

        try {
            updated = updateInTransaction(uri, values, selection,
                                          selectionArgs);
            markWriteSuccessful(db);
        } finally {
            endWrite(db);
        }

        if (updated > 0) {
            final boolean shouldSyncToNetwork = !isCallerSync(uri);
            getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
        }

        return updated;
    }

    @Override
    public int bulkInsert(Uri uri, ContentValues[] values) {
        if (values == null) {
            return 0;
        }

        int numValues = values.length;
        int successes = 0;

        final SQLiteDatabase db = getWritableDatabase(uri);

        debug("bulkInsert: explicitly starting transaction.");
        beginBatch(db);

        try {
            for (int i = 0; i < numValues; i++) {
                insertInTransaction(uri, values[i]);
                successes++;
            }
            trace("Flushing DB bulkinsert...");
            markBatchSuccessful(db);
        } finally {
            debug("bulkInsert: explicitly ending transaction.");
            endBatch(db);
        }

        if (successes > 0) {
            final boolean shouldSyncToNetwork = !isCallerSync(uri);
            getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
        }

        return successes;
    }

    /**
     * Indicates whether a query should include deleted fields
     * based on the URI.
     * @param uri query URI
     */
    protected static boolean shouldShowDeleted(Uri uri) {
        String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED);
        return !TextUtils.isEmpty(showDeleted);
    }

    /**
     * Indicates whether an insertion should be made if a record doesn't
     * exist, based on the URI.
     * @param uri query URI
     */
    protected static boolean shouldUpdateOrInsert(Uri uri) {
        String insertIfNeeded = uri.getQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED);
        return Boolean.parseBoolean(insertIfNeeded);
    }

    /**
     * Indicates whether query is a test based on the URI.
     * @param uri query URI
     */
    protected static boolean isTest(Uri uri) {
        if (uri == null) {
            return false;
        }
        String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST);
        return !TextUtils.isEmpty(isTest);
    }

    /**
     * Return true of the query is from Firefox Sync.
     * @param uri query URI
     */
    protected static boolean isCallerSync(Uri uri) {
        String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
        return !TextUtils.isEmpty(isSync);
    }

    protected static void trace(String message) {
        if (logVerbose) {
            Log.v(LOGTAG, message);
        }
    }

    protected static void debug(String message) {
        if (logDebug) {
            Log.d(LOGTAG, message);
        }
    }
}