diff --git a/mobile/android/base/home/RemoteTabsExpandableListFragment.java b/mobile/android/base/home/RemoteTabsExpandableListFragment.java index 6681734aa39..11df02b22c3 100644 --- a/mobile/android/base/home/RemoteTabsExpandableListFragment.java +++ b/mobile/android/base/home/RemoteTabsExpandableListFragment.java @@ -8,6 +8,7 @@ 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; @@ -52,6 +53,10 @@ public class RemoteTabsExpandableListFragment extends HomeFragment { 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; @@ -117,9 +122,15 @@ public class RemoteTabsExpandableListFragment extends HomeFragment { 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; } }); @@ -165,6 +176,15 @@ public class RemoteTabsExpandableListFragment extends HomeFragment { 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); @@ -176,8 +196,15 @@ public class RemoteTabsExpandableListFragment extends HomeFragment { private void updateUiFromClients(List 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; } diff --git a/mobile/android/base/home/RemoteTabsExpandableListState.java b/mobile/android/base/home/RemoteTabsExpandableListState.java new file mode 100644 index 00000000000..647e7fe2011 --- /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. + *

+ * 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. + *

+ * 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 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 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. + *

+ * 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 getStringSet(String pref) { + final Set loaded = PrefUtils.getStringSet(sharedPrefs, pref, null); + if (loaded != null) { + return new HashSet(loaded); + } else { + return new HashSet(); + } + } + + /** + * 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 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); + } +} diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index d0a35168425..2317b6d35c9 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -294,6 +294,7 @@ gbjar.sources += [ 'home/ReadingListRow.java', 'home/RecentTabsPanel.java', 'home/RemoteTabsExpandableListFragment.java', + 'home/RemoteTabsExpandableListState.java', 'home/RemoteTabsPanel.java', 'home/RemoteTabsStaticFragment.java', 'home/SearchEngine.java',