summaryrefslogtreecommitdiffstats
path: root/java/com/android/incallui/ConferenceParticipantListAdapter.java
blob: 712bdefa67f14734e5f463f5c9e0d20b578541e3 (plain)
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
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
/*
 * 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.support.annotation.Nullable;
import android.support.v4.util.ArrayMap;
import android.text.BidiFormatter;
import android.text.TextDirectionHeuristics;
import android.text.TextUtils;
import android.util.ArraySet;
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.contacts.common.compat.PhoneNumberUtilsCompat;
import com.android.contacts.common.preference.ContactsPreferences;
import com.android.contacts.common.util.ContactDisplayUtils;
import com.android.dialer.common.LogUtil;
import com.android.incallui.ContactInfoCache.ContactCacheEntry;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/** Adapter for a ListView containing conference call participant information. */
public class ConferenceParticipantListAdapter extends BaseAdapter {

  /** The ListView containing the participant information. */
  private final ListView mListView;
  /** Hashmap to make accessing participant info by call Id faster. */
  private final Map<String, ParticipantInfo> mParticipantsByCallId = new ArrayMap<>();
  /** ContactsPreferences used to lookup displayName preferences */
  @Nullable private final ContactsPreferences mContactsPreferences;
  /** Contact photo manager to retrieve cached contact photo information. */
  private final ContactPhotoManager mContactPhotoManager;
  /** Listener used to handle tap of the "disconnect' button for a participant. */
  private View.OnClickListener mDisconnectListener =
      new View.OnClickListener() {
        @Override
        public void onClick(View view) {
          DialerCall call = getCallFromView(view);
          LogUtil.i(
              "ConferenceParticipantListAdapter.mDisconnectListener.onClick", "call: " + call);
          if (call != null) {
            call.disconnect();
          }
        }
      };
  /** Listener used to handle tap of the "separate' button for a participant. */
  private View.OnClickListener mSeparateListener =
      new View.OnClickListener() {
        @Override
        public void onClick(View view) {
          DialerCall call = getCallFromView(view);
          LogUtil.i("ConferenceParticipantListAdapter.mSeparateListener.onClick", "call: " + call);
          if (call != null) {
            call.splitFromConference();
          }
        }
      };
  /** The conference participants to show in the ListView. */
  private List<ParticipantInfo> mConferenceParticipants = new ArrayList<>();
  /** {@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 contactPhotoManager The contact photo manager, used to load contact photos.
   */
  public ConferenceParticipantListAdapter(
      ListView listView, ContactPhotoManager contactPhotoManager) {

    mListView = listView;
    mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(getContext());
    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<DialerCall> conferenceParticipants, boolean parentCanSeparate) {
    if (mContactsPreferences != null) {
      mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
      mContactsPreferences.refreshValue(ContactsPreferences.SORT_ORDER_KEY);
    }
    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(DialerCall call) {
    String callId = call.getId();

    if (mParticipantsByCallId.containsKey(callId)) {
      ParticipantInfo participantInfo = mParticipantsByCallId.get(callId);
      participantInfo.setCall(call);
      refreshView(callId);
    }
  }

  private Context getContext() {
    return mListView.getContext();
  }

  /**
   * 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
            ? LayoutInflater.from(parent.getContext())
                .inflate(R.layout.caller_in_conference, parent, false)
            : convertView;

    ParticipantInfo participantInfo = mConferenceParticipants.get(position);
    DialerCall call = participantInfo.getCall();
    ContactCacheEntry contactCache = participantInfo.getContactCacheEntry();

    final ContactInfoCache cache = ContactInfoCache.getInstance(getContext());

    // 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() == DialerCall.State.INCOMING,
          new ContactLookupCallback(this));
    }

    boolean thisRowCanSeparate =
        mParentCanSeparate
            && call.can(android.telecom.Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE);
    boolean thisRowCanDisconnect =
        call.can(android.telecom.Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE);

    String name =
        ContactDisplayUtils.getPreferredDisplayName(
            contactCache.namePrimary, contactCache.nameAlternative, mContactsPreferences);

    setCallerInfoForRow(
        result,
        contactCache.namePrimary,
        call.updateNameIfRestricted(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 void setCallerInfoForRow(
      View view,
      String callerName,
      String preferredName,
      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(preferredName);

    // 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(
          PhoneNumberUtilsCompat.createTtsSpannable(
              BidiFormatter.getInstance().unicodeWrap(callerNumber, TextDirectionHeuristics.LTR)));
      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<DialerCall> conferenceParticipants) {
    final ContactInfoCache cache = ContactInfoCache.getInstance(getContext());
    boolean newParticipantAdded = false;
    Set<String> newCallIds = new ArraySet<>(conferenceParticipants.size());

    // Update or add conference participant info.
    for (DialerCall call : conferenceParticipants) {
      String callId = call.getId();
      newCallIds.add(callId);
      ContactCacheEntry contactCache = cache.getInfo(callId);
      if (contactCache == null) {
        contactCache =
            ContactInfoCache.buildCacheEntryFromCall(
                getContext(), call, call.getState() == DialerCall.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>() {
          @Override
          public int compare(ParticipantInfo p1, ParticipantInfo p2) {
            // Contact names might be null, so replace with empty string.
            ContactCacheEntry c1 = p1.getContactCacheEntry();
            String p1Name =
                ContactDisplayUtils.getPreferredSortName(
                    c1.namePrimary, c1.nameAlternative, mContactsPreferences);
            p1Name = p1Name != null ? p1Name : "";

            ContactCacheEntry c2 = p2.getContactCacheEntry();
            String p2Name =
                ContactDisplayUtils.getPreferredSortName(
                    c2.namePrimary, c2.nameAlternative, mContactsPreferences);
            p2Name = p2Name != null ? p2Name : "";

            return p1Name.compareToIgnoreCase(p2Name);
          }
        });
  }

  private DialerCall getCallFromView(View view) {
    View parent = (View) view.getParent();
    String callId = (String) parent.getTag();
    return CallList.getInstance().getCallById(callId);
  }

  /**
   * 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<>(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);
      }
    }
  }

  /**
   * Internal class which represents a participant. Includes a reference to the {@link DialerCall}
   * and the corresponding {@link ContactCacheEntry} for the participant.
   */
  private static class ParticipantInfo {

    private DialerCall mCall;
    private ContactCacheEntry mContactCacheEntry;
    private boolean mCacheLookupComplete = false;

    public ParticipantInfo(DialerCall call, ContactCacheEntry contactCacheEntry) {
      mCall = call;
      mContactCacheEntry = contactCacheEntry;
    }

    public DialerCall getCall() {
      return mCall;
    }

    public void setCall(DialerCall 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();
    }
  }
}