1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/db/SharedBrowserDatabaseProvider.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,115 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.db; 1.9 + 1.10 +import org.mozilla.gecko.db.BrowserContract.CommonColumns; 1.11 +import org.mozilla.gecko.db.BrowserContract.SyncColumns; 1.12 +import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory; 1.13 + 1.14 +import android.content.Context; 1.15 +import android.database.Cursor; 1.16 +import android.database.sqlite.SQLiteDatabase; 1.17 +import android.net.Uri; 1.18 +import android.util.Log; 1.19 + 1.20 +/** 1.21 + * A ContentProvider subclass that provides per-profile browser.db access 1.22 + * that can be safely shared between multiple providers. 1.23 + * 1.24 + * If multiple ContentProvider classes wish to share a database, it's 1.25 + * vitally important that they use the same SQLiteOpenHelpers for access. 1.26 + * 1.27 + * Failure to do so can cause accidental concurrent writes, with the result 1.28 + * being unexpected SQLITE_BUSY errors. 1.29 + * 1.30 + * This class provides a static {@link PerProfileDatabases} instance, lazily 1.31 + * initialized within {@link SharedBrowserDatabaseProvider#onCreate()}. 1.32 + */ 1.33 +public abstract class SharedBrowserDatabaseProvider extends AbstractPerProfileDatabaseProvider { 1.34 + private static final String LOGTAG = SharedBrowserDatabaseProvider.class.getSimpleName(); 1.35 + 1.36 + private static PerProfileDatabases<BrowserDatabaseHelper> databases; 1.37 + 1.38 + @Override 1.39 + protected PerProfileDatabases<BrowserDatabaseHelper> getDatabases() { 1.40 + return databases; 1.41 + } 1.42 + 1.43 + @Override 1.44 + public boolean onCreate() { 1.45 + // If necessary, do the shared DB work. 1.46 + synchronized (SharedBrowserDatabaseProvider.class) { 1.47 + if (databases != null) { 1.48 + return true; 1.49 + } 1.50 + 1.51 + final DatabaseHelperFactory<BrowserDatabaseHelper> helperFactory = new DatabaseHelperFactory<BrowserDatabaseHelper>() { 1.52 + @Override 1.53 + public BrowserDatabaseHelper makeDatabaseHelper(Context context, String databasePath) { 1.54 + return new BrowserDatabaseHelper(context, databasePath); 1.55 + } 1.56 + }; 1.57 + 1.58 + databases = new PerProfileDatabases<BrowserDatabaseHelper>(getContext(), BrowserDatabaseHelper.DATABASE_NAME, helperFactory); 1.59 + } 1.60 + 1.61 + return true; 1.62 + } 1.63 + 1.64 + /** 1.65 + * Clean up some deleted records from the specified table. 1.66 + * 1.67 + * If called in an existing transaction, it is the caller's responsibility 1.68 + * to ensure that the transaction is already upgraded to a writer, because 1.69 + * this method issues a read followed by a write, and thus is potentially 1.70 + * vulnerable to an unhandled SQLITE_BUSY failure during the upgrade. 1.71 + * 1.72 + * If not called in an existing transaction, no new explicit transaction 1.73 + * will be begun. 1.74 + */ 1.75 + protected void cleanUpSomeDeletedRecords(Uri fromUri, String tableName) { 1.76 + Log.d(LOGTAG, "Cleaning up deleted records from " + tableName); 1.77 + 1.78 + // We clean up records marked as deleted that are older than a 1.79 + // predefined max age. It's important not be too greedy here and 1.80 + // remove only a few old deleted records at a time. 1.81 + 1.82 + // we cleanup records marked as deleted that are older than a 1.83 + // predefined max age. It's important not be too greedy here and 1.84 + // remove only a few old deleted records at a time. 1.85 + 1.86 + // Maximum age of deleted records to be cleaned up (20 days in ms) 1.87 + final long MAX_AGE_OF_DELETED_RECORDS = 86400000 * 20; 1.88 + 1.89 + // Number of records marked as deleted to be removed 1.90 + final long DELETED_RECORDS_PURGE_LIMIT = 5; 1.91 + 1.92 + // Android SQLite doesn't have LIMIT on DELETE. Instead, query for the 1.93 + // IDs of matching rows, then delete them in one go. 1.94 + final long now = System.currentTimeMillis(); 1.95 + final String selection = SyncColumns.IS_DELETED + " = 1 AND " + 1.96 + SyncColumns.DATE_MODIFIED + " <= " + 1.97 + (now - MAX_AGE_OF_DELETED_RECORDS); 1.98 + 1.99 + final String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE); 1.100 + final SQLiteDatabase db = getWritableDatabaseForProfile(profile, isTest(fromUri)); 1.101 + final String[] ids; 1.102 + final String limit = Long.toString(DELETED_RECORDS_PURGE_LIMIT, 10); 1.103 + final Cursor cursor = db.query(tableName, new String[] { CommonColumns._ID }, selection, null, null, null, null, limit); 1.104 + try { 1.105 + ids = new String[cursor.getCount()]; 1.106 + int i = 0; 1.107 + while (cursor.moveToNext()) { 1.108 + ids[i++] = Long.toString(cursor.getLong(0), 10); 1.109 + } 1.110 + } finally { 1.111 + cursor.close(); 1.112 + } 1.113 + 1.114 + final String inClause = computeSQLInClause(ids.length, 1.115 + CommonColumns._ID); 1.116 + db.delete(tableName, inClause, ids); 1.117 + } 1.118 +}