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