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