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