diff -r 000000000000 -r 6474c204b198 mobile/android/base/db/TabsProvider.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mobile/android/base/db/TabsProvider.java Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,391 @@ +/* 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 java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.mozilla.gecko.db.BrowserContract.Clients; +import org.mozilla.gecko.db.BrowserContract.Tabs; + +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.text.TextUtils; + +public class TabsProvider extends PerProfileDatabaseProvider { + static final String DATABASE_NAME = "tabs.db"; + + static final int DATABASE_VERSION = 2; + + static final String TABLE_TABS = "tabs"; + static final String TABLE_CLIENTS = "clients"; + + static final int TABS = 600; + static final int TABS_ID = 601; + static final int CLIENTS = 602; + static final int CLIENTS_ID = 603; + + static final String DEFAULT_TABS_SORT_ORDER = Clients.LAST_MODIFIED + " DESC, " + Tabs.LAST_USED + " DESC"; + static final String DEFAULT_CLIENTS_SORT_ORDER = Clients.LAST_MODIFIED + " DESC"; + + static final String INDEX_TABS_GUID = "tabs_guid_index"; + static final String INDEX_TABS_POSITION = "tabs_position_index"; + static final String INDEX_CLIENTS_GUID = "clients_guid_index"; + + static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); + + static final Map TABS_PROJECTION_MAP; + static final Map CLIENTS_PROJECTION_MAP; + + static { + URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "tabs", TABS); + URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "tabs/#", TABS_ID); + URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "clients", CLIENTS); + URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "clients/#", CLIENTS_ID); + + HashMap map; + + map = new HashMap(); + map.put(Tabs._ID, Tabs._ID); + map.put(Tabs.TITLE, Tabs.TITLE); + map.put(Tabs.URL, Tabs.URL); + map.put(Tabs.HISTORY, Tabs.HISTORY); + map.put(Tabs.FAVICON, Tabs.FAVICON); + map.put(Tabs.LAST_USED, Tabs.LAST_USED); + map.put(Tabs.POSITION, Tabs.POSITION); + map.put(Clients.GUID, Clients.GUID); + map.put(Clients.NAME, Clients.NAME); + map.put(Clients.LAST_MODIFIED, Clients.LAST_MODIFIED); + TABS_PROJECTION_MAP = Collections.unmodifiableMap(map); + + map = new HashMap(); + map.put(Clients.GUID, Clients.GUID); + map.put(Clients.NAME, Clients.NAME); + map.put(Clients.LAST_MODIFIED, Clients.LAST_MODIFIED); + CLIENTS_PROJECTION_MAP = Collections.unmodifiableMap(map); + } + + private static final String selectColumn(String table, String column) { + return table + "." + column + " = ?"; + } + + final class TabsDatabaseHelper extends SQLiteOpenHelper { + public TabsDatabaseHelper(Context context, String databasePath) { + super(context, databasePath, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + debug("Creating tabs.db: " + db.getPath()); + debug("Creating " + TABLE_TABS + " table"); + + // Table for each tab on any client. + db.execSQL("CREATE TABLE " + TABLE_TABS + "(" + + Tabs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + Tabs.CLIENT_GUID + " TEXT," + + Tabs.TITLE + " TEXT," + + Tabs.URL + " TEXT," + + Tabs.HISTORY + " TEXT," + + Tabs.FAVICON + " TEXT," + + Tabs.LAST_USED + " INTEGER," + + Tabs.POSITION + " INTEGER" + + ");"); + + // Indices on CLIENT_GUID and POSITION. + db.execSQL("CREATE INDEX " + INDEX_TABS_GUID + + " ON " + TABLE_TABS + "(" + Tabs.CLIENT_GUID + ")"); + db.execSQL("CREATE INDEX " + INDEX_TABS_POSITION + + " ON " + TABLE_TABS + "(" + Tabs.POSITION + ")"); + + debug("Creating " + TABLE_CLIENTS + " table"); + + // Table for client's name-guid mapping. + db.execSQL("CREATE TABLE " + TABLE_CLIENTS + "(" + + Clients.GUID + " TEXT PRIMARY KEY," + + Clients.NAME + " TEXT," + + Clients.LAST_MODIFIED + " INTEGER" + + ");"); + + // Index on GUID. + db.execSQL("CREATE INDEX " + INDEX_CLIENTS_GUID + + " ON " + TABLE_CLIENTS + "(" + Clients.GUID + ")"); + + createLocalClient(db); + } + + // Insert a client row for our local Fennec client. + private void createLocalClient(SQLiteDatabase db) { + debug("Inserting local Fennec client into " + TABLE_CLIENTS + " table"); + + ContentValues values = new ContentValues(); + values.put(BrowserContract.Clients.LAST_MODIFIED, System.currentTimeMillis()); + db.insertOrThrow(TABLE_CLIENTS, null, values); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + debug("Upgrading tabs.db: " + db.getPath() + " from " + + oldVersion + " to " + newVersion); + + // We have to do incremental upgrades until we reach the current + // database schema version. + for (int v = oldVersion + 1; v <= newVersion; v++) { + switch(v) { + case 2: + createLocalClient(db); + break; + } + } + } + + @Override + public void onOpen(SQLiteDatabase db) { + debug("Opening tabs.db: " + db.getPath()); + db.rawQuery("PRAGMA synchronous=OFF", null).close(); + + if (shouldUseTransactions()) { + db.enableWriteAheadLogging(); + db.setLockingEnabled(false); + return; + } + + // If we're not using transactions (in particular, prior to + // Honeycomb), then we can do some lesser optimizations. + db.rawQuery("PRAGMA journal_mode=PERSIST", null).close(); + } + } + + @Override + public String getType(Uri uri) { + final int match = URI_MATCHER.match(uri); + + trace("Getting URI type: " + uri); + + switch (match) { + case TABS: + trace("URI is TABS: " + uri); + return Tabs.CONTENT_TYPE; + + case TABS_ID: + trace("URI is TABS_ID: " + uri); + return Tabs.CONTENT_ITEM_TYPE; + + case CLIENTS: + trace("URI is CLIENTS: " + uri); + return Clients.CONTENT_TYPE; + + case CLIENTS_ID: + trace("URI is CLIENTS_ID: " + uri); + return Clients.CONTENT_ITEM_TYPE; + } + + debug("URI has unrecognized type: " + uri); + + return null; + } + + @SuppressWarnings("fallthrough") + public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { + trace("Calling delete in transaction on URI: " + uri); + + final int match = URI_MATCHER.match(uri); + int deleted = 0; + + switch (match) { + case CLIENTS_ID: + trace("Delete on CLIENTS_ID: " + uri); + selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients.ROWID)); + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + // fall through + case CLIENTS: + trace("Delete on CLIENTS: " + uri); + // Delete from both TABLE_TABS and TABLE_CLIENTS. + deleteValues(uri, selection, selectionArgs, TABLE_TABS); + deleted = deleteValues(uri, selection, selectionArgs, TABLE_CLIENTS); + break; + + case TABS_ID: + trace("Delete on TABS_ID: " + uri); + selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_TABS, Tabs._ID)); + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + // fall through + case TABS: + trace("Deleting on TABS: " + uri); + deleted = deleteValues(uri, selection, selectionArgs, TABLE_TABS); + break; + + default: + throw new UnsupportedOperationException("Unknown delete URI " + uri); + } + + debug("Deleted " + deleted + " rows for URI: " + uri); + + return deleted; + } + + public Uri insertInTransaction(Uri uri, ContentValues values) { + trace("Calling insert in transaction on URI: " + uri); + + final SQLiteDatabase db = getWritableDatabase(uri); + int match = URI_MATCHER.match(uri); + long id = -1; + + switch (match) { + case CLIENTS: + String guid = values.getAsString(Clients.GUID); + debug("Inserting client in database with GUID: " + guid); + id = db.insertOrThrow(TABLE_CLIENTS, Clients.GUID, values); + break; + + case TABS: + String url = values.getAsString(Tabs.URL); + debug("Inserting tab in database with URL: " + url); + id = db.insertOrThrow(TABLE_TABS, Tabs.TITLE, values); + break; + + default: + throw new UnsupportedOperationException("Unknown insert URI " + uri); + } + + debug("Inserted ID in database: " + id); + + if (id >= 0) + return ContentUris.withAppendedId(uri, id); + + return null; + } + + public int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + trace("Calling update in transaction on URI: " + uri); + + int match = URI_MATCHER.match(uri); + int updated = 0; + + switch (match) { + case CLIENTS_ID: + trace("Update on CLIENTS_ID: " + uri); + selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients.ROWID)); + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + // fall through + case CLIENTS: + trace("Update on CLIENTS: " + uri); + updated = updateValues(uri, values, selection, selectionArgs, TABLE_CLIENTS); + break; + + case TABS_ID: + trace("Update on TABS_ID: " + uri); + selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_TABS, Tabs._ID)); + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + // fall through + case TABS: + trace("Update on TABS: " + uri); + updated = updateValues(uri, values, selection, selectionArgs, TABLE_TABS); + break; + + default: + throw new UnsupportedOperationException("Unknown update URI " + uri); + } + + debug("Updated " + updated + " rows for URI: " + uri); + + return updated; + } + + @Override + @SuppressWarnings("fallthrough") + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + SQLiteDatabase db = getReadableDatabase(uri); + final int match = URI_MATCHER.match(uri); + + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT); + + switch (match) { + case TABS_ID: + trace("Query is on TABS_ID: " + uri); + selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_TABS, Tabs._ID)); + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + // fall through + case TABS: + trace("Query is on TABS: " + uri); + if (TextUtils.isEmpty(sortOrder)) { + sortOrder = DEFAULT_TABS_SORT_ORDER; + } else { + debug("Using sort order " + sortOrder + "."); + } + + qb.setProjectionMap(TABS_PROJECTION_MAP); + qb.setTables(TABLE_TABS + " LEFT OUTER JOIN " + TABLE_CLIENTS + " ON (" + TABLE_TABS + "." + Tabs.CLIENT_GUID + " = " + TABLE_CLIENTS + "." + Clients.GUID + ")"); + break; + + case CLIENTS_ID: + trace("Query is on CLIENTS_ID: " + uri); + selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients.ROWID)); + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + // fall through + case CLIENTS: + trace("Query is on CLIENTS: " + uri); + if (TextUtils.isEmpty(sortOrder)) { + sortOrder = DEFAULT_CLIENTS_SORT_ORDER; + } else { + debug("Using sort order " + sortOrder + "."); + } + + qb.setProjectionMap(CLIENTS_PROJECTION_MAP); + qb.setTables(TABLE_CLIENTS); + break; + + default: + throw new UnsupportedOperationException("Unknown query URI " + uri); + } + + trace("Running built query."); + final Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder, limit); + cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.TABS_AUTHORITY_URI); + + return cursor; + } + + int updateValues(Uri uri, ContentValues values, String selection, String[] selectionArgs, String table) { + trace("Updating tabs on URI: " + uri); + + final SQLiteDatabase db = getWritableDatabase(uri); + beginWrite(db); + return db.update(table, values, selection, selectionArgs); + } + + int deleteValues(Uri uri, String selection, String[] selectionArgs, String table) { + debug("Deleting tabs for URI: " + uri); + + final SQLiteDatabase db = getWritableDatabase(uri); + beginWrite(db); + return db.delete(table, selection, selectionArgs); + } + + @Override + protected TabsDatabaseHelper createDatabaseHelper(Context context, String databasePath) { + return new TabsDatabaseHelper(context, databasePath); + } + + @Override + protected String getDatabaseName() { + return DATABASE_NAME; + } +}