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