Files
palemoon27/mobile/android/base/db/ReadingListProvider.java
T
2018-07-24 23:11:02 +08:00

415 lines
16 KiB
Java

/* 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.goanna.db;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import org.mozilla.goanna.db.DBUtils.UpdateOperation;
import static org.mozilla.goanna.db.BrowserContract.ReadingListItems.*;
public class ReadingListProvider extends SharedBrowserDatabaseProvider {
private static final String LOGTAG = "GoannaRLProvider";
static final String TABLE_READING_LIST = TABLE_NAME;
static final int ITEMS = 101;
static final int ITEMS_ID = 102;
static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
public static final String PLACEHOLDER_THIS_DEVICE = "$local";
static {
URI_MATCHER.addURI(BrowserContract.READING_LIST_AUTHORITY, "items", ITEMS);
URI_MATCHER.addURI(BrowserContract.READING_LIST_AUTHORITY, "items/#", ITEMS_ID);
}
/**
* Updates items that match the selection criteria. If no such items is found
* one is inserted with the attributes passed in. Returns 0 if no item updated.
*
* Only use this method for callers, not internally -- it futzes with the provided
* values to set syncing flags.
*
* @return Number of items updated or inserted
*/
public int updateOrInsertItem(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
if (!values.containsKey(CLIENT_LAST_MODIFIED)) {
values.put(CLIENT_LAST_MODIFIED, System.currentTimeMillis());
}
if (isCallerSync(uri)) {
int updated = updateItemsWithFlags(uri, values, null, selection, selectionArgs);
if (updated > 0) {
return updated;
}
return insertItem(uri, values) != -1 ? 1 : 0;
}
// Assume updated.
final ContentValues flags = processChangeValues(values);
int updated = updateItemsWithFlags(uri, values, flags, selection, selectionArgs);
if (updated <= 0) {
// Must be an insertion. Let's make sure we're NEW and discard any update flags.
values.put(SYNC_STATUS, SYNC_STATUS_NEW);
values.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_NONE);
updated = insertItem(uri, values) != -1 ? 1 : 0;
}
return updated;
}
/**
* This method does two things:
* * Based on the values provided, it computes and returns an incremental status change
* that can be applied to the database to track changes for syncing. This should be
* applied with {@link UpdateOperation#BITWISE_OR}.
* * It mutates the provided values to mark absolute field changes.
*
* @return null if no values were provided, or no change needs to be recorded.
*/
private ContentValues processChangeValues(ContentValues values) {
if (values == null || values.size() == 0) {
return null;
}
final ContentValues out = new ContentValues();
int flag = 0;
if (values.containsKey(MARKED_READ_BY) ||
values.containsKey(MARKED_READ_ON) ||
values.containsKey(IS_UNREAD)) {
flag |= SYNC_CHANGE_UNREAD_CHANGED;
}
if (values.containsKey(IS_FAVORITE)) {
flag |= SYNC_CHANGE_FAVORITE_CHANGED;
}
if (values.containsKey(RESOLVED_URL) ||
values.containsKey(RESOLVED_TITLE) ||
values.containsKey(EXCERPT)) {
flag |= SYNC_CHANGE_RESOLVED;
}
if (flag == 0) {
return null;
}
out.put(SYNC_CHANGE_FLAGS, flag);
return out;
}
/**
* Updates items that match the selection criteria.
*
* @return Number of items updated or inserted
*/
public int updateItemsWithFlags(Uri uri, ContentValues values, ContentValues flags, String selection, String[] selectionArgs) {
trace("Updating ReadingListItems on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
if (!values.containsKey(CLIENT_LAST_MODIFIED)) {
values.put(CLIENT_LAST_MODIFIED, System.currentTimeMillis());
}
if (flags == null) {
// This code path is used by Sync. Bypass metadata changes.
return db.update(TABLE_READING_LIST, values, selection, selectionArgs);
}
// Set synced items to MODIFIED; otherwise, leave the sync status untouched.
final ContentValues setModified = new ContentValues();
setModified.put(SYNC_STATUS, "CASE " + SYNC_STATUS +
" WHEN " + SYNC_STATUS_SYNCED +
" THEN " + SYNC_STATUS_MODIFIED +
" ELSE " + SYNC_STATUS +
" END");
final ContentValues[] valuesAndFlags = {values, flags, setModified};
final UpdateOperation[] ops = {UpdateOperation.ASSIGN, UpdateOperation.BITWISE_OR, UpdateOperation.EXPRESSION};
return DBUtils.updateArrays(db, TABLE_READING_LIST, valuesAndFlags, ops, selection, selectionArgs);
}
/**
* Inserts a new item into the DB. CLIENT_LAST_MODIFIED is generated if it is not specified.
*
* Non-Sync callers will have ADDED_ON and ADDED_BY set appropriately if they are missing;
* the assumption is that this is a new item added on this device.
*
* @return ID of the newly inserted item
*/
private long insertItem(Uri uri, ContentValues values) {
if (!values.containsKey(CLIENT_LAST_MODIFIED)) {
values.put(CLIENT_LAST_MODIFIED, System.currentTimeMillis());
}
// We trust the syncing code to specify SYNC_STATUS_SYNCED.
if (!isCallerSync(uri)) {
values.put(SYNC_STATUS, SYNC_STATUS_NEW);
if (!values.containsKey(ADDED_ON)) {
values.put(ADDED_ON, System.currentTimeMillis());
}
if (!values.containsKey(ADDED_BY)) {
values.put(ADDED_BY, PLACEHOLDER_THIS_DEVICE);
}
}
final String url = values.getAsString(URL);
debug("Inserting item in database with URL: " + url);
try {
return getWritableDatabase(uri).insertOrThrow(TABLE_READING_LIST, null, values);
} catch (SQLException e) {
Log.e(LOGTAG, "Insert failed.", e);
throw e;
}
}
private static final ContentValues DELETED_VALUES;
static {
final ContentValues values = new ContentValues();
values.put(IS_DELETED, 1);
values.put(URL, ""); // Non-null column.
values.putNull(RESOLVED_URL);
values.putNull(RESOLVED_TITLE);
values.putNull(TITLE);
values.putNull(EXCERPT);
values.putNull(ADDED_BY);
values.putNull(MARKED_READ_BY);
// Mark it as deleted for sync purposes.
values.put(SYNC_STATUS, SYNC_STATUS_DELETED);
values.put(SYNC_CHANGE_FLAGS, SYNC_CHANGE_NONE);
DELETED_VALUES = values;
}
/**
* Deletes items. Item is marked as 'deleted' so that sync can
* detect the change.
*
* It's the caller's responsibility to handle both original and resolved URLs.
* @return Number of deleted items
*/
int deleteItems(final Uri uri, String selection, String[] selectionArgs) {
debug("Deleting item entry for URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
// TODO: also ensure that we delete affected items from the disk cache. Bug 1133158.
if (isCallerSync(uri)) {
debug("Directly deleting from reading list.");
return db.delete(TABLE_READING_LIST, selection, selectionArgs);
}
// If we don't have a GUID for this item, then it hasn't made it
// to the server. Just delete it.
// If we do have a GUID, blank the row and mark it as deleted.
int total = 0;
final String whereNullGUID = DBUtils.concatenateWhere(selection, GUID + " IS NULL");
final String whereNotNullGUID = DBUtils.concatenateWhere(selection, GUID + " IS NOT NULL");
total += db.delete(TABLE_READING_LIST, whereNullGUID, selectionArgs);
total += updateItemsWithFlags(uri, DELETED_VALUES, null, whereNotNullGUID, selectionArgs);
return total;
}
int deleteItemByID(final Uri uri, long id) {
debug("Deleting item entry for ID: " + id);
final SQLiteDatabase db = getWritableDatabase(uri);
// TODO: also ensure that we delete affected items from the disk cache. Bug 1133158.
if (isCallerSync(uri)) {
debug("Directly deleting from reading list.");
final String selection = _ID + " = " + id;
return db.delete(TABLE_READING_LIST, selection, null);
}
// If we don't have a GUID for this item, then it hasn't made it
// to the server. Just delete it.
final String whereNullGUID = _ID + " = " + id + " AND " + GUID + " IS NULL";
final int raw = db.delete(TABLE_READING_LIST, whereNullGUID, null);
if (raw > 0) {
// _ID is unique, so this should only ever be 1, but it definitely means
// we don't need to try the second part.
return raw;
}
// If we do have a GUID, blank the row and mark it as deleted.
final String whereNotNullGUID = _ID + " = " + id + " AND " + GUID + " IS NOT NULL";
final ContentValues values = new ContentValues(DELETED_VALUES);
values.put(CLIENT_LAST_MODIFIED, System.currentTimeMillis());
return updateItemsWithFlags(uri, values, null, whereNotNullGUID, null);
}
@Override
@SuppressWarnings("fallthrough")
public int updateInTransaction(final Uri uri, ContentValues values, String selection, String[] selectionArgs) {
trace("Calling update in transaction on URI: " + uri);
int updated = 0;
int match = URI_MATCHER.match(uri);
switch (match) {
case ITEMS_ID:
debug("Update on ITEMS_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, TABLE_READING_LIST + "._id = ?");
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
case ITEMS: {
debug("Updating ITEMS: " + uri);
if (shouldUpdateOrInsert(uri)) {
// updateOrInsertItem handles change flags for us.
updated = updateOrInsertItem(uri, values, selection, selectionArgs);
} else {
// Don't use flags if we're inserting from sync.
ContentValues flags = isCallerSync(uri) ? null : processChangeValues(values);
updated = updateItemsWithFlags(uri, values, flags, selection, selectionArgs);
}
break;
}
default:
throw new UnsupportedOperationException("Unknown update URI " + uri);
}
debug("Updated " + updated + " rows for URI: " + uri);
return updated;
}
@Override
@SuppressWarnings("fallthrough")
public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
trace("Calling delete in transaction on URI: " + uri);
// This will never clean up any items that we're about to delete, so we
// might as well run it first!
cleanUpSomeDeletedRecords(uri, TABLE_READING_LIST);
int numDeleted = 0;
int match = URI_MATCHER.match(uri);
switch (match) {
case ITEMS_ID:
debug("Deleting on ITEMS_ID: " + uri);
numDeleted = deleteItemByID(uri, ContentUris.parseId(uri));
break;
case ITEMS:
debug("Deleting ITEMS: " + uri);
numDeleted = deleteItems(uri, selection, selectionArgs);
break;
default:
throw new UnsupportedOperationException("Unknown update URI " + uri);
}
debug("Deleted " + numDeleted + " rows for URI: " + uri);
return numDeleted;
}
@Override
public Uri insertInTransaction(Uri uri, ContentValues values) {
trace("Calling insert in transaction on URI: " + uri);
long id = -1;
int match = URI_MATCHER.match(uri);
switch (match) {
case ITEMS:
trace("Insert on ITEMS: " + uri);
id = insertItem(uri, values);
break;
default:
// Log here because we typically insert in a batch, and that will muffle.
Log.e(LOGTAG, "Unknown insert URI " + uri);
throw new UnsupportedOperationException("Unknown insert URI " + uri);
}
debug("Inserted ID in database: " + id);
if (id >= 0) {
return ContentUris.withAppendedId(uri, id);
}
Log.e(LOGTAG, "Got to end of insertInTransaction without returning an id!");
return null;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
String groupBy = null;
SQLiteDatabase db = getReadableDatabase(uri);
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
final int match = URI_MATCHER.match(uri);
switch (match) {
case ITEMS_ID:
trace("Query on ITEMS_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, _ID + " = ?");
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
case ITEMS:
trace("Query on ITEMS: " + uri);
if (!shouldShowDeleted(uri)) {
selection = DBUtils.concatenateWhere(IS_DELETED + " = 0", selection);
}
break;
default:
throw new UnsupportedOperationException("Unknown query URI " + uri);
}
if (TextUtils.isEmpty(sortOrder)) {
sortOrder = DEFAULT_SORT_ORDER;
}
trace("Running built query.");
qb.setTables(TABLE_READING_LIST);
Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, null, sortOrder, limit);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
@Override
public String getType(Uri uri) {
trace("Getting URI type: " + uri);
final int match = URI_MATCHER.match(uri);
switch (match) {
case ITEMS:
trace("URI is ITEMS: " + uri);
return CONTENT_TYPE;
case ITEMS_ID:
trace("URI is ITEMS_ID: " + uri);
return CONTENT_ITEM_TYPE;
}
debug("URI has unrecognized type: " + uri);
return null;
}
@Override
protected String getDeletedItemSelection(long earlierThan) {
if (earlierThan == -1L) {
return IS_DELETED + " = 1";
}
return IS_DELETED + " = 1 AND " + CLIENT_LAST_MODIFIED + " <= " + earlierThan;
}
}