/* 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.List;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.domain.Record;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
public abstract class AndroidBrowserRepositoryDataAccessor {
private static final String[] GUID_COLUMNS = new String[] { BrowserContract.SyncColumns.GUID };
protected Context context;
protected static String LOG_TAG = "BrowserDataAccessor";
protected final RepoUtils.QueryHelper queryHelper;
public AndroidBrowserRepositoryDataAccessor(Context context) {
this.context = context;
this.queryHelper = new RepoUtils.QueryHelper(context, getUri(), LOG_TAG);
}
protected abstract String[] getAllColumns();
/**
* Produce a <code>ContentValues</code> instance that represents the provided <code>Record</code>.
*
* @param record The <code>Record</code> to be converted.
* @return The <code>ContentValues</code> corresponding to <code>record</code>.
*/
protected abstract ContentValues getContentValues(Record record);
protected abstract Uri getUri();
/**
* Dump all the records in raw format.
*/
public void dumpDB() {
Cursor cur = null;
try {
cur = queryHelper.safeQuery(".dumpDB", null, null, null, null);
RepoUtils.dumpCursor(cur);
} catch (NullCursorException e) {
} finally {
if (cur != null) {
cur.close();
}
}
}
public String dateModifiedWhere(long timestamp) {
return BrowserContract.SyncColumns.DATE_MODIFIED + " >= " + Long.toString(timestamp);
}
public void delete(String where, String[] args) {
Uri uri = getUri();
context.getContentResolver().delete(uri, where, args);
}
public void wipe() {
Logger.debug(LOG_TAG, "Wiping.");
delete(null, null);
}
public void purgeDeleted() throws NullCursorException {
String where = BrowserContract.SyncColumns.IS_DELETED + "= 1";
Uri uri = getUri();
Logger.info(LOG_TAG, "Purging deleted from: " + uri);
context.getContentResolver().delete(uri, where, null);
}
/**
* Remove matching records from the database entirely, i.e., do not set a
* deleted flag, delete entirely.
*
* @param guid
* The GUID of the record to be deleted.
* @return The number of records deleted.
*/
public int purgeGuid(String guid) {
String where = BrowserContract.SyncColumns.GUID + " = ?";
String[] args = new String[] { guid };
int deleted = context.getContentResolver().delete(getUri(), where, args);
if (deleted != 1) {
Logger.warn(LOG_TAG, "Unexpectedly deleted " + deleted + " records for guid " + guid);
}
return deleted;
}
public void update(String guid, Record newRecord) {
String where = BrowserContract.SyncColumns.GUID + " = ?";
String[] args = new String[] { guid };
ContentValues cv = getContentValues(newRecord);
int updated = context.getContentResolver().update(getUri(), cv, where, args);
if (updated != 1) {
Logger.warn(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + guid);
}
}
public Uri insert(Record record) {
ContentValues cv = getContentValues(record);
return context.getContentResolver().insert(getUri(), cv);
}
/**
* Fetch all records.
* <p>
* The caller is responsible for closing the cursor.
*
* @return A cursor. You </b>must</b> close this when you're done with it.
* @throws NullCursorException
*/
public Cursor fetchAll() throws NullCursorException {
return queryHelper.safeQuery(".fetchAll", getAllColumns(), null, null, null);
}
/**
* Fetch GUIDs for records modified since the provided timestamp.
* <p>
* The caller is responsible for closing the cursor.
*
* @param timestamp A timestamp in milliseconds.
* @return A cursor. You <b>must</b> close this when you're done with it.
* @throws NullCursorException
*/
public Cursor getGUIDsSince(long timestamp) throws NullCursorException {
return queryHelper.safeQuery(".getGUIDsSince",
GUID_COLUMNS,
dateModifiedWhere(timestamp),
null, null);
}
/**
* Fetch records modified since the provided timestamp.
* <p>
* The caller is responsible for closing the cursor.
*
* @param timestamp A timestamp in milliseconds.
* @return A cursor. You <b>must</b> close this when you're done with it.
* @throws NullCursorException
*/
public Cursor fetchSince(long timestamp) throws NullCursorException {
return queryHelper.safeQuery(".fetchSince",
getAllColumns(),
dateModifiedWhere(timestamp),
null, null);
}
/**
* Fetch records for the provided GUIDs.
* <p>
* The caller is responsible for closing the cursor.
*
* @param guids The GUIDs of the records to fetch.
* @return A cursor. You <b>must</b> close this when you're done with it.
* @throws NullCursorException
*/
public Cursor fetch(String guids[]) throws NullCursorException {
String where = RepoUtils.computeSQLInClause(guids.length, "guid");
return queryHelper.safeQuery(".fetch", getAllColumns(), where, guids, null);
}
public void updateByGuid(String guid, ContentValues cv) {
String where = BrowserContract.SyncColumns.GUID + " = ?";
String[] args = new String[] { guid };
int updated = context.getContentResolver().update(getUri(), cv, where, args);
if (updated == 1) {
return;
}
Logger.warn(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + guid);
}
/**
* Insert records.
* <p>
* This inserts all the records (using <code>ContentProvider.bulkInsert</code>),
* but does <b>not</b> update the <code>androidID</code> of each record.
*
* @param records
* the records to insert.
* @return
* the number of records actually inserted.
* @throws NullCursorException
*/
public int bulkInsert(List<Record> records) throws NullCursorException {
if (records.isEmpty()) {
Logger.debug(LOG_TAG, "No records to insert, returning.");
}
int size = records.size();
ContentValues[] cvs = new ContentValues[size];
int index = 0;
for (Record record : records) {
try {
cvs[index] = getContentValues(record);
index += 1;
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception in getContentValues for record with guid " + record.guid, e);
}
}
if (index != size) {
// bulkInsert treats null ContentValues as blank rows, which we don't want
// to insert into the database.
// We expect exceptions in getContentValues to be exceedingly rare, so we
// re-allocate in the (rare) error case and maintain a fast path for the
// success case.
size = index;
ContentValues[] temp = new ContentValues[size];
System.arraycopy(cvs, 0, temp, 0, size); // No java.util.Arrays.copyOf in older Android SDKs.
}
int inserted = context.getContentResolver().bulkInsert(getUri(), cvs);
if (inserted == size) {
Logger.debug(LOG_TAG, "Inserted " + inserted + " records, as expected.");
} else {
Logger.debug(LOG_TAG, "Inserted " +
inserted + " records but expected " +
size + " records.");
}
return inserted;
}
}