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