1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/fxa/FirefoxAccounts.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,267 @@ 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.fxa; 1.9 + 1.10 +import java.io.File; 1.11 +import java.util.EnumSet; 1.12 +import java.util.concurrent.CountDownLatch; 1.13 + 1.14 +import org.mozilla.gecko.background.common.log.Logger; 1.15 +import org.mozilla.gecko.fxa.authenticator.AccountPickler; 1.16 +import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; 1.17 +import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter; 1.18 +import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper; 1.19 +import org.mozilla.gecko.sync.ThreadPool; 1.20 +import org.mozilla.gecko.sync.Utils; 1.21 + 1.22 +import android.accounts.Account; 1.23 +import android.accounts.AccountManager; 1.24 +import android.content.ContentResolver; 1.25 +import android.content.Context; 1.26 +import android.os.Bundle; 1.27 + 1.28 +/** 1.29 + * Simple public accessors for Firefox account objects. 1.30 + */ 1.31 +public class FirefoxAccounts { 1.32 + private static final String LOG_TAG = FirefoxAccounts.class.getSimpleName(); 1.33 + 1.34 + public enum SyncHint { 1.35 + /** 1.36 + * Hint that a requested sync is preferred immediately. 1.37 + * <p> 1.38 + * On many devices, not including <code>SCHEDULE_NOW</code> means a delay of 1.39 + * at least 30 seconds. 1.40 + */ 1.41 + SCHEDULE_NOW, 1.42 + 1.43 + /** 1.44 + * Hint that a requested sync may ignore local rate limiting. 1.45 + * <p> 1.46 + * This is just a hint; the actual requested sync may not obey the hint. 1.47 + */ 1.48 + IGNORE_LOCAL_RATE_LIMIT, 1.49 + 1.50 + /** 1.51 + * Hint that a requested sync may ignore remote server backoffs. 1.52 + * <p> 1.53 + * This is just a hint; the actual requested sync may not obey the hint. 1.54 + */ 1.55 + IGNORE_REMOTE_SERVER_BACKOFF, 1.56 + } 1.57 + 1.58 + public static final EnumSet<SyncHint> SOON = EnumSet.noneOf(SyncHint.class); 1.59 + 1.60 + public static final EnumSet<SyncHint> NOW = EnumSet.of( 1.61 + SyncHint.SCHEDULE_NOW); 1.62 + 1.63 + public static final EnumSet<SyncHint> FORCE = EnumSet.of( 1.64 + SyncHint.SCHEDULE_NOW, 1.65 + SyncHint.IGNORE_LOCAL_RATE_LIMIT, 1.66 + SyncHint.IGNORE_REMOTE_SERVER_BACKOFF); 1.67 + 1.68 + public interface SyncStatusListener { 1.69 + public Context getContext(); 1.70 + public Account getAccount(); 1.71 + public void onSyncStarted(); 1.72 + public void onSyncFinished(); 1.73 + } 1.74 + 1.75 + /** 1.76 + * Returns true if a FirefoxAccount exists, false otherwise. 1.77 + * 1.78 + * @param context Android context. 1.79 + * @return true if at least one Firefox account exists. 1.80 + */ 1.81 + public static boolean firefoxAccountsExist(final Context context) { 1.82 + return getFirefoxAccounts(context).length > 0; 1.83 + } 1.84 + 1.85 + /** 1.86 + * Return Firefox accounts. 1.87 + * <p> 1.88 + * If no accounts exist in the AccountManager, one may be created 1.89 + * via a pickled FirefoxAccount, if available, and that account 1.90 + * will be added to the AccountManager and returned. 1.91 + * <p> 1.92 + * Note that this can be called from any thread. 1.93 + * 1.94 + * @param context Android context. 1.95 + * @return Firefox account objects. 1.96 + */ 1.97 + public static Account[] getFirefoxAccounts(final Context context) { 1.98 + final Account[] accounts = 1.99 + AccountManager.get(context).getAccountsByType(FxAccountConstants.ACCOUNT_TYPE); 1.100 + if (accounts.length > 0) { 1.101 + return accounts; 1.102 + } 1.103 + 1.104 + final Account pickledAccount = getPickledAccount(context); 1.105 + return (pickledAccount != null) ? new Account[] {pickledAccount} : new Account[0]; 1.106 + } 1.107 + 1.108 + private static Account getPickledAccount(final Context context) { 1.109 + // To avoid a StrictMode violation for disk access, we call this from a background thread. 1.110 + // We do this every time, so the caller doesn't have to care. 1.111 + final CountDownLatch latch = new CountDownLatch(1); 1.112 + final Account[] accounts = new Account[1]; 1.113 + ThreadPool.run(new Runnable() { 1.114 + @Override 1.115 + public void run() { 1.116 + try { 1.117 + final File file = context.getFileStreamPath(FxAccountConstants.ACCOUNT_PICKLE_FILENAME); 1.118 + if (!file.exists()) { 1.119 + accounts[0] = null; 1.120 + return; 1.121 + } 1.122 + 1.123 + // There is a small race window here: if the user creates a new Firefox account 1.124 + // between our checks, this could erroneously report that no Firefox accounts 1.125 + // exist. 1.126 + final AndroidFxAccount fxAccount = 1.127 + AccountPickler.unpickle(context, FxAccountConstants.ACCOUNT_PICKLE_FILENAME); 1.128 + accounts[0] = fxAccount.getAndroidAccount(); 1.129 + } finally { 1.130 + latch.countDown(); 1.131 + } 1.132 + } 1.133 + }); 1.134 + 1.135 + try { 1.136 + latch.await(); // Wait for the background thread to return. 1.137 + } catch (InterruptedException e) { 1.138 + Logger.warn(LOG_TAG, 1.139 + "Foreground thread unexpectedly interrupted while getting pickled account", e); 1.140 + return null; 1.141 + } 1.142 + 1.143 + return accounts[0]; 1.144 + } 1.145 + 1.146 + /** 1.147 + * @param context Android context. 1.148 + * @return the configured Firefox account if one exists, or null otherwise. 1.149 + */ 1.150 + public static Account getFirefoxAccount(final Context context) { 1.151 + Account[] accounts = getFirefoxAccounts(context); 1.152 + if (accounts.length > 0) { 1.153 + return accounts[0]; 1.154 + } 1.155 + return null; 1.156 + } 1.157 + 1.158 + protected static void putHintsToSync(final Bundle extras, EnumSet<SyncHint> syncHints) { 1.159 + // stagesToSync and stagesToSkip are allowed to be null. 1.160 + if (syncHints == null) { 1.161 + throw new IllegalArgumentException("syncHints must not be null"); 1.162 + } 1.163 + 1.164 + final boolean scheduleNow = syncHints.contains(SyncHint.SCHEDULE_NOW); 1.165 + final boolean ignoreLocalRateLimit = syncHints.contains(SyncHint.IGNORE_LOCAL_RATE_LIMIT); 1.166 + final boolean ignoreRemoteServerBackoff = syncHints.contains(SyncHint.IGNORE_REMOTE_SERVER_BACKOFF); 1.167 + 1.168 + extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, scheduleNow); 1.169 + // The default when manually syncing is to ignore the local rate limit and 1.170 + // any remote server backoff requests. Since we can't add flags to a manual 1.171 + // sync instigated by the user, we have to reverse the natural conditionals. 1.172 + // See also the FORCE EnumSet. 1.173 + extras.putBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_LOCAL_RATE_LIMIT, !ignoreLocalRateLimit); 1.174 + extras.putBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_REMOTE_SERVER_BACKOFF, !ignoreRemoteServerBackoff); 1.175 + } 1.176 + 1.177 + public static EnumSet<SyncHint> getHintsToSyncFromBundle(final Bundle extras) { 1.178 + final EnumSet<SyncHint> syncHints = EnumSet.noneOf(SyncHint.class); 1.179 + 1.180 + final boolean scheduleNow = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); 1.181 + final boolean ignoreLocalRateLimit = !extras.getBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_LOCAL_RATE_LIMIT, false); 1.182 + final boolean ignoreRemoteServerBackoff = !extras.getBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_REMOTE_SERVER_BACKOFF, false); 1.183 + 1.184 + if (scheduleNow) { 1.185 + syncHints.add(SyncHint.SCHEDULE_NOW); 1.186 + } 1.187 + if (ignoreLocalRateLimit) { 1.188 + syncHints.add(SyncHint.IGNORE_LOCAL_RATE_LIMIT); 1.189 + } 1.190 + if (ignoreRemoteServerBackoff) { 1.191 + syncHints.add(SyncHint.IGNORE_REMOTE_SERVER_BACKOFF); 1.192 + } 1.193 + 1.194 + return syncHints; 1.195 + } 1.196 + 1.197 + public static void logSyncHints(EnumSet<SyncHint> syncHints) { 1.198 + final boolean scheduleNow = syncHints.contains(SyncHint.SCHEDULE_NOW); 1.199 + final boolean ignoreLocalRateLimit = syncHints.contains(SyncHint.IGNORE_LOCAL_RATE_LIMIT); 1.200 + final boolean ignoreRemoteServerBackoff = syncHints.contains(SyncHint.IGNORE_REMOTE_SERVER_BACKOFF); 1.201 + 1.202 + Logger.info(LOG_TAG, "Sync hints" + 1.203 + "; scheduling now: " + scheduleNow + 1.204 + "; ignoring local rate limit: " + ignoreLocalRateLimit + 1.205 + "; ignoring remote server backoff: " + ignoreRemoteServerBackoff + "."); 1.206 + } 1.207 + 1.208 + /** 1.209 + * Request a sync for the given Android Account. 1.210 + * <p> 1.211 + * Any hints are strictly optional: the actual requested sync is scheduled by 1.212 + * the Android sync scheduler, and the sync mechanism may ignore hints as it 1.213 + * sees fit. 1.214 + * <p> 1.215 + * It is safe to call this method from any thread. 1.216 + * 1.217 + * @param account to sync. 1.218 + * @param syncHints to pass to sync. 1.219 + * @param stagesToSync stage names to sync. 1.220 + * @param stagesToSkip stage names to skip. 1.221 + */ 1.222 + public static void requestSync(final Account account, EnumSet<SyncHint> syncHints, String[] stagesToSync, String[] stagesToSkip) { 1.223 + if (account == null) { 1.224 + throw new IllegalArgumentException("account must not be null"); 1.225 + } 1.226 + if (syncHints == null) { 1.227 + throw new IllegalArgumentException("syncHints must not be null"); 1.228 + } 1.229 + 1.230 + final Bundle extras = new Bundle(); 1.231 + putHintsToSync(extras, syncHints); 1.232 + Utils.putStageNamesToSync(extras, stagesToSync, stagesToSkip); 1.233 + 1.234 + Logger.info(LOG_TAG, "Requesting sync."); 1.235 + logSyncHints(syncHints); 1.236 + 1.237 + // We get strict mode warnings on some devices, so make the request on a 1.238 + // background thread. 1.239 + ThreadPool.run(new Runnable() { 1.240 + @Override 1.241 + public void run() { 1.242 + for (String authority : AndroidFxAccount.getAndroidAuthorities()) { 1.243 + ContentResolver.requestSync(account, authority, extras); 1.244 + } 1.245 + } 1.246 + }); 1.247 + } 1.248 + 1.249 + /** 1.250 + * Start notifying <code>syncStatusListener</code> of sync status changes. 1.251 + * <p> 1.252 + * Only a weak reference to <code>syncStatusListener</code> is held. 1.253 + * 1.254 + * @param syncStatusListener to start notifying. 1.255 + */ 1.256 + public static void addSyncStatusListener(SyncStatusListener syncStatusListener) { 1.257 + // startObserving null-checks its argument. 1.258 + FxAccountSyncStatusHelper.getInstance().startObserving(syncStatusListener); 1.259 + } 1.260 + 1.261 + /** 1.262 + * Stop notifying <code>syncStatusListener</code> of sync status changes. 1.263 + * 1.264 + * @param syncStatusListener to stop notifying. 1.265 + */ 1.266 + public static void removeSyncStatusListener(SyncStatusListener syncStatusListener) { 1.267 + // stopObserving null-checks its argument. 1.268 + FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusListener); 1.269 + } 1.270 +}