Home | History | Annotate | Download | only in incallui
      1 /*
      2  * Copyright (C) 2013 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.Manifest;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.content.pm.ApplicationInfo;
     23 import android.content.pm.PackageManager;
     24 import android.graphics.drawable.Drawable;
     25 import android.net.Uri;
     26 import android.os.Bundle;
     27 import android.os.Handler;
     28 import android.telecom.DisconnectCause;
     29 import android.telecom.PhoneCapabilities;
     30 import android.telecom.PhoneAccount;
     31 import android.telecom.PhoneAccountHandle;
     32 import android.telecom.StatusHints;
     33 import android.telecom.TelecomManager;
     34 import android.telecom.VideoProfile;
     35 import android.telephony.PhoneNumberUtils;
     36 import android.telephony.TelephonyManager;
     37 import android.text.TextUtils;
     38 import android.text.format.DateUtils;
     39 
     40 import com.android.incallui.ContactInfoCache.ContactCacheEntry;
     41 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
     42 import com.android.incallui.InCallPresenter.InCallDetailsListener;
     43 import com.android.incallui.InCallPresenter.InCallEventListener;
     44 import com.android.incallui.InCallPresenter.InCallState;
     45 import com.android.incallui.InCallPresenter.InCallStateListener;
     46 import com.android.incallui.InCallPresenter.IncomingCallListener;
     47 import com.android.incalluibind.ObjectFactory;
     48 
     49 import java.lang.ref.WeakReference;
     50 
     51 import com.google.common.base.Preconditions;
     52 
     53 /**
     54  * Presenter for the Call Card Fragment.
     55  * <p>
     56  * This class listens for changes to InCallState and passes it along to the fragment.
     57  */
     58 public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi>
     59         implements InCallStateListener, IncomingCallListener, InCallDetailsListener,
     60         InCallEventListener {
     61 
     62     private static final String TAG = CallCardPresenter.class.getSimpleName();
     63     private static final long CALL_TIME_UPDATE_INTERVAL_MS = 1000;
     64 
     65     private Call mPrimary;
     66     private Call mSecondary;
     67     private ContactCacheEntry mPrimaryContactInfo;
     68     private ContactCacheEntry mSecondaryContactInfo;
     69     private CallTimer mCallTimer;
     70     private Context mContext;
     71     private TelecomManager mTelecomManager;
     72 
     73     public static class ContactLookupCallback implements ContactInfoCacheCallback {
     74         private final WeakReference<CallCardPresenter> mCallCardPresenter;
     75         private final boolean mIsPrimary;
     76 
     77         public ContactLookupCallback(CallCardPresenter callCardPresenter, boolean isPrimary) {
     78             mCallCardPresenter = new WeakReference<CallCardPresenter>(callCardPresenter);
     79             mIsPrimary = isPrimary;
     80         }
     81 
     82         @Override
     83         public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
     84             CallCardPresenter presenter = mCallCardPresenter.get();
     85             if (presenter != null) {
     86                 presenter.onContactInfoComplete(callId, entry, mIsPrimary);
     87             }
     88         }
     89 
     90         @Override
     91         public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
     92             CallCardPresenter presenter = mCallCardPresenter.get();
     93             if (presenter != null) {
     94                 presenter.onImageLoadComplete(callId, entry);
     95             }
     96         }
     97 
     98     }
     99 
    100     public CallCardPresenter() {
    101         // create the call timer
    102         mCallTimer = new CallTimer(new Runnable() {
    103             @Override
    104             public void run() {
    105                 updateCallTime();
    106             }
    107         });
    108     }
    109 
    110     public void init(Context context, Call call) {
    111         mContext = Preconditions.checkNotNull(context);
    112 
    113         // Call may be null if disconnect happened already.
    114         if (call != null) {
    115             mPrimary = call;
    116 
    117             // start processing lookups right away.
    118             if (!call.isConferenceCall()) {
    119                 startContactInfoSearch(call, true, call.getState() == Call.State.INCOMING);
    120             } else {
    121                 updateContactEntry(null, true, true);
    122             }
    123         }
    124     }
    125 
    126     @Override
    127     public void onUiReady(CallCardUi ui) {
    128         super.onUiReady(ui);
    129 
    130         // Contact search may have completed before ui is ready.
    131         if (mPrimaryContactInfo != null) {
    132             updatePrimaryDisplayInfo(mPrimaryContactInfo, isConference(mPrimary));
    133         }
    134 
    135         // Register for call state changes last
    136         InCallPresenter.getInstance().addListener(this);
    137         InCallPresenter.getInstance().addIncomingCallListener(this);
    138         InCallPresenter.getInstance().addDetailsListener(this);
    139         InCallPresenter.getInstance().addInCallEventListener(this);
    140     }
    141 
    142     @Override
    143     public void onUiUnready(CallCardUi ui) {
    144         super.onUiUnready(ui);
    145 
    146         // stop getting call state changes
    147         InCallPresenter.getInstance().removeListener(this);
    148         InCallPresenter.getInstance().removeIncomingCallListener(this);
    149         InCallPresenter.getInstance().removeDetailsListener(this);
    150         InCallPresenter.getInstance().removeInCallEventListener(this);
    151 
    152         mPrimary = null;
    153         mPrimaryContactInfo = null;
    154         mSecondaryContactInfo = null;
    155     }
    156 
    157     @Override
    158     public void onIncomingCall(InCallState oldState, InCallState newState, Call call) {
    159         // same logic should happen as with onStateChange()
    160         onStateChange(oldState, newState, CallList.getInstance());
    161     }
    162 
    163     @Override
    164     public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
    165         Log.d(this, "onStateChange() " + newState);
    166         final CallCardUi ui = getUi();
    167         if (ui == null) {
    168             return;
    169         }
    170 
    171         Call primary = null;
    172         Call secondary = null;
    173 
    174         if (newState == InCallState.INCOMING) {
    175             primary = callList.getIncomingCall();
    176         } else if (newState == InCallState.PENDING_OUTGOING || newState == InCallState.OUTGOING) {
    177             primary = callList.getOutgoingCall();
    178             if (primary == null) {
    179                 primary = callList.getPendingOutgoingCall();
    180             }
    181 
    182             // getCallToDisplay doesn't go through outgoing or incoming calls. It will return the
    183             // highest priority call to display as the secondary call.
    184             secondary = getCallToDisplay(callList, null, true);
    185         } else if (newState == InCallState.INCALL) {
    186             primary = getCallToDisplay(callList, null, false);
    187             secondary = getCallToDisplay(callList, primary, true);
    188         }
    189 
    190         Log.d(this, "Primary call: " + primary);
    191         Log.d(this, "Secondary call: " + secondary);
    192 
    193         final boolean primaryChanged = !Call.areSame(mPrimary, primary);
    194         final boolean secondaryChanged = !Call.areSame(mSecondary, secondary);
    195 
    196         mSecondary = secondary;
    197         mPrimary = primary;
    198 
    199         if (primaryChanged && mPrimary != null) {
    200             // primary call has changed
    201             mPrimaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, mPrimary,
    202                     mPrimary.getState() == Call.State.INCOMING);
    203             updatePrimaryDisplayInfo(mPrimaryContactInfo, isConference(mPrimary));
    204             maybeStartSearch(mPrimary, true);
    205             mPrimary.setSessionModificationState(Call.SessionModificationState.NO_REQUEST);
    206         }
    207 
    208         if (mSecondary == null) {
    209             // Secondary call may have ended.  Update the ui.
    210             mSecondaryContactInfo = null;
    211             updateSecondaryDisplayInfo(false);
    212         } else if (secondaryChanged) {
    213             // secondary call has changed
    214             mSecondaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, mSecondary,
    215                     mSecondary.getState() == Call.State.INCOMING);
    216             updateSecondaryDisplayInfo(mSecondary.isConferenceCall());
    217             maybeStartSearch(mSecondary, false);
    218             mSecondary.setSessionModificationState(Call.SessionModificationState.NO_REQUEST);
    219         }
    220 
    221         // Start/stop timers.
    222         if (mPrimary != null && mPrimary.getState() == Call.State.ACTIVE) {
    223             Log.d(this, "Starting the calltime timer");
    224             mCallTimer.start(CALL_TIME_UPDATE_INTERVAL_MS);
    225         } else {
    226             Log.d(this, "Canceling the calltime timer");
    227             mCallTimer.cancel();
    228             ui.setPrimaryCallElapsedTime(false, null);
    229         }
    230 
    231         // Set the call state
    232         int callState = Call.State.IDLE;
    233         if (mPrimary != null) {
    234             callState = mPrimary.getState();
    235             updatePrimaryCallState();
    236         } else {
    237             getUi().setCallState(
    238                     callState,
    239                     VideoProfile.VideoState.AUDIO_ONLY,
    240                     Call.SessionModificationState.NO_REQUEST,
    241                     new DisconnectCause(DisconnectCause.UNKNOWN),
    242                     null,
    243                     null,
    244                     null);
    245         }
    246 
    247         // Hide/show the contact photo based on the video state.
    248         // If the primary call is a video call on hold, still show the contact photo.
    249         // If the primary call is an active video call, hide the contact photo.
    250         if (mPrimary != null) {
    251             getUi().setPhotoVisible(!(mPrimary.isVideoCall(mContext) &&
    252                     callState != Call.State.ONHOLD));
    253         }
    254 
    255         maybeShowManageConferenceCallButton();
    256 
    257         final boolean enableEndCallButton = Call.State.isConnectingOrConnected(callState) &&
    258                 callState != Call.State.INCOMING && mPrimary != null;
    259         // Hide the end call button instantly if we're receiving an incoming call.
    260         getUi().setEndCallButtonEnabled(
    261                 enableEndCallButton, callState != Call.State.INCOMING /* animate */);
    262     }
    263 
    264     @Override
    265     public void onDetailsChanged(Call call, android.telecom.Call.Details details) {
    266         updatePrimaryCallState();
    267     }
    268 
    269     private String getSubscriptionNumber() {
    270         // If it's an emergency call, and they're not populating the callback number,
    271         // then try to fall back to the phone sub info (to hopefully get the SIM's
    272         // number directly from the telephony layer).
    273         PhoneAccountHandle accountHandle = mPrimary.getAccountHandle();
    274         if (accountHandle != null) {
    275             TelecomManager mgr =
    276                     (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
    277             PhoneAccount account = mgr.getPhoneAccount(accountHandle);
    278             if (account != null) {
    279                 return getNumberFromHandle(account.getSubscriptionAddress());
    280             }
    281         }
    282         return null;
    283     }
    284 
    285     private void updatePrimaryCallState() {
    286         if (getUi() != null && mPrimary != null) {
    287             getUi().setCallState(
    288                     mPrimary.getState(),
    289                     mPrimary.getVideoState(),
    290                     mPrimary.getSessionModificationState(),
    291                     mPrimary.getDisconnectCause(),
    292                     getConnectionLabel(),
    293                     getConnectionIcon(),
    294                     getGatewayNumber());
    295             setCallbackNumber();
    296         }
    297     }
    298 
    299     /**
    300      * Only show the conference call button if we can manage the conference.
    301      */
    302     private void maybeShowManageConferenceCallButton() {
    303         if (mPrimary == null) {
    304             getUi().showManageConferenceCallButton(false);
    305             return;
    306         }
    307 
    308         final boolean canManageConference = mPrimary.can(PhoneCapabilities.MANAGE_CONFERENCE);
    309         getUi().showManageConferenceCallButton(mPrimary.isConferenceCall() && canManageConference);
    310     }
    311 
    312     private void setCallbackNumber() {
    313         String callbackNumber = null;
    314 
    315         boolean isEmergencyCall = PhoneNumberUtils.isEmergencyNumber(
    316                 getNumberFromHandle(mPrimary.getHandle()));
    317         if (isEmergencyCall) {
    318             callbackNumber = getSubscriptionNumber();
    319         } else {
    320             StatusHints statusHints = mPrimary.getTelecommCall().getDetails().getStatusHints();
    321             if (statusHints != null) {
    322                 Bundle extras = statusHints.getExtras();
    323                 if (extras != null) {
    324                     callbackNumber = extras.getString(TelecomManager.EXTRA_CALL_BACK_NUMBER);
    325                 }
    326             }
    327         }
    328 
    329         TelephonyManager telephonyManager =
    330                 (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
    331         String simNumber = telephonyManager.getLine1Number();
    332         if (PhoneNumberUtils.compare(callbackNumber, simNumber)) {
    333             Log.d(this, "Numbers are the same; not showing the callback number");
    334             callbackNumber = null;
    335         }
    336 
    337         getUi().setCallbackNumber(callbackNumber, isEmergencyCall);
    338     }
    339 
    340     public void updateCallTime() {
    341         final CallCardUi ui = getUi();
    342 
    343         if (ui == null || mPrimary == null || mPrimary.getState() != Call.State.ACTIVE) {
    344             if (ui != null) {
    345                 ui.setPrimaryCallElapsedTime(false, null);
    346             }
    347             mCallTimer.cancel();
    348         } else {
    349             final long callStart = mPrimary.getConnectTimeMillis();
    350             final long duration = System.currentTimeMillis() - callStart;
    351             ui.setPrimaryCallElapsedTime(true, DateUtils.formatElapsedTime(duration / 1000));
    352         }
    353     }
    354 
    355     public void onCallStateButtonTouched() {
    356         Intent broadcastIntent = ObjectFactory.getCallStateButtonBroadcastIntent(mContext);
    357         if (broadcastIntent != null) {
    358             Log.d(this, "Sending call state button broadcast: ", broadcastIntent);
    359             mContext.sendBroadcast(broadcastIntent, Manifest.permission.READ_PHONE_STATE);
    360         }
    361     }
    362 
    363     private void maybeStartSearch(Call call, boolean isPrimary) {
    364         // no need to start search for conference calls which show generic info.
    365         if (call != null && !call.isConferenceCall()) {
    366             startContactInfoSearch(call, isPrimary, call.getState() == Call.State.INCOMING);
    367         }
    368     }
    369 
    370     /**
    371      * Starts a query for more contact data for the save primary and secondary calls.
    372      */
    373     private void startContactInfoSearch(final Call call, final boolean isPrimary,
    374             boolean isIncoming) {
    375         final ContactInfoCache cache = ContactInfoCache.getInstance(mContext);
    376 
    377         cache.findInfo(call, isIncoming, new ContactLookupCallback(this, isPrimary));
    378     }
    379 
    380     private void onContactInfoComplete(String callId, ContactCacheEntry entry, boolean isPrimary) {
    381         updateContactEntry(entry, isPrimary, false);
    382         if (entry.name != null) {
    383             Log.d(TAG, "Contact found: " + entry);
    384         }
    385         if (entry.contactUri != null) {
    386             CallerInfoUtils.sendViewNotification(mContext, entry.contactUri);
    387         }
    388     }
    389 
    390     private void onImageLoadComplete(String callId, ContactCacheEntry entry) {
    391         if (getUi() == null) {
    392             return;
    393         }
    394 
    395         if (entry.photo != null) {
    396             if (mPrimary != null && callId.equals(mPrimary.getId())) {
    397                 getUi().setPrimaryImage(entry.photo);
    398             }
    399         }
    400     }
    401 
    402     private static boolean isConference(Call call) {
    403         return call != null && call.isConferenceCall();
    404     }
    405 
    406     private static boolean canManageConference(Call call) {
    407         return call != null && call.can(PhoneCapabilities.MANAGE_CONFERENCE);
    408     }
    409 
    410     private void updateContactEntry(ContactCacheEntry entry, boolean isPrimary,
    411             boolean isConference) {
    412         if (isPrimary) {
    413             mPrimaryContactInfo = entry;
    414             updatePrimaryDisplayInfo(entry, isConference);
    415         } else {
    416             mSecondaryContactInfo = entry;
    417             updateSecondaryDisplayInfo(isConference);
    418         }
    419     }
    420 
    421     /**
    422      * Get the highest priority call to display.
    423      * Goes through the calls and chooses which to return based on priority of which type of call
    424      * to display to the user. Callers can use the "ignore" feature to get the second best call
    425      * by passing a previously found primary call as ignore.
    426      *
    427      * @param ignore A call to ignore if found.
    428      */
    429     private Call getCallToDisplay(CallList callList, Call ignore, boolean skipDisconnected) {
    430 
    431         // Active calls come second.  An active call always gets precedent.
    432         Call retval = callList.getActiveCall();
    433         if (retval != null && retval != ignore) {
    434             return retval;
    435         }
    436 
    437         // Disconnected calls get primary position if there are no active calls
    438         // to let user know quickly what call has disconnected. Disconnected
    439         // calls are very short lived.
    440         if (!skipDisconnected) {
    441             retval = callList.getDisconnectingCall();
    442             if (retval != null && retval != ignore) {
    443                 return retval;
    444             }
    445             retval = callList.getDisconnectedCall();
    446             if (retval != null && retval != ignore) {
    447                 return retval;
    448             }
    449         }
    450 
    451         // Then we go to background call (calls on hold)
    452         retval = callList.getBackgroundCall();
    453         if (retval != null && retval != ignore) {
    454             return retval;
    455         }
    456 
    457         // Lastly, we go to a second background call.
    458         retval = callList.getSecondBackgroundCall();
    459 
    460         return retval;
    461     }
    462 
    463     private void updatePrimaryDisplayInfo(ContactCacheEntry entry, boolean isConference) {
    464         Log.d(TAG, "Update primary display " + entry);
    465         final CallCardUi ui = getUi();
    466         if (ui == null) {
    467             // TODO: May also occur if search result comes back after ui is destroyed. Look into
    468             // removing that case completely.
    469             Log.d(TAG, "updatePrimaryDisplayInfo called but ui is null!");
    470             return;
    471         }
    472 
    473         final boolean canManageConference = canManageConference(mPrimary);
    474         if (entry != null && mPrimary != null) {
    475             final String name = getNameForCall(entry);
    476             final String number = getNumberForCall(entry);
    477             final boolean nameIsNumber = name != null && name.equals(entry.number);
    478             ui.setPrimary(number, name, nameIsNumber, entry.label,
    479                     entry.photo, isConference, canManageConference, entry.isSipCall);
    480         } else {
    481             ui.setPrimary(null, null, false, null, null, isConference, canManageConference, false);
    482         }
    483 
    484     }
    485 
    486     private void updateSecondaryDisplayInfo(boolean isConference) {
    487         final CallCardUi ui = getUi();
    488         if (ui == null) {
    489             return;
    490         }
    491 
    492         final boolean canManageConference = canManageConference(mSecondary);
    493         if (mSecondaryContactInfo != null && mSecondary != null) {
    494             Log.d(TAG, "updateSecondaryDisplayInfo() " + mSecondaryContactInfo);
    495             final String nameForCall = getNameForCall(mSecondaryContactInfo);
    496 
    497             final boolean nameIsNumber = nameForCall != null && nameForCall.equals(
    498                     mSecondaryContactInfo.number);
    499             ui.setSecondary(true /* show */, nameForCall, nameIsNumber, mSecondaryContactInfo.label,
    500                     getCallProviderLabel(mSecondary), getCallProviderIcon(mSecondary),
    501                     isConference, canManageConference);
    502         } else {
    503             // reset to nothing so that it starts off blank next time we use it.
    504             ui.setSecondary(false, null, false, null, null, null, isConference, canManageConference);
    505         }
    506     }
    507 
    508 
    509     /**
    510      * Gets the phone account to display for a call.
    511      */
    512     private PhoneAccount getAccountForCall(Call call) {
    513         PhoneAccountHandle accountHandle = call.getAccountHandle();
    514         if (accountHandle == null) {
    515             return null;
    516         }
    517         return getTelecomManager().getPhoneAccount(accountHandle);
    518     }
    519 
    520     /**
    521      * Returns the gateway number for any existing outgoing call.
    522      */
    523     private String getGatewayNumber() {
    524         if (hasOutgoingGatewayCall()) {
    525             return getNumberFromHandle(mPrimary.getGatewayInfo().getGatewayAddress());
    526         }
    527         return null;
    528     }
    529 
    530     /**
    531      * Return the Drawable object of the icon to display to the left of the connection label.
    532      */
    533     private Drawable getCallProviderIcon(Call call) {
    534         PhoneAccount account = getAccountForCall(call);
    535         if (account != null && getTelecomManager().hasMultipleCallCapableAccounts()) {
    536             return account.getIcon(mContext);
    537         }
    538         return null;
    539     }
    540 
    541     /**
    542      * Return the string label to represent the call provider
    543      */
    544     private String getCallProviderLabel(Call call) {
    545         PhoneAccount account = getAccountForCall(call);
    546         if (account != null && getTelecomManager().hasMultipleCallCapableAccounts()) {
    547             return account.getLabel().toString();
    548         }
    549         return null;
    550     }
    551 
    552     /**
    553      * Returns the label (line of text above the number/name) for any given call.
    554      * For example, "calling via [Account/Google Voice]" for outgoing calls.
    555      */
    556     private String getConnectionLabel() {
    557         StatusHints statusHints = mPrimary.getTelecommCall().getDetails().getStatusHints();
    558         if (statusHints != null && !TextUtils.isEmpty(statusHints.getLabel())) {
    559             return statusHints.getLabel().toString();
    560         }
    561 
    562         if (hasOutgoingGatewayCall() && getUi() != null) {
    563             // Return the label for the gateway app on outgoing calls.
    564             final PackageManager pm = mContext.getPackageManager();
    565             try {
    566                 ApplicationInfo info = pm.getApplicationInfo(
    567                         mPrimary.getGatewayInfo().getGatewayProviderPackageName(), 0);
    568                 return pm.getApplicationLabel(info).toString();
    569             } catch (PackageManager.NameNotFoundException e) {
    570                 Log.e(this, "Gateway Application Not Found.", e);
    571                 return null;
    572             }
    573         }
    574         return getCallProviderLabel(mPrimary);
    575     }
    576 
    577     private Drawable getConnectionIcon() {
    578         StatusHints statusHints = mPrimary.getTelecommCall().getDetails().getStatusHints();
    579         if (statusHints != null && statusHints.getIconResId() != 0) {
    580             Drawable icon = statusHints.getIcon(mContext);
    581             if (icon != null) {
    582                 return icon;
    583             }
    584         }
    585         return getCallProviderIcon(mPrimary);
    586     }
    587 
    588     private boolean hasOutgoingGatewayCall() {
    589         // We only display the gateway information while STATE_DIALING so return false for any othe
    590         // call state.
    591         // TODO: mPrimary can be null because this is called from updatePrimaryDisplayInfo which
    592         // is also called after a contact search completes (call is not present yet).  Split the
    593         // UI update so it can receive independent updates.
    594         if (mPrimary == null) {
    595             return false;
    596         }
    597         return Call.State.isDialing(mPrimary.getState()) && mPrimary.getGatewayInfo() != null &&
    598                 !mPrimary.getGatewayInfo().isEmpty();
    599     }
    600 
    601     /**
    602      * Gets the name to display for the call.
    603      */
    604     private static String getNameForCall(ContactCacheEntry contactInfo) {
    605         if (TextUtils.isEmpty(contactInfo.name)) {
    606             return contactInfo.number;
    607         }
    608         return contactInfo.name;
    609     }
    610 
    611     /**
    612      * Gets the number to display for a call.
    613      */
    614     private static String getNumberForCall(ContactCacheEntry contactInfo) {
    615         // If the name is empty, we use the number for the name...so dont show a second
    616         // number in the number field
    617         if (TextUtils.isEmpty(contactInfo.name)) {
    618             return contactInfo.location;
    619         }
    620         return contactInfo.number;
    621     }
    622 
    623     public void secondaryInfoClicked() {
    624         if (mSecondary == null) {
    625             Log.wtf(this, "Secondary info clicked but no secondary call.");
    626             return;
    627         }
    628 
    629         Log.i(this, "Swapping call to foreground: " + mSecondary);
    630         TelecomAdapter.getInstance().unholdCall(mSecondary.getId());
    631     }
    632 
    633     public void endCallClicked() {
    634         if (mPrimary == null) {
    635             return;
    636         }
    637 
    638         Log.i(this, "Disconnecting call: " + mPrimary);
    639         mPrimary.setState(Call.State.DISCONNECTING);
    640         CallList.getInstance().onUpdate(mPrimary);
    641         TelecomAdapter.getInstance().disconnectCall(mPrimary.getId());
    642     }
    643 
    644     private String getNumberFromHandle(Uri handle) {
    645         return handle == null ? "" : handle.getSchemeSpecificPart();
    646     }
    647 
    648     /**
    649      * Handles a change to the full screen video state.
    650      *
    651      * @param isFullScreenVideo {@code True} if the application is entering full screen video mode.
    652      */
    653     @Override
    654     public void onFullScreenVideoStateChanged(boolean isFullScreenVideo) {
    655         final CallCardUi ui = getUi();
    656         if (ui == null) {
    657             return;
    658         }
    659         ui.setCallCardVisible(!isFullScreenVideo);
    660     }
    661 
    662     public interface CallCardUi extends Ui {
    663         void setVisible(boolean on);
    664         void setCallCardVisible(boolean visible);
    665         void setPrimary(String number, String name, boolean nameIsNumber, String label,
    666                 Drawable photo, boolean isConference, boolean canManageConference,
    667                 boolean isSipCall);
    668         void setSecondary(boolean show, String name, boolean nameIsNumber, String label,
    669                 String providerLabel, Drawable providerIcon, boolean isConference,
    670                 boolean canManageConference);
    671         void setCallState(int state, int videoState, int sessionModificationState,
    672                 DisconnectCause disconnectCause, String connectionLabel,
    673                 Drawable connectionIcon, String gatewayNumber);
    674         void setPrimaryCallElapsedTime(boolean show, String duration);
    675         void setPrimaryName(String name, boolean nameIsNumber);
    676         void setPrimaryImage(Drawable image);
    677         void setPrimaryPhoneNumber(String phoneNumber);
    678         void setPrimaryLabel(String label);
    679         void setEndCallButtonEnabled(boolean enabled, boolean animate);
    680         void setCallbackNumber(String number, boolean isEmergencyCalls);
    681         void setPhotoVisible(boolean isVisible);
    682         void setProgressSpinnerVisible(boolean visible);
    683         void showManageConferenceCallButton(boolean visible);
    684     }
    685 
    686     private TelecomManager getTelecomManager() {
    687         if (mTelecomManager == null) {
    688             mTelecomManager =
    689                     (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
    690         }
    691         return mTelecomManager;
    692     }
    693 }
    694