Bug 718238 - Part 2: parenting and ordering of bookmarks. r=nalexander

This commit is contained in:
Richard Newman 2012-02-23 08:14:05 -08:00
parent 6c76915705
commit 4f730b8bb6
8 changed files with 726 additions and 388 deletions

View File

@ -44,8 +44,9 @@ import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Locale;
import java.util.TreeMap;
import org.mozilla.apache.commons.codec.binary.Base32;
import org.mozilla.apache.commons.codec.binary.Base64;
@ -234,37 +235,12 @@ public class Utils {
return context.getSharedPreferences(prefsPath, SHARED_PREFERENCES_MODE);
}
/**
* Populate null slots in the provided array from keys in the provided Map.
* Set values in the map to be the new indices.
*
* @param dest
* @param source
* @throws Exception
*/
public static void fillArraySpaces(String[] dest, HashMap<String, Long> source) throws Exception {
int i = 0;
int c = dest.length;
int needed = source.size();
if (needed == 0) {
return;
}
if (needed > c) {
throw new Exception("Need " + needed + " array spaces, have no more than " + c);
}
for (String key : source.keySet()) {
while (i < c) {
if (dest[i] == null) {
// Great!
dest[i] = key;
source.put(key, (long) i);
break;
}
++i;
}
}
if (i >= c) {
throw new Exception("Could not fill array spaces.");
public static void addToIndexBucketMap(TreeMap<Long, ArrayList<String>> map, long index, String value) {
ArrayList<String> bucket = map.get(index);
if (bucket == null) {
bucket = new ArrayList<String>();
}
bucket.add(value);
map.put(index, bucket);
}
}

View File

@ -38,10 +38,13 @@
package org.mozilla.gecko.sync.repositories.android;
import java.util.ArrayList;
import java.util.HashMap;
import org.json.simple.JSONArray;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.android.BrowserContract.Bookmarks;
import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
import org.mozilla.gecko.sync.repositories.domain.Record;
@ -49,7 +52,6 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
public class AndroidBrowserBookmarksDataAccessor extends AndroidBrowserRepositoryDataAccessor {
@ -78,16 +80,53 @@ public class AndroidBrowserBookmarksDataAccessor extends AndroidBrowserRepositor
return BrowserContract.Bookmarks.CONTENT_URI;
}
protected Uri getPositionsUri() {
return BrowserContract.Bookmarks.POSITIONS_CONTENT_URI;
}
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", null, 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) {
Logger.debug(LOG_TAG, "Updating positions for " + childArray.size() + " items.");
String[] args = childArray.toArray(new String[childArray.size()]);
return context.getContentResolver().update(getPositionsUri(), new ContentValues(), null, args);
}
/**
* Bump the modified time of a record by ID.
*
* @param id
* @param modified
* @return
*/
public int bumpModified(long id, long modified) {
Logger.debug(LOG_TAG, "Bumping modified for " + id + " to " + modified);
String where = Bookmarks._ID + " = ?";
String[] selectionArgs = new String[] { String.valueOf(id) };
ContentValues values = new ContentValues();
values.put(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);
cv.put(BrowserContract.Bookmarks.POSITION, position);
if (position >= 0) {
cv.put(BrowserContract.Bookmarks.POSITION, position);
}
updateByGuid(guid, cv);
}
@ -96,12 +135,13 @@ public class AndroidBrowserBookmarksDataAccessor extends AndroidBrowserRepositor
* Insert them if they aren't there.
*/
public void checkAndBuildSpecialGuids() throws NullCursorException {
Cursor cur = fetch(RepoUtils.SPECIAL_GUIDS);
final String[] specialGUIDs = AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS;
Cursor cur = fetch(specialGUIDs);
long mobileRoot = 0;
long desktopRoot = 0;
// Map from GUID to whether deleted. Non-presence implies just that.
HashMap<String, Boolean> statuses = new HashMap<String, Boolean>(RepoUtils.SPECIAL_GUIDS.length);
HashMap<String, Boolean> statuses = new HashMap<String, Boolean>(specialGUIDs.length);
try {
if (cur.moveToFirst()) {
while (!cur.isAfterLast()) {
@ -123,11 +163,11 @@ public class AndroidBrowserBookmarksDataAccessor extends AndroidBrowserRepositor
}
// Insert or undelete them if missing.
for (String guid : RepoUtils.SPECIAL_GUIDS) {
for (String guid : specialGUIDs) {
if (statuses.containsKey(guid)) {
if (statuses.get(guid)) {
// Undelete.
Log.i(LOG_TAG, "Undeleting special GUID " + guid);
Logger.info(LOG_TAG, "Undeleting special GUID " + guid);
ContentValues cv = new ContentValues();
cv.put(BrowserContract.SyncColumns.IS_DELETED, 0);
updateByGuid(guid, cv);
@ -135,12 +175,15 @@ public class AndroidBrowserBookmarksDataAccessor extends AndroidBrowserRepositor
} else {
// Insert.
if (guid.equals("mobile")) {
Log.i(LOG_TAG, "No mobile folder. Inserting one.");
Logger.info(LOG_TAG, "No mobile folder. Inserting one.");
mobileRoot = insertSpecialFolder("mobile", 0);
} else if (guid.equals("places")) {
// This is awkward.
Logger.info(LOG_TAG, "No places root. Inserting one under mobile (" + mobileRoot + ").");
desktopRoot = insertSpecialFolder("places", mobileRoot);
} else {
// unfiled, menu, toolbar.
Logger.info(LOG_TAG, "No " + guid + " root. Inserting one under places (" + desktopRoot + ").");
insertSpecialFolder(guid, desktopRoot);
}
}
@ -149,7 +192,7 @@ public class AndroidBrowserBookmarksDataAccessor extends AndroidBrowserRepositor
private long insertSpecialFolder(String guid, long parentId) {
BookmarkRecord record = new BookmarkRecord(guid);
record.title = RepoUtils.SPECIAL_GUIDS_MAP.get(guid);
record.title = AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.get(guid);
record.type = "folder";
record.androidParentID = parentId;
return(RepoUtils.getAndroidIdFromUri(insert(record)));
@ -180,12 +223,31 @@ public class AndroidBrowserBookmarksDataAccessor extends AndroidBrowserRepositor
return cv;
}
// Returns a cursor with any records that list the given androidID as a parent
/**
* Returns a cursor over non-deleted records that list the given androidID as a parent.
*/
public Cursor getChildren(long androidID) throws NullCursorException {
String where = BrowserContract.Bookmarks.PARENT + " = ?";
String[] args = new String[] { String.valueOf(androidID) };
return queryHelper.safeQuery(".getChildren", getAllColumns(), where, args, null);
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() {

View File

@ -1,46 +1,18 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Android Sync Client.
*
* The Initial Developer of the Original Code is
* the Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Jason Voll <jvoll@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/* 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.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import org.json.simple.JSONArray;
import org.mozilla.gecko.R;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.NoGuidForIdException;
@ -61,11 +33,139 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
private HashMap<String, Long> guidToID = new HashMap<String, Long>();
private HashMap<Long, String> idToGuid = new HashMap<Long, String>();
/**
* Some notes on reparenting/reordering.
*
* Fennec stores new items with a high-negative position, because it doesn't care.
* On the other hand, it also doesn't give us any help managing positions.
*
* We can process records and folders in any order, though we'll usually see folders
* first because their sortindex is larger.
*
* We can also see folders that refer to children we haven't seen, and children we
* won't see (perhaps due to a TTL, perhaps due to a limit on our fetch).
*
* And of course folders can refer to local children (including ones that might
* be reconciled into oblivion!), or local children in other folders. And the local
* version of a folder -- which might be a reconciling target, or might not -- can
* have local additions or removals. (That causes complications with on-the-fly
* reordering: we don't know in advance which records will even exist by the end
* of the sync.)
*
* We opt to leave records in a reasonable state as we go, applying reordering/
* reparenting operations whenever possible. A final sequence is applied after all
* incoming records have been handled.
*
* As such, we need to track a bunch of stuff as we go:
*
* For each downloaded folder, the array of children. These will be server GUIDs,
* but not necessarily identical to the remote list: if we download a record and
* it's been locally moved, it must be removed from this child array.
*
* This mapping can be discarded when final reordering has occurred, either on
* store completion or when every child has been seen within this session.
*
* A list of orphans: records whose parent folder does not yet exist. This can be
* trimmed as orphans are reparented.
*
* Mappings from folder GUIDs to folder IDs, so that we can parent items without
* having to look in the DB. Of course, this must be kept up-to-date as we
* reconcile.
*
* Reordering also needs to occur during fetch. That is, a folder might have been
* created locally, or modified locally without any remote changes. An order must
* be generated for the folder's children array, and it must be persisted into the
* database to act as a starting point for future changes. But of course we don't
* want to incur a database write if the children already have a satisfactory order.
*
* Do we also need a list of "adopters", parents that are still waiting for children?
* As items get picked out of the orphans list, we can do on-the-fly ordering, until
* we're left with lonely records at the end.
*
* As we modify local folders, perhaps by moving children out of their purview, we
* must bump their modification time so as to cause them to be uploaded on the next
* stage of syncing. The same applies to simple reordering.
*/
// TODO: can we guarantee serial access to these?
private HashMap<String, ArrayList<String>> missingParentToChildren = new HashMap<String, ArrayList<String>>();
private HashMap<String, JSONArray> parentToChildArray = new HashMap<String, JSONArray>();
private AndroidBrowserBookmarksDataAccessor dataAccessor;
private HashMap<String, JSONArray> parentToChildArray = new HashMap<String, JSONArray>();
private int needsReparenting = 0;
private AndroidBrowserBookmarksDataAccessor dataAccessor;
/**
* An array of known-special GUIDs.
*/
public static String[] SPECIAL_GUIDS = new String[] {
// Mobile and desktop places roots have to come first.
"mobile",
"places",
"toolbar",
"menu",
"unfiled"
};
/**
* = A note about folder mapping =
*
* Note that _none_ of Places's folders actually have a special GUID. They're all
* randomly generated. Special folders are indicated by membership in the
* moz_bookmarks_roots table, and by having the parent `1`.
*
* Additionally, the mobile root is annotated. In Firefox Sync, PlacesUtils is
* used to find the IDs of these special folders.
*
* Sync skips over `places` and `tags` when finding IDs.
*
* We need to consume records with these various guids, producing a local
* representation which we are able to stably map upstream.
*
* That is:
*
* * We should not upload a `places` record or a `tags` record.
* * We can stably _store_ menu/toolbar/unfiled/mobile as special GUIDs, and set
* their parent ID as appropriate on upload.
*
*
* = Places folders =
*
* guid root_name folder_id parent
* ---------- ---------- ---------- ----------
* ? places 1 0
* ? menu 2 1
* ? toolbar 3 1
* ? tags 4 1
* ? unfiled 5 1
*
* ? mobile* 474 1
*
*
* = Fennec folders =
*
* guid folder_id parent
* ---------- ---------- ----------
* mobile ? 0
*
*/
public static final Map<String, String> SPECIAL_GUID_PARENTS;
static {
HashMap<String, String> m = new HashMap<String, String>();
m.put("places", null);
m.put("menu", "places");
m.put("toolbar", "places");
m.put("tags", "places");
m.put("unfiled", "places");
m.put("mobile", "places");
SPECIAL_GUID_PARENTS = Collections.unmodifiableMap(m);
}
/**
* A map of guids to their localized name strings.
*/
// Oh, if only we could make this final and initialize it in the static initializer.
public static Map<String, String> SPECIAL_GUIDS_MAP;
/**
* Return true if the provided record GUID should be skipped
* in child lists or fetch results.
@ -81,7 +181,17 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
public AndroidBrowserBookmarksRepositorySession(Repository repository, Context context) {
super(repository);
RepoUtils.initialize(context);
if (SPECIAL_GUIDS_MAP == null) {
HashMap<String, String> m = new HashMap<String, String>();
m.put("menu", context.getString(R.string.bookmarks_folder_menu));
m.put("places", context.getString(R.string.bookmarks_folder_places));
m.put("toolbar", context.getString(R.string.bookmarks_folder_toolbar));
m.put("unfiled", context.getString(R.string.bookmarks_folder_unfiled));
m.put("mobile", context.getString(R.string.bookmarks_folder_mobile));
SPECIAL_GUIDS_MAP = Collections.unmodifiableMap(m);
}
dbHelper = new AndroidBrowserBookmarksDataAccessor(context);
dataAccessor = (AndroidBrowserBookmarksDataAccessor) dbHelper;
}
@ -96,6 +206,15 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
return guid;
}
private long getIDForGUID(String guid) {
Long id = guidToID.get(guid);
if (id == null) {
Logger.warn(LOG_TAG, "Couldn't find local ID for GUID " + guid);
return -1;
}
return id.longValue();
}
private String getGUID(Cursor cur) {
return RepoUtils.getStringFromCursor(cur, "guid");
}
@ -104,12 +223,20 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
return RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks.PARENT);
}
// More efficient for bulk operations.
private long getPosition(Cursor cur, int positionIndex) {
return cur.getLong(positionIndex);
}
private long getPosition(Cursor cur) {
return RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks.POSITION);
}
private String getParentName(String parentGUID) throws ParentNotFoundException, NullCursorException {
if (parentGUID == null) {
return "";
}
if (RepoUtils.SPECIAL_GUIDS_MAP.containsKey(parentGUID)) {
return RepoUtils.SPECIAL_GUIDS_MAP.get(parentGUID);
if (SPECIAL_GUIDS_MAP.containsKey(parentGUID)) {
return SPECIAL_GUIDS_MAP.get(parentGUID);
}
// Get parent name from database.
@ -130,74 +257,116 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
return parentName;
}
/**
* Retrieve the child array for a record, repositioning and updating the database as necessary.
*
* @param folderID
* The database ID of the folder.
* @param persist
* True if generated positions should be written to the database. The modified
* time of the parent folder is only bumped if this is true.
* @return
* An array of GUIDs.
* @throws NullCursorException
*/
@SuppressWarnings("unchecked")
private JSONArray getChildren(long androidID) throws NullCursorException {
trace("Calling getChildren for androidID " + androidID);
private JSONArray getChildrenArray(long folderID, boolean persist) throws NullCursorException {
trace("Calling getChildren for androidID " + folderID);
JSONArray childArray = new JSONArray();
Cursor children = dataAccessor.getChildren(androidID);
Cursor children = dataAccessor.getChildren(folderID);
try {
if (!children.moveToFirst()) {
trace("No children: empty cursor.");
return childArray;
}
final int positionIndex = children.getColumnIndex(BrowserContract.Bookmarks.POSITION);
final int count = children.getCount();
Logger.debug(LOG_TAG, "Expecting " + count + " children.");
int count = children.getCount();
String[] kids = new String[count];
trace("Expecting " + count + " children.");
// Sorted by requested position.
TreeMap<Long, ArrayList<String>> guids = new TreeMap<Long, ArrayList<String>>();
// Track badly positioned records.
// TODO: use a mechanism here that preserves ordering.
HashMap<String, Long> broken = new HashMap<String, Long>();
// Get children into array in correct order.
while (!children.isAfterLast()) {
String childGuid = getGUID(children);
final String childGuid = getGUID(children);
final long childPosition = getPosition(children, positionIndex);
trace(" Child GUID: " + childGuid);
int childPosition = (int) RepoUtils.getLongFromCursor(children, BrowserContract.Bookmarks.POSITION);
trace(" Child position: " + childPosition);
if (childPosition >= count) {
Logger.warn(LOG_TAG, "Child position " + childPosition + " greater than expected children " + count);
broken.put(childGuid, 0L);
} else {
String existing = kids[childPosition];
if (existing != null) {
Logger.warn(LOG_TAG, "Child position " + childPosition + " already occupied! (" +
childGuid + ", " + existing + ")");
broken.put(childGuid, 0L);
} else {
kids[childPosition] = childGuid;
}
}
Utils.addToIndexBucketMap(guids, Math.abs(childPosition), childGuid);
children.moveToNext();
}
try {
Utils.fillArraySpaces(kids, broken);
} catch (Exception e) {
Logger.error(LOG_TAG, "Unable to reposition children to yield a valid sequence. Data loss may result.", e);
}
// TODO: now use 'broken' to edit the records on disk.
// This will suffice for taking a jumble of records and indices and
// producing a sorted sequence that preserves some kind of order --
// from the abs of the position, falling back on cursor order (that
// is, creation time and ID).
// Note that this code is not intended to merge values from two sources!
boolean changed = false;
int i = 0;
for (Entry<Long, ArrayList<String>> entry : guids.entrySet()) {
long pos = entry.getKey().longValue();
int atPos = entry.getValue().size();
// Collect into a more friendly data structure.
for (int i = 0; i < count; ++i) {
String kid = kids[i];
if (forbiddenGUID(kid)) {
continue;
// If every element has a different index, and the indices are
// in strict natural order, then changed will be false.
if (atPos > 1 || pos != i) {
changed = true;
}
for (String guid : entry.getValue()) {
if (!forbiddenGUID(guid)) {
childArray.add(guid);
}
}
childArray.add(kid);
}
if (Logger.logVerbose(LOG_TAG)) {
// Don't JSON-encode unless we're logging.
Logger.trace(LOG_TAG, "Output child array: " + childArray.toJSONString());
}
if (!changed) {
Logger.debug(LOG_TAG, "Nothing moved! Database reflects child array.");
return childArray;
}
if (!persist) {
return childArray;
}
Logger.debug(LOG_TAG, "Generating child array required moving records. Updating DB.");
final long time = now();
if (0 < dataAccessor.updatePositions(childArray)) {
Logger.debug(LOG_TAG, "Bumping parent time to " + time + ".");
dataAccessor.bumpModified(folderID, time);
}
} finally {
children.close();
}
return childArray;
}
protected static boolean isDeleted(Cursor cur) {
return RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) != 0;
}
@Override
protected Record recordFromMirrorCursor(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
protected Record retrieveDuringStore(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
// During storing of a retrieved record, we never care about the children
// array that's already present in the database -- we don't use it for
// reconciling. Skip all that effort for now.
return retrieveRecord(cur, false);
}
@Override
protected Record retrieveDuringFetch(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
return retrieveRecord(cur, true);
}
/**
* Build a record from a cursor, with a flag to dictate whether the
* children array should be computed and written back into the database.
*/
protected BookmarkRecord retrieveRecord(Cursor cur, boolean computeAndPersistChildren) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
String recordGUID = getGUID(cur);
Logger.trace(LOG_TAG, "Record from mirror cursor: " + recordGUID);
@ -206,8 +375,20 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
return null;
}
long androidParentID = getParentID(cur);
String androidParentGUID = getGUIDForID(androidParentID);
// Short-cut for deleted items.
if (isDeleted(cur)) {
return AndroidBrowserBookmarksRepositorySession.bookmarkFromMirrorCursor(cur, null, null, null);
}
long androidParentID = getParentID(cur);
// Ensure special folders stay in the right place.
String androidParentGUID = SPECIAL_GUID_PARENTS.get(recordGUID);
if (androidParentGUID == null) {
androidParentGUID = getGUIDForID(androidParentID);
}
boolean needsReparenting = false;
if (androidParentGUID == null) {
Logger.debug(LOG_TAG, "No parent GUID for record " + recordGUID + " with parent " + androidParentID);
@ -216,25 +397,64 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
Logger.error(LOG_TAG, "Have the parent android ID for the record but the parent's GUID wasn't found.");
throw new NoGuidForIdException(null);
}
// We have a parent ID but it's wrong. If the record is deleted,
// we'll just say that it was in the Unsorted Bookmarks folder.
// If not, we'll move it into Mobile Bookmarks.
needsReparenting = true;
}
// If record is a folder, build out the children array.
JSONArray childArray = getChildArrayForCursor(cur, recordGUID);
// If record is a folder, and we want to see children at this time, then build out the children array.
final JSONArray childArray;
if (computeAndPersistChildren) {
childArray = getChildrenArrayForRecordCursor(cur, recordGUID, true);
} else {
childArray = null;
}
String parentName = getParentName(androidParentGUID);
return RepoUtils.bookmarkFromMirrorCursor(cur, androidParentGUID, parentName, childArray);
BookmarkRecord bookmark = AndroidBrowserBookmarksRepositorySession.bookmarkFromMirrorCursor(cur, androidParentGUID, parentName, childArray);
if (needsReparenting) {
Logger.warn(LOG_TAG, "Bookmark record " + recordGUID + " has a bad parent pointer. Reparenting now.");
String destination = bookmark.deleted ? "unfiled" : "mobile";
bookmark.androidParentID = getIDForGUID(destination);
bookmark.androidPosition = getPosition(cur);
bookmark.parentID = destination;
bookmark.parentName = getParentName(destination);
if (!bookmark.deleted) {
// Actually move it.
// TODO: compute position. Persist.
relocateBookmark(bookmark);
}
}
return bookmark;
}
protected JSONArray getChildArrayForCursor(Cursor cur, String recordGUID) throws NullCursorException {
JSONArray childArray = null;
/**
* Ensure that the local database row for the provided bookmark
* reflects this record's parent information.
*
* @param bookmark
*/
private void relocateBookmark(BookmarkRecord bookmark) {
dataAccessor.updateParentAndPosition(bookmark.guid, bookmark.androidParentID, bookmark.androidPosition);
}
protected JSONArray getChildrenArrayForRecordCursor(Cursor cur, String recordGUID, boolean persist) throws NullCursorException {
boolean isFolder = rowIsFolder(cur);
Logger.debug(LOG_TAG, "Record " + recordGUID + " is a " + (isFolder ? "folder." : "bookmark."));
if (isFolder) {
long androidID = guidToID.get(recordGUID);
childArray = getChildren(androidID);
if (!isFolder) {
return null;
}
if (childArray != null) {
Logger.debug(LOG_TAG, "Fetched " + childArray.size() + " children for " + recordGUID);
long androidID = guidToID.get(recordGUID);
JSONArray childArray = getChildrenArray(androidID, persist);
if (childArray == null) {
return null;
}
Logger.debug(LOG_TAG, "Fetched " + childArray.size() + " children for " + recordGUID);
return childArray;
}
@ -275,7 +495,11 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
// hairy stuff. Here's the setup for it.
Logger.debug(LOG_TAG, "Preparing folder ID mappings.");
idToGuid.put(0L, "places"); // Fake our root.
// Fake our root.
Logger.debug(LOG_TAG, "Tracking places root as ID 0.");
idToGuid.put(0L, "places");
guidToID.put("places", 0L);
try {
cur.moveToFirst();
while (!cur.isAfterLast()) {
@ -308,33 +532,27 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
};
@Override
@SuppressWarnings("unchecked")
protected Record reconcileRecords(Record remoteRecord, Record localRecord,
long lastRemoteRetrieval,
long lastLocalRetrieval) {
BookmarkRecord reconciled = (BookmarkRecord) super.reconcileRecords(remoteRecord, localRecord,
lastRemoteRetrieval,
lastLocalRetrieval);
// For now we *always* use the remote record's children array as a starting point.
// We won't write it into the database yet; we'll record it and process as we go.
reconciled.children = ((BookmarkRecord) remoteRecord).children;
return reconciled;
}
@Override
protected Record prepareRecord(Record record) {
BookmarkRecord bmk = (BookmarkRecord) record;
// Check if parent exists
if (guidToID.containsKey(bmk.parentID)) {
bmk.androidParentID = guidToID.get(bmk.parentID);
JSONArray children = parentToChildArray.get(bmk.parentID);
if (children != null) {
if (!children.contains(bmk.guid)) {
children.add(bmk.guid);
parentToChildArray.put(bmk.parentID, children);
}
bmk.androidPosition = children.indexOf(bmk.guid);
}
}
else {
bmk.androidParentID = guidToID.get("unfiled");
ArrayList<String> children;
if (missingParentToChildren.containsKey(bmk.parentID)) {
children = missingParentToChildren.get(bmk.parentID);
} else {
children = new ArrayList<String>();
}
children.add(bmk.guid);
needsReparenting++;
missingParentToChildren.put(bmk.parentID, children);
if (!isSpecialRecord(record)) {
// We never want to reparent special records.
handleParenting(bmk);
}
if (Logger.LOG_PERSONAL_INFORMATION) {
@ -363,36 +581,162 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
return bmk;
}
/**
* If the provided record doesn't have correct parent information,
* update appropriate bookkeeping to improve the situation.
*
* @param bmk
*/
private void handleParenting(BookmarkRecord bmk) {
if (guidToID.containsKey(bmk.parentID)) {
bmk.androidParentID = guidToID.get(bmk.parentID);
// Might as well set a basic position from the downloaded children array.
JSONArray children = parentToChildArray.get(bmk.parentID);
if (children != null) {
int index = children.indexOf(bmk.guid);
if (index >= 0) {
bmk.androidPosition = index;
}
}
}
else {
bmk.androidParentID = guidToID.get("unfiled");
ArrayList<String> children;
if (missingParentToChildren.containsKey(bmk.parentID)) {
children = missingParentToChildren.get(bmk.parentID);
} else {
children = new ArrayList<String>();
}
children.add(bmk.guid);
needsReparenting++;
missingParentToChildren.put(bmk.parentID, children);
}
}
private boolean isSpecialRecord(Record record) {
return SPECIAL_GUID_PARENTS.containsKey(record.guid);
}
@Override
@SuppressWarnings("unchecked")
protected void updateBookkeeping(Record record) throws NoGuidForIdException,
NullCursorException,
ParentNotFoundException {
super.updateBookkeeping(record);
BookmarkRecord bmk = (BookmarkRecord) record;
// If record is folder, update maps and re-parent children if necessary
if (bmk.type.equalsIgnoreCase(AndroidBrowserBookmarksDataAccessor.TYPE_FOLDER)) {
guidToID.put(bmk.guid, bmk.androidID);
idToGuid.put(bmk.androidID, bmk.guid);
JSONArray childArray = bmk.children;
// Re-parent.
if (missingParentToChildren.containsKey(bmk.guid)) {
for (String child : missingParentToChildren.get(bmk.guid)) {
long position;
if (!bmk.children.contains(child)) {
childArray.add(child);
}
position = childArray.indexOf(child);
dataAccessor.updateParentAndPosition(child, bmk.androidID, position);
needsReparenting--;
}
missingParentToChildren.remove(bmk.guid);
}
parentToChildArray.put(bmk.guid, childArray);
// If record is folder, update maps and re-parent children if necessary.
if (!bmk.isFolder()) {
Logger.debug(LOG_TAG, "Not a folder. No bookkeeping.");
return;
}
Logger.debug(LOG_TAG, "Updating bookkeeping for folder " + record.guid);
// Mappings between ID and GUID.
// TODO: update our persisted children arrays!
// TODO: if our Android ID just changed, replace parents for all of our children.
guidToID.put(bmk.guid, bmk.androidID);
idToGuid.put(bmk.androidID, bmk.guid);
JSONArray childArray = bmk.children;
if (Logger.logVerbose(LOG_TAG)) {
Logger.trace(LOG_TAG, bmk.guid + " has children " + childArray.toJSONString());
}
parentToChildArray.put(bmk.guid, childArray);
// Re-parent.
if (missingParentToChildren.containsKey(bmk.guid)) {
for (String child : missingParentToChildren.get(bmk.guid)) {
// This might return -1; that's OK, the bookmark will
// be properly repositioned later.
long position = childArray.indexOf(child);
dataAccessor.updateParentAndPosition(child, bmk.androidID, position);
needsReparenting--;
}
missingParentToChildren.remove(bmk.guid);
}
}
@Override
protected void storeRecordDeletion(final Record record) {
if (SPECIAL_GUIDS_MAP.containsKey(record.guid)) {
Logger.debug(LOG_TAG, "Told to delete record " + record.guid + ". Ignoring.");
return;
}
final BookmarkRecord bookmarkRecord = (BookmarkRecord) record;
if (bookmarkRecord.isFolder()) {
Logger.debug(LOG_TAG, "Deleting folder. Ensuring consistency of children.");
handleFolderDeletion(bookmarkRecord);
return;
}
super.storeRecordDeletion(record);
}
/**
* When a folder deletion is received, we must ensure -- for database
* consistency -- that its children are placed somewhere sane.
*
* Note that its children might also be deleted, but we'll process
* folders first. For that reason we might want to queue up these
* folder deletions and handle them in onStoreDone.
*
* See Bug 724739.
*
* @param folder
*/
protected void handleFolderDeletion(final BookmarkRecord folder) {
// TODO: reparent children. Bug 724740.
// For now we'll trust that we'll process the item deletions, too.
super.storeRecordDeletion(folder);
}
@SuppressWarnings("unchecked")
private void finishUp() {
try {
Logger.debug(LOG_TAG, "Have " + parentToChildArray.size() + " folders whose children might need repositioning.");
for (Entry<String, JSONArray> entry : parentToChildArray.entrySet()) {
String guid = entry.getKey();
JSONArray onServer = entry.getValue();
try {
final long folderID = getIDForGUID(guid);
JSONArray inDB = getChildrenArray(folderID, false);
int added = 0;
for (Object o : inDB) {
if (!onServer.contains(o)) {
onServer.add(o);
added++;
}
}
Logger.debug(LOG_TAG, "Added " + added + " items locally.");
dataAccessor.updatePositions(new ArrayList<String>(onServer));
dataAccessor.bumpModified(folderID, now());
// Wow, this is spectacularly wasteful.
Logger.debug(LOG_TAG, "Untracking " + guid);
final Record record = retrieveByGUIDDuringStore(guid);
if (record == null) {
return;
}
untrackRecord(record);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Error repositioning children for " + guid, e);
}
}
} finally {
super.storeDone();
}
}
@Override
public void storeDone() {
Runnable command = new Runnable() {
@Override
public void run() {
finishUp();
}
};
storeWorkQueue.execute(command);
}
@Override
@ -400,4 +744,101 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
BookmarkRecord bmk = (BookmarkRecord) record;
return bmk.title + bmk.bookmarkURI + bmk.type + bmk.parentName;
}
public static BookmarkRecord computeParentFields(BookmarkRecord rec, String suggestedParentGUID, String suggestedParentName) {
final String guid = rec.guid;
if (guid == null) {
// Oh dear.
Logger.error(LOG_TAG, "No guid in computeParentFields!");
return null;
}
String realParent = SPECIAL_GUID_PARENTS.get(guid);
if (realParent == null) {
// No magic parent. Use whatever the caller suggests.
realParent = suggestedParentGUID;
} else {
Logger.debug(LOG_TAG, "Ignoring suggested parent ID " + suggestedParentGUID +
" for " + guid + "; using " + realParent);
}
if (realParent == null) {
// Oh dear.
Logger.error(LOG_TAG, "No parent for record " + guid);
return null;
}
// Always set the parent name for special folders back to default.
String parentName = SPECIAL_GUIDS_MAP.get(realParent);
if (parentName == null) {
parentName = suggestedParentName;
}
rec.parentID = realParent;
rec.parentName = parentName;
return rec;
}
private static BookmarkRecord logBookmark(BookmarkRecord rec) {
try {
Logger.debug(LOG_TAG, "Returning " + (rec.deleted ? "deleted " : "") +
"bookmark record " + rec.guid + " (" + rec.androidID +
", parent " + rec.parentID + ")");
if (!rec.deleted && Logger.LOG_PERSONAL_INFORMATION) {
Logger.pii(LOG_TAG, "> Parent name: " + rec.parentName);
Logger.pii(LOG_TAG, "> Title: " + rec.title);
Logger.pii(LOG_TAG, "> Type: " + rec.type);
Logger.pii(LOG_TAG, "> URI: " + rec.bookmarkURI);
Logger.pii(LOG_TAG, "> Android position: " + rec.androidPosition);
Logger.pii(LOG_TAG, "> Position: " + rec.pos);
if (rec.isFolder()) {
Logger.pii(LOG_TAG, "FOLDER: Children are " +
(rec.children == null ?
"null" :
rec.children.toJSONString()));
}
}
} catch (Exception e) {
Logger.debug(LOG_TAG, "Exception logging bookmark record " + rec, e);
}
return rec;
}
// Create a BookmarkRecord object from a cursor on a row containing a Fennec bookmark.
public static BookmarkRecord bookmarkFromMirrorCursor(Cursor cur, String parentGUID, String parentName, JSONArray children) {
final String collection = "bookmarks";
final String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
final long lastModified = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.DATE_MODIFIED);
final boolean deleted = isDeleted(cur);
BookmarkRecord rec = new BookmarkRecord(guid, collection, lastModified, deleted);
// No point in populating it.
if (deleted) {
return logBookmark(rec);
}
boolean isFolder = RepoUtils.getIntFromCursor(cur, BrowserContract.Bookmarks.IS_FOLDER) == 1;
rec.title = RepoUtils.getStringFromCursor(cur, BrowserContract.Bookmarks.TITLE);
rec.bookmarkURI = RepoUtils.getStringFromCursor(cur, BrowserContract.Bookmarks.URL);
rec.description = RepoUtils.getStringFromCursor(cur, BrowserContract.Bookmarks.DESCRIPTION);
rec.tags = RepoUtils.getJSONArrayFromCursor(cur, BrowserContract.Bookmarks.TAGS);
rec.keyword = RepoUtils.getStringFromCursor(cur, BrowserContract.Bookmarks.KEYWORD);
rec.type = isFolder ? AndroidBrowserBookmarksDataAccessor.TYPE_FOLDER :
AndroidBrowserBookmarksDataAccessor.TYPE_BOOKMARK;
rec.androidID = RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks._ID);
rec.androidPosition = RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks.POSITION);
rec.children = children;
// Need to restore the parentId since it isn't stored in content provider.
// We also take this opportunity to fix up parents for special folders,
// allowing us to map between the hierarchies used by Fennec and Places.
BookmarkRecord withParentFields = computeParentFields(rec, parentGUID, parentName);
if (withParentFields == null) {
// Oh dear. Something went wrong.
return null;
}
return logBookmark(withParentFields);
}
}

View File

@ -27,7 +27,12 @@ public class AndroidBrowserHistoryRepositorySession extends AndroidBrowserReposi
}
@Override
protected Record recordFromMirrorCursor(Cursor cur) {
protected Record retrieveDuringStore(Cursor cur) {
return RepoUtils.historyFromMirrorCursor(cur);
}
@Override
protected Record retrieveDuringFetch(Cursor cur) {
return RepoUtils.historyFromMirrorCursor(cur);
}

View File

@ -20,7 +20,12 @@ public class AndroidBrowserPasswordsRepositorySession extends
}
@Override
protected Record recordFromMirrorCursor(Cursor cur) {
protected Record retrieveDuringStore(Cursor cur) {
return RepoUtils.passwordFromMirrorCursor(cur);
}
@Override
protected Record retrieveDuringFetch(Cursor cur) {
return RepoUtils.passwordFromMirrorCursor(cur);
}

View File

@ -38,6 +38,7 @@
package org.mozilla.gecko.sync.repositories.android;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.domain.Record;
@ -45,7 +46,6 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
public abstract class AndroidBrowserRepositoryDataAccessor {
@ -68,7 +68,7 @@ public abstract class AndroidBrowserRepositoryDataAccessor {
}
public void wipe() {
Log.i(LOG_TAG, "wiping: " + getUri());
Logger.info(LOG_TAG, "wiping: " + getUri());
String where = BrowserContract.SyncColumns.GUID + " NOT IN ('mobile')";
context.getContentResolver().delete(getUri(), where, null);
}
@ -98,7 +98,7 @@ public abstract class AndroidBrowserRepositoryDataAccessor {
if (deleted == 1) {
return;
}
Log.w(LOG_TAG, "Unexpectedly deleted " + deleted + " rows for guid " + guid);
Logger.warn(LOG_TAG, "Unexpectedly deleted " + deleted + " rows for guid " + guid);
}
public void update(String guid, Record newRecord) {
@ -107,13 +107,12 @@ public abstract class AndroidBrowserRepositoryDataAccessor {
ContentValues cv = getContentValues(newRecord);
int updated = context.getContentResolver().update(getUri(), cv, where, args);
if (updated != 1) {
Log.w(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + guid);
Logger.warn(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + guid);
}
}
public Uri insert(Record record) {
ContentValues cv = getContentValues(record);
Log.d(LOG_TAG, "INSERTING: " + cv.getAsString("guid"));
return context.getContentResolver().insert(getUri(), cv);
}
@ -138,9 +137,9 @@ public abstract class AndroidBrowserRepositoryDataAccessor {
*/
public Cursor getGUIDsSince(long timestamp) throws NullCursorException {
return queryHelper.safeQuery(".getGUIDsSince",
GUID_COLUMNS,
dateModifiedWhere(timestamp),
null, null);
GUID_COLUMNS,
dateModifiedWhere(timestamp),
null, null);
}
/**
@ -153,9 +152,9 @@ public abstract class AndroidBrowserRepositoryDataAccessor {
*/
public Cursor fetchSince(long timestamp) throws NullCursorException {
return queryHelper.safeQuery(".fetchSince",
getAllColumns(),
dateModifiedWhere(timestamp),
null, null);
getAllColumns(),
dateModifiedWhere(timestamp),
null, null);
}
/**
@ -193,7 +192,7 @@ public abstract class AndroidBrowserRepositoryDataAccessor {
if (deleted == 1) {
return;
}
Log.w(LOG_TAG, "Unexpectedly deleted " + deleted + " rows for guid " + record.guid);
Logger.warn(LOG_TAG, "Unexpectedly deleted " + deleted + " rows for guid " + record.guid);
}
public void updateByGuid(String guid, ContentValues cv) {
@ -204,6 +203,6 @@ public abstract class AndroidBrowserRepositoryDataAccessor {
if (updated == 1) {
return;
}
Log.w(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + guid);
Logger.warn(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + guid);
}
}

View File

@ -96,7 +96,9 @@ public abstract class AndroidBrowserRepositorySession extends StoreTrackingRepos
}
/**
* Override this.
* Retrieve a record from a cursor. Act as if we don't know the final contents of
* the record: for example, a folder's child array might change.
*
* Return null if this record should not be processed.
*
* @param cur
@ -105,7 +107,20 @@ public abstract class AndroidBrowserRepositorySession extends StoreTrackingRepos
* @throws NullCursorException
* @throws ParentNotFoundException
*/
protected abstract Record recordFromMirrorCursor(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException;
protected abstract Record retrieveDuringStore(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException;
/**
* Retrieve a record from a cursor. Ensure that the contents of the database are
* updated to match the record that we're constructing: for example, the children
* of a folder might be repositioned as we generate the folder's record.
*
* @param cur
* @return
* @throws NoGuidForIdException
* @throws NullCursorException
* @throws ParentNotFoundException
*/
protected abstract Record retrieveDuringFetch(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException;
// Must be overriden by AndroidBookmarkRepositorySession.
protected boolean checkRecordType(Record record) {
@ -247,7 +262,7 @@ public abstract class AndroidBrowserRepositorySession extends StoreTrackingRepos
return;
}
while (!cursor.isAfterLast()) {
Record r = recordFromMirrorCursor(cursor);
Record r = retrieveDuringFetch(cursor);
if (r != null) {
if (filter == null || !filter.excludeRecord(r)) {
Logger.trace(LOG_TAG, "Processing record " + r.guid);
@ -408,7 +423,7 @@ public abstract class AndroidBrowserRepositorySession extends StoreTrackingRepos
Record existingRecord;
try {
// GUID matching only: deleted records don't have a payload with which to search.
existingRecord = recordForGUID(record.guid);
existingRecord = retrieveByGUIDDuringStore(record.guid);
if (record.deleted) {
if (existingRecord == null) {
// We're done. Don't bother with a callback. That can change later
@ -540,7 +555,18 @@ public abstract class AndroidBrowserRepositorySession extends StoreTrackingRepos
return toStore;
}
protected Record recordForGUID(String guid) throws
/**
* Retrieve a record from the store by GUID, without writing unnecessarily to the
* database.
*
* @param guid
* @return
* @throws NoGuidForIdException
* @throws NullCursorException
* @throws ParentNotFoundException
* @throws MultipleRecordsForGuidException
*/
protected Record retrieveByGUIDDuringStore(String guid) throws
NoGuidForIdException,
NullCursorException,
ParentNotFoundException,
@ -551,7 +577,7 @@ public abstract class AndroidBrowserRepositorySession extends StoreTrackingRepos
return null;
}
Record r = recordFromMirrorCursor(cursor);
Record r = retrieveDuringStore(cursor);
cursor.moveToNext();
if (cursor.isAfterLast()) {
@ -588,7 +614,7 @@ public abstract class AndroidBrowserRepositorySession extends StoreTrackingRepos
String guid = getRecordToGuidMap().get(recordString);
if (guid != null) {
Logger.debug(LOG_TAG, "Found one. Returning computed record.");
return recordForGUID(guid);
return retrieveByGUIDDuringStore(guid);
}
Logger.debug(LOG_TAG, "findExistingRecord failed to find one for " + record.guid);
return null;
@ -604,13 +630,17 @@ public abstract class AndroidBrowserRepositorySession extends StoreTrackingRepos
private void createRecordToGuidMap() throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
Logger.info(LOG_TAG, "BEGIN: creating record -> GUID map.");
recordToGuid = new HashMap<String, String>();
// TODO: we should be able to do this entire thing with string concatenations within SQL.
// Also consider whether it's better to fetch and process every record in the DB into
// memory, or run a query per record to do the same thing.
Cursor cur = dbHelper.fetchAll();
try {
if (!cur.moveToFirst()) {
return;
}
while (!cur.isAfterLast()) {
Record record = recordFromMirrorCursor(cur);
Record record = retrieveDuringStore(cur);
if (record != null) {
recordToGuid.put(buildRecordString(record), record.guid);
}

View File

@ -4,17 +4,11 @@
package org.mozilla.gecko.sync.repositories.android;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.json.simple.JSONArray;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.mozilla.gecko.R;
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.HistoryRecord;
import org.mozilla.gecko.sync.repositories.domain.PasswordRecord;
@ -27,89 +21,6 @@ public class RepoUtils {
private static final String LOG_TAG = "RepoUtils";
/**
* An array of known-special GUIDs.
*/
public static String[] SPECIAL_GUIDS = new String[] {
// Mobile and desktop places roots have to come first.
"mobile",
"places",
"toolbar",
"menu",
"unfiled"
};
/**
* = A note about folder mapping =
*
* Note that _none_ of Places's folders actually have a special GUID. They're all
* randomly generated. Special folders are indicated by membership in the
* moz_bookmarks_roots table, and by having the parent `1`.
*
* Additionally, the mobile root is annotated. In Firefox Sync, PlacesUtils is
* used to find the IDs of these special folders.
*
* Sync skips over `places` and `tags` when finding IDs.
*
* We need to consume records with these various guids, producing a local
* representation which we are able to stably map upstream.
*
* That is:
*
* * We should not upload a `places` record or a `tags` record.
* * We can stably _store_ menu/toolbar/unfiled/mobile as special GUIDs, and set
* their parent ID as appropriate on upload.
*
*
* = Places folders =
*
* guid root_name folder_id parent
* ---------- ---------- ---------- ----------
* ? places 1 0
* ? menu 2 1
* ? toolbar 3 1
* ? tags 4 1
* ? unfiled 5 1
*
* ? mobile* 474 1
*
*
* = Fennec folders =
*
* guid folder_id parent
* ---------- ---------- ----------
* mobile ? 0
*
*/
public static final Map<String, String> SPECIAL_GUID_PARENTS;
static {
HashMap<String, String> m = new HashMap<String, String>();
m.put("places", null);
m.put("menu", "places");
m.put("toolbar", "places");
m.put("tags", "places");
m.put("unfiled", "places");
m.put("mobile", "places");
SPECIAL_GUID_PARENTS = Collections.unmodifiableMap(m);
}
/**
* A map of guids to their localized name strings.
*/
// Oh, if only we could make this final and initialize it in the static initializer.
public static Map<String, String> SPECIAL_GUIDS_MAP;
public static void initialize(Context context) {
if (SPECIAL_GUIDS_MAP == null) {
HashMap<String, String> m = new HashMap<String, String>();
m.put("menu", context.getString(R.string.bookmarks_folder_menu));
m.put("places", context.getString(R.string.bookmarks_folder_places));
m.put("toolbar", context.getString(R.string.bookmarks_folder_toolbar));
m.put("unfiled", context.getString(R.string.bookmarks_folder_unfiled));
m.put("mobile", context.getString(R.string.bookmarks_folder_mobile));
SPECIAL_GUIDS_MAP = Collections.unmodifiableMap(m);
}
}
/**
* A helper class for monotonous SQL querying. Does timing and logging,
* offers a utility to throw on a null cursor.
@ -205,97 +116,6 @@ public class RepoUtils {
return Long.parseLong(path.substring(lastSlash + 1));
}
public static BookmarkRecord computeParentFields(BookmarkRecord rec, String suggestedParentGUID, String suggestedParentName) {
final String guid = rec.guid;
if (guid == null) {
// Oh dear.
Logger.error(LOG_TAG, "No guid in computeParentFields!");
return null;
}
String realParent = SPECIAL_GUID_PARENTS.get(guid);
if (realParent == null) {
// No magic parent. Use whatever the caller suggests.
realParent = suggestedParentGUID;
} else {
Logger.debug(LOG_TAG, "Ignoring suggested parent ID " + suggestedParentGUID +
" for " + guid + "; using " + realParent);
}
if (realParent == null) {
// Oh dear.
Logger.error(LOG_TAG, "No parent for record " + guid);
return null;
}
// Always set the parent name for special folders back to default.
String parentName = SPECIAL_GUIDS_MAP.get(realParent);
if (parentName == null) {
parentName = suggestedParentName;
}
rec.parentID = realParent;
rec.parentName = parentName;
return rec;
}
// Create a BookmarkRecord object from a cursor on a row containing a Fennec bookmark.
public static BookmarkRecord bookmarkFromMirrorCursor(Cursor cur, String parentGUID, String parentName, JSONArray children) {
String guid = getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
String collection = "bookmarks";
long lastModified = getLongFromCursor(cur, BrowserContract.SyncColumns.DATE_MODIFIED);
boolean deleted = getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1 ? true : false;
boolean isFolder = getIntFromCursor(cur, BrowserContract.Bookmarks.IS_FOLDER) == 1;
BookmarkRecord rec = new BookmarkRecord(guid, collection, lastModified, deleted);
rec.title = getStringFromCursor(cur, BrowserContract.Bookmarks.TITLE);
rec.bookmarkURI = getStringFromCursor(cur, BrowserContract.Bookmarks.URL);
rec.description = getStringFromCursor(cur, BrowserContract.Bookmarks.DESCRIPTION);
rec.tags = getJSONArrayFromCursor(cur, BrowserContract.Bookmarks.TAGS);
rec.keyword = getStringFromCursor(cur, BrowserContract.Bookmarks.KEYWORD);
rec.type = isFolder ? AndroidBrowserBookmarksDataAccessor.TYPE_FOLDER :
AndroidBrowserBookmarksDataAccessor.TYPE_BOOKMARK;
rec.androidID = getLongFromCursor(cur, BrowserContract.Bookmarks._ID);
rec.androidPosition = getLongFromCursor(cur, BrowserContract.Bookmarks.POSITION);
rec.children = children;
// Need to restore the parentId since it isn't stored in content provider.
// We also take this opportunity to fix up parents for special folders,
// allowing us to map between the hierarchies used by Fennec and Places.
BookmarkRecord withParentFields = computeParentFields(rec, parentGUID, parentName);
if (withParentFields == null) {
// Oh dear. Something went wrong.
return null;
}
return logBookmark(withParentFields);
}
private static BookmarkRecord logBookmark(BookmarkRecord rec) {
try {
Logger.debug(LOG_TAG, "Returning bookmark record " + rec.guid + " (" + rec.androidID +
", parent " + rec.parentID + ")");
if (Logger.LOG_PERSONAL_INFORMATION) {
Logger.pii(LOG_TAG, "> Parent name: " + rec.parentName);
Logger.pii(LOG_TAG, "> Title: " + rec.title);
Logger.pii(LOG_TAG, "> Type: " + rec.type);
Logger.pii(LOG_TAG, "> URI: " + rec.bookmarkURI);
Logger.pii(LOG_TAG, "> Android position: " + rec.androidPosition);
Logger.pii(LOG_TAG, "> Position: " + rec.pos);
if (rec.isFolder()) {
Logger.pii(LOG_TAG, "FOLDER: Children are " +
(rec.children == null ?
"null" :
rec.children.toJSONString()));
}
}
} catch (Exception e) {
Logger.debug(LOG_TAG, "Exception logging bookmark record " + rec, e);
}
return rec;
}
//Create a HistoryRecord object from a cursor on a row with a Moz History record in it
public static HistoryRecord historyFromMirrorCursor(Cursor cur) {