Home | History | Annotate | Download | only in dialer
      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. */
     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 
     82     public StrequentsAdapter(Context context, UiCallManager callManager) {
     83         mContext = context;
     84         mUiCallManager = callManager;
     85         mContentResolver = context.getContentResolver();
     86     }
     87 
     88     public void setStrequentsListener(@Nullable StrequentsListener<CallLogViewHolder> listener) {
     89         mStrequentsListener = listener;
     90     }
     91 
     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 
    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 
    114     private void setData(List<ContactEntry> data) {
    115         mData = data;
    116         notifyDataSetChanged();
    117     }
    118 
    119     @Override
    120     public void setMaxItems(int maxItems) {
    121         mMaxItems = maxItems;
    122     }
    123 
    124     @Override
    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
    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
    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
    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 
    197     private void onViewClicked(CallLogViewHolder viewHolder) {
    198         if (mStrequentsListener != null) {
    199             mStrequentsListener.onContactClicked(viewHolder);
    200         }
    201     }
    202 
    203     @Override
    204     public void onViewAttachedToWindow(CallLogViewHolder holder) {
    205         if (mFocusChangeListener != null) {
    206             holder.itemView.setOnFocusChangeListener(mFocusChangeListener);
    207         }
    208     }
    209 
    210     @Override
    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      */
    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      */
    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
    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      */
    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
    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 
    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 
    395         public String getNumber() {
    396             return mNumber;
    397         }
    398 
    399         public String getPrimaryText() {
    400             return mPrimaryText;
    401         }
    402 
    403         public String getSecondaryText() {
    404             return mSecondaryText;
    405         }
    406 
    407         public int[] getCallTypes() {
    408             return mCallTypes;
    409         }
    410     }
    411 
    412     private class LastCallPeriodicalUpdater implements Runnable {
    413 
    414         @Override
    415         public void run() {
    416             mLastCallData = convertLastCallCursor(mLastCallCursor);
    417             notifyItemChanged(0);
    418             mMainThreadHandler.postDelayed(this, LAST_CALL_REFRESH_INTERVAL_MILLIS);
    419         }
    420     }
    421 }
    422