/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.incallui; import android.content.Context; import android.net.Uri; import android.support.annotation.Nullable; import android.support.v4.util.ArrayMap; import android.text.BidiFormatter; import android.text.TextDirectionHeuristics; import android.text.TextUtils; import android.util.ArraySet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import com.android.contacts.common.ContactPhotoManager; import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; import com.android.contacts.common.compat.PhoneNumberUtilsCompat; import com.android.contacts.common.preference.ContactsPreferences; import com.android.contacts.common.util.ContactDisplayUtils; import com.android.dialer.common.LogUtil; import com.android.incallui.ContactInfoCache.ContactCacheEntry; import com.android.incallui.call.CallList; import com.android.incallui.call.DialerCall; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; /** Adapter for a ListView containing conference call participant information. */ public class ConferenceParticipantListAdapter extends BaseAdapter { /** The ListView containing the participant information. */ private final ListView mListView; /** Hashmap to make accessing participant info by call Id faster. */ private final Map mParticipantsByCallId = new ArrayMap<>(); /** ContactsPreferences used to lookup displayName preferences */ @Nullable private final ContactsPreferences mContactsPreferences; /** Contact photo manager to retrieve cached contact photo information. */ private final ContactPhotoManager mContactPhotoManager; /** Listener used to handle tap of the "disconnect' button for a participant. */ private View.OnClickListener mDisconnectListener = new View.OnClickListener() { @Override public void onClick(View view) { DialerCall call = getCallFromView(view); LogUtil.i( "ConferenceParticipantListAdapter.mDisconnectListener.onClick", "call: " + call); if (call != null) { call.disconnect(); } } }; /** Listener used to handle tap of the "separate' button for a participant. */ private View.OnClickListener mSeparateListener = new View.OnClickListener() { @Override public void onClick(View view) { DialerCall call = getCallFromView(view); LogUtil.i("ConferenceParticipantListAdapter.mSeparateListener.onClick", "call: " + call); if (call != null) { call.splitFromConference(); } } }; /** The conference participants to show in the ListView. */ private List mConferenceParticipants = new ArrayList<>(); /** {@code True} if the conference parent supports separating calls from the conference. */ private boolean mParentCanSeparate; /** * Creates an instance of the ConferenceParticipantListAdapter. * * @param listView The listview. * @param contactPhotoManager The contact photo manager, used to load contact photos. */ public ConferenceParticipantListAdapter( ListView listView, ContactPhotoManager contactPhotoManager) { mListView = listView; mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(getContext()); mContactPhotoManager = contactPhotoManager; } /** * Updates the adapter with the new conference participant information provided. * * @param conferenceParticipants The list of conference participants. * @param parentCanSeparate {@code True} if the parent supports separating calls from the * conference. */ public void updateParticipants( List conferenceParticipants, boolean parentCanSeparate) { if (mContactsPreferences != null) { mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); mContactsPreferences.refreshValue(ContactsPreferences.SORT_ORDER_KEY); } mParentCanSeparate = parentCanSeparate; updateParticipantInfo(conferenceParticipants); } /** * Determines the number of participants in the conference. * * @return The number of participants. */ @Override public int getCount() { return mConferenceParticipants.size(); } /** * Retrieves an item from the list of participants. * * @param position Position of the item whose data we want within the adapter's data set. * @return The {@link ParticipantInfo}. */ @Override public Object getItem(int position) { return mConferenceParticipants.get(position); } /** * Retreives the adapter-specific item id for an item at a specified position. * * @param position The position of the item within the adapter's data set whose row id we want. * @return The item id. */ @Override public long getItemId(int position) { return position; } /** * Refreshes call information for the call passed in. * * @param call The new call information. */ public void refreshCall(DialerCall call) { String callId = call.getId(); if (mParticipantsByCallId.containsKey(callId)) { ParticipantInfo participantInfo = mParticipantsByCallId.get(callId); participantInfo.setCall(call); refreshView(callId); } } private Context getContext() { return mListView.getContext(); } /** * Attempts to refresh the view for the specified call ID. This ensures the contact info and photo * loaded from cache are updated. * * @param callId The call id. */ private void refreshView(String callId) { int first = mListView.getFirstVisiblePosition(); int last = mListView.getLastVisiblePosition(); for (int position = 0; position <= last - first; position++) { View view = mListView.getChildAt(position); String rowCallId = (String) view.getTag(); if (rowCallId.equals(callId)) { getView(position + first, view, mListView); break; } } } /** * Creates or populates an existing conference participant row. * * @param position The position of the item within the adapter's data set of the item whose view * we want. * @param convertView The old view to reuse, if possible. * @param parent The parent that this view will eventually be attached to * @return The populated view. */ @Override public View getView(int position, View convertView, ViewGroup parent) { // Make sure we have a valid convertView to start with final View result = convertView == null ? LayoutInflater.from(parent.getContext()) .inflate(R.layout.caller_in_conference, parent, false) : convertView; ParticipantInfo participantInfo = mConferenceParticipants.get(position); DialerCall call = participantInfo.getCall(); ContactCacheEntry contactCache = participantInfo.getContactCacheEntry(); final ContactInfoCache cache = ContactInfoCache.getInstance(getContext()); // If a cache lookup has not yet been performed to retrieve the contact information and // photo, do it now. if (!participantInfo.isCacheLookupComplete()) { cache.findInfo( participantInfo.getCall(), participantInfo.getCall().getState() == DialerCall.State.INCOMING, new ContactLookupCallback(this)); } boolean thisRowCanSeparate = mParentCanSeparate && call.can(android.telecom.Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE); boolean thisRowCanDisconnect = call.can(android.telecom.Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE); String name = ContactDisplayUtils.getPreferredDisplayName( contactCache.namePrimary, contactCache.nameAlternative, mContactsPreferences); setCallerInfoForRow( result, contactCache.namePrimary, call.updateNameIfRestricted(name), contactCache.number, contactCache.label, contactCache.lookupKey, contactCache.displayPhotoUri, thisRowCanSeparate, thisRowCanDisconnect); // Tag the row in the conference participant list with the call id to make it easier to // find calls when contact cache information is loaded. result.setTag(call.getId()); return result; } /** * Replaces the contact info for a participant and triggers a refresh of the UI. * * @param callId The call id. * @param entry The new contact info. */ /* package */ void updateContactInfo(String callId, ContactCacheEntry entry) { if (mParticipantsByCallId.containsKey(callId)) { ParticipantInfo participantInfo = mParticipantsByCallId.get(callId); participantInfo.setContactCacheEntry(entry); participantInfo.setCacheLookupComplete(true); refreshView(callId); } } /** * Sets the caller information for a row in the conference participant list. * * @param view The view to set the details on. * @param callerName The participant's name. * @param callerNumber The participant's phone number. * @param callerNumberType The participant's phone number typ.e * @param lookupKey The lookup key for the participant (for photo lookup). * @param photoUri The URI of the contact photo. * @param thisRowCanSeparate {@code True} if this participant can separate from the conference. * @param thisRowCanDisconnect {@code True} if this participant can be disconnected. */ private void setCallerInfoForRow( View view, String callerName, String preferredName, String callerNumber, String callerNumberType, String lookupKey, Uri photoUri, boolean thisRowCanSeparate, boolean thisRowCanDisconnect) { final ImageView photoView = (ImageView) view.findViewById(R.id.callerPhoto); final TextView nameTextView = (TextView) view.findViewById(R.id.conferenceCallerName); final TextView numberTextView = (TextView) view.findViewById(R.id.conferenceCallerNumber); final TextView numberTypeTextView = (TextView) view.findViewById(R.id.conferenceCallerNumberType); final View endButton = view.findViewById(R.id.conferenceCallerDisconnect); final View separateButton = view.findViewById(R.id.conferenceCallerSeparate); endButton.setVisibility(thisRowCanDisconnect ? View.VISIBLE : View.GONE); if (thisRowCanDisconnect) { endButton.setOnClickListener(mDisconnectListener); } else { endButton.setOnClickListener(null); } separateButton.setVisibility(thisRowCanSeparate ? View.VISIBLE : View.GONE); if (thisRowCanSeparate) { separateButton.setOnClickListener(mSeparateListener); } else { separateButton.setOnClickListener(null); } DefaultImageRequest imageRequest = (photoUri != null) ? null : new DefaultImageRequest(callerName, lookupKey, true /* isCircularPhoto */); mContactPhotoManager.loadDirectoryPhoto(photoView, photoUri, false, true, imageRequest); // set the caller name nameTextView.setText(preferredName); // set the caller number in subscript, or make the field disappear. if (TextUtils.isEmpty(callerNumber)) { numberTextView.setVisibility(View.GONE); numberTypeTextView.setVisibility(View.GONE); } else { numberTextView.setVisibility(View.VISIBLE); numberTextView.setText( PhoneNumberUtilsCompat.createTtsSpannable( BidiFormatter.getInstance().unicodeWrap(callerNumber, TextDirectionHeuristics.LTR))); numberTypeTextView.setVisibility(View.VISIBLE); numberTypeTextView.setText(callerNumberType); } } /** * Updates the participant info list which is bound to the ListView. Stores the call and contact * info for all entries. The list is sorted alphabetically by participant name. * * @param conferenceParticipants The calls which make up the conference participants. */ private void updateParticipantInfo(List conferenceParticipants) { final ContactInfoCache cache = ContactInfoCache.getInstance(getContext()); boolean newParticipantAdded = false; Set newCallIds = new ArraySet<>(conferenceParticipants.size()); // Update or add conference participant info. for (DialerCall call : conferenceParticipants) { String callId = call.getId(); newCallIds.add(callId); ContactCacheEntry contactCache = cache.getInfo(callId); if (contactCache == null) { contactCache = ContactInfoCache.buildCacheEntryFromCall( getContext(), call, call.getState() == DialerCall.State.INCOMING); } if (mParticipantsByCallId.containsKey(callId)) { ParticipantInfo participantInfo = mParticipantsByCallId.get(callId); participantInfo.setCall(call); participantInfo.setContactCacheEntry(contactCache); } else { newParticipantAdded = true; ParticipantInfo participantInfo = new ParticipantInfo(call, contactCache); mConferenceParticipants.add(participantInfo); mParticipantsByCallId.put(call.getId(), participantInfo); } } // Remove any participants that no longer exist. Iterator> it = mParticipantsByCallId.entrySet().iterator(); while (it.hasNext()) { Map.Entry entry = it.next(); String existingCallId = entry.getKey(); if (!newCallIds.contains(existingCallId)) { ParticipantInfo existingInfo = entry.getValue(); mConferenceParticipants.remove(existingInfo); it.remove(); } } if (newParticipantAdded) { // Sort the list of participants by contact name. sortParticipantList(); } notifyDataSetChanged(); } /** Sorts the participant list by contact name. */ private void sortParticipantList() { Collections.sort( mConferenceParticipants, new Comparator() { @Override public int compare(ParticipantInfo p1, ParticipantInfo p2) { // Contact names might be null, so replace with empty string. ContactCacheEntry c1 = p1.getContactCacheEntry(); String p1Name = ContactDisplayUtils.getPreferredSortName( c1.namePrimary, c1.nameAlternative, mContactsPreferences); p1Name = p1Name != null ? p1Name : ""; ContactCacheEntry c2 = p2.getContactCacheEntry(); String p2Name = ContactDisplayUtils.getPreferredSortName( c2.namePrimary, c2.nameAlternative, mContactsPreferences); p2Name = p2Name != null ? p2Name : ""; return p1Name.compareToIgnoreCase(p2Name); } }); } private DialerCall getCallFromView(View view) { View parent = (View) view.getParent(); String callId = (String) parent.getTag(); return CallList.getInstance().getCallById(callId); } /** * Callback class used when making requests to the {@link ContactInfoCache} to resolve contact * info and contact photos for conference participants. */ public static class ContactLookupCallback implements ContactInfoCache.ContactInfoCacheCallback { private final WeakReference mListAdapter; public ContactLookupCallback(ConferenceParticipantListAdapter listAdapter) { mListAdapter = new WeakReference<>(listAdapter); } /** * Called when contact info has been resolved. * * @param callId The call id. * @param entry The new contact information. */ @Override public void onContactInfoComplete(String callId, ContactCacheEntry entry) { update(callId, entry); } /** * Called when contact photo has been loaded into the cache. * * @param callId The call id. * @param entry The new contact information. */ @Override public void onImageLoadComplete(String callId, ContactCacheEntry entry) { update(callId, entry); } /** * Updates the contact information for a participant. * * @param callId The call id. * @param entry The new contact information. */ private void update(String callId, ContactCacheEntry entry) { ConferenceParticipantListAdapter listAdapter = mListAdapter.get(); if (listAdapter != null) { listAdapter.updateContactInfo(callId, entry); } } } /** * Internal class which represents a participant. Includes a reference to the {@link DialerCall} * and the corresponding {@link ContactCacheEntry} for the participant. */ private static class ParticipantInfo { private DialerCall mCall; private ContactCacheEntry mContactCacheEntry; private boolean mCacheLookupComplete = false; public ParticipantInfo(DialerCall call, ContactCacheEntry contactCacheEntry) { mCall = call; mContactCacheEntry = contactCacheEntry; } public DialerCall getCall() { return mCall; } public void setCall(DialerCall call) { mCall = call; } public ContactCacheEntry getContactCacheEntry() { return mContactCacheEntry; } public void setContactCacheEntry(ContactCacheEntry entry) { mContactCacheEntry = entry; } public boolean isCacheLookupComplete() { return mCacheLookupComplete; } public void setCacheLookupComplete(boolean cacheLookupComplete) { mCacheLookupComplete = cacheLookupComplete; } @Override public boolean equals(Object o) { if (o instanceof ParticipantInfo) { ParticipantInfo p = (ParticipantInfo) o; return Objects.equals(p.getCall().getId(), mCall.getId()); } return false; } @Override public int hashCode() { return mCall.getId().hashCode(); } } }