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:
vivek 2016-02-15 16:14:31 -08:00
Родитель bee345ba8a
Коммит a7642a13bb
9 изменённых файлов: 1046 добавлений и 1 удалений

Просмотреть файл

@ -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;
}
}