• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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 package com.android.car.dialer;
17 
18 import android.content.ContentResolver;
19 import android.content.Context;
20 import android.database.Cursor;
21 import android.graphics.PorterDuff;
22 import android.provider.CallLog;
23 import android.support.annotation.Nullable;
24 import android.support.v7.widget.RecyclerView;
25 import android.text.TextUtils;
26 import android.text.format.DateUtils;
27 import android.view.LayoutInflater;
28 import android.view.View;
29 import android.view.ViewGroup;
30 
31 import com.android.car.dialer.telecom.PhoneLoader;
32 import com.android.car.dialer.telecom.TelecomUtils;
33 import com.android.car.dialer.telecom.UiCallManager;
34 import com.android.car.view.PagedListView;
35 
36 import java.util.ArrayList;
37 import java.util.Collections;
38 import java.util.HashMap;
39 import java.util.List;
40 
41 /**
42  * Adapter class for populating Contact data as loaded from the DB to an AA GroupingRecyclerView.
43  * It handles two types of contacts:
44  * <p>
45  * <ul>
46  *     <li>Strequent contacts (starred and/or frequent)
47  *     <li>Last call contact
48  * </ul>
49  */
50 public class StrequentsAdapter extends RecyclerView.Adapter<CallLogViewHolder>
51         implements PagedListView.ItemCap {
52     // The possible view types in this adapter.
53     private static final int VIEW_TYPE_EMPTY = 0;
54     private static final int VIEW_TYPE_LASTCALL = 1;
55     private static final int VIEW_TYPE_STREQUENT = 2;
56 
57     private final Context mContext;
58     private final UiCallManager mUiCallManager;
59     private List<ContactEntry> mData;
60 
61     private LastCallData mLastCallData;
62 
63     private final ContentResolver mContentResolver;
64 
65     public interface StrequentsListener<T> {
66         /** Notified when a row corresponding an individual Contact (not group) was clicked. */
onContactClicked(T viewHolder)67         void onContactClicked(T viewHolder);
68     }
69 
70     private View.OnFocusChangeListener mFocusChangeListener;
71     private StrequentsListener<CallLogViewHolder> mStrequentsListener;
72 
73     private int mMaxItems = -1;
74     private boolean mIsEmpty;
75 
StrequentsAdapter(Context context, UiCallManager callManager)76     public StrequentsAdapter(Context context, UiCallManager callManager) {
77         mContext = context;
78         mUiCallManager = callManager;
79         mContentResolver = context.getContentResolver();
80     }
81 
setStrequentsListener(@ullable StrequentsListener<CallLogViewHolder> listener)82     public void setStrequentsListener(@Nullable StrequentsListener<CallLogViewHolder> listener) {
83         mStrequentsListener = listener;
84     }
85 
setLastCallCursor(@ullable Cursor cursor)86     public void setLastCallCursor(@Nullable Cursor cursor) {
87         mLastCallData = convertLastCallCursor(cursor);
88         notifyDataSetChanged();
89     }
90 
setStrequentCursor(@ullable Cursor cursor)91     public void setStrequentCursor(@Nullable Cursor cursor) {
92         if (cursor != null) {
93             setData(convertStrequentCursorToArray(cursor));
94         } else {
95             setData(null);
96         }
97         notifyDataSetChanged();
98     }
99 
setData(List<ContactEntry> data)100     private void setData(List<ContactEntry> data) {
101         mData = data;
102         notifyDataSetChanged();
103     }
104 
105     @Override
setMaxItems(int maxItems)106     public void setMaxItems(int maxItems) {
107         mMaxItems = maxItems;
108     }
109 
110     @Override
getItemViewType(int position)111     public int getItemViewType(int position) {
112         if (mIsEmpty) {
113             return VIEW_TYPE_EMPTY;
114         } else if (position == 0 && mLastCallData != null) {
115             return VIEW_TYPE_LASTCALL;
116         } else {
117             return VIEW_TYPE_STREQUENT;
118         }
119     }
120 
121     @Override
getItemCount()122     public int getItemCount() {
123         int itemCount = mData == null ? 0 : mData.size();
124         itemCount += mLastCallData == null ? 0 : 1;
125 
126         mIsEmpty = itemCount == 0;
127 
128         // If there is no data to display, add one to the item count to display the card in the
129         // empty state.
130         if (mIsEmpty) {
131             itemCount++;
132         }
133 
134         return mMaxItems >= 0 ? Math.min(mMaxItems, itemCount) : itemCount;
135     }
136 
137     @Override
onCreateViewHolder(ViewGroup parent, int viewType)138     public CallLogViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
139         View view;
140         switch (viewType) {
141             case VIEW_TYPE_LASTCALL:
142                 view = LayoutInflater.from(parent.getContext())
143                         .inflate(R.layout.call_log_last_call_item_card, parent, false);
144                 return new CallLogViewHolder(view);
145 
146             case VIEW_TYPE_EMPTY:
147                 view = LayoutInflater.from(parent.getContext())
148                         .inflate(R.layout.car_list_item_empty, parent, false);
149                 return new CallLogViewHolder(view);
150 
151             case VIEW_TYPE_STREQUENT:
152             default:
153                 view = LayoutInflater.from(parent.getContext())
154                         .inflate(R.layout.call_log_list_item_card, parent, false);
155                 return new CallLogViewHolder(view);
156         }
157     }
158 
159     @Override
onBindViewHolder(final CallLogViewHolder viewHolder, int position)160     public void onBindViewHolder(final CallLogViewHolder viewHolder, int position) {
161         switch (viewHolder.getItemViewType()) {
162             case VIEW_TYPE_LASTCALL:
163                 onBindLastCallRow(viewHolder);
164                 break;
165 
166             case VIEW_TYPE_EMPTY:
167                 viewHolder.icon.setImageResource(R.drawable.ic_empty_speed_dial);
168                 viewHolder.title.setText(R.string.speed_dial_empty);
169                 viewHolder.title.setTextColor(mContext.getColor(R.color.car_body1_light));
170                 break;
171 
172             case VIEW_TYPE_STREQUENT:
173             default:
174                 int positionIntoData = position;
175 
176                 // If there is last call data, then decrement the position so there is not an out of
177                 // bounds error on the mData.
178                 if (mLastCallData != null) {
179                     positionIntoData--;
180                 }
181 
182                 onBindView(viewHolder, mData.get(positionIntoData));
183                 viewHolder.callType.setVisibility(View.VISIBLE);
184         }
185     }
186 
onViewClicked(CallLogViewHolder viewHolder)187     private void onViewClicked(CallLogViewHolder viewHolder) {
188         if (mStrequentsListener != null) {
189             mStrequentsListener.onContactClicked(viewHolder);
190         }
191     }
192 
193     @Override
onViewAttachedToWindow(CallLogViewHolder holder)194     public void onViewAttachedToWindow(CallLogViewHolder holder) {
195         if (mFocusChangeListener != null) {
196             holder.itemView.setOnFocusChangeListener(mFocusChangeListener);
197         }
198     }
199 
200     @Override
onViewDetachedFromWindow(CallLogViewHolder holder)201     public void onViewDetachedFromWindow(CallLogViewHolder holder) {
202         holder.itemView.setOnFocusChangeListener(null);
203     }
204 
205     /**
206      * Converts the strequents data in the given cursor into a list of {@link ContactEntry}s.
207      */
convertStrequentCursorToArray(Cursor cursor)208     private List<ContactEntry> convertStrequentCursorToArray(Cursor cursor) {
209         List<ContactEntry> strequentContactEntries = new ArrayList<>();
210         HashMap<Integer, ContactEntry> entryMap = new HashMap<>();
211         cursor.moveToPosition(-1);
212 
213         while (cursor.moveToNext()) {
214             final ContactEntry entry = ContactEntry.fromCursor(cursor, mContext);
215             entryMap.put(entry.hashCode(), entry);
216         }
217 
218         strequentContactEntries.addAll(entryMap.values());
219         Collections.sort(strequentContactEntries);
220         return strequentContactEntries;
221     }
222 
223     /**
224      * Binds the views in the entry to the data of last call.
225      *
226      * @param viewHolder the view holder corresponding to this entry
227      */
onBindLastCallRow(final CallLogViewHolder viewHolder)228     private void onBindLastCallRow(final CallLogViewHolder viewHolder) {
229         if (mLastCallData == null) {
230             return;
231         }
232 
233         viewHolder.itemView.setOnClickListener(v -> onViewClicked(viewHolder));
234 
235         String primaryText = mLastCallData.getPrimaryText();
236         String number = mLastCallData.getNumber();
237 
238         viewHolder.title.setText(mLastCallData.getPrimaryText());
239         viewHolder.text.setText(mLastCallData.getSecondaryText());
240         viewHolder.itemView.setTag(number);
241         viewHolder.callTypeIconsView.clear();
242         viewHolder.callTypeIconsView.setVisibility(View.VISIBLE);
243 
244         // mHasFirstItem is true only in main screen, or else it is in drawer, then we need to add
245         // call type icons for call history items.
246         viewHolder.smallIcon.setVisibility(View.GONE);
247         int[] callTypes = mLastCallData.getCallTypes();
248         int icons = Math.min(callTypes.length, CallTypeIconsView.MAX_CALL_TYPE_ICONS);
249         for (int i = 0; i < icons; i++) {
250             viewHolder.callTypeIconsView.add(callTypes[i]);
251         }
252 
253         setBackground(viewHolder);
254 
255         TelecomUtils.setContactBitmapAsync(mContext, viewHolder.icon, primaryText, number);
256     }
257 
258     /**
259      * Converts the last call information in the given cursor into a {@link LastCallData} object
260      * so that the cursor can be closed.
261      *
262      * @return A valid {@link LastCallData} or {@code null} if the cursor is {@code null} or has no
263      * data in it.
264      */
265     @Nullable
convertLastCallCursor(@ullable Cursor cursor)266     public LastCallData convertLastCallCursor(@Nullable Cursor cursor) {
267         if (cursor == null || cursor.getCount() == 0) {
268             return null;
269         }
270 
271         cursor.moveToFirst();
272 
273         final StringBuilder nameSb = new StringBuilder();
274         int column = PhoneLoader.getNameColumnIndex(cursor);
275         String cachedName = cursor.getString(column);
276         final String number = PhoneLoader.getPhoneNumber(cursor, mContentResolver);
277         if (cachedName == null) {
278             cachedName = TelecomUtils.getDisplayName(mContext, number);
279         }
280 
281         boolean isVoicemail = false;
282         if (cachedName == null) {
283             if (number.equals(TelecomUtils.getVoicemailNumber(mContext))) {
284                 isVoicemail = true;
285                 nameSb.append(mContext.getString(R.string.voicemail));
286             } else {
287                 String displayName = TelecomUtils.getFormattedNumber(mContext, number);
288                 if (TextUtils.isEmpty(displayName)) {
289                     displayName = mContext.getString(R.string.unknown);
290                 }
291                 nameSb.append(displayName);
292             }
293         } else {
294             nameSb.append(cachedName);
295         }
296         column = cursor.getColumnIndex(CallLog.Calls.DATE);
297         // If we set this to 0, getRelativeTime will return null and no relative time
298         // will be displayed.
299         long millis = column == -1 ? 0 : cursor.getLong(column);
300         StringBuilder secondaryText = new StringBuilder();
301         CharSequence relativeDate = getRelativeTime(millis);
302         if (!isVoicemail) {
303             CharSequence type = TelecomUtils.getTypeFromNumber(mContext, number);
304             secondaryText.append(type);
305             if (!TextUtils.isEmpty(type) && !TextUtils.isEmpty(relativeDate)) {
306                 secondaryText.append(", ");
307             }
308         }
309         if (relativeDate != null) {
310             secondaryText.append(relativeDate);
311         }
312 
313         int[] callTypes = mUiCallManager.getCallTypes(cursor, 1);
314 
315         return new LastCallData(number, nameSb.toString(), secondaryText.toString(), callTypes);
316     }
317 
318     /**
319      * Bind view function for frequent call row.
320      */
onBindView(final CallLogViewHolder viewHolder, final ContactEntry entry)321     private void onBindView(final CallLogViewHolder viewHolder, final ContactEntry entry) {
322         viewHolder.itemView.setOnClickListener(v -> onViewClicked(viewHolder));
323 
324         final String number = entry.number;
325         // TODO(mcrico): Why is being a voicemail related to not having a name?
326         boolean isVoicemail = (entry.name == null)
327                 && (number.equals(TelecomUtils.getVoicemailNumber(mContext)));
328         String secondaryText = "";
329         if (!isVoicemail) {
330             secondaryText = String.valueOf(TelecomUtils.getTypeFromNumber(mContext, number));
331         }
332 
333         viewHolder.text.setText(secondaryText);
334         viewHolder.itemView.setTag(number);
335         viewHolder.callTypeIconsView.clear();
336 
337         String displayName = entry.getDisplayName();
338         viewHolder.title.setText(displayName);
339 
340         TelecomUtils.setContactBitmapAsync(mContext, viewHolder.icon, displayName, number);
341 
342         if (entry.isStarred) {
343             viewHolder.smallIcon.setVisibility(View.VISIBLE);
344             final int iconColor = mContext.getColor(android.R.color.white);
345             viewHolder.smallIcon.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
346             viewHolder.smallIcon.setImageResource(R.drawable.ic_favorite);
347         } else {
348             viewHolder.smallIcon.setVisibility(View.GONE);
349         }
350 
351         setBackground(viewHolder);
352     }
353 
354     /**
355      * Appropriately sets the background for the View that is being bound. This method will allow
356      * for rounded corners on either the top or bottom of a card.
357      */
setBackground(CallLogViewHolder viewHolder)358     private void setBackground(CallLogViewHolder viewHolder) {
359         int itemCount = getItemCount();
360         int adapterPosition = viewHolder.getAdapterPosition();
361 
362         if (itemCount == 1) {
363             // Only element - all corners are rounded
364             viewHolder.card.setBackgroundResource(
365                     R.drawable.car_card_rounded_top_bottom_background);
366         } else if (adapterPosition == 0) {
367             // First element gets rounded top
368             viewHolder.card.setBackgroundResource(R.drawable.car_card_rounded_top_background);
369         } else if (adapterPosition == itemCount - 1) {
370             // Last one has a rounded bottom
371             viewHolder.card.setBackgroundResource(R.drawable.car_card_rounded_bottom_background);
372         } else {
373             // Middle have no rounded corners
374             viewHolder.card.setBackgroundResource(R.color.car_card);
375         }
376     }
377 
378     /**
379      * Build any timestamp and label into a single string. If the given timestamp is invalid, then
380      * {@code null} is returned.
381      */
382     @Nullable
getRelativeTime(long millis)383     private static CharSequence getRelativeTime(long millis) {
384         if (millis <= 0) {
385             return null;
386         }
387 
388         return DateUtils.getRelativeTimeSpanString(millis, System.currentTimeMillis(),
389                 DateUtils.MINUTE_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE);
390     }
391 
392     /**
393      * A container for data relating to a last call entry.
394      */
395     private class LastCallData {
396         private final String mNumber;
397         private final String mPrimaryText;
398         private final String mSecondaryText;
399         private final int[] mCallTypes;
400 
LastCallData(String number, String primaryText, String secondaryText, int[] callTypes)401         LastCallData(String number, String primaryText, String secondaryText,
402                 int[] callTypes) {
403             mNumber = number;
404             mPrimaryText = primaryText;
405             mSecondaryText = secondaryText;
406             mCallTypes = callTypes;
407         }
408 
getNumber()409         public String getNumber() {
410             return mNumber;
411         }
412 
getPrimaryText()413         public String getPrimaryText() {
414             return mPrimaryText;
415         }
416 
getSecondaryText()417         public String getSecondaryText() {
418             return mSecondaryText;
419         }
420 
getCallTypes()421         public int[] getCallTypes() {
422             return mCallTypes;
423         }
424     }
425 }
426