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