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.app.ActivityManager.TaskDescription;
     20 import android.app.FragmentManager;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.content.pm.ActivityInfo;
     24 import android.content.res.Resources;
     25 import android.graphics.Point;
     26 import android.net.Uri;
     27 import android.os.Bundle;
     28 import android.telecom.DisconnectCause;
     29 import android.telecom.PhoneAccount;
     30 import android.telecom.PhoneAccountHandle;
     31 import android.telecom.TelecomManager;
     32 import android.telecom.VideoProfile;
     33 import android.telephony.PhoneNumberUtils;
     34 import android.text.TextUtils;
     35 import android.view.Surface;
     36 import android.view.View;
     37 import android.view.Window;
     38 import android.view.WindowManager;
     39 
     40 import com.android.contacts.common.interactions.TouchPointManager;
     41 import com.android.contacts.common.testing.NeededForTesting;
     42 import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
     43 import com.android.incalluibind.ObjectFactory;
     44 import com.google.common.base.Preconditions;
     45 
     46 import java.util.Collections;
     47 import java.util.List;
     48 import java.util.Locale;
     49 import java.util.Set;
     50 import java.util.concurrent.ConcurrentHashMap;
     51 import java.util.concurrent.CopyOnWriteArrayList;
     52 
     53 /**
     54  * Takes updates from the CallList and notifies the InCallActivity (UI)
     55  * of the changes.
     56  * Responsible for starting the activity for a new call and finishing the activity when all calls
     57  * are disconnected.
     58  * Creates and manages the in-call state and provides a listener pattern for the presenters
     59  * that want to listen in on the in-call state changes.
     60  * TODO: This class has become more of a state machine at this point.  Consider renaming.
     61  */
     62 public class InCallPresenter implements CallList.Listener,
     63         CircularRevealFragment.OnCircularRevealCompleteListener {
     64 
     65     private static final String EXTRA_FIRST_TIME_SHOWN =
     66             "com.android.incallui.intent.extra.FIRST_TIME_SHOWN";
     67 
     68     private static final Bundle EMPTY_EXTRAS = new Bundle();
     69 
     70     private static InCallPresenter sInCallPresenter;
     71 
     72     /**
     73      * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
     74      * load factor before resizing, 1 means we only expect a single thread to
     75      * access the map so make only a single shard
     76      */
     77     private final Set<InCallStateListener> mListeners = Collections.newSetFromMap(
     78             new ConcurrentHashMap<InCallStateListener, Boolean>(8, 0.9f, 1));
     79     private final List<IncomingCallListener> mIncomingCallListeners = new CopyOnWriteArrayList<>();
     80     private final Set<InCallDetailsListener> mDetailsListeners = Collections.newSetFromMap(
     81             new ConcurrentHashMap<InCallDetailsListener, Boolean>(8, 0.9f, 1));
     82     private final Set<CanAddCallListener> mCanAddCallListeners = Collections.newSetFromMap(
     83             new ConcurrentHashMap<CanAddCallListener, Boolean>(8, 0.9f, 1));
     84     private final Set<InCallUiListener> mInCallUiListeners = Collections.newSetFromMap(
     85             new ConcurrentHashMap<InCallUiListener, Boolean>(8, 0.9f, 1));
     86     private final Set<InCallOrientationListener> mOrientationListeners = Collections.newSetFromMap(
     87             new ConcurrentHashMap<InCallOrientationListener, Boolean>(8, 0.9f, 1));
     88     private final Set<InCallEventListener> mInCallEventListeners = Collections.newSetFromMap(
     89             new ConcurrentHashMap<InCallEventListener, Boolean>(8, 0.9f, 1));
     90 
     91     private AudioModeProvider mAudioModeProvider;
     92     private StatusBarNotifier mStatusBarNotifier;
     93     private ContactInfoCache mContactInfoCache;
     94     private Context mContext;
     95     private CallList mCallList;
     96     private InCallActivity mInCallActivity;
     97     private InCallState mInCallState = InCallState.NO_CALLS;
     98     private ProximitySensor mProximitySensor;
     99     private boolean mServiceConnected = false;
    100     private boolean mAccountSelectionCancelled = false;
    101     private InCallCameraManager mInCallCameraManager = null;
    102     private AnswerPresenter mAnswerPresenter = new AnswerPresenter();
    103 
    104     /**
    105      * Whether or not we are currently bound and waiting for Telecom to send us a new call.
    106      */
    107     private boolean mBoundAndWaitingForOutgoingCall;
    108 
    109     /**
    110      * If there is no actual call currently in the call list, this will be used as a fallback
    111      * to determine the theme color for InCallUI.
    112      */
    113     private PhoneAccountHandle mPendingPhoneAccountHandle;
    114 
    115     /**
    116      * Determines if the InCall UI is in fullscreen mode or not.
    117      */
    118     private boolean mIsFullScreen = false;
    119 
    120     private final android.telecom.Call.Callback mCallCallback =
    121             new android.telecom.Call.Callback() {
    122         @Override
    123         public void onPostDialWait(android.telecom.Call telecomCall,
    124                 String remainingPostDialSequence) {
    125             final Call call = mCallList.getCallByTelecommCall(telecomCall);
    126             if (call == null) {
    127                 Log.w(this, "Call not found in call list: " + telecomCall);
    128                 return;
    129             }
    130             onPostDialCharWait(call.getId(), remainingPostDialSequence);
    131         }
    132 
    133         @Override
    134         public void onDetailsChanged(android.telecom.Call telecomCall,
    135                 android.telecom.Call.Details details) {
    136             final Call call = mCallList.getCallByTelecommCall(telecomCall);
    137             if (call == null) {
    138                 Log.w(this, "Call not found in call list: " + telecomCall);
    139                 return;
    140             }
    141             for (InCallDetailsListener listener : mDetailsListeners) {
    142                 listener.onDetailsChanged(call, details);
    143             }
    144         }
    145 
    146         @Override
    147         public void onConferenceableCallsChanged(android.telecom.Call telecomCall,
    148                 List<android.telecom.Call> conferenceableCalls) {
    149             Log.i(this, "onConferenceableCallsChanged: " + telecomCall);
    150             onDetailsChanged(telecomCall, telecomCall.getDetails());
    151         }
    152     };
    153 
    154     /**
    155      * Is true when the activity has been previously started. Some code needs to know not just if
    156      * the activity is currently up, but if it had been previously shown in foreground for this
    157      * in-call session (e.g., StatusBarNotifier). This gets reset when the session ends in the
    158      * tear-down method.
    159      */
    160     private boolean mIsActivityPreviouslyStarted = false;
    161 
    162     /**
    163      * Whether or not InCallService is bound to Telecom.
    164      */
    165     private boolean mServiceBound = false;
    166 
    167     /**
    168      * When configuration changes Android kills the current activity and starts a new one.
    169      * The flag is used to check if full clean up is necessary (activity is stopped and new
    170      * activity won't be started), or if a new activity will be started right after the current one
    171      * is destroyed, and therefore no need in release all resources.
    172      */
    173     private boolean mIsChangingConfigurations = false;
    174 
    175     /** Display colors for the UI. Consists of a primary color and secondary (darker) color */
    176     private MaterialPalette mThemeColors;
    177 
    178     private TelecomManager mTelecomManager;
    179 
    180     public static synchronized InCallPresenter getInstance() {
    181         if (sInCallPresenter == null) {
    182             sInCallPresenter = new InCallPresenter();
    183         }
    184         return sInCallPresenter;
    185     }
    186 
    187     @NeededForTesting
    188     static synchronized void setInstance(InCallPresenter inCallPresenter) {
    189         sInCallPresenter = inCallPresenter;
    190     }
    191 
    192     public InCallState getInCallState() {
    193         return mInCallState;
    194     }
    195 
    196     public CallList getCallList() {
    197         return mCallList;
    198     }
    199 
    200     public void setUp(Context context,
    201             CallList callList,
    202             AudioModeProvider audioModeProvider,
    203             StatusBarNotifier statusBarNotifier,
    204             ContactInfoCache contactInfoCache,
    205             ProximitySensor proximitySensor) {
    206         if (mServiceConnected) {
    207             Log.i(this, "New service connection replacing existing one.");
    208             // retain the current resources, no need to create new ones.
    209             Preconditions.checkState(context == mContext);
    210             Preconditions.checkState(callList == mCallList);
    211             Preconditions.checkState(audioModeProvider == mAudioModeProvider);
    212             return;
    213         }
    214 
    215         Preconditions.checkNotNull(context);
    216         mContext = context;
    217 
    218         mContactInfoCache = contactInfoCache;
    219 
    220         mStatusBarNotifier = statusBarNotifier;
    221         addListener(mStatusBarNotifier);
    222 
    223         mAudioModeProvider = audioModeProvider;
    224 
    225         mProximitySensor = proximitySensor;
    226         addListener(mProximitySensor);
    227 
    228         addIncomingCallListener(mAnswerPresenter);
    229         addInCallUiListener(mAnswerPresenter);
    230 
    231         mCallList = callList;
    232 
    233         // This only gets called by the service so this is okay.
    234         mServiceConnected = true;
    235 
    236         // The final thing we do in this set up is add ourselves as a listener to CallList.  This
    237         // will kick off an update and the whole process can start.
    238         mCallList.addListener(this);
    239 
    240         VideoPauseController.getInstance().setUp(this);
    241 
    242         Log.d(this, "Finished InCallPresenter.setUp");
    243     }
    244 
    245     /**
    246      * Called when the telephony service has disconnected from us.  This will happen when there are
    247      * no more active calls. However, we may still want to continue showing the UI for
    248      * certain cases like showing "Call Ended".
    249      * What we really want is to wait for the activity and the service to both disconnect before we
    250      * tear things down. This method sets a serviceConnected boolean and calls a secondary method
    251      * that performs the aforementioned logic.
    252      */
    253     public void tearDown() {
    254         Log.d(this, "tearDown");
    255         mServiceConnected = false;
    256         attemptCleanup();
    257 
    258         VideoPauseController.getInstance().tearDown();
    259     }
    260 
    261     private void attemptFinishActivity() {
    262         final boolean doFinish = (mInCallActivity != null && isActivityStarted());
    263         Log.i(this, "Hide in call UI: " + doFinish);
    264         if (doFinish) {
    265             mInCallActivity.setExcludeFromRecents(true);
    266             mInCallActivity.finish();
    267 
    268             if (mAccountSelectionCancelled) {
    269                 // This finish is a result of account selection cancellation
    270                 // do not include activity ending transition
    271                 mInCallActivity.overridePendingTransition(0, 0);
    272             }
    273         }
    274     }
    275 
    276     /**
    277      * Called when the UI begins, and starts the callstate callbacks if necessary.
    278      */
    279     public void setActivity(InCallActivity inCallActivity) {
    280         if (inCallActivity == null) {
    281             throw new IllegalArgumentException("registerActivity cannot be called with null");
    282         }
    283         if (mInCallActivity != null && mInCallActivity != inCallActivity) {
    284             Log.w(this, "Setting a second activity before destroying the first.");
    285         }
    286         updateActivity(inCallActivity);
    287     }
    288 
    289     /**
    290      * Called when the UI ends. Attempts to tear down everything if necessary. See
    291      * {@link #tearDown()} for more insight on the tear-down process.
    292      */
    293     public void unsetActivity(InCallActivity inCallActivity) {
    294         if (inCallActivity == null) {
    295             throw new IllegalArgumentException("unregisterActivity cannot be called with null");
    296         }
    297         if (mInCallActivity == null) {
    298             Log.i(this, "No InCallActivity currently set, no need to unset.");
    299             return;
    300         }
    301         if (mInCallActivity != inCallActivity) {
    302             Log.w(this, "Second instance of InCallActivity is trying to unregister when another"
    303                     + " instance is active. Ignoring.");
    304             return;
    305         }
    306         updateActivity(null);
    307     }
    308 
    309     /**
    310      * Updates the current instance of {@link InCallActivity} with the provided one. If a
    311      * {@code null} activity is provided, it means that the activity was finished and we should
    312      * attempt to cleanup.
    313      */
    314     private void updateActivity(InCallActivity inCallActivity) {
    315         boolean updateListeners = false;
    316         boolean doAttemptCleanup = false;
    317 
    318         if (inCallActivity != null) {
    319             if (mInCallActivity == null) {
    320                 updateListeners = true;
    321                 Log.i(this, "UI Initialized");
    322             } else {
    323                 // since setActivity is called onStart(), it can be called multiple times.
    324                 // This is fine and ignorable, but we do not want to update the world every time
    325                 // this happens (like going to/from background) so we do not set updateListeners.
    326             }
    327 
    328             mInCallActivity = inCallActivity;
    329             mInCallActivity.setExcludeFromRecents(false);
    330 
    331             // By the time the UI finally comes up, the call may already be disconnected.
    332             // If that's the case, we may need to show an error dialog.
    333             if (mCallList != null && mCallList.getDisconnectedCall() != null) {
    334                 maybeShowErrorDialogOnDisconnect(mCallList.getDisconnectedCall());
    335             }
    336 
    337             // When the UI comes up, we need to first check the in-call state.
    338             // If we are showing NO_CALLS, that means that a call probably connected and
    339             // then immediately disconnected before the UI was able to come up.
    340             // If we dont have any calls, start tearing down the UI instead.
    341             // NOTE: This code relies on {@link #mInCallActivity} being set so we run it after
    342             // it has been set.
    343             if (mInCallState == InCallState.NO_CALLS) {
    344                 Log.i(this, "UI Initialized, but no calls left.  shut down.");
    345                 attemptFinishActivity();
    346                 return;
    347             }
    348         } else {
    349             Log.i(this, "UI Destroyed");
    350             updateListeners = true;
    351             mInCallActivity = null;
    352 
    353             // We attempt cleanup for the destroy case but only after we recalculate the state
    354             // to see if we need to come back up or stay shut down. This is why we do the
    355             // cleanup after the call to onCallListChange() instead of directly here.
    356             doAttemptCleanup = true;
    357         }
    358 
    359         // Messages can come from the telephony layer while the activity is coming up
    360         // and while the activity is going down.  So in both cases we need to recalculate what
    361         // state we should be in after they complete.
    362         // Examples: (1) A new incoming call could come in and then get disconnected before
    363         //               the activity is created.
    364         //           (2) All calls could disconnect and then get a new incoming call before the
    365         //               activity is destroyed.
    366         //
    367         // b/1122139 - We previously had a check for mServiceConnected here as well, but there are
    368         // cases where we need to recalculate the current state even if the service in not
    369         // connected.  In particular the case where startOrFinish() is called while the app is
    370         // already finish()ing. In that case, we skip updating the state with the knowledge that
    371         // we will check again once the activity has finished. That means we have to recalculate the
    372         // state here even if the service is disconnected since we may not have finished a state
    373         // transition while finish()ing.
    374         if (updateListeners) {
    375             onCallListChange(mCallList);
    376         }
    377 
    378         if (doAttemptCleanup) {
    379             attemptCleanup();
    380         }
    381     }
    382 
    383     private boolean mAwaitingCallListUpdate = false;
    384 
    385     public void onBringToForeground(boolean showDialpad) {
    386         Log.i(this, "Bringing UI to foreground.");
    387         bringToForeground(showDialpad);
    388     }
    389 
    390     /**
    391      * TODO: Consider listening to CallList callbacks to do this instead of receiving a direct
    392      * method invocation from InCallService.
    393      */
    394     public void onCallAdded(android.telecom.Call call) {
    395         // Since a call has been added we are no longer waiting for Telecom to send us a
    396         // call.
    397         setBoundAndWaitingForOutgoingCall(false, null);
    398         call.registerCallback(mCallCallback);
    399     }
    400 
    401     /**
    402      * TODO: Consider listening to CallList callbacks to do this instead of receiving a direct
    403      * method invocation from InCallService.
    404      */
    405     public void onCallRemoved(android.telecom.Call call) {
    406         call.unregisterCallback(mCallCallback);
    407     }
    408 
    409     public void onCanAddCallChanged(boolean canAddCall) {
    410         for (CanAddCallListener listener : mCanAddCallListeners) {
    411             listener.onCanAddCallChanged(canAddCall);
    412         }
    413     }
    414 
    415     /**
    416      * Called when there is a change to the call list.
    417      * Sets the In-Call state for the entire in-call app based on the information it gets from
    418      * CallList. Dispatches the in-call state to all listeners. Can trigger the creation or
    419      * destruction of the UI based on the states that is calculates.
    420      */
    421     @Override
    422     public void onCallListChange(CallList callList) {
    423         if (mInCallActivity != null && mInCallActivity.getCallCardFragment() != null &&
    424                 mInCallActivity.getCallCardFragment().isAnimating()) {
    425             mAwaitingCallListUpdate = true;
    426             return;
    427         }
    428         if (callList == null) {
    429             return;
    430         }
    431 
    432         mAwaitingCallListUpdate = false;
    433 
    434         InCallState newState = getPotentialStateFromCallList(callList);
    435         InCallState oldState = mInCallState;
    436         Log.d(this, "onCallListChange oldState= " + oldState + " newState=" + newState);
    437         newState = startOrFinishUi(newState);
    438         Log.d(this, "onCallListChange newState changed to " + newState);
    439 
    440         // Set the new state before announcing it to the world
    441         Log.i(this, "Phone switching state: " + oldState + " -> " + newState);
    442         mInCallState = newState;
    443 
    444         // notify listeners of new state
    445         for (InCallStateListener listener : mListeners) {
    446             Log.d(this, "Notify " + listener + " of state " + mInCallState.toString());
    447             listener.onStateChange(oldState, mInCallState, callList);
    448         }
    449 
    450         if (isActivityStarted()) {
    451             final boolean hasCall = callList.getActiveOrBackgroundCall() != null ||
    452                     callList.getOutgoingCall() != null;
    453             mInCallActivity.dismissKeyguard(hasCall);
    454         }
    455     }
    456 
    457     /**
    458      * Called when there is a new incoming call.
    459      *
    460      * @param call
    461      */
    462     @Override
    463     public void onIncomingCall(Call call) {
    464         InCallState newState = startOrFinishUi(InCallState.INCOMING);
    465         InCallState oldState = mInCallState;
    466 
    467         Log.i(this, "Phone switching state: " + oldState + " -> " + newState);
    468         mInCallState = newState;
    469 
    470         for (IncomingCallListener listener : mIncomingCallListeners) {
    471             listener.onIncomingCall(oldState, mInCallState, call);
    472         }
    473     }
    474 
    475     @Override
    476     public void onUpgradeToVideo(Call call) {
    477         //NO-OP
    478     }
    479     /**
    480      * Called when a call becomes disconnected. Called everytime an existing call
    481      * changes from being connected (incoming/outgoing/active) to disconnected.
    482      */
    483     @Override
    484     public void onDisconnect(Call call) {
    485         maybeShowErrorDialogOnDisconnect(call);
    486 
    487         // We need to do the run the same code as onCallListChange.
    488         onCallListChange(mCallList);
    489 
    490         if (isActivityStarted()) {
    491             mInCallActivity.dismissKeyguard(false);
    492         }
    493     }
    494 
    495     /**
    496      * Given the call list, return the state in which the in-call screen should be.
    497      */
    498     public InCallState getPotentialStateFromCallList(CallList callList) {
    499 
    500         InCallState newState = InCallState.NO_CALLS;
    501 
    502         if (callList == null) {
    503             return newState;
    504         }
    505         if (callList.getIncomingCall() != null) {
    506             newState = InCallState.INCOMING;
    507         } else if (callList.getWaitingForAccountCall() != null) {
    508             newState = InCallState.WAITING_FOR_ACCOUNT;
    509         } else if (callList.getPendingOutgoingCall() != null) {
    510             newState = InCallState.PENDING_OUTGOING;
    511         } else if (callList.getOutgoingCall() != null) {
    512             newState = InCallState.OUTGOING;
    513         } else if (callList.getActiveCall() != null ||
    514                 callList.getBackgroundCall() != null ||
    515                 callList.getDisconnectedCall() != null ||
    516                 callList.getDisconnectingCall() != null) {
    517             newState = InCallState.INCALL;
    518         }
    519 
    520         if (newState == InCallState.NO_CALLS) {
    521             if (mBoundAndWaitingForOutgoingCall) {
    522                 return InCallState.OUTGOING;
    523             }
    524         }
    525 
    526         return newState;
    527     }
    528 
    529     public boolean isBoundAndWaitingForOutgoingCall() {
    530         return mBoundAndWaitingForOutgoingCall;
    531     }
    532 
    533     public void setBoundAndWaitingForOutgoingCall(boolean isBound, PhoneAccountHandle handle) {
    534         // NOTE: It is possible for there to be a race and have handle become null before
    535         // the circular reveal starts. This should not cause any problems because CallCardFragment
    536         // should fallback to the actual call in the CallList at that point in time to determine
    537         // the theme color.
    538         Log.i(this, "setBoundAndWaitingForOutgoingCall: " + isBound);
    539         mBoundAndWaitingForOutgoingCall = isBound;
    540         mPendingPhoneAccountHandle = handle;
    541         if (isBound && mInCallState == InCallState.NO_CALLS) {
    542             mInCallState = InCallState.OUTGOING;
    543         }
    544     }
    545 
    546     @Override
    547     public void onCircularRevealComplete(FragmentManager fm) {
    548         if (mInCallActivity != null) {
    549             mInCallActivity.showCallCardFragment(true);
    550             mInCallActivity.getCallCardFragment().animateForNewOutgoingCall();
    551             CircularRevealFragment.endCircularReveal(mInCallActivity.getFragmentManager());
    552         }
    553     }
    554 
    555     public void onShrinkAnimationComplete() {
    556         if (mAwaitingCallListUpdate) {
    557             onCallListChange(mCallList);
    558         }
    559     }
    560 
    561     public void addIncomingCallListener(IncomingCallListener listener) {
    562         Preconditions.checkNotNull(listener);
    563         mIncomingCallListeners.add(listener);
    564     }
    565 
    566     public void removeIncomingCallListener(IncomingCallListener listener) {
    567         if (listener != null) {
    568             mIncomingCallListeners.remove(listener);
    569         }
    570     }
    571 
    572     public void addListener(InCallStateListener listener) {
    573         Preconditions.checkNotNull(listener);
    574         mListeners.add(listener);
    575     }
    576 
    577     public void removeListener(InCallStateListener listener) {
    578         if (listener != null) {
    579             mListeners.remove(listener);
    580         }
    581     }
    582 
    583     public void addDetailsListener(InCallDetailsListener listener) {
    584         Preconditions.checkNotNull(listener);
    585         mDetailsListeners.add(listener);
    586     }
    587 
    588     public void removeDetailsListener(InCallDetailsListener listener) {
    589         if (listener != null) {
    590             mDetailsListeners.remove(listener);
    591         }
    592     }
    593 
    594     public void addCanAddCallListener(CanAddCallListener listener) {
    595         Preconditions.checkNotNull(listener);
    596         mCanAddCallListeners.add(listener);
    597     }
    598 
    599     public void removeCanAddCallListener(CanAddCallListener listener) {
    600         if (listener != null) {
    601             mCanAddCallListeners.remove(listener);
    602         }
    603     }
    604 
    605     public void addOrientationListener(InCallOrientationListener listener) {
    606         Preconditions.checkNotNull(listener);
    607         mOrientationListeners.add(listener);
    608     }
    609 
    610     public void removeOrientationListener(InCallOrientationListener listener) {
    611         if (listener != null) {
    612             mOrientationListeners.remove(listener);
    613         }
    614     }
    615 
    616     public void addInCallEventListener(InCallEventListener listener) {
    617         Preconditions.checkNotNull(listener);
    618         mInCallEventListeners.add(listener);
    619     }
    620 
    621     public void removeInCallEventListener(InCallEventListener listener) {
    622         if (listener != null) {
    623             mInCallEventListeners.remove(listener);
    624         }
    625     }
    626 
    627     public ProximitySensor getProximitySensor() {
    628         return mProximitySensor;
    629     }
    630 
    631     public void handleAccountSelection(PhoneAccountHandle accountHandle, boolean setDefault) {
    632         if (mCallList != null) {
    633             Call call = mCallList.getWaitingForAccountCall();
    634             if (call != null) {
    635                 String callId = call.getId();
    636                 TelecomAdapter.getInstance().phoneAccountSelected(callId, accountHandle, setDefault);
    637             }
    638         }
    639     }
    640 
    641     public void cancelAccountSelection() {
    642         mAccountSelectionCancelled = true;
    643         if (mCallList != null) {
    644             Call call = mCallList.getWaitingForAccountCall();
    645             if (call != null) {
    646                 String callId = call.getId();
    647                 TelecomAdapter.getInstance().disconnectCall(callId);
    648             }
    649         }
    650     }
    651 
    652     /**
    653      * Hangs up any active or outgoing calls.
    654      */
    655     public void hangUpOngoingCall(Context context) {
    656         // By the time we receive this intent, we could be shut down and call list
    657         // could be null.  Bail in those cases.
    658         if (mCallList == null) {
    659             if (mStatusBarNotifier == null) {
    660                 // The In Call UI has crashed but the notification still stayed up. We should not
    661                 // come to this stage.
    662                 StatusBarNotifier.clearAllCallNotifications(context);
    663             }
    664             return;
    665         }
    666 
    667         Call call = mCallList.getOutgoingCall();
    668         if (call == null) {
    669             call = mCallList.getActiveOrBackgroundCall();
    670         }
    671 
    672         if (call != null) {
    673             TelecomAdapter.getInstance().disconnectCall(call.getId());
    674             call.setState(Call.State.DISCONNECTING);
    675             mCallList.onUpdate(call);
    676         }
    677     }
    678 
    679     /**
    680      * Answers any incoming call.
    681      */
    682     public void answerIncomingCall(Context context, int videoState) {
    683         // By the time we receive this intent, we could be shut down and call list
    684         // could be null.  Bail in those cases.
    685         if (mCallList == null) {
    686             StatusBarNotifier.clearAllCallNotifications(context);
    687             return;
    688         }
    689 
    690         Call call = mCallList.getIncomingCall();
    691         if (call != null) {
    692             TelecomAdapter.getInstance().answerCall(call.getId(), videoState);
    693             showInCall(false, false/* newOutgoingCall */);
    694         }
    695     }
    696 
    697     /**
    698      * Declines any incoming call.
    699      */
    700     public void declineIncomingCall(Context context) {
    701         // By the time we receive this intent, we could be shut down and call list
    702         // could be null.  Bail in those cases.
    703         if (mCallList == null) {
    704             StatusBarNotifier.clearAllCallNotifications(context);
    705             return;
    706         }
    707 
    708         Call call = mCallList.getIncomingCall();
    709         if (call != null) {
    710             TelecomAdapter.getInstance().rejectCall(call.getId(), false, null);
    711         }
    712     }
    713 
    714     public void acceptUpgradeRequest(int videoState, Context context) {
    715         Log.d(this, " acceptUpgradeRequest videoState " + videoState);
    716         // Bail if we have been shut down and the call list is null.
    717         if (mCallList == null) {
    718             StatusBarNotifier.clearAllCallNotifications(context);
    719             Log.e(this, " acceptUpgradeRequest mCallList is empty so returning");
    720             return;
    721         }
    722 
    723         Call call = mCallList.getVideoUpgradeRequestCall();
    724         if (call != null) {
    725             VideoProfile videoProfile = new VideoProfile(videoState);
    726             call.getVideoCall().sendSessionModifyResponse(videoProfile);
    727             call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST);
    728         }
    729     }
    730 
    731     public void declineUpgradeRequest(Context context) {
    732         Log.d(this, " declineUpgradeRequest");
    733         // Bail if we have been shut down and the call list is null.
    734         if (mCallList == null) {
    735             StatusBarNotifier.clearAllCallNotifications(context);
    736             Log.e(this, " declineUpgradeRequest mCallList is empty so returning");
    737             return;
    738         }
    739 
    740         Call call = mCallList.getVideoUpgradeRequestCall();
    741         if (call != null) {
    742             VideoProfile videoProfile =
    743                     new VideoProfile(call.getVideoState());
    744             call.getVideoCall().sendSessionModifyResponse(videoProfile);
    745             call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST);
    746         }
    747     }
    748 
    749     /**
    750      * Returns true if the incall app is the foreground application.
    751      */
    752     public boolean isShowingInCallUi() {
    753         return (isActivityStarted() && mInCallActivity.isVisible());
    754     }
    755 
    756     /**
    757      * Returns true if the activity has been created and is running.
    758      * Returns true as long as activity is not destroyed or finishing.  This ensures that we return
    759      * true even if the activity is paused (not in foreground).
    760      */
    761     public boolean isActivityStarted() {
    762         return (mInCallActivity != null &&
    763                 !mInCallActivity.isDestroyed() &&
    764                 !mInCallActivity.isFinishing());
    765     }
    766 
    767     public boolean isActivityPreviouslyStarted() {
    768         return mIsActivityPreviouslyStarted;
    769     }
    770 
    771     public boolean isChangingConfigurations() {
    772         return mIsChangingConfigurations;
    773     }
    774 
    775     /*package*/
    776     void updateIsChangingConfigurations() {
    777         mIsChangingConfigurations = false;
    778         if (mInCallActivity != null) {
    779             mIsChangingConfigurations = mInCallActivity.isChangingConfigurations();
    780         }
    781         Log.d(this, "IsChangingConfigurations=" + mIsChangingConfigurations);
    782     }
    783 
    784 
    785     /**
    786      * Called when the activity goes in/out of the foreground.
    787      */
    788     public void onUiShowing(boolean showing) {
    789         // We need to update the notification bar when we leave the UI because that
    790         // could trigger it to show again.
    791         if (mStatusBarNotifier != null) {
    792             mStatusBarNotifier.updateNotification(mInCallState, mCallList);
    793         }
    794 
    795         if (mProximitySensor != null) {
    796             mProximitySensor.onInCallShowing(showing);
    797         }
    798 
    799         Intent broadcastIntent = ObjectFactory.getUiReadyBroadcastIntent(mContext);
    800         if (broadcastIntent != null) {
    801             broadcastIntent.putExtra(EXTRA_FIRST_TIME_SHOWN, !mIsActivityPreviouslyStarted);
    802 
    803             if (showing) {
    804                 Log.d(this, "Sending sticky broadcast: ", broadcastIntent);
    805                 mContext.sendStickyBroadcast(broadcastIntent);
    806             } else {
    807                 Log.d(this, "Removing sticky broadcast: ", broadcastIntent);
    808                 mContext.removeStickyBroadcast(broadcastIntent);
    809             }
    810         }
    811 
    812         if (showing) {
    813             mIsActivityPreviouslyStarted = true;
    814         } else {
    815             updateIsChangingConfigurations();
    816         }
    817 
    818         for (InCallUiListener listener : mInCallUiListeners) {
    819             listener.onUiShowing(showing);
    820         }
    821     }
    822 
    823     public void addInCallUiListener(InCallUiListener listener) {
    824         mInCallUiListeners.add(listener);
    825     }
    826 
    827     public boolean removeInCallUiListener(InCallUiListener listener) {
    828         return mInCallUiListeners.remove(listener);
    829     }
    830 
    831     /*package*/
    832     void onActivityStarted() {
    833         Log.d(this, "onActivityStarted");
    834         notifyVideoPauseController(true);
    835     }
    836 
    837     /*package*/
    838     void onActivityStopped() {
    839         Log.d(this, "onActivityStopped");
    840         notifyVideoPauseController(false);
    841     }
    842 
    843     private void notifyVideoPauseController(boolean showing) {
    844         Log.d(this, "notifyVideoPauseController: mIsChangingConfigurations=" +
    845                 mIsChangingConfigurations);
    846         if (!mIsChangingConfigurations) {
    847             VideoPauseController.getInstance().onUiShowing(showing);
    848         }
    849     }
    850 
    851     /**
    852      * Brings the app into the foreground if possible.
    853      */
    854     public void bringToForeground(boolean showDialpad) {
    855         // Before we bring the incall UI to the foreground, we check to see if:
    856         // 1. It is not currently in the foreground
    857         // 2. We are in a state where we want to show the incall ui (i.e. there are calls to
    858         // be displayed)
    859         // If the activity hadn't actually been started previously, yet there are still calls
    860         // present (e.g. a call was accepted by a bluetooth or wired headset), we want to
    861         // bring it up the UI regardless.
    862         if (!isShowingInCallUi() && mInCallState != InCallState.NO_CALLS) {
    863             showInCall(showDialpad, false /* newOutgoingCall */);
    864         }
    865     }
    866 
    867     public void onPostDialCharWait(String callId, String chars) {
    868         if (isActivityStarted()) {
    869             mInCallActivity.showPostCharWaitDialog(callId, chars);
    870         }
    871     }
    872 
    873     /**
    874      * Handles the green CALL key while in-call.
    875      * @return true if we consumed the event.
    876      */
    877     public boolean handleCallKey() {
    878         Log.v(this, "handleCallKey");
    879 
    880         // The green CALL button means either "Answer", "Unhold", or
    881         // "Swap calls", or can be a no-op, depending on the current state
    882         // of the Phone.
    883 
    884         /**
    885          * INCOMING CALL
    886          */
    887         final CallList calls = mCallList;
    888         final Call incomingCall = calls.getIncomingCall();
    889         Log.v(this, "incomingCall: " + incomingCall);
    890 
    891         // (1) Attempt to answer a call
    892         if (incomingCall != null) {
    893             TelecomAdapter.getInstance().answerCall(
    894                     incomingCall.getId(), VideoProfile.STATE_AUDIO_ONLY);
    895             return true;
    896         }
    897 
    898         /**
    899          * STATE_ACTIVE CALL
    900          */
    901         final Call activeCall = calls.getActiveCall();
    902         if (activeCall != null) {
    903             // TODO: This logic is repeated from CallButtonPresenter.java. We should
    904             // consolidate this logic.
    905             final boolean canMerge = activeCall.can(
    906                     android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE);
    907             final boolean canSwap = activeCall.can(
    908                     android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE);
    909 
    910             Log.v(this, "activeCall: " + activeCall + ", canMerge: " + canMerge +
    911                     ", canSwap: " + canSwap);
    912 
    913             // (2) Attempt actions on conference calls
    914             if (canMerge) {
    915                 TelecomAdapter.getInstance().merge(activeCall.getId());
    916                 return true;
    917             } else if (canSwap) {
    918                 TelecomAdapter.getInstance().swap(activeCall.getId());
    919                 return true;
    920             }
    921         }
    922 
    923         /**
    924          * BACKGROUND CALL
    925          */
    926         final Call heldCall = calls.getBackgroundCall();
    927         if (heldCall != null) {
    928             // We have a hold call so presumeable it will always support HOLD...but
    929             // there is no harm in double checking.
    930             final boolean canHold = heldCall.can(android.telecom.Call.Details.CAPABILITY_HOLD);
    931 
    932             Log.v(this, "heldCall: " + heldCall + ", canHold: " + canHold);
    933 
    934             // (4) unhold call
    935             if (heldCall.getState() == Call.State.ONHOLD && canHold) {
    936                 TelecomAdapter.getInstance().unholdCall(heldCall.getId());
    937                 return true;
    938             }
    939         }
    940 
    941         // Always consume hard keys
    942         return true;
    943     }
    944 
    945     /**
    946      * A dialog could have prevented in-call screen from being previously finished.
    947      * This function checks to see if there should be any UI left and if not attempts
    948      * to tear down the UI.
    949      */
    950     public void onDismissDialog() {
    951         Log.i(this, "Dialog dismissed");
    952         if (mInCallState == InCallState.NO_CALLS) {
    953             attemptFinishActivity();
    954             attemptCleanup();
    955         }
    956     }
    957 
    958     /**
    959      * Toggles whether the application is in fullscreen mode or not.
    960      *
    961      * @return {@code true} if in-call is now in fullscreen mode.
    962      */
    963     public boolean toggleFullscreenMode() {
    964         mIsFullScreen = !mIsFullScreen;
    965         Log.v(this, "toggleFullscreenMode = " + mIsFullScreen);
    966         notifyFullscreenModeChange(mIsFullScreen);
    967         return mIsFullScreen;
    968     }
    969 
    970     /**
    971      * Changes the fullscreen mode of the in-call UI.
    972      *
    973      * @param isFullScreen {@code true} if in-call should be in fullscreen mode, {@code false}
    974      *                                 otherwise.
    975      */
    976     public void setFullScreen(boolean isFullScreen) {
    977         Log.v(this, "setFullScreen = " + isFullScreen);
    978         if (mIsFullScreen == isFullScreen) {
    979             Log.v(this, "setFullScreen ignored as already in that state.");
    980             return;
    981         }
    982         mIsFullScreen = isFullScreen;
    983         notifyFullscreenModeChange(mIsFullScreen);
    984     }
    985 
    986     /**
    987      * @return {@code true} if the in-call ui is currently in fullscreen mode, {@code false}
    988      * otherwise.
    989      */
    990     public boolean isFullscreen() {
    991         return mIsFullScreen;
    992     }
    993 
    994 
    995     /**
    996      * Called by the {@link VideoCallPresenter} to inform of a change in full screen video status.
    997      *
    998      * @param isFullscreenMode {@code True} if entering full screen mode.
    999      */
   1000     public void notifyFullscreenModeChange(boolean isFullscreenMode) {
   1001         for (InCallEventListener listener : mInCallEventListeners) {
   1002             listener.onFullscreenModeChanged(isFullscreenMode);
   1003         }
   1004     }
   1005 
   1006     /**
   1007      * For some disconnected causes, we show a dialog.  This calls into the activity to show
   1008      * the dialog if appropriate for the call.
   1009      */
   1010     private void maybeShowErrorDialogOnDisconnect(Call call) {
   1011         // For newly disconnected calls, we may want to show a dialog on specific error conditions
   1012         if (isActivityStarted() && call.getState() == Call.State.DISCONNECTED) {
   1013             if (call.getAccountHandle() == null && !call.isConferenceCall()) {
   1014                 setDisconnectCauseForMissingAccounts(call);
   1015             }
   1016             mInCallActivity.maybeShowErrorDialogOnDisconnect(call.getDisconnectCause());
   1017         }
   1018     }
   1019 
   1020     /**
   1021      * When the state of in-call changes, this is the first method to get called. It determines if
   1022      * the UI needs to be started or finished depending on the new state and does it.
   1023      */
   1024     private InCallState startOrFinishUi(InCallState newState) {
   1025         Log.d(this, "startOrFinishUi: " + mInCallState + " -> " + newState);
   1026 
   1027         // TODO: Consider a proper state machine implementation
   1028 
   1029         // If the state isn't changing we have already done any starting/stopping of activities in
   1030         // a previous pass...so lets cut out early
   1031         if (newState == mInCallState) {
   1032             return newState;
   1033         }
   1034 
   1035         // A new Incoming call means that the user needs to be notified of the the call (since
   1036         // it wasn't them who initiated it).  We do this through full screen notifications and
   1037         // happens indirectly through {@link StatusBarNotifier}.
   1038         //
   1039         // The process for incoming calls is as follows:
   1040         //
   1041         // 1) CallList          - Announces existence of new INCOMING call
   1042         // 2) InCallPresenter   - Gets announcement and calculates that the new InCallState
   1043         //                      - should be set to INCOMING.
   1044         // 3) InCallPresenter   - This method is called to see if we need to start or finish
   1045         //                        the app given the new state.
   1046         // 4) StatusBarNotifier - Listens to InCallState changes. InCallPresenter calls
   1047         //                        StatusBarNotifier explicitly to issue a FullScreen Notification
   1048         //                        that will either start the InCallActivity or show the user a
   1049         //                        top-level notification dialog if the user is in an immersive app.
   1050         //                        That notification can also start the InCallActivity.
   1051         // 5) InCallActivity    - Main activity starts up and at the end of its onCreate will
   1052         //                        call InCallPresenter::setActivity() to let the presenter
   1053         //                        know that start-up is complete.
   1054         //
   1055         //          [ AND NOW YOU'RE IN THE CALL. voila! ]
   1056         //
   1057         // Our app is started using a fullScreen notification.  We need to do this whenever
   1058         // we get an incoming call. Depending on the current context of the device, either a
   1059         // incoming call HUN or the actual InCallActivity will be shown.
   1060         final boolean startIncomingCallSequence = (InCallState.INCOMING == newState);
   1061 
   1062         // A dialog to show on top of the InCallUI to select a PhoneAccount
   1063         final boolean showAccountPicker = (InCallState.WAITING_FOR_ACCOUNT == newState);
   1064 
   1065         // A new outgoing call indicates that the user just now dialed a number and when that
   1066         // happens we need to display the screen immediately or show an account picker dialog if
   1067         // no default is set. However, if the main InCallUI is already visible, we do not want to
   1068         // re-initiate the start-up animation, so we do not need to do anything here.
   1069         //
   1070         // It is also possible to go into an intermediate state where the call has been initiated
   1071         // but Telecomm has not yet returned with the details of the call (handle, gateway, etc.).
   1072         // This pending outgoing state can also launch the call screen.
   1073         //
   1074         // This is different from the incoming call sequence because we do not need to shock the
   1075         // user with a top-level notification.  Just show the call UI normally.
   1076         final boolean mainUiNotVisible = !isShowingInCallUi() || !getCallCardFragmentVisible();
   1077         boolean showCallUi = InCallState.OUTGOING == newState && mainUiNotVisible;
   1078 
   1079         // Direct transition from PENDING_OUTGOING -> INCALL means that there was an error in the
   1080         // outgoing call process, so the UI should be brought up to show an error dialog.
   1081         showCallUi |= (InCallState.PENDING_OUTGOING == mInCallState
   1082                 && InCallState.INCALL == newState && !isActivityStarted());
   1083 
   1084         // Another exception - InCallActivity is in charge of disconnecting a call with no
   1085         // valid accounts set. Bring the UI up if this is true for the current pending outgoing
   1086         // call so that:
   1087         // 1) The call can be disconnected correctly
   1088         // 2) The UI comes up and correctly displays the error dialog.
   1089         // TODO: Remove these special case conditions by making InCallPresenter a true state
   1090         // machine. Telecom should also be the component responsible for disconnecting a call
   1091         // with no valid accounts.
   1092         showCallUi |= InCallState.PENDING_OUTGOING == newState && mainUiNotVisible
   1093                 && isCallWithNoValidAccounts(mCallList.getPendingOutgoingCall());
   1094 
   1095         // The only time that we have an instance of mInCallActivity and it isn't started is
   1096         // when it is being destroyed.  In that case, lets avoid bringing up another instance of
   1097         // the activity.  When it is finally destroyed, we double check if we should bring it back
   1098         // up so we aren't going to lose anything by avoiding a second startup here.
   1099         boolean activityIsFinishing = mInCallActivity != null && !isActivityStarted();
   1100         if (activityIsFinishing) {
   1101             Log.i(this, "Undo the state change: " + newState + " -> " + mInCallState);
   1102             return mInCallState;
   1103         }
   1104 
   1105         if (showCallUi || showAccountPicker) {
   1106             Log.i(this, "Start in call UI");
   1107             showInCall(false /* showDialpad */, !showAccountPicker /* newOutgoingCall */);
   1108         } else if (startIncomingCallSequence) {
   1109             Log.i(this, "Start Full Screen in call UI");
   1110 
   1111             // We're about the bring up the in-call UI for an incoming call. If we still have
   1112             // dialogs up, we need to clear them out before showing incoming screen.
   1113             if (isActivityStarted()) {
   1114                 mInCallActivity.dismissPendingDialogs();
   1115             }
   1116             if (!startUi(newState)) {
   1117                 // startUI refused to start the UI. This indicates that it needed to restart the
   1118                 // activity.  When it finally restarts, it will call us back, so we do not actually
   1119                 // change the state yet (we return mInCallState instead of newState).
   1120                 return mInCallState;
   1121             }
   1122         } else if (newState == InCallState.NO_CALLS) {
   1123             // The new state is the no calls state.  Tear everything down.
   1124             attemptFinishActivity();
   1125             attemptCleanup();
   1126         }
   1127 
   1128         return newState;
   1129     }
   1130 
   1131     /**
   1132      * Determines whether or not a call has no valid phone accounts that can be used to make the
   1133      * call with. Emergency calls do not require a phone account.
   1134      *
   1135      * @param call to check accounts for.
   1136      * @return {@code true} if the call has no call capable phone accounts set, {@code false} if
   1137      * the call contains a phone account that could be used to initiate it with, or is an emergency
   1138      * call.
   1139      */
   1140     public static boolean isCallWithNoValidAccounts(Call call) {
   1141         if (call != null && !isEmergencyCall(call)) {
   1142             Bundle extras = call.getIntentExtras();
   1143 
   1144             if (extras == null) {
   1145                 extras = EMPTY_EXTRAS;
   1146             }
   1147 
   1148             final List<PhoneAccountHandle> phoneAccountHandles = extras
   1149                     .getParcelableArrayList(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS);
   1150 
   1151             if ((call.getAccountHandle() == null &&
   1152                     (phoneAccountHandles == null || phoneAccountHandles.isEmpty()))) {
   1153                 Log.i(InCallPresenter.getInstance(), "No valid accounts for call " + call);
   1154                 return true;
   1155             }
   1156         }
   1157         return false;
   1158     }
   1159 
   1160     private static boolean isEmergencyCall(Call call) {
   1161         final Uri handle = call.getHandle();
   1162         if (handle == null) {
   1163             return false;
   1164         }
   1165         return PhoneNumberUtils.isEmergencyNumber(handle.getSchemeSpecificPart());
   1166     }
   1167 
   1168     /**
   1169      * Sets the DisconnectCause for a call that was disconnected because it was missing a
   1170      * PhoneAccount or PhoneAccounts to select from.
   1171      * @param call
   1172      */
   1173     private void setDisconnectCauseForMissingAccounts(Call call) {
   1174         android.telecom.Call telecomCall = call.getTelecommCall();
   1175 
   1176         Bundle extras = telecomCall.getDetails().getIntentExtras();
   1177         // Initialize the extras bundle to avoid NPE
   1178         if (extras == null) {
   1179             extras = new Bundle();
   1180         }
   1181 
   1182         final List<PhoneAccountHandle> phoneAccountHandles = extras.getParcelableArrayList(
   1183                 android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS);
   1184 
   1185         if (phoneAccountHandles == null || phoneAccountHandles.isEmpty()) {
   1186             String scheme = telecomCall.getDetails().getHandle().getScheme();
   1187             final String errorMsg = PhoneAccount.SCHEME_TEL.equals(scheme) ?
   1188                     mContext.getString(R.string.callFailed_simError) :
   1189                         mContext.getString(R.string.incall_error_supp_service_unknown);
   1190             DisconnectCause disconnectCause =
   1191                     new DisconnectCause(DisconnectCause.ERROR, null, errorMsg, errorMsg);
   1192             call.setDisconnectCause(disconnectCause);
   1193         }
   1194     }
   1195 
   1196     private boolean startUi(InCallState inCallState) {
   1197         boolean isCallWaiting = mCallList.getActiveCall() != null &&
   1198                 mCallList.getIncomingCall() != null;
   1199 
   1200         // If the screen is off, we need to make sure it gets turned on for incoming calls.
   1201         // This normally works just fine thanks to FLAG_TURN_SCREEN_ON but that only works
   1202         // when the activity is first created. Therefore, to ensure the screen is turned on
   1203         // for the call waiting case, we finish() the current activity and start a new one.
   1204         // There should be no jank from this since the screen is already off and will remain so
   1205         // until our new activity is up.
   1206 
   1207         if (isCallWaiting) {
   1208             if (mProximitySensor.isScreenReallyOff() && isActivityStarted()) {
   1209                 Log.i(this, "Restarting InCallActivity to turn screen on for call waiting");
   1210                 mInCallActivity.finish();
   1211                 // When the activity actually finishes, we will start it again if there are
   1212                 // any active calls, so we do not need to start it explicitly here. Note, we
   1213                 // actually get called back on this function to restart it.
   1214 
   1215                 // We return false to indicate that we did not actually start the UI.
   1216                 return false;
   1217             } else {
   1218                 showInCall(false, false);
   1219             }
   1220         } else {
   1221             mStatusBarNotifier.updateNotification(inCallState, mCallList);
   1222         }
   1223         return true;
   1224     }
   1225 
   1226     /**
   1227      * Checks to see if both the UI is gone and the service is disconnected. If so, tear it all
   1228      * down.
   1229      */
   1230     private void attemptCleanup() {
   1231         boolean shouldCleanup = (mInCallActivity == null && !mServiceConnected &&
   1232                 mInCallState == InCallState.NO_CALLS);
   1233         Log.i(this, "attemptCleanup? " + shouldCleanup);
   1234 
   1235         if (shouldCleanup) {
   1236             mIsActivityPreviouslyStarted = false;
   1237             mIsChangingConfigurations = false;
   1238 
   1239             // blow away stale contact info so that we get fresh data on
   1240             // the next set of calls
   1241             if (mContactInfoCache != null) {
   1242                 mContactInfoCache.clearCache();
   1243             }
   1244             mContactInfoCache = null;
   1245 
   1246             if (mProximitySensor != null) {
   1247                 removeListener(mProximitySensor);
   1248                 mProximitySensor.tearDown();
   1249             }
   1250             mProximitySensor = null;
   1251 
   1252             mAudioModeProvider = null;
   1253 
   1254             if (mStatusBarNotifier != null) {
   1255                 removeListener(mStatusBarNotifier);
   1256             }
   1257             mStatusBarNotifier = null;
   1258 
   1259             if (mCallList != null) {
   1260                 mCallList.removeListener(this);
   1261             }
   1262             mCallList = null;
   1263 
   1264             mContext = null;
   1265             mInCallActivity = null;
   1266 
   1267             mListeners.clear();
   1268             mIncomingCallListeners.clear();
   1269             mDetailsListeners.clear();
   1270             mCanAddCallListeners.clear();
   1271             mOrientationListeners.clear();
   1272             mInCallEventListeners.clear();
   1273 
   1274             Log.d(this, "Finished InCallPresenter.CleanUp");
   1275         }
   1276     }
   1277 
   1278     public void showInCall(final boolean showDialpad, final boolean newOutgoingCall) {
   1279         Log.i(this, "Showing InCallActivity");
   1280         mContext.startActivity(getInCallIntent(showDialpad, newOutgoingCall));
   1281     }
   1282 
   1283     public void onServiceBind() {
   1284         mServiceBound = true;
   1285     }
   1286 
   1287     public void onServiceUnbind() {
   1288         InCallPresenter.getInstance().setBoundAndWaitingForOutgoingCall(false, null);
   1289         mServiceBound = false;
   1290     }
   1291 
   1292     public boolean isServiceBound() {
   1293         return mServiceBound;
   1294     }
   1295 
   1296     public void maybeStartRevealAnimation(Intent intent) {
   1297         if (intent == null || mInCallActivity != null) {
   1298             return;
   1299         }
   1300         final Bundle extras = intent.getBundleExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS);
   1301         if (extras == null) {
   1302             // Incoming call, just show the in-call UI directly.
   1303             return;
   1304         }
   1305 
   1306         if (extras.containsKey(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS)) {
   1307             // Account selection dialog will show up so don't show the animation.
   1308             return;
   1309         }
   1310 
   1311         final PhoneAccountHandle accountHandle =
   1312                 intent.getParcelableExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
   1313         final Point touchPoint = extras.getParcelable(TouchPointManager.TOUCH_POINT);
   1314 
   1315         InCallPresenter.getInstance().setBoundAndWaitingForOutgoingCall(true, accountHandle);
   1316 
   1317         final Intent incallIntent = getInCallIntent(false, true);
   1318         incallIntent.putExtra(TouchPointManager.TOUCH_POINT, touchPoint);
   1319         mContext.startActivity(incallIntent);
   1320     }
   1321 
   1322     public Intent getInCallIntent(boolean showDialpad, boolean newOutgoingCall) {
   1323         final Intent intent = new Intent(Intent.ACTION_MAIN, null);
   1324         intent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION | Intent.FLAG_ACTIVITY_NEW_TASK);
   1325 
   1326         intent.setClass(mContext, InCallActivity.class);
   1327         if (showDialpad) {
   1328             intent.putExtra(InCallActivity.SHOW_DIALPAD_EXTRA, true);
   1329         }
   1330         intent.putExtra(InCallActivity.NEW_OUTGOING_CALL_EXTRA, newOutgoingCall);
   1331         return intent;
   1332     }
   1333 
   1334     /**
   1335      * Retrieves the current in-call camera manager instance, creating if necessary.
   1336      *
   1337      * @return The {@link InCallCameraManager}.
   1338      */
   1339     public InCallCameraManager getInCallCameraManager() {
   1340         synchronized(this) {
   1341             if (mInCallCameraManager == null) {
   1342                 mInCallCameraManager = new InCallCameraManager(mContext);
   1343             }
   1344 
   1345             return mInCallCameraManager;
   1346         }
   1347     }
   1348 
   1349     /**
   1350      * Handles changes to the device rotation.
   1351      *
   1352      * @param rotation The device rotation (one of: {@link Surface#ROTATION_0},
   1353      *      {@link Surface#ROTATION_90}, {@link Surface#ROTATION_180},
   1354      *      {@link Surface#ROTATION_270}).
   1355      */
   1356     public void onDeviceRotationChange(int rotation) {
   1357         Log.d(this, "onDeviceRotationChange: rotation=" + rotation);
   1358         // First translate to rotation in degrees.
   1359         if (mCallList != null) {
   1360             mCallList.notifyCallsOfDeviceRotation(toRotationAngle(rotation));
   1361         } else {
   1362             Log.w(this, "onDeviceRotationChange: CallList is null.");
   1363         }
   1364     }
   1365 
   1366     /**
   1367      * Converts rotation constants to rotation in degrees.
   1368      * @param rotation Rotation constants (one of: {@link Surface#ROTATION_0},
   1369      *      {@link Surface#ROTATION_90}, {@link Surface#ROTATION_180},
   1370      *      {@link Surface#ROTATION_270}).
   1371      */
   1372     public static int toRotationAngle(int rotation) {
   1373         int rotationAngle;
   1374         switch (rotation) {
   1375             case Surface.ROTATION_0:
   1376                 rotationAngle = 0;
   1377                 break;
   1378             case Surface.ROTATION_90:
   1379                 rotationAngle = 90;
   1380                 break;
   1381             case Surface.ROTATION_180:
   1382                 rotationAngle = 180;
   1383                 break;
   1384             case Surface.ROTATION_270:
   1385                 rotationAngle = 270;
   1386                 break;
   1387             default:
   1388                 rotationAngle = 0;
   1389         }
   1390         return rotationAngle;
   1391     }
   1392 
   1393     /**
   1394      * Notifies listeners of changes in orientation (e.g. portrait/landscape).
   1395      *
   1396      * @param orientation The orientation of the device (one of: {@link Surface#ROTATION_0},
   1397      *      {@link Surface#ROTATION_90}, {@link Surface#ROTATION_180},
   1398      *      {@link Surface#ROTATION_270}).
   1399      */
   1400     public void onDeviceOrientationChange(int orientation) {
   1401         for (InCallOrientationListener listener : mOrientationListeners) {
   1402             listener.onDeviceOrientationChanged(orientation);
   1403         }
   1404     }
   1405 
   1406     /**
   1407      * Configures the in-call UI activity so it can change orientations or not.
   1408      *
   1409      * @param allowOrientationChange {@code True} if the in-call UI can change between portrait
   1410      *      and landscape.  {@Code False} if the in-call UI should be locked in portrait.
   1411      */
   1412     public void setInCallAllowsOrientationChange(boolean allowOrientationChange) {
   1413         if (mInCallActivity == null) {
   1414             Log.e(this, "InCallActivity is null. Can't set requested orientation.");
   1415             return;
   1416         }
   1417 
   1418         if (!allowOrientationChange) {
   1419             mInCallActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
   1420         } else {
   1421             mInCallActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
   1422         }
   1423     }
   1424 
   1425     public void enableScreenTimeout(boolean enable) {
   1426         Log.v(this, "enableScreenTimeout: value=" + enable);
   1427         if (mInCallActivity == null) {
   1428             Log.e(this, "enableScreenTimeout: InCallActivity is null.");
   1429             return;
   1430         }
   1431 
   1432         final Window window = mInCallActivity.getWindow();
   1433         if (enable) {
   1434             window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
   1435         } else {
   1436             window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
   1437         }
   1438     }
   1439 
   1440     /**
   1441      * Returns the space available beside the call card.
   1442      *
   1443      * @return The space beside the call card.
   1444      */
   1445     public float getSpaceBesideCallCard() {
   1446         if (mInCallActivity != null && mInCallActivity.getCallCardFragment() != null) {
   1447             return mInCallActivity.getCallCardFragment().getSpaceBesideCallCard();
   1448         }
   1449         return 0;
   1450     }
   1451 
   1452     /**
   1453      * Returns whether the call card fragment is currently visible.
   1454      *
   1455      * @return True if the call card fragment is visible.
   1456      */
   1457     public boolean getCallCardFragmentVisible() {
   1458         if (mInCallActivity != null && mInCallActivity.getCallCardFragment() != null) {
   1459             return mInCallActivity.getCallCardFragment().isVisible();
   1460         }
   1461         return false;
   1462     }
   1463 
   1464     /**
   1465      * Hides or shows the conference manager fragment.
   1466      *
   1467      * @param show {@code true} if the conference manager should be shown, {@code false} if it
   1468      *                         should be hidden.
   1469      */
   1470     public void showConferenceCallManager(boolean show) {
   1471         if (mInCallActivity == null) {
   1472             return;
   1473         }
   1474 
   1475         mInCallActivity.showConferenceFragment(show);
   1476     }
   1477 
   1478     /**
   1479      * @return True if the application is currently running in a right-to-left locale.
   1480      */
   1481     public static boolean isRtl() {
   1482         return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) ==
   1483                 View.LAYOUT_DIRECTION_RTL;
   1484     }
   1485 
   1486     /**
   1487      * Extract background color from call object. The theme colors will include a primary color
   1488      * and a secondary color.
   1489      */
   1490     public void setThemeColors() {
   1491         // This method will set the background to default if the color is PhoneAccount.NO_COLOR.
   1492         mThemeColors = getColorsFromCall(mCallList.getFirstCall());
   1493 
   1494         if (mInCallActivity == null) {
   1495             return;
   1496         }
   1497 
   1498         final Resources resources = mInCallActivity.getResources();
   1499         final int color;
   1500         if (resources.getBoolean(R.bool.is_layout_landscape)) {
   1501             color = resources.getColor(R.color.statusbar_background_color, null);
   1502         } else {
   1503             color = mThemeColors.mSecondaryColor;
   1504         }
   1505 
   1506         mInCallActivity.getWindow().setStatusBarColor(color);
   1507         final TaskDescription td = new TaskDescription(
   1508                 resources.getString(R.string.notification_ongoing_call), null, color);
   1509         mInCallActivity.setTaskDescription(td);
   1510     }
   1511 
   1512     /**
   1513      * @return A palette for colors to display in the UI.
   1514      */
   1515     public MaterialPalette getThemeColors() {
   1516         return mThemeColors;
   1517     }
   1518 
   1519     private MaterialPalette getColorsFromCall(Call call) {
   1520         if (call == null) {
   1521             return getColorsFromPhoneAccountHandle(mPendingPhoneAccountHandle);
   1522         } else {
   1523             return getColorsFromPhoneAccountHandle(call.getAccountHandle());
   1524         }
   1525     }
   1526 
   1527     private MaterialPalette getColorsFromPhoneAccountHandle(PhoneAccountHandle phoneAccountHandle) {
   1528         int highlightColor = PhoneAccount.NO_HIGHLIGHT_COLOR;
   1529         if (phoneAccountHandle != null) {
   1530             final TelecomManager tm = getTelecomManager();
   1531 
   1532             if (tm != null) {
   1533                 final PhoneAccount account = tm.getPhoneAccount(phoneAccountHandle);
   1534                 // For single-sim devices, there will be no selected highlight color, so the phone
   1535                 // account will default to NO_HIGHLIGHT_COLOR.
   1536                 if (account != null) {
   1537                     highlightColor = account.getHighlightColor();
   1538                 }
   1539             }
   1540         }
   1541         return new InCallUIMaterialColorMapUtils(
   1542                 mContext.getResources()).calculatePrimaryAndSecondaryColor(highlightColor);
   1543     }
   1544 
   1545     /**
   1546      * @return An instance of TelecomManager.
   1547      */
   1548     public TelecomManager getTelecomManager() {
   1549         if (mTelecomManager == null) {
   1550             mTelecomManager = (TelecomManager)
   1551                     mContext.getSystemService(Context.TELECOM_SERVICE);
   1552         }
   1553         return mTelecomManager;
   1554     }
   1555 
   1556     InCallActivity getActivity() {
   1557         return mInCallActivity;
   1558     }
   1559 
   1560     AnswerPresenter getAnswerPresenter() {
   1561         return mAnswerPresenter;
   1562     }
   1563 
   1564     /**
   1565      * Private constructor. Must use getInstance() to get this singleton.
   1566      */
   1567     private InCallPresenter() {
   1568     }
   1569 
   1570     /**
   1571      * All the main states of InCallActivity.
   1572      */
   1573     public enum InCallState {
   1574         // InCall Screen is off and there are no calls
   1575         NO_CALLS,
   1576 
   1577         // Incoming-call screen is up
   1578         INCOMING,
   1579 
   1580         // In-call experience is showing
   1581         INCALL,
   1582 
   1583         // Waiting for user input before placing outgoing call
   1584         WAITING_FOR_ACCOUNT,
   1585 
   1586         // UI is starting up but no call has been initiated yet.
   1587         // The UI is waiting for Telecomm to respond.
   1588         PENDING_OUTGOING,
   1589 
   1590         // User is dialing out
   1591         OUTGOING;
   1592 
   1593         public boolean isIncoming() {
   1594             return (this == INCOMING);
   1595         }
   1596 
   1597         public boolean isConnectingOrConnected() {
   1598             return (this == INCOMING ||
   1599                     this == OUTGOING ||
   1600                     this == INCALL);
   1601         }
   1602     }
   1603 
   1604     /**
   1605      * Interface implemented by classes that need to know about the InCall State.
   1606      */
   1607     public interface InCallStateListener {
   1608         // TODO: Enhance state to contain the call objects instead of passing CallList
   1609         public void onStateChange(InCallState oldState, InCallState newState, CallList callList);
   1610     }
   1611 
   1612     public interface IncomingCallListener {
   1613         public void onIncomingCall(InCallState oldState, InCallState newState, Call call);
   1614     }
   1615 
   1616     public interface CanAddCallListener {
   1617         public void onCanAddCallChanged(boolean canAddCall);
   1618     }
   1619 
   1620     public interface InCallDetailsListener {
   1621         public void onDetailsChanged(Call call, android.telecom.Call.Details details);
   1622     }
   1623 
   1624     public interface InCallOrientationListener {
   1625         public void onDeviceOrientationChanged(int orientation);
   1626     }
   1627 
   1628     /**
   1629      * Interface implemented by classes that need to know about events which occur within the
   1630      * In-Call UI.  Used as a means of communicating between fragments that make up the UI.
   1631      */
   1632     public interface InCallEventListener {
   1633         public void onFullscreenModeChanged(boolean isFullscreenMode);
   1634     }
   1635 
   1636     public interface InCallUiListener {
   1637         void onUiShowing(boolean showing);
   1638     }
   1639 }
   1640