gecko/mobile/android/base/db/BrowserProvider.java.in

1544 lines
58 KiB
Java

/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- */
/* 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/. */
#filter substitution
package @ANDROID_PACKAGE_NAME@.db;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import org.mozilla.gecko.GeckoBackgroundThread;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
import org.mozilla.gecko.db.BrowserContract.CommonColumns;
import org.mozilla.gecko.db.BrowserContract.History;
import org.mozilla.gecko.db.BrowserContract.Images;
import org.mozilla.gecko.db.BrowserContract.Schema;
import org.mozilla.gecko.db.BrowserContract.SyncColumns;
import org.mozilla.gecko.db.BrowserContract.URLColumns;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.DBUtils;
import org.mozilla.gecko.sync.Utils;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.MatrixCursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
public class BrowserProvider extends ContentProvider {
private static final String LOGTAG = "GeckoBrowserProvider";
private Context mContext;
static final String DATABASE_NAME = "browser.db";
static final int DATABASE_VERSION = 2;
// Maximum age of deleted records to be cleaned up (20 days in ms)
static final long MAX_AGE_OF_DELETED_RECORDS = 86400000 * 20;
// Number of records marked as deleted to be removed
static final long DELETED_RECORDS_PURGE_LIMIT = 5;
static final String TABLE_BOOKMARKS = "bookmarks";
static final String TABLE_HISTORY = "history";
static final String TABLE_IMAGES = "images";
static final String TABLE_BOOKMARKS_TMP = TABLE_BOOKMARKS + "_tmp";
static final String VIEW_BOOKMARKS_WITH_IMAGES = "bookmarks_with_images";
static final String VIEW_HISTORY_WITH_IMAGES = "history_with_images";
// Bookmark matches
static final int BOOKMARKS = 100;
static final int BOOKMARKS_ID = 101;
static final int BOOKMARKS_FOLDER_ID = 102;
static final int BOOKMARKS_PARENT = 103;
static final int BOOKMARKS_POSITIONS = 104;
// History matches
static final int HISTORY = 200;
static final int HISTORY_ID = 201;
// Image matches
static final int IMAGES = 300;
static final int IMAGES_ID = 301;
// Schema matches
static final int SCHEMA = 400;
static final String DEFAULT_BOOKMARKS_SORT_ORDER = Bookmarks.IS_FOLDER
+ " DESC, " + Bookmarks.POSITION + " ASC, " + Bookmarks._ID
+ " ASC";
static final String DEFAULT_HISTORY_SORT_ORDER = History.DATE_LAST_VISITED + " DESC";
static final String TABLE_BOOKMARKS_JOIN_IMAGES = TABLE_BOOKMARKS + " LEFT OUTER JOIN " +
TABLE_IMAGES + " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " +
qualifyColumn(TABLE_IMAGES, Images.URL);
static final String TABLE_HISTORY_JOIN_IMAGES = TABLE_HISTORY + " LEFT OUTER JOIN " +
TABLE_IMAGES + " ON " + qualifyColumn(TABLE_HISTORY, History.URL) + " = " +
qualifyColumn(TABLE_IMAGES, Images.URL);
static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
static final Map<String, String> BOOKMARKS_PROJECTION_MAP;
static final Map<String, String> HISTORY_PROJECTION_MAP;
static final Map<String, String> IMAGES_PROJECTION_MAP;
static final Map<String, String> SCHEMA_PROJECTION_MAP;
static {
// We will reuse this.
HashMap<String, String> map;
// Bookmarks
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks", BOOKMARKS);
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/#", BOOKMARKS_ID);
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/parents", BOOKMARKS_PARENT);
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/positions", BOOKMARKS_POSITIONS);
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/folder/#", BOOKMARKS_FOLDER_ID);
map = new HashMap<String, String>();
map.put(Bookmarks._ID, Bookmarks._ID);
map.put(Bookmarks.TITLE, Bookmarks.TITLE);
map.put(Bookmarks.URL, Bookmarks.URL);
map.put(Bookmarks.FAVICON, Bookmarks.FAVICON);
map.put(Bookmarks.THUMBNAIL, Bookmarks.THUMBNAIL);
map.put(Bookmarks.IS_FOLDER, Bookmarks.IS_FOLDER);
map.put(Bookmarks.PARENT, Bookmarks.PARENT);
map.put(Bookmarks.POSITION, Bookmarks.POSITION);
map.put(Bookmarks.TAGS, Bookmarks.TAGS);
map.put(Bookmarks.DESCRIPTION, Bookmarks.DESCRIPTION);
map.put(Bookmarks.KEYWORD, Bookmarks.KEYWORD);
map.put(Bookmarks.DATE_CREATED, Bookmarks.DATE_CREATED);
map.put(Bookmarks.DATE_MODIFIED, Bookmarks.DATE_MODIFIED);
map.put(Bookmarks.GUID, Bookmarks.GUID);
map.put(Bookmarks.IS_DELETED, Bookmarks.IS_DELETED);
BOOKMARKS_PROJECTION_MAP = Collections.unmodifiableMap(map);
// History
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history", HISTORY);
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history/#", HISTORY_ID);
map = new HashMap<String, String>();
map.put(History._ID, History._ID);
map.put(History.TITLE, History.TITLE);
map.put(History.URL, History.URL);
map.put(History.FAVICON, History.FAVICON);
map.put(History.THUMBNAIL, History.THUMBNAIL);
map.put(History.VISITS, History.VISITS);
map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED);
map.put(History.DATE_CREATED, History.DATE_CREATED);
map.put(History.DATE_MODIFIED, History.DATE_MODIFIED);
map.put(History.GUID, History.GUID);
map.put(History.IS_DELETED, History.IS_DELETED);
HISTORY_PROJECTION_MAP = Collections.unmodifiableMap(map);
// Images
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "images", IMAGES);
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "images/#", IMAGES_ID);
map = new HashMap<String, String>();
map.put(Images._ID, Images._ID);
map.put(Images.URL, Images.URL);
map.put(Images.FAVICON, Images.FAVICON);
map.put(Images.FAVICON_URL, Images.FAVICON_URL);
map.put(Images.THUMBNAIL, Images.THUMBNAIL);
map.put(Images.DATE_CREATED, Images.DATE_CREATED);
map.put(Images.DATE_MODIFIED, Images.DATE_MODIFIED);
map.put(Images.GUID, Images.GUID);
map.put(Images.IS_DELETED, Images.IS_DELETED);
IMAGES_PROJECTION_MAP = Collections.unmodifiableMap(map);
// Schema
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "schema", SCHEMA);
map = new HashMap<String, String>();
map.put(Schema.VERSION, Schema.VERSION);
SCHEMA_PROJECTION_MAP = Collections.unmodifiableMap(map);
}
private HashMap<String, DatabaseHelper> mDatabasePerProfile;
static final String qualifyColumn(String table, String column) {
return table + "." + column;
}
private static boolean hasImagesInProjection(String[] projection) {
if (projection == null) return true;
for (int i = 0; i < projection.length; ++i) {
if (projection[i].equals(Images.FAVICON) ||
projection[i].equals(Images.THUMBNAIL))
return true;
}
return false;
}
// Calculate these once, at initialization. isLoggable is too expensive to
// have in-line in each log call.
private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
protected static void trace(String message) {
if (logVerbose) {
Log.v(LOGTAG, message);
}
}
protected static void debug(String message) {
if (logDebug) {
Log.d(LOGTAG, message);
}
}
final class DatabaseHelper extends SQLiteOpenHelper {
public DatabaseHelper(Context context, String databasePath) {
super(context, databasePath, null, DATABASE_VERSION);
}
private void createBookmarksTable(SQLiteDatabase db) {
debug("Creating " + TABLE_BOOKMARKS + " table");
// Android versions older than Froyo ship with an sqlite
// that doesn't support foreign keys.
String foreignKeyOnParent = null;
if (Build.VERSION.SDK_INT >= 8) {
foreignKeyOnParent = ", FOREIGN KEY (" + Bookmarks.PARENT +
") REFERENCES " + TABLE_BOOKMARKS + "(" + Bookmarks._ID + ")";
}
db.execSQL("CREATE TABLE " + TABLE_BOOKMARKS + "(" +
Bookmarks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
Bookmarks.TITLE + " TEXT," +
Bookmarks.URL + " TEXT," +
Bookmarks.IS_FOLDER + " INTEGER NOT NULL DEFAULT 0," +
Bookmarks.PARENT + " INTEGER," +
Bookmarks.POSITION + " INTEGER NOT NULL," +
Bookmarks.KEYWORD + " TEXT," +
Bookmarks.DESCRIPTION + " TEXT," +
Bookmarks.TAGS + " TEXT," +
Bookmarks.DATE_CREATED + " INTEGER," +
Bookmarks.DATE_MODIFIED + " INTEGER," +
Bookmarks.GUID + " TEXT," +
Bookmarks.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
(foreignKeyOnParent != null ? foreignKeyOnParent : "") +
");");
db.execSQL("CREATE INDEX bookmarks_url_index ON " + TABLE_BOOKMARKS + "("
+ Bookmarks.URL + ")");
db.execSQL("CREATE UNIQUE INDEX bookmarks_guid_index ON " + TABLE_BOOKMARKS + "("
+ Bookmarks.GUID + ")");
db.execSQL("CREATE INDEX bookmarks_modified_index ON " + TABLE_BOOKMARKS + "("
+ Bookmarks.DATE_MODIFIED + ")");
}
private void createHistoryTable(SQLiteDatabase db) {
debug("Creating " + TABLE_HISTORY + " table");
db.execSQL("CREATE TABLE " + TABLE_HISTORY + "(" +
History._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
History.TITLE + " TEXT," +
History.URL + " TEXT NOT NULL," +
History.VISITS + " INTEGER NOT NULL DEFAULT 0," +
History.DATE_LAST_VISITED + " INTEGER," +
History.DATE_CREATED + " INTEGER," +
History.DATE_MODIFIED + " INTEGER," +
History.GUID + " TEXT," +
History.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
");");
db.execSQL("CREATE INDEX history_url_index ON " + TABLE_HISTORY + "("
+ History.URL + ")");
db.execSQL("CREATE UNIQUE INDEX history_guid_index ON " + TABLE_HISTORY + "("
+ History.GUID + ")");
db.execSQL("CREATE INDEX history_modified_index ON " + TABLE_HISTORY + "("
+ History.DATE_MODIFIED + ")");
db.execSQL("CREATE INDEX history_visited_index ON " + TABLE_HISTORY + "("
+ History.DATE_LAST_VISITED + ")");
}
private void createImagesTable(SQLiteDatabase db) {
debug("Creating " + TABLE_IMAGES + " table");
db.execSQL("CREATE TABLE " + TABLE_IMAGES + " (" +
Images._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
Images.URL + " TEXT UNIQUE NOT NULL," +
Images.FAVICON + " BLOB," +
Images.FAVICON_URL + " TEXT," +
Images.THUMBNAIL + " BLOB," +
Images.DATE_CREATED + " INTEGER," +
Images.DATE_MODIFIED + " INTEGER," +
Images.GUID + " TEXT," +
Images.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
");");
db.execSQL("CREATE INDEX images_url_index ON " + TABLE_IMAGES + "("
+ Images.URL + ")");
db.execSQL("CREATE UNIQUE INDEX images_guid_index ON " + TABLE_IMAGES + "("
+ Images.GUID + ")");
db.execSQL("CREATE INDEX images_modified_index ON " + TABLE_IMAGES + "("
+ Images.DATE_MODIFIED + ")");
}
private void createBookmarksWithImagesView(SQLiteDatabase db) {
debug("Creating " + VIEW_BOOKMARKS_WITH_IMAGES + " view");
db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_BOOKMARKS_WITH_IMAGES + " AS " +
"SELECT " + qualifyColumn(TABLE_BOOKMARKS, "*") +
", " + Images.FAVICON + ", " + Images.THUMBNAIL + " FROM " +
TABLE_BOOKMARKS_JOIN_IMAGES);
}
private void createHistoryWithImagesView(SQLiteDatabase db) {
debug("Creating " + VIEW_HISTORY_WITH_IMAGES + " view");
db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_HISTORY_WITH_IMAGES + " AS " +
"SELECT " + qualifyColumn(TABLE_HISTORY, "*") +
", " + Images.FAVICON + ", " + Images.THUMBNAIL + " FROM " +
TABLE_HISTORY_JOIN_IMAGES);
}
@Override
public void onCreate(SQLiteDatabase db) {
debug("Creating browser.db: " + db.getPath());
createBookmarksTable(db);
createHistoryTable(db);
createImagesTable(db);
createBookmarksWithImagesView(db);
createHistoryWithImagesView(db);
createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID,
R.string.bookmarks_folder_places, 0);
createOrUpdateAllSpecialFolders(db);
// FIXME: Create default bookmarks here (bug 728224)
}
private void createOrUpdateAllSpecialFolders(SQLiteDatabase db) {
createOrUpdateSpecialFolder(db, Bookmarks.MOBILE_FOLDER_GUID,
R.string.bookmarks_folder_mobile, 0);
createOrUpdateSpecialFolder(db, Bookmarks.TOOLBAR_FOLDER_GUID,
R.string.bookmarks_folder_toolbar, 1);
createOrUpdateSpecialFolder(db, Bookmarks.MENU_FOLDER_GUID,
R.string.bookmarks_folder_menu, 2);
createOrUpdateSpecialFolder(db, Bookmarks.TAGS_FOLDER_GUID,
R.string.bookmarks_folder_tags, 3);
createOrUpdateSpecialFolder(db, Bookmarks.UNFILED_FOLDER_GUID,
R.string.bookmarks_folder_unfiled, 4);
}
private void createOrUpdateSpecialFolder(SQLiteDatabase db,
String guid, int titleId, int position) {
ContentValues values = new ContentValues();
values.put(Bookmarks.GUID, guid);
values.put(Bookmarks.IS_FOLDER, 1);
values.put(Bookmarks.POSITION, position);
if (guid.equals(Bookmarks.PLACES_FOLDER_GUID))
values.put(Bookmarks._ID, Bookmarks.FIXED_ROOT_ID);
// Set the parent to 0, which sync assumes is the root
values.put(Bookmarks.PARENT, Bookmarks.FIXED_ROOT_ID);
String title = mContext.getResources().getString(titleId);
values.put(Bookmarks.TITLE, title);
long now = System.currentTimeMillis();
values.put(Bookmarks.DATE_CREATED, now);
values.put(Bookmarks.DATE_MODIFIED, now);
int updated = db.update(TABLE_BOOKMARKS, values,
Bookmarks.GUID + " = ?",
new String[] { guid });
if (updated == 0) {
db.insert(TABLE_BOOKMARKS, Bookmarks.GUID, values);
debug("Inserted special folder: " + guid);
} else {
debug("Updated special folder: " + guid);
}
}
private boolean isSpecialFolder(ContentValues values) {
String guid = values.getAsString(Bookmarks.GUID);
if (guid == null)
return false;
return guid.equals(Bookmarks.MOBILE_FOLDER_GUID) ||
guid.equals(Bookmarks.MENU_FOLDER_GUID) ||
guid.equals(Bookmarks.TOOLBAR_FOLDER_GUID) ||
guid.equals(Bookmarks.UNFILED_FOLDER_GUID) ||
guid.equals(Bookmarks.TAGS_FOLDER_GUID);
}
private void migrateBookmarkFolder(SQLiteDatabase db, int folderId) {
Cursor c = null;
debug("Migrating bookmark folder with id = " + folderId);
String selection = Bookmarks.PARENT + " = " + folderId;
String[] selectionArgs = null;
boolean isRootFolder = (folderId == Bookmarks.FIXED_ROOT_ID);
// If we're loading the root folder, we have to account for
// any previously created special folder that was created without
// setting a parent id (e.g. mobile folder) and making sure we're
// not adding any infinite recursion as root's parent is root itself.
if (isRootFolder) {
selection = Bookmarks.GUID + " != ?" + " AND (" +
selection + " OR " + Bookmarks.PARENT + " = NULL)";
selectionArgs = new String[] { Bookmarks.PLACES_FOLDER_GUID };
}
List<Integer> subFolders = new ArrayList<Integer>();
List<ContentValues> invalidSpecialEntries = new ArrayList<ContentValues>();
try {
c = db.query(TABLE_BOOKMARKS_TMP,
null,
selection,
selectionArgs,
null, null, null);
// The key point here is that bookmarks should be added in
// parent order to avoid any problems with the foreign key
// in Bookmarks.PARENT.
while (c.moveToNext()) {
ContentValues values = new ContentValues();
// We're using a null projection in the query which
// means we're getting all columns from the table.
// It's safe to simply transform the row into the
// values to be inserted on the new table.
DatabaseUtils.cursorRowToContentValues(c, values);
boolean isSpecialFolder = isSpecialFolder(values);
// The mobile folder used to be created with PARENT = NULL.
// We want fix that here.
if (values.getAsLong(Bookmarks.PARENT) == null && isSpecialFolder)
values.put(Bookmarks.PARENT, Bookmarks.FIXED_ROOT_ID);
if (isRootFolder && !isSpecialFolder) {
invalidSpecialEntries.add(values);
continue;
}
debug("Migrating bookmark: " + values.getAsString(Bookmarks.TITLE));
db.insert(TABLE_BOOKMARKS, Bookmarks.URL, values);
Integer isFolder = values.getAsInteger(Bookmarks.IS_FOLDER);
if (isFolder != null && isFolder == 1)
subFolders.add(values.getAsInteger(Bookmarks._ID));
}
} finally {
if (c != null)
c.close();
}
// At this point is safe to assume that the mobile folder is
// in the new table given that we've always created it on
// database creation time.
final int nInvalidSpecialEntries = invalidSpecialEntries.size();
if (nInvalidSpecialEntries > 0) {
Long mobileFolderId = guidToID(db, Bookmarks.MOBILE_FOLDER_GUID);
debug("Found " + nInvalidSpecialEntries + " invalid special folder entries");
for (int i = 0; i < nInvalidSpecialEntries; i++) {
ContentValues values = invalidSpecialEntries.get(i);
values.put(Bookmarks.PARENT, mobileFolderId);
db.insert(TABLE_BOOKMARKS, Bookmarks.URL, values);
}
}
final int nSubFolders = subFolders.size();
for (int i = 0; i < nSubFolders; i++) {
int subFolderId = subFolders.get(i);
migrateBookmarkFolder(db, subFolderId);
}
}
private void upgradeDatabaseFrom1to2(SQLiteDatabase db) {
debug("Renaming bookmarks table to " + TABLE_BOOKMARKS_TMP);
db.execSQL("ALTER TABLE " + TABLE_BOOKMARKS +
" RENAME TO " + TABLE_BOOKMARKS_TMP);
debug("Dropping views and indexes related to " + TABLE_BOOKMARKS);
db.execSQL("DROP VIEW IF EXISTS " + VIEW_BOOKMARKS_WITH_IMAGES);
db.execSQL("DROP INDEX IF EXISTS bookmarks_url_index");
db.execSQL("DROP INDEX IF EXISTS bookmarks_guid_index");
db.execSQL("DROP INDEX IF EXISTS bookmarks_modified_index");
createBookmarksTable(db);
createBookmarksWithImagesView(db);
createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID,
R.string.bookmarks_folder_places, 0);
migrateBookmarkFolder(db, Bookmarks.FIXED_ROOT_ID);
// Ensure all special folders exist and have the
// right folder hierarchy.
createOrUpdateAllSpecialFolders(db);
debug("Dropping bookmarks temporary table");
db.execSQL("DROP TABLE IF EXISTS " + TABLE_BOOKMARKS_TMP);
}
private void upgradeDatabaseFrom2to3(SQLiteDatabase db) {
debug("Dropping view: " + VIEW_BOOKMARKS_WITH_IMAGES);
db.execSQL("DROP VIEW IF EXISTS " + VIEW_BOOKMARKS_WITH_IMAGES);
createBookmarksWithImagesView(db);
debug("Dropping view: " + VIEW_HISTORY_WITH_IMAGES);
db.execSQL("DROP VIEW IF EXISTS " + VIEW_HISTORY_WITH_IMAGES);
createHistoryWithImagesView(db);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
debug("Upgrading browser.db: " + db.getPath() + " from " +
oldVersion + " to " + newVersion);
db.beginTransaction();
// 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:
upgradeDatabaseFrom1to2(db);
break;
case 3:
upgradeDatabaseFrom2to3(db);
break;
}
}
db.endTransaction();
}
@Override
public void onOpen(SQLiteDatabase db) {
debug("Opening browser.db: " + db.getPath());
// From Honeycomb on, it's possible to run several db
// commands in parallel using multiple connections.
if (Build.VERSION.SDK_INT >= 11) {
db.enableWriteAheadLogging();
} else {
// Pre-Honeycomb, we can do some lesser optimizations.
Cursor cursor = null;
try {
cursor = db.rawQuery("PRAGMA synchronous=NORMAL", null);
} finally {
if (cursor != null)
cursor.close();
}
cursor = null;
try {
cursor = db.rawQuery("PRAGMA journal_mode=PERSIST", null);
} finally {
if (cursor != null)
cursor.close();
}
}
}
}
private Long guidToID(SQLiteDatabase db, String guid) {
Cursor c = null;
try {
c = db.query(TABLE_BOOKMARKS,
new String[] { Bookmarks._ID },
Bookmarks.GUID + " = ?",
new String[] { guid },
null, null, null);
if (c == null || !c.moveToFirst())
return null;
return c.getLong(c.getColumnIndex(Bookmarks._ID));
} finally {
if (c != null)
c.close();
}
}
private DatabaseHelper getDatabaseHelperForProfile(String profile) {
// Each profile has a separate browser.db database. The target
// profile is provided using a URI query argument in each request
// to our content provider.
// Always fallback to default profile if none has been provided.
if (TextUtils.isEmpty(profile)) {
profile = BrowserContract.DEFAULT_PROFILE;
}
DatabaseHelper dbHelper;
synchronized (this) {
dbHelper = mDatabasePerProfile.get(profile);
if (dbHelper != null) {
return dbHelper;
}
dbHelper = new DatabaseHelper(getContext(), getDatabasePath(profile));
mDatabasePerProfile.put(profile, dbHelper);
}
debug("Created database helper for profile: " + profile);
return dbHelper;
}
private String getDatabasePath(String profile) {
trace("Getting database path for profile: " + profile);
// On Android releases older than 2.3, it's not possible to use
// SQLiteOpenHelper with a full path. Fallback to using separate
// db files per profile in the app directory.
if (Build.VERSION.SDK_INT <= 8) {
return "browser-" + profile + ".db";
}
File profileDir = GeckoProfile.get(mContext, profile).getDir();
if (profileDir == null) {
debug("Couldn't find directory for profile: " + profile);
return null;
}
String databasePath = new File(profileDir, DATABASE_NAME).getAbsolutePath();
debug("Successfully created database path for profile: " + databasePath);
return databasePath;
}
private SQLiteDatabase getReadableDatabase(Uri uri) {
trace("Getting readable database for URI: " + uri);
String profile = null;
if (uri != null)
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
return getDatabaseHelperForProfile(profile).getReadableDatabase();
}
private SQLiteDatabase getWritableDatabase(Uri uri) {
trace("Getting writable database for URI: " + uri);
String profile = null;
if (uri != null)
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
return getDatabaseHelperForProfile(profile).getWritableDatabase();
}
private void cleanupSomeDeletedRecords(Uri fromUri, Uri targetUri, String tableName) {
// we cleanup records marked as deleted that are older than a
// predefined max age. It's important not be too greedy here and
// remove only a few old deleted records at a time.
String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
// The PARAM_SHOW_DELETED argument is necessary to return the records
// that were marked as deleted. We use PARAM_IS_SYNC here to ensure
// that we'll be actually deleting records instead of flagging them.
Uri uriWithArgs = targetUri.buildUpon()
.appendQueryParameter(BrowserContract.PARAM_PROFILE, profile)
.appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(DELETED_RECORDS_PURGE_LIMIT))
.appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1")
.appendQueryParameter(BrowserContract.PARAM_IS_SYNC, "1")
.build();
Cursor cursor = null;
try {
long now = System.currentTimeMillis();
String selection = SyncColumns.IS_DELETED + " = 1 AND " +
SyncColumns.DATE_MODIFIED + " <= " + (now - MAX_AGE_OF_DELETED_RECORDS);
cursor = query(uriWithArgs,
new String[] { CommonColumns._ID },
selection,
null,
null);
while (cursor.moveToNext()) {
Uri uriWithId = ContentUris.withAppendedId(uriWithArgs, cursor.getLong(0));
delete(uriWithId, null, null);
debug("Removed old deleted item with URI: " + uriWithId);
}
} finally {
if (cursor != null)
cursor.close();
}
}
private boolean isCallerSync(Uri uri) {
String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
return !TextUtils.isEmpty(isSync);
}
private boolean shouldShowDeleted(Uri uri) {
String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED);
return !TextUtils.isEmpty(showDeleted);
}
@Override
public boolean onCreate() {
debug("Creating BrowserProvider");
GeckoBackgroundThread.post(new Runnable() {
@Override
public void run() {
// Kick this off early. It is synchronized so that other callers will wait
try {
GeckoProfile.get(getContext()).getDir();
} catch (Exception ex) {
Log.e(LOGTAG, "Error getting profile dir", ex);
}
}
});
synchronized (this) {
mContext = getContext();
mDatabasePerProfile = new HashMap<String, DatabaseHelper>();
}
return true;
}
@Override
public String getType(Uri uri) {
final int match = URI_MATCHER.match(uri);
trace("Getting URI type: " + uri);
switch (match) {
case BOOKMARKS:
trace("URI is BOOKMARKS: " + uri);
return Bookmarks.CONTENT_TYPE;
case BOOKMARKS_ID:
trace("URI is BOOKMARKS_ID: " + uri);
return Bookmarks.CONTENT_ITEM_TYPE;
case HISTORY:
trace("URI is HISTORY: " + uri);
return History.CONTENT_TYPE;
case HISTORY_ID:
trace("URI is HISTORY_ID: " + uri);
return History.CONTENT_ITEM_TYPE;
}
debug("URI has unrecognized type: " + uri);
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
trace("Calling delete on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
int deleted = 0;
if (Build.VERSION.SDK_INT >= 11) {
trace("Beginning delete transaction: " + uri);
db.beginTransaction();
try {
deleted = deleteInTransaction(uri, selection, selectionArgs);
db.setTransactionSuccessful();
trace("Successful delete transaction: " + uri);
} finally {
db.endTransaction();
}
} else {
deleted = deleteInTransaction(uri, selection, selectionArgs);
}
if (deleted > 0)
getContext().getContentResolver().notifyChange(uri, null);
return deleted;
}
@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 BOOKMARKS_ID:
trace("Delete on BOOKMARKS_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?");
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
// fall through
case BOOKMARKS: {
trace("Deleting bookmarks: " + uri);
deleted = deleteBookmarks(uri, selection, selectionArgs);
deleteUnusedImages(uri);
break;
}
case HISTORY_ID:
trace("Delete on HISTORY_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?");
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
// fall through
case HISTORY: {
trace("Deleting history: " + uri);
deleted = deleteHistory(uri, selection, selectionArgs);
deleteUnusedImages(uri);
break;
}
case IMAGES_ID:
debug("Delete on IMAGES_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, TABLE_IMAGES + "._id = ?");
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
// fall through
case IMAGES: {
trace("Deleting images: " + uri);
deleted = deleteImages(uri, selection, selectionArgs);
break;
}
default:
throw new UnsupportedOperationException("Unknown delete URI " + uri);
}
debug("Deleted " + deleted + " rows for URI: " + uri);
return deleted;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
trace("Calling insert on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
Uri result = null;
if (Build.VERSION.SDK_INT >= 11) {
trace("Beginning insert transaction: " + uri);
db.beginTransaction();
try {
result = insertInTransaction(uri, values);
db.setTransactionSuccessful();
trace("Successful insert transaction: " + uri);
} finally {
db.endTransaction();
}
} else {
result = insertInTransaction(uri, values);
}
if (result != null)
getContext().getContentResolver().notifyChange(uri, null);
return result;
}
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 BOOKMARKS: {
trace("Insert on BOOKMARKS: " + uri);
// Generate values if not specified. Don't overwrite
// if specified by caller.
long now = System.currentTimeMillis();
if (!values.containsKey(Bookmarks.DATE_CREATED)) {
values.put(Bookmarks.DATE_CREATED, now);
}
if (!values.containsKey(Bookmarks.DATE_MODIFIED)) {
values.put(Bookmarks.DATE_MODIFIED, now);
}
if (!values.containsKey(Bookmarks.GUID)) {
values.put(Bookmarks.GUID, Utils.generateGuid());
}
if (!values.containsKey(Bookmarks.POSITION)) {
debug("Inserting bookmark with no position for URI");
values.put(Bookmarks.POSITION, Long.toString(Long.MIN_VALUE));
}
String url = values.getAsString(Bookmarks.URL);
ContentValues imageValues = extractImageValues(values, url);
Integer isFolder = values.getAsInteger(Bookmarks.IS_FOLDER);
if ((isFolder == null || isFolder != 1) && imageValues != null
&& !TextUtils.isEmpty(url)) {
debug("Inserting bookmark image for URL: " + url);
updateOrInsertImage(uri, imageValues, Images.URL + " = ?",
new String[] { url });
}
debug("Inserting bookmark in database with URL: " + url);
id = db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.TITLE, values);
break;
}
case HISTORY: {
trace("Insert on HISTORY: " + uri);
long now = System.currentTimeMillis();
values.put(History.DATE_CREATED, now);
values.put(History.DATE_MODIFIED, now);
// Generate GUID for new history entry. Don't override specified GUIDs.
if (!values.containsKey(History.GUID)) {
values.put(History.GUID, Utils.generateGuid());
}
String url = values.getAsString(History.URL);
ContentValues imageValues = extractImageValues(values,
values.getAsString(History.URL));
if (imageValues != null) {
debug("Inserting history image for URL: " + url);
updateOrInsertImage(uri, imageValues, Images.URL + " = ?",
new String[] { url });
}
debug("Inserting history in database with URL: " + url);
id = db.insertOrThrow(TABLE_HISTORY, History.VISITS, values);
break;
}
case IMAGES: {
trace("Insert on IMAGES: " + uri);
long now = System.currentTimeMillis();
values.put(History.DATE_CREATED, now);
values.put(History.DATE_MODIFIED, now);
// Generate GUID for new history entry
values.put(History.GUID, Utils.generateGuid());
String url = values.getAsString(Images.URL);
debug("Inserting image in database with URL: " + url);
id = db.insertOrThrow(TABLE_IMAGES, Images.URL, 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;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
trace("Calling update on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
int updated = 0;
if (Build.VERSION.SDK_INT >= 11) {
trace("Beginning update transaction: " + uri);
db.beginTransaction();
try {
updated = updateInTransaction(uri, values, selection, selectionArgs);
db.setTransactionSuccessful();
trace("Successful update transaction: " + uri);
} finally {
db.endTransaction();
}
} else {
updated = updateInTransaction(uri, values, selection, selectionArgs);
}
if (updated > 0)
getContext().getContentResolver().notifyChange(uri, null);
return updated;
}
@SuppressWarnings("fallthrough")
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) {
// We provide a dedicated (hacky) API for callers to bulk-update the positions of
// folder children by passing an array of GUID strings as `selectionArgs`.
// Each child will have its position column set to its index in the provided array.
//
// This avoids callers having to issue a large number of UPDATE queries through
// the usual channels. See Bug 728783.
//
// Note that this is decidedly not a general-purpose API; use at your own risk.
// `values` and `selection` are ignored.
case BOOKMARKS_POSITIONS: {
debug("Update on BOOKMARKS_POSITIONS: " + uri);
updated = updateBookmarkPositions(uri, selectionArgs);
break;
}
case BOOKMARKS_PARENT: {
debug("Update on BOOKMARKS_PARENT: " + uri);
updated = updateBookmarkParents(uri, values, selection, selectionArgs);
break;
}
case BOOKMARKS_ID:
debug("Update on BOOKMARKS_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?");
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
// fall through
case BOOKMARKS: {
debug("Updating bookmark: " + uri);
updated = updateBookmarks(uri, values, selection, selectionArgs);
break;
}
case HISTORY_ID:
debug("Update on HISTORY_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?");
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
// fall through
case HISTORY: {
debug("Updating history: " + uri);
updated = updateHistory(uri, values, selection, selectionArgs);
break;
}
case IMAGES: {
debug("Update on IMAGES: " + uri);
String url = values.getAsString(Images.URL);
if (TextUtils.isEmpty(url))
throw new IllegalArgumentException("Images.URL is required");
updated = updateExistingImage(uri, values, Images.URL + " = ?",
new String[] { url });
break;
}
default:
throw new UnsupportedOperationException("Unknown update URI " + uri);
}
debug("Updated " + updated + " rows for URI: " + uri);
return updated;
}
@Override
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 BOOKMARKS_FOLDER_ID:
case BOOKMARKS_ID:
case BOOKMARKS: {
debug("Query is on bookmarks: " + uri);
if (match == BOOKMARKS_ID) {
selection = DBUtils.concatenateWhere(selection, Bookmarks._ID + " = ?");
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
} else if (match == BOOKMARKS_FOLDER_ID) {
selection = DBUtils.concatenateWhere(selection, Bookmarks.PARENT + " = ?");
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
}
if (!shouldShowDeleted(uri))
selection = DBUtils.concatenateWhere(Bookmarks.IS_DELETED + " = 0", selection);
if (TextUtils.isEmpty(sortOrder)) {
sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER;
} else {
debug("Using sort order " + sortOrder + ".");
}
qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
if (hasImagesInProjection(projection))
qb.setTables(VIEW_BOOKMARKS_WITH_IMAGES);
else
qb.setTables(TABLE_BOOKMARKS);
break;
}
case HISTORY_ID:
case HISTORY: {
debug("Query is on history: " + uri);
if (match == HISTORY_ID) {
selection = DBUtils.concatenateWhere(selection, History._ID + " = ?");
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
}
if (!shouldShowDeleted(uri))
selection = DBUtils.concatenateWhere(History.IS_DELETED + " = 0", selection);
if (TextUtils.isEmpty(sortOrder))
sortOrder = DEFAULT_HISTORY_SORT_ORDER;
qb.setProjectionMap(HISTORY_PROJECTION_MAP);
if (hasImagesInProjection(projection))
qb.setTables(VIEW_HISTORY_WITH_IMAGES);
else
qb.setTables(TABLE_HISTORY);
break;
}
case IMAGES_ID:
case IMAGES: {
debug("Query is on images: " + uri);
if (match == IMAGES_ID) {
selection = DBUtils.concatenateWhere(selection, Images._ID + " = ?");
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
}
if (!shouldShowDeleted(uri))
selection = DBUtils.concatenateWhere(Images.IS_DELETED + " = 0", selection);
qb.setProjectionMap(IMAGES_PROJECTION_MAP);
qb.setTables(TABLE_IMAGES);
break;
}
case SCHEMA: {
debug("Query is on schema.");
MatrixCursor schemaCursor = new MatrixCursor(new String[] { Schema.VERSION });
schemaCursor.newRow().add(DATABASE_VERSION);
return schemaCursor;
}
default:
throw new UnsupportedOperationException("Unknown query URI " + uri);
}
trace("Running built query.");
Cursor cursor = qb.query(db, projection, selection, selectionArgs, null,
null, sortOrder, limit);
cursor.setNotificationUri(getContext().getContentResolver(),
BrowserContract.AUTHORITY_URI);
return cursor;
}
ContentValues extractImageValues(ContentValues values, String url) {
trace("Extracting image values for URI: " + url);
ContentValues imageValues = null;
if (values.containsKey(Bookmarks.FAVICON)) {
debug("Has favicon value on URL: " + url);
imageValues = new ContentValues();
imageValues.put(Images.FAVICON,
values.getAsByteArray(Bookmarks.FAVICON));
values.remove(Bookmarks.FAVICON);
}
if (values.containsKey(Bookmarks.THUMBNAIL)) {
debug("Has favicon value on URL: " + url);
if (imageValues == null)
imageValues = new ContentValues();
imageValues.put(Images.THUMBNAIL,
values.getAsByteArray(Bookmarks.THUMBNAIL));
values.remove(Bookmarks.THUMBNAIL);
}
if (imageValues != null && url != null) {
debug("Has URL value");
imageValues.put(Images.URL, url);
}
return imageValues;
}
int getUrlCount(SQLiteDatabase db, String table, String url) {
Cursor c = db.query(table, new String[] { "COUNT(*)" },
URLColumns.URL + " = ?", new String[] { url }, null, null,
null);
int count = 0;
try {
if (c.moveToFirst())
count = c.getInt(0);
} finally {
c.close();
}
return count;
}
/**
* Construct an update expression that will modify the positions of
* records in-place.
*
* The provided selectionArgs is expected to be an implicit mapping from
* GUID to new position.
*/
int updateBookmarkPositions(Uri uri, String[] guids) {
if (guids == null)
return 0;
int guidsCount = guids.length;
if (guidsCount == 0)
return 0;
// Each must appear twice: once in a CASE, and once in the IN clause.
String[] args = new String[guidsCount * 2];
System.arraycopy(guids, 0, args, 0, guidsCount);
System.arraycopy(guids, 0, args, guidsCount, guidsCount);
StringBuilder b = new StringBuilder("UPDATE " + TABLE_BOOKMARKS +
" SET " + Bookmarks.POSITION + " = CASE guid");
int i = 0;
for (; i < guids.length; ++i) {
if (guids[i] == null) {
// We don't want to issue the query if not every GUID is specified.
debug("updateBookmarkPositions called with null GUID at index " + i);
return 0;
}
b.append(" WHEN ? THEN " + i);
}
b.append(" END WHERE " + Bookmarks.GUID + " IN (");
i = 1;
while (i++ < guidsCount) {
b.append("?, ");
}
b.append("?)");
getWritableDatabase(uri).execSQL(b.toString(), args);
// We can't easily get a modified count without calling something like changes().
return guidsCount;
}
/**
* Construct an update expression that will modify the parents of any records
* that match.
*/
int updateBookmarkParents(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
trace("Updating bookmark parents of " + selection + " (" + selectionArgs[0] + ")");
String where = Bookmarks._ID + " IN (" +
" SELECT DISTINCT " + Bookmarks.PARENT +
" FROM " + TABLE_BOOKMARKS +
" WHERE " + selection + " )";
return getWritableDatabase(uri).update(TABLE_BOOKMARKS, values, where, selectionArgs);
}
int updateBookmarks(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
trace("Updating bookmarks on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
int updated = 0;
final String[] bookmarksProjection = new String[] {
Bookmarks._ID, // 0
Bookmarks.URL, // 1
};
trace("Quering bookmarks to update on URI: " + uri);
Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection,
selection, selectionArgs, null, null, null);
try {
if (!values.containsKey(Bookmarks.DATE_MODIFIED))
values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
boolean updatingUrl = values.containsKey(Bookmarks.URL);
String url = null;
if (updatingUrl)
url = values.getAsString(Bookmarks.URL);
ContentValues imageValues = extractImageValues(values, url);
while (cursor.moveToNext()) {
long id = cursor.getLong(0);
trace("Updating bookmark with ID: " + id);
updated += db.update(TABLE_BOOKMARKS, values, "_id = ?",
new String[] { Long.toString(id) });
if (imageValues == null)
continue;
if (!updatingUrl) {
url = cursor.getString(1);
imageValues.put(Images.URL, url);
}
if (!TextUtils.isEmpty(url)) {
trace("Updating bookmark image for URL: " + url);
updateOrInsertImage(uri, imageValues, Images.URL + " = ?",
new String[] { url });
}
}
} finally {
if (cursor != null)
cursor.close();
}
return updated;
}
int updateHistory(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
trace("Updating history on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
int updated = 0;
final String[] historyProjection = new String[] { History._ID, // 0
History.URL, // 1
};
Cursor cursor = db.query(TABLE_HISTORY, historyProjection, selection,
selectionArgs, null, null, null);
try {
values.put(History.DATE_MODIFIED, System.currentTimeMillis());
boolean updatingUrl = values.containsKey(History.URL);
String url = null;
if (updatingUrl)
url = values.getAsString(History.URL);
ContentValues imageValues = extractImageValues(values, url);
while (cursor.moveToNext()) {
long id = cursor.getLong(0);
trace("Updating history entry with ID: " + id);
updated += db.update(TABLE_HISTORY, values, "_id = ?",
new String[] { Long.toString(id) });
if (imageValues == null)
continue;
if (!updatingUrl) {
url = cursor.getString(1);
imageValues.put(Images.URL, url);
}
if (!TextUtils.isEmpty(url)) {
trace("Updating history image for URL: " + url);
updateOrInsertImage(uri, imageValues, Images.URL + " = ?",
new String[] { url });
}
}
} finally {
if (cursor != null)
cursor.close();
}
return updated;
}
int updateOrInsertImage(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
return updateImage(uri, values, selection, selectionArgs,
true /* insert if needed */);
}
int updateExistingImage(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
return updateImage(uri, values, selection, selectionArgs,
false /* only update, no insert */);
}
int updateImage(Uri uri, ContentValues values, String selection,
String[] selectionArgs, boolean insertIfNeeded) {
String url = values.getAsString(Images.URL);
trace("Updating image for URL: " + url);
final SQLiteDatabase db = getWritableDatabase(uri);
long now = System.currentTimeMillis();
// Thumbnails update on every page load. We don't want to flood
// sync with meaningless last modified date. Only update modified
// date when favicons bits change.
if (values.containsKey(Images.FAVICON) || values.containsKey(Images.FAVICON_URL))
values.put(Images.DATE_MODIFIED, now);
// Restore and update an existing image record marked as
// deleted if possible.
if (insertIfNeeded)
values.put(Images.IS_DELETED, 0);
debug("Trying to update image for URL: " + url);
int updated = db.update(TABLE_IMAGES, values, selection, selectionArgs);
if (updated == 0 && insertIfNeeded) {
// Generate GUID for new image, if one is not already provided.
if (!values.containsKey(Images.GUID)) {
values.put(Images.GUID, Utils.generateGuid());
}
values.put(Images.DATE_CREATED, now);
values.put(Images.DATE_MODIFIED, now);
trace("No update, inserting image for URL: " + url);
db.insert(TABLE_IMAGES, Images.FAVICON, values);
updated = 1;
}
return updated;
}
int deleteHistory(Uri uri, String selection, String[] selectionArgs) {
debug("Deleting history entry for URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
if (isCallerSync(uri)) {
return db.delete(TABLE_HISTORY, selection, selectionArgs);
}
debug("Marking history entry as deleted for URI: " + uri);
ContentValues values = new ContentValues();
values.put(History.IS_DELETED, 1);
// Wipe sensitive data.
values.putNull(History.TITLE);
values.put(History.URL, ""); // Column is NOT NULL.
values.put(History.DATE_CREATED, 0);
values.put(History.DATE_LAST_VISITED, 0);
values.put(History.VISITS, 0);
cleanupSomeDeletedRecords(uri, History.CONTENT_URI, TABLE_HISTORY);
return updateHistory(uri, values, selection, selectionArgs);
}
int deleteBookmarks(Uri uri, String selection, String[] selectionArgs) {
debug("Deleting bookmarks for URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
if (isCallerSync(uri)) {
return db.delete(TABLE_BOOKMARKS, selection, selectionArgs);
}
debug("Marking bookmarks as deleted for URI: " + uri);
ContentValues values = new ContentValues();
values.put(Bookmarks.IS_DELETED, 1);
cleanupSomeDeletedRecords(uri, Bookmarks.CONTENT_URI, TABLE_BOOKMARKS);
return updateBookmarks(uri, values, selection, selectionArgs);
}
int deleteImages(Uri uri, String selection, String[] selectionArgs) {
debug("Deleting images for URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
if (isCallerSync(uri)) {
return db.delete(TABLE_IMAGES, selection, null);
}
debug("Marking images as deleted for URI: " + uri);
ContentValues values = new ContentValues();
values.put(History.IS_DELETED, 1);
cleanupSomeDeletedRecords(uri, Images.CONTENT_URI, TABLE_IMAGES);
return updateExistingImage(uri, values, selection, null);
}
int deleteUnusedImages(Uri uri) {
debug("Deleting all unused images for URI: " + uri);
String selection = Images.URL + " NOT IN (SELECT " + Bookmarks.URL +
" FROM " + TABLE_BOOKMARKS + " WHERE " + Bookmarks.URL + " IS NOT NULL AND " +
qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " = 0) AND " +
Images.URL + " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY +
" WHERE " + History.URL + " IS NOT NULL AND " +
qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0)";
return deleteImages(uri, selection, null);
}
}