pt 1 in reviving the android build (copied from pm 28a1, wish me luck)

This commit is contained in:
wuggy 2026-04-08 15:43:25 -07:00
commit d7788a6d6d
4249 changed files with 468189 additions and 0 deletions

View file

@ -0,0 +1,79 @@
/* 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.annotation.RobocopTarget;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
/**
* The base class for ContentProviders that wish to use a different DB
* for each profile.
*
* This class has logic shared between ordinary per-profile CPs and
* those that wish to share DB connections between CPs.
*/
public abstract class AbstractPerProfileDatabaseProvider extends AbstractTransactionalProvider {
/**
* Extend this to provide access to your own map of shared databases. This
* is a method so that your subclass doesn't collide with others!
*/
protected abstract PerProfileDatabases<? extends SQLiteOpenHelper> getDatabases();
/*
* Fetches a readable database based on the profile indicated in the
* passed URI. If the URI does not contain a profile param, the default profile
* is used.
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a readable SQLiteDatabase
*/
@Override
protected SQLiteDatabase getReadableDatabase(Uri uri) {
String profile = null;
if (uri != null) {
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
}
return getDatabases().getDatabaseHelperForProfile(profile, isTest(uri)).getReadableDatabase();
}
/*
* Fetches a writable database based on the profile indicated in the
* passed URI. If the URI does not contain a profile param, the default profile
* is used
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a writable SQLiteDatabase
*/
@Override
protected SQLiteDatabase getWritableDatabase(Uri uri) {
String profile = null;
if (uri != null) {
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
}
return getDatabases().getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase();
}
protected SQLiteDatabase getWritableDatabaseForProfile(String profile, boolean isTest) {
return getDatabases().getDatabaseHelperForProfile(profile, isTest).getWritableDatabase();
}
/**
* This method should ONLY be used for testing purposes.
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a writable SQLiteDatabase
*/
@Override
@RobocopTarget
public SQLiteDatabase getWritableDatabaseForTesting(Uri uri) {
return getWritableDatabase(uri);
}
}

View file

@ -0,0 +1,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);
}
}
}

View file

@ -0,0 +1,64 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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/. */
package org.mozilla.gecko.db;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.util.Log;
// BaseTable provides a basic implementation of a Table for tables that don't require advanced operations during
// insert, delete, update, or query operations. Implementors must still provide onCreate and onUpgrade operations.
public abstract class BaseTable implements Table {
private static final String LOGTAG = "GeckoBaseTable";
private static final boolean DEBUG = false;
protected static void log(String msg) {
if (DEBUG) {
Log.i(LOGTAG, msg);
}
}
// Table implementation
@Override
public Table.ContentProviderInfo[] getContentProviderInfo() {
return new Table.ContentProviderInfo[0];
}
// Returns the name of the table to modify/query
protected abstract String getTable();
// Table implementation
@Override
public Cursor query(SQLiteDatabase db, Uri uri, int dbId, String[] columns, String selection, String[] selectionArgs, String sortOrder, String groupBy, String limit) {
Cursor c = db.query(getTable(), columns, selection, selectionArgs, groupBy, null, sortOrder, limit);
log("query " + columns + " in " + selection + " = " + c);
return c;
}
@Override
public int update(SQLiteDatabase db, Uri uri, int dbId, ContentValues values, String selection, String[] selectionArgs) {
int updated = db.updateWithOnConflict(getTable(), values, selection, selectionArgs, SQLiteDatabase.CONFLICT_REPLACE);
log("update " + values + " in " + selection + " = " + updated);
return updated;
}
@Override
public long insert(SQLiteDatabase db, Uri uri, int dbId, ContentValues values) {
long inserted = db.insertOrThrow(getTable(), null, values);
log("insert " + values + " = " + inserted);
return inserted;
}
@Override
public int delete(SQLiteDatabase db, Uri uri, int dbId, String selection, String[] selectionArgs) {
int deleted = db.delete(getTable(), selection, selectionArgs);
log("delete " + selection + " = " + deleted);
return deleted;
}
};

View file

@ -0,0 +1,785 @@
/* -*- 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/. */
package org.mozilla.gecko.db;
import org.mozilla.gecko.AppConstants;
import android.net.Uri;
import android.support.annotation.NonNull;
import org.mozilla.gecko.annotation.RobocopTarget;
@RobocopTarget
public class BrowserContract {
public static final String AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.browser";
public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
public static final String PASSWORDS_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.passwords";
public static final Uri PASSWORDS_AUTHORITY_URI = Uri.parse("content://" + PASSWORDS_AUTHORITY);
public static final String FORM_HISTORY_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.formhistory";
public static final Uri FORM_HISTORY_AUTHORITY_URI = Uri.parse("content://" + FORM_HISTORY_AUTHORITY);
public static final String TABS_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.tabs";
public static final Uri TABS_AUTHORITY_URI = Uri.parse("content://" + TABS_AUTHORITY);
public static final String HOME_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.home";
public static final Uri HOME_AUTHORITY_URI = Uri.parse("content://" + HOME_AUTHORITY);
public static final String PROFILES_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".profiles";
public static final Uri PROFILES_AUTHORITY_URI = Uri.parse("content://" + PROFILES_AUTHORITY);
public static final String READING_LIST_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.readinglist";
public static final Uri READING_LIST_AUTHORITY_URI = Uri.parse("content://" + READING_LIST_AUTHORITY);
public static final String SEARCH_HISTORY_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.searchhistory";
public static final Uri SEARCH_HISTORY_AUTHORITY_URI = Uri.parse("content://" + SEARCH_HISTORY_AUTHORITY);
public static final String LOGINS_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.logins";
public static final Uri LOGINS_AUTHORITY_URI = Uri.parse("content://" + LOGINS_AUTHORITY);
public static final String PARAM_PROFILE = "profile";
public static final String PARAM_PROFILE_PATH = "profilePath";
public static final String PARAM_LIMIT = "limit";
public static final String PARAM_SUGGESTEDSITES_LIMIT = "suggestedsites_limit";
public static final String PARAM_TOPSITES_DISABLE_PINNED = "topsites_disable_pinned";
public static final String PARAM_IS_SYNC = "sync";
public static final String PARAM_SHOW_DELETED = "show_deleted";
public static final String PARAM_IS_TEST = "test";
public static final String PARAM_INSERT_IF_NEEDED = "insert_if_needed";
public static final String PARAM_INCREMENT_VISITS = "increment_visits";
public static final String PARAM_INCREMENT_REMOTE_AGGREGATES = "increment_remote_aggregates";
public static final String PARAM_EXPIRE_PRIORITY = "priority";
public static final String PARAM_DATASET_ID = "dataset_id";
public static final String PARAM_GROUP_BY = "group_by";
static public enum ExpirePriority {
NORMAL,
AGGRESSIVE
}
/**
* Produces a SQL expression used for sorting results of the "combined" view by frecency.
* Combines remote and local frecency calculations, weighting local visits much heavier.
*
* @param includesBookmarks When URL is bookmarked, should we give it bonus frecency points?
* @param ascending Indicates if sorting order ascending
* @return Combined frecency sorting expression
*/
static public String getCombinedFrecencySortOrder(boolean includesBookmarks, boolean ascending) {
final long now = System.currentTimeMillis();
StringBuilder order = new StringBuilder(getRemoteFrecencySQL(now) + " + " + getLocalFrecencySQL(now));
if (includesBookmarks) {
order.insert(0, "(CASE WHEN " + Combined.BOOKMARK_ID + " > -1 THEN 100 ELSE 0 END) + ");
}
order.append(ascending ? " ASC" : " DESC");
return order.toString();
}
/**
* See Bug 1265525 for details (explanation + graphs) on how Remote frecency compares to Local frecency for different
* combinations of visits count and age.
*
* @param now Base time in milliseconds for age calculation
* @return remote frecency SQL calculation
*/
static public String getRemoteFrecencySQL(final long now) {
return getFrecencyCalculation(now, 1, 110, Combined.REMOTE_VISITS_COUNT, Combined.REMOTE_DATE_LAST_VISITED);
}
/**
* Local frecency SQL calculation. Note higher scale factor and squared visit count which achieve
* visits generated locally being much preferred over remote visits.
* See Bug 1265525 for details (explanation + comparison graphs).
*
* @param now Base time in milliseconds for age calculation
* @return local frecency SQL calculation
*/
static public String getLocalFrecencySQL(final long now) {
String visitCountExpr = "(" + Combined.LOCAL_VISITS_COUNT + " + 2)";
visitCountExpr = visitCountExpr + " * " + visitCountExpr;
return getFrecencyCalculation(now, 2, 225, visitCountExpr, Combined.LOCAL_DATE_LAST_VISITED);
}
/**
* Our version of frecency is computed by scaling the number of visits by a multiplier
* that approximates Gaussian decay, based on how long ago the entry was last visited.
* Since we're limited by the math we can do with sqlite, we're calculating this
* approximation using the Cauchy distribution: multiplier = scale_const / (age^2 + scale_const).
* For example, with 15 as our scale parameter, we get a scale constant 15^2 = 225. Then:
* frecencyScore = numVisits * max(1, 100 * 225 / (age*age + 225)). (See bug 704977)
*
* @param now Base time in milliseconds for age calculation
* @param minFrecency Minimum allowed frecency value
* @param multiplier Scale constant
* @param visitCountExpr Expression which will produce a visit count
* @param lastVisitExpr Expression which will produce "last-visited" timestamp
* @return Frecency SQL calculation
*/
static public String getFrecencyCalculation(final long now, final int minFrecency, final int multiplier, @NonNull final String visitCountExpr, @NonNull final String lastVisitExpr) {
final long nowInMicroseconds = now * 1000;
final long microsecondsPerDay = 86400000000L;
final String ageExpr = "(" + nowInMicroseconds + " - " + lastVisitExpr + ") / " + microsecondsPerDay;
return visitCountExpr + " * MAX(" + minFrecency + ", 100 * " + multiplier + " / (" + ageExpr + " * " + ageExpr + " + " + multiplier + "))";
}
@RobocopTarget
public interface CommonColumns {
public static final String _ID = "_id";
}
@RobocopTarget
public interface DateSyncColumns {
public static final String DATE_CREATED = "created";
public static final String DATE_MODIFIED = "modified";
}
@RobocopTarget
public interface SyncColumns extends DateSyncColumns {
public static final String GUID = "guid";
public static final String IS_DELETED = "deleted";
}
@RobocopTarget
public interface URLColumns {
public static final String URL = "url";
public static final String TITLE = "title";
}
@RobocopTarget
public interface FaviconColumns {
public static final String FAVICON = "favicon";
public static final String FAVICON_ID = "favicon_id";
public static final String FAVICON_URL = "favicon_url";
}
@RobocopTarget
public interface HistoryColumns {
public static final String DATE_LAST_VISITED = "date";
public static final String VISITS = "visits";
// Aggregates used to speed up top sites and search frecency-powered queries
public static final String LOCAL_VISITS = "visits_local";
public static final String REMOTE_VISITS = "visits_remote";
public static final String LOCAL_DATE_LAST_VISITED = "date_local";
public static final String REMOTE_DATE_LAST_VISITED = "date_remote";
}
@RobocopTarget
public interface VisitsColumns {
public static final String HISTORY_GUID = "history_guid";
public static final String VISIT_TYPE = "visit_type";
public static final String DATE_VISITED = "date";
// Used to distinguish between visits that were generated locally vs those that came in from Sync.
// Since we don't track "origin clientID" for visits, this is the best we can do for now.
public static final String IS_LOCAL = "is_local";
}
public interface PageMetadataColumns {
public static final String HISTORY_GUID = "history_guid";
public static final String DATE_CREATED = "created";
public static final String HAS_IMAGE = "has_image";
public static final String JSON = "json";
}
public interface DeletedColumns {
public static final String ID = "id";
public static final String GUID = "guid";
public static final String TIME_DELETED = "timeDeleted";
}
@RobocopTarget
public static final class Favicons implements CommonColumns, DateSyncColumns {
private Favicons() {}
public static final String TABLE_NAME = "favicons";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "favicons");
public static final String URL = "url";
public static final String DATA = "data";
public static final String PAGE_URL = "page_url";
}
@RobocopTarget
public static final class Thumbnails implements CommonColumns {
private Thumbnails() {}
public static final String TABLE_NAME = "thumbnails";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "thumbnails");
public static final String URL = "url";
public static final String DATA = "data";
}
public static final class Profiles {
private Profiles() {}
public static final String NAME = "name";
public static final String PATH = "path";
}
@RobocopTarget
public static final class Bookmarks implements CommonColumns, URLColumns, FaviconColumns, SyncColumns {
private Bookmarks() {}
public static final String TABLE_NAME = "bookmarks";
public static final String VIEW_WITH_FAVICONS = "bookmarks_with_favicons";
public static final String VIEW_WITH_ANNOTATIONS = "bookmarks_with_annotations";
public static final int FIXED_ROOT_ID = 0;
public static final int FAKE_DESKTOP_FOLDER_ID = -1;
public static final int FIXED_READING_LIST_ID = -2;
public static final int FIXED_PINNED_LIST_ID = -3;
public static final int FIXED_SCREENSHOT_FOLDER_ID = -4;
public static final int FAKE_READINGLIST_SMARTFOLDER_ID = -5;
/**
* This ID and the following negative IDs are reserved for bookmarks from Android's partner
* bookmark provider.
*/
public static final long FAKE_PARTNER_BOOKMARKS_START = -1000;
public static final String MOBILE_FOLDER_GUID = "mobile";
public static final String PLACES_FOLDER_GUID = "places";
public static final String MENU_FOLDER_GUID = "menu";
public static final String TAGS_FOLDER_GUID = "tags";
public static final String TOOLBAR_FOLDER_GUID = "toolbar";
public static final String UNFILED_FOLDER_GUID = "unfiled";
public static final String FAKE_DESKTOP_FOLDER_GUID = "desktop";
public static final String PINNED_FOLDER_GUID = "pinned";
public static final String SCREENSHOT_FOLDER_GUID = "screenshots";
public static final String FAKE_READINGLIST_SMARTFOLDER_GUID = "readinglist";
public static final int TYPE_FOLDER = 0;
public static final int TYPE_BOOKMARK = 1;
public static final int TYPE_SEPARATOR = 2;
public static final int TYPE_LIVEMARK = 3;
public static final int TYPE_QUERY = 4;
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "bookmarks");
public static final Uri PARENTS_CONTENT_URI = Uri.withAppendedPath(CONTENT_URI, "parents");
// Hacky API for bulk-updating positions. Bug 728783.
public static final Uri POSITIONS_CONTENT_URI = Uri.withAppendedPath(CONTENT_URI, "positions");
public static final long DEFAULT_POSITION = Long.MIN_VALUE;
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/bookmark";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/bookmark";
public static final String TYPE = "type";
public static final String PARENT = "parent";
public static final String POSITION = "position";
public static final String TAGS = "tags";
public static final String DESCRIPTION = "description";
public static final String KEYWORD = "keyword";
public static final String ANNOTATION_KEY = "annotation_key";
public static final String ANNOTATION_VALUE = "annotation_value";
}
@RobocopTarget
public static final class History implements CommonColumns, URLColumns, HistoryColumns, FaviconColumns, SyncColumns {
private History() {}
public static final String TABLE_NAME = "history";
public static final String VIEW_WITH_FAVICONS = "history_with_favicons";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "history");
public static final Uri CONTENT_OLD_URI = Uri.withAppendedPath(AUTHORITY_URI, "history/old");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/browser-history";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/browser-history";
}
@RobocopTarget
public static final class Visits implements CommonColumns, VisitsColumns {
private Visits() {}
public static final String TABLE_NAME = "visits";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "visits");
public static final int VISIT_IS_LOCAL = 1;
public static final int VISIT_IS_REMOTE = 0;
}
// Combined bookmarks and history
@RobocopTarget
public static final class Combined implements CommonColumns, URLColumns, HistoryColumns, FaviconColumns {
private Combined() {}
public static final String VIEW_NAME = "combined";
public static final String VIEW_WITH_FAVICONS = "combined_with_favicons";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "combined");
public static final String BOOKMARK_ID = "bookmark_id";
public static final String HISTORY_ID = "history_id";
public static final String REMOTE_VISITS_COUNT = "remoteVisitCount";
public static final String REMOTE_DATE_LAST_VISITED = "remoteDateLastVisited";
public static final String LOCAL_VISITS_COUNT = "localVisitCount";
public static final String LOCAL_DATE_LAST_VISITED = "localDateLastVisited";
}
public static final class Schema {
private Schema() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "schema");
public static final String VERSION = "version";
}
public static final class Passwords {
private Passwords() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(PASSWORDS_AUTHORITY_URI, "passwords");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/passwords";
public static final String ID = "id";
public static final String HOSTNAME = "hostname";
public static final String HTTP_REALM = "httpRealm";
public static final String FORM_SUBMIT_URL = "formSubmitURL";
public static final String USERNAME_FIELD = "usernameField";
public static final String PASSWORD_FIELD = "passwordField";
public static final String ENCRYPTED_USERNAME = "encryptedUsername";
public static final String ENCRYPTED_PASSWORD = "encryptedPassword";
public static final String ENC_TYPE = "encType";
public static final String TIME_CREATED = "timeCreated";
public static final String TIME_LAST_USED = "timeLastUsed";
public static final String TIME_PASSWORD_CHANGED = "timePasswordChanged";
public static final String TIMES_USED = "timesUsed";
public static final String GUID = "guid";
// This needs to be kept in sync with the types defined in toolkit/components/passwordmgr/nsILoginManagerCrypto.idl#45
public static final int ENCTYPE_SDR = 1;
}
public static final class DeletedPasswords implements DeletedColumns {
private DeletedPasswords() {}
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/deleted-passwords";
public static final Uri CONTENT_URI = Uri.withAppendedPath(PASSWORDS_AUTHORITY_URI, "deleted-passwords");
}
@RobocopTarget
public static final class GeckoDisabledHosts {
private GeckoDisabledHosts() {}
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/disabled-hosts";
public static final Uri CONTENT_URI = Uri.withAppendedPath(PASSWORDS_AUTHORITY_URI, "disabled-hosts");
public static final String HOSTNAME = "hostname";
}
public static final class FormHistory {
private FormHistory() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(FORM_HISTORY_AUTHORITY_URI, "formhistory");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/formhistory";
public static final String ID = "id";
public static final String FIELD_NAME = "fieldname";
public static final String VALUE = "value";
public static final String TIMES_USED = "timesUsed";
public static final String FIRST_USED = "firstUsed";
public static final String LAST_USED = "lastUsed";
public static final String GUID = "guid";
}
public static final class DeletedFormHistory implements DeletedColumns {
private DeletedFormHistory() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(FORM_HISTORY_AUTHORITY_URI, "deleted-formhistory");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/deleted-formhistory";
}
@RobocopTarget
public static final class Tabs implements CommonColumns {
private Tabs() {}
public static final String TABLE_NAME = "tabs";
public static final Uri CONTENT_URI = Uri.withAppendedPath(TABS_AUTHORITY_URI, "tabs");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/tab";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/tab";
// Title of the tab.
public static final String TITLE = "title";
// Topmost URL from the history array. Allows processing of this tab without
// parsing that array.
public static final String URL = "url";
// Sync-assigned GUID for client device. NULL for local tabs.
public static final String CLIENT_GUID = "client_guid";
// JSON-encoded array of history URL strings, from most recent to least recent.
public static final String HISTORY = "history";
// Favicon URL for the tab's topmost history entry.
public static final String FAVICON = "favicon";
// Last used time of the tab.
public static final String LAST_USED = "last_used";
// Position of the tab. 0 represents foreground.
public static final String POSITION = "position";
}
public static final class Clients implements CommonColumns {
private Clients() {}
public static final Uri CONTENT_RECENCY_URI = Uri.withAppendedPath(TABS_AUTHORITY_URI, "clients_recency");
public static final Uri CONTENT_URI = Uri.withAppendedPath(TABS_AUTHORITY_URI, "clients");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/client";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/client";
// Client-provided name string. Could conceivably be null.
public static final String NAME = "name";
// Sync-assigned GUID for client device. NULL for local tabs.
public static final String GUID = "guid";
// Last modified time for the client's tab record. For remote records, a server
// timestamp provided by Sync during insertion.
public static final String LAST_MODIFIED = "last_modified";
public static final String DEVICE_TYPE = "device_type";
}
// Data storage for dynamic panels on about:home
@RobocopTarget
public static final class HomeItems implements CommonColumns {
private HomeItems() {}
public static final Uri CONTENT_FAKE_URI = Uri.withAppendedPath(HOME_AUTHORITY_URI, "items/fake");
public static final Uri CONTENT_URI = Uri.withAppendedPath(HOME_AUTHORITY_URI, "items");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/homeitem";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/homeitem";
public static final String DATASET_ID = "dataset_id";
public static final String URL = "url";
public static final String TITLE = "title";
public static final String DESCRIPTION = "description";
public static final String IMAGE_URL = "image_url";
public static final String BACKGROUND_COLOR = "background_color";
public static final String BACKGROUND_URL = "background_url";
public static final String CREATED = "created";
public static final String FILTER = "filter";
public static final String[] DEFAULT_PROJECTION =
new String[] { _ID, DATASET_ID, URL, TITLE, DESCRIPTION, IMAGE_URL, BACKGROUND_COLOR, BACKGROUND_URL, FILTER };
}
@RobocopTarget
public static final class ReadingListItems implements CommonColumns, URLColumns {
public static final String EXCERPT = "excerpt";
public static final String CLIENT_LAST_MODIFIED = "client_last_modified";
public static final String GUID = "guid";
public static final String SERVER_LAST_MODIFIED = "last_modified";
public static final String SERVER_STORED_ON = "stored_on";
public static final String ADDED_ON = "added_on";
public static final String MARKED_READ_ON = "marked_read_on";
public static final String IS_DELETED = "is_deleted";
public static final String IS_ARCHIVED = "is_archived";
public static final String IS_UNREAD = "is_unread";
public static final String IS_ARTICLE = "is_article";
public static final String IS_FAVORITE = "is_favorite";
public static final String RESOLVED_URL = "resolved_url";
public static final String RESOLVED_TITLE = "resolved_title";
public static final String ADDED_BY = "added_by";
public static final String MARKED_READ_BY = "marked_read_by";
public static final String WORD_COUNT = "word_count";
public static final String READ_POSITION = "read_position";
public static final String CONTENT_STATUS = "content_status";
public static final String SYNC_STATUS = "sync_status";
public static final String SYNC_CHANGE_FLAGS = "sync_change_flags";
private ReadingListItems() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(READING_LIST_AUTHORITY_URI, "items");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/readinglistitem";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/readinglistitem";
// CONTENT_STATUS represents the result of an attempt to fetch content for the reading list item.
public static final int STATUS_UNFETCHED = 0;
public static final int STATUS_FETCH_FAILED_TEMPORARY = 1;
public static final int STATUS_FETCH_FAILED_PERMANENT = 2;
public static final int STATUS_FETCH_FAILED_UNSUPPORTED_FORMAT = 3;
public static final int STATUS_FETCHED_ARTICLE = 4;
// See https://github.com/mozilla-services/readinglist/wiki/Client-phases for how this is expected to work.
//
// If an item is SYNCED, it doesn't need to be uploaded.
//
// If its status is NEW, the entire record should be uploaded.
//
// If DELETED, the record should be deleted. A record can only move into this state from SYNCED; NEW records
// are deleted immediately.
//
public static final int SYNC_STATUS_SYNCED = 0;
public static final int SYNC_STATUS_NEW = 1; // Upload everything.
public static final int SYNC_STATUS_DELETED = 2; // Delete the record from the server.
public static final int SYNC_STATUS_MODIFIED = 3; // Consult SYNC_CHANGE_FLAGS.
// SYNC_CHANGE_FLAG represents the sets of fields that need to be uploaded.
// If its status is only UNREAD_CHANGED (and maybe FAVORITE_CHANGED?), then it can easily be uploaded
// in a fire-and-forget manner. This change can never conflict.
//
// If its status is RESOLVED, then one or more of the content-oriented fields has changed, and a full
// upload of those fields should occur. These can result in conflicts.
//
// Note that these are flags; they should be considered together when deciding on a course of action.
//
// These flags are meaningless for records in any state other than SYNCED. They can be safely altered in
// other states (to avoid having to query to pre-fill a ContentValues), but should be ignored.
public static final int SYNC_CHANGE_NONE = 0;
public static final int SYNC_CHANGE_UNREAD_CHANGED = 1 << 0; // => marked_read_{on,by}, is_unread
public static final int SYNC_CHANGE_FAVORITE_CHANGED = 1 << 1; // => is_favorite
public static final int SYNC_CHANGE_RESOLVED = 1 << 2; // => is_article, resolved_{url,title}, excerpt, word_count
public static final String DEFAULT_SORT_ORDER = CLIENT_LAST_MODIFIED + " DESC";
public static final String[] DEFAULT_PROJECTION = new String[] { _ID, URL, TITLE, EXCERPT, WORD_COUNT, IS_UNREAD };
// Minimum fields required to create a reading list item.
public static final String[] REQUIRED_FIELDS = { ReadingListItems.URL, ReadingListItems.TITLE };
// All fields that might be mapped from the DB into a record object.
public static final String[] ALL_FIELDS = {
CommonColumns._ID,
URLColumns.URL,
URLColumns.TITLE,
EXCERPT,
CLIENT_LAST_MODIFIED,
GUID,
SERVER_LAST_MODIFIED,
SERVER_STORED_ON,
ADDED_ON,
MARKED_READ_ON,
IS_DELETED,
IS_ARCHIVED,
IS_UNREAD,
IS_ARTICLE,
IS_FAVORITE,
RESOLVED_URL,
RESOLVED_TITLE,
ADDED_BY,
MARKED_READ_BY,
WORD_COUNT,
READ_POSITION,
CONTENT_STATUS,
SYNC_STATUS,
SYNC_CHANGE_FLAGS,
};
public static final String TABLE_NAME = "reading_list";
}
@RobocopTarget
public static final class TopSites implements CommonColumns, URLColumns {
private TopSites() {}
public static final int TYPE_BLANK = 0;
public static final int TYPE_TOP = 1;
public static final int TYPE_PINNED = 2;
public static final int TYPE_SUGGESTED = 3;
public static final String BOOKMARK_ID = "bookmark_id";
public static final String HISTORY_ID = "history_id";
public static final String TYPE = "type";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "topsites");
}
public static final class Highlights {
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "highlights");
public static final String DATE = "date";
}
@RobocopTarget
public static final class SearchHistory implements CommonColumns, HistoryColumns {
private SearchHistory() {}
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/searchhistory";
public static final String QUERY = "query";
public static final String DATE = "date";
public static final String TABLE_NAME = "searchhistory";
public static final Uri CONTENT_URI = Uri.withAppendedPath(SEARCH_HISTORY_AUTHORITY_URI, "searchhistory");
}
@RobocopTarget
public static final class SuggestedSites implements CommonColumns, URLColumns {
private SuggestedSites() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "suggestedsites");
}
public static final class ActivityStreamBlocklist implements CommonColumns {
private ActivityStreamBlocklist() {}
public static final String TABLE_NAME = "activity_stream_blocklist";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, TABLE_NAME);
public static final String URL = "url";
public static final String CREATED = "created";
}
@RobocopTarget
public static final class UrlAnnotations implements CommonColumns, DateSyncColumns {
private UrlAnnotations() {}
public static final String TABLE_NAME = "urlannotations";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, TABLE_NAME);
public static final String URL = "url";
public static final String KEY = "key";
public static final String VALUE = "value";
public static final String SYNC_STATUS = "sync_status";
public enum Key {
// We use a parameter, rather than name(), as defensive coding: we can't let the
// enum name change because we've already stored values into the DB.
SCREENSHOT ("screenshot"),
/**
* This key maps URLs to its feeds.
*
* Key: feed
* Value: URL of feed
*/
FEED("feed"),
/**
* This key maps URLs of feeds to an object describing the feed.
*
* Key: feed_subscription
* Value: JSON object describing feed
*/
FEED_SUBSCRIPTION("feed_subscription"),
/**
* Indicates that this URL (if stored as a bookmark) should be opened into reader view.
*
* Key: reader_view
* Value: String "true" to indicate that we would like to open into reader view.
*/
READER_VIEW("reader_view"),
/**
* Indicator that the user interacted with the URL in regards to home screen shortcuts.
*
* Key: home_screen_shortcut
* Value: True: User created an home screen shortcut for this URL
* False: User declined to create a shortcut for this URL
*/
HOME_SCREEN_SHORTCUT("home_screen_shortcut");
private final String dbValue;
Key(final String dbValue) { this.dbValue = dbValue; }
public String getDbValue() { return dbValue; }
}
public enum SyncStatus {
// We use a parameter, rather than ordinal(), as defensive coding: we can't let the
// ordinal values change because we've already stored values into the DB.
NEW (0);
// Value stored into the database for this column.
private final int dbValue;
SyncStatus(final int dbValue) {
this.dbValue = dbValue;
}
public int getDBValue() { return dbValue; }
}
/**
* Value used to indicate that a reader view item is saved. We use the
*/
public static final String READER_VIEW_SAVED_VALUE = "true";
}
public static final class Numbers {
private Numbers() {}
public static final String TABLE_NAME = "numbers";
public static final String POSITION = "position";
public static final int MAX_VALUE = 50;
}
@RobocopTarget
public static final class Logins implements CommonColumns {
private Logins() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(LOGINS_AUTHORITY_URI, "logins");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/logins";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/logins";
public static final String TABLE_LOGINS = "logins";
public static final String HOSTNAME = "hostname";
public static final String HTTP_REALM = "httpRealm";
public static final String FORM_SUBMIT_URL = "formSubmitURL";
public static final String USERNAME_FIELD = "usernameField";
public static final String PASSWORD_FIELD = "passwordField";
public static final String ENCRYPTED_USERNAME = "encryptedUsername";
public static final String ENCRYPTED_PASSWORD = "encryptedPassword";
public static final String ENC_TYPE = "encType";
public static final String TIME_CREATED = "timeCreated";
public static final String TIME_LAST_USED = "timeLastUsed";
public static final String TIME_PASSWORD_CHANGED = "timePasswordChanged";
public static final String TIMES_USED = "timesUsed";
public static final String GUID = "guid";
}
@RobocopTarget
public static final class DeletedLogins implements CommonColumns {
private DeletedLogins() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(LOGINS_AUTHORITY_URI, "deleted-logins");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/deleted-logins";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/deleted-logins";
public static final String TABLE_DELETED_LOGINS = "deleted_logins";
public static final String GUID = "guid";
public static final String TIME_DELETED = "timeDeleted";
}
@RobocopTarget
public static final class LoginsDisabledHosts implements CommonColumns {
private LoginsDisabledHosts() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(LOGINS_AUTHORITY_URI, "logins-disabled-hosts");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/logins-disabled-hosts";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/logins-disabled-hosts";
public static final String TABLE_DISABLED_HOSTS = "logins_disabled_hosts";
public static final String HOSTNAME = "hostname";
}
@RobocopTarget
public static final class PageMetadata implements CommonColumns, PageMetadataColumns {
private PageMetadata() {}
public static final String TABLE_NAME = "page_metadata";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "page_metadata");
}
// We refer to the service by name to decouple services from the rest of the code base.
public static final String TAB_RECEIVED_SERVICE_CLASS_NAME = "org.mozilla.gecko.tabqueue.TabReceivedService";
public static final String SKIP_TAB_QUEUE_FLAG = "skip_tab_queue";
public static final String EXTRA_CLIENT_GUID = "org.mozilla.gecko.extra.CLIENT_ID";
}

View file

@ -0,0 +1,205 @@
/* 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.io.File;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.db.BrowserContract.ExpirePriority;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.graphics.drawable.BitmapDrawable;
import android.support.v4.content.CursorLoader;
/**
* Interface for interactions with all databases. If you want an instance
* that implements this, you should go through GeckoProfile. E.g.,
* <code>BrowserDB.from(context)</code>.
*/
public abstract class BrowserDB {
public static enum FilterFlags {
EXCLUDE_PINNED_SITES
}
public abstract Searches getSearches();
public abstract TabsAccessor getTabsAccessor();
public abstract URLMetadata getURLMetadata();
@RobocopTarget public abstract UrlAnnotations getUrlAnnotations();
/**
* Add default bookmarks to the database.
* Takes an offset; returns a new offset.
*/
public abstract int addDefaultBookmarks(Context context, ContentResolver cr, int offset);
/**
* Add bookmarks from the provided distribution.
* Takes an offset; returns a new offset.
*/
public abstract int addDistributionBookmarks(ContentResolver cr, Distribution distribution, int offset);
/**
* Invalidate cached data.
*/
public abstract void invalidate();
public abstract int getCount(ContentResolver cr, String database);
/**
* @return a cursor representing the contents of the DB filtered according to the arguments.
* Can return <code>null</code>. <code>CursorLoader</code> will handle this correctly.
*/
public abstract Cursor filter(ContentResolver cr, CharSequence constraint,
int limit, EnumSet<BrowserDB.FilterFlags> flags);
/**
* @return a cursor over top sites (high-ranking bookmarks and history).
* Can return <code>null</code>.
* Returns no more than <code>limit</code> results.
* Suggested sites will be limited to being within the first <code>suggestedRangeLimit</code> results.
*/
public abstract Cursor getTopSites(ContentResolver cr, int suggestedRangeLimit, int limit);
public abstract CursorLoader getActivityStreamTopSites(Context context, int limit);
public abstract void updateVisitedHistory(ContentResolver cr, String uri);
public abstract void updateHistoryTitle(ContentResolver cr, String uri, String title);
/**
* Can return <code>null</code>.
*/
public abstract Cursor getAllVisitedHistory(ContentResolver cr);
/**
* Can return <code>null</code>.
*/
public abstract Cursor getRecentHistory(ContentResolver cr, int limit);
public abstract Cursor getHistoryForURL(ContentResolver cr, String uri);
public abstract Cursor getRecentHistoryBetweenTime(ContentResolver cr, int historyLimit, long start, long end);
public abstract long getPrePathLastVisitedTimeMilliseconds(ContentResolver cr, String prePath);
public abstract void expireHistory(ContentResolver cr, ExpirePriority priority);
public abstract void removeHistoryEntry(ContentResolver cr, String url);
public abstract void clearHistory(ContentResolver cr, boolean clearSearchHistory);
public abstract String getUrlForKeyword(ContentResolver cr, String keyword);
public abstract boolean isBookmark(ContentResolver cr, String uri);
public abstract boolean addBookmark(ContentResolver cr, String title, String uri);
public abstract Cursor getBookmarkForUrl(ContentResolver cr, String url);
public abstract Cursor getBookmarksForPartialUrl(ContentResolver cr, String partialUrl);
public abstract void removeBookmarksWithURL(ContentResolver cr, String uri);
public abstract void registerBookmarkObserver(ContentResolver cr, ContentObserver observer);
public abstract void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword);
public abstract boolean hasBookmarkWithGuid(ContentResolver cr, String guid);
public abstract boolean insertPageMetadata(ContentProviderClient contentProviderClient, String pageUrl, boolean hasImage, String metadataJSON);
public abstract int deletePageMetadata(ContentProviderClient contentProviderClient, String pageUrl);
/**
* Can return <code>null</code>.
*/
public abstract Cursor getBookmarksInFolder(ContentResolver cr, long folderId);
public abstract int getBookmarkCountForFolder(ContentResolver cr, long folderId);
/**
* Get the favicon from the database, if any, associated with the given favicon URL. (That is,
* the URL of the actual favicon image, not the URL of the page with which the favicon is associated.)
* @param cr The ContentResolver to use.
* @param faviconURL The URL of the favicon to fetch from the database.
* @return The decoded Bitmap from the database, if any. null if none is stored.
*/
public abstract LoadFaviconResult getFaviconForUrl(Context context, ContentResolver cr, String faviconURL);
/**
* Try to find a usable favicon URL in the history or bookmarks table.
*/
public abstract String getFaviconURLFromPageURL(ContentResolver cr, String uri);
public abstract byte[] getThumbnailForUrl(ContentResolver cr, String uri);
public abstract void updateThumbnailForUrl(ContentResolver cr, String uri, BitmapDrawable thumbnail);
/**
* Query for non-null thumbnails matching the provided <code>urls</code>.
* The returned cursor will have no more than, but possibly fewer than,
* the requested number of thumbnails.
*
* Returns null if the provided list of URLs is empty or null.
*/
public abstract Cursor getThumbnailsForUrls(ContentResolver cr,
List<String> urls);
public abstract void removeThumbnails(ContentResolver cr);
// Utility function for updating existing history using batch operations
public abstract void updateHistoryInBatch(ContentResolver cr,
Collection<ContentProviderOperation> operations, String url,
String title, long date, int visits);
public abstract void updateBookmarkInBatch(ContentResolver cr,
Collection<ContentProviderOperation> operations, String url,
String title, String guid, long parent, long added, long modified,
long position, String keyword, int type);
public abstract void pinSite(ContentResolver cr, String url, String title, int position);
public abstract void unpinSite(ContentResolver cr, int position);
public abstract boolean hideSuggestedSite(String url);
public abstract void setSuggestedSites(SuggestedSites suggestedSites);
public abstract SuggestedSites getSuggestedSites();
public abstract boolean hasSuggestedImageUrl(String url);
public abstract String getSuggestedImageUrlForUrl(String url);
public abstract int getSuggestedBackgroundColorForUrl(String url);
/**
* Obtain a set of links for highlights from bookmarks and history.
*
* @param context The context to load the cursor.
* @param limit Maximum number of results to return.
*/
public abstract CursorLoader getHighlights(Context context, int limit);
/**
* Block a page from the highlights list.
*
* @param url The page URL. Only pages exactly matching this URL will be blocked.
*/
public abstract void blockActivityStreamSite(ContentResolver cr, String url);
public static BrowserDB from(final Context context) {
return from(GeckoProfile.get(context));
}
public static BrowserDB from(final GeckoProfile profile) {
synchronized (profile.getLock()) {
BrowserDB db = (BrowserDB) profile.getData();
if (db != null) {
return db;
}
db = new LocalBrowserDB(profile.getName());
profile.setData(db);
return db;
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,450 @@
/* 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 android.annotation.TargetApi;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.os.Build;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoProfile;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.Telemetry;
import java.util.Map;
public class DBUtils {
private static final String LOGTAG = "GeckoDBUtils";
public static final int SQLITE_MAX_VARIABLE_NUMBER = 999;
public static final String qualifyColumn(String table, String column) {
return table + "." + column;
}
// This is available in Android >= 11. Implemented locally to be
// compatible with older versions.
public static String concatenateWhere(String a, String b) {
if (TextUtils.isEmpty(a)) {
return b;
}
if (TextUtils.isEmpty(b)) {
return a;
}
return "(" + a + ") AND (" + b + ")";
}
// This is available in Android >= 11. Implemented locally to be
// compatible with older versions.
public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) {
if (originalValues == null || originalValues.length == 0) {
return newValues;
}
if (newValues == null || newValues.length == 0) {
return originalValues;
}
String[] result = new String[originalValues.length + newValues.length];
System.arraycopy(originalValues, 0, result, 0, originalValues.length);
System.arraycopy(newValues, 0, result, originalValues.length, newValues.length);
return result;
}
/**
* Concatenate multiple lists of selection arguments. <code>values</code> may be <code>null</code>.
*/
public static String[] concatenateSelectionArgs(String[]... values) {
// Since we're most likely to be concatenating a few arrays of many values, it is most
// efficient to iterate over the arrays once to obtain their lengths, allowing us to create one target array
// (as opposed to copying arrays on every iteration, which would result in many more copies).
int totalLength = 0;
for (String[] v : values) {
if (v != null) {
totalLength += v.length;
}
}
String[] result = new String[totalLength];
int position = 0;
for (String[] v: values) {
if (v != null) {
int currentLength = v.length;
System.arraycopy(v, 0, result, position, currentLength);
position += currentLength;
}
}
return result;
}
public static void replaceKey(ContentValues aValues, String aOriginalKey,
String aNewKey, String aDefault) {
String value = aDefault;
if (aOriginalKey != null && aValues.containsKey(aOriginalKey)) {
value = aValues.get(aOriginalKey).toString();
aValues.remove(aOriginalKey);
}
if (!aValues.containsKey(aNewKey)) {
aValues.put(aNewKey, value);
}
}
private static String HISTOGRAM_DATABASE_LOCKED = "DATABASE_LOCKED_EXCEPTION";
private static String HISTOGRAM_DATABASE_UNLOCKED = "DATABASE_SUCCESSFUL_UNLOCK";
public static void ensureDatabaseIsNotLocked(SQLiteOpenHelper dbHelper, String databasePath) {
final int maxAttempts = 5;
int attempt = 0;
SQLiteDatabase db = null;
for (; attempt < maxAttempts; attempt++) {
try {
// Try a simple test and exit the loop.
db = dbHelper.getWritableDatabase();
break;
} catch (Exception e) {
// We assume that this is a android.database.sqlite.SQLiteDatabaseLockedException.
// That class is only available on API 11+.
Telemetry.addToHistogram(HISTOGRAM_DATABASE_LOCKED, attempt);
// Things could get very bad if we don't find a way to unlock the DB.
Log.d(LOGTAG, "Database is locked, trying to kill any zombie processes: " + databasePath);
GeckoAppShell.killAnyZombies();
try {
Thread.sleep(attempt * 100);
} catch (InterruptedException ie) {
}
}
}
if (db == null) {
Log.w(LOGTAG, "Failed to unlock database.");
GeckoAppShell.listOfOpenFiles();
return;
}
// If we needed to retry, but we succeeded, report that in telemetry.
// Failures are indicated by a lower frequency of UNLOCKED than LOCKED.
if (attempt > 1) {
Telemetry.addToHistogram(HISTOGRAM_DATABASE_UNLOCKED, attempt - 1);
}
}
/**
* Copies a table <b>between</b> database files.
*
* This method assumes that the source table and destination table already exist in the
* source and destination databases, respectively.
*
* The table is copied row-by-row in a single transaction.
*
* @param source The source database that the table will be copied from.
* @param sourceTableName The name of the source table.
* @param destination The destination database that the table will be copied to.
* @param destinationTableName The name of the destination table.
* @return true if all rows were copied; false otherwise.
*/
public static boolean copyTable(SQLiteDatabase source, String sourceTableName,
SQLiteDatabase destination, String destinationTableName) {
Cursor cursor = null;
try {
destination.beginTransaction();
cursor = source.query(sourceTableName, null, null, null, null, null, null);
Log.d(LOGTAG, "Trying to copy " + cursor.getCount() + " rows from " + sourceTableName + " to " + destinationTableName);
final ContentValues contentValues = new ContentValues();
while (cursor.moveToNext()) {
contentValues.clear();
DatabaseUtils.cursorRowToContentValues(cursor, contentValues);
destination.insert(destinationTableName, null, contentValues);
}
destination.setTransactionSuccessful();
Log.d(LOGTAG, "Successfully copied " + cursor.getCount() + " rows from " + sourceTableName + " to " + destinationTableName);
return true;
} catch (Exception e) {
Log.w(LOGTAG, "Got exception copying rows from " + sourceTableName + " to " + destinationTableName + "; ignoring.", e);
return false;
} finally {
destination.endTransaction();
if (cursor != null) {
cursor.close();
}
}
}
/**
* Verifies that 0-byte arrays aren't added as favicon or thumbnail data.
* @param values ContentValues of query
* @param columnName Name of data column to verify
*/
public static void stripEmptyByteArray(ContentValues values, String columnName) {
if (values.containsKey(columnName)) {
byte[] data = values.getAsByteArray(columnName);
if (data == null || data.length == 0) {
Log.w(LOGTAG, "Tried to insert an empty or non-byte-array image. Ignoring.");
values.putNull(columnName);
}
}
}
/**
* Builds a selection string that searches for a list of arguments in a particular column.
* For example URL in (?,?,?). Callers should pass the actual arguments into their query
* as selection args.
* @para columnName The column to search in
* @para size The number of arguments to search for
*/
public static String computeSQLInClause(int items, String field) {
final StringBuilder builder = new StringBuilder(field);
builder.append(" IN (");
int i = 0;
for (; i < items - 1; ++i) {
builder.append("?, ");
}
if (i < items) {
builder.append("?");
}
builder.append(")");
return builder.toString();
}
/**
* Turn a single-column cursor of longs into a single SQL "IN" clause.
* We can do this without using selection arguments because Long isn't
* vulnerable to injection.
*/
public static String computeSQLInClauseFromLongs(final Cursor cursor, String field) {
final StringBuilder builder = new StringBuilder(field);
builder.append(" IN (");
final int commaLimit = cursor.getCount() - 1;
int i = 0;
while (cursor.moveToNext()) {
builder.append(cursor.getLong(0));
if (i++ < commaLimit) {
builder.append(", ");
}
}
builder.append(")");
return builder.toString();
}
public static Uri appendProfile(final String profile, final Uri uri) {
return uri.buildUpon().appendQueryParameter(BrowserContract.PARAM_PROFILE, profile).build();
}
public static Uri appendProfileWithDefault(final String profile, final Uri uri) {
if (profile == null) {
return appendProfile(GeckoProfile.DEFAULT_PROFILE, uri);
}
return appendProfile(profile, uri);
}
/**
* Use the following when no conflict action is specified.
*/
private static final int CONFLICT_NONE = 0;
private static final String[] CONFLICT_VALUES = new String[] {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "};
/**
* Convenience method for updating rows in the database.
*
* @param table the table to update in
* @param values a map from column names to new column values. null is a
* valid value that will be translated to NULL.
* @param whereClause the optional WHERE clause to apply when updating.
* Passing null will update all rows.
* @param whereArgs You may include ?s in the where clause, which
* will be replaced by the values from whereArgs. The values
* will be bound as Strings.
* @return the number of rows affected
*/
@RobocopTarget
public static int updateArrays(SQLiteDatabase db, String table, ContentValues[] values, UpdateOperation[] ops, String whereClause, String[] whereArgs) {
return updateArraysWithOnConflict(db, table, values, ops, whereClause, whereArgs, CONFLICT_NONE, true);
}
public static void updateArraysBlindly(SQLiteDatabase db, String table, ContentValues[] values, UpdateOperation[] ops, String whereClause, String[] whereArgs) {
updateArraysWithOnConflict(db, table, values, ops, whereClause, whereArgs, CONFLICT_NONE, false);
}
@RobocopTarget
public enum UpdateOperation {
/**
* ASSIGN is the usual update: replaces the value in the named column with the provided value.
*
* foo = ?
*/
ASSIGN,
/**
* BITWISE_OR applies the provided value to the existing value with a bitwise OR. This is useful for adding to flags.
*
* foo |= ?
*/
BITWISE_OR,
/**
* EXPRESSION is an end-run around the API: it allows callers to specify a fragment of SQL to splice into the
* SET part of the query.
*
* foo = $value
*
* Be very careful not to use user input in this.
*/
EXPRESSION,
}
/**
* This is an evil reimplementation of SQLiteDatabase's methods to allow for
* smarter updating.
*
* Each ContentValues has an associated enum that describes how to unify input values with the existing column values.
*/
private static int updateArraysWithOnConflict(SQLiteDatabase db, String table,
ContentValues[] values,
UpdateOperation[] ops,
String whereClause,
String[] whereArgs,
int conflictAlgorithm,
boolean returnChangedRows) {
if (values == null || values.length == 0) {
throw new IllegalArgumentException("Empty values");
}
if (ops == null || ops.length != values.length) {
throw new IllegalArgumentException("ops and values don't match");
}
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE ");
sql.append(CONFLICT_VALUES[conflictAlgorithm]);
sql.append(table);
sql.append(" SET ");
// move all bind args to one array
int setValuesSize = 0;
for (int i = 0; i < values.length; i++) {
// EXPRESSION types don't contribute any placeholders.
if (ops[i] != UpdateOperation.EXPRESSION) {
setValuesSize += values[i].size();
}
}
int bindArgsSize = (whereArgs == null) ? setValuesSize : (setValuesSize + whereArgs.length);
Object[] bindArgs = new Object[bindArgsSize];
int arg = 0;
for (int i = 0; i < values.length; i++) {
final ContentValues v = values[i];
final UpdateOperation op = ops[i];
// Alas, code duplication.
switch (op) {
case ASSIGN:
for (Map.Entry<String, Object> entry : v.valueSet()) {
final String colName = entry.getKey();
sql.append((arg > 0) ? "," : "");
sql.append(colName);
bindArgs[arg++] = entry.getValue();
sql.append("= ?");
}
break;
case BITWISE_OR:
for (Map.Entry<String, Object> entry : v.valueSet()) {
final String colName = entry.getKey();
sql.append((arg > 0) ? "," : "");
sql.append(colName);
bindArgs[arg++] = entry.getValue();
sql.append("= ? | ");
sql.append(colName);
}
break;
case EXPRESSION:
// Treat each value as a literal SQL string.
for (Map.Entry<String, Object> entry : v.valueSet()) {
final String colName = entry.getKey();
sql.append((arg > 0) ? "," : "");
sql.append(colName);
sql.append(" = ");
sql.append(entry.getValue());
}
break;
}
}
if (whereArgs != null) {
for (arg = setValuesSize; arg < bindArgsSize; arg++) {
bindArgs[arg] = whereArgs[arg - setValuesSize];
}
}
if (!TextUtils.isEmpty(whereClause)) {
sql.append(" WHERE ");
sql.append(whereClause);
}
// What a huge pain in the ass, all because SQLiteDatabase doesn't expose .executeSql,
// and we can't get a DB handle. Nor can we easily construct a statement with arguments
// already bound.
final SQLiteStatement statement = db.compileStatement(sql.toString());
try {
bindAllArgs(statement, bindArgs);
if (!returnChangedRows) {
statement.execute();
return 0;
}
// This is a separate method so we can annotate it with @TargetApi.
return executeStatementReturningChangedRows(statement);
} finally {
statement.close();
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private static int executeStatementReturningChangedRows(SQLiteStatement statement) {
return statement.executeUpdateDelete();
}
// All because {@link SQLiteProgram#bind(integer, Object)} is private.
private static void bindAllArgs(SQLiteStatement statement, Object[] bindArgs) {
if (bindArgs == null) {
return;
}
for (int i = bindArgs.length; i != 0; i--) {
Object v = bindArgs[i - 1];
if (v == null) {
statement.bindNull(i);
} else if (v instanceof String) {
statement.bindString(i, (String) v);
} else if (v instanceof Double) {
statement.bindDouble(i, (Double) v);
} else if (v instanceof Float) {
statement.bindDouble(i, (Float) v);
} else if (v instanceof Long) {
statement.bindLong(i, (Long) v);
} else if (v instanceof Integer) {
statement.bindLong(i, (Integer) v);
} else if (v instanceof Byte) {
statement.bindLong(i, (Byte) v);
} else if (v instanceof byte[]) {
statement.bindBlob(i, (byte[]) v);
}
}
}
}

View file

@ -0,0 +1,166 @@
/* 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.lang.IllegalArgumentException;
import java.util.HashMap;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.db.BrowserContract.FormHistory;
import org.mozilla.gecko.db.BrowserContract.DeletedFormHistory;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.sqlite.SQLiteBridge;
import org.mozilla.gecko.sync.Utils;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
public class FormHistoryProvider extends SQLiteBridgeContentProvider {
static final String TABLE_FORM_HISTORY = "moz_formhistory";
static final String TABLE_DELETED_FORM_HISTORY = "moz_deleted_formhistory";
private static final int FORM_HISTORY = 100;
private static final int DELETED_FORM_HISTORY = 101;
private static final UriMatcher URI_MATCHER;
// This should be kept in sync with the db version in toolkit/components/satchel/nsFormHistory.js
private static final int DB_VERSION = 4;
private static final String DB_FILENAME = "formhistory.sqlite";
private static final String TELEMETRY_TAG = "SQLITEBRIDGE_PROVIDER_FORMS";
private static final String WHERE_GUID_IS_NULL = BrowserContract.DeletedFormHistory.GUID + " IS NULL";
private static final String WHERE_GUID_IS_VALUE = BrowserContract.DeletedFormHistory.GUID + " = ?";
private static final String LOG_TAG = "FormHistoryProvider";
static {
URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
URI_MATCHER.addURI(BrowserContract.FORM_HISTORY_AUTHORITY, "formhistory", FORM_HISTORY);
URI_MATCHER.addURI(BrowserContract.FORM_HISTORY_AUTHORITY, "deleted-formhistory", DELETED_FORM_HISTORY);
}
public FormHistoryProvider() {
super(LOG_TAG);
}
@Override
public String getType(Uri uri) {
final int match = URI_MATCHER.match(uri);
switch (match) {
case FORM_HISTORY:
return FormHistory.CONTENT_TYPE;
case DELETED_FORM_HISTORY:
return DeletedFormHistory.CONTENT_TYPE;
default:
throw new UnsupportedOperationException("Unknown type " + uri);
}
}
@Override
public String getTable(Uri uri) {
String table = null;
final int match = URI_MATCHER.match(uri);
switch (match) {
case DELETED_FORM_HISTORY:
table = TABLE_DELETED_FORM_HISTORY;
break;
case FORM_HISTORY:
table = TABLE_FORM_HISTORY;
break;
default:
throw new UnsupportedOperationException("Unknown table " + uri);
}
return table;
}
@Override
public String getSortOrder(Uri uri, String aRequested) {
if (!TextUtils.isEmpty(aRequested)) {
return aRequested;
}
return null;
}
@Override
public void setupDefaults(Uri uri, ContentValues values) {
int match = URI_MATCHER.match(uri);
long now = System.currentTimeMillis();
switch (match) {
case DELETED_FORM_HISTORY:
values.put(DeletedFormHistory.TIME_DELETED, now);
// Deleted entries must contain a guid
if (!values.containsKey(FormHistory.GUID)) {
throw new IllegalArgumentException("Must provide a GUID for a deleted form history");
}
break;
case FORM_HISTORY:
// Generate GUID for new entry. Don't override specified GUIDs.
if (!values.containsKey(FormHistory.GUID)) {
String guid = Utils.generateGuid();
values.put(FormHistory.GUID, guid);
}
break;
default:
throw new UnsupportedOperationException("Unknown insert URI " + uri);
}
}
@Override
public void initGecko() {
GeckoAppShell.notifyObservers("FormHistory:Init", null);
}
@Override
public void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db) {
if (!values.containsKey(FormHistory.GUID)) {
return;
}
String guid = values.getAsString(FormHistory.GUID);
if (guid == null) {
db.delete(TABLE_DELETED_FORM_HISTORY, WHERE_GUID_IS_NULL, null);
return;
}
String[] args = new String[] { guid };
db.delete(TABLE_DELETED_FORM_HISTORY, WHERE_GUID_IS_VALUE, args);
}
@Override
public void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db) { }
@Override
public void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db) { }
@Override
protected String getDBName() {
return DB_FILENAME;
}
@Override
protected String getTelemetryPrefix() {
return TELEMETRY_TAG;
}
@Override
protected int getDBVersion() {
return DB_VERSION;
}
}

View file

@ -0,0 +1,194 @@
/* 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.io.IOException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.BrowserContract.HomeItems;
import org.mozilla.gecko.db.DBUtils;
import org.mozilla.gecko.sqlite.SQLiteBridge;
import org.mozilla.gecko.util.RawResource;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.util.Log;
public class HomeProvider extends SQLiteBridgeContentProvider {
private static final String LOGTAG = "GeckoHomeProvider";
// This should be kept in sync with the db version in mobile/android/modules/HomeProvider.jsm
private static final int DB_VERSION = 3;
private static final String DB_FILENAME = "home.sqlite";
private static final String TELEMETRY_TAG = "SQLITEBRIDGE_PROVIDER_HOME";
private static final String TABLE_ITEMS = "items";
// Endpoint to return static fake data.
static final int ITEMS_FAKE = 100;
static final int ITEMS = 101;
static final int ITEMS_ID = 102;
static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
static {
URI_MATCHER.addURI(BrowserContract.HOME_AUTHORITY, "items/fake", ITEMS_FAKE);
URI_MATCHER.addURI(BrowserContract.HOME_AUTHORITY, "items", ITEMS);
URI_MATCHER.addURI(BrowserContract.HOME_AUTHORITY, "items/#", ITEMS_ID);
}
public HomeProvider() {
super(LOGTAG);
}
@Override
public String getType(Uri uri) {
final int match = URI_MATCHER.match(uri);
switch (match) {
case ITEMS_FAKE: {
return HomeItems.CONTENT_TYPE;
}
case ITEMS: {
return HomeItems.CONTENT_TYPE;
}
default: {
throw new UnsupportedOperationException("Unknown type " + uri);
}
}
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
final int match = URI_MATCHER.match(uri);
// If we're querying the fake items, don't try to get the database.
if (match == ITEMS_FAKE) {
return queryFakeItems(uri, projection, selection, selectionArgs, sortOrder);
}
final String datasetId = uri.getQueryParameter(BrowserContract.PARAM_DATASET_ID);
if (datasetId == null) {
throw new IllegalArgumentException("All queries should contain a dataset ID parameter");
}
selection = DBUtils.concatenateWhere(selection, HomeItems.DATASET_ID + " = ?");
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { datasetId });
// Otherwise, let the SQLiteContentProvider implementation take care of this query for us!
Cursor c = super.query(uri, projection, selection, selectionArgs, sortOrder);
// SQLiteBridgeContentProvider may return a null Cursor if the database hasn't been created yet.
// However, we need a non-null cursor in order to listen for notifications.
if (c == null) {
c = new MatrixCursor(projection != null ? projection : HomeItems.DEFAULT_PROJECTION);
}
final ContentResolver cr = getContext().getContentResolver();
c.setNotificationUri(cr, getDatasetNotificationUri(datasetId));
return c;
}
/**
* Returns a cursor populated with static fake data.
*/
private Cursor queryFakeItems(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
JSONArray items = null;
try {
final String jsonString = RawResource.getAsString(getContext(), R.raw.fake_home_items);
items = new JSONArray(jsonString);
} catch (IOException e) {
Log.e(LOGTAG, "Error getting fake home items", e);
return null;
} catch (JSONException e) {
Log.e(LOGTAG, "Error parsing fake_home_items.json", e);
return null;
}
final MatrixCursor c = new MatrixCursor(HomeItems.DEFAULT_PROJECTION);
for (int i = 0; i < items.length(); i++) {
try {
final JSONObject item = items.getJSONObject(i);
c.addRow(new Object[] {
item.getInt("id"),
item.getString("dataset_id"),
item.getString("url"),
item.getString("title"),
item.getString("description"),
item.getString("image_url"),
item.getString("filter")
});
} catch (JSONException e) {
Log.e(LOGTAG, "Error creating cursor row for fake home item", e);
}
}
return c;
}
/**
* SQLiteBridgeContentProvider implementation
*/
@Override
protected String getDBName() {
return DB_FILENAME;
}
@Override
protected String getTelemetryPrefix() {
return TELEMETRY_TAG;
}
@Override
protected int getDBVersion() {
return DB_VERSION;
}
@Override
public String getTable(Uri uri) {
final int match = URI_MATCHER.match(uri);
switch (match) {
case ITEMS: {
return TABLE_ITEMS;
}
default: {
throw new UnsupportedOperationException("Unknown table " + uri);
}
}
}
@Override
public String getSortOrder(Uri uri, String aRequested) {
return null;
}
@Override
public void setupDefaults(Uri uri, ContentValues values) { }
@Override
public void initGecko() { }
@Override
public void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db) { }
@Override
public void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db) { }
@Override
public void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db) { }
public static Uri getDatasetNotificationUri(String datasetId) {
return Uri.withAppendedPath(HomeItems.CONTENT_URI, datasetId);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
/* -*- 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/. */
package org.mozilla.gecko.db;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.net.Uri;
/**
* Helper class for dealing with the search provider inside Fennec.
*/
public class LocalSearches implements Searches {
private final Uri uriWithProfile;
public LocalSearches(String mProfile) {
uriWithProfile = DBUtils.appendProfileWithDefault(mProfile, BrowserContract.SearchHistory.CONTENT_URI);
}
@Override
public void insert(ContentResolver cr, String query) {
final ContentValues values = new ContentValues();
values.put(BrowserContract.SearchHistory.QUERY, query);
cr.insert(uriWithProfile, values);
}
}

View file

@ -0,0 +1,320 @@
/* 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.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import org.json.JSONArray;
import org.json.JSONException;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UIAsyncTask;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
public class LocalTabsAccessor implements TabsAccessor {
private static final String LOGTAG = "GeckoTabsAccessor";
private static final long THREE_WEEKS_IN_MILLISECONDS = TimeUnit.MILLISECONDS.convert(21L, TimeUnit.DAYS);
public static final String[] TABS_PROJECTION_COLUMNS = new String[] {
BrowserContract.Tabs.TITLE,
BrowserContract.Tabs.URL,
BrowserContract.Clients.GUID,
BrowserContract.Clients.NAME,
BrowserContract.Tabs.LAST_USED,
BrowserContract.Clients.LAST_MODIFIED,
BrowserContract.Clients.DEVICE_TYPE,
};
public static final String[] CLIENTS_PROJECTION_COLUMNS = new String[] {
BrowserContract.Clients.GUID,
BrowserContract.Clients.NAME,
BrowserContract.Clients.LAST_MODIFIED,
BrowserContract.Clients.DEVICE_TYPE
};
private static final String REMOTE_CLIENTS_SELECTION = BrowserContract.Clients.GUID + " IS NOT NULL";
private static final String LOCAL_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NULL";
private static final String REMOTE_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NOT NULL";
private static final String REMOTE_TABS_SELECTION_CLIENT_RECENCY = REMOTE_TABS_SELECTION +
" AND " + BrowserContract.Clients.LAST_MODIFIED + " > ?";
private static final String REMOTE_TABS_SORT_ORDER =
// Most recently synced clients first.
BrowserContract.Clients.LAST_MODIFIED + " DESC, " +
// If two clients somehow had the same last modified time, this will
// group them (arbitrarily).
BrowserContract.Clients.GUID + " DESC, " +
// Within a single client, most recently used tabs first.
BrowserContract.Tabs.LAST_USED + " DESC";
private static final String LOCAL_CLIENT_SELECTION = BrowserContract.Clients.GUID + " IS NULL";
private static final Pattern FILTERED_URL_PATTERN = Pattern.compile("^(about|chrome|wyciwyg|file):");
private final Uri clientsRecencyUriWithProfile;
private final Uri tabsUriWithProfile;
private final Uri clientsUriWithProfile;
public LocalTabsAccessor(String profileName) {
tabsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Tabs.CONTENT_URI);
clientsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Clients.CONTENT_URI);
clientsRecencyUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Clients.CONTENT_RECENCY_URI);
}
/**
* Extracts a List of just RemoteClients from a cursor.
* The supplied cursor should be grouped by guid and sorted by most recently used.
*/
@Override
public List<RemoteClient> getClientsWithoutTabsByRecencyFromCursor(Cursor cursor) {
final ArrayList<RemoteClient> clients = new ArrayList<>(cursor.getCount());
final int originalPosition = cursor.getPosition();
try {
if (!cursor.moveToFirst()) {
return clients;
}
final int clientGuidIndex = cursor.getColumnIndex(BrowserContract.Clients.GUID);
final int clientNameIndex = cursor.getColumnIndex(BrowserContract.Clients.NAME);
final int clientLastModifiedIndex = cursor.getColumnIndex(BrowserContract.Clients.LAST_MODIFIED);
final int clientDeviceTypeIndex = cursor.getColumnIndex(BrowserContract.Clients.DEVICE_TYPE);
while (!cursor.isAfterLast()) {
final String clientGuid = cursor.getString(clientGuidIndex);
final String clientName = cursor.getString(clientNameIndex);
final String deviceType = cursor.getString(clientDeviceTypeIndex);
final long lastModified = cursor.getLong(clientLastModifiedIndex);
clients.add(new RemoteClient(clientGuid, clientName, lastModified, deviceType));
cursor.moveToNext();
}
} finally {
cursor.moveToPosition(originalPosition);
}
return clients;
}
/**
* Extract client and tab records from a cursor.
* <p>
* The position of the cursor is moved to before the first record before
* reading. The cursor is advanced until there are no more records to be
* read. The position of the cursor is restored before returning.
*
* @param cursor
* to extract records from. The records should already be grouped
* by client GUID.
* @return list of clients, each containing list of tabs.
*/
@Override
public List<RemoteClient> getClientsFromCursor(final Cursor cursor) {
final ArrayList<RemoteClient> clients = new ArrayList<RemoteClient>();
final int originalPosition = cursor.getPosition();
try {
if (!cursor.moveToFirst()) {
return clients;
}
final int tabTitleIndex = cursor.getColumnIndex(BrowserContract.Tabs.TITLE);
final int tabUrlIndex = cursor.getColumnIndex(BrowserContract.Tabs.URL);
final int tabLastUsedIndex = cursor.getColumnIndex(BrowserContract.Tabs.LAST_USED);
final int clientGuidIndex = cursor.getColumnIndex(BrowserContract.Clients.GUID);
final int clientNameIndex = cursor.getColumnIndex(BrowserContract.Clients.NAME);
final int clientLastModifiedIndex = cursor.getColumnIndex(BrowserContract.Clients.LAST_MODIFIED);
final int clientDeviceTypeIndex = cursor.getColumnIndex(BrowserContract.Clients.DEVICE_TYPE);
// A walking partition, chunking by client GUID. We assume the
// cursor records are already grouped by client GUID; see the query
// sort order.
RemoteClient lastClient = null;
while (!cursor.isAfterLast()) {
final String clientGuid = cursor.getString(clientGuidIndex);
if (lastClient == null || !TextUtils.equals(lastClient.guid, clientGuid)) {
final String clientName = cursor.getString(clientNameIndex);
final long lastModified = cursor.getLong(clientLastModifiedIndex);
final String deviceType = cursor.getString(clientDeviceTypeIndex);
lastClient = new RemoteClient(clientGuid, clientName, lastModified, deviceType);
clients.add(lastClient);
}
final String tabTitle = cursor.getString(tabTitleIndex);
final String tabUrl = cursor.getString(tabUrlIndex);
final long tabLastUsed = cursor.getLong(tabLastUsedIndex);
lastClient.tabs.add(new RemoteTab(tabTitle, tabUrl, tabLastUsed));
cursor.moveToNext();
}
} finally {
cursor.moveToPosition(originalPosition);
}
return clients;
}
@Override
public Cursor getRemoteClientsByRecencyCursor(Context context) {
final Uri uri = clientsRecencyUriWithProfile;
return context.getContentResolver().query(uri, CLIENTS_PROJECTION_COLUMNS,
REMOTE_CLIENTS_SELECTION, null, null);
}
@Override
public Cursor getRemoteTabsCursor(Context context) {
return getRemoteTabsCursor(context, -1);
}
@Override
public Cursor getRemoteTabsCursor(Context context, int limit) {
Uri uri = tabsUriWithProfile;
if (limit > 0) {
uri = uri.buildUpon()
.appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit))
.build();
}
final String threeWeeksAgoTimestampMillis = Long.valueOf(
System.currentTimeMillis() - THREE_WEEKS_IN_MILLISECONDS).toString();
return context.getContentResolver().query(uri,
TABS_PROJECTION_COLUMNS,
REMOTE_TABS_SELECTION_CLIENT_RECENCY,
new String[] {threeWeeksAgoTimestampMillis},
REMOTE_TABS_SORT_ORDER);
}
// This method returns all tabs from all remote clients,
// ordered by most recent client first, most recent tab first
@Override
public void getTabs(final Context context, final OnQueryTabsCompleteListener listener) {
getTabs(context, 0, listener);
}
// This method returns limited number of tabs from all remote clients,
// ordered by most recent client first, most recent tab first
@Override
public void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener) {
// If there is no listener, no point in doing work.
if (listener == null)
return;
(new UIAsyncTask.WithoutParams<List<RemoteClient>>(ThreadUtils.getBackgroundHandler()) {
@Override
protected List<RemoteClient> doInBackground() {
final Cursor cursor = getRemoteTabsCursor(context, limit);
if (cursor == null)
return null;
try {
return Collections.unmodifiableList(getClientsFromCursor(cursor));
} finally {
cursor.close();
}
}
@Override
protected void onPostExecute(List<RemoteClient> clients) {
listener.onQueryTabsComplete(clients);
}
}).execute();
}
// Updates the modified time of the local client with the current time.
private void updateLocalClient(final ContentResolver cr) {
ContentValues values = new ContentValues();
values.put(BrowserContract.Clients.LAST_MODIFIED, System.currentTimeMillis());
cr.update(clientsUriWithProfile, values, LOCAL_CLIENT_SELECTION, null);
}
// Deletes all local tabs.
private void deleteLocalTabs(final ContentResolver cr) {
cr.delete(tabsUriWithProfile, LOCAL_TABS_SELECTION, null);
}
/**
* Tabs are positioned in the DB in the same order that they appear in the tabs param.
* - URL should never empty or null. Skip this tab if there's no URL.
* - TITLE should always a string, either a page title or empty.
* - LAST_USED should always be numeric.
* - FAVICON should be a URL or null.
* - HISTORY should be serialized JSON array of URLs.
* - POSITION should always be numeric.
* - CLIENT_GUID should always be null to represent the local client.
*/
private void insertLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs) {
// Reuse this for serializing individual history URLs as JSON.
JSONArray history = new JSONArray();
ArrayList<ContentValues> valuesToInsert = new ArrayList<ContentValues>();
int position = 0;
for (Tab tab : tabs) {
// Skip this tab if it has a null URL or is in private browsing mode, or is a filtered URL.
String url = tab.getURL();
if (url == null || tab.isPrivate() || isFilteredURL(url))
continue;
ContentValues values = new ContentValues();
values.put(BrowserContract.Tabs.URL, url);
values.put(BrowserContract.Tabs.TITLE, tab.getTitle());
values.put(BrowserContract.Tabs.LAST_USED, tab.getLastUsed());
String favicon = tab.getFaviconURL();
if (favicon != null)
values.put(BrowserContract.Tabs.FAVICON, favicon);
else
values.putNull(BrowserContract.Tabs.FAVICON);
// We don't have access to session history in Java, so for now, we'll
// just use a JSONArray that holds most recent history item.
try {
history.put(0, tab.getURL());
values.put(BrowserContract.Tabs.HISTORY, history.toString());
} catch (JSONException e) {
Log.w(LOGTAG, "JSONException adding URL to tab history array.", e);
}
values.put(BrowserContract.Tabs.POSITION, position++);
// A null client guid corresponds to the local client.
values.putNull(BrowserContract.Tabs.CLIENT_GUID);
valuesToInsert.add(values);
}
ContentValues[] valuesToInsertArray = valuesToInsert.toArray(new ContentValues[valuesToInsert.size()]);
cr.bulkInsert(tabsUriWithProfile, valuesToInsertArray);
}
// Deletes all local tabs and replaces them with a new list of tabs.
@Override
public synchronized void persistLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs) {
deleteLocalTabs(cr);
insertLocalTabs(cr, tabs);
updateLocalClient(cr);
}
/**
* Matches the supplied URL string against the set of URLs to filter.
*
* @return true if the supplied URL should be skipped; false otherwise.
*/
private boolean isFilteredURL(String url) {
return FILTERED_URL_PATTERN.matcher(url).lookingAt();
}
}

View file

@ -0,0 +1,240 @@
/* -*- 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/.
*/
package org.mozilla.gecko.db;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
import org.mozilla.gecko.util.ThreadUtils;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
import android.util.LruCache;
// Holds metadata info about URLs. Supports some helper functions for getting back a HashMap of key value data.
public class LocalURLMetadata implements URLMetadata {
private static final String LOGTAG = "GeckoURLMetadata";
private final Uri uriWithProfile;
public LocalURLMetadata(String mProfile) {
uriWithProfile = DBUtils.appendProfileWithDefault(mProfile, URLMetadataTable.CONTENT_URI);
}
// A list of columns in the table. It's used to simplify some loops for reading/writing data.
private static final Set<String> COLUMNS;
static {
final HashSet<String> tempModel = new HashSet<>(4);
tempModel.add(URLMetadataTable.URL_COLUMN);
tempModel.add(URLMetadataTable.TILE_IMAGE_URL_COLUMN);
tempModel.add(URLMetadataTable.TILE_COLOR_COLUMN);
tempModel.add(URLMetadataTable.TOUCH_ICON_COLUMN);
COLUMNS = Collections.unmodifiableSet(tempModel);
}
// Store a cache of recent results. This number is chosen to match the max number of tiles on about:home
private static final int CACHE_SIZE = 9;
// Note: Members of this cache are unmodifiable.
private final LruCache<String, Map<String, Object>> cache = new LruCache<String, Map<String, Object>>(CACHE_SIZE);
/**
* Converts a JSON object into a unmodifiable Map of known metadata properties.
* Will throw away any properties that aren't stored in the database.
*
* Incoming data can include a list like: {touchIconList:{56:"http://x.com/56.png", 76:"http://x.com/76.png"}}.
* This will then be filtered to find the most appropriate touchIcon, i.e. the closest icon size that is larger
* than (or equal to) the preferred homescreen launcher icon size, which is then stored in the "touchIcon" property.
*/
@Override
public Map<String, Object> fromJSON(JSONObject obj) {
Map<String, Object> data = new HashMap<String, Object>();
for (String key : COLUMNS) {
if (obj.has(key)) {
data.put(key, obj.optString(key));
}
}
try {
JSONObject icons;
if (obj.has("touchIconList") &&
(icons = obj.getJSONObject("touchIconList")).length() > 0) {
int preferredSize = GeckoAppShell.getPreferredIconSize();
Iterator<String> keys = icons.keys();
ArrayList<Integer> sizes = new ArrayList<Integer>(icons.length());
while (keys.hasNext()) {
sizes.add(new Integer(keys.next()));
}
final int bestSize = LoadFaviconResult.selectBestSizeFromList(sizes, preferredSize);
final String iconURL = icons.getString(Integer.toString(bestSize));
data.put(URLMetadataTable.TOUCH_ICON_COLUMN, iconURL);
}
} catch (JSONException e) {
Log.w(LOGTAG, "Exception processing touchIconList for LocalURLMetadata; ignoring.", e);
}
return Collections.unmodifiableMap(data);
}
/**
* Converts a Cursor into a unmodifiable Map of known metadata properties.
* Will throw away any properties that aren't stored in the database.
* Will also not iterate through multiple rows in the cursor.
*/
private Map<String, Object> fromCursor(Cursor c) {
Map<String, Object> data = new HashMap<String, Object>();
String[] columns = c.getColumnNames();
for (String column : columns) {
if (COLUMNS.contains(column)) {
try {
data.put(column, c.getString(c.getColumnIndexOrThrow(column)));
} catch (Exception ex) {
Log.i(LOGTAG, "Error getting data for " + column, ex);
}
}
}
return Collections.unmodifiableMap(data);
}
/**
* Returns an unmodifiable Map of url->Metadata (i.e. A second HashMap) for a list of urls.
* Must not be called from UI or Gecko threads.
*/
@Override
public Map<String, Map<String, Object>> getForURLs(final ContentResolver cr,
final Collection<String> urls,
final List<String> requestedColumns) {
ThreadUtils.assertNotOnUiThread();
ThreadUtils.assertNotOnGeckoThread();
final Map<String, Map<String, Object>> data = new HashMap<String, Map<String, Object>>();
// Nothing to query for
if (urls.isEmpty() || requestedColumns.isEmpty()) {
Log.e(LOGTAG, "Queried metadata for nothing");
return data;
}
// Search the cache for any of these urls
List<String> urlsToQuery = new ArrayList<String>();
for (String url : urls) {
final Map<String, Object> hit = cache.get(url);
if (hit != null) {
// Cache hit: we've found the URL in the cache, however we may not have cached the desired columns
// for that URL. Hence we need to check whether our cache hit contains those columns, and directly
// retrieve the desired data if not. (E.g. the top sites panel retrieves the tile, and tilecolor. If
// we later try to retrieve the touchIcon for a top-site the cache hit will only point to
// tile+tilecolor, and not the required touchIcon. In this case we don't want to use the cache.)
boolean useCache = true;
for (String c: requestedColumns) {
if (!hit.containsKey(c)) {
useCache = false;
}
}
if (useCache) {
data.put(url, hit);
} else {
urlsToQuery.add(url);
}
} else {
urlsToQuery.add(url);
}
}
// If everything was in the cache, we're done!
if (urlsToQuery.size() == 0) {
return Collections.unmodifiableMap(data);
}
final String selection = DBUtils.computeSQLInClause(urlsToQuery.size(), URLMetadataTable.URL_COLUMN);
List<String> columns = requestedColumns;
// We need the url to build our final HashMap, so we force it to be included in the query.
if (!columns.contains(URLMetadataTable.URL_COLUMN)) {
// The requestedColumns may be immutable (e.g. if the caller used Collections.singletonList), hence
// we have to create a copy.
columns = new ArrayList<String>(columns);
columns.add(URLMetadataTable.URL_COLUMN);
}
final Cursor cursor = cr.query(uriWithProfile,
columns.toArray(new String[columns.size()]), // columns,
selection, // selection
urlsToQuery.toArray(new String[urlsToQuery.size()]), // selectionargs
null);
try {
if (!cursor.moveToFirst()) {
return Collections.unmodifiableMap(data);
}
do {
final Map<String, Object> metadata = fromCursor(cursor);
final String url = cursor.getString(cursor.getColumnIndexOrThrow(URLMetadataTable.URL_COLUMN));
data.put(url, metadata);
cache.put(url, metadata);
} while (cursor.moveToNext());
} finally {
cursor.close();
}
return Collections.unmodifiableMap(data);
}
/**
* Saves a HashMap of metadata into the database. Will iterate through columns
* in the Database and only save rows with matching keys in the HashMap.
* Must not be called from UI or Gecko threads.
*/
@Override
public void save(final ContentResolver cr, final Map<String, Object> data) {
ThreadUtils.assertNotOnUiThread();
ThreadUtils.assertNotOnGeckoThread();
try {
ContentValues values = new ContentValues();
for (String key : COLUMNS) {
if (data.containsKey(key)) {
values.put(key, (String) data.get(key));
}
}
if (values.size() == 0) {
return;
}
Uri uri = uriWithProfile.buildUpon()
.appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
.build();
cr.update(uri, values, URLMetadataTable.URL_COLUMN + "=?", new String[] {
(String) data.get(URLMetadataTable.URL_COLUMN)
});
} catch (Exception ex) {
Log.e(LOGTAG, "error saving", ex);
}
}
}

View file

@ -0,0 +1,253 @@
/* 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 android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.json.JSONException;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.db.BrowserContract.UrlAnnotations.Key;
import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
public class LocalUrlAnnotations implements UrlAnnotations {
private static final String LOGTAG = "LocalUrlAnnotations";
private Uri urlAnnotationsTableWithProfile;
public LocalUrlAnnotations(final String profile) {
urlAnnotationsTableWithProfile = DBUtils.appendProfile(profile, BrowserContract.UrlAnnotations.CONTENT_URI);
}
/**
* Get all feed subscriptions.
*/
@Override
public Cursor getFeedSubscriptions(ContentResolver cr) {
return queryByKey(cr,
Key.FEED_SUBSCRIPTION,
new String[] { BrowserContract.UrlAnnotations.URL, BrowserContract.UrlAnnotations.VALUE },
null);
}
/**
* Insert mapping from website URL to URL of the feed.
*/
@Override
public void insertFeedUrl(ContentResolver cr, String originUrl, String feedUrl) {
insertAnnotation(cr, originUrl, Key.FEED, feedUrl);
}
@Override
public boolean hasAcceptedOrDeclinedHomeScreenShortcut(ContentResolver cr, String url) {
return hasResultsForSelection(cr,
BrowserContract.UrlAnnotations.URL + " = ?",
new String[]{url});
}
@Override
public void insertHomeScreenShortcut(ContentResolver cr, String url, boolean hasCreatedShortCut) {
insertAnnotation(cr, url, Key.HOME_SCREEN_SHORTCUT, String.valueOf(hasCreatedShortCut));
}
/**
* Returns true if there's a mapping from the given website URL to a feed URL. False otherwise.
*/
@Override
public boolean hasFeedUrlForWebsite(ContentResolver cr, String websiteUrl) {
return hasResultsForSelection(cr,
BrowserContract.UrlAnnotations.URL + " = ? AND " + BrowserContract.UrlAnnotations.KEY + " = ?",
new String[]{websiteUrl, Key.FEED.getDbValue()});
}
/**
* Returns true if there's a website URL with this feed URL. False otherwise.
*/
@Override
public boolean hasWebsiteForFeedUrl(ContentResolver cr, String feedUrl) {
return hasResultsForSelection(cr,
BrowserContract.UrlAnnotations.VALUE + " = ? AND " + BrowserContract.UrlAnnotations.KEY + " = ?",
new String[]{feedUrl, Key.FEED.getDbValue()});
}
/**
* Delete the feed URL mapping for this website URL.
*/
@Override
public void deleteFeedUrl(ContentResolver cr, String websiteUrl) {
deleteAnnotation(cr, websiteUrl, Key.FEED);
}
/**
* Get website URLs that are mapped to the given feed URL.
*/
@Override
public Cursor getWebsitesWithFeedUrl(ContentResolver cr) {
return cr.query(urlAnnotationsTableWithProfile,
new String[] { BrowserContract.UrlAnnotations.URL },
BrowserContract.UrlAnnotations.KEY + " = ?",
new String[] { Key.FEED.getDbValue() },
null);
}
/**
* Returns true if there's a subscription for this feed URL. False otherwise.
*/
@Override
public boolean hasFeedSubscription(ContentResolver cr, String feedUrl) {
return hasResultsForSelection(cr,
BrowserContract.UrlAnnotations.URL + " = ? AND " + BrowserContract.UrlAnnotations.KEY + " = ?",
new String[]{feedUrl, Key.FEED_SUBSCRIPTION.getDbValue()});
}
/**
* Insert the given feed subscription (Mapping from feed URL to the subscription object).
*/
@Override
public void insertFeedSubscription(ContentResolver cr, FeedSubscription subscription) {
try {
insertAnnotation(cr, subscription.getFeedUrl(), Key.FEED_SUBSCRIPTION, subscription.toJSON().toString());
} catch (JSONException e) {
Log.w(LOGTAG, "Could not serialize subscription");
}
}
/**
* Update the feed subscription with new values.
*/
@Override
public void updateFeedSubscription(ContentResolver cr, FeedSubscription subscription) {
try {
updateAnnotation(cr, subscription.getFeedUrl(), Key.FEED_SUBSCRIPTION, subscription.toJSON().toString());
} catch (JSONException e) {
Log.w(LOGTAG, "Could not serialize subscription");
}
}
/**
* Delete the subscription for the feed URL.
*/
@Override
public void deleteFeedSubscription(ContentResolver cr, FeedSubscription subscription) {
deleteAnnotation(cr, subscription.getFeedUrl(), Key.FEED_SUBSCRIPTION);
}
private int deleteAnnotation(final ContentResolver cr, final String url, final Key key) {
return cr.delete(urlAnnotationsTableWithProfile,
BrowserContract.UrlAnnotations.KEY + " = ? AND " + BrowserContract.UrlAnnotations.URL + " = ?",
new String[] { key.getDbValue(), url });
}
private int updateAnnotation(final ContentResolver cr, final String url, final Key key, final String value) {
ContentValues values = new ContentValues();
values.put(BrowserContract.UrlAnnotations.VALUE, value);
values.put(BrowserContract.UrlAnnotations.DATE_MODIFIED, System.currentTimeMillis());
return cr.update(urlAnnotationsTableWithProfile,
values,
BrowserContract.UrlAnnotations.KEY + " = ? AND " + BrowserContract.UrlAnnotations.URL + " = ?",
new String[]{key.getDbValue(), url});
}
private void insertAnnotation(final ContentResolver cr, final String url, final Key key, final String value) {
insertAnnotation(cr, url, key.getDbValue(), value);
}
@RobocopTarget
@Override
public void insertAnnotation(final ContentResolver cr, final String url, final String key, final String value) {
final long creationTime = System.currentTimeMillis();
final ContentValues values = new ContentValues(5);
values.put(BrowserContract.UrlAnnotations.URL, url);
values.put(BrowserContract.UrlAnnotations.KEY, key);
values.put(BrowserContract.UrlAnnotations.VALUE, value);
values.put(BrowserContract.UrlAnnotations.DATE_CREATED, creationTime);
values.put(BrowserContract.UrlAnnotations.DATE_MODIFIED, creationTime);
cr.insert(urlAnnotationsTableWithProfile, values);
}
/**
* @return true if the table contains rows for the given selection.
*/
private boolean hasResultsForSelection(ContentResolver cr, String selection, String[] selectionArgs) {
Cursor cursor = cr.query(urlAnnotationsTableWithProfile,
new String[] { BrowserContract.UrlAnnotations._ID },
selection,
selectionArgs,
null);
if (cursor == null) {
return false;
}
try {
return cursor.getCount() > 0;
} finally {
cursor.close();
}
}
private Cursor queryByKey(final ContentResolver cr, @NonNull final Key key, @Nullable final String[] projections,
@Nullable final String sortOrder) {
return cr.query(urlAnnotationsTableWithProfile,
projections,
BrowserContract.UrlAnnotations.KEY + " = ?", new String[] { key.getDbValue() },
sortOrder);
}
@Override
public Cursor getScreenshots(ContentResolver cr) {
return queryByKey(cr,
Key.SCREENSHOT,
new String[] {
BrowserContract.UrlAnnotations._ID,
BrowserContract.UrlAnnotations.URL,
BrowserContract.UrlAnnotations.KEY,
BrowserContract.UrlAnnotations.VALUE,
BrowserContract.UrlAnnotations.DATE_CREATED,
},
BrowserContract.UrlAnnotations.DATE_CREATED + " DESC");
}
public void insertScreenshot(final ContentResolver cr, final String pageUrl, final String screenshotPath) {
insertAnnotation(cr, pageUrl, Key.SCREENSHOT.getDbValue(), screenshotPath);
}
@Override
public void insertReaderViewUrl(final ContentResolver cr, final String pageUrl) {
insertAnnotation(cr, pageUrl, Key.READER_VIEW.getDbValue(), BrowserContract.UrlAnnotations.READER_VIEW_SAVED_VALUE);
}
@Override
public void deleteReaderViewUrl(ContentResolver cr, String pageURL) {
deleteAnnotation(cr, pageURL, Key.READER_VIEW);
}
public int getAnnotationCount(ContentResolver cr, Key key) {
final String countColumnname = "count";
final Cursor c = queryByKey(cr,
key,
new String[] {
"COUNT(*) AS " + countColumnname
},
null);
try {
if (c != null && c.moveToFirst()) {
return c.getInt(c.getColumnIndexOrThrow(countColumnname));
} else {
return 0;
}
} finally {
if (c != null) {
c.close();
}
}
}
}

View file

@ -0,0 +1,520 @@
/* 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 android.content.ContentUris;
import android.content.ContentValues;
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.SQLiteQueryBuilder;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Base64;
import org.mozilla.gecko.db.BrowserContract.DeletedLogins;
import org.mozilla.gecko.db.BrowserContract.Logins;
import org.mozilla.gecko.db.BrowserContract.LoginsDisabledHosts;
import org.mozilla.gecko.sync.Utils;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import javax.crypto.Cipher;
import javax.crypto.NullCipher;
import static org.mozilla.gecko.db.BrowserContract.DeletedLogins.TABLE_DELETED_LOGINS;
import static org.mozilla.gecko.db.BrowserContract.Logins.TABLE_LOGINS;
import static org.mozilla.gecko.db.BrowserContract.LoginsDisabledHosts.TABLE_DISABLED_HOSTS;
public class LoginsProvider extends SharedBrowserDatabaseProvider {
private static final int LOGINS = 100;
private static final int LOGINS_ID = 101;
private static final int DELETED_LOGINS = 102;
private static final int DELETED_LOGINS_ID = 103;
private static final int DISABLED_HOSTS = 104;
private static final int DISABLED_HOSTS_HOSTNAME = 105;
private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
private static final HashMap<String, String> LOGIN_PROJECTION_MAP;
private static final HashMap<String, String> DELETED_LOGIN_PROJECTION_MAP;
private static final HashMap<String, String> DISABLED_HOSTS_PROJECTION_MAP;
private static final String DEFAULT_LOGINS_SORT_ORDER = Logins.HOSTNAME + " ASC";
private static final String DEFAULT_DELETED_LOGINS_SORT_ORDER = DeletedLogins.TIME_DELETED + " ASC";
private static final String DEFAULT_DISABLED_HOSTS_SORT_ORDER = LoginsDisabledHosts.HOSTNAME + " ASC";
private static final String WHERE_GUID_IS_NULL = DeletedLogins.GUID + " IS NULL";
private static final String WHERE_GUID_IS_VALUE = DeletedLogins.GUID + " = ?";
protected static final String INDEX_LOGINS_HOSTNAME = "login_hostname_index";
protected static final String INDEX_LOGINS_HOSTNAME_FORM_SUBMIT_URL = "login_hostname_formSubmitURL_index";
protected static final String INDEX_LOGINS_HOSTNAME_HTTP_REALM = "login_hostname_httpRealm_index";
static {
URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "logins", LOGINS);
URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "logins/#", LOGINS_ID);
URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "deleted-logins", DELETED_LOGINS);
URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "deleted-logins/#", DELETED_LOGINS_ID);
URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "logins-disabled-hosts", DISABLED_HOSTS);
URI_MATCHER.addURI(BrowserContract.LOGINS_AUTHORITY, "logins-disabled-hosts/hostname/*", DISABLED_HOSTS_HOSTNAME);
LOGIN_PROJECTION_MAP = new HashMap<>();
LOGIN_PROJECTION_MAP.put(Logins._ID, Logins._ID);
LOGIN_PROJECTION_MAP.put(Logins.HOSTNAME, Logins.HOSTNAME);
LOGIN_PROJECTION_MAP.put(Logins.HTTP_REALM, Logins.HTTP_REALM);
LOGIN_PROJECTION_MAP.put(Logins.FORM_SUBMIT_URL, Logins.FORM_SUBMIT_URL);
LOGIN_PROJECTION_MAP.put(Logins.USERNAME_FIELD, Logins.USERNAME_FIELD);
LOGIN_PROJECTION_MAP.put(Logins.PASSWORD_FIELD, Logins.PASSWORD_FIELD);
LOGIN_PROJECTION_MAP.put(Logins.ENCRYPTED_USERNAME, Logins.ENCRYPTED_USERNAME);
LOGIN_PROJECTION_MAP.put(Logins.ENCRYPTED_PASSWORD, Logins.ENCRYPTED_PASSWORD);
LOGIN_PROJECTION_MAP.put(Logins.GUID, Logins.GUID);
LOGIN_PROJECTION_MAP.put(Logins.ENC_TYPE, Logins.ENC_TYPE);
LOGIN_PROJECTION_MAP.put(Logins.TIME_CREATED, Logins.TIME_CREATED);
LOGIN_PROJECTION_MAP.put(Logins.TIME_LAST_USED, Logins.TIME_LAST_USED);
LOGIN_PROJECTION_MAP.put(Logins.TIME_PASSWORD_CHANGED, Logins.TIME_PASSWORD_CHANGED);
LOGIN_PROJECTION_MAP.put(Logins.TIMES_USED, Logins.TIMES_USED);
DELETED_LOGIN_PROJECTION_MAP = new HashMap<>();
DELETED_LOGIN_PROJECTION_MAP.put(DeletedLogins._ID, DeletedLogins._ID);
DELETED_LOGIN_PROJECTION_MAP.put(DeletedLogins.GUID, DeletedLogins.GUID);
DELETED_LOGIN_PROJECTION_MAP.put(DeletedLogins.TIME_DELETED, DeletedLogins.TIME_DELETED);
DISABLED_HOSTS_PROJECTION_MAP = new HashMap<>();
DISABLED_HOSTS_PROJECTION_MAP.put(LoginsDisabledHosts._ID, LoginsDisabledHosts._ID);
DISABLED_HOSTS_PROJECTION_MAP.put(LoginsDisabledHosts.HOSTNAME, LoginsDisabledHosts.HOSTNAME);
}
private static String projectColumn(String table, String column) {
return table + "." + column;
}
private static String selectColumn(String table, String column) {
return projectColumn(table, column) + " = ?";
}
@Override
protected Uri insertInTransaction(Uri uri, ContentValues values) {
trace("Calling insert in transaction on URI: " + uri);
final int match = URI_MATCHER.match(uri);
final SQLiteDatabase db = getWritableDatabase(uri);
final long id;
String guid;
setupDefaultValues(values, uri);
switch (match) {
case LOGINS:
removeDeletedLoginsByGUIDInTransaction(values, db);
// Encrypt sensitive data.
encryptContentValueFields(values);
guid = values.getAsString(Logins.GUID);
debug("Inserting login in database with GUID: " + guid);
id = db.insertOrThrow(TABLE_LOGINS, Logins.GUID, values);
break;
case DELETED_LOGINS:
guid = values.getAsString(DeletedLogins.GUID);
debug("Inserting deleted-login in database with GUID: " + guid);
id = db.insertOrThrow(TABLE_DELETED_LOGINS, DeletedLogins.GUID, values);
break;
case DISABLED_HOSTS:
String hostname = values.getAsString(LoginsDisabledHosts.HOSTNAME);
debug("Inserting disabled-host in database with hostname: " + hostname);
id = db.insertOrThrow(TABLE_DISABLED_HOSTS, LoginsDisabledHosts.HOSTNAME, 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
@SuppressWarnings("fallthrough")
protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
trace("Calling delete in transaction on URI: " + uri);
final int match = URI_MATCHER.match(uri);
final String table;
final SQLiteDatabase db = getWritableDatabase(uri);
beginWrite(db);
switch (match) {
case LOGINS_ID:
trace("Delete on LOGINS_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_LOGINS, Logins._ID));
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[]{Long.toString(ContentUris.parseId(uri))});
// Store the deleted client in deleted-logins table.
final String guid = getLoginGUIDByID(selection, selectionArgs, db);
if (guid == null) {
// No matching logins found for the id.
return 0;
}
boolean isInsertSuccessful = storeDeletedLoginForGUIDInTransaction(guid, db);
if (!isInsertSuccessful) {
// Failed to insert into deleted-logins, return early.
return 0;
}
// fall through
case LOGINS:
trace("Delete on LOGINS: " + uri);
table = TABLE_LOGINS;
break;
case DELETED_LOGINS_ID:
trace("Delete on DELETED_LOGINS_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_DELETED_LOGINS, DeletedLogins._ID));
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[]{Long.toString(ContentUris.parseId(uri))});
// fall through
case DELETED_LOGINS:
trace("Delete on DELETED_LOGINS_ID: " + uri);
table = TABLE_DELETED_LOGINS;
break;
case DISABLED_HOSTS_HOSTNAME:
trace("Delete on DISABLED_HOSTS_HOSTNAME: " + uri);
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_DISABLED_HOSTS, LoginsDisabledHosts.HOSTNAME));
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[]{uri.getLastPathSegment()});
// fall through
case DISABLED_HOSTS:
trace("Delete on DISABLED_HOSTS: " + uri);
table = TABLE_DISABLED_HOSTS;
break;
default:
throw new UnsupportedOperationException("Unknown delete URI " + uri);
}
debug("Deleting " + table + " for URI: " + uri);
return db.delete(table, selection, selectionArgs);
}
@Override
@SuppressWarnings("fallthrough")
protected int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
trace("Calling update in transaction on URI: " + uri);
final int match = URI_MATCHER.match(uri);
final SQLiteDatabase db = getWritableDatabase(uri);
final String table;
beginWrite(db);
switch (match) {
case LOGINS_ID:
trace("Update on LOGINS_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_LOGINS, Logins._ID));
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[]{Long.toString(ContentUris.parseId(uri))});
case LOGINS:
trace("Update on LOGINS: " + uri);
table = TABLE_LOGINS;
// Encrypt sensitive data.
encryptContentValueFields(values);
break;
default:
throw new UnsupportedOperationException("Unknown update URI " + uri);
}
trace("Updating " + table + " on URI: " + uri);
return db.update(table, values, selection, selectionArgs);
}
@Override
@SuppressWarnings("fallthrough")
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
trace("Calling query on URI: " + uri);
final SQLiteDatabase db = getReadableDatabase(uri);
final int match = URI_MATCHER.match(uri);
final String groupBy = null;
final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
final String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
switch (match) {
case LOGINS_ID:
trace("Query is on LOGINS_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_LOGINS, Logins._ID));
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
// fall through
case LOGINS:
trace("Query is on LOGINS: " + uri);
if (TextUtils.isEmpty(sortOrder)) {
sortOrder = DEFAULT_LOGINS_SORT_ORDER;
} else {
debug("Using sort order " + sortOrder + ".");
}
qb.setProjectionMap(LOGIN_PROJECTION_MAP);
qb.setTables(TABLE_LOGINS);
break;
case DELETED_LOGINS_ID:
trace("Query is on DELETED_LOGINS_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_DELETED_LOGINS, DeletedLogins._ID));
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
// fall through
case DELETED_LOGINS:
trace("Query is on DELETED_LOGINS: " + uri);
if (TextUtils.isEmpty(sortOrder)) {
sortOrder = DEFAULT_DELETED_LOGINS_SORT_ORDER;
} else {
debug("Using sort order " + sortOrder + ".");
}
qb.setProjectionMap(DELETED_LOGIN_PROJECTION_MAP);
qb.setTables(TABLE_DELETED_LOGINS);
break;
case DISABLED_HOSTS_HOSTNAME:
trace("Query is on DISABLED_HOSTS_HOSTNAME: " + uri);
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_DISABLED_HOSTS, LoginsDisabledHosts.HOSTNAME));
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { uri.getLastPathSegment() });
// fall through
case DISABLED_HOSTS:
trace("Query is on DISABLED_HOSTS: " + uri);
if (TextUtils.isEmpty(sortOrder)) {
sortOrder = DEFAULT_DISABLED_HOSTS_SORT_ORDER;
} else {
debug("Using sort order " + sortOrder + ".");
}
qb.setProjectionMap(DISABLED_HOSTS_PROJECTION_MAP);
qb.setTables(TABLE_DISABLED_HOSTS);
break;
default:
throw new UnsupportedOperationException("Unknown query URI " + uri);
}
trace("Running built query.");
Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, null, sortOrder, limit);
// If decryptManyCursorRows does not return the original cursor, it closes it, so there's
// no need to close here.
cursor = decryptManyCursorRows(cursor);
cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.LOGINS_AUTHORITY_URI);
return cursor;
}
@Override
public String getType(@NonNull Uri uri) {
final int match = URI_MATCHER.match(uri);
switch (match) {
case LOGINS:
return Logins.CONTENT_TYPE;
case LOGINS_ID:
return Logins.CONTENT_ITEM_TYPE;
case DELETED_LOGINS:
return DeletedLogins.CONTENT_TYPE;
case DELETED_LOGINS_ID:
return DeletedLogins.CONTENT_ITEM_TYPE;
case DISABLED_HOSTS:
return LoginsDisabledHosts.CONTENT_TYPE;
case DISABLED_HOSTS_HOSTNAME:
return LoginsDisabledHosts.CONTENT_ITEM_TYPE;
default:
throw new UnsupportedOperationException("Unknown type " + uri);
}
}
/**
* Caller is responsible for invoking this method inside a transaction.
*/
private String getLoginGUIDByID(final String selection, final String[] selectionArgs, final SQLiteDatabase db) {
final Cursor cursor = db.query(Logins.TABLE_LOGINS, new String[]{Logins.GUID}, selection, selectionArgs, null, null, DEFAULT_LOGINS_SORT_ORDER);
try {
if (!cursor.moveToFirst()) {
return null;
}
return cursor.getString(cursor.getColumnIndexOrThrow(Logins.GUID));
} finally {
cursor.close();
}
}
/**
* Caller is responsible for invoking this method inside a transaction.
*/
private boolean storeDeletedLoginForGUIDInTransaction(final String guid, final SQLiteDatabase db) {
if (guid == null) {
return false;
}
final ContentValues values = new ContentValues();
values.put(DeletedLogins.GUID, guid);
values.put(DeletedLogins.TIME_DELETED, System.currentTimeMillis());
return db.insert(TABLE_DELETED_LOGINS, DeletedLogins.GUID, values) > 0;
}
/**
* Caller is responsible for invoking this method inside a transaction.
*/
private void removeDeletedLoginsByGUIDInTransaction(ContentValues values, SQLiteDatabase db) {
if (values.containsKey(Logins.GUID)) {
final String guid = values.getAsString(Logins.GUID);
if (guid == null) {
db.delete(TABLE_DELETED_LOGINS, WHERE_GUID_IS_NULL, null);
} else {
String[] args = new String[]{guid};
db.delete(TABLE_DELETED_LOGINS, WHERE_GUID_IS_VALUE, args);
}
}
}
private void setupDefaultValues(ContentValues values, Uri uri) throws IllegalArgumentException {
final int match = URI_MATCHER.match(uri);
final long now = System.currentTimeMillis();
switch (match) {
case DELETED_LOGINS:
values.put(DeletedLogins.TIME_DELETED, now);
// deleted-logins must contain a guid
if (!values.containsKey(DeletedLogins.GUID)) {
throw new IllegalArgumentException("Must provide GUID for deleted-login");
}
break;
case LOGINS:
values.put(Logins.TIME_CREATED, now);
// Generate GUID for new login. Don't override specified GUIDs.
if (!values.containsKey(Logins.GUID)) {
final String guid = Utils.generateGuid();
values.put(Logins.GUID, guid);
}
// The database happily accepts strings for long values; this just lets us re-use
// the existing helper method.
String nowString = Long.toString(now);
DBUtils.replaceKey(values, null, Logins.HTTP_REALM, null);
DBUtils.replaceKey(values, null, Logins.FORM_SUBMIT_URL, null);
DBUtils.replaceKey(values, null, Logins.ENC_TYPE, "0");
DBUtils.replaceKey(values, null, Logins.TIME_LAST_USED, nowString);
DBUtils.replaceKey(values, null, Logins.TIME_PASSWORD_CHANGED, nowString);
DBUtils.replaceKey(values, null, Logins.TIMES_USED, "0");
break;
case DISABLED_HOSTS:
if (!values.containsKey(LoginsDisabledHosts.HOSTNAME)) {
throw new IllegalArgumentException("Must provide hostname for disabled-host");
}
break;
default:
throw new UnsupportedOperationException("Unknown URI in setupDefaultValues " + uri);
}
}
private void encryptContentValueFields(final ContentValues values) {
if (values.containsKey(Logins.ENCRYPTED_PASSWORD)) {
final String res = encrypt(values.getAsString(Logins.ENCRYPTED_PASSWORD));
values.put(Logins.ENCRYPTED_PASSWORD, res);
}
if (values.containsKey(Logins.ENCRYPTED_USERNAME)) {
final String res = encrypt(values.getAsString(Logins.ENCRYPTED_USERNAME));
values.put(Logins.ENCRYPTED_USERNAME, res);
}
}
/**
* Replace each password and username encrypted ciphertext with its equivalent decrypted
* plaintext in the given cursor.
* <p/>
* The encryption algorithm used to protect logins is unspecified; and further, a consumer of
* consumers should never have access to encrypted ciphertext.
*
* @param cursor containing at least one of password and username encrypted ciphertexts.
* @return a new {@link Cursor} with password and username decrypted plaintexts.
*/
private Cursor decryptManyCursorRows(final Cursor cursor) {
final int passwordIndex = cursor.getColumnIndex(Logins.ENCRYPTED_PASSWORD);
final int usernameIndex = cursor.getColumnIndex(Logins.ENCRYPTED_USERNAME);
if (passwordIndex == -1 && usernameIndex == -1) {
return cursor;
}
// Special case, decrypt the encrypted username or password before returning the cursor.
final MatrixCursor newCursor = new MatrixCursor(cursor.getColumnNames(), cursor.getColumnCount());
try {
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
final ContentValues values = new ContentValues();
DatabaseUtils.cursorRowToContentValues(cursor, values);
if (passwordIndex > -1) {
String decrypted = decrypt(values.getAsString(Logins.ENCRYPTED_PASSWORD));
values.put(Logins.ENCRYPTED_PASSWORD, decrypted);
}
if (usernameIndex > -1) {
String decrypted = decrypt(values.getAsString(Logins.ENCRYPTED_USERNAME));
values.put(Logins.ENCRYPTED_USERNAME, decrypted);
}
final MatrixCursor.RowBuilder rowBuilder = newCursor.newRow();
for (String key : cursor.getColumnNames()) {
rowBuilder.add(values.get(key));
}
}
} finally {
// Close the old cursor before returning the new one.
cursor.close();
}
return newCursor;
}
private String encrypt(@NonNull String initialValue) {
try {
final Cipher cipher = getCipher(Cipher.ENCRYPT_MODE);
return Base64.encodeToString(cipher.doFinal(initialValue.getBytes("UTF-8")), Base64.URL_SAFE);
} catch (Exception e) {
debug("encryption failed : " + e);
throw new IllegalStateException("Logins encryption failed", e);
}
}
private String decrypt(@NonNull String initialValue) {
try {
final Cipher cipher = getCipher(Cipher.DECRYPT_MODE);
return new String(cipher.doFinal(Base64.decode(initialValue.getBytes("UTF-8"), Base64.URL_SAFE)));
} catch (Exception e) {
debug("Decryption failed : " + e);
throw new IllegalStateException("Logins decryption failed", e);
}
}
private Cipher getCipher(int mode) throws UnsupportedEncodingException, GeneralSecurityException {
return new NullCipher();
}
}

View file

@ -0,0 +1,348 @@
/* 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.HashMap;
import org.mozilla.gecko.CrashHandler;
import org.mozilla.gecko.GeckoApp;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoMessageReceiver;
import org.mozilla.gecko.NSSBridge;
import org.mozilla.gecko.db.BrowserContract.DeletedPasswords;
import org.mozilla.gecko.db.BrowserContract.GeckoDisabledHosts;
import org.mozilla.gecko.db.BrowserContract.Passwords;
import org.mozilla.gecko.mozglue.GeckoLoader;
import org.mozilla.gecko.sqlite.MatrixBlobCursor;
import org.mozilla.gecko.sqlite.SQLiteBridge;
import org.mozilla.gecko.sync.Utils;
import android.content.ContentValues;
import android.content.Intent;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
public class PasswordsProvider extends SQLiteBridgeContentProvider {
static final String TABLE_PASSWORDS = "moz_logins";
static final String TABLE_DELETED_PASSWORDS = "moz_deleted_logins";
static final String TABLE_DISABLED_HOSTS = "moz_disabledHosts";
private static final String TELEMETRY_TAG = "SQLITEBRIDGE_PROVIDER_PASSWORDS";
private static final int PASSWORDS = 100;
private static final int DELETED_PASSWORDS = 101;
private static final int DISABLED_HOSTS = 102;
static final String DEFAULT_PASSWORDS_SORT_ORDER = Passwords.HOSTNAME + " ASC";
static final String DEFAULT_DELETED_PASSWORDS_SORT_ORDER = DeletedPasswords.TIME_DELETED + " ASC";
private static final UriMatcher URI_MATCHER;
private static final HashMap<String, String> PASSWORDS_PROJECTION_MAP;
private static final HashMap<String, String> DELETED_PASSWORDS_PROJECTION_MAP;
private static final HashMap<String, String> DISABLED_HOSTS_PROJECTION_MAP;
// this should be kept in sync with the version in toolkit/components/passwordmgr/storage-mozStorage.js
private static final int DB_VERSION = 6;
private static final String DB_FILENAME = "signons.sqlite";
private static final String WHERE_GUID_IS_NULL = BrowserContract.DeletedPasswords.GUID + " IS NULL";
private static final String WHERE_GUID_IS_VALUE = BrowserContract.DeletedPasswords.GUID + " = ?";
private static final String LOG_TAG = "GeckoPasswordsProvider";
private CrashHandler mCrashHandler;
static {
URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
// content://org.mozilla.gecko.providers.browser/passwords/#
URI_MATCHER.addURI(BrowserContract.PASSWORDS_AUTHORITY, "passwords", PASSWORDS);
PASSWORDS_PROJECTION_MAP = new HashMap<String, String>();
PASSWORDS_PROJECTION_MAP.put(Passwords.ID, Passwords.ID);
PASSWORDS_PROJECTION_MAP.put(Passwords.HOSTNAME, Passwords.HOSTNAME);
PASSWORDS_PROJECTION_MAP.put(Passwords.HTTP_REALM, Passwords.HTTP_REALM);
PASSWORDS_PROJECTION_MAP.put(Passwords.FORM_SUBMIT_URL, Passwords.FORM_SUBMIT_URL);
PASSWORDS_PROJECTION_MAP.put(Passwords.USERNAME_FIELD, Passwords.USERNAME_FIELD);
PASSWORDS_PROJECTION_MAP.put(Passwords.PASSWORD_FIELD, Passwords.PASSWORD_FIELD);
PASSWORDS_PROJECTION_MAP.put(Passwords.ENCRYPTED_USERNAME, Passwords.ENCRYPTED_USERNAME);
PASSWORDS_PROJECTION_MAP.put(Passwords.ENCRYPTED_PASSWORD, Passwords.ENCRYPTED_PASSWORD);
PASSWORDS_PROJECTION_MAP.put(Passwords.GUID, Passwords.GUID);
PASSWORDS_PROJECTION_MAP.put(Passwords.ENC_TYPE, Passwords.ENC_TYPE);
PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_CREATED, Passwords.TIME_CREATED);
PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_LAST_USED, Passwords.TIME_LAST_USED);
PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_PASSWORD_CHANGED, Passwords.TIME_PASSWORD_CHANGED);
PASSWORDS_PROJECTION_MAP.put(Passwords.TIMES_USED, Passwords.TIMES_USED);
URI_MATCHER.addURI(BrowserContract.PASSWORDS_AUTHORITY, "deleted-passwords", DELETED_PASSWORDS);
DELETED_PASSWORDS_PROJECTION_MAP = new HashMap<String, String>();
DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.ID, DeletedPasswords.ID);
DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.GUID, DeletedPasswords.GUID);
DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.TIME_DELETED, DeletedPasswords.TIME_DELETED);
URI_MATCHER.addURI(BrowserContract.PASSWORDS_AUTHORITY, "disabled-hosts", DISABLED_HOSTS);
DISABLED_HOSTS_PROJECTION_MAP = new HashMap<String, String>();
DISABLED_HOSTS_PROJECTION_MAP.put(GeckoDisabledHosts.HOSTNAME, GeckoDisabledHosts.HOSTNAME);
}
public PasswordsProvider() {
super(LOG_TAG);
}
@Override
public boolean onCreate() {
mCrashHandler = CrashHandler.createDefaultCrashHandler(getContext());
// We don't use .loadMozGlue because we're in a different process,
// and we just want to reuse code rather than use the loader lock etc.
GeckoLoader.doLoadLibrary(getContext(), "mozglue");
return super.onCreate();
}
@Override
public void shutdown() {
super.shutdown();
if (mCrashHandler != null) {
mCrashHandler.unregister();
mCrashHandler = null;
}
}
@Override
protected String getDBName() {
return DB_FILENAME;
}
@Override
protected String getTelemetryPrefix() {
return TELEMETRY_TAG;
}
@Override
protected int getDBVersion() {
return DB_VERSION;
}
@Override
public String getType(Uri uri) {
final int match = URI_MATCHER.match(uri);
switch (match) {
case PASSWORDS:
return Passwords.CONTENT_TYPE;
case DELETED_PASSWORDS:
return DeletedPasswords.CONTENT_TYPE;
case DISABLED_HOSTS:
return GeckoDisabledHosts.CONTENT_TYPE;
default:
throw new UnsupportedOperationException("Unknown type " + uri);
}
}
@Override
public String getTable(Uri uri) {
final int match = URI_MATCHER.match(uri);
switch (match) {
case DELETED_PASSWORDS:
return TABLE_DELETED_PASSWORDS;
case PASSWORDS:
return TABLE_PASSWORDS;
case DISABLED_HOSTS:
return TABLE_DISABLED_HOSTS;
default:
throw new UnsupportedOperationException("Unknown table " + uri);
}
}
@Override
public String getSortOrder(Uri uri, String aRequested) {
if (!TextUtils.isEmpty(aRequested)) {
return aRequested;
}
final int match = URI_MATCHER.match(uri);
switch (match) {
case DELETED_PASSWORDS:
return DEFAULT_DELETED_PASSWORDS_SORT_ORDER;
case PASSWORDS:
return DEFAULT_PASSWORDS_SORT_ORDER;
case DISABLED_HOSTS:
return null;
default:
throw new UnsupportedOperationException("Unknown URI " + uri);
}
}
@Override
public void setupDefaults(Uri uri, ContentValues values)
throws IllegalArgumentException {
int match = URI_MATCHER.match(uri);
long now = System.currentTimeMillis();
switch (match) {
case DELETED_PASSWORDS:
values.put(DeletedPasswords.TIME_DELETED, now);
// Deleted passwords must contain a guid
if (!values.containsKey(Passwords.GUID)) {
throw new IllegalArgumentException("Must provide a GUID for a deleted password");
}
break;
case PASSWORDS:
values.put(Passwords.TIME_CREATED, now);
// Generate GUID for new password. Don't override specified GUIDs.
if (!values.containsKey(Passwords.GUID)) {
String guid = Utils.generateGuid();
values.put(Passwords.GUID, guid);
}
String nowString = Long.toString(now);
DBUtils.replaceKey(values, null, Passwords.HOSTNAME, "");
DBUtils.replaceKey(values, null, Passwords.HTTP_REALM, "");
DBUtils.replaceKey(values, null, Passwords.FORM_SUBMIT_URL, "");
DBUtils.replaceKey(values, null, Passwords.USERNAME_FIELD, "");
DBUtils.replaceKey(values, null, Passwords.PASSWORD_FIELD, "");
DBUtils.replaceKey(values, null, Passwords.ENCRYPTED_USERNAME, "");
DBUtils.replaceKey(values, null, Passwords.ENCRYPTED_PASSWORD, "");
DBUtils.replaceKey(values, null, Passwords.ENC_TYPE, "0");
DBUtils.replaceKey(values, null, Passwords.TIME_LAST_USED, nowString);
DBUtils.replaceKey(values, null, Passwords.TIME_PASSWORD_CHANGED, nowString);
DBUtils.replaceKey(values, null, Passwords.TIMES_USED, "0");
break;
case DISABLED_HOSTS:
if (!values.containsKey(GeckoDisabledHosts.HOSTNAME)) {
throw new IllegalArgumentException("Must provide a hostname for a disabled host");
}
break;
default:
throw new UnsupportedOperationException("Unknown URI " + uri);
}
}
@Override
public void initGecko() {
// We're not in the main process. The receiver of this Intent can
// communicate with Gecko in the main process.
Intent initIntent = new Intent(getContext(), GeckoMessageReceiver.class);
initIntent.setAction(GeckoApp.ACTION_INIT_PW);
mContext.sendBroadcast(initIntent);
}
private String doCrypto(String initialValue, Uri uri, Boolean encrypt) {
String profilePath = null;
if (uri != null) {
profilePath = uri.getQueryParameter(BrowserContract.PARAM_PROFILE_PATH);
}
String result = "";
try {
if (encrypt) {
if (profilePath != null) {
result = NSSBridge.encrypt(mContext, profilePath, initialValue);
} else {
result = NSSBridge.encrypt(mContext, initialValue);
}
} else {
if (profilePath != null) {
result = NSSBridge.decrypt(mContext, profilePath, initialValue);
} else {
result = NSSBridge.decrypt(mContext, initialValue);
}
}
} catch (Exception ex) {
Log.e(LOG_TAG, "Error in NSSBridge");
throw new RuntimeException(ex);
}
return result;
}
@Override
public void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db) {
if (values.containsKey(Passwords.GUID)) {
String guid = values.getAsString(Passwords.GUID);
if (guid == null) {
db.delete(TABLE_DELETED_PASSWORDS, WHERE_GUID_IS_NULL, null);
return;
}
String[] args = new String[] { guid };
db.delete(TABLE_DELETED_PASSWORDS, WHERE_GUID_IS_VALUE, args);
}
if (values.containsKey(Passwords.ENCRYPTED_PASSWORD)) {
String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_PASSWORD), uri, true);
values.put(Passwords.ENCRYPTED_PASSWORD, res);
values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR);
}
if (values.containsKey(Passwords.ENCRYPTED_USERNAME)) {
String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_USERNAME), uri, true);
values.put(Passwords.ENCRYPTED_USERNAME, res);
values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR);
}
}
@Override
public void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db) {
if (values.containsKey(Passwords.ENCRYPTED_PASSWORD)) {
String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_PASSWORD), uri, true);
values.put(Passwords.ENCRYPTED_PASSWORD, res);
values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR);
}
if (values.containsKey(Passwords.ENCRYPTED_USERNAME)) {
String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_USERNAME), uri, true);
values.put(Passwords.ENCRYPTED_USERNAME, res);
values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR);
}
}
@Override
public void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db) {
int passwordIndex = -1;
int usernameIndex = -1;
String profilePath = null;
try {
passwordIndex = cursor.getColumnIndexOrThrow(Passwords.ENCRYPTED_PASSWORD);
} catch (Exception ex) { }
try {
usernameIndex = cursor.getColumnIndexOrThrow(Passwords.ENCRYPTED_USERNAME);
} catch (Exception ex) { }
if (passwordIndex > -1 || usernameIndex > -1) {
MatrixBlobCursor m = (MatrixBlobCursor)cursor;
if (cursor.moveToFirst()) {
do {
if (passwordIndex > -1) {
String decrypted = doCrypto(cursor.getString(passwordIndex), uri, false);;
m.set(passwordIndex, decrypted);
}
if (usernameIndex > -1) {
String decrypted = doCrypto(cursor.getString(usernameIndex), uri, false);
m.set(usernameIndex, decrypted);
}
} while (cursor.moveToNext());
}
}
}
}

View file

@ -0,0 +1,55 @@
/* 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 org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
import android.content.Context;
import android.database.sqlite.SQLiteOpenHelper;
/**
* Abstract class containing methods needed to make a SQLite-based content
* provider with a database helper of type T, where one database helper is
* held per profile.
*/
public abstract class PerProfileDatabaseProvider<T extends SQLiteOpenHelper> extends AbstractPerProfileDatabaseProvider {
private PerProfileDatabases<T> databases;
@Override
protected PerProfileDatabases<T> getDatabases() {
return databases;
}
protected abstract String getDatabaseName();
/**
* Creates and returns an instance of the appropriate DB helper.
*
* @param context to use to create the database helper
* @param databasePath path to the DB file
* @return instance of the database helper
*/
protected abstract T createDatabaseHelper(Context context, String databasePath);
@Override
public boolean onCreate() {
synchronized (this) {
databases = new PerProfileDatabases<T>(
getContext(), getDatabaseName(), new DatabaseHelperFactory<T>() {
@Override
public T makeDatabaseHelper(Context context, String databasePath) {
final T helper = createDatabaseHelper(context, databasePath);
if (Versions.feature16Plus) {
helper.setWriteAheadLoggingEnabled(true);
}
return helper;
}
});
}
return true;
}
}

View file

@ -0,0 +1,94 @@
/* 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.io.File;
import java.util.HashMap;
import org.mozilla.gecko.GeckoProfile;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.text.TextUtils;
/**
* Manages a set of per-profile database storage helpers.
*/
public class PerProfileDatabases<T extends SQLiteOpenHelper> {
private final HashMap<String, T> mStorages = new HashMap<String, T>();
private final Context mContext;
private final String mDatabaseName;
private final DatabaseHelperFactory<T> mHelperFactory;
// Only used during tests.
public void shutdown() {
synchronized (this) {
for (T t : mStorages.values()) {
try {
t.close();
} catch (Throwable e) {
// Never mind.
}
}
}
}
public interface DatabaseHelperFactory<T> {
public T makeDatabaseHelper(Context context, String databasePath);
}
public PerProfileDatabases(final Context context, final String databaseName, final DatabaseHelperFactory<T> helperFactory) {
mContext = context;
mDatabaseName = databaseName;
mHelperFactory = helperFactory;
}
public String getDatabasePathForProfile(String profile) {
final File profileDir = GeckoProfile.get(mContext, profile).getDir();
if (profileDir == null) {
return null;
}
return new File(profileDir, mDatabaseName).getAbsolutePath();
}
public T getDatabaseHelperForProfile(String profile) {
return getDatabaseHelperForProfile(profile, false);
}
public T getDatabaseHelperForProfile(String profile, boolean isTest) {
// Always fall back to default profile if none has been provided.
if (profile == null) {
profile = GeckoProfile.get(mContext).getName();
}
synchronized (this) {
if (mStorages.containsKey(profile)) {
return mStorages.get(profile);
}
final String databasePath = isTest ? mDatabaseName : getDatabasePathForProfile(profile);
if (databasePath == null) {
throw new IllegalStateException("Database path is null for profile: " + profile);
}
final T helper = mHelperFactory.makeDatabaseHelper(mContext, databasePath);
DBUtils.ensureDatabaseIsNotLocked(helper, databasePath);
mStorages.put(profile, helper);
return helper;
}
}
public synchronized void shrinkMemory() {
for (T t : mStorages.values()) {
final SQLiteDatabase db = t.getWritableDatabase();
db.execSQL("PRAGMA shrink_memory");
}
}
}

View file

@ -0,0 +1,69 @@
/* 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.ArrayList;
import android.os.Parcel;
import android.os.Parcelable;
/**
* A thin representation of a remote client.
* <p>
* We use the hash of the client's GUID as the ID elsewhere.
*/
public class RemoteClient implements Parcelable {
public final String guid;
public final String name;
public final long lastModified;
public final String deviceType;
public final ArrayList<RemoteTab> tabs;
public RemoteClient(String guid, String name, long lastModified, String deviceType) {
this.guid = guid;
this.name = name;
this.lastModified = lastModified;
this.deviceType = deviceType;
this.tabs = new ArrayList<>();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(guid);
parcel.writeString(name);
parcel.writeLong(lastModified);
parcel.writeString(deviceType);
parcel.writeTypedList(tabs);
}
public static final Creator<RemoteClient> CREATOR = new Creator<RemoteClient>() {
@Override
public RemoteClient createFromParcel(final Parcel source) {
final String guid = source.readString();
final String name = source.readString();
final long lastModified = source.readLong();
final String deviceType = source.readString();
final RemoteClient client = new RemoteClient(guid, name, lastModified, deviceType);
source.readTypedList(client.tabs, RemoteTab.CREATOR);
return client;
}
@Override
public RemoteClient[] newArray(final int size) {
return new RemoteClient[size];
}
};
public boolean isDesktop() {
return "desktop".equals(deviceType);
}
}

View file

@ -0,0 +1,90 @@
/* 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 android.os.Parcel;
import android.os.Parcelable;
/**
* A thin representation of a remote tab.
* <p>
* These are generated functions.
*/
public class RemoteTab implements Parcelable {
public final String title;
public final String url;
public final long lastUsed;
public RemoteTab(String title, String url, long lastUsed) {
this.title = title;
this.url = url;
this.lastUsed = lastUsed;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(title);
parcel.writeString(url);
parcel.writeLong(lastUsed);
}
public static final Creator<RemoteTab> CREATOR = new Creator<RemoteTab>() {
@Override
public RemoteTab createFromParcel(final Parcel source) {
final String title = source.readString();
final String url = source.readString();
final long lastUsed = source.readLong();
return new RemoteTab(title, url, lastUsed);
}
@Override
public RemoteTab[] newArray(final int size) {
return new RemoteTab[size];
}
};
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((title == null) ? 0 : title.hashCode());
result = prime * result + ((url == null) ? 0 : url.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
RemoteTab other = (RemoteTab) obj;
if (title == null) {
if (other.title != null) {
return false;
}
} else if (!title.equals(other.title)) {
return false;
}
if (url == null) {
if (other.url != null) {
return false;
}
} else if (!url.equals(other.url)) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,471 @@
/* 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.io.File;
import java.util.HashMap;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.GeckoThread;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.mozglue.GeckoLoader;
import org.mozilla.gecko.sqlite.SQLiteBridge;
import org.mozilla.gecko.sqlite.SQLiteBridgeException;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
/*
* Provides a basic ContentProvider that sets up and sends queries through
* SQLiteBridge. Content providers should extend this by setting the appropriate
* table and version numbers in onCreate, and implementing the abstract methods:
*
* public abstract String getTable(Uri uri);
* public abstract String getSortOrder(Uri uri, String aRequested);
* public abstract void setupDefaults(Uri uri, ContentValues values);
* public abstract void initGecko();
*/
public abstract class SQLiteBridgeContentProvider extends ContentProvider {
private static final String ERROR_MESSAGE_DATABASE_IS_LOCKED = "Can't step statement: (5) database is locked";
private HashMap<String, SQLiteBridge> mDatabasePerProfile;
protected Context mContext;
private final String mLogTag;
protected SQLiteBridgeContentProvider(String logTag) {
mLogTag = logTag;
}
/**
* Subclasses must override this to allow error reporting code to compose
* the correct histogram name.
*
* Ensure that you define the new histograms if you define a new class!
*/
protected abstract String getTelemetryPrefix();
/**
* Errors are recorded in telemetry using an enumerated histogram.
*
* <https://developer.mozilla.org/en-US/docs/Mozilla/Performance/
* Adding_a_new_Telemetry_probe#Choosing_a_Histogram_Type>
*
* These are the allowable enumeration values. Keep these in sync with the
* histogram definition!
*
*/
private static enum TelemetryErrorOp {
BULKINSERT (0),
DELETE (1),
INSERT (2),
QUERY (3),
UPDATE (4);
private final int bucket;
TelemetryErrorOp(final int bucket) {
this.bucket = bucket;
}
public int getBucket() {
return bucket;
}
}
@Override
public void shutdown() {
if (mDatabasePerProfile == null) {
return;
}
synchronized (this) {
for (SQLiteBridge bridge : mDatabasePerProfile.values()) {
if (bridge != null) {
try {
bridge.close();
} catch (Exception ex) { }
}
}
mDatabasePerProfile = null;
}
super.shutdown();
}
@Override
public void finalize() {
shutdown();
}
/**
* Return true of the query is from Firefox Sync.
* @param uri query URI
*/
public static boolean isCallerSync(Uri uri) {
String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
return !TextUtils.isEmpty(isSync);
}
private SQLiteBridge getDB(Context context, final String databasePath) {
SQLiteBridge bridge = null;
boolean dbNeedsSetup = true;
try {
String resourcePath = context.getPackageResourcePath();
GeckoLoader.loadSQLiteLibs(context, resourcePath);
GeckoLoader.loadNSSLibs(context, resourcePath);
bridge = SQLiteBridge.openDatabase(databasePath, null, 0);
int version = bridge.getVersion();
dbNeedsSetup = version != getDBVersion();
} catch (SQLiteBridgeException ex) {
// close the database
if (bridge != null) {
bridge.close();
}
// this will throw if the database can't be found
// we should attempt to set it up if Gecko is running
dbNeedsSetup = true;
Log.e(mLogTag, "Error getting version ", ex);
// if Gecko is not running, we should bail out. Otherwise we try to
// let Gecko build the database for us
if (!GeckoThread.isRunning()) {
Log.e(mLogTag, "Can not set up database. Gecko is not running");
return null;
}
}
// If the database is not set up yet, or is the wrong schema version, we send an initialize
// call to Gecko. Gecko will handle building the database file correctly, as well as any
// migrations that are necessary
if (dbNeedsSetup) {
bridge = null;
initGecko();
}
return bridge;
}
/**
* Returns the absolute path of a database file depending on the specified profile and dbName.
* @param profile
* the profile whose dbPath must be returned
* @param dbName
* the name of the db file whose absolute path must be returned
* @return the absolute path of the db file or <code>null</code> if it was not possible to retrieve a valid path
*
*/
private String getDatabasePathForProfile(String profile, String dbName) {
// Depends on the vagaries of GeckoProfile.get, so null check for safety.
File profileDir = GeckoProfile.get(mContext, profile).getDir();
if (profileDir == null) {
return null;
}
String databasePath = new File(profileDir, dbName).getAbsolutePath();
return databasePath;
}
/**
* Returns a SQLiteBridge object according to the specified profile id and to the name of db related to the
* current provider instance.
* @param profile
* the id of the profile to be used to retrieve the related SQLiteBridge
* @return the <code>SQLiteBridge</code> related to the specified profile id or <code>null</code> if it was
* not possible to retrieve a valid SQLiteBridge
*/
private SQLiteBridge getDatabaseForProfile(String profile) {
if (profile == null) {
profile = GeckoProfile.get(mContext).getName();
Log.d(mLogTag, "No profile provided, using '" + profile + "'");
}
final String dbName = getDBName();
String mapKey = profile + "/" + dbName;
SQLiteBridge db = null;
synchronized (this) {
db = mDatabasePerProfile.get(mapKey);
if (db != null) {
return db;
}
final String dbPath = getDatabasePathForProfile(profile, dbName);
if (dbPath == null) {
Log.e(mLogTag, "Failed to get a valid db path for profile '" + profile + "'' dbName '" + dbName + "'");
return null;
}
db = getDB(mContext, dbPath);
if (db != null) {
mDatabasePerProfile.put(mapKey, db);
}
}
return db;
}
/**
* Returns a SQLiteBridge object according to the specified profile path and to the name of db related to the
* current provider instance.
* @param profilePath
* the profilePath to be used to retrieve the related SQLiteBridge
* @return the <code>SQLiteBridge</code> related to the specified profile path or <code>null</code> if it was
* not possible to retrieve a valid <code>SQLiteBridge</code>
*/
private SQLiteBridge getDatabaseForProfilePath(String profilePath) {
File profileDir = new File(profilePath, getDBName());
final String dbPath = profileDir.getPath();
return getDatabaseForDBPath(dbPath);
}
/**
* Returns a SQLiteBridge object according to the specified file path.
* @param dbPath
* the path of the file to be used to retrieve the related SQLiteBridge
* @return the <code>SQLiteBridge</code> related to the specified file path or <code>null</code> if it was
* not possible to retrieve a valid <code>SQLiteBridge</code>
*
*/
private SQLiteBridge getDatabaseForDBPath(String dbPath) {
SQLiteBridge db = null;
synchronized (this) {
db = mDatabasePerProfile.get(dbPath);
if (db != null) {
return db;
}
db = getDB(mContext, dbPath);
if (db != null) {
mDatabasePerProfile.put(dbPath, db);
}
}
return db;
}
/**
* Returns a SQLiteBridge object to be used to perform operations on the given <code>Uri</code>.
* @param uri
* the <code>Uri</code> to be used to retrieve the related SQLiteBridge
* @return a <code>SQLiteBridge</code> object to be used on the given uri or <code>null</code> if it was
* not possible to retrieve a valid <code>SQLiteBridge</code>
*
*/
private SQLiteBridge getDatabase(Uri uri) {
String profile = null;
String profilePath = null;
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
profilePath = uri.getQueryParameter(BrowserContract.PARAM_PROFILE_PATH);
// Testing will specify the absolute profile path
if (profilePath != null) {
return getDatabaseForProfilePath(profilePath);
}
return getDatabaseForProfile(profile);
}
@Override
public boolean onCreate() {
mContext = getContext();
synchronized (this) {
mDatabasePerProfile = new HashMap<String, SQLiteBridge>();
}
return true;
}
@Override
public String getType(Uri uri) {
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int deleted = 0;
final SQLiteBridge db = getDatabase(uri);
if (db == null) {
return deleted;
}
try {
deleted = db.delete(getTable(uri), selection, selectionArgs);
} catch (SQLiteBridgeException ex) {
reportError(ex, TelemetryErrorOp.DELETE);
throw ex;
}
return deleted;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
long id = -1;
final SQLiteBridge db = getDatabase(uri);
// If we can not get a SQLiteBridge instance, its likely that the database
// has not been set up and Gecko is not running. We return null and expect
// callers to try again later
if (db == null) {
return null;
}
setupDefaults(uri, values);
boolean useTransaction = !db.inTransaction();
try {
if (useTransaction) {
db.beginTransaction();
}
// onPreInsert does a check for the item in the deleted table in some cases
// so we put it inside this transaction
onPreInsert(values, uri, db);
id = db.insert(getTable(uri), null, values);
if (useTransaction) {
db.setTransactionSuccessful();
}
} catch (SQLiteBridgeException ex) {
reportError(ex, TelemetryErrorOp.INSERT);
throw ex;
} finally {
if (useTransaction) {
db.endTransaction();
}
}
return ContentUris.withAppendedId(uri, id);
}
@Override
public int bulkInsert(Uri uri, ContentValues[] allValues) {
final SQLiteBridge db = getDatabase(uri);
// If we can not get a SQLiteBridge instance, its likely that the database
// has not been set up and Gecko is not running. We return 0 and expect
// callers to try again later
if (db == null) {
return 0;
}
int rowsAdded = 0;
String table = getTable(uri);
try {
db.beginTransaction();
for (ContentValues initialValues : allValues) {
ContentValues values = new ContentValues(initialValues);
setupDefaults(uri, values);
onPreInsert(values, uri, db);
db.insert(table, null, values);
rowsAdded++;
}
db.setTransactionSuccessful();
} catch (SQLiteBridgeException ex) {
reportError(ex, TelemetryErrorOp.BULKINSERT);
throw ex;
} finally {
db.endTransaction();
}
if (rowsAdded > 0) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
mContext.getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return rowsAdded;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
int updated = 0;
final SQLiteBridge db = getDatabase(uri);
// If we can not get a SQLiteBridge instance, its likely that the database
// has not been set up and Gecko is not running. We return null and expect
// callers to try again later
if (db == null) {
return updated;
}
onPreUpdate(values, uri, db);
try {
updated = db.update(getTable(uri), values, selection, selectionArgs);
} catch (SQLiteBridgeException ex) {
reportError(ex, TelemetryErrorOp.UPDATE);
throw ex;
}
return updated;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
Cursor cursor = null;
final SQLiteBridge db = getDatabase(uri);
// If we can not get a SQLiteBridge instance, its likely that the database
// has not been set up and Gecko is not running. We return null and expect
// callers to try again later
if (db == null) {
return cursor;
}
sortOrder = getSortOrder(uri, sortOrder);
try {
cursor = db.query(getTable(uri), projection, selection, selectionArgs, null, null, sortOrder, null);
onPostQuery(cursor, uri, db);
} catch (SQLiteBridgeException ex) {
reportError(ex, TelemetryErrorOp.QUERY);
throw ex;
}
return cursor;
}
private String getHistogram(SQLiteBridgeException e) {
// If you add values here, make sure to update
// toolkit/components/telemetry/Histograms.json.
if (ERROR_MESSAGE_DATABASE_IS_LOCKED.equals(e.getMessage())) {
return getTelemetryPrefix() + "_LOCKED";
}
return null;
}
protected void reportError(SQLiteBridgeException e, TelemetryErrorOp op) {
Log.e(mLogTag, "Error in database " + op.name(), e);
final String histogram = getHistogram(e);
if (histogram == null) {
return;
}
Telemetry.addToHistogram(histogram, op.getBucket());
}
protected abstract String getDBName();
protected abstract int getDBVersion();
protected abstract String getTable(Uri uri);
protected abstract String getSortOrder(Uri uri, String aRequested);
protected abstract void setupDefaults(Uri uri, ContentValues values);
protected abstract void initGecko();
protected abstract void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db);
protected abstract void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db);
protected abstract void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db);
}

View file

@ -0,0 +1,127 @@
/* 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.db.BrowserContract;
import org.mozilla.gecko.db.BrowserContract.SearchHistory;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
public class SearchHistoryProvider extends SharedBrowserDatabaseProvider {
private static final String LOG_TAG = "GeckoSearchProvider";
private static final boolean DEBUG_ENABLED = false;
/**
* Collapse whitespace.
*/
private String stripWhitespace(String query) {
if (TextUtils.isEmpty(query)) {
return "";
}
// Collapse whitespace
return query.trim().replaceAll("\\s+", " ");
}
@Override
public Uri insertInTransaction(Uri uri, ContentValues cv) {
final String query = stripWhitespace(cv.getAsString(SearchHistory.QUERY));
// We don't support inserting empty search queries.
if (TextUtils.isEmpty(query)) {
return null;
}
final SQLiteDatabase db = getWritableDatabase(uri);
long id = -1;
/*
* Attempt to insert the query. The catch block handles the case when
* the query already exists in the DB.
*/
try {
cv.put(SearchHistory.QUERY, query);
cv.put(SearchHistory.VISITS, 1);
cv.put(SearchHistory.DATE_LAST_VISITED, System.currentTimeMillis());
id = db.insertOrThrow(SearchHistory.TABLE_NAME, null, cv);
if (id > 0) {
return ContentUris.withAppendedId(uri, id);
}
} catch (SQLException e) {
// This happens when the column already exists for this term.
if (DEBUG_ENABLED) {
Log.w(LOG_TAG, String.format("Query `%s` already in db", query));
}
}
/*
* Increment the VISITS counter and update the DATE_LAST_VISITED.
*/
final String sql = "UPDATE " + SearchHistory.TABLE_NAME + " SET " +
SearchHistory.VISITS + " = " + SearchHistory.VISITS + " + 1, " +
SearchHistory.DATE_LAST_VISITED + " = " + System.currentTimeMillis() +
" WHERE " + SearchHistory.QUERY + " = ?";
final Cursor c = db.rawQuery(sql, new String[] { query });
try {
if (c.getCount() > 1) {
// There is a UNIQUE constraint on the QUERY column,
// so there should only be one match.
return null;
}
if (c.moveToFirst()) {
return ContentUris.withAppendedId(uri, c.getInt(c.getColumnIndex(SearchHistory._ID)));
}
} finally {
c.close();
}
return null;
}
@Override
public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
return getWritableDatabase(uri).delete(SearchHistory.TABLE_NAME,
selection, selectionArgs);
}
/**
* Since we are managing counts and the full-text db, an update
* could mangle the internal state. So we disable it.
*/
@Override
public int updateInTransaction(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
throw new UnsupportedOperationException("This content provider does not support updating items");
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
final String groupBy = null;
final String having = null;
final String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
final Cursor cursor = getReadableDatabase(uri).query(SearchHistory.TABLE_NAME, projection,
selection, selectionArgs, groupBy, having, sortOrder, limit);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
@Override
public String getType(Uri uri) {
return SearchHistory.CONTENT_TYPE;
}
}

View file

@ -0,0 +1,12 @@
/* -*- 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/. */
package org.mozilla.gecko.db;
import android.content.ContentResolver;
public interface Searches {
public void insert(ContentResolver cr, String query);
}

View file

@ -0,0 +1,128 @@
/* 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 org.mozilla.gecko.db.BrowserContract.CommonColumns;
import org.mozilla.gecko.db.BrowserContract.SyncColumns;
import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.util.Log;
/**
* A ContentProvider subclass that provides per-profile browser.db access
* that can be safely shared between multiple providers.
*
* If multiple ContentProvider classes wish to share a database, it's
* vitally important that they use the same SQLiteOpenHelpers for access.
*
* Failure to do so can cause accidental concurrent writes, with the result
* being unexpected SQLITE_BUSY errors.
*
* This class provides a static {@link PerProfileDatabases} instance, lazily
* initialized within {@link SharedBrowserDatabaseProvider#onCreate()}.
*/
public abstract class SharedBrowserDatabaseProvider extends AbstractPerProfileDatabaseProvider {
private static final String LOGTAG = SharedBrowserDatabaseProvider.class.getSimpleName();
private static PerProfileDatabases<BrowserDatabaseHelper> databases;
@Override
protected PerProfileDatabases<BrowserDatabaseHelper> getDatabases() {
return databases;
}
@Override
public void shutdown() {
synchronized (SharedBrowserDatabaseProvider.class) {
databases.shutdown();
databases = null;
}
}
@Override
public boolean onCreate() {
// If necessary, do the shared DB work.
synchronized (SharedBrowserDatabaseProvider.class) {
if (databases != null) {
return true;
}
final DatabaseHelperFactory<BrowserDatabaseHelper> helperFactory = new DatabaseHelperFactory<BrowserDatabaseHelper>() {
@Override
public BrowserDatabaseHelper makeDatabaseHelper(Context context, String databasePath) {
final BrowserDatabaseHelper helper = new BrowserDatabaseHelper(context, databasePath);
if (Versions.feature16Plus) {
helper.setWriteAheadLoggingEnabled(true);
}
return helper;
}
};
databases = new PerProfileDatabases<BrowserDatabaseHelper>(getContext(), BrowserDatabaseHelper.DATABASE_NAME, helperFactory);
}
return true;
}
/**
* Clean up some deleted records from the specified table.
*
* If called in an existing transaction, it is the caller's responsibility
* to ensure that the transaction is already upgraded to a writer, because
* this method issues a read followed by a write, and thus is potentially
* vulnerable to an unhandled SQLITE_BUSY failure during the upgrade.
*
* If not called in an existing transaction, no new explicit transaction
* will be begun.
*/
protected void cleanUpSomeDeletedRecords(Uri fromUri, String tableName) {
Log.d(LOGTAG, "Cleaning up deleted records from " + tableName);
// We clean up 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.
// 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.
// Maximum age of deleted records to be cleaned up (20 days in ms)
final long MAX_AGE_OF_DELETED_RECORDS = 86400000 * 20;
// Number of records marked as deleted to be removed
final long DELETED_RECORDS_PURGE_LIMIT = 5;
// Android SQLite doesn't have LIMIT on DELETE. Instead, query for the
// IDs of matching rows, then delete them in one go.
final long now = System.currentTimeMillis();
final String selection = getDeletedItemSelection(now - MAX_AGE_OF_DELETED_RECORDS);
final String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
final SQLiteDatabase db = getWritableDatabaseForProfile(profile, isTest(fromUri));
final String limit = Long.toString(DELETED_RECORDS_PURGE_LIMIT, 10);
final Cursor cursor = db.query(tableName, new String[] { CommonColumns._ID }, selection, null, null, null, null, limit);
final String inClause;
try {
inClause = DBUtils.computeSQLInClauseFromLongs(cursor, CommonColumns._ID);
} finally {
cursor.close();
}
db.delete(tableName, inClause, null);
}
// Override this, or override cleanUpSomeDeletedRecords.
protected String getDeletedItemSelection(long earlierThan) {
if (earlierThan == -1L) {
return SyncColumns.IS_DELETED + " = 1";
}
return SyncColumns.IS_DELETED + " = 1 AND " + SyncColumns.DATE_MODIFIED + " <= " + earlierThan;
}
}

View file

@ -0,0 +1,629 @@
/* -*- 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/. */
package org.mozilla.gecko.db;
import android.content.Context;
import android.content.ContentResolver;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.R;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.restrictions.Restrictions;
import org.mozilla.gecko.util.RawResource;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.preferences.GeckoPreferences;
/**
* {@code SuggestedSites} provides API to get a list of locale-specific
* suggested sites to be used in Fennec's top sites panel. It provides
* only a single method to fetch the list as a {@code Cursor}. This cursor
* will then be wrapped by {@code TopSitesCursorWrapper} to blend top,
* pinned, and suggested sites in the UI. The returned {@code Cursor}
* uses its own schema defined in {@code BrowserContract.SuggestedSites}
* for clarity.
*
* Under the hood, {@code SuggestedSites} keeps reference to the
* parsed list of sites to avoid reparsing the JSON file on every
* {@code get()} call.
*
* The default list of suggested sites is stored in a raw Android
* resource ({@code R.raw.suggestedsites}) which is dynamically
* generated at build time for each target locale.
*
* Changes to the list of suggested sites are saved in SharedPreferences.
*/
@RobocopTarget
public class SuggestedSites {
private static final String LOGTAG = "GeckoSuggestedSites";
// SharedPreference key for suggested sites that should be hidden.
public static final String PREF_SUGGESTED_SITES_HIDDEN = GeckoPreferences.NON_PREF_PREFIX + "suggestedSites.hidden";
public static final String PREF_SUGGESTED_SITES_HIDDEN_OLD = "suggestedSites.hidden";
// Locale used to generate the current suggested sites.
public static final String PREF_SUGGESTED_SITES_LOCALE = GeckoPreferences.NON_PREF_PREFIX + "suggestedSites.locale";
public static final String PREF_SUGGESTED_SITES_LOCALE_OLD = "suggestedSites.locale";
// File in profile dir with the list of suggested sites.
private static final String FILENAME = "suggestedsites.json";
private static final String[] COLUMNS = new String[] {
BrowserContract.SuggestedSites._ID,
BrowserContract.SuggestedSites.URL,
BrowserContract.SuggestedSites.TITLE,
BrowserContract.Combined.HISTORY_ID
};
private static final String JSON_KEY_URL = "url";
private static final String JSON_KEY_TITLE = "title";
private static final String JSON_KEY_IMAGE_URL = "imageurl";
private static final String JSON_KEY_BG_COLOR = "bgcolor";
private static final String JSON_KEY_RESTRICTED = "restricted";
private static class Site {
public final String url;
public final String title;
public final String imageUrl;
public final String bgColor;
public final boolean restricted;
public Site(JSONObject json) throws JSONException {
this.restricted = !json.isNull(JSON_KEY_RESTRICTED);
this.url = json.getString(JSON_KEY_URL);
this.title = json.getString(JSON_KEY_TITLE);
this.imageUrl = json.getString(JSON_KEY_IMAGE_URL);
this.bgColor = json.getString(JSON_KEY_BG_COLOR);
validate();
}
public Site(String url, String title, String imageUrl, String bgColor) {
this.url = url;
this.title = title;
this.imageUrl = imageUrl;
this.bgColor = bgColor;
this.restricted = false;
validate();
}
private void validate() {
// Site instances must have non-empty values for all properties except IDs.
if (TextUtils.isEmpty(url) ||
TextUtils.isEmpty(title) ||
TextUtils.isEmpty(imageUrl) ||
TextUtils.isEmpty(bgColor)) {
throw new IllegalStateException("Suggested sites must have a URL, title, " +
"image URL, and background color.");
}
}
@Override
public String toString() {
return "{ url = " + url + "\n" +
"restricted = " + restricted + "\n" +
"title = " + title + "\n" +
"imageUrl = " + imageUrl + "\n" +
"bgColor = " + bgColor + " }";
}
public JSONObject toJSON() throws JSONException {
final JSONObject json = new JSONObject();
if (restricted) {
json.put(JSON_KEY_RESTRICTED, true);
}
json.put(JSON_KEY_URL, url);
json.put(JSON_KEY_TITLE, title);
json.put(JSON_KEY_IMAGE_URL, imageUrl);
json.put(JSON_KEY_BG_COLOR, bgColor);
return json;
}
}
final Context context;
final Distribution distribution;
private File cachedFile;
private Map<String, Site> cachedSites;
private Set<String> cachedBlacklist;
public SuggestedSites(Context appContext) {
this(appContext, null);
}
public SuggestedSites(Context appContext, Distribution distribution) {
this(appContext, distribution, null);
}
public SuggestedSites(Context appContext, Distribution distribution, File file) {
this.context = appContext;
this.distribution = distribution;
this.cachedFile = file;
}
synchronized File getFile() {
if (cachedFile == null) {
cachedFile = GeckoProfile.get(context).getFile(FILENAME);
}
return cachedFile;
}
private static boolean isNewLocale(Context context, Locale requestedLocale) {
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
String locale = prefs.getString(PREF_SUGGESTED_SITES_LOCALE_OLD, null);
if (locale != null) {
// Migrate the old pref and remove it
final Editor editor = prefs.edit();
editor.remove(PREF_SUGGESTED_SITES_LOCALE_OLD);
editor.putString(PREF_SUGGESTED_SITES_LOCALE, locale);
editor.apply();
} else {
locale = prefs.getString(PREF_SUGGESTED_SITES_LOCALE, null);
}
if (locale == null) {
// Initialize config with the current locale
updateSuggestedSitesLocale(context);
return true;
}
return !TextUtils.equals(requestedLocale.toString(), locale);
}
/**
* Return the current locale and its fallback (en_US) in order.
*/
private static List<Locale> getAcceptableLocales() {
final List<Locale> locales = new ArrayList<Locale>();
final Locale defaultLocale = Locale.getDefault();
locales.add(defaultLocale);
if (!defaultLocale.equals(Locale.US)) {
locales.add(Locale.US);
}
return locales;
}
private static Map<String, Site> loadSites(File f) throws IOException {
Scanner scanner = null;
try {
scanner = new Scanner(f, "UTF-8");
return loadSites(scanner.useDelimiter("\\A").next());
} finally {
if (scanner != null) {
scanner.close();
}
}
}
private static Map<String, Site> loadSites(String jsonString) {
if (TextUtils.isEmpty(jsonString)) {
return null;
}
Map<String, Site> sites = null;
try {
final JSONArray jsonSites = new JSONArray(jsonString);
sites = new LinkedHashMap<String, Site>(jsonSites.length());
final int count = jsonSites.length();
for (int i = 0; i < count; i++) {
final Site site = new Site(jsonSites.getJSONObject(i));
sites.put(site.url, site);
}
} catch (Exception e) {
Log.e(LOGTAG, "Failed to refresh suggested sites", e);
return null;
}
return sites;
}
/**
* Saves suggested sites file to disk. Access to this method should
* be synchronized on 'file'.
*/
static void saveSites(File f, Map<String, Site> sites) {
ThreadUtils.assertNotOnUiThread();
if (sites == null || sites.isEmpty()) {
return;
}
OutputStreamWriter osw = null;
try {
final JSONArray jsonSites = new JSONArray();
for (Site site : sites.values()) {
jsonSites.put(site.toJSON());
}
osw = new OutputStreamWriter(new FileOutputStream(f), "UTF-8");
final String jsonString = jsonSites.toString();
osw.write(jsonString, 0, jsonString.length());
} catch (Exception e) {
Log.e(LOGTAG, "Failed to save suggested sites", e);
} finally {
if (osw != null) {
try {
osw.close();
} catch (IOException e) {
// Ignore.
}
}
}
}
private void maybeWaitForDistribution() {
if (distribution == null) {
return;
}
distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
@Override
public void distributionNotFound() {
// If distribution doesn't exist, simply continue to load
// suggested sites directly from resources. See refresh().
}
@Override
public void distributionFound(Distribution distribution) {
Log.d(LOGTAG, "Running post-distribution task: suggested sites.");
// Merge suggested sites from distribution with the
// default ones. Distribution takes precedence.
Map<String, Site> sites = loadFromDistribution(distribution);
if (sites == null) {
sites = new LinkedHashMap<String, Site>();
}
sites.putAll(loadFromResource());
// Update cached list of sites.
setCachedSites(sites);
// Save the result to disk.
final File file = getFile();
synchronized (file) {
saveSites(file, sites);
}
// Then notify any active loaders about the changes.
final ContentResolver cr = context.getContentResolver();
cr.notifyChange(BrowserContract.SuggestedSites.CONTENT_URI, null);
}
@Override
public void distributionArrivedLate(Distribution distribution) {
distributionFound(distribution);
}
});
}
/**
* Loads suggested sites from a distribution file either matching the
* current locale or with the fallback locale (en-US).
*
* It's assumed that the given distribution instance is ready to be
* used and exists.
*/
static Map<String, Site> loadFromDistribution(Distribution dist) {
for (Locale locale : getAcceptableLocales()) {
try {
final String languageTag = Locales.getLanguageTag(locale);
final String path = String.format("suggestedsites/locales/%s/%s",
languageTag, FILENAME);
final File f = dist.getDistributionFile(path);
if (f == null) {
Log.d(LOGTAG, "No suggested sites for locale: " + languageTag);
continue;
}
return loadSites(f);
} catch (Exception e) {
Log.e(LOGTAG, "Failed to open suggested sites for locale " +
locale + " in distribution.", e);
}
}
return null;
}
private Map<String, Site> loadFromProfile() {
try {
final File file = getFile();
synchronized (file) {
return loadSites(file);
}
} catch (FileNotFoundException e) {
maybeWaitForDistribution();
} catch (IOException e) {
// Fall through, return null.
}
return null;
}
Map<String, Site> loadFromResource() {
try {
return loadSites(RawResource.getAsString(context, R.raw.suggestedsites));
} catch (IOException e) {
return null;
}
}
private synchronized void setCachedSites(Map<String, Site> sites) {
cachedSites = Collections.unmodifiableMap(sites);
updateSuggestedSitesLocale(context);
}
/**
* Refreshes the cached list of sites either from the default raw
* source or standard file location. This will be called on every
* cache miss during a {@code get()} call.
*/
private void refresh() {
Log.d(LOGTAG, "Refreshing suggested sites from file");
Map<String, Site> sites = loadFromProfile();
if (sites == null) {
sites = loadFromResource();
}
// Update cached list of sites.
if (sites != null) {
setCachedSites(sites);
}
}
private static void updateSuggestedSitesLocale(Context context) {
final Editor editor = GeckoSharedPrefs.forProfile(context).edit();
editor.putString(PREF_SUGGESTED_SITES_LOCALE, Locale.getDefault().toString());
editor.apply();
}
private synchronized Site getSiteForUrl(String url) {
if (cachedSites == null) {
return null;
}
return cachedSites.get(url);
}
/**
* Returns a {@code Cursor} with the list of suggested websites.
*
* @param limit maximum number of suggested sites.
*/
public Cursor get(int limit) {
return get(limit, Locale.getDefault());
}
/**
* Returns a {@code Cursor} with the list of suggested websites.
*
* @param limit maximum number of suggested sites.
* @param locale the target locale.
*/
public Cursor get(int limit, Locale locale) {
return get(limit, locale, null);
}
/**
* Returns a {@code Cursor} with the list of suggested websites.
*
* @param limit maximum number of suggested sites.
* @param excludeUrls list of URLs to be excluded from the list.
*/
public Cursor get(int limit, List<String> excludeUrls) {
return get(limit, Locale.getDefault(), excludeUrls);
}
/**
* Returns a {@code Cursor} with the list of suggested websites.
*
* @param limit maximum number of suggested sites.
* @param locale the target locale.
* @param excludeUrls list of URLs to be excluded from the list.
*/
public synchronized Cursor get(int limit, Locale locale, List<String> excludeUrls) {
final MatrixCursor cursor = new MatrixCursor(COLUMNS);
final boolean isNewLocale = isNewLocale(context, locale);
// Force the suggested sites file in profile dir to be re-generated
// if the locale has changed.
if (isNewLocale) {
getFile().delete();
}
if (cachedSites == null || isNewLocale) {
Log.d(LOGTAG, "No cached sites, refreshing.");
refresh();
}
// Return empty cursor if there was an error when
// loading the suggested sites or the list is empty.
if (cachedSites == null || cachedSites.isEmpty()) {
return cursor;
}
excludeUrls = includeBlacklist(excludeUrls);
final int sitesCount = cachedSites.size();
Log.d(LOGTAG, "Number of suggested sites: " + sitesCount);
final int maxCount = Math.min(limit, sitesCount);
// History IDS: real history is positive, -1 is no history id in the combined table
// hence we can start at -2 for suggested sites
int id = -1;
for (Site site : cachedSites.values()) {
// Decrement ID here: this ensure we have a consistent ID to URL mapping, even if items
// are removed. If we instead decremented at the point of insertion we'd end up with
// ID conflicts when a suggested site is removed. (note that cachedSites does not change
// while we're already showing topsites)
--id;
if (cursor.getCount() == maxCount) {
break;
}
if (excludeUrls != null && excludeUrls.contains(site.url)) {
continue;
}
final boolean restrictedProfile = Restrictions.isRestrictedProfile(context);
if (restrictedProfile == site.restricted) {
final RowBuilder row = cursor.newRow();
row.add(id);
row.add(site.url);
row.add(site.title);
row.add(id);
}
}
cursor.setNotificationUri(context.getContentResolver(),
BrowserContract.SuggestedSites.CONTENT_URI);
return cursor;
}
public boolean contains(String url) {
return (getSiteForUrl(url) != null);
}
public String getImageUrlForUrl(String url) {
final Site site = getSiteForUrl(url);
return (site != null ? site.imageUrl : null);
}
public String getBackgroundColorForUrl(String url) {
final Site site = getSiteForUrl(url);
return (site != null ? site.bgColor : null);
}
private Set<String> loadBlacklist() {
Log.d(LOGTAG, "Loading blacklisted suggested sites from SharedPreferences.");
final Set<String> blacklist = new HashSet<String>();
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
String sitesString = prefs.getString(PREF_SUGGESTED_SITES_HIDDEN_OLD, null);
if (sitesString != null) {
// Migrate the old pref and remove it
final Editor editor = prefs.edit();
editor.remove(PREF_SUGGESTED_SITES_HIDDEN_OLD);
editor.putString(PREF_SUGGESTED_SITES_HIDDEN, sitesString);
editor.apply();
} else {
sitesString = prefs.getString(PREF_SUGGESTED_SITES_HIDDEN, null);
}
if (sitesString != null) {
for (String site : sitesString.trim().split(" ")) {
blacklist.add(Uri.decode(site));
}
}
return blacklist;
}
private List<String> includeBlacklist(List<String> originalList) {
if (cachedBlacklist == null) {
cachedBlacklist = loadBlacklist();
}
if (cachedBlacklist.isEmpty()) {
return originalList;
}
if (originalList == null) {
originalList = new ArrayList<String>();
}
originalList.addAll(cachedBlacklist);
return originalList;
}
/**
* Blacklist a suggested site so it will no longer be returned as a suggested site.
* This method should only be called from a background thread because it may write
* to SharedPreferences.
*
* Urls that are not Suggested Sites are ignored.
*
* @param url String url of site to blacklist
* @return true is blacklisted, false otherwise
*/
public synchronized boolean hideSite(String url) {
ThreadUtils.assertNotOnUiThread();
if (cachedSites == null) {
refresh();
if (cachedSites == null) {
Log.w(LOGTAG, "Could not load suggested sites!");
return false;
}
}
if (cachedSites.containsKey(url)) {
if (cachedBlacklist == null) {
cachedBlacklist = loadBlacklist();
}
// Check if site has already been blacklisted, just in case.
if (!cachedBlacklist.contains(url)) {
saveToBlacklist(url);
cachedBlacklist.add(url);
return true;
}
}
return false;
}
private void saveToBlacklist(String url) {
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
final String prefString = prefs.getString(PREF_SUGGESTED_SITES_HIDDEN, "");
final String siteString = prefString.concat(" " + Uri.encode(url));
prefs.edit().putString(PREF_SUGGESTED_SITES_HIDDEN, siteString).apply();
}
}

View file

@ -0,0 +1,47 @@
/* -*- 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/. */
package org.mozilla.gecko.db;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
// Tables provide a basic wrapper around ContentProvider methods to make it simpler to add new tables into storage.
// If you create a new Table type, make sure to add it to the sTables list in BrowserProvider to ensure it is queried.
interface Table {
// Provides information to BrowserProvider about the type of URIs this Table can handle.
public static class ContentProviderInfo {
public final int id; // A number of ID for this table. Used by the UriMatcher in BrowserProvider
public final String name; // A name for this table. Will be appended onto uris querying this table
// This is also used to define the mimetype of data returned from this db, i.e.
// BrowserProvider will return "vnd.android.cursor.item/" + name
public ContentProviderInfo(int id, String name) {
if (name == null) {
throw new IllegalArgumentException("Content provider info must specify a name");
}
this.id = id;
this.name = name;
}
}
// Return a list of Info about the ContentProvider URIs this will match
ContentProviderInfo[] getContentProviderInfo();
// Called by BrowserDBHelper whenever the database is created or upgraded.
// Order in which tables are created/upgraded isn't guaranteed (yet), so be careful if your Table depends on something in a
// separate table.
void onCreate(SQLiteDatabase db);
void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
// Called by BrowserProvider when this database queried/modified
// The dbId here should match the dbId's you returned in your getContentProviderInfo() call
Cursor query(SQLiteDatabase db, Uri uri, int dbId, String[] projection, String selection, String[] selectionArgs, String sortOrder, String groupBy, String limit);
int update(SQLiteDatabase db, Uri uri, int dbId, ContentValues values, String selection, String[] selectionArgs);
long insert(SQLiteDatabase db, Uri uri, int dbId, ContentValues values);
int delete(SQLiteDatabase db, Uri uri, int dbId, String selection, String[] selectionArgs);
};

View file

@ -0,0 +1,28 @@
/* 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 android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import org.mozilla.gecko.Tab;
import java.util.List;
public interface TabsAccessor {
public interface OnQueryTabsCompleteListener {
public void onQueryTabsComplete(List<RemoteClient> clients);
}
public Cursor getRemoteClientsByRecencyCursor(Context context);
public Cursor getRemoteTabsCursor(Context context);
public Cursor getRemoteTabsCursor(Context context, int limit);
public List<RemoteClient> getClientsWithoutTabsByRecencyFromCursor(final Cursor cursor);
public List<RemoteClient> getClientsFromCursor(final Cursor cursor);
public void getTabs(final Context context, final OnQueryTabsCompleteListener listener);
public void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener);
public void persistLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs);
}

View file

@ -0,0 +1,361 @@
/* 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.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
public class TabsProvider extends SharedBrowserDatabaseProvider {
private static final long ONE_DAY_IN_MILLISECONDS = 1000 * 60 * 60 * 24;
private static final long ONE_WEEK_IN_MILLISECONDS = 7 * ONE_DAY_IN_MILLISECONDS;
private static final long THREE_WEEKS_IN_MILLISECONDS = 3 * ONE_WEEK_IN_MILLISECONDS;
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 int CLIENTS_RECENCY = 604;
// Exclude clients that are more than three weeks old and also any duplicates that are older than one week old.
static final String EXCLUDE_STALE_CLIENTS_SUBQUERY =
"(SELECT " + Clients.GUID +
", " + Clients.NAME +
", " + Clients.LAST_MODIFIED +
", " + Clients.DEVICE_TYPE +
" FROM " + TABLE_CLIENTS +
" WHERE " + Clients.LAST_MODIFIED + " > %1$s " +
" GROUP BY " + Clients.NAME +
" UNION ALL " +
" SELECT c." + Clients.GUID + " AS " + Clients.GUID +
", c." + Clients.NAME + " AS " + Clients.NAME +
", c." + Clients.LAST_MODIFIED + " AS " + Clients.LAST_MODIFIED +
", c." + Clients.DEVICE_TYPE + " AS " + Clients.DEVICE_TYPE +
" FROM " + TABLE_CLIENTS + " AS c " +
" JOIN (" +
" SELECT " + Clients.GUID +
", " + "MAX( " + Clients.LAST_MODIFIED + ") AS " + Clients.LAST_MODIFIED +
" FROM " + TABLE_CLIENTS +
" WHERE (" + Clients.LAST_MODIFIED + " < %1$s" + " AND " + Clients.LAST_MODIFIED + " > %2$s) AND " +
Clients.NAME + " NOT IN " + "( SELECT " + Clients.NAME + " FROM " + TABLE_CLIENTS + " WHERE " + Clients.LAST_MODIFIED + " > %1$s)" +
" GROUP BY " + Clients.NAME +
") AS c2" +
" ON c." + Clients.GUID + " = c2." + Clients.GUID + ")";
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 DEFAULT_CLIENTS_RECENCY_SORT_ORDER = "COALESCE(MAX(" + Tabs.LAST_USED + "), " + Clients.LAST_MODIFIED + ") DESC";
static final String INDEX_TABS_GUID = "tabs_guid_index";
static final String INDEX_TABS_POSITION = "tabs_position_index";
static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
static final Map<String, String> TABS_PROJECTION_MAP;
static final Map<String, String> CLIENTS_PROJECTION_MAP;
static final Map<String, String> CLIENTS_RECENCY_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);
URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "clients_recency", CLIENTS_RECENCY);
HashMap<String, String> map;
map = new HashMap<String, String>();
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);
map.put(Clients.DEVICE_TYPE, Clients.DEVICE_TYPE);
TABS_PROJECTION_MAP = Collections.unmodifiableMap(map);
map = new HashMap<String, String>();
map.put(Clients.GUID, Clients.GUID);
map.put(Clients.NAME, Clients.NAME);
map.put(Clients.LAST_MODIFIED, Clients.LAST_MODIFIED);
map.put(Clients.DEVICE_TYPE, Clients.DEVICE_TYPE);
CLIENTS_PROJECTION_MAP = Collections.unmodifiableMap(map);
map = new HashMap<>();
map.put(Clients.GUID, projectColumn(TABLE_CLIENTS, Clients.GUID) + " AS guid");
map.put(Clients.NAME, projectColumn(TABLE_CLIENTS, Clients.NAME) + " AS name");
map.put(Clients.LAST_MODIFIED, projectColumn(TABLE_CLIENTS, Clients.LAST_MODIFIED) + " AS last_modified");
map.put(Clients.DEVICE_TYPE, projectColumn(TABLE_CLIENTS, Clients.DEVICE_TYPE) + " AS device_type");
// last_used is the max of the tab last_used times, or if there are no tabs,
// the client's last_modified time.
map.put(Tabs.LAST_USED, "COALESCE(MAX(" + projectColumn(TABLE_TABS, Tabs.LAST_USED) + "), " + projectColumn(TABLE_CLIENTS, Clients.LAST_MODIFIED) + ") AS last_used");
CLIENTS_RECENCY_PROJECTION_MAP = Collections.unmodifiableMap(map);
}
private static final String projectColumn(String table, String column) {
return table + "." + column;
}
private static final String selectColumn(String table, String column) {
return projectColumn(table, column) + " = ?";
}
@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;
}
@Override
@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._ID));
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
// fall through
case CLIENTS:
trace("Delete on CLIENTS: " + uri);
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;
}
@Override
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;
}
@Override
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._ID));
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);
String groupBy = null;
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._ID));
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;
case CLIENTS_RECENCY:
trace("Query is on CLIENTS_RECENCY: " + uri);
if (TextUtils.isEmpty(sortOrder)) {
sortOrder = DEFAULT_CLIENTS_RECENCY_SORT_ORDER;
} else {
debug("Using sort order " + sortOrder + ".");
}
final long oneWeekAgo = System.currentTimeMillis() - ONE_WEEK_IN_MILLISECONDS;
final long threeWeeksAgo = System.currentTimeMillis() - THREE_WEEKS_IN_MILLISECONDS;
final String excludeStaleClientsTable = String.format(EXCLUDE_STALE_CLIENTS_SUBQUERY, oneWeekAgo, threeWeeksAgo);
qb.setProjectionMap(CLIENTS_RECENCY_PROJECTION_MAP);
// Use a subquery to quietly exclude stale duplicate client records.
qb.setTables(excludeStaleClientsTable + " AS " + TABLE_CLIENTS + " LEFT OUTER JOIN " + TABLE_TABS +
" ON (" + projectColumn(TABLE_CLIENTS, Clients.GUID) +
" = " + projectColumn(TABLE_TABS, Tabs.CLIENT_GUID) + ")");
groupBy = projectColumn(TABLE_CLIENTS, Clients.GUID);
break;
default:
throw new UnsupportedOperationException("Unknown query URI " + uri);
}
trace("Running built query.");
final Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, 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);
}
}

View file

@ -0,0 +1,25 @@
/* -*- 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/.
*/
package org.mozilla.gecko.db;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.json.JSONObject;
import org.mozilla.gecko.annotation.RobocopTarget;
import android.content.ContentResolver;
@RobocopTarget
public interface URLMetadata {
public Map<String, Object> fromJSON(JSONObject obj);
public Map<String, Map<String, Object>> getForURLs(final ContentResolver cr,
final Collection<String> urls,
final List<String> columns);
public void save(final ContentResolver cr, final Map<String, Object> data);
}

View file

@ -0,0 +1,92 @@
/* -*- 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/.
*/
package org.mozilla.gecko.db;
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
import org.mozilla.gecko.db.BrowserContract.History;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
// Holds metadata info about urls. Supports some helper functions for getting back a HashMap of key value data.
public class URLMetadataTable extends BaseTable {
private static final String LOGTAG = "GeckoURLMetadataTable";
private static final String TABLE = "metadata"; // Name of the table in the db
private static final int TABLE_ID_NUMBER = BrowserProvider.METADATA;
// Uri for querying this table
public static final Uri CONTENT_URI = Uri.withAppendedPath(BrowserContract.AUTHORITY_URI, "metadata");
// Columns in the table
public static final String ID_COLUMN = "id";
public static final String URL_COLUMN = "url";
public static final String TILE_IMAGE_URL_COLUMN = "tileImage";
public static final String TILE_COLOR_COLUMN = "tileColor";
public static final String TOUCH_ICON_COLUMN = "touchIcon";
URLMetadataTable() { }
@Override
protected String getTable() {
return TABLE;
}
@Override
public void onCreate(SQLiteDatabase db) {
String create = "CREATE TABLE " + TABLE + " (" +
ID_COLUMN + " INTEGER PRIMARY KEY, " +
URL_COLUMN + " TEXT NON NULL UNIQUE, " +
TILE_IMAGE_URL_COLUMN + " STRING, " +
TILE_COLOR_COLUMN + " STRING, " +
TOUCH_ICON_COLUMN + " STRING);";
db.execSQL(create);
}
private void upgradeDatabaseFrom26To27(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE +
" ADD COLUMN " + TOUCH_ICON_COLUMN + " STRING");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// This table was added in v21 of the db. Force its creation if we're coming from an earlier version
if (newVersion >= 21 && oldVersion < 21) {
onCreate(db);
return;
}
// Removed the redundant metadata_url_idx index in version 26
if (newVersion >= 26 && oldVersion < 26) {
db.execSQL("DROP INDEX IF EXISTS metadata_url_idx");
}
if (newVersion >= 27 && oldVersion < 27) {
upgradeDatabaseFrom26To27(db);
}
}
@Override
public Table.ContentProviderInfo[] getContentProviderInfo() {
return new Table.ContentProviderInfo[] {
new Table.ContentProviderInfo(TABLE_ID_NUMBER, TABLE)
};
}
public int deleteUnused(final SQLiteDatabase db) {
final String selection = URL_COLUMN + " NOT IN " +
"(SELECT " + History.URL +
" FROM " + History.TABLE_NAME +
" WHERE " + History.IS_DELETED + " = 0" +
" UNION " +
" SELECT " + Bookmarks.URL +
" FROM " + Bookmarks.TABLE_NAME +
" WHERE " + Bookmarks.IS_DELETED + " = 0 " +
" AND " + Bookmarks.URL + " IS NOT NULL)";
return db.delete(getTable(), selection, null);
}
}

View file

@ -0,0 +1,51 @@
/* 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 android.content.ContentResolver;
import android.database.Cursor;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
public interface UrlAnnotations {
@RobocopTarget void insertAnnotation(ContentResolver cr, String url, String key, String value);
Cursor getScreenshots(ContentResolver cr);
void insertScreenshot(ContentResolver cr, String pageUrl, String screenshotPath);
Cursor getFeedSubscriptions(ContentResolver cr);
Cursor getWebsitesWithFeedUrl(ContentResolver cr);
void deleteFeedUrl(ContentResolver cr, String websiteUrl);
boolean hasWebsiteForFeedUrl(ContentResolver cr, String feedUrl);
void deleteFeedSubscription(ContentResolver cr, FeedSubscription subscription);
void updateFeedSubscription(ContentResolver cr, FeedSubscription subscription);
boolean hasFeedSubscription(ContentResolver cr, String feedUrl);
void insertFeedSubscription(ContentResolver cr, FeedSubscription subscription);
boolean hasFeedUrlForWebsite(ContentResolver cr, String websiteUrl);
void insertFeedUrl(ContentResolver cr, String originUrl, String feedUrl);
void insertReaderViewUrl(ContentResolver cr, String pageURL);
void deleteReaderViewUrl(ContentResolver cr, String pageURL);
/**
* Did the user ever interact with this URL in regards to home screen shortcuts?
*
* @return true if the user has created a home screen shortcut or declined to create one in the
* past. This method will still return true if the shortcut has been removed from the
* home screen by the user.
*/
boolean hasAcceptedOrDeclinedHomeScreenShortcut(ContentResolver cr, String url);
/**
* Insert an indication that the user has interacted with this URL in regards to home screen
* shortcuts.
*
* @param hasCreatedShortCut True if a home screen shortcut has been created for this URL. False
* if the user has actively declined to create a shortcut for this URL.
*/
void insertHomeScreenShortcut(ContentResolver cr, String url, boolean hasCreatedShortCut);
int getAnnotationCount(ContentResolver cr, BrowserContract.UrlAnnotations.Key key);
}