зеркало из https://github.com/mozilla/gecko-dev.git
Bug 946857 - Part 2: Add Android LoginsProvider. r=nalexander
LoginsProvider is an all-Android implementation of PasswordsProvider. PasswordsProvider is an SQLite database backed by the version of SQLite that ships with Gecko. It is concurrently accessed from Gecko and it runs with a special lifecycle that includes a separate heavy-weight process. Eventually we'll migrate the Gecko-side passwords interface to use the new Android-side LoginsProvider, but for now we just want to get the new provider landed and the tests running. MozReview-Commit-ID: Bx19D68tMtI *** Bug 946857 - Fold into part2: review nits. MozReview-Commit-ID: LmPwIvebfrr --HG-- extra : rebase_source : 0caccd0773f3e2feb80d72fb4b52ac086c25f7d2
This commit is contained in:
Родитель
bee345ba8a
Коммит
a7642a13bb
|
@ -293,6 +293,11 @@
|
|||
android:exported="false"
|
||||
android:process="@MANGLED_ANDROID_PACKAGE_NAME@.PasswordsProvider"/>
|
||||
|
||||
<provider android:name="org.mozilla.gecko.db.LoginsProvider"
|
||||
android:label="@string/sync_configure_engines_title_passwords"
|
||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.logins"
|
||||
android:exported="false"/>
|
||||
|
||||
<provider android:name="org.mozilla.gecko.db.FormHistoryProvider"
|
||||
android:label="@string/sync_configure_engines_title_history"
|
||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.formhistory"
|
||||
|
|
|
@ -36,6 +36,9 @@ public class BrowserContract {
|
|||
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";
|
||||
|
@ -535,6 +538,55 @@ public class BrowserContract {
|
|||
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";
|
||||
}
|
||||
|
||||
// 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";
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ public final class BrowserDatabaseHelper extends SQLiteOpenHelper {
|
|||
|
||||
// Replace the Bug number below with your Bug that is conducting a DB upgrade, as to force a merge conflict with any
|
||||
// other patches that require a DB upgrade.
|
||||
public static final int DATABASE_VERSION = 29; // Bug 760956
|
||||
public static final int DATABASE_VERSION = 30; // Bug 946857
|
||||
public static final String DATABASE_NAME = "browser.db";
|
||||
|
||||
final protected Context mContext;
|
||||
|
@ -56,6 +56,9 @@ public final class BrowserDatabaseHelper extends SQLiteOpenHelper {
|
|||
static final String TABLE_READING_LIST = ReadingListItems.TABLE_NAME;
|
||||
static final String TABLE_TABS = TabsProvider.TABLE_TABS;
|
||||
static final String TABLE_CLIENTS = TabsProvider.TABLE_CLIENTS;
|
||||
static final String TABLE_LOGINS = BrowserContract.Logins.TABLE_LOGINS;
|
||||
static final String TABLE_DELETED_LOGINS = BrowserContract.DeletedLogins.TABLE_DELETED_LOGINS;
|
||||
static final String TABLE_DISABLED_HOSTS = BrowserContract.LoginsDisabledHosts.TABLE_DISABLED_HOSTS;
|
||||
|
||||
static final String VIEW_COMBINED = Combined.VIEW_NAME;
|
||||
static final String VIEW_BOOKMARKS_WITH_FAVICONS = Bookmarks.VIEW_WITH_FAVICONS;
|
||||
|
@ -329,6 +332,62 @@ public final class BrowserDatabaseHelper extends SQLiteOpenHelper {
|
|||
|
||||
}
|
||||
|
||||
private void createLoginsTable(SQLiteDatabase db, final String tableName) {
|
||||
debug("Creating logins.db: " + db.getPath());
|
||||
debug("Creating " + tableName + " table");
|
||||
|
||||
// Table for each login.
|
||||
db.execSQL("CREATE TABLE " + tableName + "(" +
|
||||
BrowserContract.Logins._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
|
||||
BrowserContract.Logins.HOSTNAME + " TEXT NOT NULL," +
|
||||
BrowserContract.Logins.HTTP_REALM + " TEXT," +
|
||||
BrowserContract.Logins.FORM_SUBMIT_URL + " TEXT," +
|
||||
BrowserContract.Logins.USERNAME_FIELD + " TEXT NOT NULL," +
|
||||
BrowserContract.Logins.PASSWORD_FIELD + " TEXT NOT NULL," +
|
||||
BrowserContract.Logins.ENCRYPTED_USERNAME + " TEXT NOT NULL," +
|
||||
BrowserContract.Logins.ENCRYPTED_PASSWORD + " TEXT NOT NULL," +
|
||||
BrowserContract.Logins.GUID + " TEXT UNIQUE NOT NULL," +
|
||||
BrowserContract.Logins.ENC_TYPE + " INTEGER NOT NULL, " +
|
||||
BrowserContract.Logins.TIME_CREATED + " INTEGER," +
|
||||
BrowserContract.Logins.TIME_LAST_USED + " INTEGER," +
|
||||
BrowserContract.Logins.TIME_PASSWORD_CHANGED + " INTEGER," +
|
||||
BrowserContract.Logins.TIMES_USED + " INTEGER" +
|
||||
");");
|
||||
}
|
||||
|
||||
private void createLoginsTableIndices(SQLiteDatabase db, final String tableName) {
|
||||
// No need to create an index on GUID, it is an unique column.
|
||||
db.execSQL("CREATE INDEX " + LoginsProvider.INDEX_LOGINS_HOSTNAME +
|
||||
" ON " + tableName + "(" + BrowserContract.Logins.HOSTNAME + ")");
|
||||
db.execSQL("CREATE INDEX " + LoginsProvider.INDEX_LOGINS_HOSTNAME_FORM_SUBMIT_URL +
|
||||
" ON " + tableName + "(" + BrowserContract.Logins.HOSTNAME + "," + BrowserContract.Logins.FORM_SUBMIT_URL + ")");
|
||||
db.execSQL("CREATE INDEX " + LoginsProvider.INDEX_LOGINS_HOSTNAME_HTTP_REALM +
|
||||
" ON " + tableName + "(" + BrowserContract.Logins.HOSTNAME + "," + BrowserContract.Logins.HTTP_REALM + ")");
|
||||
}
|
||||
|
||||
private void createDeletedLoginsTable(SQLiteDatabase db, final String tableName) {
|
||||
debug("Creating deleted_logins.db: " + db.getPath());
|
||||
debug("Creating " + tableName + " table");
|
||||
|
||||
// Table for each deleted login.
|
||||
db.execSQL("CREATE TABLE " + tableName + "(" +
|
||||
BrowserContract.DeletedLogins._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
|
||||
BrowserContract.DeletedLogins.GUID + " TEXT UNIQUE NOT NULL," +
|
||||
BrowserContract.DeletedLogins.TIME_DELETED + " INTEGER NOT NULL" +
|
||||
");");
|
||||
}
|
||||
|
||||
private void createDisabledHostsTable(SQLiteDatabase db, final String tableName) {
|
||||
debug("Creating disabled_hosts.db: " + db.getPath());
|
||||
debug("Creating " + tableName + " table");
|
||||
|
||||
// Table for each disabled host.
|
||||
db.execSQL("CREATE TABLE " + tableName + "(" +
|
||||
BrowserContract.LoginsDisabledHosts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
|
||||
BrowserContract.LoginsDisabledHosts.HOSTNAME + " TEXT UNIQUE NOT NULL ON CONFLICT REPLACE" +
|
||||
");");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
debug("Creating browser.db: " + db.getPath());
|
||||
|
@ -360,6 +419,11 @@ public final class BrowserDatabaseHelper extends SQLiteOpenHelper {
|
|||
createReadingListIndices(db, TABLE_READING_LIST);
|
||||
createUrlAnnotationsTable(db);
|
||||
createNumbersTable(db);
|
||||
|
||||
createDeletedLoginsTable(db, TABLE_DELETED_LOGINS);
|
||||
createDisabledHostsTable(db, TABLE_DISABLED_HOSTS);
|
||||
createLoginsTable(db, TABLE_LOGINS);
|
||||
createLoginsTableIndices(db, TABLE_LOGINS);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1066,6 +1130,14 @@ public final class BrowserDatabaseHelper extends SQLiteOpenHelper {
|
|||
createNumbersTable(db);
|
||||
}
|
||||
|
||||
private void upgradeDatabaseFrom29to30(final SQLiteDatabase db) {
|
||||
debug("creating logins table");
|
||||
createDeletedLoginsTable(db, TABLE_DELETED_LOGINS);
|
||||
createDisabledHostsTable(db, TABLE_DISABLED_HOSTS);
|
||||
createLoginsTable(db, TABLE_LOGINS);
|
||||
createLoginsTableIndices(db, TABLE_LOGINS);
|
||||
}
|
||||
|
||||
private void createV19CombinedView(SQLiteDatabase db) {
|
||||
db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED);
|
||||
db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS);
|
||||
|
@ -1158,6 +1230,11 @@ public final class BrowserDatabaseHelper extends SQLiteOpenHelper {
|
|||
|
||||
case 29:
|
||||
upgradeDatabaseFrom28to29(db);
|
||||
break;
|
||||
|
||||
case 30:
|
||||
upgradeDatabaseFrom29to30(db);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -227,6 +227,7 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
|
|||
'db/LocalTabsAccessor.java',
|
||||
'db/LocalUrlAnnotations.java',
|
||||
'db/LocalURLMetadata.java',
|
||||
'db/LoginsProvider.java',
|
||||
'db/PasswordsProvider.java',
|
||||
'db/PerProfileDatabaseProvider.java',
|
||||
'db/PerProfileDatabases.java',
|
||||
|
|
|
@ -40,6 +40,7 @@ public class BrowserContractHelpers extends BrowserContract {
|
|||
public static final Uri DELETED_FORM_HISTORY_CONTENT_URI = withSyncAndProfile(DeletedFormHistory.CONTENT_URI);
|
||||
public static final Uri TABS_CONTENT_URI = withSyncAndProfile(Tabs.CONTENT_URI);
|
||||
public static final Uri CLIENTS_CONTENT_URI = withSyncAndProfile(Clients.CONTENT_URI);
|
||||
public static final Uri LOGINS_CONTENT_URI = withSyncAndProfile(Logins.CONTENT_URI);
|
||||
|
||||
public static final String[] PasswordColumns = new String[] {
|
||||
Passwords.ID,
|
||||
|
|
Двоичный файл не отображается.
|
@ -142,3 +142,5 @@ skip-if = android_version == "10"
|
|||
# testStumblerSetting disabled on Android 4.3, bug 1145846
|
||||
[src/org/mozilla/gecko/tests/testStumblerSetting.java]
|
||||
skip-if = android_version == "10" || android_version == "18"
|
||||
|
||||
[src/org/mozilla/gecko/tests/testLoginsProvider.java]
|
||||
|
|
|
@ -0,0 +1,387 @@
|
|||
/* 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.tests;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.mozilla.gecko.db.BrowserContract;
|
||||
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.db.LoginsProvider;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import static org.mozilla.gecko.db.BrowserContract.CommonColumns._ID;
|
||||
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 testLoginsProvider extends ContentProviderTest {
|
||||
|
||||
private static final String DB_NAME = "browser.db";
|
||||
|
||||
private final TestCase[] TESTS_TO_RUN = {
|
||||
new InsertLoginsTest(),
|
||||
new UpdateLoginsTest(),
|
||||
new DeleteLoginsTest(),
|
||||
new InsertDeletedLoginsTest(),
|
||||
new InsertDeletedLoginsFailureTest(),
|
||||
new DisabledHostsInsertTest(),
|
||||
new DisabledHostsInsertFailureTest(),
|
||||
new InsertLoginsWithDefaultValuesTest(),
|
||||
new InsertLoginsWithDuplicateGuidFailureTest(),
|
||||
new DeleteLoginsByNonExistentGuidTest(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Factory function that makes new LoginsProvider instances.
|
||||
* <p>
|
||||
* We want a fresh provider each test, so this should be invoked in
|
||||
* <code>setUp</code> before each individual test.
|
||||
*/
|
||||
private static final Callable<ContentProvider> sProviderFactory = new Callable<ContentProvider>() {
|
||||
@Override
|
||||
public ContentProvider call() {
|
||||
return new LoginsProvider();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp(sProviderFactory, BrowserContract.LOGINS_AUTHORITY, DB_NAME);
|
||||
for (TestCase test: TESTS_TO_RUN) {
|
||||
mTests.add(test);
|
||||
}
|
||||
}
|
||||
|
||||
public void testLoginProviderTests() throws Exception {
|
||||
for (Runnable test : mTests) {
|
||||
final String testName = test.getClass().getSimpleName();
|
||||
setTestName(testName);
|
||||
ensureEmptyDatabase();
|
||||
mAsserter.dumpLog("testLoginsProvider: Database empty - Starting " + testName + ".");
|
||||
test.run();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipe DB.
|
||||
*/
|
||||
private void ensureEmptyDatabase() {
|
||||
getWritableDatabase(Logins.CONTENT_URI).delete(TABLE_LOGINS, null, null);
|
||||
getWritableDatabase(DeletedLogins.CONTENT_URI).delete(TABLE_DELETED_LOGINS, null, null);
|
||||
getWritableDatabase(LoginsDisabledHosts.CONTENT_URI).delete(TABLE_DISABLED_HOSTS, null, null);
|
||||
}
|
||||
|
||||
private SQLiteDatabase getWritableDatabase(Uri uri) {
|
||||
Uri testUri = appendUriParam(uri, BrowserContract.PARAM_IS_TEST, "1");
|
||||
DelegatingTestContentProvider delegateProvider = (DelegatingTestContentProvider) mProvider;
|
||||
LoginsProvider loginsProvider = (LoginsProvider) delegateProvider.getTargetProvider();
|
||||
return loginsProvider.getWritableDatabaseForTesting(testUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* LoginsProvider insert logins test.
|
||||
*/
|
||||
private class InsertLoginsTest extends TestCase {
|
||||
@Override
|
||||
public void test() throws Exception {
|
||||
ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
|
||||
"http://www.example.com", "username1", "password1", "username1", "password1", "guid1");
|
||||
long id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
|
||||
verifyLoginExists(contentValues, id);
|
||||
Cursor cursor = mProvider.query(Logins.CONTENT_URI, null, Logins.GUID + " = ?", new String[] { "guid1" }, null);
|
||||
verifyRowMatches(contentValues, cursor, "logins found");
|
||||
|
||||
// Empty ("") encrypted username and password are valid.
|
||||
contentValues = createLogin("http://www.example.com", "http://www.example.com",
|
||||
"http://www.example.com", "username1", "password1", "", "", "guid2");
|
||||
id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
|
||||
verifyLoginExists(contentValues, id);
|
||||
cursor = mProvider.query(Logins.CONTENT_URI, null, Logins.GUID + " = ?", new String[] { "guid2" }, null);
|
||||
verifyRowMatches(contentValues, cursor, "logins found");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LoginsProvider updates logins test.
|
||||
*/
|
||||
private class UpdateLoginsTest extends TestCase {
|
||||
@Override
|
||||
public void test() throws Exception {
|
||||
final String guid1 = "guid1";
|
||||
ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
|
||||
"http://www.example.com", "username1", "password1", "username1", "password1", guid1);
|
||||
long timeBeforeCreated = System.currentTimeMillis();
|
||||
long id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
|
||||
long timeAfterCreated = System.currentTimeMillis();
|
||||
verifyLoginExists(contentValues, id);
|
||||
|
||||
Cursor cursor = getLoginById(id);
|
||||
try {
|
||||
mAsserter.ok(cursor.moveToFirst(), "cursor is not empty", "");
|
||||
verifyBounded(timeBeforeCreated, cursor.getLong(cursor.getColumnIndexOrThrow(Logins.TIME_CREATED)), timeAfterCreated);
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
contentValues.put(BrowserContract.Logins.ENCRYPTED_USERNAME, "username2");
|
||||
contentValues.put(Logins.ENCRYPTED_PASSWORD, "password2");
|
||||
|
||||
Uri updateUri = Logins.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build();
|
||||
int numUpdated = mProvider.update(updateUri, contentValues, null, null);
|
||||
mAsserter.is(1, numUpdated, "Correct number updated");
|
||||
verifyLoginExists(contentValues, id);
|
||||
|
||||
contentValues.put(BrowserContract.Logins.ENCRYPTED_USERNAME, "username1");
|
||||
contentValues.put(Logins.ENCRYPTED_PASSWORD, "password1");
|
||||
|
||||
updateUri = Logins.CONTENT_URI;
|
||||
numUpdated = mProvider.update(updateUri, contentValues, Logins.GUID + " = ?", new String[]{guid1});
|
||||
mAsserter.is(1, numUpdated, "Correct number updated");
|
||||
verifyLoginExists(contentValues, id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LoginsProvider deletion logins test.
|
||||
* - inserts a new logins
|
||||
* - deletes the logins and verify deleted-logins table has entry for deleted guid.
|
||||
*/
|
||||
private class DeleteLoginsTest extends TestCase {
|
||||
@Override
|
||||
public void test() throws Exception {
|
||||
final String guid1 = "guid1";
|
||||
ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
|
||||
"http://www.example.com", "username1", "password1", "username1", "password1", guid1);
|
||||
long id = ContentUris.parseId(mProvider.insert(Logins.CONTENT_URI, contentValues));
|
||||
verifyLoginExists(contentValues, id);
|
||||
|
||||
Uri deletedUri = Logins.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build();
|
||||
int numDeleted = mProvider.delete(deletedUri, null, null);
|
||||
mAsserter.is(1, numDeleted, "Correct number deleted");
|
||||
verifyNoRowExists(Logins.CONTENT_URI, "No login entry found");
|
||||
|
||||
contentValues = new ContentValues();
|
||||
contentValues.put(DeletedLogins.GUID, guid1);
|
||||
Cursor cursor = mProvider.query(DeletedLogins.CONTENT_URI, null, null, null, null);
|
||||
verifyRowMatches(contentValues, cursor, "deleted-login found");
|
||||
cursor = mProvider.query(DeletedLogins.CONTENT_URI, null, DeletedLogins.GUID + " = ?", new String[] { guid1 }, null);
|
||||
verifyRowMatches(contentValues, cursor, "deleted-login found");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LoginsProvider re-insert logins test.
|
||||
* - inserts a row into deleted-logins
|
||||
* - insert the same login (matching guid) and verify deleted-logins table is empty.
|
||||
*/
|
||||
private class InsertDeletedLoginsTest extends TestCase {
|
||||
@Override
|
||||
public void test() throws Exception {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(DeletedLogins.GUID, "guid1");
|
||||
long id = ContentUris.parseId(mProvider.insert(DeletedLogins.CONTENT_URI, contentValues));
|
||||
final Uri insertedUri = DeletedLogins.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build();
|
||||
Cursor cursor = mProvider.query(insertedUri, null, null, null, null);
|
||||
verifyRowMatches(contentValues, cursor, "deleted-login found");
|
||||
verifyNoRowExists(BrowserContract.Logins.CONTENT_URI, "No login entry found");
|
||||
|
||||
contentValues = createLogin("http://www.example.com", "http://www.example.com",
|
||||
"http://www.example.com", "username1", "password1", "username1", "password1", "guid1");
|
||||
id = ContentUris.parseId(mProvider.insert(Logins.CONTENT_URI, contentValues));
|
||||
verifyLoginExists(contentValues, id);
|
||||
verifyNoRowExists(DeletedLogins.CONTENT_URI, "No deleted-login entry found");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LoginsProvider insert Deleted logins test.
|
||||
* - inserts a row into deleted-login without GUID.
|
||||
*/
|
||||
private class InsertDeletedLoginsFailureTest extends TestCase {
|
||||
@Override
|
||||
public void test() throws Exception {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
try {
|
||||
mProvider.insert(DeletedLogins.CONTENT_URI, contentValues);
|
||||
fail("Failed to throw IllegalArgumentException while missing GUID");
|
||||
} catch (Exception e) {
|
||||
mAsserter.is(e.getClass(), IllegalArgumentException.class, "IllegalArgumentException thrown for invalid GUID");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LoginsProvider disabled host test.
|
||||
* - inserts a disabled-host
|
||||
* - delete the inserted disabled-host and verify disabled-hosts table is empty.
|
||||
*/
|
||||
private class DisabledHostsInsertTest extends TestCase {
|
||||
@Override
|
||||
public void test() throws Exception {
|
||||
final String hostname = "localhost";
|
||||
final ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(LoginsDisabledHosts.HOSTNAME, hostname);
|
||||
mProvider.insert(LoginsDisabledHosts.CONTENT_URI, contentValues);
|
||||
final Uri insertedUri = LoginsDisabledHosts.CONTENT_URI.buildUpon().appendPath("hostname").appendPath(hostname).build();
|
||||
final Cursor cursor = mProvider.query(insertedUri, null, null, null, null);
|
||||
verifyRowMatches(contentValues, cursor, "disabled-hosts found");
|
||||
|
||||
final Uri deletedUri = LoginsDisabledHosts.CONTENT_URI.buildUpon().appendPath("hostname").appendPath(hostname).build();
|
||||
final int numDeleted = mProvider.delete(deletedUri, null, null);
|
||||
mAsserter.is(1, numDeleted, "Correct number deleted");
|
||||
verifyNoRowExists(LoginsDisabledHosts.CONTENT_URI, "No disabled-hosts entry found");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LoginsProvider disabled host insert failure testcase.
|
||||
* - inserts a disabled-host without providing hostname
|
||||
*/
|
||||
private class DisabledHostsInsertFailureTest extends TestCase {
|
||||
@Override
|
||||
public void test() throws Exception {
|
||||
final String hostname = "localhost";
|
||||
final ContentValues contentValues = new ContentValues();
|
||||
try {
|
||||
mProvider.insert(LoginsDisabledHosts.CONTENT_URI, contentValues);
|
||||
fail("Failed to throw IllegalArgumentException while missing hostname");
|
||||
} catch (Exception e) {
|
||||
mAsserter.is(e.getClass(), IllegalArgumentException.class, "IllegalArgumentException thrown for invalid hostname");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LoginsProvider login insertion with default values test.
|
||||
* - insert a login missing GUID, FORM_SUBMIT_URL, HTTP_REALM and verify default values are set.
|
||||
*/
|
||||
private class InsertLoginsWithDefaultValuesTest extends TestCase {
|
||||
@Override
|
||||
protected void test() throws Exception {
|
||||
ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
|
||||
"http://www.example.com", "username1", "password1", "username1", "password1", null);
|
||||
// Remove GUID, HTTP_REALM, FORM_SUBMIT_URL from content values
|
||||
contentValues.remove(Logins.GUID);
|
||||
contentValues.remove(Logins.FORM_SUBMIT_URL);
|
||||
contentValues.remove(Logins.HTTP_REALM);
|
||||
|
||||
long id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
|
||||
Cursor cursor = getLoginById(id);
|
||||
assertNotNull(cursor);
|
||||
cursor.moveToFirst();
|
||||
|
||||
mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.GUID)), null, "GUID is not null");
|
||||
mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.HTTP_REALM)), null, "HTTP_REALM is not null");
|
||||
mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.FORM_SUBMIT_URL)), null, "FORM_SUBMIT_URL is not null");
|
||||
mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.TIME_LAST_USED)), null, "TIME_LAST_USED is not null");
|
||||
mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.TIME_CREATED)), null, "TIME_CREATED is not null");
|
||||
mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.TIME_PASSWORD_CHANGED)), null, "TIME_PASSWORD_CHANGED is not null");
|
||||
mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.ENC_TYPE)), "0", "ENC_TYPE is 0");
|
||||
mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.TIMES_USED)), "0", "TIMES_USED is 0");
|
||||
|
||||
// Verify other values.
|
||||
verifyRowMatches(contentValues, cursor, "Updated login found");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LoginsProvider login insertion with duplicate GUID test.
|
||||
* - insert two different logins with same GUID and verify that only one login exists.
|
||||
*/
|
||||
private class InsertLoginsWithDuplicateGuidFailureTest extends TestCase {
|
||||
@Override
|
||||
protected void test() throws Exception {
|
||||
final String guid = "guid1";
|
||||
ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
|
||||
"http://www.example.com", "username1", "password1", "username1", "password1", guid);
|
||||
long id1 = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
|
||||
verifyLoginExists(contentValues, id1);
|
||||
|
||||
// Insert another login with duplicate GUID.
|
||||
contentValues = createLogin("http://www.example2.com", "http://www.example2.com",
|
||||
"http://www.example2.com", "username2", "password2", "username2", "password2", guid);
|
||||
Uri insertUri = mProvider.insert(Logins.CONTENT_URI, contentValues);
|
||||
mAsserter.is(insertUri, null, "Duplicate Guid insertion id1");
|
||||
|
||||
// Verify login with id1 still exists.
|
||||
verifyLoginExists(contentValues, id1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LoginsProvider deletion by non-existent GUID test.
|
||||
* - delete a login with random GUID and verify that no entry was deleted.
|
||||
*/
|
||||
private class DeleteLoginsByNonExistentGuidTest extends TestCase {
|
||||
@Override
|
||||
protected void test() throws Exception {
|
||||
Uri deletedUri = Logins.CONTENT_URI;
|
||||
int numDeleted = mProvider.delete(deletedUri, Logins.GUID + "= ?", new String[] { "guid1" });
|
||||
mAsserter.is(0, numDeleted, "Correct number deleted");
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyBounded(long left, long middle, long right) {
|
||||
mAsserter.ok(left <= middle, "Left <= middle", left + " <= " + middle);
|
||||
mAsserter.ok(middle <= right, "Middle <= right", middle + " <= " + right);
|
||||
}
|
||||
|
||||
private Cursor getById(Uri uri, long id, String[] projection) {
|
||||
return mProvider.query(uri, projection,
|
||||
_ID + " = ?",
|
||||
new String[] { String.valueOf(id) },
|
||||
null);
|
||||
}
|
||||
|
||||
private Cursor getLoginById(long id) {
|
||||
return getById(Logins.CONTENT_URI, id, null);
|
||||
}
|
||||
|
||||
private void verifyLoginExists(ContentValues contentValues, long id) {
|
||||
Cursor cursor = getLoginById(id);
|
||||
verifyRowMatches(contentValues, cursor, "Updated login found");
|
||||
}
|
||||
|
||||
private void verifyRowMatches(ContentValues contentValues, Cursor cursor, String name) {
|
||||
try {
|
||||
mAsserter.ok(cursor.moveToFirst(), name, "cursor is not empty");
|
||||
CursorMatches(cursor, contentValues);
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyNoRowExists(Uri contentUri, String name) {
|
||||
Cursor cursor = mProvider.query(contentUri, null, null, null, null);
|
||||
try {
|
||||
mAsserter.is(0, cursor.getCount(), name);
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
private ContentValues createLogin(String hostname, String httpRealm, String formSubmitUrl,
|
||||
String usernameField, String passwordField, String encryptedUsername,
|
||||
String encryptedPassword, String guid) {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(Logins.HOSTNAME, hostname);
|
||||
values.put(Logins.HTTP_REALM, httpRealm);
|
||||
values.put(Logins.FORM_SUBMIT_URL, formSubmitUrl);
|
||||
values.put(Logins.USERNAME_FIELD, usernameField);
|
||||
values.put(Logins.PASSWORD_FIELD, passwordField);
|
||||
values.put(Logins.ENCRYPTED_USERNAME, encryptedUsername);
|
||||
values.put(Logins.ENCRYPTED_PASSWORD, encryptedPassword);
|
||||
values.put(Logins.GUID, guid);
|
||||
return values;
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче