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 com.android.dialer.common.Assert;
34 import com.android.dialer.contactphoto.ContactPhotoManager;
35 import com.android.dialer.dialercontact.DialerContact;
36 import com.android.dialer.duo.DuoComponent;
37 import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
38 import com.android.dialer.enrichedcall.EnrichedCallComponent;
39 import com.android.dialer.enrichedcall.EnrichedCallManager;
40 import com.android.dialer.lettertile.LetterTileDrawable;
41 import com.android.dialer.searchfragment.common.Projections;
42 import com.android.dialer.searchfragment.common.QueryBoldingUtil;
43 import com.android.dialer.searchfragment.common.R;
44 import com.android.dialer.searchfragment.common.RowClickListener;
45 import com.android.dialer.searchfragment.common.SearchCursor;
46 import com.android.dialer.widget.BidiTextView;
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 BidiTextView nameOrNumberView;
71   private final BidiTextView 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 
SearchContactViewHolder(View view, RowClickListener listener)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    */
bind(SearchCursor cursor, String query)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.dialer.contacts.resources.R.string.call_subject_type_and_number,
106                 label,
107                 number);
108 
109     nameOrNumberView.setText(QueryBoldingUtil.getNameWithQueryBolded(query, name, context));
110     numberView.setText(QueryBoldingUtil.getNumberWithQueryBolded(query, secondaryInfo));
111     setCallToAction(cursor, query);
112 
113     if (shouldShowPhoto(cursor)) {
114       nameOrNumberView.setVisibility(View.VISIBLE);
115       photo.setVisibility(View.VISIBLE);
116       String photoUri = cursor.getString(Projections.PHOTO_URI);
117       ContactPhotoManager.getInstance(context)
118           .loadDialerThumbnailOrPhoto(
119               photo,
120               getContactUri(cursor),
121               cursor.getLong(Projections.PHOTO_ID),
122               photoUri == null ? null : Uri.parse(photoUri),
123               name,
124               LetterTileDrawable.TYPE_DEFAULT);
125     } else {
126       nameOrNumberView.setVisibility(View.GONE);
127       photo.setVisibility(View.INVISIBLE);
128     }
129   }
130 
131   // Show the contact photo next to only the first number if a contact has multiple numbers
shouldShowPhoto(SearchCursor cursor)132   private boolean shouldShowPhoto(SearchCursor cursor) {
133     int currentPosition = cursor.getPosition();
134     String currentLookupKey = cursor.getString(Projections.LOOKUP_KEY);
135     cursor.moveToPosition(currentPosition - 1);
136 
137     if (!cursor.isHeader() && !cursor.isBeforeFirst()) {
138       String previousLookupKey = cursor.getString(Projections.LOOKUP_KEY);
139       cursor.moveToPosition(currentPosition);
140       return !currentLookupKey.equals(previousLookupKey);
141     }
142     cursor.moveToPosition(currentPosition);
143     return true;
144   }
145 
getContactUri(Cursor cursor)146   private static Uri getContactUri(Cursor cursor) {
147     long contactId = cursor.getLong(Projections.ID);
148     String lookupKey = cursor.getString(Projections.LOOKUP_KEY);
149     return Contacts.getLookupUri(contactId, lookupKey);
150   }
151 
152   // TODO(calderwoodra): handle CNAP and cequint types.
153   // TODO(calderwoodra): unify this into a utility method with CallLogAdapter#getNumberType
getLabel(Resources resources, Cursor cursor)154   private static String getLabel(Resources resources, Cursor cursor) {
155     int numberType = cursor.getInt(Projections.PHONE_TYPE);
156     String numberLabel = cursor.getString(Projections.PHONE_LABEL);
157 
158     // Returns empty label instead of "custom" if the custom label is empty.
159     if (numberType == Phone.TYPE_CUSTOM && TextUtils.isEmpty(numberLabel)) {
160       return "";
161     }
162     return (String) Phone.getTypeLabel(resources, numberType, numberLabel);
163   }
164 
setCallToAction(SearchCursor cursor, String query)165   private void setCallToAction(SearchCursor cursor, String query) {
166     currentAction = getCallToAction(context, cursor, query);
167     switch (currentAction) {
168       case CallToAction.NONE:
169         callToActionView.setVisibility(View.GONE);
170         callToActionView.setOnClickListener(null);
171         break;
172       case CallToAction.SHARE_AND_CALL:
173         callToActionView.setVisibility(View.VISIBLE);
174         callToActionView.setImageDrawable(
175             context.getDrawable(com.android.dialer.contacts.resources.R.drawable.ic_phone_attach));
176         callToActionView.setContentDescription(
177             context.getString(R.string.description_search_call_and_share));
178         callToActionView.setOnClickListener(this);
179         break;
180       case CallToAction.DUO_CALL:
181       case CallToAction.VIDEO_CALL:
182         callToActionView.setVisibility(View.VISIBLE);
183         callToActionView.setImageDrawable(
184             context.getDrawable(R.drawable.quantum_ic_videocam_vd_white_24));
185         callToActionView.setContentDescription(
186             context.getString(R.string.description_search_video_call));
187         callToActionView.setOnClickListener(this);
188         break;
189       default:
190         throw Assert.createIllegalStateFailException(
191             "Invalid Call to action type: " + currentAction);
192     }
193   }
194 
getCallToAction( Context context, SearchCursor cursor, String query)195   private static @CallToAction int getCallToAction(
196       Context context, SearchCursor cursor, String query) {
197     int carrierPresence = cursor.getInt(Projections.CARRIER_PRESENCE);
198     String number = cursor.getString(Projections.PHONE_NUMBER);
199     if ((carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) == 1) {
200       return CallToAction.VIDEO_CALL;
201     }
202 
203     if (DuoComponent.get(context).getDuo().isReachable(context, number)) {
204       return CallToAction.DUO_CALL;
205     }
206 
207     EnrichedCallManager manager = EnrichedCallComponent.get(context).getEnrichedCallManager();
208     EnrichedCallCapabilities capabilities = manager.getCapabilities(number);
209     if (capabilities != null && capabilities.isCallComposerCapable()) {
210       return CallToAction.SHARE_AND_CALL;
211     } else if (shouldRequestCapabilities(cursor, capabilities, query)) {
212       manager.requestCapabilities(number);
213     }
214     return CallToAction.NONE;
215   }
216 
217   /**
218    * An RPC is initiated for each number we request capabilities for, so to limit the network load
219    * and latency on slow networks, we only want to request capabilities for potential contacts the
220    * user is interested in calling. The requirements are that:
221    *
222    * <ul>
223    *   <li>The search query must be 3 or more characters; OR
224    *   <li>There must be 4 or fewer contacts listed in the cursor.
225    * </ul>
226    */
shouldRequestCapabilities( SearchCursor cursor, @Nullable EnrichedCallCapabilities capabilities, @Nullable String query)227   private static boolean shouldRequestCapabilities(
228       SearchCursor cursor,
229       @Nullable EnrichedCallCapabilities capabilities,
230       @Nullable String query) {
231     if (capabilities != null) {
232       return false;
233     }
234 
235     if (query != null && query.length() >= 3) {
236       return true;
237     }
238 
239     // TODO(calderwoodra): implement SearchCursor#getHeaderCount
240     if (cursor.getCount() <= 5) { // 4 contacts + 1 header row element
241       return true;
242     }
243     return false;
244   }
245 
246   @Override
onClick(View view)247   public void onClick(View view) {
248     if (view == callToActionView) {
249       switch (currentAction) {
250         case CallToAction.SHARE_AND_CALL:
251           listener.openCallAndShare(dialerContact);
252           break;
253         case CallToAction.VIDEO_CALL:
254           listener.placeVideoCall(number, position);
255           break;
256         case CallToAction.DUO_CALL:
257           listener.placeDuoCall(number);
258           break;
259         case CallToAction.NONE:
260         default:
261           throw Assert.createIllegalStateFailException(
262               "Invalid Call to action type: " + currentAction);
263       }
264     } else {
265       listener.placeVoiceCall(number, position);
266     }
267   }
268 
getDialerContact(Context context, Cursor cursor)269   private static DialerContact getDialerContact(Context context, Cursor cursor) {
270     DialerContact.Builder contact = DialerContact.newBuilder();
271     String displayName = cursor.getString(Projections.DISPLAY_NAME);
272     String number = cursor.getString(Projections.PHONE_NUMBER);
273     Uri contactUri =
274         Contacts.getLookupUri(
275             cursor.getLong(Projections.CONTACT_ID), cursor.getString(Projections.LOOKUP_KEY));
276 
277     contact
278         .setNumber(number)
279         .setPhotoId(cursor.getLong(Projections.PHOTO_ID))
280         .setContactType(LetterTileDrawable.TYPE_DEFAULT)
281         .setNameOrNumber(displayName)
282         .setNumberLabel(
283             Phone.getTypeLabel(
284                     context.getResources(),
285                     cursor.getInt(Projections.PHONE_TYPE),
286                     cursor.getString(Projections.PHONE_LABEL))
287                 .toString());
288 
289     String photoUri = cursor.getString(Projections.PHOTO_URI);
290     if (photoUri != null) {
291       contact.setPhotoUri(photoUri);
292     }
293 
294     if (contactUri != null) {
295       contact.setContactUri(contactUri.toString());
296     }
297 
298     if (!TextUtils.isEmpty(displayName)) {
299       contact.setDisplayNumber(number);
300     }
301 
302     return contact.build();
303   }
304 }
305