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