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