michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.fxa; michael@0: michael@0: import java.io.File; michael@0: import java.util.EnumSet; michael@0: import java.util.concurrent.CountDownLatch; michael@0: michael@0: import org.mozilla.gecko.background.common.log.Logger; michael@0: import org.mozilla.gecko.fxa.authenticator.AccountPickler; michael@0: import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; michael@0: import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter; michael@0: import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper; michael@0: import org.mozilla.gecko.sync.ThreadPool; michael@0: import org.mozilla.gecko.sync.Utils; michael@0: michael@0: import android.accounts.Account; michael@0: import android.accounts.AccountManager; michael@0: import android.content.ContentResolver; michael@0: import android.content.Context; michael@0: import android.os.Bundle; michael@0: michael@0: /** michael@0: * Simple public accessors for Firefox account objects. michael@0: */ michael@0: public class FirefoxAccounts { michael@0: private static final String LOG_TAG = FirefoxAccounts.class.getSimpleName(); michael@0: michael@0: public enum SyncHint { michael@0: /** michael@0: * Hint that a requested sync is preferred immediately. michael@0: *

michael@0: * On many devices, not including SCHEDULE_NOW means a delay of michael@0: * at least 30 seconds. michael@0: */ michael@0: SCHEDULE_NOW, michael@0: michael@0: /** michael@0: * Hint that a requested sync may ignore local rate limiting. michael@0: *

michael@0: * This is just a hint; the actual requested sync may not obey the hint. michael@0: */ michael@0: IGNORE_LOCAL_RATE_LIMIT, michael@0: michael@0: /** michael@0: * Hint that a requested sync may ignore remote server backoffs. michael@0: *

michael@0: * This is just a hint; the actual requested sync may not obey the hint. michael@0: */ michael@0: IGNORE_REMOTE_SERVER_BACKOFF, michael@0: } michael@0: michael@0: public static final EnumSet SOON = EnumSet.noneOf(SyncHint.class); michael@0: michael@0: public static final EnumSet NOW = EnumSet.of( michael@0: SyncHint.SCHEDULE_NOW); michael@0: michael@0: public static final EnumSet FORCE = EnumSet.of( michael@0: SyncHint.SCHEDULE_NOW, michael@0: SyncHint.IGNORE_LOCAL_RATE_LIMIT, michael@0: SyncHint.IGNORE_REMOTE_SERVER_BACKOFF); michael@0: michael@0: public interface SyncStatusListener { michael@0: public Context getContext(); michael@0: public Account getAccount(); michael@0: public void onSyncStarted(); michael@0: public void onSyncFinished(); michael@0: } michael@0: michael@0: /** michael@0: * Returns true if a FirefoxAccount exists, false otherwise. michael@0: * michael@0: * @param context Android context. michael@0: * @return true if at least one Firefox account exists. michael@0: */ michael@0: public static boolean firefoxAccountsExist(final Context context) { michael@0: return getFirefoxAccounts(context).length > 0; michael@0: } michael@0: michael@0: /** michael@0: * Return Firefox accounts. michael@0: *

michael@0: * If no accounts exist in the AccountManager, one may be created michael@0: * via a pickled FirefoxAccount, if available, and that account michael@0: * will be added to the AccountManager and returned. michael@0: *

michael@0: * Note that this can be called from any thread. michael@0: * michael@0: * @param context Android context. michael@0: * @return Firefox account objects. michael@0: */ michael@0: public static Account[] getFirefoxAccounts(final Context context) { michael@0: final Account[] accounts = michael@0: AccountManager.get(context).getAccountsByType(FxAccountConstants.ACCOUNT_TYPE); michael@0: if (accounts.length > 0) { michael@0: return accounts; michael@0: } michael@0: michael@0: final Account pickledAccount = getPickledAccount(context); michael@0: return (pickledAccount != null) ? new Account[] {pickledAccount} : new Account[0]; michael@0: } michael@0: michael@0: private static Account getPickledAccount(final Context context) { michael@0: // To avoid a StrictMode violation for disk access, we call this from a background thread. michael@0: // We do this every time, so the caller doesn't have to care. michael@0: final CountDownLatch latch = new CountDownLatch(1); michael@0: final Account[] accounts = new Account[1]; michael@0: ThreadPool.run(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: try { michael@0: final File file = context.getFileStreamPath(FxAccountConstants.ACCOUNT_PICKLE_FILENAME); michael@0: if (!file.exists()) { michael@0: accounts[0] = null; michael@0: return; michael@0: } michael@0: michael@0: // There is a small race window here: if the user creates a new Firefox account michael@0: // between our checks, this could erroneously report that no Firefox accounts michael@0: // exist. michael@0: final AndroidFxAccount fxAccount = michael@0: AccountPickler.unpickle(context, FxAccountConstants.ACCOUNT_PICKLE_FILENAME); michael@0: accounts[0] = fxAccount.getAndroidAccount(); michael@0: } finally { michael@0: latch.countDown(); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: try { michael@0: latch.await(); // Wait for the background thread to return. michael@0: } catch (InterruptedException e) { michael@0: Logger.warn(LOG_TAG, michael@0: "Foreground thread unexpectedly interrupted while getting pickled account", e); michael@0: return null; michael@0: } michael@0: michael@0: return accounts[0]; michael@0: } michael@0: michael@0: /** michael@0: * @param context Android context. michael@0: * @return the configured Firefox account if one exists, or null otherwise. michael@0: */ michael@0: public static Account getFirefoxAccount(final Context context) { michael@0: Account[] accounts = getFirefoxAccounts(context); michael@0: if (accounts.length > 0) { michael@0: return accounts[0]; michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: protected static void putHintsToSync(final Bundle extras, EnumSet syncHints) { michael@0: // stagesToSync and stagesToSkip are allowed to be null. michael@0: if (syncHints == null) { michael@0: throw new IllegalArgumentException("syncHints must not be null"); michael@0: } michael@0: michael@0: final boolean scheduleNow = syncHints.contains(SyncHint.SCHEDULE_NOW); michael@0: final boolean ignoreLocalRateLimit = syncHints.contains(SyncHint.IGNORE_LOCAL_RATE_LIMIT); michael@0: final boolean ignoreRemoteServerBackoff = syncHints.contains(SyncHint.IGNORE_REMOTE_SERVER_BACKOFF); michael@0: michael@0: extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, scheduleNow); michael@0: // The default when manually syncing is to ignore the local rate limit and michael@0: // any remote server backoff requests. Since we can't add flags to a manual michael@0: // sync instigated by the user, we have to reverse the natural conditionals. michael@0: // See also the FORCE EnumSet. michael@0: extras.putBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_LOCAL_RATE_LIMIT, !ignoreLocalRateLimit); michael@0: extras.putBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_REMOTE_SERVER_BACKOFF, !ignoreRemoteServerBackoff); michael@0: } michael@0: michael@0: public static EnumSet getHintsToSyncFromBundle(final Bundle extras) { michael@0: final EnumSet syncHints = EnumSet.noneOf(SyncHint.class); michael@0: michael@0: final boolean scheduleNow = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); michael@0: final boolean ignoreLocalRateLimit = !extras.getBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_LOCAL_RATE_LIMIT, false); michael@0: final boolean ignoreRemoteServerBackoff = !extras.getBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_REMOTE_SERVER_BACKOFF, false); michael@0: michael@0: if (scheduleNow) { michael@0: syncHints.add(SyncHint.SCHEDULE_NOW); michael@0: } michael@0: if (ignoreLocalRateLimit) { michael@0: syncHints.add(SyncHint.IGNORE_LOCAL_RATE_LIMIT); michael@0: } michael@0: if (ignoreRemoteServerBackoff) { michael@0: syncHints.add(SyncHint.IGNORE_REMOTE_SERVER_BACKOFF); michael@0: } michael@0: michael@0: return syncHints; michael@0: } michael@0: michael@0: public static void logSyncHints(EnumSet syncHints) { michael@0: final boolean scheduleNow = syncHints.contains(SyncHint.SCHEDULE_NOW); michael@0: final boolean ignoreLocalRateLimit = syncHints.contains(SyncHint.IGNORE_LOCAL_RATE_LIMIT); michael@0: final boolean ignoreRemoteServerBackoff = syncHints.contains(SyncHint.IGNORE_REMOTE_SERVER_BACKOFF); michael@0: michael@0: Logger.info(LOG_TAG, "Sync hints" + michael@0: "; scheduling now: " + scheduleNow + michael@0: "; ignoring local rate limit: " + ignoreLocalRateLimit + michael@0: "; ignoring remote server backoff: " + ignoreRemoteServerBackoff + "."); michael@0: } michael@0: michael@0: /** michael@0: * Request a sync for the given Android Account. michael@0: *

michael@0: * Any hints are strictly optional: the actual requested sync is scheduled by michael@0: * the Android sync scheduler, and the sync mechanism may ignore hints as it michael@0: * sees fit. michael@0: *

michael@0: * It is safe to call this method from any thread. michael@0: * michael@0: * @param account to sync. michael@0: * @param syncHints to pass to sync. michael@0: * @param stagesToSync stage names to sync. michael@0: * @param stagesToSkip stage names to skip. michael@0: */ michael@0: public static void requestSync(final Account account, EnumSet syncHints, String[] stagesToSync, String[] stagesToSkip) { michael@0: if (account == null) { michael@0: throw new IllegalArgumentException("account must not be null"); michael@0: } michael@0: if (syncHints == null) { michael@0: throw new IllegalArgumentException("syncHints must not be null"); michael@0: } michael@0: michael@0: final Bundle extras = new Bundle(); michael@0: putHintsToSync(extras, syncHints); michael@0: Utils.putStageNamesToSync(extras, stagesToSync, stagesToSkip); michael@0: michael@0: Logger.info(LOG_TAG, "Requesting sync."); michael@0: logSyncHints(syncHints); michael@0: michael@0: // We get strict mode warnings on some devices, so make the request on a michael@0: // background thread. michael@0: ThreadPool.run(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: for (String authority : AndroidFxAccount.getAndroidAuthorities()) { michael@0: ContentResolver.requestSync(account, authority, extras); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Start notifying syncStatusListener of sync status changes. michael@0: *

michael@0: * Only a weak reference to syncStatusListener is held. michael@0: * michael@0: * @param syncStatusListener to start notifying. michael@0: */ michael@0: public static void addSyncStatusListener(SyncStatusListener syncStatusListener) { michael@0: // startObserving null-checks its argument. michael@0: FxAccountSyncStatusHelper.getInstance().startObserving(syncStatusListener); michael@0: } michael@0: michael@0: /** michael@0: * Stop notifying syncStatusListener of sync status changes. michael@0: * michael@0: * @param syncStatusListener to stop notifying. michael@0: */ michael@0: public static void removeSyncStatusListener(SyncStatusListener syncStatusListener) { michael@0: // stopObserving null-checks its argument. michael@0: FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusListener); michael@0: } michael@0: }