Bug 959290 - Make ContentProvider for Reading List. r=lucasr

This commit is contained in:
Sola Ogunsakin 2014-03-03 15:34:57 -08:00
parent 43381ad91d
commit a01edc17fd
12 changed files with 618 additions and 155 deletions

View File

@ -307,6 +307,10 @@
android:authorities="@ANDROID_PACKAGE_NAME@.db.home"
android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/>
<provider android:name="org.mozilla.gecko.db.ReadingListProvider"
android:authorities="@ANDROID_PACKAGE_NAME@.db.readinglist"
android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/>
<service
android:exported="false"
android:name="org.mozilla.gecko.updater.UpdateService"

View File

@ -27,6 +27,9 @@ public class BrowserContract {
public static final String HOME_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.home";
public static final Uri HOME_AUTHORITY_URI = Uri.parse("content://" + HOME_AUTHORITY);
public static final String READING_LIST_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.readinglist";
public static final Uri READING_LIST_AUTHORITY_URI = Uri.parse("content://" + READING_LIST_AUTHORITY);
public static final String PARAM_PROFILE = "profile";
public static final String PARAM_PROFILE_PATH = "profilePath";
public static final String PARAM_LIMIT = "limit";
@ -378,4 +381,22 @@ public class BrowserContract {
static final String FAVICON_DB = "favicon_urls.db";
}
@RobocopTarget
public static final class ReadingListItems implements CommonColumns, URLColumns, SyncColumns {
private ReadingListItems() {}
public static final Uri CONTENT_URI = Uri.withAppendedPath(READING_LIST_AUTHORITY_URI, "items");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/readinglistitem";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/readinglistitem";
public static final String EXCERPT = "excerpt";
public static final String READ = "read";
public static final String LENGTH = "length";
public static final String DEFAULT_SORT_ORDER = _ID + " DESC";
public static final String[] DEFAULT_PROJECTION = new String[] { _ID, URL, TITLE, EXCERPT, LENGTH };
public static final String TABLE_NAME = "reading_list";
}
}

View File

@ -68,6 +68,8 @@ public class BrowserDB {
@RobocopTarget
public Cursor getBookmarksInFolder(ContentResolver cr, long folderId);
public Cursor getReadingList(ContentResolver cr);
public boolean isVisited(ContentResolver cr, String uri);
public int getReadingListCount(ContentResolver cr);
@ -100,6 +102,8 @@ public class BrowserDB {
public void removeReadingListItemWithURL(ContentResolver cr, String uri);
public void removeReadingListItem(ContentResolver cr, int id);
public LoadFaviconResult getFaviconForUrl(ContentResolver cr, String uri);
public String getFaviconUrlForHistoryUrl(ContentResolver cr, String url);
@ -216,6 +220,11 @@ public class BrowserDB {
return sDb.getBookmarksInFolder(cr, folderId);
}
@RobocopTarget
public static Cursor getReadingList(ContentResolver cr) {
return sDb.getReadingList(cr);
}
public static String getUrlForKeyword(ContentResolver cr, String keyword) {
return sDb.getUrlForKeyword(cr, keyword);
}
@ -270,6 +279,10 @@ public class BrowserDB {
sDb.removeReadingListItemWithURL(cr, uri);
}
public static void removeReadingListItem(ContentResolver cr, int id) {
sDb.removeReadingListItem(cr, id);
}
public static LoadFaviconResult getFaviconForFaviconUrl(ContentResolver cr, String faviconURL) {
return sDb.getFaviconForUrl(cr, faviconURL);
}

View File

@ -26,6 +26,7 @@ import org.mozilla.gecko.db.BrowserContract.FaviconColumns;
import org.mozilla.gecko.db.BrowserContract.Favicons;
import org.mozilla.gecko.db.BrowserContract.History;
import org.mozilla.gecko.db.BrowserContract.Obsolete;
import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
import org.mozilla.gecko.db.BrowserContract.Thumbnails;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.sync.Utils;
@ -49,7 +50,7 @@ import android.util.Log;
final class BrowserDatabaseHelper extends SQLiteOpenHelper {
private static final String LOGTAG = "GeckoBrowserDBHelper";
public static final int DATABASE_VERSION = 17;
public static final int DATABASE_VERSION = 18;
public static final String DATABASE_NAME = "browser.db";
final protected Context mContext;
@ -58,6 +59,7 @@ final class BrowserDatabaseHelper extends SQLiteOpenHelper {
static final String TABLE_HISTORY = History.TABLE_NAME;
static final String TABLE_FAVICONS = Favicons.TABLE_NAME;
static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME;
static final String TABLE_READING_LIST = ReadingListItems.TABLE_NAME;
static final String VIEW_COMBINED = Combined.VIEW_NAME;
static final String VIEW_BOOKMARKS_WITH_FAVICONS = Bookmarks.VIEW_WITH_FAVICONS;
@ -766,6 +768,8 @@ final class BrowserDatabaseHelper extends SQLiteOpenHelper {
// Create distribution bookmarks before our own default bookmarks
int pos = createDistributionBookmarks(db);
createDefaultBookmarks(db, pos);
createReadingListTable(db);
}
private String getLocalizedProperty(JSONObject bookmark, String property, Locale locale) throws JSONException {
@ -856,6 +860,27 @@ final class BrowserDatabaseHelper extends SQLiteOpenHelper {
return pos;
}
private void createReadingListTable(SQLiteDatabase db) {
debug("Creating " + TABLE_READING_LIST + " table");
db.execSQL("CREATE TABLE " + TABLE_READING_LIST + "(" +
ReadingListItems._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
ReadingListItems.URL + " TEXT NOT NULL, " +
ReadingListItems.TITLE + " TEXT, " +
ReadingListItems.EXCERPT + " TEXT, " +
ReadingListItems.READ + " TINYINT DEFAULT 0, " +
ReadingListItems.IS_DELETED + " TINYINT DEFAULT 0, " +
ReadingListItems.GUID + " TEXT UNIQUE NOT NULL, " +
ReadingListItems.DATE_MODIFIED + " INTEGER NOT NULL, " +
ReadingListItems.DATE_CREATED + " INTEGER NOT NULL, " +
ReadingListItems.LENGTH + " INTEGER DEFAULT 0 ); ");
db.execSQL("CREATE INDEX reading_list_url ON " + TABLE_READING_LIST + "("
+ ReadingListItems.URL + ")");
db.execSQL("CREATE UNIQUE INDEX reading_list_guid ON " + TABLE_READING_LIST + "("
+ ReadingListItems.GUID + ")");
}
// Inserts default bookmarks, starting at a specified position
private void createDefaultBookmarks(SQLiteDatabase db, int pos) {
Class<?> stringsClass = R.string.class;
@ -1523,6 +1548,68 @@ final class BrowserDatabaseHelper extends SQLiteOpenHelper {
}
}
/*
* Moves reading list items from 'bookmarks' table to 'reading_list' table. Uses the
* same item GUID.
*/
private void upgradeDatabaseFrom17to18(SQLiteDatabase db) {
debug("Moving reading list items from 'bookmarks' table to 'reading_list' table");
final String selection = Bookmarks.PARENT + " = ? AND " + Bookmarks.IS_DELETED + " = ? ";
final String[] selectionArgs = { String.valueOf(Bookmarks.FIXED_READING_LIST_ID), "0" };
final String[] projection = { Bookmarks._ID,
Bookmarks.GUID,
Bookmarks.URL,
Bookmarks.DATE_MODIFIED,
Bookmarks.DATE_CREATED,
Bookmarks.TITLE };
Cursor cursor = null;
try {
// Start transaction
db.beginTransaction();
// Create 'reading_list' table
createReadingListTable(db);
// Get all the reading list items from bookmarks table
cursor = db.query(TABLE_BOOKMARKS, projection, selection, selectionArgs,
null, null, null);
// Insert reading list items into reading_list table
while (cursor.moveToNext()) {
debug(DatabaseUtils.dumpCurrentRowToString(cursor));
ContentValues values = new ContentValues();
DatabaseUtils.cursorStringToContentValues(cursor, Bookmarks.URL, values, ReadingListItems.URL);
DatabaseUtils.cursorStringToContentValues(cursor, Bookmarks.GUID, values, ReadingListItems.GUID);
DatabaseUtils.cursorStringToContentValues(cursor, Bookmarks.TITLE, values, ReadingListItems.TITLE);
DatabaseUtils.cursorLongToContentValues(cursor, Bookmarks.DATE_CREATED, values, ReadingListItems.DATE_CREATED);
DatabaseUtils.cursorLongToContentValues(cursor, Bookmarks.DATE_MODIFIED, values, ReadingListItems.DATE_MODIFIED);
db.insertOrThrow(TABLE_READING_LIST, null, values);
}
// Delete reading list items from bookmarks table
db.delete(TABLE_BOOKMARKS,
Bookmarks.PARENT + " = ? ",
new String[] { String.valueOf(Bookmarks.FIXED_READING_LIST_ID) });
// Delete reading list special folder
db.delete(TABLE_BOOKMARKS,
Bookmarks._ID + " = ? ",
new String[] { String.valueOf(Bookmarks.FIXED_READING_LIST_ID) });
// Done
db.setTransactionSuccessful();
} catch (SQLException e) {
Log.e(LOGTAG, "Error migrating reading list items", e);
} finally {
if (cursor != null) {
cursor.close();
}
db.endTransaction();
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
debug("Upgrading browser.db: " + db.getPath() + " from " +
@ -1595,6 +1682,10 @@ final class BrowserDatabaseHelper extends SQLiteOpenHelper {
case 17:
upgradeDatabaseFrom16to17(db);
break;
case 18:
upgradeDatabaseFrom17to18(db);
break;
}
}

View File

@ -43,12 +43,6 @@ import android.util.Log;
public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper> {
private static final String LOGTAG = "GeckoBrowserProvider";
// Maximum age of deleted records to be cleaned up (20 days in ms)
static final long MAX_AGE_OF_DELETED_RECORDS = 86400000 * 20;
// Number of records marked as deleted to be removed
static final long DELETED_RECORDS_PURGE_LIMIT = 5;
// How many records to reposition in a single query.
// This should be less than the SQLite maximum number of query variables
// (currently 999) divided by the number of variables used per positioning
@ -272,88 +266,6 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
}
}
/*
* This utility is replicated from RepoUtils, which is managed by android-sync.
*/
private static String computeSQLInClause(int items, String field) {
final StringBuilder builder = new StringBuilder(field);
builder.append(" IN (");
int i = 0;
for (; i < items - 1; ++i) {
builder.append("?, ");
}
if (i < items) {
builder.append("?");
}
builder.append(")");
return builder.toString();
}
/**
* Turn a single-column cursor of longs into a single SQL "IN" clause.
* We can do this without using selection arguments because Long isn't
* vulnerable to injection.
*/
private static String computeSQLInClauseFromLongs(final Cursor cursor, String field) {
final StringBuilder builder = new StringBuilder(field);
builder.append(" IN (");
final int commaLimit = cursor.getCount() - 1;
int i = 0;
while (cursor.moveToNext()) {
builder.append(cursor.getLong(0));
if (i++ < commaLimit) {
builder.append(", ");
}
}
builder.append(")");
return builder.toString();
}
/**
* Clean up some deleted records from the specified table.
*
* If called in an existing transaction, it is the caller's responsibility
* to ensure that the transaction is already upgraded to a writer, because
* this method issues a read followed by a write, and thus is potentially
* vulnerable to an unhandled SQLITE_BUSY failure during the upgrade.
*
* If not called in an existing transaction, no new explicit transaction
* will be begun.
*/
private void cleanupSomeDeletedRecords(Uri fromUri, Uri targetUri, String tableName) {
Log.d(LOGTAG, "Cleaning up deleted records from " + tableName);
// We clean up records marked as deleted that are older than a
// predefined max age. It's important not be too greedy here and
// remove only a few old deleted records at a time.
// Android SQLite doesn't have LIMIT on DELETE. Instead, query for the
// IDs of matching rows, then delete them in one go.
final long now = System.currentTimeMillis();
final String selection = SyncColumns.IS_DELETED + " = 1 AND " +
SyncColumns.DATE_MODIFIED + " <= " +
(now - MAX_AGE_OF_DELETED_RECORDS);
final String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
final SQLiteDatabase db = getWritableDatabaseForProfile(profile, isTest(fromUri));
final String[] ids;
final String limit = Long.toString(DELETED_RECORDS_PURGE_LIMIT, 10);
final Cursor cursor = db.query(tableName, new String[] { CommonColumns._ID }, selection, null, null, null, null, limit);
try {
ids = new String[cursor.getCount()];
int i = 0;
while (cursor.moveToNext()) {
ids[i++] = Long.toString(cursor.getLong(0), 10);
}
} finally {
cursor.close();
}
final String inClause = computeSQLInClause(ids.length,
CommonColumns._ID);
db.delete(tableName, inClause, ids);
}
/**
* Remove enough history items to bring the database count below <code>retain</code>,
* removing no items with a modified time after <code>keepAfter</code>.
@ -418,21 +330,6 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
db.execSQL(sql);
}
private boolean isCallerSync(Uri uri) {
String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
return !TextUtils.isEmpty(isSync);
}
private boolean shouldShowDeleted(Uri uri) {
String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED);
return !TextUtils.isEmpty(showDeleted);
}
private boolean shouldUpdateOrInsert(Uri uri) {
String insertIfNeeded = uri.getQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED);
return Boolean.parseBoolean(insertIfNeeded);
}
private boolean shouldIncrementVisits(Uri uri) {
String incrementVisits = uri.getQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS);
return Boolean.parseBoolean(incrementVisits);

View File

@ -17,6 +17,7 @@ import org.mozilla.gecko.db.BrowserContract.ExpirePriority;
import org.mozilla.gecko.db.BrowserContract.FaviconColumns;
import org.mozilla.gecko.db.BrowserContract.Favicons;
import org.mozilla.gecko.db.BrowserContract.History;
import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
import org.mozilla.gecko.db.BrowserContract.SyncColumns;
import org.mozilla.gecko.db.BrowserContract.Thumbnails;
import org.mozilla.gecko.db.BrowserContract.URLColumns;
@ -65,6 +66,7 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
private final Uri mUpdateHistoryUriWithProfile;
private final Uri mFaviconsUriWithProfile;
private final Uri mThumbnailsUriWithProfile;
private final Uri mReadingListUriWithProfile;
private static final String[] DEFAULT_BOOKMARK_COLUMNS =
new String[] { Bookmarks._ID,
@ -87,6 +89,7 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
mCombinedUriWithProfile = appendProfile(Combined.CONTENT_URI);
mFaviconsUriWithProfile = appendProfile(Favicons.CONTENT_URI);
mThumbnailsUriWithProfile = appendProfile(Thumbnails.CONTENT_URI);
mReadingListUriWithProfile = appendProfile(ReadingListItems.CONTENT_URI);
mDeletedHistoryUriWithProfile = mHistoryUriWithProfile.buildUpon().
appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1").build();
@ -421,6 +424,16 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
return new LocalDBCursor(c);
}
@Override
public Cursor getReadingList(ContentResolver cr) {
return cr.query(mReadingListUriWithProfile,
ReadingListItems.DEFAULT_PROJECTION,
null,
null,
null);
}
// Returns true if any desktop bookmarks exist, which will be true if the user
// has set up sync at one point, or done a profile migration from XUL fennec.
private boolean desktopBookmarksExist(ContentResolver cr) {
@ -454,18 +467,18 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
@Override
public int getReadingListCount(ContentResolver cr) {
// This method is about the Reading List, not normal bookmarks
Cursor c = null;
try {
c = cr.query(mBookmarksUriWithProfile,
new String[] { Bookmarks._ID },
Bookmarks.PARENT + " = ?",
new String[] { String.valueOf(Bookmarks.FIXED_READING_LIST_ID) },
c = cr.query(mReadingListUriWithProfile,
new String[] { ReadingListItems._ID },
null,
null,
null);
return c.getCount();
} finally {
if (c != null)
if (c != null) {
c.close();
}
}
}
@ -498,14 +511,11 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
public boolean isReadingListItem(ContentResolver cr, String uri) {
Cursor c = null;
try {
c = cr.query(mBookmarksUriWithProfile,
new String[] { Bookmarks._ID },
Bookmarks.URL + " = ? AND " +
Bookmarks.PARENT + " == ?",
new String[] { uri,
String.valueOf(Bookmarks.FIXED_READING_LIST_ID) },
Bookmarks.URL);
c = cr.query(mReadingListUriWithProfile,
new String[] { ReadingListItems._ID },
ReadingListItems.URL + " = ? ",
new String[] { uri },
null);
return c.getCount() > 0;
} catch (NullPointerException e) {
Log.e(LOGTAG, "NullPointerException in isReadingListItem");
@ -690,20 +700,33 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
@Override
public void addReadingListItem(ContentResolver cr, String title, String uri) {
addBookmarkItem(cr, title, uri, Bookmarks.FIXED_READING_LIST_ID);
final ContentValues values = new ContentValues();
values.put(ReadingListItems.IS_DELETED, 0);
values.put(ReadingListItems.URL, uri);
values.put(ReadingListItems.TITLE, title);
// Restore deleted record if possible
final Uri insertUri = mReadingListUriWithProfile
.buildUpon()
.appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
.build();
final int updated = cr.update(insertUri,
values,
ReadingListItems.URL + " = ? ",
new String[] { uri });
debug("Updated " + updated + " rows to new modified time.");
}
@Override
public void removeReadingListItemWithURL(ContentResolver cr, String uri) {
Uri contentUri = mBookmarksUriWithProfile;
cr.delete(mReadingListUriWithProfile, ReadingListItems.URL + " = ? ", new String[] { uri });
}
// Do this now so that the items still exist!
bumpParents(cr, Bookmarks.URL, uri);
final String[] urlArgs = new String[] { uri, String.valueOf(Bookmarks.FIXED_READING_LIST_ID) };
final String urlEquals = Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " == ?";
cr.delete(contentUri, urlEquals, urlArgs);
@Override
public void removeReadingListItem(ContentResolver cr, int id) {
cr.delete(mReadingListUriWithProfile, ReadingListItems._ID + " = ? ", new String[] { String.valueOf(id) });
}
@Override

View File

@ -0,0 +1,261 @@
/* 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 org.mozilla.gecko.db.BrowserContract.ReadingListItems;
import org.mozilla.gecko.sync.Utils;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
public class ReadingListProvider extends TransactionalProvider<BrowserDatabaseHelper> {
private static final String LOGTAG = "GeckoReadingListProv";
static final String TABLE_READING_LIST = ReadingListItems.TABLE_NAME;
static final int ITEMS = 101;
static final int ITEMS_ID = 102;
static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
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.
*
* @return Number of items updated or inserted
*/
public int updateOrInsertItem(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int updated = updateItems(uri, values, selection, selectionArgs);
if (updated <= 0) {
updated = insertItem(uri, values) != -1 ? 1 : 0;
}
return updated;
}
/**
* Updates items that match the selection criteria.
*
* @return Number of items updated or inserted
*/
public int updateItems(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
trace("Updating ReadingListItems on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
if (!values.containsKey(ReadingListItems.DATE_MODIFIED)) {
values.put(ReadingListItems.DATE_MODIFIED, System.currentTimeMillis());
}
return db.update(TABLE_READING_LIST, values, selection, selectionArgs);
}
/**
* Inserts a new item into the DB. DATE_CREATED, DATE_MODIFIED
* and GUID fields are generated if they are not specified.
*
* @return ID of the newly inserted item
*/
long insertItem(Uri uri, ContentValues values) {
long now = System.currentTimeMillis();
if (!values.containsKey(ReadingListItems.DATE_CREATED)) {
values.put(ReadingListItems.DATE_CREATED, now);
}
if (!values.containsKey(ReadingListItems.DATE_MODIFIED)) {
values.put(ReadingListItems.DATE_MODIFIED, now);
}
if (!values.containsKey(ReadingListItems.GUID)) {
values.put(ReadingListItems.GUID, Utils.generateGuid());
}
String url = values.getAsString(ReadingListItems.URL);
debug("Inserting item in database with URL: " + url);
return getWritableDatabase(uri)
.insertOrThrow(TABLE_READING_LIST, null, values);
}
/**
* Deletes items. Item is marked as 'deleted' so that sync can
* detect the change.
*
* @return Number of deleted items
*/
int deleteItems(Uri uri, String selection, String[] selectionArgs) {
debug("Deleting item entry for URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
if (isCallerSync(uri)) {
return db.delete(TABLE_READING_LIST, selection, selectionArgs);
}
debug("Marking item entry as deleted for URI: " + uri);
ContentValues values = new ContentValues();
values.put(ReadingListItems.IS_DELETED, 1);
cleanupSomeDeletedRecords(uri, ReadingListItems.CONTENT_URI, TABLE_READING_LIST);
return updateItems(uri, values, selection, selectionArgs);
}
@Override
@SuppressWarnings("fallthrough")
public int updateInTransaction(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);
updated = shouldUpdateOrInsert(uri) ?
updateOrInsertItem(uri, values, selection, selectionArgs) :
updateItems(uri, values, 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);
int numDeleted = 0;
int match = URI_MATCHER.match(uri);
switch (match) {
case ITEMS_ID:
debug("Deleting 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("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:
throw new UnsupportedOperationException("Unknown insert URI " + uri);
}
debug("Inserted ID in database: " + id);
if (id >= 0) {
return ContentUris.withAppendedId(uri, 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, ReadingListItems._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(ReadingListItems.IS_DELETED + " = 0", selection);
break;
default:
throw new UnsupportedOperationException("Unknown query URI " + uri);
}
if (TextUtils.isEmpty(sortOrder)) {
sortOrder = ReadingListItems.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 ReadingListItems.CONTENT_TYPE;
case ITEMS_ID:
trace("URI is ITEMS_ID: " + uri);
return ReadingListItems.CONTENT_ITEM_TYPE;
}
debug("URI has unrecognized type: " + uri);
return null;
}
@Override
protected BrowserDatabaseHelper createDatabaseHelper(Context context,
String databasePath) {
return new BrowserDatabaseHelper(context, databasePath);
}
@Override
protected String getDatabaseName() {
return BrowserDatabaseHelper.DATABASE_NAME;
}
}

View File

@ -4,11 +4,15 @@
package org.mozilla.gecko.db;
import org.mozilla.gecko.db.BrowserContract.CommonColumns;
import org.mozilla.gecko.db.BrowserContract.SyncColumns;
import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
@ -111,6 +115,45 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
return mDatabases.getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase();
}
/**
* Return true of the query is from Firefox Sync.
* @param uri query URI
*/
public static boolean isCallerSync(Uri uri) {
String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
return !TextUtils.isEmpty(isSync);
}
/**
* Indicates whether a query should include deleted fields
* based on the URI.
* @param uri query URI
*/
public static boolean shouldShowDeleted(Uri uri) {
String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED);
return !TextUtils.isEmpty(showDeleted);
}
/**
* Indicates whether an insertion should be made if a record doesn't
* exist, based on the URI.
* @param uri query URI
*/
public static boolean shouldUpdateOrInsert(Uri uri) {
String insertIfNeeded = uri.getQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED);
return Boolean.parseBoolean(insertIfNeeded);
}
/**
* Indicates whether query is a test based on the URI.
* @param uri query URI
*/
public static boolean isTest(Uri uri) {
String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST);
return !TextUtils.isEmpty(isTest);
}
protected SQLiteDatabase getWritableDatabaseForProfile(String profile, boolean isTest) {
return mDatabases.getDatabaseHelperForProfile(profile, isTest).getWritableDatabase();
}
@ -246,6 +289,43 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
isInBatchOperation.set(Boolean.FALSE);
}
/*
* This utility is replicated from RepoUtils, which is managed by android-sync.
*/
protected static String computeSQLInClause(int items, String field) {
final StringBuilder builder = new StringBuilder(field);
builder.append(" IN (");
int i = 0;
for (; i < items - 1; ++i) {
builder.append("?, ");
}
if (i < items) {
builder.append("?");
}
builder.append(")");
return builder.toString();
}
/**
* Turn a single-column cursor of longs into a single SQL "IN" clause.
* We can do this without using selection arguments because Long isn't
* vulnerable to injection.
*/
protected static String computeSQLInClauseFromLongs(final Cursor cursor, String field) {
final StringBuilder builder = new StringBuilder(field);
builder.append(" IN (");
final int commaLimit = cursor.getCount() - 1;
int i = 0;
while (cursor.moveToNext()) {
builder.append(cursor.getLong(0));
if (i++ < commaLimit) {
builder.append(", ");
}
}
builder.append(")");
return builder.toString();
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
trace("Calling delete on URI: " + uri + ", " + selection + ", " + selectionArgs);
@ -348,9 +428,59 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
return successes;
}
protected boolean isTest(Uri uri) {
String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST);
return !TextUtils.isEmpty(isTest);
/**
* Clean up some deleted records from the specified table.
*
* If called in an existing transaction, it is the caller's responsibility
* to ensure that the transaction is already upgraded to a writer, because
* this method issues a read followed by a write, and thus is potentially
* vulnerable to an unhandled SQLITE_BUSY failure during the upgrade.
*
* If not called in an existing transaction, no new explicit transaction
* will be begun.
*/
protected void cleanupSomeDeletedRecords(Uri fromUri, Uri targetUri, String tableName) {
Log.d(LOGTAG, "Cleaning up deleted records from " + tableName);
// We clean up records marked as deleted that are older than a
// predefined max age. It's important not be too greedy here and
// remove only a few old deleted records at a time.
// we cleanup records marked as deleted that are older than a
// predefined max age. It's important not be too greedy here and
// remove only a few old deleted records at a time.
// Maximum age of deleted records to be cleaned up (20 days in ms)
final long MAX_AGE_OF_DELETED_RECORDS = 86400000 * 20;
// Number of records marked as deleted to be removed
final long DELETED_RECORDS_PURGE_LIMIT = 5;
// Android SQLite doesn't have LIMIT on DELETE. Instead, query for the
// IDs of matching rows, then delete them in one go.
final long now = System.currentTimeMillis();
final String selection = SyncColumns.IS_DELETED + " = 1 AND " +
SyncColumns.DATE_MODIFIED + " <= " +
(now - MAX_AGE_OF_DELETED_RECORDS);
final String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
final SQLiteDatabase db = getWritableDatabaseForProfile(profile, isTest(fromUri));
final String[] ids;
final String limit = Long.toString(DELETED_RECORDS_PURGE_LIMIT, 10);
final Cursor cursor = db.query(tableName, new String[] { CommonColumns._ID }, selection, null, null, null, null, limit);
try {
ids = new String[cursor.getCount()];
int i = 0;
while (cursor.moveToNext()) {
ids[i++] = Long.toString(cursor.getLong(0), 10);
}
} finally {
cursor.close();
}
final String inClause = computeSQLInClause(ids.length,
CommonColumns._ID);
db.delete(tableName, inClause, ids);
}
// Calculate these once, at initialization. isLoggable is too expensive to

View File

@ -21,10 +21,10 @@ public class HomeContextMenuInfo extends AdapterContextMenuInfo {
public String url;
public String title;
public boolean isFolder = false;
public boolean inReadingList = false;
public int display = Combined.DISPLAY_NORMAL;
public int historyId = -1;
public int bookmarkId = -1;
public int readingListItemId = -1;
public HomeContextMenuInfo(View targetView, int position, long id) {
super(targetView, position, id);
@ -39,7 +39,11 @@ public class HomeContextMenuInfo extends AdapterContextMenuInfo {
}
public boolean isInReadingList() {
return inReadingList;
return readingListItemId > -1;
}
public boolean canRemove() {
return hasBookmarkId() || hasHistoryId() || isInReadingList();
}
public String getDisplayTitle() {

View File

@ -91,8 +91,8 @@ abstract class HomeFragment extends Fragment {
menu.findItem(R.id.home_edit_bookmark).setVisible(false);
}
// Hide the "Remove" menuitem if this item doesn't have a bookmark or history ID.
if (!info.hasBookmarkId() && !info.hasHistoryId()) {
// Hide the "Remove" menuitem if this item not removable.
if (!info.canRemove()) {
menu.findItem(R.id.home_remove).setVisible(false);
}
@ -176,7 +176,12 @@ abstract class HomeFragment extends Fragment {
}
if (info.hasBookmarkId()) {
new RemoveBookmarkTask(context, info.bookmarkId, info.url, info.isInReadingList()).execute();
new RemoveBookmarkTask(context, info.bookmarkId).execute();
return true;
}
if (info.isInReadingList()) {
(new RemoveReadingListItemTask(context, info.readingListItemId, info.url)).execute();
return true;
}
}
@ -223,36 +228,49 @@ abstract class HomeFragment extends Fragment {
private static class RemoveBookmarkTask extends UiAsyncTask<Void, Void, Void> {
private final Context mContext;
private final int mId;
private final String mUrl;
private final boolean mInReadingList;
public RemoveBookmarkTask(Context context, int id, String url, boolean inReadingList) {
public RemoveBookmarkTask(Context context, int id) {
super(ThreadUtils.getBackgroundHandler());
mContext = context;
mId = id;
mUrl = url;
mInReadingList = inReadingList;
}
@Override
public Void doInBackground(Void... params) {
ContentResolver cr = mContext.getContentResolver();
BrowserDB.removeBookmark(cr, mId);
if (mInReadingList) {
GeckoEvent e = GeckoEvent.createBroadcastEvent("Reader:Remove", mUrl);
GeckoAppShell.sendEventToGecko(e);
}
return null;
}
@Override
public void onPostExecute(Void result) {
// The remove from reading list toast is handled in Reader:Removed,
// so handle only the bookmark removed toast here.
if (!mInReadingList) {
Toast.makeText(mContext, R.string.bookmark_removed, Toast.LENGTH_SHORT).show();
}
Toast.makeText(mContext, R.string.bookmark_removed, Toast.LENGTH_SHORT).show();
}
}
private static class RemoveReadingListItemTask extends UiAsyncTask<Void, Void, Void> {
private final int mId;
private final String mUrl;
private final Context mContext;
public RemoveReadingListItemTask(Context context, int id, String url) {
super(ThreadUtils.getBackgroundHandler());
mId = id;
mUrl = url;
mContext = context;
}
@Override
public Void doInBackground(Void... params) {
ContentResolver cr = mContext.getContentResolver();
BrowserDB.removeReadingListItem(cr, mId);
GeckoEvent e = GeckoEvent.createBroadcastEvent("Reader:Remove", mUrl);
GeckoAppShell.sendEventToGecko(e);
return null;
}
}

View File

@ -9,7 +9,7 @@ import java.util.EnumSet;
import org.mozilla.gecko.R;
import org.mozilla.gecko.ReaderModeUtils;
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.BrowserDB.URLColumns;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
@ -36,6 +36,7 @@ import android.widget.TextView;
* Fragment that displays reading list contents in a ListView.
*/
public class ReadingListPanel extends HomeFragment {
// Cursor loader ID for reading list
private static final int LOADER_ID_READING_LIST = 0;
@ -113,10 +114,9 @@ public class ReadingListPanel extends HomeFragment {
@Override
public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
info.url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL));
info.title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE));
info.bookmarkId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID));
info.inReadingList = true;
info.url = cursor.getString(cursor.getColumnIndexOrThrow(ReadingListItems.URL));
info.title = cursor.getString(cursor.getColumnIndexOrThrow(ReadingListItems.TITLE));
info.readingListItemId = cursor.getInt(cursor.getColumnIndexOrThrow(ReadingListItems._ID));
return info;
}
});
@ -201,7 +201,7 @@ public class ReadingListPanel extends HomeFragment {
@Override
public Cursor loadCursor() {
return BrowserDB.getBookmarksInFolder(getContext().getContentResolver(), Bookmarks.FIXED_READING_LIST_ID);
return BrowserDB.getReadingList(getContext().getContentResolver());
}
}

View File

@ -117,6 +117,7 @@ gbjar.sources += [
'db/LocalBrowserDB.java',
'db/PasswordsProvider.java',
'db/PerProfileDatabases.java',
'db/ReadingListProvider.java',
'db/SQLiteBridgeContentProvider.java',
'db/TabsProvider.java',
'db/TransactionalProvider.java',