@Override
 public void handleSuccess(GlobalSession globalSession) {
   Logger.info(LOG_TAG, "GlobalSession indicated success.");
   Logger.debug(LOG_TAG, "Prefs target: " + globalSession.config.prefsPath);
   globalSession.config.persistToPrefs();
   notifyMonitor();
 }
Example #2
0
  /**
   * Create a HistoryRecord object from a cursor row.
   *
   * @return a HistoryRecord, or null if this row would produce an invalid record (e.g., with a null
   *     URI or no visits).
   */
  public static HistoryRecord historyFromMirrorCursor(Cursor cur) {
    final String guid = getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
    if (guid == null) {
      Logger.debug(LOG_TAG, "Skipping history record with null GUID.");
      return null;
    }

    final String historyURI = getStringFromCursor(cur, BrowserContract.History.URL);
    if (!isValidHistoryURI(historyURI)) {
      Logger.debug(
          LOG_TAG, "Skipping history record " + guid + " with unwanted/invalid URI " + historyURI);
      return null;
    }

    final long visitCount = getLongFromCursor(cur, BrowserContract.History.VISITS);
    if (visitCount <= 0) {
      Logger.debug(LOG_TAG, "Skipping history record " + guid + " with <= 0 visit count.");
      return null;
    }

    final String collection = "history";
    final long lastModified = getLongFromCursor(cur, BrowserContract.SyncColumns.DATE_MODIFIED);
    final boolean deleted =
        getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1 ? true : false;

    final HistoryRecord rec = new HistoryRecord(guid, collection, lastModified, deleted);

    rec.androidID = getLongFromCursor(cur, BrowserContract.History._ID);
    rec.fennecDateVisited = getLongFromCursor(cur, BrowserContract.History.DATE_LAST_VISITED);
    rec.fennecVisitCount = visitCount;
    rec.histURI = historyURI;
    rec.title = getStringFromCursor(cur, BrowserContract.History.TITLE);

    return logHistory(rec);
  }
  /**
   * Override these in your subclasses.
   *
   * @return true if this stage should be executed.
   * @throws MetaGlobalException
   */
  protected boolean isEnabled() throws MetaGlobalException {
    EngineSettings engineSettings = null;
    try {
      engineSettings = getEngineSettings();
    } catch (Exception e) {
      Logger.warn(
          LOG_TAG, "Unable to get engine settings for " + this + ": fetching config failed.", e);
      // Fall through; null engineSettings will pass below.
    }

    // We can be disabled by the server's meta/global record, or malformed in the server's
    // meta/global record.
    // We catch the subclasses of MetaGlobalException to trigger various resets and wipes in
    // execute().
    boolean enabledInMetaGlobal = session.engineIsEnabled(this.getEngineName(), engineSettings);
    if (!enabledInMetaGlobal) {
      Logger.debug(LOG_TAG, "Stage " + this.getEngineName() + " disabled by server meta/global.");
      return false;
    }

    // We can also be disabled just for this sync.
    if (session.config.stagesToSync == null) {
      return true;
    }
    boolean enabledThisSync =
        session.config.stagesToSync.contains(
            this.getEngineName()); // For ServerSyncStage, stage name == engine name.
    if (!enabledThisSync) {
      Logger.debug(LOG_TAG, "Stage " + this.getEngineName() + " disabled just for this sync.");
    }
    return enabledThisSync;
  }
  @Override
  public void execute(final JPakeClient jClient) {
    Logger.debug(LOG_TAG, "Retrieving next message.");

    final GetRequestStageDelegate callbackDelegate =
        new GetRequestStageDelegate() {

          @Override
          public void handleSuccess(HttpResponse response) {
            if (jClient.finished) {
              Logger.debug(LOG_TAG, "Finished; returning.");
              return;
            }
            JPakeResponse res = new JPakeResponse(response);

            Header etagHeader = response.getFirstHeader("etag");
            if (etagHeader == null) {
              Logger.error(LOG_TAG, "Server did not supply ETag.");
              jClient.abort(Constants.JPAKE_ERROR_SERVER);
              return;
            }

            jClient.theirEtag = etagHeader.getValue();
            try {
              jClient.jIncoming = res.jsonObjectBody();
            } catch (Exception e) {
              Logger.error(LOG_TAG, "Illegal state.", e);
              jClient.abort(Constants.JPAKE_ERROR_INVALID);
              return;
            }
            Logger.debug(LOG_TAG, "incoming message: " + jClient.jIncoming.toJSONString());

            jClient.runNextStage();
          }

          @Override
          public void handleFailure(String error) {
            jClient.abort(error);
          }

          @Override
          public void handleError(Exception e) {
            Logger.error(LOG_TAG, "Threw HTTP exception.", e);
            jClient.abort(Constants.JPAKE_ERROR_NETWORK);
          }
        };

    Resource httpRequest;
    try {
      httpRequest = createGetRequest(callbackDelegate, jClient);
    } catch (URISyntaxException e) {
      Logger.error(LOG_TAG, "Incorrect URI syntax.", e);
      jClient.abort(Constants.JPAKE_ERROR_INVALID);
      return;
    }

    Logger.debug(LOG_TAG, "Scheduling GET request.");
    getStepTimerTask = new GetStepTimerTask(httpRequest);
    timerScheduler.schedule(getStepTimerTask, jClient.jpakePollInterval);
  }
 /**
  * Override this in your subclasses to return values to save between sessions. Note that
  * RepositorySession automatically bumps the timestamp to the time the last sync began. If
  * unbundled but not begun, this will be the same as the value in the input bundle.
  *
  * <p>The Synchronizer most likely wants to bump the bundle timestamp to be a value return from a
  * fetch call.
  *
  * @param optional
  * @return
  */
 protected RepositorySessionBundle getBundle(RepositorySessionBundle optional) {
   Logger.debug(LOG_TAG, "RepositorySession.getBundle(optional).");
   // Why don't we just persist the old bundle?
   RepositorySessionBundle bundle = (optional == null) ? new RepositorySessionBundle() : optional;
   bundle.put("timestamp", this.lastSyncTimestamp);
   Logger.debug(LOG_TAG, "Setting bundle timestamp to " + this.lastSyncTimestamp);
   return bundle;
 }
 public void finish(final RepositorySessionFinishDelegate delegate) {
   if (this.status == SessionStatus.ACTIVE) {
     this.status = SessionStatus.DONE;
     delegate.deferredFinishDelegate(delegateQueue).onFinishSucceeded(this, this.getBundle(null));
   } else {
     Logger.error(LOG_TAG, "Tried to finish() an unstarted or already finished session");
     Exception e = new InvalidSessionTransitionException(null);
     delegate.deferredFinishDelegate(delegateQueue).onFinishFailed(e);
   }
   Logger.info(LOG_TAG, "Shutting down work queues.");
   //   storeWorkQueue.shutdown();
   //   delegateQueue.shutdown();
 }
 /**
  * Rename mobile folders to "mobile", both in and out. The other half of this logic lives in
  * {@link #computeParentFields(BookmarkRecord, String, String)}, where the parent name of a record
  * is set from {@link #SPECIAL_GUIDS_MAP} rather than from source data.
  *
  * <p>Apply this approach generally for symmetry.
  */
 @Override
 protected void fixupRecord(Record record) {
   final BookmarkRecord r = (BookmarkRecord) record;
   final String parentName = SPECIAL_GUIDS_MAP.get(r.parentID);
   if (parentName == null) {
     return;
   }
   if (Logger.logVerbose(LOG_TAG)) {
     Logger.trace(
         LOG_TAG, "Replacing parent name \"" + r.parentName + "\" with \"" + parentName + "\".");
   }
   r.parentName = parentName;
 }
 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, "> Position:         " + rec.androidPosition);
       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;
 }
  /**
   * We synced this engine! Persist timestamps and advance the session.
   *
   * @param synchronizer the <code>Synchronizer</code> that succeeded.
   */
  @Override
  public void onSynchronized(Synchronizer synchronizer) {
    Logger.debug(LOG_TAG, "onSynchronized.");

    SynchronizerConfiguration newConfig = synchronizer.save();
    if (newConfig != null) {
      persistConfig(newConfig);
    } else {
      Logger.warn(LOG_TAG, "Didn't get configuration from synchronizer after success.");
    }

    Logger.info(LOG_TAG, "Advancing session.");
    session.advance();
  }
  @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 boolean insertFolder(BookmarkRecord record) {
    // A folder that is *not* deleted needs its androidID updated, so that
    // updateBookkeeping can re-parent, etc.
    Record toStore = prepareRecord(record);
    try {
      Uri recordURI = dbHelper.insert(toStore);
      if (recordURI == null) {
        delegate.onRecordStoreFailed(
            new RuntimeException("Got null URI inserting folder with guid " + toStore.guid + "."),
            record.guid);
        return false;
      }
      toStore.androidID = ContentUris.parseId(recordURI);
      Logger.debug(
          LOG_TAG,
          "Inserted folder with guid " + toStore.guid + " as androidID " + toStore.androidID);

      updateBookkeeping(toStore);
    } catch (Exception e) {
      delegate.onRecordStoreFailed(e, record.guid);
      return false;
    }
    trackRecord(toStore);
    delegate.onRecordStoreSucceeded(record.guid);
    return true;
  }
Example #13
0
 public Cursor checkNullCursor(String logLabel, Cursor cursor) throws NullCursorException {
   if (cursor == null) {
     Logger.error(tag, "Got null cursor exception in " + logLabel);
     throw new NullCursorException(null);
   }
   return cursor;
 }
 /**
  * Synchronously perform the shared work of beginning. Throws on failure.
  *
  * @throws InvalidSessionTransitionException
  */
 protected void sharedBegin() throws InvalidSessionTransitionException {
   if (this.status == SessionStatus.UNSTARTED) {
     this.status = SessionStatus.ACTIVE;
   } else {
     Logger.error(LOG_TAG, "Tried to begin() an already active or finished session");
     throw new InvalidSessionTransitionException(null);
   }
 }
 private long getIDForGUID(String guid) {
   Long id = parentGuidToIDMap.get(guid);
   if (id == null) {
     Logger.warn(LOG_TAG, "Couldn't find local ID for GUID " + guid);
     return -1;
   }
   return id.longValue();
 }
  // 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);
    }

    int rowType = getTypeFromCursor(cur);
    String typeString = BrowserContractHelpers.typeStringForCode(rowType);

    if (typeString == null) {
      Logger.warn(LOG_TAG, "Unsupported type code " + rowType);
      return null;
    } else {
      Logger.trace(LOG_TAG, "Record " + guid + " has type " + typeString);
    }

    rec.type = typeString;
    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.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);
  }
 public void update(String guid, Record newRecord) {
   String where = BrowserContract.SyncColumns.GUID + " = ?";
   String[] args = new String[] {guid};
   ContentValues cv = getContentValues(newRecord);
   int updated = context.getContentResolver().update(getUri(), cv, where, args);
   if (updated != 1) {
     Logger.warn(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + guid);
   }
 }
Example #18
0
 public static JSONArray getJSONArrayFromCursor(Cursor cur, String colId) {
   String jsonArrayAsString = getStringFromCursor(cur, colId);
   if (jsonArrayAsString == null) {
     return new JSONArray();
   }
   try {
     return ExtendedJSONObject.parseJSONArray(getStringFromCursor(cur, colId));
   } catch (NonArrayJSONException e) {
     Logger.error(LOG_TAG, "JSON parsing error for " + colId, e);
     return null;
   } catch (IOException e) {
     Logger.error(LOG_TAG, "JSON parsing error for " + colId, e);
     return null;
   } catch (ParseException e) {
     Logger.error(LOG_TAG, "JSON parsing error for " + colId, e);
     return null;
   }
 }
Example #19
0
  /**
   * Synchronously wipe the server.
   *
   * <p>Logs and re-throws an exception on failure.
   */
  public void wipeServer() throws Exception {
    final WipeWaiter monitor = new WipeWaiter();

    final Runnable doWipe =
        new Runnable() {
          @Override
          public void run() {
            wipeServer(
                session,
                new WipeServerDelegate() {
                  @Override
                  public void onWiped(long timestamp) {
                    synchronized (monitor) {
                      monitor.notify();
                    }
                  }

                  @Override
                  public void onWipeFailed(Exception e) {
                    synchronized (monitor) {
                      monitor.notify(e, false);
                    }
                  }
                });
          }
        };

    final Thread wiping = new Thread(doWipe);
    synchronized (monitor) {
      wiping.start();
      try {
        monitor.wait();
      } catch (InterruptedException e) {
        Logger.error(LOG_TAG, "Server wipe interrupted.");
      }
    }

    if (!monitor.wipeSucceeded) {
      Logger.error(LOG_TAG, "Failed to wipe server.");
      throw monitor.error;
    }

    Logger.info(LOG_TAG, "Wiping server complete.");
  }
  /**
   * Remove matching records from the database entirely, i.e., do not set a deleted flag, delete
   * entirely.
   *
   * @param guid The GUID of the record to be deleted.
   * @return The number of records deleted.
   */
  public int purgeGuid(String guid) {
    String where = BrowserContract.SyncColumns.GUID + " = ?";
    String[] args = new String[] {guid};

    int deleted = context.getContentResolver().delete(getUri(), where, args);
    if (deleted != 1) {
      Logger.warn(LOG_TAG, "Unexpectedly deleted " + deleted + " records for guid " + guid);
    }
    return deleted;
  }
Example #21
0
  /**
   * Reset timestamps and possibly set syncID.
   *
   * @param syncID if non-null, new syncID to persist.
   */
  protected void resetLocal(String syncID) {
    // Clear both timestamps.
    SynchronizerConfiguration config;
    try {
      config = this.getConfig();
    } catch (Exception e) {
      Logger.warn(LOG_TAG, "Unable to reset " + this + ": fetching config failed.", e);
      return;
    }

    if (syncID != null) {
      config.syncID = syncID;
      Logger.info(LOG_TAG, "Setting syncID for " + this + " to '" + syncID + "'.");
    }
    config.localBundle.setTimestamp(0L);
    config.remoteBundle.setTimestamp(0L);
    persistConfig(config);
    Logger.info(LOG_TAG, "Reset timestamps for " + this);
  }
  @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);
  }
Example #23
0
  /**
   * We failed to sync this engine! Do not persist timestamps (which means that the next sync will
   * include this sync's data), but do advance the session (if we didn't get a Retry-After header).
   *
   * @param synchronizer the <code>Synchronizer</code> that failed.
   */
  @Override
  public void onSynchronizeFailed(
      Synchronizer synchronizer, Exception lastException, String reason) {
    Logger.warn(LOG_TAG, "Synchronize failed: " + reason, lastException);

    // This failure could be due to a 503 or a 401 and it could have headers.
    // Interrogate the headers but only abort the global session if Retry-After header is set.
    if (lastException instanceof HTTPFailureException) {
      SyncStorageResponse response = ((HTTPFailureException) lastException).response;
      if (response.retryAfterInSeconds() > 0) {
        session.handleHTTPError(response, reason); // Calls session.abort().
        return;
      } else {
        session.interpretHTTPFailure(response.httpResponse()); // Does not call session.abort().
      }
    }

    Logger.info(LOG_TAG, "Advancing session even though stage failed. Timestamps not persisted.");
    session.advance();
  }
  /**
   * Asynchronously request an immediate sync, optionally syncing only the given named stages.
   *
   * <p>Returns immediately.
   *
   * @param account the Android <code>Account</code> instance to sync.
   * @param stageNames stage names to sync, or <code>null</code> to sync all known stages.
   */
  public static void requestImmediateSync(final Account account, final String[] stageNames) {
    if (account == null) {
      Logger.warn(LOG_TAG, "Not requesting immediate sync because Android Account is null.");
      return;
    }

    final Bundle extras = new Bundle();
    Utils.putStageNamesToSync(extras, stageNames, null);
    extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
    ContentResolver.requestSync(account, BrowserContract.AUTHORITY, extras);
  }
 public void storeDone(final long end) {
   Logger.debug(LOG_TAG, "Scheduling onStoreCompleted for after storing is done.");
   Runnable command =
       new Runnable() {
         @Override
         public void run() {
           delegate.onStoreCompleted(end);
         }
       };
   storeWorkQueue.execute(command);
 }
 @Override
 protected void storeRecordDeletion(final Record record, final Record existingRecord) {
   if (SPECIAL_GUIDS_MAP.containsKey(record.guid)) {
     Logger.debug(LOG_TAG, "Told to delete record " + record.guid + ". Ignoring.");
     return;
   }
   final BookmarkRecord bookmarkRecord = (BookmarkRecord) record;
   final BookmarkRecord existingBookmark = (BookmarkRecord) existingRecord;
   final boolean isFolder = existingBookmark.isFolder();
   final String parentGUID = existingBookmark.parentID;
   deletionManager.deleteRecord(bookmarkRecord.guid, isFolder, parentGUID);
 }
  /**
   * Produce a record that is some combination of the remote and local records provided.
   *
   * <p>The returned record must be produced without mutating either remoteRecord or localRecord. It
   * is acceptable to return either remoteRecord or localRecord if no modifications are to be
   * propagated.
   *
   * <p>The returned record *should* have the local androidID and the remote GUID, and some optional
   * merge of data from the two records.
   *
   * <p>This method can be called with records that are identical, or differ in any regard.
   *
   * <p>This method will not be called if:
   *
   * <p>* either record is marked as deleted, or * there is no local mapping for a new remote
   * record.
   *
   * <p>Otherwise, it will be called precisely once.
   *
   * <p>Side-effects (e.g., for transactional storage) can be hooked in here.
   *
   * @param remoteRecord The record retrieved from upstream, already adjusted for clock skew.
   * @param localRecord The record retrieved from local storage.
   * @param lastRemoteRetrieval The timestamp of the last retrieved set of remote records, adjusted
   *     for clock skew.
   * @param lastLocalRetrieval The timestamp of the last retrieved set of local records.
   * @return A Record instance to apply, or null to apply nothing.
   */
  protected Record reconcileRecords(
      final Record remoteRecord,
      final Record localRecord,
      final long lastRemoteRetrieval,
      final long lastLocalRetrieval) {
    Logger.debug(
        LOG_TAG, "Reconciling remote " + remoteRecord.guid + " against local " + localRecord.guid);

    if (localRecord.equalPayloads(remoteRecord)) {
      if (remoteRecord.lastModified > localRecord.lastModified) {
        Logger.debug(LOG_TAG, "Records are equal. No record application needed.");
        return null;
      }

      // Local wins.
      return null;
    }

    // TODO: Decide what to do based on:
    // * Which of the two records is modified;
    // * Whether they are equal or congruent;
    // * The modified times of each record (interpreted through the lens of clock skew);
    // * ...
    boolean localIsMoreRecent = localRecord.lastModified > remoteRecord.lastModified;
    Logger.debug(LOG_TAG, "Local record is more recent? " + localIsMoreRecent);
    Record donor = localIsMoreRecent ? localRecord : remoteRecord;

    // Modify the local record to match the remote record's GUID and values.
    // Preserve the local Android ID, and merge data where possible.
    // It sure would be nice if copyWithIDs didn't give a shit about androidID, mm?
    Record out = donor.copyWithIDs(remoteRecord.guid, localRecord.androidID);

    // We don't want to upload the record if the remote record was
    // applied without changes.
    // This logic will become more complicated as reconciling becomes smarter.
    if (!localIsMoreRecent) {
      trackRecord(out);
    }
    return out;
  }
  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;
  }
Example #29
0
  /** Asynchronously wipe collection on server. */
  protected void wipeServer(
      final CredentialsSource credentials, final WipeServerDelegate wipeDelegate) {
    SyncStorageRequest request;

    try {
      request = new SyncStorageRequest(session.config.collectionURI(getCollection()));
    } catch (URISyntaxException ex) {
      Logger.warn(LOG_TAG, "Invalid URI in wipeServer.");
      wipeDelegate.onWipeFailed(ex);
      return;
    }

    request.delegate =
        new SyncStorageRequestDelegate() {

          @Override
          public String ifUnmodifiedSince() {
            return null;
          }

          @Override
          public void handleRequestSuccess(SyncStorageResponse response) {
            BaseResource.consumeEntity(response);
            resetLocal();
            wipeDelegate.onWiped(response.normalizedWeaveTimestamp());
          }

          @Override
          public void handleRequestFailure(SyncStorageResponse response) {
            Logger.warn(
                LOG_TAG, "Got request failure " + response.getStatusCode() + " in wipeServer.");
            // Process HTTP failures here to pick up backoffs, etc.
            session.interpretHTTPFailure(response.httpResponse());
            BaseResource.consumeEntity(
                response); // The exception thrown should not need the body of the response.
            wipeDelegate.onWipeFailed(new HTTPFailureException(response));
          }

          @Override
          public void handleRequestError(Exception ex) {
            Logger.warn(LOG_TAG, "Got exception in wipeServer.", ex);
            wipeDelegate.onWipeFailed(ex);
          }

          @Override
          public String credentials() {
            return credentials.credentials();
          }
        };

    request.delete();
  }
  @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);
    }
  }