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.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. */
     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) {
     77         mContext = context;
     78         mContentResolver = context.getContentResolver();
     79     }
     80 
     81     public void setStrequentsListener(@Nullable StrequentsListener<CallLogViewHolder> listener) {
     82         mStrequentsListener = listener;
     83     }
     84 
     85     public void setFocusChangeListener(@Nullable View.OnFocusChangeListener listener) {
     86         mFocusChangeListener = listener;
     87     }
     88 
     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 
     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 
    105     private void setData(List<ContactEntry> data) {
    106         mData = data;
    107         notifyDataSetChanged();
    108     }
    109 
    110     @Override
    111     public void setMaxItems(int maxItems) {
    112         mMaxItems = maxItems;
    113     }
    114 
    115     @Override
    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
    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
    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
    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 
    192     private void onViewClicked(CallLogViewHolder viewHolder) {
    193         if (mStrequentsListener != null) {
    194             mStrequentsListener.onContactClicked(viewHolder);
    195         }
    196     }
    197 
    198     @Override
    199     public void onViewAttachedToWindow(CallLogViewHolder holder) {
    200         if (mFocusChangeListener != null) {
    201             holder.itemView.setOnFocusChangeListener(mFocusChangeListener);
    202         }
    203     }
    204 
    205     @Override
    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      */
    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      */
    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
    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      */
    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      */
    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
    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 
    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 
    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 
    418         public String getNumber() {
    419             return mNumber;
    420         }
    421 
    422         public String getPrimaryText() {
    423             return mPrimaryText;
    424         }
    425 
    426         public String getSecondaryText() {
    427             return mSecondaryText;
    428         }
    429 
    430         public int[] getCallTypes() {
    431             return mCallTypes;
    432         }
    433     }
    434 }
    435