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