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.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. */
     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 
     76     public StrequentsAdapter(Context context, UiCallManager callManager) {
     77         mContext = context;
     78         mUiCallManager = callManager;
     79         mContentResolver = context.getContentResolver();
     80     }
     81 
     82     public void setStrequentsListener(@Nullable StrequentsListener<CallLogViewHolder> listener) {
     83         mStrequentsListener = listener;
     84     }
     85 
     86     public void setLastCallCursor(@Nullable Cursor cursor) {
     87         mLastCallData = convertLastCallCursor(cursor);
     88         notifyDataSetChanged();
     89     }
     90 
     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 
    100     private void setData(List<ContactEntry> data) {
    101         mData = data;
    102         notifyDataSetChanged();
    103     }
    104 
    105     @Override
    106     public void setMaxItems(int maxItems) {
    107         mMaxItems = maxItems;
    108     }
    109 
    110     @Override
    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
    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
    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
    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 
    187     private void onViewClicked(CallLogViewHolder viewHolder) {
    188         if (mStrequentsListener != null) {
    189             mStrequentsListener.onContactClicked(viewHolder);
    190         }
    191     }
    192 
    193     @Override
    194     public void onViewAttachedToWindow(CallLogViewHolder holder) {
    195         if (mFocusChangeListener != null) {
    196             holder.itemView.setOnFocusChangeListener(mFocusChangeListener);
    197         }
    198     }
    199 
    200     @Override
    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      */
    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      */
    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
    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      */
    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      */
    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
    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 
    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 
    409         public String getNumber() {
    410             return mNumber;
    411         }
    412 
    413         public String getPrimaryText() {
    414             return mPrimaryText;
    415         }
    416 
    417         public String getSecondaryText() {
    418             return mSecondaryText;
    419         }
    420 
    421         public int[] getCallTypes() {
    422             return mCallTypes;
    423         }
    424     }
    425 }
    426