Bug 802749 - Make background sync over metered connections optional. r=Grisha Kruglov
authorMehdi Soleimannejad <mehdisolamannejad@gmail.com>
Fri, 22 Sep 2017 10:25:26 +0330
changeset 383306 2f33884ae03df743e1d4d1de5cd3209a68d08077
parent 383305 f069d6b4d4866f93358048b962aaba12730415d0
child 383307 7ae47699361c77e4282d9532160d2d220bbd28cf
push id95539
push userkwierso@gmail.com
push dateThu, 28 Sep 2017 00:01:12 +0000
treeherdermozilla-inbound@72de90e66155 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersGrisha
bugs802749
milestone58.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 802749 - Make background sync over metered connections optional. r=Grisha Kruglov MozReview-Commit-ID: 4UhQqyxT90N
mobile/android/base/locales/en-US/sync_strings.dtd
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusFragment.java
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java
mobile/android/services/src/main/res/xml/fxaccount_status_prefscreen.xml
mobile/android/services/strings.xml.in
--- a/mobile/android/base/locales/en-US/sync_strings.dtd
+++ b/mobile/android/base/locales/en-US/sync_strings.dtd
@@ -63,16 +63,20 @@
 <!ENTITY fxaccount_status_needs_master_sync_automatically_enabled_v21 '&syncBrand.shortName.label; is set up, but not syncing automatically. Toggle “Auto-sync data” in the menu of Android Settings &gt; Accounts.'>
 <!ENTITY fxaccount_status_needs_finish_migrating 'Tap to sign in to your new Firefox Account.'>
 <!ENTITY fxaccount_status_choose_what 'Choose what to sync'>
 <!ENTITY fxaccount_status_bookmarks 'Bookmarks'>
 <!ENTITY fxaccount_status_history 'History'>
 <!ENTITY fxaccount_status_passwords2 'Logins'>
 <!ENTITY fxaccount_status_tabs 'Open tabs'>
 <!ENTITY fxaccount_status_additional_settings 'Additional Settings'>
+<!ENTITY fxaccount_pref_sync_use_metered 'Sync over metered connections'>
+<!-- Localization note: Only affects background syncing, user initiated 
+     syncs will still be done regardless of the connection -->
+<!ENTITY fxaccount_pref_sync_use_metered_summary 'Allow usage of cellular data and metered Wi-Fi to sync in the background'>
 <!ENTITY fxaccount_status_legal 'Legal' >
 <!-- Localization note: when tapped, the following two strings link to
      external web pages.  Compare fxaccount_policy_{linktos,linkprivacy}:
      these strings are separated to accommodate languages that decline
      the two uses differently. -->
 <!ENTITY fxaccount_status_linktos2 'Terms of service'>
 <!ENTITY fxaccount_status_linkprivacy2 'Privacy notice'>
 <!ENTITY fxaccount_remove_account 'Disconnect&ellipsis;'>
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusFragment.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusFragment.java
@@ -8,23 +8,19 @@ import android.accounts.Account;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.os.Handler;
-import android.preference.CheckBoxPreference;
-import android.preference.EditTextPreference;
-import android.preference.Preference;
+import android.preference.*;
 import android.preference.Preference.OnPreferenceChangeListener;
 import android.preference.Preference.OnPreferenceClickListener;
-import android.preference.PreferenceCategory;
-import android.preference.PreferenceScreen;
 import android.support.v4.content.LocalBroadcastManager;
 import android.text.TextUtils;
 import android.text.format.DateUtils;
 
 import com.squareup.picasso.Picasso;
 import com.squareup.picasso.Target;
 
 import org.mozilla.gecko.AppConstants;
@@ -32,16 +28,17 @@ import org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.background.preferences.PreferenceFragment;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.SyncStatusListener;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.login.Married;
 import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
 import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
@@ -100,16 +97,17 @@ public class FxAccountStatusFragment
   protected Preference needsFinishMigratingPreference;
 
   protected CheckBoxPreference bookmarksPreference;
   protected CheckBoxPreference historyPreference;
   protected CheckBoxPreference tabsPreference;
   protected CheckBoxPreference passwordsPreference;
 
   protected EditTextPreference deviceNamePreference;
+  protected SwitchPreference syncOverMeteredPreference;
   protected Preference syncServerPreference;
   protected Preference syncNowPreference;
 
   protected volatile AndroidFxAccount fxAccount;
   // The contract is: when fxAccount is non-null, then clientsDataDelegate is
   // non-null.  If violated then an IllegalStateException is thrown.
   protected volatile SharedPreferencesClientsDataDelegate clientsDataDelegate;
 
@@ -179,16 +177,19 @@ public class FxAccountStatusFragment
     needsVerificationPreference.setOnPreferenceClickListener(this);
     needsFinishMigratingPreference.setOnPreferenceClickListener(this);
 
     bookmarksPreference.setOnPreferenceClickListener(this);
     historyPreference.setOnPreferenceClickListener(this);
     tabsPreference.setOnPreferenceClickListener(this);
     passwordsPreference.setOnPreferenceClickListener(this);
 
+    syncOverMeteredPreference = (SwitchPreference) ensureFindPreference(FxAccountSyncAdapter.PREFS_SYNC_METERED);
+    syncOverMeteredPreference.setOnPreferenceChangeListener(this);
+
     deviceNamePreference = (EditTextPreference) ensureFindPreference("device_name");
     deviceNamePreference.setOnPreferenceChangeListener(this);
 
     syncServerPreference = ensureFindPreference("sync_server");
 
     syncNowPreference = ensureFindPreference("sync_now");
     syncNowPreference.setEnabled(true);
     syncNowPreference.setOnPreferenceClickListener(this);
@@ -943,12 +944,20 @@ public class FxAccountStatusFragment
       // Force sync the client record, we want the user to see the device name change immediately
       // on the FxA Device Manager if possible ( = we are online) to avoid confusion
       // ("I changed my Android's device name but I don't see it on my computer").
       fxAccount.requestImmediateSync(STAGES_TO_SYNC_ON_DEVICE_NAME_CHANGE, null, true);
       hardRefresh(); // Updates the value displayed to the user, among other things.
       return true;
     }
 
+    if (preference == syncOverMeteredPreference) {
+      try {
+        fxAccount.getSyncPrefs().edit().putBoolean(FxAccountSyncAdapter.PREFS_SYNC_METERED, (Boolean) newValue).apply();
+      } catch (Exception e) {
+        Logger.error(LOG_TAG, "Failed to save the new for syncMeteredPreference");
+      }
+    }
+
     // For everything else, accept the change.
     return true;
   }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
@@ -142,16 +142,17 @@ public class AndroidFxAccount {
     keysToCarryOver.add(ACCOUNT_KEY_DESCRIPTOR);
     ACCOUNT_KEY_TO_CARRY_OVER_ON_RENAME_SET = Collections.unmodifiableList(keysToCarryOver);
   }
 
   private static final String PREF_KEY_LAST_SYNCED_TIMESTAMP = "lastSyncedTimestamp";
 
   protected final Context context;
   private final AccountManager accountManager;
+  private final long neverSynced = -1;
 
   // This is really, really meant to be final. Only changed when account name changes.
   // See Bug 1368147.
   protected volatile Account account;
 
   /**
    * Create an Android Firefox Account instance backed by an Android Account
    * instance.
@@ -642,17 +643,17 @@ public class AndroidFxAccount {
     return active;
   }
 
   /**
    * Request an immediate sync.  Use this to sync as soon as possible in response to user action.
    *
    * @param stagesToSync stage names to sync; can be null to sync <b>all</b> known stages.
    * @param stagesToSkip stage names to skip; can be null to skip <b>no</b> known stages.
-   * @param ignoreSettings whether we should check preferences for syncing over metered connections.
+   * @param ignoreSettings whether we should check if syncing is allowed via in-app or system settings.
    */
   public void requestImmediateSync(String[] stagesToSync, String[] stagesToSkip, boolean ignoreSettings) {
     FirefoxAccounts.requestImmediateSync(getAndroidAccount(), stagesToSync, stagesToSkip, ignoreSettings);
   }
 
   /**
    * Request an eventual sync.  Use this to request the system queue a sync for some time in the
    * future.
@@ -794,25 +795,33 @@ public class AndroidFxAccount {
     try {
       getSyncPrefs().edit().putLong(PREF_KEY_LAST_SYNCED_TIMESTAMP, now).commit();
     } catch (Exception e) {
       Logger.warn(LOG_TAG, "Got exception setting last synced time; ignoring.", e);
     }
   }
 
   public long getLastSyncedTimestamp() {
-    final long neverSynced = -1L;
     try {
       return getSyncPrefs().getLong(PREF_KEY_LAST_SYNCED_TIMESTAMP, neverSynced);
     } catch (Exception e) {
       Logger.warn(LOG_TAG, "Got exception getting last synced time; ignoring.", e);
       return neverSynced;
     }
   }
 
+  public boolean neverSynced() {
+    try {
+      return getSyncPrefs().getLong(PREF_KEY_LAST_SYNCED_TIMESTAMP, neverSynced) == -1;
+    } catch (Exception e) {
+      Logger.warn(LOG_TAG, "Got exception getting last synced time; ignoring.", e);
+      return false;
+    }
+  }
+
   // Debug only!  This is dangerous!
   public void unsafeTransitionToDefaultEndpoints() {
     unsafeTransitionToStageEndpoints(
         FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT,
         FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT,
         FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT);
     }
 
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java
@@ -7,16 +7,17 @@ package org.mozilla.gecko.fxa.sync;
 import android.accounts.Account;
 import android.content.AbstractThreadedSyncAdapter;
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.SyncResult;
+import android.net.ConnectivityManager;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.support.v4.content.LocalBroadcastManager;
 
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.background.fxa.SkewHandler;
 import org.mozilla.gecko.browserid.JSONWebTokenUtils;
@@ -68,16 +69,18 @@ import java.util.concurrent.TimeUnit;
 
 public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
   private static final String LOG_TAG = FxAccountSyncAdapter.class.getSimpleName();
 
   public static final int NOTIFICATION_ID = LOG_TAG.hashCode();
 
   // Tracks the last seen storage hostname for backoff purposes.
   private static final String PREF_BACKOFF_STORAGE_HOST = "backoffStorageHost";
+  // Preference key for allowing sync over metered connections.
+  public static final String PREFS_SYNC_METERED = "sync.allow_metered";
 
   // Used to do cheap in-memory rate limiting. Don't sync again if we
   // successfully synced within this duration.
   private static final int MINIMUM_SYNC_DELAY_MILLIS = 15 * 1000;        // 15 seconds.
   private volatile long lastSyncRealtimeMillis;
 
   // Non-user initiated sync can't take longer than 30 minutes.
   // To ensure we're not churning through device's battery/resources, we limit sync to 10 minutes,
@@ -497,16 +500,37 @@ public class FxAccountSyncAdapter extend
   @Override
   public void onPerformSync(final Account account, final Bundle extras, final String authority, ContentProviderClient provider, final SyncResult syncResult) {
     Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG);
     Logger.resetLogging();
 
     final Context context = getContext();
     final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
 
+    // This flag is used to conclude whether we should ignore syncing
+    // based on user preference for syncing over metered connections.
+    boolean shouldRejectSyncViaSettings = false;
+    // Check whether we should ignore settings or not.
+    if (!extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false)) {
+      // If it's not user-initiated, we should check if we are allowed to sync on metered connections.
+      boolean isMeteredAllowed = true;
+      try {
+        isMeteredAllowed = fxAccount.getSyncPrefs().getBoolean(PREFS_SYNC_METERED, true);
+      } catch (Exception e) {
+        Logger.error(LOG_TAG, "Failed to read sync preferences. Allowing metered connections by default.");
+      }
+      // Check if the device is on a metered connection or not.
+      final ConnectivityManager manager = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+      final boolean isMetered = manager.isActiveNetworkMetered();
+      // If the connection is metered and syncing over metered connections is
+      // not permitted, we should bail.
+      shouldRejectSyncViaSettings = !isMeteredAllowed && isMetered;
+    }
+
+
     // NB: we use elapsedRealtime which is time since boot, to ensure our clock is monotonic and isn't
     // paused while CPU is in the power-saving mode.
     final long syncDeadline = SystemClock.elapsedRealtime() + SYNC_DEADLINE_DELTA_MILLIS;
 
     Logger.info(LOG_TAG, "Syncing FxAccount" +
         " account named like " + Utils.obfuscateEmail(account.name) +
         " for authority " + authority +
         " with instance " + this + ".");
@@ -540,19 +564,36 @@ public class FxAccountSyncAdapter extend
       }
     });
 
     final BlockingQueue<Result> latch = new LinkedBlockingQueue<>(1);
 
     Collection<String> knownStageNames = SyncConfiguration.validEngineNames();
     Collection<String> stageNamesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras);
 
+    // If syncing should be rejected due to metered connection preferences
+    // and we are doing the first sync ever, we should at least sync the
+    // 'clients' collection to ensure we upload our local client record.
+    // see {@link <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=802749">Bug 802749</a>}
+    // for more information.
+    if (shouldRejectSyncViaSettings && fxAccount.neverSynced()) {
+      stageNamesToSync.clear();
+      stageNamesToSync.add("clients");
+    }
+
     final SyncDelegate syncDelegate = new SyncDelegate(latch, syncResult, fxAccount, stageNamesToSync);
     Result offeredResult = null;
 
+    if (shouldRejectSyncViaSettings && !fxAccount.neverSynced()) {
+      // The user is on a metered connection and has disabled syncing over metered connections,
+      // we should reject the sync.
+      syncDelegate.rejectSync();
+      return;
+    }
+
     try {
       // This will be the same chunk of SharedPreferences that we pass through to GlobalSession/SyncConfiguration.
       final SharedPreferences sharedPrefs = fxAccount.getSyncPrefs();
 
       final BackoffHandler backgroundBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "background");
       final BackoffHandler rateLimitBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "rate");
 
       // If this sync was triggered by user action, this will be true.
--- a/mobile/android/services/src/main/res/xml/fxaccount_status_prefscreen.xml
+++ b/mobile/android/services/src/main/res/xml/fxaccount_status_prefscreen.xml
@@ -79,16 +79,21 @@
             android:title="@string/fxaccount_status_passwords" />
     </PreferenceCategory>
 
     <EditTextPreference
         android:singleLine="true"
         android:key="device_name"
         android:persistent="false"
         android:title="@string/fxaccount_status_device_name" />
+    <SwitchPreference 
+	android:key="sync.allow_metered"
+	android:defaultValue="true"
+	android:title="@string/fxaccount_pref_sync_use_metered"
+	android:summary="@string/fxaccount_pref_sync_use_metered_summary"/>
 
     <org.mozilla.gecko.fxa.activities.CustomColorPreference
         android:editable="false"
         android:key="remove_account"
         android:persistent="false"
         gecko:titleColor="@color/rejection_red"
         android:title="@string/fxaccount_remove_account" />
 
--- a/mobile/android/services/strings.xml.in
+++ b/mobile/android/services/strings.xml.in
@@ -42,16 +42,18 @@
 <string name="fxaccount_status_needs_master_sync_automatically_enabled">&fxaccount_status_needs_master_sync_automatically_enabled;</string>
 <string name="fxaccount_status_needs_master_sync_automatically_enabled_v21">&fxaccount_status_needs_master_sync_automatically_enabled_v21;</string>
 <string name="fxaccount_status_needs_finish_migrating">&fxaccount_status_needs_finish_migrating;</string>
 <string name="fxaccount_status_choose_what">&fxaccount_status_choose_what;</string>
 <string name="fxaccount_status_bookmarks">&fxaccount_status_bookmarks;</string>
 <string name="fxaccount_status_history">&fxaccount_status_history;</string>
 <string name="fxaccount_status_passwords">&fxaccount_status_passwords2;</string>
 <string name="fxaccount_status_tabs">&fxaccount_status_tabs;</string>
+<string name="fxaccount_pref_sync_use_metered">&fxaccount_pref_sync_use_metered;</string>
+<string name="fxaccount_pref_sync_use_metered_summary">&fxaccount_pref_sync_use_metered_summary;</string>
 <string name="fxaccount_status_additional_settings">&fxaccount_status_additional_settings;</string>
 <string name="fxaccount_status_legal">&fxaccount_status_legal;</string>
 <string name="fxaccount_status_linktos">&fxaccount_status_linktos2;</string>
 <string name="fxaccount_status_linkprivacy">&fxaccount_status_linkprivacy2;</string>
 <string name="fxaccount_remove_account">&fxaccount_remove_account;</string>
 
 <string name="fxaccount_label">&fxaccount_account_type_label;</string>