Home | History | Annotate | Download | only in cp2
      1 /*
      2  * Copyright (C) 2017 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 
     17 package com.android.dialer.searchfragment.cp2;
     18 
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.database.Cursor;
     22 import android.net.Uri;
     23 import android.provider.ContactsContract.CommonDataKinds.Phone;
     24 import android.provider.ContactsContract.Contacts;
     25 import android.support.annotation.IntDef;
     26 import android.support.annotation.Nullable;
     27 import android.support.v7.widget.RecyclerView.ViewHolder;
     28 import android.text.TextUtils;
     29 import android.view.View;
     30 import android.view.View.OnClickListener;
     31 import android.widget.ImageView;
     32 import android.widget.QuickContactBadge;
     33 import android.widget.TextView;
     34 import com.android.dialer.common.Assert;
     35 import com.android.dialer.contactphoto.ContactPhotoManager;
     36 import com.android.dialer.dialercontact.DialerContact;
     37 import com.android.dialer.duo.DuoComponent;
     38 import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
     39 import com.android.dialer.enrichedcall.EnrichedCallComponent;
     40 import com.android.dialer.enrichedcall.EnrichedCallManager;
     41 import com.android.dialer.lettertile.LetterTileDrawable;
     42 import com.android.dialer.searchfragment.common.Projections;
     43 import com.android.dialer.searchfragment.common.QueryBoldingUtil;
     44 import com.android.dialer.searchfragment.common.R;
     45 import com.android.dialer.searchfragment.common.RowClickListener;
     46 import com.android.dialer.searchfragment.common.SearchCursor;
     47 import java.lang.annotation.Retention;
     48 import java.lang.annotation.RetentionPolicy;
     49 
     50 /** ViewHolder for a contact row. */
     51 public final class SearchContactViewHolder extends ViewHolder implements OnClickListener {
     52 
     53   /** IntDef for the different types of actions that can be shown. */
     54   @Retention(RetentionPolicy.SOURCE)
     55   @IntDef({
     56     CallToAction.NONE,
     57     CallToAction.VIDEO_CALL,
     58     CallToAction.DUO_CALL,
     59     CallToAction.SHARE_AND_CALL
     60   })
     61   @interface CallToAction {
     62     int NONE = 0;
     63     int VIDEO_CALL = 1;
     64     int DUO_CALL = 2;
     65     int SHARE_AND_CALL = 3;
     66   }
     67 
     68   private final RowClickListener listener;
     69   private final QuickContactBadge photo;
     70   private final TextView nameOrNumberView;
     71   private final TextView numberView;
     72   private final ImageView callToActionView;
     73   private final Context context;
     74 
     75   private int position;
     76   private String number;
     77   private DialerContact dialerContact;
     78   private @CallToAction int currentAction;
     79 
     80   public SearchContactViewHolder(View view, RowClickListener listener) {
     81     super(view);
     82     this.listener = listener;
     83     view.setOnClickListener(this);
     84     photo = view.findViewById(R.id.photo);
     85     nameOrNumberView = view.findViewById(R.id.primary);
     86     numberView = view.findViewById(R.id.secondary);
     87     callToActionView = view.findViewById(R.id.call_to_action);
     88     context = view.getContext();
     89   }
     90 
     91   /**
     92    * Binds the ViewHolder with a cursor from {@link SearchContactsCursorLoader} with the data found
     93    * at the cursors set position.
     94    */
     95   public void bind(SearchCursor cursor, String query) {
     96     dialerContact = getDialerContact(context, cursor);
     97     position = cursor.getPosition();
     98     number = cursor.getString(Projections.PHONE_NUMBER);
     99     String name = cursor.getString(Projections.DISPLAY_NAME);
    100     String label = getLabel(context.getResources(), cursor);
    101     String secondaryInfo =
    102         TextUtils.isEmpty(label)
    103             ? number
    104             : context.getString(
    105                 com.android.contacts.common.R.string.call_subject_type_and_number, label, number);
    106 
    107     nameOrNumberView.setText(QueryBoldingUtil.getNameWithQueryBolded(query, name, context));
    108     numberView.setText(QueryBoldingUtil.getNumberWithQueryBolded(query, secondaryInfo));
    109     setCallToAction(cursor, query);
    110 
    111     if (shouldShowPhoto(cursor)) {
    112       nameOrNumberView.setVisibility(View.VISIBLE);
    113       photo.setVisibility(View.VISIBLE);
    114       String photoUri = cursor.getString(Projections.PHOTO_URI);
    115       ContactPhotoManager.getInstance(context)
    116           .loadDialerThumbnailOrPhoto(
    117               photo,
    118               getContactUri(cursor),
    119               cursor.getLong(Projections.PHOTO_ID),
    120               photoUri == null ? null : Uri.parse(photoUri),
    121               name,
    122               LetterTileDrawable.TYPE_DEFAULT);
    123     } else {
    124       nameOrNumberView.setVisibility(View.GONE);
    125       photo.setVisibility(View.INVISIBLE);
    126     }
    127   }
    128 
    129   // Show the contact photo next to only the first number if a contact has multiple numbers
    130   private boolean shouldShowPhoto(SearchCursor cursor) {
    131     int currentPosition = cursor.getPosition();
    132     String currentLookupKey = cursor.getString(Projections.LOOKUP_KEY);
    133     cursor.moveToPosition(currentPosition - 1);
    134 
    135     if (!cursor.isHeader() && !cursor.isBeforeFirst()) {
    136       String previousLookupKey = cursor.getString(Projections.LOOKUP_KEY);
    137       cursor.moveToPosition(currentPosition);
    138       return !currentLookupKey.equals(previousLookupKey);
    139     }
    140     cursor.moveToPosition(currentPosition);
    141     return true;
    142   }
    143 
    144   private static Uri getContactUri(Cursor cursor) {
    145     long contactId = cursor.getLong(Projections.ID);
    146     String lookupKey = cursor.getString(Projections.LOOKUP_KEY);
    147     return Contacts.getLookupUri(contactId, lookupKey);
    148   }
    149 
    150   // TODO(calderwoodra): handle CNAP and cequint types.
    151   // TODO(calderwoodra): unify this into a utility method with CallLogAdapter#getNumberType
    152   private static String getLabel(Resources resources, Cursor cursor) {
    153     int numberType = cursor.getInt(Projections.PHONE_TYPE);
    154     String numberLabel = cursor.getString(Projections.PHONE_LABEL);
    155 
    156     // Returns empty label instead of "custom" if the custom label is empty.
    157     if (numberType == Phone.TYPE_CUSTOM && TextUtils.isEmpty(numberLabel)) {
    158       return "";
    159     }
    160     return (String) Phone.getTypeLabel(resources, numberType, numberLabel);
    161   }
    162 
    163   private void setCallToAction(SearchCursor cursor, String query) {
    164     currentAction = getCallToAction(context, cursor, query);
    165     switch (currentAction) {
    166       case CallToAction.NONE:
    167         callToActionView.setVisibility(View.GONE);
    168         callToActionView.setOnClickListener(null);
    169         break;
    170       case CallToAction.SHARE_AND_CALL:
    171         callToActionView.setVisibility(View.VISIBLE);
    172         callToActionView.setImageDrawable(
    173             context.getDrawable(com.android.contacts.common.R.drawable.ic_phone_attach));
    174         callToActionView.setContentDescription(
    175             context.getString(R.string.description_search_call_and_share));
    176         callToActionView.setOnClickListener(this);
    177         break;
    178       case CallToAction.DUO_CALL:
    179       case CallToAction.VIDEO_CALL:
    180         callToActionView.setVisibility(View.VISIBLE);
    181         callToActionView.setImageDrawable(
    182             context.getDrawable(R.drawable.quantum_ic_videocam_white_24));
    183         callToActionView.setContentDescription(
    184             context.getString(R.string.description_search_video_call));
    185         callToActionView.setOnClickListener(this);
    186         break;
    187       default:
    188         throw Assert.createIllegalStateFailException(
    189             "Invalid Call to action type: " + currentAction);
    190     }
    191   }
    192 
    193   private static @CallToAction int getCallToAction(
    194       Context context, SearchCursor cursor, String query) {
    195     int carrierPresence = cursor.getInt(Projections.CARRIER_PRESENCE);
    196     String number = cursor.getString(Projections.PHONE_NUMBER);
    197     if ((carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) == 1) {
    198       return CallToAction.VIDEO_CALL;
    199     }
    200 
    201     if (DuoComponent.get(context).getDuo().isReachable(context, number)) {
    202       return CallToAction.DUO_CALL;
    203     }
    204 
    205     EnrichedCallManager manager = EnrichedCallComponent.get(context).getEnrichedCallManager();
    206     EnrichedCallCapabilities capabilities = manager.getCapabilities(number);
    207     if (capabilities != null && capabilities.isCallComposerCapable()) {
    208       return CallToAction.SHARE_AND_CALL;
    209     } else if (shouldRequestCapabilities(cursor, capabilities, query)) {
    210       manager.requestCapabilities(number);
    211     }
    212     return CallToAction.NONE;
    213   }
    214 
    215   /**
    216    * An RPC is initiated for each number we request capabilities for, so to limit the network load
    217    * and latency on slow networks, we only want to request capabilities for potential contacts the
    218    * user is interested in calling. The requirements are that:
    219    *
    220    * <ul>
    221    *   <li>The search query must be 3 or more characters; OR
    222    *   <li>There must be 4 or fewer contacts listed in the cursor.
    223    * </ul>
    224    */
    225   private static boolean shouldRequestCapabilities(
    226       SearchCursor cursor,
    227       @Nullable EnrichedCallCapabilities capabilities,
    228       @Nullable String query) {
    229     if (capabilities != null) {
    230       return false;
    231     }
    232 
    233     if (query != null && query.length() >= 3) {
    234       return true;
    235     }
    236 
    237     // TODO(calderwoodra): implement SearchCursor#getHeaderCount
    238     if (cursor.getCount() <= 5) { // 4 contacts + 1 header row element
    239       return true;
    240     }
    241     return false;
    242   }
    243 
    244   @Override
    245   public void onClick(View view) {
    246     if (view == callToActionView) {
    247       switch (currentAction) {
    248         case CallToAction.SHARE_AND_CALL:
    249           listener.openCallAndShare(dialerContact);
    250           break;
    251         case CallToAction.VIDEO_CALL:
    252           listener.placeVideoCall(number, position);
    253           break;
    254         case CallToAction.DUO_CALL:
    255           listener.placeDuoCall(number);
    256           break;
    257         case CallToAction.NONE:
    258         default:
    259           throw Assert.createIllegalStateFailException(
    260               "Invalid Call to action type: " + currentAction);
    261       }
    262     } else {
    263       listener.placeVoiceCall(number, position);
    264     }
    265   }
    266 
    267   private static DialerContact getDialerContact(Context context, Cursor cursor) {
    268     DialerContact.Builder contact = DialerContact.newBuilder();
    269     String displayName = cursor.getString(Projections.DISPLAY_NAME);
    270     String number = cursor.getString(Projections.PHONE_NUMBER);
    271     Uri contactUri =
    272         Contacts.getLookupUri(
    273             cursor.getLong(Projections.CONTACT_ID), cursor.getString(Projections.LOOKUP_KEY));
    274 
    275     contact
    276         .setNumber(number)
    277         .setPhotoId(cursor.getLong(Projections.PHOTO_ID))
    278         .setContactType(LetterTileDrawable.TYPE_DEFAULT)
    279         .setNameOrNumber(displayName)
    280         .setNumberLabel(
    281             Phone.getTypeLabel(
    282                     context.getResources(),
    283                     cursor.getInt(Projections.PHONE_TYPE),
    284                     cursor.getString(Projections.PHONE_LABEL))
    285                 .toString());
    286 
    287     String photoUri = cursor.getString(Projections.PHOTO_URI);
    288     if (photoUri != null) {
    289       contact.setPhotoUri(photoUri);
    290     }
    291 
    292     if (contactUri != null) {
    293       contact.setContactUri(contactUri.toString());
    294     }
    295 
    296     if (!TextUtils.isEmpty(displayName)) {
    297       contact.setDisplayNumber(number);
    298     }
    299 
    300     return contact.build();
    301   }
    302 }
    303