Ejemplo n.º 1
0
 public static void logClient(ClientRecord rec) {
   if (Logger.shouldLogVerbose(LOG_TAG)) {
     Logger.trace(LOG_TAG, "Returning client record " + rec.guid + " (" + rec.androidID + ")");
     Logger.trace(LOG_TAG, "Client Name:   " + rec.name);
     Logger.trace(LOG_TAG, "Client Type:   " + rec.type);
     Logger.trace(LOG_TAG, "Last Modified: " + rec.lastModified);
     Logger.trace(LOG_TAG, "Deleted:       " + rec.deleted);
   }
 }
 /**
  * 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;
 }
  // 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);
  }
  @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);
    }
  }
 protected static void trace(String message) {
   Logger.trace(LOG_TAG, message);
 }
  /**
   * 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);

    if (forbiddenGUID(recordGUID)) {
      Logger.debug(LOG_TAG, "Ignoring " + recordGUID + " record in recordFromMirrorCursor.");
      return null;
    }

    // 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);
      // If the parent has been stored and somehow has a null GUID, throw an error.
      if (parentIDToGuidMap.containsKey(androidParentID)) {
        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, 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);
    BookmarkRecord bookmark =
        AndroidBrowserBookmarksRepositorySession.bookmarkFromMirrorCursor(
            cur, androidParentGUID, parentName, childArray);

    if (bookmark == null) {
      Logger.warn(
          LOG_TAG,
          "Unable to extract bookmark from cursor. Record GUID "
              + recordGUID
              + ", parent "
              + androidParentGUID
              + "/"
              + androidParentID);
      return null;
    }

    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;
  }
  /**
   * 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();
    }
  }
 @Override
 public void handleStageCompleted(Stage currentState, GlobalSession globalSession) {
   Logger.trace(LOG_TAG, "Stage completed: " + currentState);
 }
 private void notifyMonitor() {
   synchronized (syncMonitor) {
     Logger.trace(LOG_TAG, "Notifying sync monitor.");
     syncMonitor.notifyAll();
   }
 }
  /**
   * Now that we have a sync key and password, go ahead and do the work.
   *
   * @throws NoSuchAlgorithmException
   * @throws IllegalArgumentException
   * @throws SyncConfigurationException
   * @throws AlreadySyncingException
   * @throws NonObjectJSONException
   * @throws ParseException
   * @throws IOException
   * @throws CryptoException
   */
  protected void performSync(
      final Account account,
      final Bundle extras,
      final String authority,
      final ContentProviderClient provider,
      final SyncResult syncResult,
      final String username,
      final String password,
      final String prefsPath,
      final String serverURL,
      final String syncKey)
      throws NoSuchAlgorithmException, SyncConfigurationException, IllegalArgumentException,
          AlreadySyncingException, IOException, ParseException, NonObjectJSONException,
          CryptoException {
    Logger.trace(LOG_TAG, "Performing sync.");
    syncStartTimestamp = System.currentTimeMillis();

    /**
     * Bug 769745: pickle Sync account parameters to JSON file. Un-pickle in <code>
     * SyncAccounts.syncAccountsExist</code>.
     */
    try {
      // Constructor can throw on nulls, which should not happen -- but let's be safe.
      final SyncAccountParameters params =
          new SyncAccountParameters(
              mContext,
              null,
              account.name, // Un-encoded, like "*****@*****.**".
              syncKey,
              password,
              serverURL,
              null, // We'll re-fetch cluster URL; not great, but not harmful.
              getClientName(),
              getAccountGUID());

      // Bug 772971: pickle Sync account parameters on background thread to
      // avoid strict mode warnings.
      ThreadPool.run(
          new Runnable() {
            @Override
            public void run() {
              final boolean syncAutomatically =
                  ContentResolver.getSyncAutomatically(account, authority);
              try {
                AccountPickler.pickle(
                    mContext, Constants.ACCOUNT_PICKLE_FILENAME, params, syncAutomatically);
              } catch (Exception e) {
                // Should never happen, but we really don't want to die in a background thread.
                Logger.warn(
                    LOG_TAG, "Got exception pickling current account details; ignoring.", e);
              }
            }
          });
    } catch (IllegalArgumentException e) {
      // Do nothing.
    }

    // TODO: default serverURL.
    final KeyBundle keyBundle = new KeyBundle(username, syncKey);
    GlobalSession globalSession =
        new GlobalSession(
            SyncConfiguration.DEFAULT_USER_API,
            serverURL,
            username,
            password,
            prefsPath,
            keyBundle,
            this,
            this.mContext,
            extras,
            this);

    globalSession.start();
  }
  @Override
  public void onPerformSync(
      final Account account,
      final Bundle extras,
      final String authority,
      final ContentProviderClient provider,
      final SyncResult syncResult) {
    Logger.resetLogging();
    Utils.reseedSharedRandom(); // Make sure we don't work with the same random seed for too long.

    // Set these so that we don't need to thread them through assorted calls and callbacks.
    this.syncResult = syncResult;
    this.localAccount = account;

    SyncAccountParameters params;
    try {
      params =
          SyncAccounts.blockingFromAndroidAccountV0(
              mContext, AccountManager.get(mContext), this.localAccount);
    } catch (Exception e) {
      // Updates syncResult and (harmlessly) calls notifyMonitor().
      processException(null, e);
      return;
    }

    // params and the following fields are non-null at this point.
    final String username = params.username; // Encoded with Utils.usernameFromAccount.
    final String password = params.password;
    final String serverURL = params.serverURL;
    final String syncKey = params.syncKey;

    final AtomicBoolean setNextSync = new AtomicBoolean(true);
    final SyncAdapter self = this;
    final Runnable runnable =
        new Runnable() {
          @Override
          public void run() {
            Logger.trace(LOG_TAG, "AccountManagerCallback invoked.");
            // TODO: N.B.: Future must not be used on the main thread.
            try {
              Logger.info(
                  LOG_TAG,
                  "Syncing account named " + account.name + " for authority " + authority + ".");

              // We dump this information right away to help with debugging.
              Logger.debug(LOG_TAG, "Username: "******"Server:   " + serverURL);
              if (Logger.LOG_PERSONAL_INFORMATION) {
                Logger.debug(LOG_TAG, "Password: "******"Sync key: " + syncKey);
              } else {
                Logger.debug(LOG_TAG, "Password? " + (password != null));
                Logger.debug(LOG_TAG, "Sync key? " + (syncKey != null));
              }

              // Support multiple accounts by mapping each server/account pair to a branch of the
              // shared preferences space.
              final String product = GlobalConstants.BROWSER_INTENT_PACKAGE;
              final String profile = Constants.DEFAULT_PROFILE;
              final long version = SyncConfiguration.CURRENT_PREFS_VERSION;
              self.accountSharedPreferences =
                  Utils.getSharedPreferences(
                      mContext, product, username, serverURL, profile, version);

              Logger.info(
                  LOG_TAG,
                  "Client is named '"
                      + getClientName()
                      + "'"
                      + ", has client guid "
                      + getAccountGUID()
                      + ", and has "
                      + getClientsCount()
                      + " clients.");

              thisSyncIsForced =
                  (extras != null)
                      && (extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false));
              long delay = delayMilliseconds();
              if (delay > 0) {
                if (thisSyncIsForced) {
                  Logger.info(
                      LOG_TAG, "Forced sync: overruling remaining backoff of " + delay + "ms.");
                } else {
                  Logger.info(LOG_TAG, "Not syncing: must wait another " + delay + "ms.");
                  long remainingSeconds = delay / 1000;
                  syncResult.delayUntil = remainingSeconds + BACKOFF_PAD_SECONDS;
                  setNextSync.set(false);
                  self.notifyMonitor();
                  return;
                }
              }

              final String prefsPath =
                  Utils.getPrefsPath(product, username, serverURL, profile, version);
              self.performSync(
                  account,
                  extras,
                  authority,
                  provider,
                  syncResult,
                  username,
                  password,
                  prefsPath,
                  serverURL,
                  syncKey);
            } catch (Exception e) {
              self.processException(null, e);
              return;
            }
          }
        };

    synchronized (syncMonitor) {
      // Perform the work in a new thread from within this synchronized block,
      // which allows us to be waiting on the monitor before the callback can
      // notify us in a failure case. Oh, concurrent programming.
      new Thread(runnable).start();

      // Start our stale connection monitor thread.
      ConnectionMonitorThread stale = new ConnectionMonitorThread();
      stale.start();

      Logger.trace(LOG_TAG, "Waiting on sync monitor.");
      try {
        syncMonitor.wait();

        if (setNextSync.get()) {
          long interval = getSyncInterval();
          long next = System.currentTimeMillis() + interval;
          Logger.info(
              LOG_TAG,
              "Setting minimum next sync time to " + next + " (" + interval + "ms from now).");
          extendEarliestNextSync(next);
        }
        Logger.info(
            LOG_TAG,
            "Sync took "
                + Utils.formatDuration(syncStartTimestamp, System.currentTimeMillis())
                + ".");
      } catch (InterruptedException e) {
        Logger.warn(LOG_TAG, "Waiting on sync monitor interrupted.", e);
      } finally {
        // And we're done with HTTP stuff.
        stale.shutdown();
      }
    }
  }