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