1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2  * This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 package org.mozilla.gecko.home;
7 
8 import android.content.Context;
9 import android.support.annotation.UiThread;
10 import android.support.v4.util.Pair;
11 import android.support.v7.widget.RecyclerView;
12 import android.text.format.DateUtils;
13 import android.view.LayoutInflater;
14 import android.view.View;
15 import android.view.ViewGroup;
16 import android.widget.TextView;
17 import org.mozilla.gecko.GeckoSharedPrefs;
18 import org.mozilla.gecko.R;
19 import org.mozilla.gecko.db.RemoteClient;
20 import org.mozilla.gecko.db.RemoteTab;
21 
22 import java.util.ArrayList;
23 import java.util.Calendar;
24 import java.util.Date;
25 import java.util.GregorianCalendar;
26 import java.util.HashMap;
27 import java.util.LinkedList;
28 import java.util.List;
29 import java.util.Map;
30 
31 import static org.mozilla.gecko.home.CombinedHistoryItem.ItemType.*;
32 
33 public class ClientsAdapter extends RecyclerView.Adapter<CombinedHistoryItem> implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder {
34     public static final String LOGTAG = "GeckoClientsAdapter";
35 
36     /**
37      * If a device claims to have synced before this date, we will assume it has never synced.
38      */
39     public static final Date EARLIEST_VALID_SYNCED_DATE;
40     static {
41         final Calendar c = GregorianCalendar.getInstance();
42         c.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
43         EARLIEST_VALID_SYNCED_DATE = c.getTime();
44     }
45 
46     List<Pair<String, Integer>> adapterList = new LinkedList<>();
47 
48     // List of hidden remote clients.
49     // Only accessed from the UI thread.
50     protected final List<RemoteClient> hiddenClients = new ArrayList<>();
51     private Map<String, RemoteClient> visibleClients = new HashMap<>();
52 
53     // Maintain group collapsed and hidden state. Only accessed from the UI thread.
54     protected static RemoteTabsExpandableListState sState;
55 
56     private final Context context;
57 
ClientsAdapter(Context context)58     public ClientsAdapter(Context context) {
59         this.context = context;
60 
61         // This races when multiple Fragments are created. That's okay: one
62         // will win, and thereafter, all will be okay. If we create and then
63         // drop an instance the shared SharedPreferences backing all the
64         // instances will maintain the state for us. Since everything happens on
65         // the UI thread, this doesn't even need to be volatile.
66         if (sState == null) {
67             sState = new RemoteTabsExpandableListState(GeckoSharedPrefs.forProfile(context));
68         }
69 
70         this.setHasStableIds(true);
71     }
72 
73     @Override
onCreateViewHolder(ViewGroup parent, int viewType)74     public CombinedHistoryItem onCreateViewHolder(ViewGroup parent, int viewType) {
75         final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
76         final View view;
77 
78         final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
79 
80         switch (itemType) {
81             case NAVIGATION_BACK:
82                 view = inflater.inflate(R.layout.home_combined_back_item, parent, false);
83                 return new CombinedHistoryItem.HistoryItem(view);
84 
85             case CLIENT:
86                 view = inflater.inflate(R.layout.home_remote_tabs_group, parent, false);
87                 return new CombinedHistoryItem.ClientItem(view);
88 
89             case CHILD:
90                 view = inflater.inflate(R.layout.home_item_row, parent, false);
91                 return new CombinedHistoryItem.HistoryItem(view);
92 
93             case HIDDEN_DEVICES:
94                 view = inflater.inflate(R.layout.home_remote_tabs_hidden_devices, parent, false);
95                 return new CombinedHistoryItem.BasicItem(view);
96         }
97         return null;
98     }
99 
100     @Override
onBindViewHolder(CombinedHistoryItem holder, final int position)101     public void onBindViewHolder(CombinedHistoryItem holder, final int position) {
102        final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
103 
104         switch (itemType) {
105             case CLIENT:
106                 final CombinedHistoryItem.ClientItem clientItem = (CombinedHistoryItem.ClientItem) holder;
107                 final String clientGuid = adapterList.get(position).first;
108                 final RemoteClient client = visibleClients.get(clientGuid);
109                 clientItem.bind(context, client, sState.isClientCollapsed(clientGuid));
110                 break;
111 
112             case CHILD:
113                 final Pair<String, Integer> pair = adapterList.get(position);
114                 RemoteTab remoteTab = visibleClients.get(pair.first).tabs.get(pair.second);
115                 ((CombinedHistoryItem.HistoryItem) holder).bind(remoteTab);
116                 break;
117 
118             case HIDDEN_DEVICES:
119                 final String hiddenDevicesLabel = context.getResources().getString(R.string.home_remote_tabs_many_hidden_devices, hiddenClients.size());
120                 ((TextView) holder.itemView).setText(hiddenDevicesLabel);
121                 break;
122         }
123     }
124 
125     @Override
getItemCount()126     public int getItemCount () {
127         return adapterList.size();
128     }
129 
getItemTypeForPosition(int position)130     private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) {
131         if (position == 0) {
132             return NAVIGATION_BACK;
133         }
134 
135         final Pair<String, Integer> pair = adapterList.get(position);
136         if (pair == null) {
137             return HIDDEN_DEVICES;
138         } else if (pair.second == -1) {
139             return CLIENT;
140         } else {
141             return CHILD;
142         }
143     }
144 
145     @Override
getItemViewType(int position)146     public int getItemViewType(int position) {
147         return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position));
148     }
149 
150     @Override
getItemId(int position)151     public long getItemId(int position) {
152         // RecyclerView.NO_ID is -1, so start our hard-coded IDs at -2.
153         final int NAVIGATION_BACK_ID = -2;
154         final int HIDDEN_DEVICES_ID = -3;
155 
156         final String clientGuid;
157         // adapterList is a list of tuples (clientGuid, tabId).
158         final Pair<String, Integer> pair = adapterList.get(position);
159 
160         switch (getItemTypeForPosition(position)) {
161             case NAVIGATION_BACK:
162                 return NAVIGATION_BACK_ID;
163 
164             case HIDDEN_DEVICES:
165                 return HIDDEN_DEVICES_ID;
166 
167             // For Clients, return hashCode of their GUIDs.
168             case CLIENT:
169                 clientGuid = pair.first;
170                 return clientGuid.hashCode();
171 
172             // For Tabs, return hashCode of their URLs.
173             case CHILD:
174                 clientGuid = pair.first;
175                 final Integer tabId = pair.second;
176 
177                 final RemoteClient remoteClient = visibleClients.get(clientGuid);
178                 if (remoteClient == null) {
179                     return RecyclerView.NO_ID;
180                 }
181 
182                 final RemoteTab remoteTab = remoteClient.tabs.get(tabId);
183                 if (remoteTab == null) {
184                     return RecyclerView.NO_ID;
185                 }
186 
187                 return remoteTab.url.hashCode();
188 
189             default:
190                 throw new IllegalStateException("Unexpected Home Panel item type");
191         }
192     }
193 
getClientsCount()194     public int getClientsCount() {
195         return hiddenClients.size() + visibleClients.size();
196     }
197 
198     @UiThread
setClients(List<RemoteClient> clients)199     public void setClients(List<RemoteClient> clients) {
200         adapterList.clear();
201         adapterList.add(null);
202 
203         hiddenClients.clear();
204         visibleClients.clear();
205 
206         for (RemoteClient client : clients) {
207             final String guid = client.guid;
208             if (sState.isClientHidden(guid)) {
209                 hiddenClients.add(client);
210             } else {
211                 visibleClients.put(guid, client);
212                 adapterList.addAll(getVisibleItems(client));
213             }
214         }
215 
216         // Add item for unhiding clients.
217         if (!hiddenClients.isEmpty()) {
218             adapterList.add(null);
219         }
220 
221         notifyDataSetChanged();
222     }
223 
getVisibleItems(RemoteClient client)224     private static List<Pair<String, Integer>> getVisibleItems(RemoteClient client) {
225         List<Pair<String, Integer>> list = new LinkedList<>();
226         final String guid = client.guid;
227         list.add(new Pair<>(guid, -1));
228         if (!sState.isClientCollapsed(client.guid)) {
229             for (int i = 0; i < client.tabs.size(); i++) {
230                 list.add(new Pair<>(guid, i));
231             }
232         }
233         return list;
234     }
235 
getHiddenClients()236     public List<RemoteClient> getHiddenClients() {
237         return hiddenClients;
238     }
239 
toggleClient(int position)240     public void toggleClient(int position) {
241         final Pair<String, Integer> pair = adapterList.get(position);
242         if (pair.second != -1) {
243             return;
244         }
245 
246         final String clientGuid = pair.first;
247         final RemoteClient client = visibleClients.get(clientGuid);
248 
249         final boolean isCollapsed = sState.isClientCollapsed(clientGuid);
250 
251         sState.setClientCollapsed(clientGuid, !isCollapsed);
252         notifyItemChanged(position);
253 
254         if (isCollapsed) {
255             for (int i = client.tabs.size() - 1; i > -1; i--) {
256                 // Insert child tabs at the index right after the client item that was clicked.
257                 adapterList.add(position + 1, new Pair<>(clientGuid, i));
258             }
259             notifyItemRangeInserted(position + 1, client.tabs.size());
260         } else {
261             int i = client.tabs.size();
262             while (i > 0) {
263                 adapterList.remove(position + 1);
264                 i--;
265             }
266             notifyItemRangeRemoved(position + 1, client.tabs.size());
267         }
268     }
269 
unhideClients(List<RemoteClient> selectedClients)270     public void unhideClients(List<RemoteClient> selectedClients) {
271         final int numClients = selectedClients.size();
272         if (numClients == 0) {
273             return;
274         }
275 
276         final int insertionIndex = adapterList.size() - 1;
277         int itemCount = numClients;
278 
279         for (RemoteClient client : selectedClients) {
280             final String clientGuid = client.guid;
281 
282             sState.setClientHidden(clientGuid, false);
283             hiddenClients.remove(client);
284 
285             visibleClients.put(clientGuid, client);
286             sState.setClientCollapsed(clientGuid, false);
287             adapterList.addAll(adapterList.size() - 1, getVisibleItems(client));
288 
289             itemCount += client.tabs.size();
290         }
291 
292         notifyItemRangeInserted(insertionIndex, itemCount);
293 
294         final int hiddenDevicesIndex = adapterList.size() - 1;
295         if (hiddenClients.isEmpty()) {
296             // No more hidden clients, remove "unhide" item.
297             adapterList.remove(hiddenDevicesIndex);
298             notifyItemRemoved(hiddenDevicesIndex);
299         } else {
300             // Update "hidden clients" item because number of hidden clients changed.
301             notifyItemChanged(hiddenDevicesIndex);
302         }
303     }
304 
removeItem(int position)305     public void removeItem(int position) {
306         final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
307         switch (itemType) {
308             case CLIENT:
309                 final String clientGuid = adapterList.get(position).first;
310                 final RemoteClient client = visibleClients.remove(clientGuid);
311                 final boolean hadHiddenClients = !hiddenClients.isEmpty();
312 
313                 int removeCount = sState.isClientCollapsed(clientGuid) ? 1 : client.tabs.size() + 1;
314                 int c = removeCount;
315                 while (c > 0) {
316                     adapterList.remove(position);
317                     c--;
318                 }
319                 notifyItemRangeRemoved(position, removeCount);
320 
321                 sState.setClientHidden(clientGuid, true);
322                 hiddenClients.add(client);
323 
324                 if (!hadHiddenClients) {
325                     // Add item for unhiding clients;
326                     adapterList.add(null);
327                     notifyItemInserted(adapterList.size() - 1);
328                 } else {
329                     // Update "hidden clients" item because number of hidden clients changed.
330                     notifyItemChanged(adapterList.size() - 1);
331                 }
332                 break;
333         }
334     }
335 
336     @Override
makeContextMenuInfoFromPosition(View view, int position)337     public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) {
338         final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
339         HomeContextMenuInfo info;
340         final Pair<String, Integer> pair = adapterList.get(position);
341         switch (itemType) {
342             case CHILD:
343                 info = new HomeContextMenuInfo(view, position, -1);
344                 return populateChildInfoFromTab(info, visibleClients.get(pair.first).tabs.get(pair.second));
345 
346             case CLIENT:
347                 info = new CombinedHistoryPanel.RemoteTabsClientContextMenuInfo(view, position, -1, visibleClients.get(pair.first));
348                 return info;
349         }
350         return null;
351     }
352 
populateChildInfoFromTab(HomeContextMenuInfo info, RemoteTab tab)353     protected static HomeContextMenuInfo populateChildInfoFromTab(HomeContextMenuInfo info, RemoteTab tab) {
354         info.url = tab.url;
355         info.title = tab.title;
356         return info;
357     }
358 
359     /**
360      * Return a relative "Last synced" time span for the given tab record.
361      *
362      * @param now local time.
363      * @param time to format string for.
364      * @return string describing time span
365      */
getLastSyncedString(Context context, long now, long time)366     public static String getLastSyncedString(Context context, long now, long time) {
367         if (new Date(time).before(EARLIEST_VALID_SYNCED_DATE)) {
368             return context.getString(R.string.remote_tabs_never_synced);
369         }
370         final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
371         return context.getResources().getString(R.string.remote_tabs_last_synced, relativeTimeSpanString);
372     }
373 }
374