1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.dialer.speeddial;
18 
19 import android.content.Context;
20 import android.support.annotation.IntDef;
21 import android.support.annotation.NonNull;
22 import android.support.annotation.Nullable;
23 import android.support.v7.widget.GridLayoutManager.SpanSizeLookup;
24 import android.support.v7.widget.RecyclerView;
25 import android.support.v7.widget.RecyclerView.ViewHolder;
26 import android.support.v7.widget.helper.ItemTouchHelper;
27 import android.util.ArrayMap;
28 import android.view.LayoutInflater;
29 import android.view.ViewGroup;
30 import android.view.animation.AnticipateInterpolator;
31 import android.widget.FrameLayout;
32 import com.android.dialer.common.Assert;
33 import com.android.dialer.speeddial.FavoritesViewHolder.FavoriteContactsListener;
34 import com.android.dialer.speeddial.HeaderViewHolder.SpeedDialHeaderListener;
35 import com.android.dialer.speeddial.SpeedDialFragment.HostInterface;
36 import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListener;
37 import com.android.dialer.speeddial.draghelper.SpeedDialItemTouchHelperCallback.ItemTouchHelperAdapter;
38 import com.android.dialer.speeddial.loader.SpeedDialUiItem;
39 import com.google.common.collect.ImmutableList;
40 import java.lang.annotation.Retention;
41 import java.lang.annotation.RetentionPolicy;
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.Map;
45 
46 /**
47  * RecyclerView adapter for {@link SpeedDialFragment}.
48  *
49  * <p>Displays a list in the following order:
50  *
51  * <ol>
52  *   <li>Favorite contacts header (with add button)
53  *   <li>Favorite contacts
54  *   <li>Suggested contacts header
55  *   <li>Suggested contacts
56  * </ol>
57  */
58 public final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
59     implements ItemTouchHelperAdapter {
60 
61   private static final int NON_CONTACT_ITEM_NUMBER_BEFORE_FAVORITES = 2;
62   private static final int NON_CONTACT_ITEM_NUMBER_BEFORE_SUGGESTION = 3;
63 
64   private static final float IN_REMOVE_VIEW_SCALE = 0.5f;
65   private static final float IN_REMOVE_VIEW_ALPHA = 0.5f;
66 
67   @Retention(RetentionPolicy.SOURCE)
68   @IntDef({RowType.STARRED_HEADER, RowType.SUGGESTION_HEADER, RowType.STARRED, RowType.SUGGESTION})
69   @interface RowType {
70     int REMOVE_VIEW = 0;
71     int STARRED_HEADER = 1;
72     int SUGGESTION_HEADER = 2;
73     int STARRED = 3;
74     int SUGGESTION = 4;
75   }
76 
77   private final Context context;
78   private final FavoriteContactsListener favoritesListener;
79   private final SuggestedContactsListener suggestedListener;
80   private final SpeedDialHeaderListener headerListener;
81   private final HostInterface hostInterface;
82 
83   private final Map<Integer, Integer> positionToRowTypeMap = new ArrayMap<>();
84   private List<SpeedDialUiItem> speedDialUiItems;
85 
86   // Needed for FavoriteViewHolder
87   private ItemTouchHelper itemTouchHelper;
88 
89   private RemoveViewHolder removeViewHolder;
90   private FavoritesViewHolder draggingFavoritesViewHolder;
91 
SpeedDialAdapter( Context context, FavoriteContactsListener favoritesListener, SuggestedContactsListener suggestedListener, SpeedDialHeaderListener headerListener, HostInterface hostInterface)92   public SpeedDialAdapter(
93       Context context,
94       FavoriteContactsListener favoritesListener,
95       SuggestedContactsListener suggestedListener,
96       SpeedDialHeaderListener headerListener,
97       HostInterface hostInterface) {
98     this.context = context;
99     this.favoritesListener = favoritesListener;
100     this.suggestedListener = suggestedListener;
101     this.headerListener = headerListener;
102     this.hostInterface = hostInterface;
103   }
104 
105   @Override
getItemViewType(int position)106   public int getItemViewType(int position) {
107     return positionToRowTypeMap.get(position);
108   }
109 
110   @NonNull
111   @Override
onCreateViewHolder(@onNull ViewGroup parent, int viewType)112   public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
113     LayoutInflater inflater = LayoutInflater.from(context);
114     switch (viewType) {
115       case RowType.STARRED:
116         return new FavoritesViewHolder(
117             inflater.inflate(R.layout.favorite_item_layout, parent, false),
118             itemTouchHelper,
119             favoritesListener);
120       case RowType.SUGGESTION:
121         return new SuggestionViewHolder(
122             inflater.inflate(R.layout.suggestion_row_layout, parent, false), suggestedListener);
123       case RowType.STARRED_HEADER:
124       case RowType.SUGGESTION_HEADER:
125         return new HeaderViewHolder(
126             inflater.inflate(R.layout.speed_dial_header_layout, parent, false), headerListener);
127       case RowType.REMOVE_VIEW:
128         removeViewHolder =
129             new RemoveViewHolder(
130                 inflater.inflate(R.layout.favorite_remove_view_layout, parent, false));
131         return removeViewHolder;
132       default:
133         throw Assert.createIllegalStateFailException("Invalid viewType: " + viewType);
134     }
135   }
136 
137   @Override
onBindViewHolder(@onNull ViewHolder holder, int position)138   public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
139     switch (getItemViewType(position)) {
140       case RowType.STARRED_HEADER:
141         ((HeaderViewHolder) holder).setHeaderText(R.string.favorites_header);
142         ((HeaderViewHolder) holder).showAddButton(true);
143         return;
144       case RowType.SUGGESTION_HEADER:
145         ((HeaderViewHolder) holder).setHeaderText(R.string.suggestions_header);
146         ((HeaderViewHolder) holder).showAddButton(false);
147         return;
148       case RowType.STARRED:
149         ((FavoritesViewHolder) holder).bind(context, speedDialUiItems.get(position - 2));
150         // Removed item might come back
151         FrameLayout avatarContainer = ((FavoritesViewHolder) holder).getAvatarContainer();
152         avatarContainer.setScaleX(1);
153         avatarContainer.setScaleY(1);
154         avatarContainer.setAlpha(1);
155         break;
156       case RowType.SUGGESTION:
157         ((SuggestionViewHolder) holder).bind(context, speedDialUiItems.get(position - 3));
158         break;
159       case RowType.REMOVE_VIEW:
160         break;
161       default:
162         throw Assert.createIllegalStateFailException("Invalid view holder: " + holder);
163     }
164   }
165 
166   @Override
getItemCount()167   public int getItemCount() {
168     return positionToRowTypeMap.size();
169   }
170 
setSpeedDialUiItems(List<SpeedDialUiItem> immutableSpeedDialUiItems)171   public void setSpeedDialUiItems(List<SpeedDialUiItem> immutableSpeedDialUiItems) {
172     speedDialUiItems = new ArrayList<>();
173     speedDialUiItems.addAll(immutableSpeedDialUiItems);
174     speedDialUiItems.sort(
175         (o1, o2) -> {
176           if (o1.isStarred() && o2.isStarred()) {
177             return Integer.compare(o1.pinnedPosition().or(-1), o2.pinnedPosition().or(-1));
178           }
179           return Boolean.compare(o2.isStarred(), o1.isStarred());
180         });
181     updatePositionToRowTypeMap();
182   }
183 
updatePositionToRowTypeMap()184   private void updatePositionToRowTypeMap() {
185     positionToRowTypeMap.clear();
186     if (speedDialUiItems.isEmpty()) {
187       return;
188     }
189 
190     positionToRowTypeMap.put(0, RowType.REMOVE_VIEW);
191     // Show the add favorites even if there are no favorite contacts
192     positionToRowTypeMap.put(1, RowType.STARRED_HEADER);
193     int positionOfSuggestionHeader = NON_CONTACT_ITEM_NUMBER_BEFORE_FAVORITES;
194     for (int i = 0; i < speedDialUiItems.size(); i++) {
195       if (speedDialUiItems.get(i).isStarred()) {
196         positionToRowTypeMap.put(i + NON_CONTACT_ITEM_NUMBER_BEFORE_FAVORITES, RowType.STARRED);
197         positionOfSuggestionHeader++;
198       } else {
199         positionToRowTypeMap.put(i + NON_CONTACT_ITEM_NUMBER_BEFORE_SUGGESTION, RowType.SUGGESTION);
200       }
201     }
202     if (!speedDialUiItems.get(speedDialUiItems.size() - 1).isStarred()) {
203       positionToRowTypeMap.put(positionOfSuggestionHeader, RowType.SUGGESTION_HEADER);
204     }
205   }
206 
getSpeedDialUiItems()207   public ImmutableList<SpeedDialUiItem> getSpeedDialUiItems() {
208     if (speedDialUiItems == null || speedDialUiItems.isEmpty()) {
209       return ImmutableList.of();
210     }
211     return ImmutableList.copyOf(speedDialUiItems);
212   }
213 
getSpanSizeLookup()214   public SpanSizeLookup getSpanSizeLookup() {
215     return new SpanSizeLookup() {
216       @Override
217       public int getSpanSize(int position) {
218         switch (getItemViewType(position)) {
219           case RowType.SUGGESTION:
220           case RowType.STARRED_HEADER:
221           case RowType.SUGGESTION_HEADER:
222           case RowType.REMOVE_VIEW:
223             return 3; // span the whole screen
224           case RowType.STARRED:
225             return 1; // span 1/3 of the screen
226           default:
227             throw Assert.createIllegalStateFailException(
228                 "Invalid row type: " + positionToRowTypeMap.get(position));
229         }
230       }
231     };
232   }
233 
234   @Override
235   public void onItemMove(int fromPosition, int toPosition) {
236     if (toPosition == 0) {
237       // drop to removeView
238       return;
239     }
240     // fromPosition/toPosition correspond to adapter position, which is off by 1 from the list
241     // position b/c of the favorites header. So subtract 1 here.
242     speedDialUiItems.add(toPosition - 2, speedDialUiItems.remove(fromPosition - 2));
243     notifyItemMoved(fromPosition, toPosition);
244   }
245 
246   @Override
247   public boolean canDropOver(ViewHolder target) {
248     return target instanceof FavoritesViewHolder || target instanceof RemoveViewHolder;
249   }
250 
251   @Override
252   public void onSelectedChanged(@Nullable ViewHolder viewHolder, int actionState) {
253     switch (actionState) {
254       case ItemTouchHelper.ACTION_STATE_DRAG:
255         if (viewHolder != null) {
256           draggingFavoritesViewHolder = (FavoritesViewHolder) viewHolder;
257           draggingFavoritesViewHolder.onSelectedChanged(true);
258           hostInterface.dragFavorite(true);
259           removeViewHolder.show();
260         }
261         break;
262       case ItemTouchHelper.ACTION_STATE_IDLE:
263         // viewHolder is null in this case
264         if (draggingFavoritesViewHolder != null) {
265           draggingFavoritesViewHolder.onSelectedChanged(false);
266           draggingFavoritesViewHolder = null;
267           hostInterface.dragFavorite(false);
268           removeViewHolder.hide();
269         }
270         break;
271       default:
272         break;
273     }
274   }
275 
276   @Override
277   public void enterRemoveView() {
278     if (draggingFavoritesViewHolder != null) {
279       draggingFavoritesViewHolder
280           .getAvatarContainer()
281           .animate()
282           .scaleX(IN_REMOVE_VIEW_SCALE)
283           .scaleY(IN_REMOVE_VIEW_SCALE)
284           .alpha(IN_REMOVE_VIEW_ALPHA)
285           .start();
286     }
287   }
288 
289   @Override
290   public void leaveRemoveView() {
291     if (draggingFavoritesViewHolder != null) {
292       draggingFavoritesViewHolder
293           .getAvatarContainer()
294           .animate()
295           .scaleX(1)
296           .scaleY(1)
297           .alpha(1)
298           .start();
299     }
300   }
301 
302   @Override
303   public void dropOnRemoveView(ViewHolder fromViewHolder) {
304     if (!(fromViewHolder instanceof FavoritesViewHolder)) {
305       return;
306     }
307     int fromPosition = fromViewHolder.getAdapterPosition();
308 
309     SpeedDialUiItem removedItem = speedDialUiItems.remove(fromPosition - 2);
310     favoritesListener.onRequestRemove(removedItem);
311     ((FavoritesViewHolder) fromViewHolder)
312         .getAvatarContainer()
313         .animate()
314         .scaleX(0)
315         .scaleY(0)
316         .alpha(0)
317         .setInterpolator(new AnticipateInterpolator())
318         .start();
319     updatePositionToRowTypeMap();
320   }
321 
322   public void setItemTouchHelper(ItemTouchHelper itemTouchHelper) {
323     this.itemTouchHelper = itemTouchHelper;
324   }
325 
326   /** Returns true if there are suggested contacts. */
327   public boolean hasFrequents() {
328     return !speedDialUiItems.isEmpty() && getItemViewType(getItemCount() - 1) == RowType.SUGGESTION;
329   }
330 }
331