/* 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.sync.repositories.android;
import java.util.ArrayList;
import java.util.List;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserContract.DeletedColumns;
import org.mozilla.gecko.db.BrowserContract.DeletedPasswords;
import org.mozilla.gecko.db.BrowserContract.Passwords;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.repositories.InactiveSessionException;
import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.RecordFilter;
import org.mozilla.gecko.sync.repositories.Repository;
import org.mozilla.gecko.sync.repositories.StoreTrackingRepositorySession;
import org.mozilla.gecko.sync.repositories.android.RepoUtils.QueryHelper;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
import org.mozilla.gecko.sync.repositories.domain.PasswordRecord;
import org.mozilla.gecko.sync.repositories.domain.Record;
import android.content.ContentProviderClient;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
public class PasswordsRepositorySession extends
StoreTrackingRepositorySession {
public static class PasswordsRepository extends Repository {
@Override
public void createSession(RepositorySessionCreationDelegate delegate,
Context context) {
PasswordsRepositorySession session = new PasswordsRepositorySession(PasswordsRepository.this, context);
final RepositorySessionCreationDelegate deferredCreationDelegate = delegate.deferredCreationDelegate();
deferredCreationDelegate.onSessionCreated(session);
}
}
private static final String LOG_TAG = "PasswordsRepoSession";
private static String COLLECTION = "passwords";
private RepoUtils.QueryHelper passwordsHelper;
private RepoUtils.QueryHelper deletedPasswordsHelper;
private ContentProviderClient passwordsProvider;
private final Context context;
public PasswordsRepositorySession(Repository repository, Context context) {
super(repository);
this.context = context;
this.passwordsHelper = new QueryHelper(context, BrowserContractHelpers.PASSWORDS_CONTENT_URI, LOG_TAG);
this.deletedPasswordsHelper = new QueryHelper(context, BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, LOG_TAG);
this.passwordsProvider = context.getContentResolver().acquireContentProviderClient(BrowserContract.PASSWORDS_AUTHORITY_URI);
}
private static final String[] GUID_COLS = new String[] { Passwords.GUID };
private static final String[] DELETED_GUID_COLS = new String[] { DeletedColumns.GUID };
private static final String WHERE_GUID_IS = Passwords.GUID + " = ?";
private static final String WHERE_DELETED_GUID_IS = DeletedPasswords.GUID + " = ?";
@Override
public void guidsSince(final long timestamp, final RepositorySessionGuidsSinceDelegate delegate) {
final Runnable guidsSinceRunnable = new Runnable() {
@Override
public void run() {
if (!isActive()) {
delegate.onGuidsSinceFailed(new InactiveSessionException(null));
return;
}
// Checks succeeded, now get GUIDs.
final List<String> guids = new ArrayList<String>();
try {
Logger.debug(LOG_TAG, "Fetching guidsSince from data table.");
final Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".getGUIDsSince", GUID_COLS, dateModifiedWhere(timestamp), null, null);
try {
if (data.moveToFirst()) {
while (!data.isAfterLast()) {
guids.add(RepoUtils.getStringFromCursor(data, Passwords.GUID));
data.moveToNext();
}
}
} finally {
data.close();
}
// Fetch guids from deleted table.
Logger.debug(LOG_TAG, "Fetching guidsSince from deleted table.");
final Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".getGUIDsSince", DELETED_GUID_COLS, dateModifiedWhereDeleted(timestamp), null, null);
try {
if (deleted.moveToFirst()) {
while (!deleted.isAfterLast()) {
guids.add(RepoUtils.getStringFromCursor(deleted, DeletedColumns.GUID));
deleted.moveToNext();
}
}
} finally {
deleted.close();
}
} catch (Exception e) {
Logger.error(LOG_TAG, "Exception in fetch.");
delegate.onGuidsSinceFailed(e);
return;
}
String[] guidStrings = new String[guids.size()];
delegate.onGuidsSinceSucceeded(guids.toArray(guidStrings));
}
};
delegateQueue.execute(guidsSinceRunnable);
}
@Override
public void fetchSince(final long timestamp, final RepositorySessionFetchRecordsDelegate delegate) {
final RecordFilter filter = this.storeTracker.getFilter();
final Runnable fetchSinceRunnable = new Runnable() {
@Override
public void run() {
if (!isActive()) {
delegate.onFetchFailed(new InactiveSessionException(null), null);
return;
}
final long end = now();
try {
// Fetch from data table.
Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".fetchSince",
getAllColumns(),
dateModifiedWhere(timestamp),
null, null);
if (!fetchAndCloseCursorDeleted(data, false, filter, delegate)) {
return;
}
// Fetch from deleted table.
Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".fetchSince",
getAllDeletedColumns(),
dateModifiedWhereDeleted(timestamp),
null, null);
if (!fetchAndCloseCursorDeleted(deleted, true, filter, delegate)) {
return;
}
// Success!
try {
delegate.onFetchCompleted(end);
} catch (Exception e) {
Logger.error(LOG_TAG, "Delegate fetch completed callback failed.", e);
// Don't call failure callback.
return;
}
} catch (Exception e) {
Logger.error(LOG_TAG, "Exception in fetch.");
delegate.onFetchFailed(e, null);
}
}
};
delegateQueue.execute(fetchSinceRunnable);
}
@Override
public void fetch(final String[] guids, final RepositorySessionFetchRecordsDelegate delegate) {
if (guids == null || guids.length < 1) {
Logger.error(LOG_TAG, "No guids to be fetched.");
final long end = now();
delegateQueue.execute(new Runnable() {
@Override
public void run() {
delegate.onFetchCompleted(end);
}
});
return;
}
// Checks succeeded, now fetch.
final RecordFilter filter = this.storeTracker.getFilter();
final Runnable fetchRunnable = new Runnable() {
@Override
public void run() {
if (!isActive()) {
delegate.onFetchFailed(new InactiveSessionException(null), null);
return;
}
final long end = now();
final String where = RepoUtils.computeSQLInClause(guids.length, "guid");
Logger.trace(LOG_TAG, "Fetch guids where: " + where);
try {
// Fetch records from data table.
Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".fetch",
getAllColumns(),
where, guids, null);
if (!fetchAndCloseCursorDeleted(data, false, filter, delegate)) {
return;
}
// Fetch records from deleted table.
Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".fetch",
getAllDeletedColumns(),
where, guids, null);
if (!fetchAndCloseCursorDeleted(deleted, true, filter, delegate)) {
return;
}
delegate.onFetchCompleted(end);
} catch (Exception e) {
Logger.error(LOG_TAG, "Exception in fetch.");
delegate.onFetchFailed(e, null);
}
}
};
delegateQueue.execute(fetchRunnable);
}
@Override
public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) {
fetchSince(0, delegate);
}
@Override
public void store(final Record record) throws NoStoreDelegateException {
if (delegate == null) {
Logger.error(LOG_TAG, "No store delegate.");
throw new NoStoreDelegateException();
}
if (record == null) {
Logger.error(LOG_TAG, "Record sent to store was null.");
throw new IllegalArgumentException("Null record passed to PasswordsRepositorySession.store().");
}
if (!(record instanceof PasswordRecord)) {
Logger.error(LOG_TAG, "Can't store anything but a PasswordRecord.");
throw new IllegalArgumentException("Non-PasswordRecord passed to PasswordsRepositorySession.store().");
}
final PasswordRecord remoteRecord = (PasswordRecord) record;
final Runnable storeRunnable = new Runnable() {
@Override
public void run() {
if (!isActive()) {
Logger.warn(LOG_TAG, "RepositorySession is inactive. Store failing.");
delegate.onRecordStoreFailed(new InactiveSessionException(null), record.guid);
return;
}
final String guid = remoteRecord.guid;
if (guid == null) {
delegate.onRecordStoreFailed(new RuntimeException("Can't store record with null GUID."), record.guid);
return;
}
PasswordRecord existingRecord;
try {
existingRecord = retrieveByGUID(guid);
} catch (NullCursorException e) {
// Indicates a serious problem.
delegate.onRecordStoreFailed(e, record.guid);
return;
} catch (RemoteException e) {
delegate.onRecordStoreFailed(e, record.guid);
return;
}
long lastLocalRetrieval = 0; // lastSyncTimestamp?
long lastRemoteRetrieval = 0; // TODO: adjust for clock skew.
boolean remotelyModified = remoteRecord.lastModified > lastRemoteRetrieval;
// Check deleted state first.
if (remoteRecord.deleted) {
if (existingRecord == null) {
// Do nothing, record does not exist anyways.
Logger.info(LOG_TAG, "Incoming record " + remoteRecord.guid + " is deleted, and no local version.");
return;
}
if (existingRecord.deleted) {
// Record is already tracked as deleted. Delete from local.
storeRecordDeletion(existingRecord); // different from ABRepoSess.
Logger.info(LOG_TAG, "Incoming record " + remoteRecord.guid + " and local are both deleted.");
return;
}
// Which one wins?
if (!remotelyModified) {
trace("Ignoring deleted record from the past.");
return;
}
boolean locallyModified = existingRecord.lastModified > lastLocalRetrieval;
if (!locallyModified) {
trace("Remote modified, local not. Deleting.");
storeRecordDeletion(remoteRecord);
return;
}
trace("Both local and remote records have been modified.");
if (remoteRecord.lastModified > existingRecord.lastModified) {
trace("Remote is newer, and deleted. Deleting local.");
storeRecordDeletion(remoteRecord);
return;
}
trace("Remote is older, local is not deleted. Ignoring.");
if (!locallyModified) {
Logger.warn(LOG_TAG, "Inconsistency: old remote record is deleted, but local record not modified!");
// Ensure that this is tracked for upload.
}
return;
}
// End deletion logic.
// Now we're processing a non-deleted incoming record.
if (existingRecord == null) {
trace("Looking up match for record " + remoteRecord.guid);
try {
existingRecord = findExistingRecord(remoteRecord);
} catch (RemoteException e) {
Logger.error(LOG_TAG, "Remote exception in findExistingRecord.");
delegate.onRecordStoreFailed(e, record.guid);
} catch (NullCursorException e) {
Logger.error(LOG_TAG, "Null cursor in findExistingRecord.");
delegate.onRecordStoreFailed(e, record.guid);
}
}
if (existingRecord == null) {
// The record is new.
trace("No match. Inserting.");
Logger.debug(LOG_TAG, "Didn't find matching record. Inserting.");
Record inserted = null;
try {
inserted = insert(remoteRecord);
} catch (RemoteException e) {
Logger.debug(LOG_TAG, "Record insert caused a RemoteException.");
delegate.onRecordStoreFailed(e, record.guid);
return;
}
trackRecord(inserted);
delegate.onRecordStoreSucceeded(inserted.guid);
return;
}
// We found a local dupe.
trace("Incoming record " + remoteRecord.guid + " dupes to local record " + existingRecord.guid);
Logger.debug(LOG_TAG, "remote " + remoteRecord.guid + " dupes to " + existingRecord.guid);
Record toStore = reconcileRecords(remoteRecord, existingRecord, lastRemoteRetrieval, lastLocalRetrieval);
if (toStore == null) {
Logger.debug(LOG_TAG, "Reconciling returned null. Not inserting a record.");
return;
}
// TODO: pass in timestamps?
Logger.debug(LOG_TAG, "Replacing " + existingRecord.guid + " with record " + toStore.guid);
Record replaced = null;
try {
replaced = replace(existingRecord, toStore);
} catch (RemoteException e) {
Logger.debug(LOG_TAG, "Record replace caused a RemoteException.");
delegate.onRecordStoreFailed(e, record.guid);
return;
}
// Note that we don't track records here; deciding that is the job
// of reconcileRecords.
Logger.debug(LOG_TAG, "Calling delegate callback with guid " + replaced.guid +
"(" + replaced.androidID + ")");
delegate.onRecordStoreSucceeded(record.guid);
return;
}
};
storeWorkQueue.execute(storeRunnable);
}
@Override
public void wipe(final RepositorySessionWipeDelegate delegate) {
Logger.info(LOG_TAG, "Wiping " + BrowserContractHelpers.PASSWORDS_CONTENT_URI + ", " + BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI);
Runnable wipeRunnable = new Runnable() {
@Override
public void run() {
if (!isActive()) {
delegate.onWipeFailed(new InactiveSessionException(null));
return;
}
// Wipe both data and deleted.
try {
context.getContentResolver().delete(BrowserContractHelpers.PASSWORDS_CONTENT_URI, null, null);
context.getContentResolver().delete(BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, null, null);
} catch (Exception e) {
delegate.onWipeFailed(e);
return;
}
delegate.onWipeSucceeded();
}
};
storeWorkQueue.execute(wipeRunnable);
}
@Override
public void abort() {
passwordsProvider.release();
super.abort();
}
@Override
public void finish(final RepositorySessionFinishDelegate delegate) throws InactiveSessionException {
passwordsProvider.release();
super.finish(delegate);
}
public void deleteGUID(String guid) throws RemoteException {
final String[] args = new String[] { guid };
int deleted = passwordsProvider.delete(BrowserContractHelpers.PASSWORDS_CONTENT_URI, WHERE_GUID_IS, args) +
passwordsProvider.delete(BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, WHERE_DELETED_GUID_IS, args);
if (deleted == 1) {
return;
}
Logger.warn(LOG_TAG, "Unexpectedly deleted " + deleted + " rows for guid " + guid);
}
/**
* Insert record and return the record with its updated androidId set.
*
* @param record the record to insert.
* @return updated record.
* @throws RemoteException
*/
public PasswordRecord insert(PasswordRecord record) throws RemoteException {
record.timePasswordChanged = now();
// TODO: are these necessary for Fennec autocomplete?
// record.timesUsed = 1;
// record.timeLastUsed = now();
ContentValues cv = getContentValues(record);
Uri insertedUri = passwordsProvider.insert(BrowserContractHelpers.PASSWORDS_CONTENT_URI, cv);
if (insertedUri == null) {
throw new RemoteException(); // Not much to be done here, save throw.
}
record.androidID = ContentUris.parseId(insertedUri);
return record;
}
public Record replace(Record origRecord, Record newRecord) throws RemoteException {
PasswordRecord newPasswordRecord = (PasswordRecord) newRecord;
PasswordRecord origPasswordRecord = (PasswordRecord) origRecord;
propagateTimes(newPasswordRecord, origPasswordRecord);
ContentValues cv = getContentValues(newPasswordRecord);
final String[] args = new String[] { origRecord.guid };
int updated = context.getContentResolver().update(BrowserContractHelpers.PASSWORDS_CONTENT_URI, cv, WHERE_GUID_IS, args);
if (updated != 1) {
Logger.warn(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + origPasswordRecord.guid);
}
return newRecord;
}
// When replacing a record, propagate the times.
private static void propagateTimes(PasswordRecord toRecord, PasswordRecord fromRecord) {
toRecord.timePasswordChanged = now();
toRecord.timeCreated = fromRecord.timeCreated;
toRecord.timeLastUsed = fromRecord.timeLastUsed;
toRecord.timesUsed = fromRecord.timesUsed;
}
private static String[] getAllColumns() {
return BrowserContractHelpers.PasswordColumns;
}
private static String[] getAllDeletedColumns() {
return BrowserContractHelpers.DeletedColumns;
}
/**
* Constructs the DB query string for entry age for deleted records.
*
* @param timestamp
* @return String DB query string for dates to fetch.
*/
private static String dateModifiedWhereDeleted(long timestamp) {
return DeletedColumns.TIME_DELETED + " >= " + Long.toString(timestamp);
}
/**
* Constructs the DB query string for entry age for (undeleted) records.
*
* @param timestamp
* @return String DB query string for dates to fetch.
*/
private static String dateModifiedWhere(long timestamp) {
return Passwords.TIME_PASSWORD_CHANGED + " >= " + Long.toString(timestamp);
}
/**
* Fetch from the cursor with the given parameters, invoking
* delegate callbacks and closing the cursor.
* Returns true on success, false if failure was signaled.
*
* @param cursor
fetch* cursor.
* @param deleted
* true if using deleted table, false when using data table.
* @param delegate
* FetchRecordsDelegate to process records.
*/
private static boolean fetchAndCloseCursorDeleted(final Cursor cursor,
final boolean deleted,
final RecordFilter filter,
final RepositorySessionFetchRecordsDelegate delegate) {
if (cursor == null) {
return true;
}
try {
while (cursor.moveToNext()) {
Record r = deleted ? deletedPasswordRecordFromCursor(cursor) : passwordRecordFromCursor(cursor);
if (r != null) {
if (filter == null || !filter.excludeRecord(r)) {
Logger.debug(LOG_TAG, "Processing record " + r.guid);
delegate.onFetchedRecord(r);
} else {
Logger.debug(LOG_TAG, "Skipping filtered record " + r.guid);
}
}
}
} catch (Exception e) {
Logger.error(LOG_TAG, "Exception in fetch.");
delegate.onFetchFailed(e, null);
return false;
} finally {
cursor.close();
}
return true;
}
private PasswordRecord retrieveByGUID(String guid) throws NullCursorException, RemoteException {
final String[] guidArg = new String[] { guid };
// Check data table.
final Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".store", BrowserContractHelpers.PasswordColumns, WHERE_GUID_IS, guidArg, null);
try {
if (data.moveToFirst()) {
return passwordRecordFromCursor(data);
}
} finally {
data.close();
}
// Check deleted table.
final Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".retrieveByGuid", BrowserContractHelpers.DeletedColumns, WHERE_DELETED_GUID_IS, guidArg, null);
try {
if (deleted.moveToFirst()) {
return deletedPasswordRecordFromCursor(deleted);
}
} finally {
deleted.close();
}
return null;
}
private static final String WHERE_RECORD_DATA =
Passwords.HOSTNAME + " = ? AND " +
Passwords.HTTP_REALM + " = ? AND " +
Passwords.FORM_SUBMIT_URL + " = ? AND " +
Passwords.USERNAME_FIELD + " = ? AND " +
Passwords.PASSWORD_FIELD + " = ?";
private PasswordRecord findExistingRecord(PasswordRecord record) throws NullCursorException, RemoteException {
PasswordRecord foundRecord = null;
Cursor cursor = null;
// Only check the data table.
// We can't encrypt username directly for query, so run a more general query and then filter.
final String[] whereArgs = new String[] {
record.hostname,
record.httpRealm,
record.formSubmitURL,
record.usernameField,
record.passwordField
};
try {
cursor = passwordsHelper.safeQuery(passwordsProvider, ".findRecord", getAllColumns(), WHERE_RECORD_DATA, whereArgs, null);
while (cursor.moveToNext()) {
foundRecord = passwordRecordFromCursor(cursor);
// We don't directly query for username because the
// username/password values are encrypted in the db.
// We don't have the keys for encrypting our query,
// so we run a more general query and then filter
// the returned records for a matching username.
Logger.pii(LOG_TAG, "Checking incoming [" + record.encryptedUsername + "] to [" + foundRecord.encryptedUsername + "]");
if (record.encryptedUsername.equals(foundRecord.encryptedUsername)) {
Logger.trace(LOG_TAG, "Found matching record: " + foundRecord.guid);
return foundRecord;
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
Logger.debug(LOG_TAG, "No matching records, returning null.");
return null;
}
private void storeRecordDeletion(Record record) {
try {
deleteGUID(record.guid);
} catch (RemoteException e) {
Logger.error(LOG_TAG, "RemoteException in password delete.");
delegate.onRecordStoreFailed(e, record.guid);
return;
}
delegate.onRecordStoreSucceeded(record.guid);
}
/**
* Make a PasswordRecord from a Cursor.
* @param cur
* Cursor from query.
* @param deleted
* true if creating a deleted Record, false if otherwise.
* @return
* PasswordRecord populated from Cursor.
*/
private static PasswordRecord passwordRecordFromCursor(Cursor cur) {
if (cur.isAfterLast()) {
return null;
}
String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.GUID);
long lastModified = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_PASSWORD_CHANGED);
PasswordRecord rec = new PasswordRecord(guid, COLLECTION, lastModified, false);
rec.id = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ID);
rec.hostname = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.HOSTNAME);
rec.httpRealm = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.HTTP_REALM);
rec.formSubmitURL = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.FORM_SUBMIT_URL);
rec.usernameField = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.USERNAME_FIELD);
rec.passwordField = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.PASSWORD_FIELD);
rec.encType = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ENC_TYPE);
// TODO decryption of username/password here (Bug 711636)
rec.encryptedUsername = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ENCRYPTED_USERNAME);
rec.encryptedPassword = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ENCRYPTED_PASSWORD);
rec.timeCreated = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_CREATED);
rec.timeLastUsed = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_LAST_USED);
rec.timePasswordChanged = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_PASSWORD_CHANGED);
rec.timesUsed = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIMES_USED);
return rec;
}
private static PasswordRecord deletedPasswordRecordFromCursor(Cursor cur) {
if (cur.isAfterLast()) {
return null;
}
String guid = RepoUtils.getStringFromCursor(cur, DeletedColumns.GUID);
long lastModified = RepoUtils.getLongFromCursor(cur, DeletedColumns.TIME_DELETED);
PasswordRecord rec = new PasswordRecord(guid, COLLECTION, lastModified, true);
rec.androidID = RepoUtils.getLongFromCursor(cur, DeletedColumns.ID);
return rec;
}
private static ContentValues getContentValues(Record record) {
PasswordRecord rec = (PasswordRecord) record;
ContentValues cv = new ContentValues();
cv.put(BrowserContract.Passwords.GUID, rec.guid);
cv.put(BrowserContract.Passwords.HOSTNAME, rec.hostname);
cv.put(BrowserContract.Passwords.HTTP_REALM, rec.httpRealm);
cv.put(BrowserContract.Passwords.FORM_SUBMIT_URL, rec.formSubmitURL);
cv.put(BrowserContract.Passwords.USERNAME_FIELD, rec.usernameField);
cv.put(BrowserContract.Passwords.PASSWORD_FIELD, rec.passwordField);
// TODO Do encryption of username/password here. Bug 711636
cv.put(BrowserContract.Passwords.ENC_TYPE, rec.encType);
cv.put(BrowserContract.Passwords.ENCRYPTED_USERNAME, rec.encryptedUsername);
cv.put(BrowserContract.Passwords.ENCRYPTED_PASSWORD, rec.encryptedPassword);
cv.put(BrowserContract.Passwords.TIME_CREATED, rec.timeCreated);
cv.put(BrowserContract.Passwords.TIME_LAST_USED, rec.timeLastUsed);
cv.put(BrowserContract.Passwords.TIME_PASSWORD_CHANGED, rec.timePasswordChanged);
cv.put(BrowserContract.Passwords.TIMES_USED, rec.timesUsed);
return cv;
}
}