1 /* 2 * Copyright (C) 2014 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.incallui; 18 19 import android.content.Context; 20 import android.net.Uri; 21 import android.support.annotation.Nullable; 22 import android.support.v4.util.ArrayMap; 23 import android.telephony.PhoneNumberUtils; 24 import android.text.BidiFormatter; 25 import android.text.TextDirectionHeuristics; 26 import android.text.TextUtils; 27 import android.util.ArraySet; 28 import android.util.TypedValue; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.widget.BaseAdapter; 33 import android.widget.ImageView; 34 import android.widget.ListView; 35 import android.widget.TextView; 36 import com.android.contacts.common.preference.ContactsPreferences; 37 import com.android.contacts.common.util.ContactDisplayUtils; 38 import com.android.dialer.common.LogUtil; 39 import com.android.dialer.contactphoto.ContactPhotoManager; 40 import com.android.dialer.contactphoto.ContactPhotoManager.DefaultImageRequest; 41 import com.android.incallui.ContactInfoCache.ContactCacheEntry; 42 import com.android.incallui.call.CallList; 43 import com.android.incallui.call.DialerCall; 44 import java.lang.ref.WeakReference; 45 import java.util.ArrayList; 46 import java.util.Collections; 47 import java.util.Comparator; 48 import java.util.Iterator; 49 import java.util.List; 50 import java.util.Map; 51 import java.util.Objects; 52 import java.util.Set; 53 54 /** Adapter for a ListView containing conference call participant information. */ 55 public class ConferenceParticipantListAdapter extends BaseAdapter { 56 57 /** The ListView containing the participant information. */ 58 private final ListView listView; 59 /** Hashmap to make accessing participant info by call Id faster. */ 60 private final Map<String, ParticipantInfo> participantsByCallId = new ArrayMap<>(); 61 /** ContactsPreferences used to lookup displayName preferences */ 62 @Nullable private final ContactsPreferences contactsPreferences; 63 /** Contact photo manager to retrieve cached contact photo information. */ 64 private final ContactPhotoManager contactPhotoManager; 65 /** Listener used to handle tap of the "disconnect' button for a participant. */ 66 private View.OnClickListener disconnectListener = 67 new View.OnClickListener() { 68 @Override 69 public void onClick(View view) { 70 DialerCall call = getCallFromView(view); 71 LogUtil.i( 72 "ConferenceParticipantListAdapter.mDisconnectListener.onClick", "call: " + call); 73 if (call != null) { 74 call.disconnect(); 75 } 76 } 77 }; 78 /** Listener used to handle tap of the "separate' button for a participant. */ 79 private View.OnClickListener separateListener = 80 new View.OnClickListener() { 81 @Override 82 public void onClick(View view) { 83 DialerCall call = getCallFromView(view); 84 LogUtil.i("ConferenceParticipantListAdapter.mSeparateListener.onClick", "call: " + call); 85 if (call != null) { 86 call.splitFromConference(); 87 } 88 } 89 }; 90 /** The conference participants to show in the ListView. */ 91 private List<ParticipantInfo> conferenceParticipants = new ArrayList<>(); 92 /** {@code True} if the conference parent supports separating calls from the conference. */ 93 private boolean parentCanSeparate; 94 95 /** 96 * Creates an instance of the ConferenceParticipantListAdapter. 97 * 98 * @param listView The listview. 99 * @param contactPhotoManager The contact photo manager, used to load contact photos. 100 */ 101 public ConferenceParticipantListAdapter( 102 ListView listView, ContactPhotoManager contactPhotoManager) { 103 104 this.listView = listView; 105 contactsPreferences = ContactsPreferencesFactory.newContactsPreferences(getContext()); 106 this.contactPhotoManager = contactPhotoManager; 107 } 108 109 /** 110 * Updates the adapter with the new conference participant information provided. 111 * 112 * @param conferenceParticipants The list of conference participants. 113 * @param parentCanSeparate {@code True} if the parent supports separating calls from the 114 * conference. 115 */ 116 public void updateParticipants( 117 List<DialerCall> conferenceParticipants, boolean parentCanSeparate) { 118 if (contactsPreferences != null) { 119 contactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); 120 contactsPreferences.refreshValue(ContactsPreferences.SORT_ORDER_KEY); 121 } 122 this.parentCanSeparate = parentCanSeparate; 123 updateParticipantInfo(conferenceParticipants); 124 } 125 126 /** 127 * Determines the number of participants in the conference. 128 * 129 * @return The number of participants. 130 */ 131 @Override 132 public int getCount() { 133 return conferenceParticipants.size(); 134 } 135 136 /** 137 * Retrieves an item from the list of participants. 138 * 139 * @param position Position of the item whose data we want within the adapter's data set. 140 * @return The {@link ParticipantInfo}. 141 */ 142 @Override 143 public Object getItem(int position) { 144 return conferenceParticipants.get(position); 145 } 146 147 /** 148 * Retreives the adapter-specific item id for an item at a specified position. 149 * 150 * @param position The position of the item within the adapter's data set whose row id we want. 151 * @return The item id. 152 */ 153 @Override 154 public long getItemId(int position) { 155 return position; 156 } 157 158 /** 159 * Refreshes call information for the call passed in. 160 * 161 * @param call The new call information. 162 */ 163 public void refreshCall(DialerCall call) { 164 String callId = call.getId(); 165 166 if (participantsByCallId.containsKey(callId)) { 167 ParticipantInfo participantInfo = participantsByCallId.get(callId); 168 participantInfo.setCall(call); 169 refreshView(callId); 170 } 171 } 172 173 private Context getContext() { 174 return listView.getContext(); 175 } 176 177 /** 178 * Attempts to refresh the view for the specified call ID. This ensures the contact info and photo 179 * loaded from cache are updated. 180 * 181 * @param callId The call id. 182 */ 183 private void refreshView(String callId) { 184 int first = listView.getFirstVisiblePosition(); 185 int last = listView.getLastVisiblePosition(); 186 187 for (int position = 0; position <= last - first; position++) { 188 View view = listView.getChildAt(position); 189 String rowCallId = (String) view.getTag(); 190 if (rowCallId.equals(callId)) { 191 getView(position + first, view, listView); 192 break; 193 } 194 } 195 } 196 197 /** 198 * Creates or populates an existing conference participant row. 199 * 200 * @param position The position of the item within the adapter's data set of the item whose view 201 * we want. 202 * @param convertView The old view to reuse, if possible. 203 * @param parent The parent that this view will eventually be attached to 204 * @return The populated view. 205 */ 206 @Override 207 public View getView(int position, View convertView, ViewGroup parent) { 208 // Make sure we have a valid convertView to start with 209 final View result = 210 convertView == null 211 ? LayoutInflater.from(parent.getContext()) 212 .inflate(R.layout.caller_in_conference, parent, false) 213 : convertView; 214 215 ParticipantInfo participantInfo = conferenceParticipants.get(position); 216 DialerCall call = participantInfo.getCall(); 217 ContactCacheEntry contactCache = participantInfo.getContactCacheEntry(); 218 219 final ContactInfoCache cache = ContactInfoCache.getInstance(getContext()); 220 221 // If a cache lookup has not yet been performed to retrieve the contact information and 222 // photo, do it now. 223 if (!participantInfo.isCacheLookupComplete()) { 224 cache.findInfo( 225 participantInfo.getCall(), 226 participantInfo.getCall().getState() == DialerCall.State.INCOMING, 227 new ContactLookupCallback(this)); 228 } 229 230 boolean thisRowCanSeparate = 231 parentCanSeparate 232 && call.can(android.telecom.Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE); 233 boolean thisRowCanDisconnect = 234 call.can(android.telecom.Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE); 235 236 String name = 237 ContactDisplayUtils.getPreferredDisplayName( 238 contactCache.namePrimary, contactCache.nameAlternative, contactsPreferences); 239 240 setCallerInfoForRow( 241 result, 242 contactCache.namePrimary, 243 call.updateNameIfRestricted(name), 244 contactCache.number, 245 contactCache.lookupKey, 246 contactCache.displayPhotoUri, 247 thisRowCanSeparate, 248 thisRowCanDisconnect, 249 call.getNonConferenceState()); 250 251 // Tag the row in the conference participant list with the call id to make it easier to 252 // find calls when contact cache information is loaded. 253 result.setTag(call.getId()); 254 255 return result; 256 } 257 258 /** 259 * Replaces the contact info for a participant and triggers a refresh of the UI. 260 * 261 * @param callId The call id. 262 * @param entry The new contact info. 263 */ 264 /* package */ void updateContactInfo(String callId, ContactCacheEntry entry) { 265 if (participantsByCallId.containsKey(callId)) { 266 ParticipantInfo participantInfo = participantsByCallId.get(callId); 267 participantInfo.setContactCacheEntry(entry); 268 participantInfo.setCacheLookupComplete(true); 269 refreshView(callId); 270 } 271 } 272 273 /** 274 * Sets the caller information for a row in the conference participant list. 275 * 276 * @param view The view to set the details on. 277 * @param callerName The participant's name. 278 * @param callerNumber The participant's phone number. 279 * @param lookupKey The lookup key for the participant (for photo lookup). 280 * @param photoUri The URI of the contact photo. 281 * @param thisRowCanSeparate {@code True} if this participant can separate from the conference. 282 * @param thisRowCanDisconnect {@code True} if this participant can be disconnected. 283 */ 284 private void setCallerInfoForRow( 285 View view, 286 String callerName, 287 String preferredName, 288 String callerNumber, 289 String lookupKey, 290 Uri photoUri, 291 boolean thisRowCanSeparate, 292 boolean thisRowCanDisconnect, 293 int callState) { 294 295 final ImageView photoView = (ImageView) view.findViewById(R.id.callerPhoto); 296 final TextView statusTextView = (TextView) view.findViewById(R.id.conferenceCallerStatus); 297 final TextView nameTextView = (TextView) view.findViewById(R.id.conferenceCallerName); 298 final TextView numberTextView = (TextView) view.findViewById(R.id.conferenceCallerNumber); 299 final View endButton = view.findViewById(R.id.conferenceCallerDisconnect); 300 final View separateButton = view.findViewById(R.id.conferenceCallerSeparate); 301 302 if (callState == DialerCall.State.ONHOLD) { 303 setViewsOnHold(photoView, statusTextView, nameTextView, numberTextView); 304 } else { 305 setViewsNotOnHold(photoView, statusTextView, nameTextView, numberTextView); 306 } 307 308 endButton.setVisibility(thisRowCanDisconnect ? View.VISIBLE : View.GONE); 309 if (thisRowCanDisconnect) { 310 endButton.setOnClickListener(disconnectListener); 311 } else { 312 endButton.setOnClickListener(null); 313 } 314 315 separateButton.setVisibility(thisRowCanSeparate ? View.VISIBLE : View.GONE); 316 if (thisRowCanSeparate) { 317 separateButton.setOnClickListener(separateListener); 318 } else { 319 separateButton.setOnClickListener(null); 320 } 321 322 String displayNameForImage = TextUtils.isEmpty(callerName) ? callerNumber : callerName; 323 DefaultImageRequest imageRequest = 324 (photoUri != null) 325 ? null 326 : new DefaultImageRequest(displayNameForImage, lookupKey, true /* isCircularPhoto */); 327 328 contactPhotoManager.loadDirectoryPhoto(photoView, photoUri, false, true, imageRequest); 329 330 // set the caller name 331 if (TextUtils.isEmpty(preferredName)) { 332 nameTextView.setVisibility(View.GONE); 333 } else { 334 nameTextView.setVisibility(View.VISIBLE); 335 nameTextView.setText(preferredName); 336 } 337 338 // set the caller number in subscript, or make the field disappear. 339 if (TextUtils.isEmpty(callerNumber)) { 340 numberTextView.setVisibility(View.GONE); 341 } else { 342 numberTextView.setVisibility(View.VISIBLE); 343 numberTextView.setText( 344 PhoneNumberUtils.createTtsSpannable( 345 BidiFormatter.getInstance().unicodeWrap(callerNumber, TextDirectionHeuristics.LTR))); 346 } 347 } 348 349 private void setViewsOnHold( 350 ImageView photoView, 351 TextView statusTextView, 352 TextView nameTextView, 353 TextView numberTextView) { 354 CharSequence onHoldText = 355 TextUtils.concat(getContext().getText(R.string.notification_on_hold).toString(), " "); 356 statusTextView.setText(onHoldText); 357 statusTextView.setVisibility(View.VISIBLE); 358 359 int onHoldColor = getContext().getColor(R.color.dialer_secondary_text_color_hiden); 360 nameTextView.setTextColor(onHoldColor); 361 numberTextView.setTextColor(onHoldColor); 362 363 TypedValue alpha = new TypedValue(); 364 getContext().getResources().getValue(R.dimen.alpha_hiden, alpha, true); 365 photoView.setAlpha(alpha.getFloat()); 366 } 367 368 private void setViewsNotOnHold( 369 ImageView photoView, 370 TextView statusTextView, 371 TextView nameTextView, 372 TextView numberTextView) { 373 statusTextView.setVisibility(View.GONE); 374 375 nameTextView.setTextColor( 376 getContext().getColor(R.color.conference_call_manager_caller_name_text_color)); 377 numberTextView.setTextColor( 378 getContext().getColor(R.color.conference_call_manager_secondary_text_color)); 379 380 TypedValue alpha = new TypedValue(); 381 getContext().getResources().getValue(R.dimen.alpha_enabled, alpha, true); 382 photoView.setAlpha(alpha.getFloat()); 383 } 384 385 /** 386 * Updates the participant info list which is bound to the ListView. Stores the call and contact 387 * info for all entries. The list is sorted alphabetically by participant name. 388 * 389 * @param conferenceParticipants The calls which make up the conference participants. 390 */ 391 private void updateParticipantInfo(List<DialerCall> conferenceParticipants) { 392 final ContactInfoCache cache = ContactInfoCache.getInstance(getContext()); 393 boolean newParticipantAdded = false; 394 Set<String> newCallIds = new ArraySet<>(conferenceParticipants.size()); 395 396 // Update or add conference participant info. 397 for (DialerCall call : conferenceParticipants) { 398 String callId = call.getId(); 399 newCallIds.add(callId); 400 ContactCacheEntry contactCache = cache.getInfo(callId); 401 if (contactCache == null) { 402 contactCache = 403 ContactInfoCache.buildCacheEntryFromCall( 404 getContext(), call, call.getState() == DialerCall.State.INCOMING); 405 } 406 407 if (participantsByCallId.containsKey(callId)) { 408 ParticipantInfo participantInfo = participantsByCallId.get(callId); 409 participantInfo.setCall(call); 410 participantInfo.setContactCacheEntry(contactCache); 411 } else { 412 newParticipantAdded = true; 413 ParticipantInfo participantInfo = new ParticipantInfo(call, contactCache); 414 this.conferenceParticipants.add(participantInfo); 415 participantsByCallId.put(call.getId(), participantInfo); 416 } 417 } 418 419 // Remove any participants that no longer exist. 420 Iterator<Map.Entry<String, ParticipantInfo>> it = participantsByCallId.entrySet().iterator(); 421 while (it.hasNext()) { 422 Map.Entry<String, ParticipantInfo> entry = it.next(); 423 String existingCallId = entry.getKey(); 424 if (!newCallIds.contains(existingCallId)) { 425 ParticipantInfo existingInfo = entry.getValue(); 426 this.conferenceParticipants.remove(existingInfo); 427 it.remove(); 428 } 429 } 430 431 if (newParticipantAdded) { 432 // Sort the list of participants by contact name. 433 sortParticipantList(); 434 } 435 notifyDataSetChanged(); 436 } 437 438 /** Sorts the participant list by contact name. */ 439 private void sortParticipantList() { 440 Collections.sort( 441 conferenceParticipants, 442 new Comparator<ParticipantInfo>() { 443 @Override 444 public int compare(ParticipantInfo p1, ParticipantInfo p2) { 445 // Contact names might be null, so replace with empty string. 446 ContactCacheEntry c1 = p1.getContactCacheEntry(); 447 String p1Name = 448 ContactDisplayUtils.getPreferredSortName( 449 c1.namePrimary, c1.nameAlternative, contactsPreferences); 450 p1Name = p1Name != null ? p1Name : ""; 451 452 ContactCacheEntry c2 = p2.getContactCacheEntry(); 453 String p2Name = 454 ContactDisplayUtils.getPreferredSortName( 455 c2.namePrimary, c2.nameAlternative, contactsPreferences); 456 p2Name = p2Name != null ? p2Name : ""; 457 458 return p1Name.compareToIgnoreCase(p2Name); 459 } 460 }); 461 } 462 463 private DialerCall getCallFromView(View view) { 464 View parent = (View) view.getParent(); 465 String callId = (String) parent.getTag(); 466 return CallList.getInstance().getCallById(callId); 467 } 468 469 /** 470 * Callback class used when making requests to the {@link ContactInfoCache} to resolve contact 471 * info and contact photos for conference participants. 472 */ 473 public static class ContactLookupCallback implements ContactInfoCache.ContactInfoCacheCallback { 474 475 private final WeakReference<ConferenceParticipantListAdapter> listAdapter; 476 477 public ContactLookupCallback(ConferenceParticipantListAdapter listAdapter) { 478 this.listAdapter = new WeakReference<>(listAdapter); 479 } 480 481 /** 482 * Called when contact info has been resolved. 483 * 484 * @param callId The call id. 485 * @param entry The new contact information. 486 */ 487 @Override 488 public void onContactInfoComplete(String callId, ContactCacheEntry entry) { 489 update(callId, entry); 490 } 491 492 /** 493 * Called when contact photo has been loaded into the cache. 494 * 495 * @param callId The call id. 496 * @param entry The new contact information. 497 */ 498 @Override 499 public void onImageLoadComplete(String callId, ContactCacheEntry entry) { 500 update(callId, entry); 501 } 502 503 /** 504 * Updates the contact information for a participant. 505 * 506 * @param callId The call id. 507 * @param entry The new contact information. 508 */ 509 private void update(String callId, ContactCacheEntry entry) { 510 ConferenceParticipantListAdapter listAdapter = this.listAdapter.get(); 511 if (listAdapter != null) { 512 listAdapter.updateContactInfo(callId, entry); 513 } 514 } 515 } 516 517 /** 518 * Internal class which represents a participant. Includes a reference to the {@link DialerCall} 519 * and the corresponding {@link ContactCacheEntry} for the participant. 520 */ 521 private static class ParticipantInfo { 522 523 private DialerCall call; 524 private ContactCacheEntry contactCacheEntry; 525 private boolean cacheLookupComplete = false; 526 527 public ParticipantInfo(DialerCall call, ContactCacheEntry contactCacheEntry) { 528 this.call = call; 529 this.contactCacheEntry = contactCacheEntry; 530 } 531 532 public DialerCall getCall() { 533 return call; 534 } 535 536 public void setCall(DialerCall call) { 537 this.call = call; 538 } 539 540 public ContactCacheEntry getContactCacheEntry() { 541 return contactCacheEntry; 542 } 543 544 public void setContactCacheEntry(ContactCacheEntry entry) { 545 contactCacheEntry = entry; 546 } 547 548 public boolean isCacheLookupComplete() { 549 return cacheLookupComplete; 550 } 551 552 public void setCacheLookupComplete(boolean cacheLookupComplete) { 553 this.cacheLookupComplete = cacheLookupComplete; 554 } 555 556 @Override 557 public boolean equals(Object o) { 558 if (o instanceof ParticipantInfo) { 559 ParticipantInfo p = (ParticipantInfo) o; 560 return Objects.equals(p.getCall().getId(), call.getId()); 561 } 562 return false; 563 } 564 565 @Override 566 public int hashCode() { 567 return call.getId().hashCode(); 568 } 569 } 570 } 571