@Override public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException { // Check for the existence of special folders // and insert them if they don't exist. Cursor cur; try { Logger.debug(LOG_TAG, "Check and build special GUIDs."); dataAccessor.checkAndBuildSpecialGuids(); cur = dataAccessor.getGuidsIDsForFolders(); Logger.debug(LOG_TAG, "Got GUIDs for folders."); } catch (android.database.sqlite.SQLiteConstraintException e) { Logger.error(LOG_TAG, "Got sqlite constraint exception working with Fennec bookmark DB.", e); delegate.onBeginFailed(e); return; } catch (NullCursorException e) { delegate.onBeginFailed(e); return; } catch (Exception e) { delegate.onBeginFailed(e); return; } // To deal with parent mapping of bookmarks we have to do some // hairy stuff. Here's the setup for it. Logger.debug(LOG_TAG, "Preparing folder ID mappings."); // Fake our root. Logger.debug(LOG_TAG, "Tracking places root as ID 0."); parentIDToGuidMap.put(0L, "places"); parentGuidToIDMap.put("places", 0L); try { cur.moveToFirst(); while (!cur.isAfterLast()) { String guid = getGUID(cur); long id = RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks._ID); parentGuidToIDMap.put(guid, id); parentIDToGuidMap.put(id, guid); Logger.debug(LOG_TAG, "GUID " + guid + " maps to " + id); cur.moveToNext(); } } finally { cur.close(); } deletionManager = new BookmarksDeletionManager(dataAccessor, DEFAULT_DELETION_FLUSH_THRESHOLD); // We just crawled the database enumerating all folders; we'll start the // insertion manager with exactly these folders as the known parents (the // collection is copied) in the manager constructor. insertionManager = new BookmarksInsertionManager( DEFAULT_INSERTION_FLUSH_THRESHOLD, parentGuidToIDMap.keySet(), this); Logger.debug(LOG_TAG, "Done with initial setup of bookmarks session."); super.begin(delegate); }
@SuppressWarnings("unchecked") private void finishUp() { try { flushQueues(); 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); final JSONArray inDB = new JSONArray(); final boolean clean = getChildrenArray(folderID, false, inDB); final boolean sameArrays = Utils.sameArrays(onServer, inDB); // If the local children and the remote children are already // the same, then we don't need to bump the modified time of the // parent: we wouldn't upload a different record, so avoid the cycle. if (!sameArrays) { int added = 0; for (Object o : inDB) { if (!onServer.contains(o)) { onServer.add(o); added++; } } Logger.debug(LOG_TAG, "Added " + added + " items locally."); Logger.debug(LOG_TAG, "Untracking and bumping " + guid + "(" + folderID + ")"); dataAccessor.bumpModified(folderID, now()); untrackGUID(guid); } // If the arrays are different, or they're the same but not flushed to disk, // write them out now. if (!sameArrays || !clean) { dataAccessor.updatePositions(new ArrayList<String>(onServer)); } } catch (Exception e) { Logger.warn(LOG_TAG, "Error repositioning children for " + guid, e); } } } finally { super.storeDone(); } }
private String getParentName(String parentGUID) throws ParentNotFoundException, NullCursorException { if (parentGUID == null) { return ""; } if (SPECIAL_GUIDS_MAP.containsKey(parentGUID)) { return SPECIAL_GUIDS_MAP.get(parentGUID); } // Get parent name from database. String parentName = ""; Cursor name = dataAccessor.fetch(new String[] {parentGUID}); try { name.moveToFirst(); if (!name.isAfterLast()) { parentName = RepoUtils.getStringFromCursor(name, BrowserContract.Bookmarks.TITLE); } else { Logger.error( LOG_TAG, "Couldn't find record with guid '" + parentGUID + "' when looking for parent name."); throw new ParentNotFoundException(null); } } finally { name.close(); } return parentName; }
/** Implement method of BookmarksInsertionManager.BookmarkInserter. */ @Override public void bulkInsertNonFolders(Collection<BookmarkRecord> records) { // All of these records are *not* deleted and *not* folders, so we don't // need to update androidID at all! // TODO: persist records that fail to insert for later retry. ArrayList<Record> toStores = new ArrayList<Record>(records.size()); for (Record record : records) { toStores.add(prepareRecord(record)); } try { int stored = dataAccessor.bulkInsert(toStores); if (stored != toStores.size()) { // Something failed; most pessimistic action is to declare that all insertions failed. // TODO: perform the bulkInsert in a transaction and rollback unless all insertions succeed? for (Record failed : toStores) { delegate.onRecordStoreFailed( new RuntimeException( "Possibly failed to bulkInsert non-folder with guid " + failed.guid + "."), failed.guid); } return; } } catch (NullCursorException e) { for (Record failed : toStores) { delegate.onRecordStoreFailed(e, failed.guid); } return; } // Success For All! for (Record succeeded : toStores) { try { updateBookkeeping(succeeded); } catch (Exception e) { Logger.warn( LOG_TAG, "Got exception updating bookkeeping of non-folder with guid " + succeeded.guid + ".", e); } trackRecord(succeeded); delegate.onRecordStoreSucceeded(succeeded.guid); } }
@Override 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.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. parentGuidToIDMap.put(bmk.guid, bmk.androidID); parentIDToGuidMap.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); } }
/** * 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); }
/** * 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. * @param childArray A new, empty JSONArray which will be populated with an array of GUIDs. * @return True if the resulting array is "clean" (i.e., reflects the content of the database). * @throws NullCursorException */ @SuppressWarnings("unchecked") private boolean getChildrenArray(long folderID, boolean persist, JSONArray childArray) throws NullCursorException { trace("Calling getChildren for androidID " + folderID); Cursor children = dataAccessor.getChildren(folderID); try { if (!children.moveToFirst()) { trace("No children: empty cursor."); return true; } final int positionIndex = children.getColumnIndex(BrowserContract.Bookmarks.POSITION); final int count = children.getCount(); Logger.debug(LOG_TAG, "Expecting " + count + " children."); // Sorted by requested position. TreeMap<Long, ArrayList<String>> guids = new TreeMap<Long, ArrayList<String>>(); while (!children.isAfterLast()) { final String childGuid = getGUID(children); final long childPosition = getPosition(children, positionIndex); trace(" Child GUID: " + childGuid); trace(" Child position: " + childPosition); Utils.addToIndexBucketMap(guids, Math.abs(childPosition), childGuid); children.moveToNext(); } // 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(); // 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; } ++i; for (String guid : entry.getValue()) { if (!forbiddenGUID(guid)) { childArray.add(guid); } } } 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 true; } if (!persist) { Logger.debug(LOG_TAG, "Returned array does not match database, and not persisting."); return false; } 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); } return true; } finally { children.close(); } }