mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-05 23:38:38 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
64
mobile/android/base/java/org/mozilla/gecko/db/BaseTable.java
Normal file
64
mobile/android/base/java/org/mozilla/gecko/db/BaseTable.java
Normal 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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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";
|
||||
}
|
||||
205
mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
Normal file
205
mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
Normal 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
2340
mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
Normal file
2340
mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
Normal file
File diff suppressed because it is too large
Load diff
450
mobile/android/base/java/org/mozilla/gecko/db/DBUtils.java
Normal file
450
mobile/android/base/java/org/mozilla/gecko/db/DBUtils.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
194
mobile/android/base/java/org/mozilla/gecko/db/HomeProvider.java
Normal file
194
mobile/android/base/java/org/mozilla/gecko/db/HomeProvider.java
Normal 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);
|
||||
}
|
||||
}
|
||||
1938
mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
Normal file
1938
mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
90
mobile/android/base/java/org/mozilla/gecko/db/RemoteTab.java
Normal file
90
mobile/android/base/java/org/mozilla/gecko/db/RemoteTab.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
12
mobile/android/base/java/org/mozilla/gecko/db/Searches.java
Normal file
12
mobile/android/base/java/org/mozilla/gecko/db/Searches.java
Normal 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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
47
mobile/android/base/java/org/mozilla/gecko/db/Table.java
Normal file
47
mobile/android/base/java/org/mozilla/gecko/db/Table.java
Normal 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);
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
361
mobile/android/base/java/org/mozilla/gecko/db/TabsProvider.java
Normal file
361
mobile/android/base/java/org/mozilla/gecko/db/TabsProvider.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue