mobile/android/base/sync/Utils.java

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/base/sync/Utils.java	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,617 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +package org.mozilla.gecko.sync;
     1.9 +
    1.10 +import java.io.BufferedReader;
    1.11 +import java.io.FileInputStream;
    1.12 +import java.io.IOException;
    1.13 +import java.io.InputStreamReader;
    1.14 +import java.io.UnsupportedEncodingException;
    1.15 +import java.math.BigDecimal;
    1.16 +import java.math.BigInteger;
    1.17 +import java.net.URLDecoder;
    1.18 +import java.security.MessageDigest;
    1.19 +import java.security.NoSuchAlgorithmException;
    1.20 +import java.security.SecureRandom;
    1.21 +import java.text.DecimalFormat;
    1.22 +import java.util.ArrayList;
    1.23 +import java.util.Collection;
    1.24 +import java.util.HashMap;
    1.25 +import java.util.HashSet;
    1.26 +import java.util.Locale;
    1.27 +import java.util.Map;
    1.28 +import java.util.TreeMap;
    1.29 +
    1.30 +import org.json.simple.JSONArray;
    1.31 +import org.mozilla.apache.commons.codec.binary.Base32;
    1.32 +import org.mozilla.apache.commons.codec.binary.Base64;
    1.33 +import org.mozilla.gecko.background.common.log.Logger;
    1.34 +import org.mozilla.gecko.background.nativecode.NativeCrypto;
    1.35 +import org.mozilla.gecko.sync.setup.Constants;
    1.36 +
    1.37 +import android.annotation.SuppressLint;
    1.38 +import android.content.Context;
    1.39 +import android.content.SharedPreferences;
    1.40 +import android.os.Bundle;
    1.41 +
    1.42 +public class Utils {
    1.43 +
    1.44 +  private static final String LOG_TAG = "Utils";
    1.45 +
    1.46 +  private static SecureRandom sharedSecureRandom = new SecureRandom();
    1.47 +
    1.48 +  // See <http://developer.android.com/reference/android/content/Context.html#getSharedPreferences%28java.lang.String,%20int%29>
    1.49 +  public static final int SHARED_PREFERENCES_MODE = 0;
    1.50 +
    1.51 +  public static String generateGuid() {
    1.52 +    byte[] encodedBytes = Base64.encodeBase64(generateRandomBytes(9), false);
    1.53 +    return new String(encodedBytes).replace("+", "-").replace("/", "_");
    1.54 +  }
    1.55 +
    1.56 +  /**
    1.57 +   * Helper to generate secure random bytes.
    1.58 +   *
    1.59 +   * @param length
    1.60 +   *        Number of bytes to generate.
    1.61 +   */
    1.62 +  public static byte[] generateRandomBytes(int length) {
    1.63 +    byte[] bytes = new byte[length];
    1.64 +    sharedSecureRandom.nextBytes(bytes);
    1.65 +    return bytes;
    1.66 +  }
    1.67 +
    1.68 +  /**
    1.69 +   * Helper to generate a random integer in a specified range.
    1.70 +   *
    1.71 +   * @param r
    1.72 +   *        Generate an integer between 0 and r-1 inclusive.
    1.73 +   */
    1.74 +  public static BigInteger generateBigIntegerLessThan(BigInteger r) {
    1.75 +    int maxBytes = (int) Math.ceil(((double) r.bitLength()) / 8);
    1.76 +    BigInteger randInt = new BigInteger(generateRandomBytes(maxBytes));
    1.77 +    return randInt.mod(r);
    1.78 +  }
    1.79 +
    1.80 +  /**
    1.81 +   * Helper to reseed the shared secure random number generator.
    1.82 +   */
    1.83 +  public static void reseedSharedRandom() {
    1.84 +    sharedSecureRandom.setSeed(sharedSecureRandom.generateSeed(8));
    1.85 +  }
    1.86 +
    1.87 +  /**
    1.88 +   * Helper to convert a byte array to a hex-encoded string
    1.89 +   */
    1.90 +  public static String byte2Hex(final byte[] b) {
    1.91 +    return byte2Hex(b, 2 * b.length);
    1.92 +  }
    1.93 +
    1.94 +  public static String byte2Hex(final byte[] b, int hexLength) {
    1.95 +    final StringBuilder hs = new StringBuilder(Math.max(2*b.length, hexLength));
    1.96 +    String stmp;
    1.97 +
    1.98 +    for (int n = 0; n < hexLength - 2*b.length; n++) {
    1.99 +      hs.append("0");
   1.100 +    }
   1.101 +
   1.102 +    for (int n = 0; n < b.length; n++) {
   1.103 +      stmp = Integer.toHexString(b[n] & 0XFF);
   1.104 +
   1.105 +      if (stmp.length() == 1) {
   1.106 +        hs.append("0");
   1.107 +      }
   1.108 +      hs.append(stmp);
   1.109 +    }
   1.110 +
   1.111 +    return hs.toString();
   1.112 +  }
   1.113 +
   1.114 +  public static byte[] concatAll(byte[] first, byte[]... rest) {
   1.115 +    int totalLength = first.length;
   1.116 +    for (byte[] array : rest) {
   1.117 +      totalLength += array.length;
   1.118 +    }
   1.119 +
   1.120 +    byte[] result = new byte[totalLength];
   1.121 +    int offset = first.length;
   1.122 +
   1.123 +    System.arraycopy(first, 0, result, 0, offset);
   1.124 +
   1.125 +    for (byte[] array : rest) {
   1.126 +      System.arraycopy(array, 0, result, offset, array.length);
   1.127 +      offset += array.length;
   1.128 +    }
   1.129 +    return result;
   1.130 +  }
   1.131 +
   1.132 +  /**
   1.133 +   * Utility for Base64 decoding. Should ensure that the correct
   1.134 +   * Apache Commons version is used.
   1.135 +   *
   1.136 +   * @param base64
   1.137 +   *        An input string. Will be decoded as UTF-8.
   1.138 +   * @return
   1.139 +   *        A byte array of decoded values.
   1.140 +   * @throws UnsupportedEncodingException
   1.141 +   *         Should not occur.
   1.142 +   */
   1.143 +  public static byte[] decodeBase64(String base64) throws UnsupportedEncodingException {
   1.144 +    return Base64.decodeBase64(base64.getBytes("UTF-8"));
   1.145 +  }
   1.146 +
   1.147 +  @SuppressLint("DefaultLocale")
   1.148 +  public static byte[] decodeFriendlyBase32(String base32) {
   1.149 +    Base32 converter = new Base32();
   1.150 +    final String translated = base32.replace('8', 'l').replace('9', 'o');
   1.151 +    return converter.decode(translated.toUpperCase());
   1.152 +  }
   1.153 +
   1.154 +  public static byte[] hex2Byte(String str, int byteLength) {
   1.155 +    byte[] second = hex2Byte(str);
   1.156 +    if (second.length >= byteLength) {
   1.157 +      return second;
   1.158 +    }
   1.159 +    // New Java arrays are zeroed:
   1.160 +    // http://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.12.5
   1.161 +    byte[] first = new byte[byteLength - second.length];
   1.162 +    return Utils.concatAll(first, second);
   1.163 +  }
   1.164 +
   1.165 +  public static byte[] hex2Byte(String str) {
   1.166 +    if (str.length() % 2 == 1) {
   1.167 +      str = "0" + str;
   1.168 +    }
   1.169 +
   1.170 +    byte[] bytes = new byte[str.length() / 2];
   1.171 +    for (int i = 0; i < bytes.length; i++) {
   1.172 +      bytes[i] = (byte) Integer.parseInt(str.substring(2 * i, 2 * i + 2), 16);
   1.173 +    }
   1.174 +    return bytes;
   1.175 +  }
   1.176 +
   1.177 +  public static String millisecondsToDecimalSecondsString(long ms) {
   1.178 +    return millisecondsToDecimalSeconds(ms).toString();
   1.179 +  }
   1.180 +
   1.181 +  // For dumping into JSON without quotes.
   1.182 +  public static BigDecimal millisecondsToDecimalSeconds(long ms) {
   1.183 +    return new BigDecimal(ms).movePointLeft(3);
   1.184 +  }
   1.185 +
   1.186 +  // This lives until Bug 708956 lands, and we don't have to do it any more.
   1.187 +  public static long decimalSecondsToMilliseconds(String decimal) {
   1.188 +    try {
   1.189 +      return new BigDecimal(decimal).movePointRight(3).longValue();
   1.190 +    } catch (Exception e) {
   1.191 +      return -1;
   1.192 +    }
   1.193 +  }
   1.194 +
   1.195 +  // Oh, Java.
   1.196 +  public static long decimalSecondsToMilliseconds(Double decimal) {
   1.197 +    // Truncates towards 0.
   1.198 +    return (long)(decimal * 1000);
   1.199 +  }
   1.200 +
   1.201 +  public static long decimalSecondsToMilliseconds(Long decimal) {
   1.202 +    return decimal * 1000;
   1.203 +  }
   1.204 +
   1.205 +  public static long decimalSecondsToMilliseconds(Integer decimal) {
   1.206 +    return (long)(decimal * 1000);
   1.207 +  }
   1.208 +
   1.209 +  public static byte[] sha256(byte[] in)
   1.210 +      throws NoSuchAlgorithmException {
   1.211 +    MessageDigest sha1 = MessageDigest.getInstance("SHA-256");
   1.212 +    return sha1.digest(in);
   1.213 +  }
   1.214 +
   1.215 +  protected static byte[] sha1(final String utf8)
   1.216 +      throws NoSuchAlgorithmException, UnsupportedEncodingException {
   1.217 +    final byte[] bytes = utf8.getBytes("UTF-8");
   1.218 +    try {
   1.219 +      return NativeCrypto.sha1(bytes);
   1.220 +    } catch (final LinkageError e) {
   1.221 +      // This will throw UnsatisifiedLinkError (missing mozglue) the first time it is called, and
   1.222 +      // ClassNotDefFoundError, for the uninitialized NativeCrypto class, each subsequent time this
   1.223 +      // is called; LinkageError is their common ancestor.
   1.224 +      Logger.warn(LOG_TAG, "Got throwable stretching password using native sha1 implementation; " +
   1.225 +          "ignoring and using Java implementation.", e);
   1.226 +      final MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
   1.227 +      return sha1.digest(utf8.getBytes("UTF-8"));
   1.228 +    }
   1.229 +  }
   1.230 +
   1.231 +  protected static String sha1Base32(final String utf8)
   1.232 +      throws NoSuchAlgorithmException, UnsupportedEncodingException {
   1.233 +    return new Base32().encodeAsString(sha1(utf8)).toLowerCase(Locale.US);
   1.234 +  }
   1.235 +
   1.236 +  /**
   1.237 +   * If we encounter characters not allowed by the API (as found for
   1.238 +   * instance in an email address), hash the value.
   1.239 +   * @param account
   1.240 +   *        An account string.
   1.241 +   * @return
   1.242 +   *        An acceptable string.
   1.243 +   * @throws UnsupportedEncodingException
   1.244 +   * @throws NoSuchAlgorithmException
   1.245 +   */
   1.246 +  public static String usernameFromAccount(final String account) throws NoSuchAlgorithmException, UnsupportedEncodingException {
   1.247 +    if (account == null || account.equals("")) {
   1.248 +      throw new IllegalArgumentException("No account name provided.");
   1.249 +    }
   1.250 +    if (account.matches("^[A-Za-z0-9._-]+$")) {
   1.251 +      return account.toLowerCase(Locale.US);
   1.252 +    }
   1.253 +    return sha1Base32(account.toLowerCase(Locale.US));
   1.254 +  }
   1.255 +
   1.256 +  public static SharedPreferences getSharedPreferences(final Context context, final String product, final String username, final String serverURL, final String profile, final long version)
   1.257 +      throws NoSuchAlgorithmException, UnsupportedEncodingException {
   1.258 +    String prefsPath = getPrefsPath(product, username, serverURL, profile, version);
   1.259 +    return context.getSharedPreferences(prefsPath, SHARED_PREFERENCES_MODE);
   1.260 +  }
   1.261 +
   1.262 +  /**
   1.263 +   * Get shared preferences path for a Sync account.
   1.264 +   *
   1.265 +   * @param product the Firefox Sync product package name (like "org.mozilla.firefox").
   1.266 +   * @param username the Sync account name, optionally encoded with <code>Utils.usernameFromAccount</code>.
   1.267 +   * @param serverURL the Sync account server URL.
   1.268 +   * @param profile the Firefox profile name.
   1.269 +   * @param version the version of preferences to reference.
   1.270 +   * @return the path.
   1.271 +   * @throws NoSuchAlgorithmException
   1.272 +   * @throws UnsupportedEncodingException
   1.273 +   */
   1.274 +  public static String getPrefsPath(final String product, final String username, final String serverURL, final String profile, final long version)
   1.275 +      throws NoSuchAlgorithmException, UnsupportedEncodingException {
   1.276 +    final String encodedAccount = sha1Base32(serverURL + ":" + usernameFromAccount(username));
   1.277 +
   1.278 +    if (version <= 0) {
   1.279 +      return "sync.prefs." + encodedAccount;
   1.280 +    } else {
   1.281 +      final String sanitizedProduct = product.replace('.', '!').replace(' ', '!');
   1.282 +      return "sync.prefs." + sanitizedProduct + "." + encodedAccount + "." + profile + "." + version;
   1.283 +    }
   1.284 +  }
   1.285 +
   1.286 +  public static void addToIndexBucketMap(TreeMap<Long, ArrayList<String>> map, long index, String value) {
   1.287 +    ArrayList<String> bucket = map.get(index);
   1.288 +    if (bucket == null) {
   1.289 +      bucket = new ArrayList<String>();
   1.290 +    }
   1.291 +    bucket.add(value);
   1.292 +    map.put(index, bucket);
   1.293 +  }
   1.294 +
   1.295 +  /**
   1.296 +   * Yes, an equality method that's null-safe.
   1.297 +   */
   1.298 +  private static boolean same(Object a, Object b) {
   1.299 +    if (a == b) {
   1.300 +      return true;
   1.301 +    }
   1.302 +    if (a == null || b == null) {
   1.303 +      return false;      // If both null, case above applies.
   1.304 +    }
   1.305 +    return a.equals(b);
   1.306 +  }
   1.307 +
   1.308 +  /**
   1.309 +   * Return true if the two arrays are both null, or are both arrays
   1.310 +   * containing the same elements in the same order.
   1.311 +   */
   1.312 +  public static boolean sameArrays(JSONArray a, JSONArray b) {
   1.313 +    if (a == b) {
   1.314 +      return true;
   1.315 +    }
   1.316 +    if (a == null || b == null) {
   1.317 +      return false;
   1.318 +    }
   1.319 +    final int size = a.size();
   1.320 +    if (size != b.size()) {
   1.321 +      return false;
   1.322 +    }
   1.323 +    for (int i = 0; i < size; ++i) {
   1.324 +      if (!same(a.get(i), b.get(i))) {
   1.325 +        return false;
   1.326 +      }
   1.327 +    }
   1.328 +    return true;
   1.329 +  }
   1.330 +
   1.331 +  /**
   1.332 +   * Takes a URI, extracting URI components.
   1.333 +   * @param scheme the URI scheme on which to match.
   1.334 +   */
   1.335 +  @SuppressWarnings("deprecation")
   1.336 +  public static Map<String, String> extractURIComponents(String scheme, String uri) {
   1.337 +    if (uri.indexOf(scheme) != 0) {
   1.338 +      throw new IllegalArgumentException("URI scheme does not match: " + scheme);
   1.339 +    }
   1.340 +
   1.341 +    // Do this the hard way to avoid taking a large dependency on
   1.342 +    // HttpClient or getting all regex-tastic.
   1.343 +    String components = uri.substring(scheme.length());
   1.344 +    HashMap<String, String> out = new HashMap<String, String>();
   1.345 +    String[] parts = components.split("&");
   1.346 +    for (int i = 0; i < parts.length; ++i) {
   1.347 +      String part = parts[i];
   1.348 +      if (part.length() == 0) {
   1.349 +        continue;
   1.350 +      }
   1.351 +      String[] pair = part.split("=", 2);
   1.352 +      switch (pair.length) {
   1.353 +      case 0:
   1.354 +        continue;
   1.355 +      case 1:
   1.356 +        out.put(URLDecoder.decode(pair[0]), null);
   1.357 +        break;
   1.358 +      case 2:
   1.359 +        out.put(URLDecoder.decode(pair[0]), URLDecoder.decode(pair[1]));
   1.360 +        break;
   1.361 +      }
   1.362 +    }
   1.363 +    return out;
   1.364 +  }
   1.365 +
   1.366 +  // Because TextUtils.join is not stubbed.
   1.367 +  public static String toDelimitedString(String delimiter, Collection<? extends Object> items) {
   1.368 +    if (items == null || items.size() == 0) {
   1.369 +      return "";
   1.370 +    }
   1.371 +
   1.372 +    StringBuilder sb = new StringBuilder();
   1.373 +    int i = 0;
   1.374 +    int c = items.size();
   1.375 +    for (Object object : items) {
   1.376 +      sb.append(object.toString());
   1.377 +      if (++i < c) {
   1.378 +        sb.append(delimiter);
   1.379 +      }
   1.380 +    }
   1.381 +    return sb.toString();
   1.382 +  }
   1.383 +
   1.384 +  public static String toCommaSeparatedString(Collection<? extends Object> items) {
   1.385 +    return toDelimitedString(", ", items);
   1.386 +  }
   1.387 +
   1.388 +  /**
   1.389 +   * Names of stages to sync: (ALL intersect SYNC) intersect (ALL minus SKIP).
   1.390 +   *
   1.391 +   * @param knownStageNames collection of known stage names (set ALL above).
   1.392 +   * @param toSync set SYNC above, or <code>null</code> to sync all known stages.
   1.393 +   * @param toSkip set SKIP above, or <code>null</code> to not skip any stages.
   1.394 +   * @return stage names.
   1.395 +   */
   1.396 +  public static Collection<String> getStagesToSync(final Collection<String> knownStageNames, Collection<String> toSync, Collection<String> toSkip) {
   1.397 +    if (toSkip == null) {
   1.398 +      toSkip = new HashSet<String>();
   1.399 +    } else {
   1.400 +      toSkip = new HashSet<String>(toSkip);
   1.401 +    }
   1.402 +
   1.403 +    if (toSync == null) {
   1.404 +      toSync = new HashSet<String>(knownStageNames);
   1.405 +    } else {
   1.406 +      toSync = new HashSet<String>(toSync);
   1.407 +    }
   1.408 +    toSync.retainAll(knownStageNames);
   1.409 +    toSync.removeAll(toSkip);
   1.410 +    return toSync;
   1.411 +  }
   1.412 +
   1.413 +  /**
   1.414 +   * Get names of stages to sync: (ALL intersect SYNC) intersect (ALL minus SKIP).
   1.415 +   *
   1.416 +   * @param knownStageNames collection of known stage names (set ALL above).
   1.417 +   * @param extras
   1.418 +   *          a <code>Bundle</code> instance (possibly null) optionally containing keys
   1.419 +   *          <code>EXTRAS_KEY_STAGES_TO_SYNC</code> (set SYNC above) and
   1.420 +   *          <code>EXTRAS_KEY_STAGES_TO_SKIP</code> (set SKIP above).
   1.421 +   * @return stage names.
   1.422 +   */
   1.423 +  public static Collection<String> getStagesToSyncFromBundle(final Collection<String> knownStageNames, final Bundle extras) {
   1.424 +    if (extras == null) {
   1.425 +      return knownStageNames;
   1.426 +    }
   1.427 +    String toSyncString = extras.getString(Constants.EXTRAS_KEY_STAGES_TO_SYNC);
   1.428 +    String toSkipString = extras.getString(Constants.EXTRAS_KEY_STAGES_TO_SKIP);
   1.429 +    if (toSyncString == null && toSkipString == null) {
   1.430 +      return knownStageNames;
   1.431 +    }
   1.432 +
   1.433 +    ArrayList<String> toSync = null;
   1.434 +    ArrayList<String> toSkip = null;
   1.435 +    if (toSyncString != null) {
   1.436 +      try {
   1.437 +        toSync = new ArrayList<String>(ExtendedJSONObject.parseJSONObject(toSyncString).keySet());
   1.438 +      } catch (Exception e) {
   1.439 +        Logger.warn(LOG_TAG, "Got exception parsing stages to sync: '" + toSyncString + "'.", e);
   1.440 +      }
   1.441 +    }
   1.442 +    if (toSkipString != null) {
   1.443 +      try {
   1.444 +        toSkip = new ArrayList<String>(ExtendedJSONObject.parseJSONObject(toSkipString).keySet());
   1.445 +      } catch (Exception e) {
   1.446 +        Logger.warn(LOG_TAG, "Got exception parsing stages to skip: '" + toSkipString + "'.", e);
   1.447 +      }
   1.448 +    }
   1.449 +
   1.450 +    Logger.info(LOG_TAG, "Asked to sync '" + Utils.toCommaSeparatedString(toSync) +
   1.451 +                         "' and to skip '" + Utils.toCommaSeparatedString(toSkip) + "'.");
   1.452 +    return getStagesToSync(knownStageNames, toSync, toSkip);
   1.453 +  }
   1.454 +
   1.455 +  /**
   1.456 +   * Put names of stages to sync and to skip into sync extras bundle.
   1.457 +   *
   1.458 +   * @param bundle
   1.459 +   *          a <code>Bundle</code> instance (possibly null).
   1.460 +   * @param stagesToSync
   1.461 +   *          collection of stage names to sync: key
   1.462 +   *          <code>EXTRAS_KEY_STAGES_TO_SYNC</code>; ignored if <code>null</code>.
   1.463 +   * @param stagesToSkip
   1.464 +   *          collection of stage names to skip: key
   1.465 +   *          <code>EXTRAS_KEY_STAGES_TO_SKIP</code>; ignored if <code>null</code>.
   1.466 +   */
   1.467 +  public static void putStageNamesToSync(final Bundle bundle, final String[] stagesToSync, final String[] stagesToSkip) {
   1.468 +    if (bundle == null) {
   1.469 +      return;
   1.470 +    }
   1.471 +
   1.472 +    if (stagesToSync != null) {
   1.473 +      ExtendedJSONObject o = new ExtendedJSONObject();
   1.474 +      for (String stageName : stagesToSync) {
   1.475 +        o.put(stageName, 0);
   1.476 +      }
   1.477 +      bundle.putString(Constants.EXTRAS_KEY_STAGES_TO_SYNC, o.toJSONString());
   1.478 +    }
   1.479 +
   1.480 +    if (stagesToSkip != null) {
   1.481 +      ExtendedJSONObject o = new ExtendedJSONObject();
   1.482 +      for (String stageName : stagesToSkip) {
   1.483 +        o.put(stageName, 0);
   1.484 +      }
   1.485 +      bundle.putString(Constants.EXTRAS_KEY_STAGES_TO_SKIP, o.toJSONString());
   1.486 +    }
   1.487 +  }
   1.488 +
   1.489 +  /**
   1.490 +   * Read contents of file as a string.
   1.491 +   *
   1.492 +   * @param context Android context.
   1.493 +   * @param filename name of file to read; must not be null.
   1.494 +   * @return <code>String</code> instance.
   1.495 +   */
   1.496 +  public static String readFile(final Context context, final String filename) {
   1.497 +    if (filename == null) {
   1.498 +      throw new IllegalArgumentException("Passed null filename in readFile.");
   1.499 +    }
   1.500 +
   1.501 +    FileInputStream fis = null;
   1.502 +    InputStreamReader isr = null;
   1.503 +    BufferedReader br = null;
   1.504 +
   1.505 +    try {
   1.506 +      fis = context.openFileInput(filename);
   1.507 +      isr = new InputStreamReader(fis);
   1.508 +      br = new BufferedReader(isr);
   1.509 +      StringBuilder sb = new StringBuilder();
   1.510 +      String line;
   1.511 +      while ((line = br.readLine()) != null) {
   1.512 +        sb.append(line);
   1.513 +      }
   1.514 +      return sb.toString();
   1.515 +    } catch (Exception e) {
   1.516 +      return null;
   1.517 +    } finally {
   1.518 +      if (isr != null) {
   1.519 +        try {
   1.520 +          isr.close();
   1.521 +        } catch (IOException e) {
   1.522 +          // Ignore.
   1.523 +        }
   1.524 +      }
   1.525 +      if (fis != null) {
   1.526 +        try {
   1.527 +          fis.close();
   1.528 +        } catch (IOException e) {
   1.529 +          // Ignore.
   1.530 +        }
   1.531 +      }
   1.532 +    }
   1.533 +  }
   1.534 +
   1.535 +  /**
   1.536 +   * Format a duration as a string, like "0.56 seconds".
   1.537 +   *
   1.538 +   * @param startMillis start time in milliseconds.
   1.539 +   * @param endMillis end time in milliseconds.
   1.540 +   * @return formatted string.
   1.541 +   */
   1.542 +  public static String formatDuration(long startMillis, long endMillis) {
   1.543 +    final long duration = endMillis - startMillis;
   1.544 +    return new DecimalFormat("#0.00 seconds").format(((double) duration) / 1000);
   1.545 +  }
   1.546 +
   1.547 +  /**
   1.548 +   * This will take a string containing a UTF-8 representation of a UTF-8
   1.549 +   * byte array — e.g., "pïgéons1" — and return UTF-8 (e.g., "pïgéons1").
   1.550 +   *
   1.551 +   * This is the format produced by desktop Firefox when exchanging credentials
   1.552 +   * containing non-ASCII characters.
   1.553 +   */
   1.554 +  public static String decodeUTF8(final String in) throws UnsupportedEncodingException {
   1.555 +    final int length = in.length();
   1.556 +    final byte[] asciiBytes = new byte[length];
   1.557 +    for (int i = 0; i < length; ++i) {
   1.558 +      asciiBytes[i] = (byte) in.codePointAt(i);
   1.559 +    }
   1.560 +    return new String(asciiBytes, "UTF-8");
   1.561 +  }
   1.562 +
   1.563 +  /**
   1.564 +   * Replace "foo@bar.com" with "XXX@XXX.XXX".
   1.565 +   */
   1.566 +  public static String obfuscateEmail(final String in) {
   1.567 +    return in.replaceAll("[^@\\.]", "X");
   1.568 +  }
   1.569 +
   1.570 +  public static String nodeWeaveURL(String serverURL, String username) {
   1.571 +    String userPart = username + "/node/weave";
   1.572 +    if (serverURL == null) {
   1.573 +      return SyncConstants.DEFAULT_AUTH_SERVER + "user/1.0/" + userPart;
   1.574 +    }
   1.575 +    if (!serverURL.endsWith("/")) {
   1.576 +      serverURL = serverURL + "/";
   1.577 +    }
   1.578 +    return serverURL + "user/1.0/" + userPart;
   1.579 +  }
   1.580 +
   1.581 +  public static void throwIfNull(Object... objects) {
   1.582 +    for (Object object : objects) {
   1.583 +      if (object == null) {
   1.584 +        throw new IllegalArgumentException("object must not be null");
   1.585 +      }
   1.586 +    }
   1.587 +  }
   1.588 +
   1.589 +  /**
   1.590 +   * Gecko uses locale codes like "es-ES", whereas a Java {@link Locale}
   1.591 +   * stringifies as "es_ES".
   1.592 +   *
   1.593 +   * This method approximates the Java 7 method <code>Locale#toLanguageTag()</code>.
   1.594 +   * <p>
   1.595 +   * <b>Warning:</b> all consumers of this method will need to be audited when
   1.596 +   * we have active locale switching.
   1.597 +   *
   1.598 +   * @return a locale string suitable for passing to Gecko.
   1.599 +   */
   1.600 +  public static String getLanguageTag(final Locale locale) {
   1.601 +    // If this were Java 7:
   1.602 +    // return locale.toLanguageTag();
   1.603 +
   1.604 +    String language = locale.getLanguage();  // Can, but should never be, an empty string.
   1.605 +    // Modernize certain language codes.
   1.606 +    if (language.equals("iw")) {
   1.607 +      language = "he";
   1.608 +    } else if (language.equals("in")) {
   1.609 +      language = "id";
   1.610 +    } else if (language.equals("ji")) {
   1.611 +      language = "yi";
   1.612 +    }
   1.613 +
   1.614 +    String country = locale.getCountry();    // Can be an empty string.
   1.615 +    if (country.equals("")) {
   1.616 +      return language;
   1.617 +    }
   1.618 +    return language + "-" + country;
   1.619 +  }
   1.620 +}

mercurial