/* 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.Collection;
import java.util.HashMap;
import java.util.Map;
import org.json.simple.JSONArray;
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.BookmarkRecord;
import org.mozilla.gecko.sync.repositories.domain.Record;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
public class AndroidBrowserBookmarksDataAccessor extends AndroidBrowserRepositoryDataAccessor {
private static final String LOG_TAG = "BookmarksDataAccessor";
/*
* Fragments of SQL to make our lives easier.
*/
private static final String BOOKMARK_IS_FOLDER = BrowserContract.Bookmarks.TYPE + " = " +
BrowserContract.Bookmarks.TYPE_FOLDER;
private static final String GUID_NOT_TAGS_OR_PLACES = BrowserContract.SyncColumns.GUID + " NOT IN ('" +
BrowserContract.Bookmarks.TAGS_FOLDER_GUID + "', '" +
BrowserContract.Bookmarks.PLACES_FOLDER_GUID + "')";
private static final String EXCLUDE_SPECIAL_GUIDS_WHERE_CLAUSE;
static {
if (AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS.length > 0) {
StringBuilder b = new StringBuilder(BrowserContract.SyncColumns.GUID + " NOT IN (");
int remaining = AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS.length - 1;
for (String specialGuid : AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS) {
b.append('"');
b.append(specialGuid);
b.append('"');
if (remaining-- > 0) {
b.append(", ");
}
}
b.append(')');
EXCLUDE_SPECIAL_GUIDS_WHERE_CLAUSE = b.toString();
} else {
EXCLUDE_SPECIAL_GUIDS_WHERE_CLAUSE = null; // null is a valid WHERE clause.
}
}
public static final String TYPE_FOLDER = "folder";
public static final String TYPE_BOOKMARK = "bookmark";
private final RepoUtils.QueryHelper queryHelper;
public AndroidBrowserBookmarksDataAccessor(Context context) {
super(context);
this.queryHelper = new RepoUtils.QueryHelper(context, getUri(), LOG_TAG);
}
@Override
protected Uri getUri() {
return BrowserContractHelpers.BOOKMARKS_CONTENT_URI;
}
protected Uri getPositionsUri() {
return BrowserContractHelpers.BOOKMARKS_POSITIONS_CONTENT_URI;
}
@Override
public void wipe() {
Uri uri = getUri();
Logger.info(LOG_TAG, "wiping (except for special guids): " + uri);
context.getContentResolver().delete(uri, EXCLUDE_SPECIAL_GUIDS_WHERE_CLAUSE, null);
}
private String[] GUID_AND_ID = new String[] { BrowserContract.Bookmarks.GUID,
BrowserContract.Bookmarks._ID };
protected Cursor getGuidsIDsForFolders() throws NullCursorException {
// Exclude "places" and "tags", in case they've ended up in the DB.
String where = BOOKMARK_IS_FOLDER + " AND " + GUID_NOT_TAGS_OR_PLACES;
return queryHelper.safeQuery(".getGuidsIDsForFolders", GUID_AND_ID, where, null, null);
}
/**
* Issue a request to the Content Provider to update the positions of the
* records named by the provided GUIDs to the index of their GUID in the
* provided array.
*
* @param childArray
* A sequence of GUID strings.
*/
public int updatePositions(ArrayList<String> childArray) {
final int size = childArray.size();
if (size == 0) {
return 0;
}
Logger.debug(LOG_TAG, "Updating positions for " + size + " items.");
String[] args = childArray.toArray(new String[size]);
return context.getContentResolver().update(getPositionsUri(), new ContentValues(), null, args);
}
public int bumpModifiedByGUID(Collection<String> ids, long modified) {
final int size = ids.size();
if (size == 0) {
return 0;
}
Logger.debug(LOG_TAG, "Bumping modified for " + size + " items to " + modified);
String where = RepoUtils.computeSQLInClause(size, BrowserContract.Bookmarks.GUID);
String[] selectionArgs = ids.toArray(new String[size]);
ContentValues values = new ContentValues();
values.put(BrowserContract.Bookmarks.DATE_MODIFIED, modified);
return context.getContentResolver().update(getUri(), values, where, selectionArgs);
}
/**
* Bump the modified time of a record by ID.
*/
public int bumpModified(long id, long modified) {
Logger.debug(LOG_TAG, "Bumping modified for " + id + " to " + modified);
String where = BrowserContract.Bookmarks._ID + " = ?";
String[] selectionArgs = new String[] { String.valueOf(id) };
ContentValues values = new ContentValues();
values.put(BrowserContract.Bookmarks.DATE_MODIFIED, modified);
return context.getContentResolver().update(getUri(), values, where, selectionArgs);
}
protected void updateParentAndPosition(String guid, long newParentId, long position) {
ContentValues cv = new ContentValues();
cv.put(BrowserContract.Bookmarks.PARENT, newParentId);
if (position >= 0) {
cv.put(BrowserContract.Bookmarks.POSITION, position);
}
updateByGuid(guid, cv);
}
protected Map<String, Long> idsForGUIDs(String[] guids) throws NullCursorException {
final String where = RepoUtils.computeSQLInClause(guids.length, BrowserContract.Bookmarks.GUID);
Cursor c = queryHelper.safeQuery(".idsForGUIDs", GUID_AND_ID, where, guids, null);
try {
HashMap<String, Long> out = new HashMap<String, Long>();
if (!c.moveToFirst()) {
return out;
}
final int guidIndex = c.getColumnIndexOrThrow(BrowserContract.Bookmarks.GUID);
final int idIndex = c.getColumnIndexOrThrow(BrowserContract.Bookmarks._ID);
while (!c.isAfterLast()) {
out.put(c.getString(guidIndex), c.getLong(idIndex));
c.moveToNext();
}
return out;
} finally {
c.close();
}
}
/**
* Move the children of each source folder to the destination folder.
* Bump the modified time of each child.
* The caller should bump the modified time of the destination if desired.
*
* @param fromIDs the Android IDs of the source folders.
* @param to the Android ID of the destination folder.
* @return the number of updated rows.
*/
protected int moveChildren(String[] fromIDs, long to) {
long now = System.currentTimeMillis();
long pos = -1;
ContentValues cv = new ContentValues();
cv.put(BrowserContract.Bookmarks.PARENT, to);
cv.put(BrowserContract.Bookmarks.DATE_MODIFIED, now);
cv.put(BrowserContract.Bookmarks.POSITION, pos);
final String where = RepoUtils.computeSQLInClause(fromIDs.length, BrowserContract.Bookmarks.PARENT);
return context.getContentResolver().update(getUri(), cv, where, fromIDs);
}
/*
* Verify that all special GUIDs are present and that they aren't marked as deleted.
* Insert them if they aren't there.
*/
public void checkAndBuildSpecialGuids() throws NullCursorException {
final String[] specialGUIDs = AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS;
Cursor cur = fetch(specialGUIDs);
long placesRoot = 0;
// Map from GUID to whether deleted. Non-presence implies just that.
HashMap<String, Boolean> statuses = new HashMap<String, Boolean>(specialGUIDs.length);
try {
if (cur.moveToFirst()) {
while (!cur.isAfterLast()) {
String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
if ("places".equals(guid)) {
placesRoot = RepoUtils.getLongFromCursor(cur, BrowserContract.CommonColumns._ID);
}
// Make sure none of these folders are marked as deleted.
boolean deleted = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1;
statuses.put(guid, deleted);
cur.moveToNext();
}
}
} finally {
cur.close();
}
// Insert or undelete them if missing.
for (String guid : specialGUIDs) {
if (statuses.containsKey(guid)) {
if (statuses.get(guid)) {
// Undelete.
Logger.info(LOG_TAG, "Undeleting special GUID " + guid);
ContentValues cv = new ContentValues();
cv.put(BrowserContract.SyncColumns.IS_DELETED, 0);
updateByGuid(guid, cv);
}
} else {
// Insert.
if (guid.equals("places")) {
// This is awkward.
Logger.info(LOG_TAG, "No places root. Inserting one.");
placesRoot = insertSpecialFolder("places", 0);
} else if (guid.equals("mobile")) {
Logger.info(LOG_TAG, "No mobile folder. Inserting one under the places root.");
insertSpecialFolder("mobile", placesRoot);
} else {
// unfiled, menu, toolbar.
Logger.info(LOG_TAG, "No " + guid + " root. Inserting one under places (" + placesRoot + ").");
insertSpecialFolder(guid, placesRoot);
}
}
}
}
private long insertSpecialFolder(String guid, long parentId) {
BookmarkRecord record = new BookmarkRecord(guid);
record.title = AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.get(guid);
record.type = "folder";
record.androidParentID = parentId;
return ContentUris.parseId(insert(record));
}
@Override
protected ContentValues getContentValues(Record record) {
BookmarkRecord rec = (BookmarkRecord) record;
if (rec.deleted) {
ContentValues cv = new ContentValues();
cv.put(BrowserContract.SyncColumns.GUID, rec.guid);
cv.put(BrowserContract.Bookmarks.IS_DELETED, 1);
return cv;
}
final int recordType = BrowserContractHelpers.typeCodeForString(rec.type);
if (recordType == -1) {
throw new IllegalStateException("Unexpected record type " + rec.type);
}
ContentValues cv = new ContentValues();
cv.put(BrowserContract.SyncColumns.GUID, rec.guid);
cv.put(BrowserContract.Bookmarks.TYPE, recordType);
cv.put(BrowserContract.Bookmarks.TITLE, rec.title);
cv.put(BrowserContract.Bookmarks.URL, rec.bookmarkURI);
cv.put(BrowserContract.Bookmarks.DESCRIPTION, rec.description);
if (rec.tags == null) {
rec.tags = new JSONArray();
}
cv.put(BrowserContract.Bookmarks.TAGS, rec.tags.toJSONString());
cv.put(BrowserContract.Bookmarks.KEYWORD, rec.keyword);
cv.put(BrowserContract.Bookmarks.PARENT, rec.androidParentID);
cv.put(BrowserContract.Bookmarks.POSITION, rec.androidPosition);
// Note that we don't set the modified timestamp: we allow the
// content provider to do that for us.
return cv;
}
/**
* Returns a cursor over non-deleted records that list the given androidID as a parent.
*/
public Cursor getChildren(long androidID) throws NullCursorException {
return getChildren(androidID, false);
}
/**
* Returns a cursor with any records that list the given androidID as a parent.
* Excludes 'places', and optionally any deleted records.
*/
public Cursor getChildren(long androidID, boolean includeDeleted) throws NullCursorException {
final String where = BrowserContract.Bookmarks.PARENT + " = ? AND " +
BrowserContract.SyncColumns.GUID + " <> ? " +
(!includeDeleted ? ("AND " + BrowserContract.SyncColumns.IS_DELETED + " = 0") : "");
final String[] args = new String[] { String.valueOf(androidID), "places" };
// Order by position, falling back on creation date and ID.
final String order = BrowserContract.Bookmarks.POSITION + ", " +
BrowserContract.SyncColumns.DATE_CREATED + ", " +
BrowserContract.Bookmarks._ID;
return queryHelper.safeQuery(".getChildren", getAllColumns(), where, args, order);
}
@Override
protected String[] getAllColumns() {
return BrowserContractHelpers.BookmarkColumns;
}
}