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