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