mobile/android/base/sync/Utils.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 package org.mozilla.gecko.sync;
michael@0 6
michael@0 7 import java.io.BufferedReader;
michael@0 8 import java.io.FileInputStream;
michael@0 9 import java.io.IOException;
michael@0 10 import java.io.InputStreamReader;
michael@0 11 import java.io.UnsupportedEncodingException;
michael@0 12 import java.math.BigDecimal;
michael@0 13 import java.math.BigInteger;
michael@0 14 import java.net.URLDecoder;
michael@0 15 import java.security.MessageDigest;
michael@0 16 import java.security.NoSuchAlgorithmException;
michael@0 17 import java.security.SecureRandom;
michael@0 18 import java.text.DecimalFormat;
michael@0 19 import java.util.ArrayList;
michael@0 20 import java.util.Collection;
michael@0 21 import java.util.HashMap;
michael@0 22 import java.util.HashSet;
michael@0 23 import java.util.Locale;
michael@0 24 import java.util.Map;
michael@0 25 import java.util.TreeMap;
michael@0 26
michael@0 27 import org.json.simple.JSONArray;
michael@0 28 import org.mozilla.apache.commons.codec.binary.Base32;
michael@0 29 import org.mozilla.apache.commons.codec.binary.Base64;
michael@0 30 import org.mozilla.gecko.background.common.log.Logger;
michael@0 31 import org.mozilla.gecko.background.nativecode.NativeCrypto;
michael@0 32 import org.mozilla.gecko.sync.setup.Constants;
michael@0 33
michael@0 34 import android.annotation.SuppressLint;
michael@0 35 import android.content.Context;
michael@0 36 import android.content.SharedPreferences;
michael@0 37 import android.os.Bundle;
michael@0 38
michael@0 39 public class Utils {
michael@0 40
michael@0 41 private static final String LOG_TAG = "Utils";
michael@0 42
michael@0 43 private static SecureRandom sharedSecureRandom = new SecureRandom();
michael@0 44
michael@0 45 // See <http://developer.android.com/reference/android/content/Context.html#getSharedPreferences%28java.lang.String,%20int%29>
michael@0 46 public static final int SHARED_PREFERENCES_MODE = 0;
michael@0 47
michael@0 48 public static String generateGuid() {
michael@0 49 byte[] encodedBytes = Base64.encodeBase64(generateRandomBytes(9), false);
michael@0 50 return new String(encodedBytes).replace("+", "-").replace("/", "_");
michael@0 51 }
michael@0 52
michael@0 53 /**
michael@0 54 * Helper to generate secure random bytes.
michael@0 55 *
michael@0 56 * @param length
michael@0 57 * Number of bytes to generate.
michael@0 58 */
michael@0 59 public static byte[] generateRandomBytes(int length) {
michael@0 60 byte[] bytes = new byte[length];
michael@0 61 sharedSecureRandom.nextBytes(bytes);
michael@0 62 return bytes;
michael@0 63 }
michael@0 64
michael@0 65 /**
michael@0 66 * Helper to generate a random integer in a specified range.
michael@0 67 *
michael@0 68 * @param r
michael@0 69 * Generate an integer between 0 and r-1 inclusive.
michael@0 70 */
michael@0 71 public static BigInteger generateBigIntegerLessThan(BigInteger r) {
michael@0 72 int maxBytes = (int) Math.ceil(((double) r.bitLength()) / 8);
michael@0 73 BigInteger randInt = new BigInteger(generateRandomBytes(maxBytes));
michael@0 74 return randInt.mod(r);
michael@0 75 }
michael@0 76
michael@0 77 /**
michael@0 78 * Helper to reseed the shared secure random number generator.
michael@0 79 */
michael@0 80 public static void reseedSharedRandom() {
michael@0 81 sharedSecureRandom.setSeed(sharedSecureRandom.generateSeed(8));
michael@0 82 }
michael@0 83
michael@0 84 /**
michael@0 85 * Helper to convert a byte array to a hex-encoded string
michael@0 86 */
michael@0 87 public static String byte2Hex(final byte[] b) {
michael@0 88 return byte2Hex(b, 2 * b.length);
michael@0 89 }
michael@0 90
michael@0 91 public static String byte2Hex(final byte[] b, int hexLength) {
michael@0 92 final StringBuilder hs = new StringBuilder(Math.max(2*b.length, hexLength));
michael@0 93 String stmp;
michael@0 94
michael@0 95 for (int n = 0; n < hexLength - 2*b.length; n++) {
michael@0 96 hs.append("0");
michael@0 97 }
michael@0 98
michael@0 99 for (int n = 0; n < b.length; n++) {
michael@0 100 stmp = Integer.toHexString(b[n] & 0XFF);
michael@0 101
michael@0 102 if (stmp.length() == 1) {
michael@0 103 hs.append("0");
michael@0 104 }
michael@0 105 hs.append(stmp);
michael@0 106 }
michael@0 107
michael@0 108 return hs.toString();
michael@0 109 }
michael@0 110
michael@0 111 public static byte[] concatAll(byte[] first, byte[]... rest) {
michael@0 112 int totalLength = first.length;
michael@0 113 for (byte[] array : rest) {
michael@0 114 totalLength += array.length;
michael@0 115 }
michael@0 116
michael@0 117 byte[] result = new byte[totalLength];
michael@0 118 int offset = first.length;
michael@0 119
michael@0 120 System.arraycopy(first, 0, result, 0, offset);
michael@0 121
michael@0 122 for (byte[] array : rest) {
michael@0 123 System.arraycopy(array, 0, result, offset, array.length);
michael@0 124 offset += array.length;
michael@0 125 }
michael@0 126 return result;
michael@0 127 }
michael@0 128
michael@0 129 /**
michael@0 130 * Utility for Base64 decoding. Should ensure that the correct
michael@0 131 * Apache Commons version is used.
michael@0 132 *
michael@0 133 * @param base64
michael@0 134 * An input string. Will be decoded as UTF-8.
michael@0 135 * @return
michael@0 136 * A byte array of decoded values.
michael@0 137 * @throws UnsupportedEncodingException
michael@0 138 * Should not occur.
michael@0 139 */
michael@0 140 public static byte[] decodeBase64(String base64) throws UnsupportedEncodingException {
michael@0 141 return Base64.decodeBase64(base64.getBytes("UTF-8"));
michael@0 142 }
michael@0 143
michael@0 144 @SuppressLint("DefaultLocale")
michael@0 145 public static byte[] decodeFriendlyBase32(String base32) {
michael@0 146 Base32 converter = new Base32();
michael@0 147 final String translated = base32.replace('8', 'l').replace('9', 'o');
michael@0 148 return converter.decode(translated.toUpperCase());
michael@0 149 }
michael@0 150
michael@0 151 public static byte[] hex2Byte(String str, int byteLength) {
michael@0 152 byte[] second = hex2Byte(str);
michael@0 153 if (second.length >= byteLength) {
michael@0 154 return second;
michael@0 155 }
michael@0 156 // New Java arrays are zeroed:
michael@0 157 // http://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.12.5
michael@0 158 byte[] first = new byte[byteLength - second.length];
michael@0 159 return Utils.concatAll(first, second);
michael@0 160 }
michael@0 161
michael@0 162 public static byte[] hex2Byte(String str) {
michael@0 163 if (str.length() % 2 == 1) {
michael@0 164 str = "0" + str;
michael@0 165 }
michael@0 166
michael@0 167 byte[] bytes = new byte[str.length() / 2];
michael@0 168 for (int i = 0; i < bytes.length; i++) {
michael@0 169 bytes[i] = (byte) Integer.parseInt(str.substring(2 * i, 2 * i + 2), 16);
michael@0 170 }
michael@0 171 return bytes;
michael@0 172 }
michael@0 173
michael@0 174 public static String millisecondsToDecimalSecondsString(long ms) {
michael@0 175 return millisecondsToDecimalSeconds(ms).toString();
michael@0 176 }
michael@0 177
michael@0 178 // For dumping into JSON without quotes.
michael@0 179 public static BigDecimal millisecondsToDecimalSeconds(long ms) {
michael@0 180 return new BigDecimal(ms).movePointLeft(3);
michael@0 181 }
michael@0 182
michael@0 183 // This lives until Bug 708956 lands, and we don't have to do it any more.
michael@0 184 public static long decimalSecondsToMilliseconds(String decimal) {
michael@0 185 try {
michael@0 186 return new BigDecimal(decimal).movePointRight(3).longValue();
michael@0 187 } catch (Exception e) {
michael@0 188 return -1;
michael@0 189 }
michael@0 190 }
michael@0 191
michael@0 192 // Oh, Java.
michael@0 193 public static long decimalSecondsToMilliseconds(Double decimal) {
michael@0 194 // Truncates towards 0.
michael@0 195 return (long)(decimal * 1000);
michael@0 196 }
michael@0 197
michael@0 198 public static long decimalSecondsToMilliseconds(Long decimal) {
michael@0 199 return decimal * 1000;
michael@0 200 }
michael@0 201
michael@0 202 public static long decimalSecondsToMilliseconds(Integer decimal) {
michael@0 203 return (long)(decimal * 1000);
michael@0 204 }
michael@0 205
michael@0 206 public static byte[] sha256(byte[] in)
michael@0 207 throws NoSuchAlgorithmException {
michael@0 208 MessageDigest sha1 = MessageDigest.getInstance("SHA-256");
michael@0 209 return sha1.digest(in);
michael@0 210 }
michael@0 211
michael@0 212 protected static byte[] sha1(final String utf8)
michael@0 213 throws NoSuchAlgorithmException, UnsupportedEncodingException {
michael@0 214 final byte[] bytes = utf8.getBytes("UTF-8");
michael@0 215 try {
michael@0 216 return NativeCrypto.sha1(bytes);
michael@0 217 } catch (final LinkageError e) {
michael@0 218 // This will throw UnsatisifiedLinkError (missing mozglue) the first time it is called, and
michael@0 219 // ClassNotDefFoundError, for the uninitialized NativeCrypto class, each subsequent time this
michael@0 220 // is called; LinkageError is their common ancestor.
michael@0 221 Logger.warn(LOG_TAG, "Got throwable stretching password using native sha1 implementation; " +
michael@0 222 "ignoring and using Java implementation.", e);
michael@0 223 final MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
michael@0 224 return sha1.digest(utf8.getBytes("UTF-8"));
michael@0 225 }
michael@0 226 }
michael@0 227
michael@0 228 protected static String sha1Base32(final String utf8)
michael@0 229 throws NoSuchAlgorithmException, UnsupportedEncodingException {
michael@0 230 return new Base32().encodeAsString(sha1(utf8)).toLowerCase(Locale.US);
michael@0 231 }
michael@0 232
michael@0 233 /**
michael@0 234 * If we encounter characters not allowed by the API (as found for
michael@0 235 * instance in an email address), hash the value.
michael@0 236 * @param account
michael@0 237 * An account string.
michael@0 238 * @return
michael@0 239 * An acceptable string.
michael@0 240 * @throws UnsupportedEncodingException
michael@0 241 * @throws NoSuchAlgorithmException
michael@0 242 */
michael@0 243 public static String usernameFromAccount(final String account) throws NoSuchAlgorithmException, UnsupportedEncodingException {
michael@0 244 if (account == null || account.equals("")) {
michael@0 245 throw new IllegalArgumentException("No account name provided.");
michael@0 246 }
michael@0 247 if (account.matches("^[A-Za-z0-9._-]+$")) {
michael@0 248 return account.toLowerCase(Locale.US);
michael@0 249 }
michael@0 250 return sha1Base32(account.toLowerCase(Locale.US));
michael@0 251 }
michael@0 252
michael@0 253 public static SharedPreferences getSharedPreferences(final Context context, final String product, final String username, final String serverURL, final String profile, final long version)
michael@0 254 throws NoSuchAlgorithmException, UnsupportedEncodingException {
michael@0 255 String prefsPath = getPrefsPath(product, username, serverURL, profile, version);
michael@0 256 return context.getSharedPreferences(prefsPath, SHARED_PREFERENCES_MODE);
michael@0 257 }
michael@0 258
michael@0 259 /**
michael@0 260 * Get shared preferences path for a Sync account.
michael@0 261 *
michael@0 262 * @param product the Firefox Sync product package name (like "org.mozilla.firefox").
michael@0 263 * @param username the Sync account name, optionally encoded with <code>Utils.usernameFromAccount</code>.
michael@0 264 * @param serverURL the Sync account server URL.
michael@0 265 * @param profile the Firefox profile name.
michael@0 266 * @param version the version of preferences to reference.
michael@0 267 * @return the path.
michael@0 268 * @throws NoSuchAlgorithmException
michael@0 269 * @throws UnsupportedEncodingException
michael@0 270 */
michael@0 271 public static String getPrefsPath(final String product, final String username, final String serverURL, final String profile, final long version)
michael@0 272 throws NoSuchAlgorithmException, UnsupportedEncodingException {
michael@0 273 final String encodedAccount = sha1Base32(serverURL + ":" + usernameFromAccount(username));
michael@0 274
michael@0 275 if (version <= 0) {
michael@0 276 return "sync.prefs." + encodedAccount;
michael@0 277 } else {
michael@0 278 final String sanitizedProduct = product.replace('.', '!').replace(' ', '!');
michael@0 279 return "sync.prefs." + sanitizedProduct + "." + encodedAccount + "." + profile + "." + version;
michael@0 280 }
michael@0 281 }
michael@0 282
michael@0 283 public static void addToIndexBucketMap(TreeMap<Long, ArrayList<String>> map, long index, String value) {
michael@0 284 ArrayList<String> bucket = map.get(index);
michael@0 285 if (bucket == null) {
michael@0 286 bucket = new ArrayList<String>();
michael@0 287 }
michael@0 288 bucket.add(value);
michael@0 289 map.put(index, bucket);
michael@0 290 }
michael@0 291
michael@0 292 /**
michael@0 293 * Yes, an equality method that's null-safe.
michael@0 294 */
michael@0 295 private static boolean same(Object a, Object b) {
michael@0 296 if (a == b) {
michael@0 297 return true;
michael@0 298 }
michael@0 299 if (a == null || b == null) {
michael@0 300 return false; // If both null, case above applies.
michael@0 301 }
michael@0 302 return a.equals(b);
michael@0 303 }
michael@0 304
michael@0 305 /**
michael@0 306 * Return true if the two arrays are both null, or are both arrays
michael@0 307 * containing the same elements in the same order.
michael@0 308 */
michael@0 309 public static boolean sameArrays(JSONArray a, JSONArray b) {
michael@0 310 if (a == b) {
michael@0 311 return true;
michael@0 312 }
michael@0 313 if (a == null || b == null) {
michael@0 314 return false;
michael@0 315 }
michael@0 316 final int size = a.size();
michael@0 317 if (size != b.size()) {
michael@0 318 return false;
michael@0 319 }
michael@0 320 for (int i = 0; i < size; ++i) {
michael@0 321 if (!same(a.get(i), b.get(i))) {
michael@0 322 return false;
michael@0 323 }
michael@0 324 }
michael@0 325 return true;
michael@0 326 }
michael@0 327
michael@0 328 /**
michael@0 329 * Takes a URI, extracting URI components.
michael@0 330 * @param scheme the URI scheme on which to match.
michael@0 331 */
michael@0 332 @SuppressWarnings("deprecation")
michael@0 333 public static Map<String, String> extractURIComponents(String scheme, String uri) {
michael@0 334 if (uri.indexOf(scheme) != 0) {
michael@0 335 throw new IllegalArgumentException("URI scheme does not match: " + scheme);
michael@0 336 }
michael@0 337
michael@0 338 // Do this the hard way to avoid taking a large dependency on
michael@0 339 // HttpClient or getting all regex-tastic.
michael@0 340 String components = uri.substring(scheme.length());
michael@0 341 HashMap<String, String> out = new HashMap<String, String>();
michael@0 342 String[] parts = components.split("&");
michael@0 343 for (int i = 0; i < parts.length; ++i) {
michael@0 344 String part = parts[i];
michael@0 345 if (part.length() == 0) {
michael@0 346 continue;
michael@0 347 }
michael@0 348 String[] pair = part.split("=", 2);
michael@0 349 switch (pair.length) {
michael@0 350 case 0:
michael@0 351 continue;
michael@0 352 case 1:
michael@0 353 out.put(URLDecoder.decode(pair[0]), null);
michael@0 354 break;
michael@0 355 case 2:
michael@0 356 out.put(URLDecoder.decode(pair[0]), URLDecoder.decode(pair[1]));
michael@0 357 break;
michael@0 358 }
michael@0 359 }
michael@0 360 return out;
michael@0 361 }
michael@0 362
michael@0 363 // Because TextUtils.join is not stubbed.
michael@0 364 public static String toDelimitedString(String delimiter, Collection<? extends Object> items) {
michael@0 365 if (items == null || items.size() == 0) {
michael@0 366 return "";
michael@0 367 }
michael@0 368
michael@0 369 StringBuilder sb = new StringBuilder();
michael@0 370 int i = 0;
michael@0 371 int c = items.size();
michael@0 372 for (Object object : items) {
michael@0 373 sb.append(object.toString());
michael@0 374 if (++i < c) {
michael@0 375 sb.append(delimiter);
michael@0 376 }
michael@0 377 }
michael@0 378 return sb.toString();
michael@0 379 }
michael@0 380
michael@0 381 public static String toCommaSeparatedString(Collection<? extends Object> items) {
michael@0 382 return toDelimitedString(", ", items);
michael@0 383 }
michael@0 384
michael@0 385 /**
michael@0 386 * Names of stages to sync: (ALL intersect SYNC) intersect (ALL minus SKIP).
michael@0 387 *
michael@0 388 * @param knownStageNames collection of known stage names (set ALL above).
michael@0 389 * @param toSync set SYNC above, or <code>null</code> to sync all known stages.
michael@0 390 * @param toSkip set SKIP above, or <code>null</code> to not skip any stages.
michael@0 391 * @return stage names.
michael@0 392 */
michael@0 393 public static Collection<String> getStagesToSync(final Collection<String> knownStageNames, Collection<String> toSync, Collection<String> toSkip) {
michael@0 394 if (toSkip == null) {
michael@0 395 toSkip = new HashSet<String>();
michael@0 396 } else {
michael@0 397 toSkip = new HashSet<String>(toSkip);
michael@0 398 }
michael@0 399
michael@0 400 if (toSync == null) {
michael@0 401 toSync = new HashSet<String>(knownStageNames);
michael@0 402 } else {
michael@0 403 toSync = new HashSet<String>(toSync);
michael@0 404 }
michael@0 405 toSync.retainAll(knownStageNames);
michael@0 406 toSync.removeAll(toSkip);
michael@0 407 return toSync;
michael@0 408 }
michael@0 409
michael@0 410 /**
michael@0 411 * Get names of stages to sync: (ALL intersect SYNC) intersect (ALL minus SKIP).
michael@0 412 *
michael@0 413 * @param knownStageNames collection of known stage names (set ALL above).
michael@0 414 * @param extras
michael@0 415 * a <code>Bundle</code> instance (possibly null) optionally containing keys
michael@0 416 * <code>EXTRAS_KEY_STAGES_TO_SYNC</code> (set SYNC above) and
michael@0 417 * <code>EXTRAS_KEY_STAGES_TO_SKIP</code> (set SKIP above).
michael@0 418 * @return stage names.
michael@0 419 */
michael@0 420 public static Collection<String> getStagesToSyncFromBundle(final Collection<String> knownStageNames, final Bundle extras) {
michael@0 421 if (extras == null) {
michael@0 422 return knownStageNames;
michael@0 423 }
michael@0 424 String toSyncString = extras.getString(Constants.EXTRAS_KEY_STAGES_TO_SYNC);
michael@0 425 String toSkipString = extras.getString(Constants.EXTRAS_KEY_STAGES_TO_SKIP);
michael@0 426 if (toSyncString == null && toSkipString == null) {
michael@0 427 return knownStageNames;
michael@0 428 }
michael@0 429
michael@0 430 ArrayList<String> toSync = null;
michael@0 431 ArrayList<String> toSkip = null;
michael@0 432 if (toSyncString != null) {
michael@0 433 try {
michael@0 434 toSync = new ArrayList<String>(ExtendedJSONObject.parseJSONObject(toSyncString).keySet());
michael@0 435 } catch (Exception e) {
michael@0 436 Logger.warn(LOG_TAG, "Got exception parsing stages to sync: '" + toSyncString + "'.", e);
michael@0 437 }
michael@0 438 }
michael@0 439 if (toSkipString != null) {
michael@0 440 try {
michael@0 441 toSkip = new ArrayList<String>(ExtendedJSONObject.parseJSONObject(toSkipString).keySet());
michael@0 442 } catch (Exception e) {
michael@0 443 Logger.warn(LOG_TAG, "Got exception parsing stages to skip: '" + toSkipString + "'.", e);
michael@0 444 }
michael@0 445 }
michael@0 446
michael@0 447 Logger.info(LOG_TAG, "Asked to sync '" + Utils.toCommaSeparatedString(toSync) +
michael@0 448 "' and to skip '" + Utils.toCommaSeparatedString(toSkip) + "'.");
michael@0 449 return getStagesToSync(knownStageNames, toSync, toSkip);
michael@0 450 }
michael@0 451
michael@0 452 /**
michael@0 453 * Put names of stages to sync and to skip into sync extras bundle.
michael@0 454 *
michael@0 455 * @param bundle
michael@0 456 * a <code>Bundle</code> instance (possibly null).
michael@0 457 * @param stagesToSync
michael@0 458 * collection of stage names to sync: key
michael@0 459 * <code>EXTRAS_KEY_STAGES_TO_SYNC</code>; ignored if <code>null</code>.
michael@0 460 * @param stagesToSkip
michael@0 461 * collection of stage names to skip: key
michael@0 462 * <code>EXTRAS_KEY_STAGES_TO_SKIP</code>; ignored if <code>null</code>.
michael@0 463 */
michael@0 464 public static void putStageNamesToSync(final Bundle bundle, final String[] stagesToSync, final String[] stagesToSkip) {
michael@0 465 if (bundle == null) {
michael@0 466 return;
michael@0 467 }
michael@0 468
michael@0 469 if (stagesToSync != null) {
michael@0 470 ExtendedJSONObject o = new ExtendedJSONObject();
michael@0 471 for (String stageName : stagesToSync) {
michael@0 472 o.put(stageName, 0);
michael@0 473 }
michael@0 474 bundle.putString(Constants.EXTRAS_KEY_STAGES_TO_SYNC, o.toJSONString());
michael@0 475 }
michael@0 476
michael@0 477 if (stagesToSkip != null) {
michael@0 478 ExtendedJSONObject o = new ExtendedJSONObject();
michael@0 479 for (String stageName : stagesToSkip) {
michael@0 480 o.put(stageName, 0);
michael@0 481 }
michael@0 482 bundle.putString(Constants.EXTRAS_KEY_STAGES_TO_SKIP, o.toJSONString());
michael@0 483 }
michael@0 484 }
michael@0 485
michael@0 486 /**
michael@0 487 * Read contents of file as a string.
michael@0 488 *
michael@0 489 * @param context Android context.
michael@0 490 * @param filename name of file to read; must not be null.
michael@0 491 * @return <code>String</code> instance.
michael@0 492 */
michael@0 493 public static String readFile(final Context context, final String filename) {
michael@0 494 if (filename == null) {
michael@0 495 throw new IllegalArgumentException("Passed null filename in readFile.");
michael@0 496 }
michael@0 497
michael@0 498 FileInputStream fis = null;
michael@0 499 InputStreamReader isr = null;
michael@0 500 BufferedReader br = null;
michael@0 501
michael@0 502 try {
michael@0 503 fis = context.openFileInput(filename);
michael@0 504 isr = new InputStreamReader(fis);
michael@0 505 br = new BufferedReader(isr);
michael@0 506 StringBuilder sb = new StringBuilder();
michael@0 507 String line;
michael@0 508 while ((line = br.readLine()) != null) {
michael@0 509 sb.append(line);
michael@0 510 }
michael@0 511 return sb.toString();
michael@0 512 } catch (Exception e) {
michael@0 513 return null;
michael@0 514 } finally {
michael@0 515 if (isr != null) {
michael@0 516 try {
michael@0 517 isr.close();
michael@0 518 } catch (IOException e) {
michael@0 519 // Ignore.
michael@0 520 }
michael@0 521 }
michael@0 522 if (fis != null) {
michael@0 523 try {
michael@0 524 fis.close();
michael@0 525 } catch (IOException e) {
michael@0 526 // Ignore.
michael@0 527 }
michael@0 528 }
michael@0 529 }
michael@0 530 }
michael@0 531
michael@0 532 /**
michael@0 533 * Format a duration as a string, like "0.56 seconds".
michael@0 534 *
michael@0 535 * @param startMillis start time in milliseconds.
michael@0 536 * @param endMillis end time in milliseconds.
michael@0 537 * @return formatted string.
michael@0 538 */
michael@0 539 public static String formatDuration(long startMillis, long endMillis) {
michael@0 540 final long duration = endMillis - startMillis;
michael@0 541 return new DecimalFormat("#0.00 seconds").format(((double) duration) / 1000);
michael@0 542 }
michael@0 543
michael@0 544 /**
michael@0 545 * This will take a string containing a UTF-8 representation of a UTF-8
michael@0 546 * byte array — e.g., "pïgéons1" — and return UTF-8 (e.g., "pïgéons1").
michael@0 547 *
michael@0 548 * This is the format produced by desktop Firefox when exchanging credentials
michael@0 549 * containing non-ASCII characters.
michael@0 550 */
michael@0 551 public static String decodeUTF8(final String in) throws UnsupportedEncodingException {
michael@0 552 final int length = in.length();
michael@0 553 final byte[] asciiBytes = new byte[length];
michael@0 554 for (int i = 0; i < length; ++i) {
michael@0 555 asciiBytes[i] = (byte) in.codePointAt(i);
michael@0 556 }
michael@0 557 return new String(asciiBytes, "UTF-8");
michael@0 558 }
michael@0 559
michael@0 560 /**
michael@0 561 * Replace "foo@bar.com" with "XXX@XXX.XXX".
michael@0 562 */
michael@0 563 public static String obfuscateEmail(final String in) {
michael@0 564 return in.replaceAll("[^@\\.]", "X");
michael@0 565 }
michael@0 566
michael@0 567 public static String nodeWeaveURL(String serverURL, String username) {
michael@0 568 String userPart = username + "/node/weave";
michael@0 569 if (serverURL == null) {
michael@0 570 return SyncConstants.DEFAULT_AUTH_SERVER + "user/1.0/" + userPart;
michael@0 571 }
michael@0 572 if (!serverURL.endsWith("/")) {
michael@0 573 serverURL = serverURL + "/";
michael@0 574 }
michael@0 575 return serverURL + "user/1.0/" + userPart;
michael@0 576 }
michael@0 577
michael@0 578 public static void throwIfNull(Object... objects) {
michael@0 579 for (Object object : objects) {
michael@0 580 if (object == null) {
michael@0 581 throw new IllegalArgumentException("object must not be null");
michael@0 582 }
michael@0 583 }
michael@0 584 }
michael@0 585
michael@0 586 /**
michael@0 587 * Gecko uses locale codes like "es-ES", whereas a Java {@link Locale}
michael@0 588 * stringifies as "es_ES".
michael@0 589 *
michael@0 590 * This method approximates the Java 7 method <code>Locale#toLanguageTag()</code>.
michael@0 591 * <p>
michael@0 592 * <b>Warning:</b> all consumers of this method will need to be audited when
michael@0 593 * we have active locale switching.
michael@0 594 *
michael@0 595 * @return a locale string suitable for passing to Gecko.
michael@0 596 */
michael@0 597 public static String getLanguageTag(final Locale locale) {
michael@0 598 // If this were Java 7:
michael@0 599 // return locale.toLanguageTag();
michael@0 600
michael@0 601 String language = locale.getLanguage(); // Can, but should never be, an empty string.
michael@0 602 // Modernize certain language codes.
michael@0 603 if (language.equals("iw")) {
michael@0 604 language = "he";
michael@0 605 } else if (language.equals("in")) {
michael@0 606 language = "id";
michael@0 607 } else if (language.equals("ji")) {
michael@0 608 language = "yi";
michael@0 609 }
michael@0 610
michael@0 611 String country = locale.getCountry(); // Can be an empty string.
michael@0 612 if (country.equals("")) {
michael@0 613 return language;
michael@0 614 }
michael@0 615 return language + "-" + country;
michael@0 616 }
michael@0 617 }

mercurial