Home | History | Annotate | Download | only in incallui
      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