예제 #1
0
  /**
   * Generate a fresh meta/global record.
   *
   * @return meta/global record.
   */
  public MetaGlobal generateNewMetaGlobal() {
    final String newSyncID = Utils.generateGuid();
    final String metaURL = this.config.metaURL();

    ExtendedJSONObject engines = new ExtendedJSONObject();
    for (String engineName : enabledEngineNames()) {
      EngineSettings engineSettings = null;
      try {
        GlobalSyncStage globalStage = this.getSyncStageByName(engineName);
        Integer version = globalStage.getStorageVersion();
        if (version == null) {
          continue; // Don't want this stage to be included in meta/global.
        }
        engineSettings = new EngineSettings(Utils.generateGuid(), version.intValue());
      } catch (NoSuchStageException e) {
        // No trouble; Android Sync might not recognize this engine yet.
        // By default, version 0.  Other clients will see the 0 version and reset/wipe accordingly.
        engineSettings = new EngineSettings(Utils.generateGuid(), 0);
      }
      engines.put(engineName, engineSettings.toJSONObject());
    }

    MetaGlobal metaGlobal = new MetaGlobal(metaURL, this.getAuthHeaderProvider());
    metaGlobal.setSyncID(newSyncID);
    metaGlobal.setStorageVersion(STORAGE_VERSION);
    metaGlobal.setEngines(engines);

    return metaGlobal;
  }
예제 #2
0
  public InfoCollections(final ExtendedJSONObject record) {
    Logger.debug(LOG_TAG, "info/collections is " + record.toJSONString());
    HashMap<String, Long> map = new HashMap<String, Long>();

    for (Entry<String, Object> entry : record.entrySet()) {
      final String key = entry.getKey();
      final Object value = entry.getValue();

      // These objects are most likely going to be Doubles. Regardless, we
      // want to get them in a more sane time format.
      if (value instanceof Double) {
        map.put(key, Utils.decimalSecondsToMilliseconds((Double) value));
        continue;
      }
      if (value instanceof Long) {
        map.put(key, Utils.decimalSecondsToMilliseconds((Long) value));
        continue;
      }
      if (value instanceof Integer) {
        map.put(key, Utils.decimalSecondsToMilliseconds((Integer) value));
        continue;
      }
      Logger.warn(LOG_TAG, "Skipping info/collections entry for " + key);
    }

    this.timestamps = Collections.unmodifiableMap(map);
  }
예제 #3
0
 @Test
 public void testEncodeDecodeRandomSizeArrays() {
   for (int i = 0; i < 10; i++) {
     int length1 = Utils.generateBigIntegerLessThan(BigInteger.valueOf(50)).intValue() + 10;
     int length2 = Utils.generateBigIntegerLessThan(BigInteger.valueOf(50)).intValue() + 10;
     doTestEncodeDecodeArrays(length1, length2);
   }
 }
 public User(String email, byte[] quickStretchedPW) {
   this.email = email;
   this.quickStretchedPW = quickStretchedPW;
   this.uid = "uid/" + this.email;
   this.verified = false;
   this.kA = Utils.generateRandomBytes(FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES);
   this.wrapkB = Utils.generateRandomBytes(FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES);
   this.devices = new HashMap<String, FxAccountDevice>();
 }
예제 #5
0
  @Test
  public void testUploadUpdatedMetaGlobal() throws Exception {
    // Set up session with meta/global.
    final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
    final GlobalSession session =
        MockPrefsGlobalSession.getSession(
            TEST_USERNAME,
            TEST_PASSWORD,
            new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY),
            callback,
            null,
            null);
    session.config.metaGlobal = session.generateNewMetaGlobal();
    session.enginesToUpdate.clear();

    // Set enabledEngines in meta/global, including a "new engine."
    String[] origEngines =
        new String[] {"bookmarks", "clients", "forms", "history", "tabs", "new-engine"};

    ExtendedJSONObject origEnginesJSONObject = new ExtendedJSONObject();
    for (String engineName : origEngines) {
      EngineSettings mockEngineSettings =
          new EngineSettings(Utils.generateGuid(), Integer.valueOf(0));
      origEnginesJSONObject.put(engineName, mockEngineSettings);
    }
    session.config.metaGlobal.setEngines(origEnginesJSONObject);

    // Engines to remove.
    String[] toRemove = new String[] {"bookmarks", "tabs"};
    for (String name : toRemove) {
      session.removeEngineFromMetaGlobal(name);
    }

    // Engines to add.
    String[] toAdd = new String[] {"passwords"};
    for (String name : toAdd) {
      String syncId = Utils.generateGuid();
      session.recordForMetaGlobalUpdate(name, new EngineSettings(syncId, Integer.valueOf(1)));
    }

    // Update engines.
    session.uploadUpdatedMetaGlobal();

    // Check resulting enabledEngines.
    Set<String> expected = new HashSet<String>();
    for (String name : origEngines) {
      expected.add(name);
    }
    for (String name : toRemove) {
      expected.remove(name);
    }
    for (String name : toAdd) {
      expected.add(name);
    }
    assertEquals(expected, session.config.metaGlobal.getEnabledEngineNames());
  }
예제 #6
0
 public void doTestEncodeDecodeArrays(int length1, int length2) {
   if (4 + length1 + length2 > 127) {
     throw new IllegalArgumentException("Total length must be < 128 - 4.");
   }
   byte[] first = Utils.generateRandomBytes(length1);
   byte[] second = Utils.generateRandomBytes(length2);
   byte[] encoded = ASNUtils.encodeTwoArraysToASN1(first, second);
   byte[][] arrays = ASNUtils.decodeTwoArraysFromASN1(encoded);
   Assert.assertArrayEquals(first, arrays[0]);
   Assert.assertArrayEquals(second, arrays[1]);
 }
 protected LoginResponse addLogin(User user, byte[] sessionToken, byte[] keyFetchToken) {
   // byte[] sessionToken = Utils.generateRandomBytes(8);
   if (sessionToken != null) {
     sessionTokens.put(Utils.byte2Hex(sessionToken), user.email);
   }
   // byte[] keyFetchToken = Utils.generateRandomBytes(8);
   if (keyFetchToken != null) {
     keyFetchTokens.put(Utils.byte2Hex(keyFetchToken), user.email);
   }
   return new LoginResponse(user.email, user.uid, user.verified, sessionToken, keyFetchToken);
 }
예제 #8
0
  public GlobalSession(
      SyncConfiguration config,
      BaseGlobalSessionCallback callback,
      Context context,
      Bundle extras,
      ClientsDataDelegate clientsDelegate,
      NodeAssignmentCallback nodeAssignmentCallback)
      throws SyncConfigurationException, IllegalArgumentException, IOException, ParseException,
          NonObjectJSONException {

    if (callback == null) {
      throw new IllegalArgumentException("Must provide a callback to GlobalSession constructor.");
    }

    Logger.debug(LOG_TAG, "GlobalSession initialized with bundle " + extras);

    this.callback = callback;
    this.context = context;
    this.clientsDelegate = clientsDelegate;
    this.nodeAssignmentCallback = nodeAssignmentCallback;

    this.config = config;
    registerCommands();
    prepareStages();

    Collection<String> knownStageNames = SyncConfiguration.validEngineNames();
    config.stagesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras);

    // TODO: data-driven plan for the sync, referring to prepareStages.
  }
예제 #9
0
 /** Return a hex-encoded string value as a byte array. */
 public byte[] getByteArrayHex(String key) {
   String s = (String) this.object.get(key);
   if (s == null) {
     return null;
   }
   return Utils.hex2Byte(s);
 }
예제 #10
0
  /** This needs to return a string because of the tortured prefs access in GlobalSession. */
  public String getSyncPrefsPath() throws GeneralSecurityException, UnsupportedEncodingException {
    String profile = getProfile();
    String username = account.name;

    if (profile == null) {
      throw new IllegalStateException("Missing profile. Cannot fetch prefs.");
    }

    if (username == null) {
      throw new IllegalStateException("Missing username. Cannot fetch prefs.");
    }

    final String tokenServerURI = getTokenServerURI();
    if (tokenServerURI == null) {
      throw new IllegalStateException("No token server URI. Cannot fetch prefs.");
    }

    final String fxaServerURI = getAccountServerURI();
    if (fxaServerURI == null) {
      throw new IllegalStateException("No account server URI. Cannot fetch prefs.");
    }

    final String product = GlobalConstants.BROWSER_INTENT_PACKAGE + ".fxa";
    final long version = CURRENT_PREFS_VERSION;

    // This is unique for each syncing 'view' of the account.
    final String serverURLThing = fxaServerURI + "!" + tokenServerURI;
    return Utils.getPrefsPath(product, username, serverURLThing, profile, version);
  }
 @SuppressWarnings("static-method")
 @Test
 public void testUTF8() throws UnsupportedEncodingException {
   final String in = "pïgéons1";
   final String out = "pïgéons1";
   assertEquals(out, Utils.decodeUTF8(in));
 }
예제 #12
0
  /**
   * The timestamp returned from a Sync server is a decimal number of seconds, e.g., 1323393518.04.
   *
   * <p>We want milliseconds since epoch.
   *
   * @return milliseconds since the epoch, as a long, or -1 if the header was missing or invalid.
   */
  public long normalizedWeaveTimestamp() {
    String h = "x-weave-timestamp";
    if (!this.hasHeader(h)) {
      return -1;
    }

    return Utils.decimalSecondsToMilliseconds(this.response.getFirstHeader(h).getValue());
  }
예제 #13
0
 public static byte[] hex2Byte(String str, int byteLength) {
   byte[] second = hex2Byte(str);
   if (second.length >= byteLength) {
     return second;
   }
   // New Java arrays are zeroed:
   // http://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.12.5
   byte[] first = new byte[byteLength - second.length];
   return Utils.concatAll(first, second);
 }
예제 #14
0
 /**
  * Extract a JSON dictionary of the string values associated to this account.
  *
  * <p><b>For debugging use only!</b> The contents of this JSON object completely determine the
  * user's Firefox Account status and yield access to whatever user data the device has access to.
  *
  * @return JSON-object of Strings.
  */
 public ExtendedJSONObject toJSONObject() {
   ExtendedJSONObject o = unbundle();
   o.put("email", account.name);
   try {
     o.put("emailUTF8", Utils.byte2Hex(account.name.getBytes("UTF-8")));
   } catch (UnsupportedEncodingException e) {
     // Ignore.
   }
   return o;
 }
  /**
   * 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);
  }
 protected void linkifyOldFirefoxLink() {
   TextView oldFirefox = (TextView) findViewById(R.id.old_firefox);
   String text = getResources().getString(R.string.fxaccount_getting_started_old_firefox);
   String VERSION = AppConstants.MOZ_APP_VERSION;
   String OS = AppConstants.OS_TARGET;
   // We'll need to adjust this when we have active locale switching.
   String LOCALE = Utils.getLanguageTag(Locale.getDefault());
   String url = getResources().getString(R.string.fxaccount_link_old_firefox, VERSION, OS, LOCALE);
   FxAccountConstants.pii(
       LOG_TAG, "Old Firefox url is: " + url); // Don't want to leak locale in particular.
   ActivityUtils.linkTextView(oldFirefox, text, url);
 }
  @Test
  public void testAuthHeaderFromPassword()
      throws NonObjectJSONException, IOException, ParseException {
    final ExtendedJSONObject parsed = new ExtendedJSONObject(DESKTOP_PASSWORD_JSON);

    final String password = parsed.getString("password");
    final String decoded = Utils.decodeUTF8(password);

    final byte[] expectedBytes = Utils.decodeBase64(BTOA_PASSWORD);
    final String expected = new String(expectedBytes, "UTF-8");

    assertEquals(DESKTOP_ASSERTED_SIZE, password.length());
    assertEquals(expected, decoded);

    System.out.println("Retrieved password: "******"Expected password:  "******"Rescued password:   " + decoded);

    assertEquals(getCreds(expected), getCreds(decoded));
    assertEquals(getCreds(decoded), DESKTOP_BASIC_AUTH);
  }
 @Override
 public synchronized String getAccountGUID() {
   String accountGUID =
       accountSharedPreferences.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null);
   if (accountGUID == null) {
     Logger.debug(LOG_TAG, "Account GUID was null. Creating a new one.");
     accountGUID = Utils.generateGuid();
     accountSharedPreferences
         .edit()
         .putString(SyncConfiguration.PREF_ACCOUNT_GUID, accountGUID)
         .commit();
   }
   return accountGUID;
 }
예제 #19
0
  /**
   * Get names of stages to sync: (ALL intersect SYNC) intersect (ALL minus SKIP).
   *
   * @param knownStageNames collection of known stage names (set ALL above).
   * @param extras a <code>Bundle</code> instance (possibly null) optionally containing keys <code>
   *     EXTRAS_KEY_STAGES_TO_SYNC</code> (set SYNC above) and <code>EXTRAS_KEY_STAGES_TO_SKIP
   *     </code> (set SKIP above).
   * @return stage names.
   */
  public static Collection<String> getStagesToSyncFromBundle(
      final Collection<String> knownStageNames, final Bundle extras) {
    if (extras == null) {
      return knownStageNames;
    }
    String toSyncString = extras.getString(Constants.EXTRAS_KEY_STAGES_TO_SYNC);
    String toSkipString = extras.getString(Constants.EXTRAS_KEY_STAGES_TO_SKIP);
    if (toSyncString == null && toSkipString == null) {
      return knownStageNames;
    }

    ArrayList<String> toSync = null;
    ArrayList<String> toSkip = null;
    if (toSyncString != null) {
      try {
        toSync = new ArrayList<String>(ExtendedJSONObject.parseJSONObject(toSyncString).keySet());
      } catch (Exception e) {
        Logger.warn(LOG_TAG, "Got exception parsing stages to sync: '" + toSyncString + "'.", e);
      }
    }
    if (toSkipString != null) {
      try {
        toSkip = new ArrayList<String>(ExtendedJSONObject.parseJSONObject(toSkipString).keySet());
      } catch (Exception e) {
        Logger.warn(LOG_TAG, "Got exception parsing stages to skip: '" + toSkipString + "'.", e);
      }
    }

    Logger.info(
        LOG_TAG,
        "Asked to sync '"
            + Utils.toCommaSeparatedString(toSync)
            + "' and to skip '"
            + Utils.toCommaSeparatedString(toSkip)
            + "'.");
    return getStagesToSync(knownStageNames, toSync, toSkip);
  }
 @Override
 public void recoveryEmailStatus(
     byte[] sessionToken, RequestDelegate<RecoveryEmailStatusResponse> requestDelegate) {
   String email = sessionTokens.get(Utils.byte2Hex(sessionToken));
   User user = users.get(email);
   if (email == null || user == null) {
     handleFailure(
         requestDelegate,
         HttpStatus.SC_UNAUTHORIZED,
         FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN,
         "invalid sessionToken");
     return;
   }
   requestDelegate.handleSuccess(new RecoveryEmailStatusResponse(email, user.verified));
 }
 public FxAccount10CreateDelegate(
     String email, byte[] stretchedPWBytes, String mainSalt, String srpSalt)
     throws NoSuchAlgorithmException, UnsupportedEncodingException {
   this.email = email;
   this.mainSalt = mainSalt;
   this.srpSalt = srpSalt;
   byte[] srpSaltBytes = Utils.hex2Byte(srpSalt, FxAccountUtils.SALT_LENGTH_BYTES);
   this.v =
       FxAccountUtils.srpVerifierLowercaseV(
           email.getBytes("UTF-8"),
           stretchedPWBytes,
           srpSaltBytes,
           SRPConstants._2048.g,
           SRPConstants._2048.N);
 }
  @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();
    }
  }
예제 #23
0
  public boolean hasUpdatedMetaGlobal() {
    if (enginesToUpdate.isEmpty()) {
      Logger.info(
          LOG_TAG,
          "Not uploading updated meta/global record since there are no engines requesting upload.");
      return false;
    }

    if (Logger.shouldLogVerbose(LOG_TAG)) {
      Logger.trace(
          LOG_TAG,
          "Uploading updated meta/global record since there are engine changes to meta/global.");
      Logger.trace(
          LOG_TAG,
          "Engines requesting update ["
              + Utils.toCommaSeparatedString(enginesToUpdate.keySet())
              + "]");
    }

    return true;
  }
 @Override
 public void keys(byte[] keyFetchToken, RequestDelegate<TwoKeys> requestDelegate) {
   String email = keyFetchTokens.get(Utils.byte2Hex(keyFetchToken));
   User user = users.get(email);
   if (email == null || user == null) {
     handleFailure(
         requestDelegate,
         HttpStatus.SC_UNAUTHORIZED,
         FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN,
         "invalid keyFetchToken");
     return;
   }
   if (!user.verified) {
     handleFailure(
         requestDelegate,
         HttpStatus.SC_BAD_REQUEST,
         FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT,
         "user is unverified");
     return;
   }
   requestDelegate.handleSuccess(new TwoKeys(user.kA, user.wrapkB));
 }
예제 #25
0
  private String constructPrefsPath(String product, long version, String extra)
      throws GeneralSecurityException, UnsupportedEncodingException {
    String profile = getProfile();
    String username = account.name;

    if (profile == null) {
      throw new IllegalStateException("Missing profile. Cannot fetch prefs.");
    }

    if (username == null) {
      throw new IllegalStateException("Missing username. Cannot fetch prefs.");
    }

    final String fxaServerURI = getAccountServerURI();
    if (fxaServerURI == null) {
      throw new IllegalStateException("No account server URI. Cannot fetch prefs.");
    }

    // This is unique for each syncing 'view' of the account.
    final String serverURLThing = fxaServerURI + "!" + extra;
    return Utils.getPrefsPath(product, username, serverURLThing, profile, version);
  }
예제 #26
0
  protected synchronized EventDispatcherImpl registerNewInstance(String classname, String filename)
      throws ClassNotFoundException, InstantiationException, IllegalAccessException,
          InvocationTargetException, NoSuchMethodException, IOException {
    Log.d(LOGTAG, "Attempting to instantiate " + classname + "from filename " + filename);

    // It's important to maintain the extension, either .dex, .apk, .jar.
    final String extension = getExtension(filename);
    final File dexFile =
        GeckoJarReader.extractStream(
            mApplicationContext, filename, mApplicationContext.getCacheDir(), "." + extension);
    try {
      if (dexFile == null) {
        throw new IOException("Could not find file " + filename);
      }
      final File tmpDir =
          mApplicationContext.getDir("dex", 0); // We'd prefer getCodeCacheDir but it's API 21+.
      final DexClassLoader loader =
          new DexClassLoader(
              dexFile.getAbsolutePath(),
              tmpDir.getAbsolutePath(),
              null,
              mApplicationContext.getClassLoader());
      final Class<?> c = loader.loadClass(classname);
      final Constructor<?> constructor =
          c.getDeclaredConstructor(Context.class, JavaAddonInterfaceV1.EventDispatcher.class);
      final String guid = Utils.generateGuid();
      final EventDispatcherImpl dispatcher = new EventDispatcherImpl(guid, filename);
      final Object instance = constructor.newInstance(mApplicationContext, dispatcher);
      mGUIDToDispatcherMap.put(guid, dispatcher);
      return dispatcher;
    } finally {
      // DexClassLoader writes an optimized version, so we can get rid of our temporary extracted
      // version.
      if (dexFile != null) {
        dexFile.delete();
      }
    }
  }
  protected void showClientRemoteException(final FxAccountClientRemoteException e) {
    if (!e.isAccountLocked()) {
      remoteErrorTextView.setText(e.getErrorMessageStringResource());
      return;
    }

    // This horrible bit of special-casing is because we want this error message
    // to contain a clickable, extra chunk of text, but we don't want to pollute
    // the exception class with Android specifics.
    final int messageId = e.getErrorMessageStringResource();
    final int clickableId = R.string.fxaccount_resend_unlock_code_button_label;
    final Spannable span =
        Utils.interpolateClickableSpan(
            this,
            messageId,
            clickableId,
            new ClickableSpan() {
              @Override
              public void onClick(View widget) {
                // It would be best to capture the email address sent to the server
                // and use it here, but this will do for now. If the user modifies
                // the email address entered, the error text is hidden, so sending a
                // changed email address would be the result of an unusual race.
                final String email = emailEdit.getText().toString();
                byte[] emailUTF8 = null;
                try {
                  emailUTF8 = email.getBytes("UTF-8");
                } catch (UnsupportedEncodingException e) {
                  // It's okay, we'll fail in the code resender.
                }
                FxAccountUnlockCodeResender.resendUnlockCode(
                    FxAccountAbstractSetupActivity.this, getAuthServerEndpoint(), emailUTF8);
              }
            });
    remoteErrorTextView.setMovementMethod(LinkMovementMethod.getInstance());
    remoteErrorTextView.setText(span);
  }
예제 #28
0
  // Add a bookmark in the Desktop folder so we can check the folder navigation in the bookmarks
  // page
  private void setUpDesktopBookmarks() {
    blockForGeckoReady();

    // Get the folder id of the StringHelper.DESKTOP_FOLDER_LABEL folder
    Long desktopFolderId = mDatabaseHelper.getFolderIdFromGuid("toolbar");

    // Generate a Guid for the bookmark
    final String generatedGuid = Utils.generateGuid();
    mAsserter.ok(
        (generatedGuid != null),
        "Generating a random Guid for the bookmark",
        "We could not generate a Guid for the bookmark");

    // Insert the bookmark
    ContentResolver resolver = getActivity().getContentResolver();
    Uri bookmarksUri = mDatabaseHelper.buildUri(DatabaseHelper.BrowserDataType.BOOKMARKS);

    long now = System.currentTimeMillis();
    ContentValues values = new ContentValues();
    values.put("title", StringHelper.ROBOCOP_BLANK_PAGE_02_TITLE);
    values.put("url", DESKTOP_BOOKMARK_URL);
    values.put("parent", desktopFolderId);
    values.put("modified", now);
    values.put("type", 1);
    values.put("guid", generatedGuid);
    values.put("position", 10);
    values.put("created", now);

    int updated =
        resolver.update(bookmarksUri, values, "url = ?", new String[] {DESKTOP_BOOKMARK_URL});
    if (updated == 0) {
      Uri uri = resolver.insert(bookmarksUri, values);
      mAsserter.ok(true, "Inserted at: ", uri.toString());
    } else {
      mAsserter.ok(false, "Failed to insert the Desktop bookmark", "Something went wrong");
    }
  }
 @Override
 public void sign(
     byte[] sessionToken,
     ExtendedJSONObject publicKey,
     long certificateDurationInMilliseconds,
     RequestDelegate<String> requestDelegate) {
   String email = sessionTokens.get(Utils.byte2Hex(sessionToken));
   User user = users.get(email);
   if (email == null || user == null) {
     handleFailure(
         requestDelegate,
         HttpStatus.SC_UNAUTHORIZED,
         FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN,
         "invalid sessionToken");
     return;
   }
   if (!user.verified) {
     handleFailure(
         requestDelegate,
         HttpStatus.SC_BAD_REQUEST,
         FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT,
         "user is unverified");
     return;
   }
   try {
     final long iat = System.currentTimeMillis();
     final long dur = certificateDurationInMilliseconds;
     final long exp = iat + dur;
     String certificate =
         mockMyIdTokenFactory.createMockMyIDCertificate(
             RSACryptoImplementation.createPublicKey(publicKey), "test", iat, exp);
     requestDelegate.handleSuccess(certificate);
   } catch (Exception e) {
     requestDelegate.handleError(e);
   }
 }
 @Override
 public void deviceList(byte[] sessionToken, RequestDelegate<FxAccountDevice[]> requestDelegate) {
   String email = sessionTokens.get(Utils.byte2Hex(sessionToken));
   User user = users.get(email);
   if (email == null || user == null) {
     handleFailure(
         requestDelegate,
         HttpStatus.SC_UNAUTHORIZED,
         FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN,
         "invalid sessionToken");
     return;
   }
   if (!user.verified) {
     handleFailure(
         requestDelegate,
         HttpStatus.SC_BAD_REQUEST,
         FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT,
         "user is unverified");
     return;
   }
   Collection<FxAccountDevice> devices = user.devices.values();
   FxAccountDevice[] devicesArray = devices.toArray(new FxAccountDevice[devices.size()]);
   requestDelegate.handleSuccess(devicesArray);
 }