Bug 1064304 - Part 3: Persist group collapsed/expanded state in SharedPreferences. r=rnewman
authorNick Alexander <nalexander@mozilla.com>
Tue, 16 Sep 2014 15:41:10 -0700
changeset 228891 8685b21b77961bc2ec7fa9d8cb4a12b3ecd66db6
parent 228890 bc600c098de320434b768661ae230d0d4002f169
child 228892 00a5099d9f09a33af39997ff6919fb47c299de98
push id4187
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:29:12 +0000
treeherdermozilla-beta@f23cc6a30c11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman
bugs1064304
milestone35.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 1064304 - Part 3: Persist group collapsed/expanded state in SharedPreferences. r=rnewman
mobile/android/base/home/RemoteTabsExpandableListFragment.java
mobile/android/base/home/RemoteTabsExpandableListState.java
mobile/android/base/moz.build
--- a/mobile/android/base/home/RemoteTabsExpandableListFragment.java
+++ b/mobile/android/base/home/RemoteTabsExpandableListFragment.java
@@ -3,16 +3,17 @@
  * 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.home;
 
 import java.util.EnumSet;
 import java.util.List;
 
+import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.RemoteTabsExpandableListAdapter;
 import org.mozilla.gecko.TabsAccessor;
 import org.mozilla.gecko.TabsAccessor.RemoteClient;
 import org.mozilla.gecko.TabsAccessor.RemoteTab;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
@@ -47,16 +48,20 @@ public class RemoteTabsExpandableListFra
     // Logging tag name.
     private static final String LOGTAG = "GeckoRemoteTabsExpList";
 
     // Cursor loader ID.
     private static final int LOADER_ID_REMOTE_TABS = 0;
 
     private static final String[] STAGES_TO_SYNC_ON_REFRESH = new String[] { "clients", "tabs" };
 
+    // Maintain group collapsed and hidden state.
+    // Only accessed from the UI thread.
+    private static RemoteTabsExpandableListState sState;
+
     // Adapter for the list of remote tabs.
     private RemoteTabsExpandableListAdapter mAdapter;
 
     // The view shown by the fragment.
     private HomeExpandableListView mList;
 
     // Reference to the View to display when there are no results.
     private View mEmptyView;
@@ -112,19 +117,25 @@ public class RemoteTabsExpandableListFra
                 mUrlOpenListener.onUrlOpen(tab.url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
                 return true;
             }
         });
 
         mList.setOnGroupClickListener(new OnGroupClickListener() {
             @Override
             public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) {
-                // Since we don't indicate the expansion state yet, don't allow
-                // collapsing groups at all.
-                return true;
+                final ExpandableListAdapter adapter = parent.getExpandableListAdapter();
+                final RemoteClient client = (RemoteClient) adapter.getGroup(groupPosition);
+                if (client != null) {
+                    // After we process this click, the group's expanded state will have flipped.
+                    sState.setClientCollapsed(client.guid, mList.isGroupExpanded(groupPosition));
+                }
+
+                // We want the system to handle the click, expanding or collapsing as necessary.
+                return false;
             }
         });
 
         // Show a context menu only for tabs (not for clients).
         mList.setContextMenuInfoFactory(new HomeContextMenuInfo.ExpandableFactory() {
             @Override
             public HomeContextMenuInfo makeInfoForAdapter(View view, int position, long id, ExpandableListAdapter adapter) {
                 long packedPosition = mList.getExpandableListPosition(position);
@@ -160,29 +171,45 @@ public class RemoteTabsExpandableListFra
             mSyncStatusListener = null;
         }
     }
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
 
+        // This races when multiple Fragments are created. That's okay: one
+        // will win, and thereafter, all will be okay. If we create and then
+        // drop an instance the shared SharedPreferences backing all the
+        // instances will maintain the state for us. Since everything happens on
+        // the UI thread, this doesn't even need to be volatile.
+        if (sState == null) {
+            sState = new RemoteTabsExpandableListState(GeckoSharedPrefs.forProfile(getActivity()));
+        }
+
         // Intialize adapter
         mAdapter = new RemoteTabsExpandableListAdapter(R.layout.home_remote_tabs_group, R.layout.home_remote_tabs_child, null);
         mList.setAdapter(mAdapter);
 
         // Create callbacks before the initial loader is started
         mCursorLoaderCallbacks = new CursorLoaderCallbacks();
         loadIfVisible();
     }
 
     private void updateUiFromClients(List<RemoteClient> clients) {
         if (clients != null && !clients.isEmpty()) {
-            for (int i = 0; i < mList.getExpandableListAdapter().getGroupCount(); i++) {
-                mList.expandGroup(i);
+            // No sense crashing if we've made an error.
+            int groupCount = Math.min(mList.getExpandableListAdapter().getGroupCount(), clients.size());
+            for (int i = 0; i < groupCount; i++) {
+                final RemoteClient client = clients.get(i);
+                if (sState.isClientCollapsed(client.guid)) {
+                    mList.collapseGroup(i);
+                } else {
+                    mList.expandGroup(i);
+                }
             }
             return;
         }
 
         // Cursor is empty, so set the empty view if it hasn't been set already.
         if (mEmptyView == null) {
             // Set empty panel view. We delay this so that the empty view won't flash.
             final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub);
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/RemoteTabsExpandableListState.java
@@ -0,0 +1,139 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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.home;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.mozilla.gecko.util.PrefUtils;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+
+/**
+ * Encapsulate visual state maintained by the Remote Tabs home panel.
+ * <p>
+ * This state should persist across database updates by Sync and the like. This
+ * state could be stored in a separate "clients_metadata" table and served by
+ * the Tabs provider, but that is heavy-weight for what we want to achieve. Such
+ * a scheme would require either an expensive table join, or a tricky
+ * co-ordination between multiple cursors. In contrast, this is easy and cheap
+ * enough to do on the main thread.
+ * <p>
+ * This state is "per SharedPreferences" object. In practice, there should exist
+ * one state object per Gecko Profile; since we can't change profiles without
+ * killing our process, this can be a static singleton.
+ */
+public class RemoteTabsExpandableListState {
+    private static final String PREF_COLLAPSED_CLIENT_GUIDS = "remote_tabs_collapsed_client_guids";
+    private static final String PREF_HIDDEN_CLIENT_GUIDS = "remote_tabs_hidden_client_guids";
+
+    protected final SharedPreferences sharedPrefs;
+
+    // Synchronized by the state instance. The default is to expand a clients
+    // tabs, so "not present" means "expanded".
+    // Only accessed from the UI thread.
+    protected final Set<String> collapsedClients;
+
+    // Synchronized by the state instance. The default is to show a client, so
+    // "not present" means "shown".
+    // Only accessed from the UI thread.
+    protected final Set<String> hiddenClients;
+
+    public RemoteTabsExpandableListState(SharedPreferences sharedPrefs) {
+        if (null == sharedPrefs) {
+            throw new IllegalArgumentException("sharedPrefs must not be null");
+        }
+        this.sharedPrefs = sharedPrefs;
+
+        this.collapsedClients = getStringSet(PREF_COLLAPSED_CLIENT_GUIDS);
+        this.hiddenClients = getStringSet(PREF_HIDDEN_CLIENT_GUIDS);
+    }
+
+    /**
+     * Extract a string set from shared preferences.
+     * <p>
+     * Nota bene: it is not OK to modify the set returned by {@link SharedPreferences#getStringSet(String, Set)}.
+     *
+     * @param pref to read from.
+     * @returns string set; never null.
+     */
+    protected Set<String> getStringSet(String pref) {
+        final Set<String> loaded = PrefUtils.getStringSet(sharedPrefs, pref, null);
+        if (loaded != null) {
+            return new HashSet<String>(loaded);
+        } else {
+            return new HashSet<String>();
+        }
+    }
+
+    /**
+     * Update client membership in a set.
+     *
+     * @param pref
+     *            to write updated set to.
+     * @param clients
+     *            set to update membership in.
+     * @param clientGuid
+     *            to update membership of.
+     * @param isMember
+     *            whether the client is a member of the set.
+     * @return true if the set of clients was modified.
+     */
+    protected boolean updateClientMembership(String pref, Set<String> clients, String clientGuid, boolean isMember) {
+        final boolean modified;
+        if (isMember) {
+            modified = clients.add(clientGuid);
+        } else {
+            modified = clients.remove(clientGuid);
+        }
+
+        if (modified) {
+            // This starts an asynchronous write. We don't care if we drop the
+            // write, and we don't really care if we race between writes, since
+            // we will return results from our in-memory cache.
+            final Editor editor = sharedPrefs.edit();
+            PrefUtils.putStringSet(editor, pref, clients);
+            editor.apply();
+        }
+
+        return modified;
+    }
+
+    /**
+     * Mark a client as collapsed.
+     *
+     * @param clientGuid
+     *            to update.
+     * @param collapsed
+     *            whether the client is collapsed.
+     * @return true if the set of collapsed clients was modified.
+     */
+    protected synchronized boolean setClientCollapsed(String clientGuid, boolean collapsed) {
+        return updateClientMembership(PREF_COLLAPSED_CLIENT_GUIDS, collapsedClients, clientGuid, collapsed);
+    }
+
+    public synchronized boolean isClientCollapsed(String clientGuid) {
+        return collapsedClients.contains(clientGuid);
+    }
+
+    /**
+     * Mark a client as hidden.
+     *
+     * @param clientGuid
+     *            to update.
+     * @param hidden
+     *            whether the client is hidden.
+     * @return true if the set of hidden clients was modified.
+     */
+    protected synchronized boolean setClientHidden(String clientGuid, boolean hidden) {
+        return updateClientMembership(PREF_HIDDEN_CLIENT_GUIDS, hiddenClients, clientGuid, hidden);
+    }
+
+    public synchronized boolean isClientHidden(String clientGuid) {
+        return hiddenClients.contains(clientGuid);
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -289,16 +289,17 @@ gbjar.sources += [
     'home/PanelRefreshLayout.java',
     'home/PanelViewAdapter.java',
     'home/PanelViewItemHandler.java',
     'home/PinSiteDialog.java',
     'home/ReadingListPanel.java',
     'home/ReadingListRow.java',
     'home/RecentTabsPanel.java',
     'home/RemoteTabsExpandableListFragment.java',
+    'home/RemoteTabsExpandableListState.java',
     'home/RemoteTabsPanel.java',
     'home/RemoteTabsStaticFragment.java',
     'home/SearchEngine.java',
     'home/SearchEngineRow.java',
     'home/SearchLoader.java',
     'home/SimpleCursorLoader.java',
     'home/TabMenuStrip.java',
     'home/TabMenuStripLayout.java',