Bug 1232447 : Update Sync now preference setting item height r?nalexander draft
authorvivek <vivekb.balakrishnan@gmail.com>
Mon, 11 Jan 2016 19:47:12 +0200
changeset 320519 d35dafb537ab6f6ed7c7521896b79e9877fd79e5
parent 320464 121a7977bd8169f013f2a3025b01d1e1b3b60a2d
child 512759 764895ecc870e5372c07fa038fabda220ce3f19d
push id9221
push userbmo:vivekb.balakrishnan@gmail.com
push dateMon, 11 Jan 2016 18:10:06 +0000
reviewersnalexander
bugs1232447
milestone46.0a1
Bug 1232447 : Update Sync now preference setting item height r?nalexander * SyncPreference summary, title and icon update handled from Fragment instead of the view.
mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferenceFragment.java
mobile/android/base/java/org/mozilla/gecko/preferences/SyncPreference.java
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferenceFragment.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferenceFragment.java
@@ -1,54 +1,82 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * 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.preferences;
 
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
 import java.util.Locale;
 
+import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.BrowserLocaleManager;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.LocaleManager;
 import org.mozilla.gecko.PrefsHelper;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.TelemetryContract.Method;
 import org.mozilla.gecko.fxa.AccountLoaderNative;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.activities.PicassoPreferenceIconTarget;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.util.ThreadUtils;
 
 import android.accounts.Account;
 import android.app.ActionBar;
 import android.app.Activity;
 import android.app.LoaderManager;
 import android.content.Context;
 import android.content.Loader;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.os.Bundle;
+import android.os.Handler;
 import android.preference.PreferenceActivity;
 import android.preference.PreferenceFragment;
 import android.preference.PreferenceScreen;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
 import android.util.Log;
 import android.view.Menu;
 import android.view.MenuInflater;
 
+import com.squareup.picasso.Picasso;
+
 /* A simple implementation of PreferenceFragment for large screen devices
  * This will strip category headers (so that they aren't shown to the user twice)
  * as well as initializing Gecko prefs when a fragment is shown.
 */
 public class GeckoPreferenceFragment extends PreferenceFragment {
 
-    public static final int ACCOUNT_LOADER_ID = 1;
+    /**
+     * If a device claims to have synced before this date, we will assume it has never synced.
+     */
+    private static final Date EARLIEST_VALID_SYNCED_DATE;
+    private static final long LAST_SYNCED_TIME_UPDATE_INTERVAL_IN_MILLISECONDS = 60 * 1000;
+    private static final int ACCOUNT_LOADER_ID = 1;
+
     private AccountLoaderCallbacks accountLoaderCallbacks;
     private SyncPreference syncPreference;
+    private PicassoPreferenceIconTarget profileAvatarTarget;
+    private Handler handler;
+    private Runnable lastSyncedTimeUpdateRunnable;
+
+    static {
+        final Calendar c = GregorianCalendar.getInstance();
+        c.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
+        EARLIEST_VALID_SYNCED_DATE = c.getTime();
+    }
 
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
         Log.d(LOGTAG, "onConfigurationChanged: " + newConfig.locale);
 
         final Activity context = getActivity();
 
@@ -84,16 +112,21 @@ public class GeckoPreferenceFragment ext
         }
 
         addPreferencesFromResource(res);
 
         PreferenceScreen screen = getPreferenceScreen();
         setPreferenceScreen(screen);
         mPrefsRequestId = ((GeckoPreferences)getActivity()).setupPreferences(screen);
         syncPreference = (SyncPreference) findPreference(GeckoPreferences.PREFS_SYNC);
+        if (syncPreference != null) {
+            handler = new Handler();
+            final float cornerRadius = getActivity().getResources().getDimension(R.dimen.fxaccount_profile_image_width) / 2;
+            profileAvatarTarget = new PicassoPreferenceIconTarget(getActivity().getResources(), syncPreference, cornerRadius);
+        }
     }
 
     /**
      * Return the title to use for this preference fragment.
      *
      * We only return titles for the preference screens that are
      * launched directly, and thus might need to be redisplayed.
      *
@@ -185,16 +218,27 @@ public class GeckoPreferenceFragment ext
         // super.onResume that you wouldn't do in onCreate.
         applyLocale(Locale.getDefault());
         super.onResume();
 
         // Force reload as the account may have been deleted while the app was in background.
         getLoaderManager().restartLoader(ACCOUNT_LOADER_ID, null, accountLoaderCallbacks);
     }
 
+    @Override
+    public void onPause() {
+        super.onPause();
+
+        // Focus lost, remove scheduled update if any.
+        if (lastSyncedTimeUpdateRunnable != null) {
+            handler.removeCallbacks(lastSyncedTimeUpdateRunnable);
+            lastSyncedTimeUpdateRunnable = null;
+        }
+    }
+
     private void applyLocale(final Locale currentLocale) {
         final Context context = getActivity().getApplicationContext();
 
         BrowserLocaleManager.getInstance().updateConfiguration(context, currentLocale);
 
         if (!currentLocale.equals(lastLocale)) {
             // Locales differ. Let's redisplay.
             Log.d(LOGTAG, "Locale changed: " + currentLocale);
@@ -251,38 +295,115 @@ public class GeckoPreferenceFragment ext
         if (mPrefsRequestId > 0) {
             PrefsHelper.removeObserver(mPrefsRequestId);
         }
 
         final int res = getResource();
         if (res == R.xml.preferences) {
             Telemetry.stopUISession(TelemetryContract.Session.SETTINGS);
         }
+
+        if (syncPreference != null) {
+            Picasso.with(getActivity()).cancelRequest(profileAvatarTarget);
+        }
+    }
+
+
+    private void update(final AndroidFxAccount fxAccount) {
+        if (syncPreference == null) {
+            return;
+        }
+
+        if (fxAccount == null) {
+            ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    syncPreference.setTitle(R.string.pref_sync);
+                    syncPreference.setSummary(R.string.pref_sync_summary);
+                    if (AppConstants.Versions.feature11Plus) {
+                        // Cancel any pending task.
+                        Picasso.with(getActivity()).cancelRequest(profileAvatarTarget);
+                        // Clear previously set icon.
+                        syncPreference.setIcon(R.drawable.sync_avatar_default);
+                    }
+
+                }
+            });
+            return;
+        }
+
+        // Update title and last synced time from account information.
+        final ExtendedJSONObject profileJSON = fxAccount.getProfileJSON();
+        final String userName = profileJSON != null ? profileJSON.getString(FxAccountConstants.KEY_PROFILE_JSON_USERNAME) : null;
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                syncPreference.setTitle(TextUtils.isEmpty(userName) ? fxAccount.getEmail() : userName);
+                syncPreference.setSummary(getLastSyncedString(fxAccount.getLastSyncedTimestamp()));
+            }
+        });
+
+        // Schedule a runnable to update last sync time.
+        lastSyncedTimeUpdateRunnable = new LastSyncTimeUpdateRunnable(fxAccount);
+        handler.postDelayed(lastSyncedTimeUpdateRunnable, LAST_SYNCED_TIME_UPDATE_INTERVAL_IN_MILLISECONDS);
+
+        // Updating icons from Java is not supported prior to API 11.
+        if (!AppConstants.Versions.feature11Plus) {
+            return;
+        }
+
+        // Avatar URI empty, return early.
+        final String avatarURI = profileJSON != null ? profileJSON.getString(FxAccountConstants.KEY_PROFILE_JSON_AVATAR) : null;
+        if (TextUtils.isEmpty(avatarURI)) {
+            return;
+        }
+
+        Picasso.with(getActivity())
+                .load(avatarURI)
+                .centerInside()
+                .resizeDimen(R.dimen.fxaccount_profile_image_width, R.dimen.fxaccount_profile_image_height)
+                .placeholder(R.drawable.sync_avatar_default)
+                .error(R.drawable.sync_avatar_default)
+                .into(profileAvatarTarget);
+    }
+
+    // This is a helper function similar to TabsAccessor.getLastSyncedString() to calculate relative "Last synced" time span.
+    private String getLastSyncedString(final long startTime) {
+        if (new Date(startTime).before(EARLIEST_VALID_SYNCED_DATE)) {
+            return getActivity().getString(R.string.fxaccount_status_never_synced);
+        }
+        final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(startTime);
+        return getActivity().getResources().getString(R.string.fxaccount_status_last_synced, relativeTimeSpanString);
+    }
+
+    /**
+     * The Runnable that schedules a future update and updates the last synced time.
+     */
+    protected class LastSyncTimeUpdateRunnable implements Runnable  {
+        private final AndroidFxAccount fxAccount;
+        LastSyncTimeUpdateRunnable(AndroidFxAccount fxAccount) {
+            this.fxAccount = fxAccount;
+        }
+
+        @Override
+        public void run() {
+            syncPreference.setSummary(getLastSyncedString(fxAccount.getLastSyncedTimestamp()));
+            handler.postDelayed(lastSyncedTimeUpdateRunnable, LAST_SYNCED_TIME_UPDATE_INTERVAL_IN_MILLISECONDS);
+        }
     }
 
     private class AccountLoaderCallbacks implements LoaderManager.LoaderCallbacks<Account> {
         @Override
         public Loader<Account> onCreateLoader(int id, Bundle args) {
             return new AccountLoaderNative(getActivity());
         }
 
         @Override
         public void onLoadFinished(Loader<Account> loader, Account account) {
-            if (syncPreference == null) {
-                return;
-            }
-
-            if (account == null) {
-                syncPreference.update(null);
-                return;
-            }
-
-            syncPreference.update(new AndroidFxAccount(getActivity(), account));
+            update(account == null ? null : new AndroidFxAccount(getActivity(), account));
         }
 
         @Override
         public void onLoaderReset(Loader<Account> loader) {
-            if (syncPreference != null) {
-                syncPreference.update(null);
-            }
+            update(null);
         }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/SyncPreference.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/SyncPreference.java
@@ -1,53 +1,36 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * 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.preferences;
 
-import android.accounts.Account;
 import android.content.Context;
 import android.content.Intent;
 import android.preference.Preference;
-import android.text.TextUtils;
 import android.util.AttributeSet;
-import android.view.View;
-import android.widget.TextView;
 
-import com.squareup.picasso.Picasso;
-import com.squareup.picasso.Target;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.TelemetryContract.Method;
-import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity;
-import org.mozilla.gecko.fxa.activities.PicassoPreferenceIconTarget;
-import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
-import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.setup.SyncAccounts;
 import org.mozilla.gecko.sync.setup.activities.SetupSyncActivity;
-import org.mozilla.gecko.util.ThreadUtils;
 
 class SyncPreference extends Preference {
     private static final boolean DEFAULT_TO_FXA = true;
 
     private final Context mContext;
-    private final Target profileAvatarTarget;
 
     public SyncPreference(Context context, AttributeSet attrs) {
         super(context, attrs);
         mContext = context;
-        final float cornerRadius = mContext.getResources().getDimension(R.dimen.fxaccount_profile_image_width) / 2;
-        profileAvatarTarget = new PicassoPreferenceIconTarget(mContext.getResources(), this, cornerRadius);
     }
 
     private void openSync11Settings() {
         // Show Sync setup if no accounts exist; otherwise, show account settings.
         if (SyncAccounts.syncAccountsExist(mContext)) {
             // We don't check for failure here. If you already have Sync set up,
             // then there's nothing we can do.
             SyncAccounts.openSyncSettings(mContext);
@@ -60,69 +43,16 @@ class SyncPreference extends Preference 
     private void launchFxASetup() {
         final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
         intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
         mContext.startActivity(intent);
     }
 
-    public void update(final AndroidFxAccount fxAccount) {
-        if (fxAccount == null) {
-            ThreadUtils.postToUiThread(new Runnable() {
-                @Override
-                public void run() {
-                    setTitle(R.string.pref_sync);
-                    setSummary(R.string.pref_sync_summary);
-                    if (AppConstants.Versions.feature11Plus) {
-                        // Cancel any pending task.
-                        Picasso.with(mContext).cancelRequest(profileAvatarTarget);
-                        // Clear previously set icon.
-                        setIcon(R.drawable.sync_avatar_default);
-                    }
-
-                }
-            });
-            return;
-        }
-
-        // Update title from account email.
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                setTitle(fxAccount.getEmail());
-                setSummary("");
-            }
-        });
-
-        // Updating icons from Java is not supported prior to API 11.
-        if (!AppConstants.Versions.feature11Plus) {
-            return;
-        }
-
-        final ExtendedJSONObject profileJSON = fxAccount.getProfileJSON();
-        if (profileJSON == null) {
-            return;
-        }
-
-        // Avatar URI empty, return early.
-        final String avatarURI = profileJSON.getString(FxAccountConstants.KEY_PROFILE_JSON_AVATAR);
-        if (TextUtils.isEmpty(avatarURI)) {
-            return;
-        }
-
-        Picasso.with(mContext)
-                .load(avatarURI)
-                .centerInside()
-                .resizeDimen(R.dimen.fxaccount_profile_image_width, R.dimen.fxaccount_profile_image_height)
-                .placeholder(R.drawable.sync_avatar_default)
-                .error(R.drawable.sync_avatar_default)
-                .into(profileAvatarTarget);
-    }
-
     @Override
     protected void onClick() {
         // If we're not defaulting to FxA, just do what we've always done.
         if (!DEFAULT_TO_FXA) {
             openSync11Settings();
             return;
         }