diff --git a/mobile/android/base/sync/GlobalSession.java b/mobile/android/base/sync/GlobalSession.java index 7c850ca979a3..be74eafa5c3b 100644 --- a/mobile/android/base/sync/GlobalSession.java +++ b/mobile/android/base/sync/GlobalSession.java @@ -162,6 +162,11 @@ public class GlobalSession implements CredentialsSource, PrefsSource, HttpRespon registerCommands(); prepareStages(); + Collection knownStageNames = new HashSet(); + for (Stage stage : Stage.getNamedStages()) { + knownStageNames.add(stage.getRepositoryName()); + } + config.stagesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras); // TODO: data-driven plan for the sync, referring to prepareStages. } diff --git a/mobile/android/base/sync/SyncConfiguration.java b/mobile/android/base/sync/SyncConfiguration.java index b154e037e819..3f45f7ec1203 100644 --- a/mobile/android/base/sync/SyncConfiguration.java +++ b/mobile/android/base/sync/SyncConfiguration.java @@ -6,6 +6,7 @@ package org.mozilla.gecko.sync; import java.net.URI; import java.net.URISyntaxException; +import java.util.Collection; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -195,7 +196,19 @@ public class SyncConfiguration implements CredentialsSource { * Copied from latest downloaded meta/global record and used to generate a * fresh meta/global record for upload. */ - public Set enabledEngineNames; + public Set enabledEngineNames; + + /** + * Names of stages to sync this sync, or null to sync + * all known stages. + *

+ * Generated each sync from extras bundle passed to + * SyncAdapter.onPerformSync and not persisted. + *

+ * Not synchronized! Set this exactly once per global session and don't modify + * it -- especially not from multiple threads. + */ + public Collection stagesToSync; // Fields that maintain a reference to a SharedPreferences instance, used for // persistence. diff --git a/mobile/android/base/sync/Utils.java b/mobile/android/base/sync/Utils.java index fdc796af5a91..d6b25ea1dd4d 100644 --- a/mobile/android/base/sync/Utils.java +++ b/mobile/android/base/sync/Utils.java @@ -14,6 +14,7 @@ import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.TreeMap; @@ -21,9 +22,11 @@ import java.util.TreeMap; import org.json.simple.JSONArray; import org.mozilla.apache.commons.codec.binary.Base32; import org.mozilla.apache.commons.codec.binary.Base64; +import org.mozilla.gecko.sync.setup.Constants; import android.content.Context; import android.content.SharedPreferences; +import android.os.Bundle; public class Utils { @@ -337,4 +340,105 @@ public class Utils { public static String toCommaSeparatedString(Collection items) { return toDelimitedString(", ", items); } + + /** + * Names of stages to sync: (ALL intersect SYNC) intersect (ALL minus SKIP). + * + * @param knownStageNames collection of known stage names (set ALL above). + * @param toSync set SYNC above, or null to sync all known stages. + * @param toSkip set SKIP above, or null to not skip any stages. + * @return stage names. + */ + public static Collection getStagesToSync(final Collection knownStageNames, Collection toSync, Collection toSkip) { + if (toSkip == null) { + toSkip = new HashSet(); + } else { + toSkip = new HashSet(toSkip); + } + + if (toSync == null) { + toSync = new HashSet(knownStageNames); + } else { + toSync = new HashSet(toSync); + } + toSync.retainAll(knownStageNames); + toSync.removeAll(toSkip); + return toSync; + } + + /** + * Get names of stages to sync: (ALL intersect SYNC) intersect (ALL minus SKIP). + * + * @param knownStageNames collection of known stage names (set ALL above). + * @param bundle + * a Bundle instance (possibly null) optionally containing keys + * EXTRAS_KEY_STAGES_TO_SYNC (set SYNC above) and + * EXTRAS_KEY_STAGES_TO_SKIP (set SKIP above). + * @return stage names. + */ + public static Collection getStagesToSyncFromBundle(final Collection 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 toSync = null; + ArrayList toSkip = null; + if (toSyncString != null) { + try { + toSync = new ArrayList(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(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); + } + + /** + * Put names of stages to sync and to skip into sync extras bundle. + * + * @param bundle + * a Bundle instance (possibly null). + * @param stagesToSync + * collection of stage names to sync: key + * EXTRAS_KEY_STAGES_TO_SYNC; ignored if null. + * @param stagesToSkip + * collection of stage names to skip: key + * EXTRAS_KEY_STAGES_TO_SKIP; ignored if null. + */ + public static void putStageNamesToSync(final Bundle bundle, final String[] stagesToSync, final String[] stagesToSkip) { + if (bundle == null) { + return; + } + + if (stagesToSync != null) { + ExtendedJSONObject o = new ExtendedJSONObject(); + for (String stageName : stagesToSync) { + o.put(stageName, 0); + } + bundle.putString(Constants.EXTRAS_KEY_STAGES_TO_SYNC, o.toJSONString()); + } + + if (stagesToSkip != null) { + ExtendedJSONObject o = new ExtendedJSONObject(); + for (String stageName : stagesToSkip) { + o.put(stageName, 0); + } + bundle.putString(Constants.EXTRAS_KEY_STAGES_TO_SKIP, o.toJSONString()); + } + } } diff --git a/mobile/android/base/sync/setup/Constants.java b/mobile/android/base/sync/setup/Constants.java index f0f8f10048b3..2d11e0fadf2c 100644 --- a/mobile/android/base/sync/setup/Constants.java +++ b/mobile/android/base/sync/setup/Constants.java @@ -18,6 +18,24 @@ public class Constants { public static final String NUM_CLIENTS = "account.numClients"; public static final String DATA_ENABLE_ON_UPGRADE = "data.enableOnUpgrade"; + /** + * Key in sync extras bundle specifying stages to sync this sync session. + *

+ * Corresponding value should be a String JSON-encoding an object, the keys of + * which are the stage names to sync. For example: + * "{ \"stageToSync\": 0 }". + */ + public static final String EXTRAS_KEY_STAGES_TO_SYNC = "sync"; + + /** + * Key in sync extras bundle specifying stages to skip this sync session. + *

+ * Corresponding value should be a String JSON-encoding an object, the keys of + * which are the stage names to skip. For example: + * "{ \"stageToSkip\": 0 }". + */ + public static final String EXTRAS_KEY_STAGES_TO_SKIP = "skip"; + // Constants for Activities. public static final String INTENT_EXTRA_IS_SETUP = "isSetup"; public static final String INTENT_EXTRA_IS_PAIR = "isPair"; diff --git a/mobile/android/base/sync/setup/activities/SendTabActivity.java b/mobile/android/base/sync/setup/activities/SendTabActivity.java index eef1fecbe87b..fb91ea774e94 100644 --- a/mobile/android/base/sync/setup/activities/SendTabActivity.java +++ b/mobile/android/base/sync/setup/activities/SendTabActivity.java @@ -1,3 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + package org.mozilla.gecko.sync.setup.activities; import java.util.List; @@ -11,6 +15,8 @@ import org.mozilla.gecko.sync.repositories.NullCursorException; import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor; import org.mozilla.gecko.sync.repositories.domain.ClientRecord; import org.mozilla.gecko.sync.setup.Constants; +import org.mozilla.gecko.sync.stage.SyncClientsEngineStage; +import org.mozilla.gecko.sync.syncadapter.SyncAdapter; import android.accounts.Account; import android.accounts.AccountManager; @@ -105,6 +111,9 @@ public class SendTabActivity extends Activity { for (int i = 0; i < guids.length; i++) { processor.sendURIToClientForDisplay(uri, guids[i], title, getAccountGUID(), getApplicationContext()); } + + Logger.info(LOG_TAG, "Requesting immediate clients stage sync."); + SyncAdapter.requestImmediateSync(localAccount, new String[] { SyncClientsEngineStage.COLLECTION_NAME }); } }.start(); diff --git a/mobile/android/base/sync/stage/GlobalSyncStage.java b/mobile/android/base/sync/stage/GlobalSyncStage.java index 97055a864bcf..7719d2096ede 100644 --- a/mobile/android/base/sync/stage/GlobalSyncStage.java +++ b/mobile/android/base/sync/stage/GlobalSyncStage.java @@ -23,7 +23,7 @@ public interface GlobalSyncStage { ensureSpecialRecords, updateEngineTimestamps, */ - syncClientsEngine("clients"), + syncClientsEngine(SyncClientsEngineStage.STAGE_NAME), /* processFirstSyncPref, processClientCommands, diff --git a/mobile/android/base/sync/stage/ServerSyncStage.java b/mobile/android/base/sync/stage/ServerSyncStage.java index 81fd3ff3ddce..27586c0b2600 100644 --- a/mobile/android/base/sync/stage/ServerSyncStage.java +++ b/mobile/android/base/sync/stage/ServerSyncStage.java @@ -80,8 +80,23 @@ public abstract class ServerSyncStage implements // Fall through; null engineSettings will pass below. } + // We can be disabled by the server's meta/global record, or malformed in the server's meta/global record. // We catch the subclasses of MetaGlobalException to trigger various resets and wipes in execute(). - return session.engineIsEnabled(this.getEngineName(), engineSettings); + boolean enabledInMetaGlobal = session.engineIsEnabled(this.getEngineName(), engineSettings); + if (!enabledInMetaGlobal) { + Logger.debug(LOG_TAG, "Stage " + this.getEngineName() + " disabled by server meta/global."); + return false; + } + + // We can also be disabled just for this sync. + if (session.config.stagesToSync == null) { + return true; + } + boolean enabledThisSync = session.config.stagesToSync.contains(this.getEngineName()); // For ServerSyncStage, stage name == engine name. + if (!enabledThisSync) { + Logger.debug(LOG_TAG, "Stage " + this.getEngineName() + " disabled just for this sync."); + } + return enabledThisSync; } protected EngineSettings getEngineSettings() throws NonObjectJSONException, IOException, ParseException { @@ -160,7 +175,7 @@ public abstract class ServerSyncStage implements * Reset timestamps and possibly set syncID. * @param syncID if non-null, new syncID to persist. */ - public void resetLocal(String syncID) { + protected void resetLocal(String syncID) { // Clear both timestamps. SynchronizerConfiguration config; try { @@ -437,7 +452,7 @@ public abstract class ServerSyncStage implements try { if (!this.isEnabled()) { - Logger.info(LOG_TAG, "Stage " + name + " disabled; skipping."); + Logger.info(LOG_TAG, "Skipping stage " + name + "."); session.advance(); return; } diff --git a/mobile/android/base/sync/stage/SyncClientsEngineStage.java b/mobile/android/base/sync/stage/SyncClientsEngineStage.java index bd62e0a1c2a5..d44d7e95da71 100644 --- a/mobile/android/base/sync/stage/SyncClientsEngineStage.java +++ b/mobile/android/base/sync/stage/SyncClientsEngineStage.java @@ -44,6 +44,7 @@ public class SyncClientsEngineStage implements GlobalSyncStage { private static final String LOG_TAG = "SyncClientsEngineStage"; public static final String COLLECTION_NAME = "clients"; + public static final String STAGE_NAME = COLLECTION_NAME; public static final int CLIENTS_TTL_REFRESH = 604800000; // 7 days in milliseconds. public static final int MAX_UPLOAD_FAILURE_COUNT = 5; @@ -324,6 +325,15 @@ public class SyncClientsEngineStage implements GlobalSyncStage { @Override public void execute() throws NoSuchStageException { + // We can be disabled just for this sync. + boolean disabledThisSync = session.config.stagesToSync != null && + !session.config.stagesToSync.contains(STAGE_NAME); + if (disabledThisSync) { + Logger.debug(LOG_TAG, "Stage " + STAGE_NAME + " disabled just for this sync."); + session.advance(); + return; + } + if (shouldDownload()) { downloadClientRecords(); // Will kick off upload, too… } else { diff --git a/mobile/android/base/sync/syncadapter/SyncAdapter.java b/mobile/android/base/sync/syncadapter/SyncAdapter.java index d48819c3983b..1382c7a8523e 100644 --- a/mobile/android/base/sync/syncadapter/SyncAdapter.java +++ b/mobile/android/base/sync/syncadapter/SyncAdapter.java @@ -10,6 +10,7 @@ import java.security.NoSuchAlgorithmException; import java.util.concurrent.TimeUnit; import org.json.simple.parser.ParseException; +import org.mozilla.gecko.db.BrowserContract; import org.mozilla.gecko.sync.AlreadySyncingException; import org.mozilla.gecko.sync.GlobalConstants; import org.mozilla.gecko.sync.GlobalSession; @@ -36,6 +37,7 @@ import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.content.AbstractThreadedSyncAdapter; import android.content.ContentProviderClient; +import android.content.ContentResolver; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; @@ -221,6 +223,29 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe return delayMilliseconds() > 0; } + /** + * Asynchronously request an immediate sync, optionally syncing only the given + * named stages. + *

+ * Returns immediately. + * + * @param account + * the Android Account instance to sync. + * @param stageNames + * stage names to sync, or null 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); + } + @Override public void onPerformSync(final Account account, final Bundle extras, @@ -234,7 +259,11 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe this.syncResult = syncResult; this.localAccount = account; - thisSyncIsForced = (extras != null) && (extras.getBoolean("force", false)); + Log.i(LOG_TAG, "Syncing client named " + getClientName() + + " with client guid " + getAccountGUID() + + " (sync account has " + getClientsCount() + " clients)."); + + thisSyncIsForced = (extras != null) && (extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false)); long delay = delayMilliseconds(); if (delay > 0) { if (thisSyncIsForced) {