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