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