Home | History | Annotate | Download | only in incallui
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.incallui;
     18 
     19 import android.Manifest;
     20 import android.app.PendingIntent;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.content.pm.ActivityInfo;
     24 import android.os.Bundle;
     25 import android.telecom.DisconnectCause;
     26 import android.telecom.PhoneAccount;
     27 import android.telecom.PhoneCapabilities;
     28 import android.telecom.Phone;
     29 import android.telecom.PhoneAccountHandle;
     30 import android.telecom.VideoProfile;
     31 import android.text.TextUtils;
     32 import android.view.Surface;
     33 import android.view.View;
     34 
     35 import com.google.common.base.Preconditions;
     36 
     37 import com.android.incalluibind.ObjectFactory;
     38 
     39 import java.util.Collections;
     40 import java.util.List;
     41 import java.util.Locale;
     42 import java.util.Set;
     43 import java.util.concurrent.ConcurrentHashMap;
     44 import java.util.concurrent.CopyOnWriteArrayList;
     45 
     46 /**
     47  * Takes updates from the CallList and notifies the InCallActivity (UI)
     48  * of the changes.
     49  * Responsible for starting the activity for a new call and finishing the activity when all calls
     50  * are disconnected.
     51  * Creates and manages the in-call state and provides a listener pattern for the presenters
     52  * that want to listen in on the in-call state changes.
     53  * TODO: This class has become more of a state machine at this point.  Consider renaming.
     54  */
     55 public class InCallPresenter implements CallList.Listener, InCallPhoneListener {
     56 
     57     private static final String EXTRA_FIRST_TIME_SHOWN =
     58             "com.android.incallui.intent.extra.FIRST_TIME_SHOWN";
     59 
     60     private static InCallPresenter sInCallPresenter;
     61 
     62     /**
     63      * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
     64      * load factor before resizing, 1 means we only expect a single thread to
     65      * access the map so make only a single shard
     66      */
     67     private final Set<InCallStateListener> mListeners = Collections.newSetFromMap(
     68             new ConcurrentHashMap<InCallStateListener, Boolean>(8, 0.9f, 1));
     69     private final List<IncomingCallListener> mIncomingCallListeners = new CopyOnWriteArrayList<>();
     70     private final Set<InCallDetailsListener> mDetailsListeners = Collections.newSetFromMap(
     71             new ConcurrentHashMap<InCallDetailsListener, Boolean>(8, 0.9f, 1));
     72     private final Set<InCallOrientationListener> mOrientationListeners = Collections.newSetFromMap(
     73             new ConcurrentHashMap<InCallOrientationListener, Boolean>(8, 0.9f, 1));
     74     private final Set<InCallEventListener> mInCallEventListeners = Collections.newSetFromMap(
     75             new ConcurrentHashMap<InCallEventListener, Boolean>(8, 0.9f, 1));
     76 
     77     private AudioModeProvider mAudioModeProvider;
     78     private StatusBarNotifier mStatusBarNotifier;
     79     private ContactInfoCache mContactInfoCache;
     80     private Context mContext;
     81     private CallList mCallList;
     82     private InCallActivity mInCallActivity;
     83     private InCallState mInCallState = InCallState.NO_CALLS;
     84     private ProximitySensor mProximitySensor;
     85     private boolean mServiceConnected = false;
     86     private boolean mAccountSelectionCancelled = false;
     87     private InCallCameraManager mInCallCameraManager = null;
     88 
     89     private final Phone.Listener mPhoneListener = new Phone.Listener() {
     90         @Override
     91         public void onBringToForeground(Phone phone, boolean showDialpad) {
     92             Log.i(this, "Bringing UI to foreground.");
     93             bringToForeground(showDialpad);
     94         }
     95         @Override
     96         public void onCallAdded(Phone phone, android.telecom.Call call) {
     97             call.addListener(mCallListener);
     98         }
     99         @Override
    100         public void onCallRemoved(Phone phone, android.telecom.Call call) {
    101             call.removeListener(mCallListener);
    102         }
    103     };
    104 
    105     private final android.telecom.Call.Listener mCallListener =
    106             new android.telecom.Call.Listener() {
    107         @Override
    108         public void onPostDialWait(android.telecom.Call call, String remainingPostDialSequence) {
    109             onPostDialCharWait(
    110                     CallList.getInstance().getCallByTelecommCall(call).getId(),
    111                     remainingPostDialSequence);
    112         }
    113 
    114         @Override
    115         public void onDetailsChanged(android.telecom.Call call,
    116                 android.telecom.Call.Details details) {
    117             for (InCallDetailsListener listener : mDetailsListeners) {
    118                 listener.onDetailsChanged(CallList.getInstance().getCallByTelecommCall(call),
    119                         details);
    120             }
    121         }
    122 
    123         @Override
    124         public void onConferenceableCallsChanged(
    125                 android.telecom.Call call, List<android.telecom.Call> conferenceableCalls) {
    126             Log.i(this, "onConferenceableCallsChanged: " + call);
    127             for (InCallDetailsListener listener : mDetailsListeners) {
    128                 listener.onDetailsChanged(CallList.getInstance().getCallByTelecommCall(call),
    129                         call.getDetails());
    130             }
    131         }
    132     };
    133 
    134     /**
    135      * Is true when the activity has been previously started. Some code needs to know not just if
    136      * the activity is currently up, but if it had been previously shown in foreground for this
    137      * in-call session (e.g., StatusBarNotifier). This gets reset when the session ends in the
    138      * tear-down method.
    139      */
    140     private boolean mIsActivityPreviouslyStarted = false;
    141 
    142     private Phone mPhone;
    143 
    144     public static synchronized InCallPresenter getInstance() {
    145         if (sInCallPresenter == null) {
    146             sInCallPresenter = new InCallPresenter();
    147         }
    148         return sInCallPresenter;
    149     }
    150 
    151     @Override
    152     public void setPhone(Phone phone) {
    153         mPhone = phone;
    154         mPhone.addListener(mPhoneListener);
    155     }
    156 
    157     @Override
    158     public void clearPhone() {
    159         mPhone.removeListener(mPhoneListener);
    160         mPhone = null;
    161     }
    162 
    163     public InCallState getInCallState() {
    164         return mInCallState;
    165     }
    166 
    167     public CallList getCallList() {
    168         return mCallList;
    169     }
    170 
    171     public void setUp(Context context, CallList callList, AudioModeProvider audioModeProvider) {
    172         if (mServiceConnected) {
    173             Log.i(this, "New service connection replacing existing one.");
    174             // retain the current resources, no need to create new ones.
    175             Preconditions.checkState(context == mContext);
    176             Preconditions.checkState(callList == mCallList);
    177             Preconditions.checkState(audioModeProvider == mAudioModeProvider);
    178             return;
    179         }
    180 
    181         Preconditions.checkNotNull(context);
    182         mContext = context;
    183 
    184         mContactInfoCache = ContactInfoCache.getInstance(context);
    185 
    186         mStatusBarNotifier = new StatusBarNotifier(context, mContactInfoCache);
    187         addListener(mStatusBarNotifier);
    188 
    189         mAudioModeProvider = audioModeProvider;
    190 
    191         mProximitySensor = new ProximitySensor(context, mAudioModeProvider);
    192         addListener(mProximitySensor);
    193 
    194         mCallList = callList;
    195 
    196         // This only gets called by the service so this is okay.
    197         mServiceConnected = true;
    198 
    199         // The final thing we do in this set up is add ourselves as a listener to CallList.  This
    200         // will kick off an update and the whole process can start.
    201         mCallList.addListener(this);
    202 
    203         Log.d(this, "Finished InCallPresenter.setUp");
    204     }
    205 
    206     /**
    207      * Called when the telephony service has disconnected from us.  This will happen when there are
    208      * no more active calls. However, we may still want to continue showing the UI for
    209      * certain cases like showing "Call Ended".
    210      * What we really want is to wait for the activity and the service to both disconnect before we
    211      * tear things down. This method sets a serviceConnected boolean and calls a secondary method
    212      * that performs the aforementioned logic.
    213      */
    214     public void tearDown() {
    215         Log.d(this, "tearDown");
    216         mServiceConnected = false;
    217         attemptCleanup();
    218     }
    219 
    220     private void attemptFinishActivity() {
    221         final boolean doFinish = (mInCallActivity != null && isActivityStarted());
    222         Log.i(this, "Hide in call UI: " + doFinish);
    223 
    224         if (doFinish) {
    225             mInCallActivity.finish();
    226 
    227             if (mAccountSelectionCancelled) {
    228                 // This finish is a result of account selection cancellation
    229                 // do not include activity ending transition
    230                 mInCallActivity.overridePendingTransition(0, 0);
    231             }
    232         }
    233     }
    234 
    235     /**
    236      * Called when the UI begins or ends. Starts the callstate callbacks if the UI just began.
    237      * Attempts to tear down everything if the UI just ended. See #tearDown for more insight on
    238      * the tear-down process.
    239      */
    240     public void setActivity(InCallActivity inCallActivity) {
    241         boolean updateListeners = false;
    242         boolean doAttemptCleanup = false;
    243 
    244         if (inCallActivity != null) {
    245             if (mInCallActivity == null) {
    246                 updateListeners = true;
    247                 Log.i(this, "UI Initialized");
    248             } else if (mInCallActivity != inCallActivity) {
    249                 Log.wtf(this, "Setting a second activity before destroying the first.");
    250             } else {
    251                 // since setActivity is called onStart(), it can be called multiple times.
    252                 // This is fine and ignorable, but we do not want to update the world every time
    253                 // this happens (like going to/from background) so we do not set updateListeners.
    254             }
    255 
    256             mInCallActivity = inCallActivity;
    257 
    258             // By the time the UI finally comes up, the call may already be disconnected.
    259             // If that's the case, we may need to show an error dialog.
    260             if (mCallList != null && mCallList.getDisconnectedCall() != null) {
    261                 maybeShowErrorDialogOnDisconnect(mCallList.getDisconnectedCall());
    262             }
    263 
    264             // When the UI comes up, we need to first check the in-call state.
    265             // If we are showing NO_CALLS, that means that a call probably connected and
    266             // then immediately disconnected before the UI was able to come up.
    267             // If we dont have any calls, start tearing down the UI instead.
    268             // NOTE: This code relies on {@link #mInCallActivity} being set so we run it after
    269             // it has been set.
    270             if (mInCallState == InCallState.NO_CALLS) {
    271                 Log.i(this, "UI Intialized, but no calls left.  shut down.");
    272                 attemptFinishActivity();
    273                 return;
    274             }
    275         } else {
    276             Log.i(this, "UI Destroyed)");
    277             updateListeners = true;
    278             mInCallActivity = null;
    279 
    280             // We attempt cleanup for the destroy case but only after we recalculate the state
    281             // to see if we need to come back up or stay shut down. This is why we do the cleanup
    282             // after the call to onCallListChange() instead of directly here.
    283             doAttemptCleanup = true;
    284         }
    285 
    286         // Messages can come from the telephony layer while the activity is coming up
    287         // and while the activity is going down.  So in both cases we need to recalculate what
    288         // state we should be in after they complete.
    289         // Examples: (1) A new incoming call could come in and then get disconnected before
    290         //               the activity is created.
    291         //           (2) All calls could disconnect and then get a new incoming call before the
    292         //               activity is destroyed.
    293         //
    294         // b/1122139 - We previously had a check for mServiceConnected here as well, but there are
    295         // cases where we need to recalculate the current state even if the service in not
    296         // connected.  In particular the case where startOrFinish() is called while the app is
    297         // already finish()ing. In that case, we skip updating the state with the knowledge that
    298         // we will check again once the activity has finished. That means we have to recalculate the
    299         // state here even if the service is disconnected since we may not have finished a state
    300         // transition while finish()ing.
    301         if (updateListeners) {
    302             onCallListChange(mCallList);
    303         }
    304 
    305         if (doAttemptCleanup) {
    306             attemptCleanup();
    307         }
    308     }
    309 
    310     /**
    311      * Called when there is a change to the call list.
    312      * Sets the In-Call state for the entire in-call app based on the information it gets from
    313      * CallList. Dispatches the in-call state to all listeners. Can trigger the creation or
    314      * destruction of the UI based on the states that is calculates.
    315      */
    316     @Override
    317     public void onCallListChange(CallList callList) {
    318         if (callList == null) {
    319             return;
    320         }
    321         InCallState newState = getPotentialStateFromCallList(callList);
    322         InCallState oldState = mInCallState;
    323         newState = startOrFinishUi(newState);
    324 
    325         // Set the new state before announcing it to the world
    326         Log.i(this, "Phone switching state: " + oldState + " -> " + newState);
    327         mInCallState = newState;
    328 
    329         // notify listeners of new state
    330         for (InCallStateListener listener : mListeners) {
    331             Log.d(this, "Notify " + listener + " of state " + mInCallState.toString());
    332             listener.onStateChange(oldState, mInCallState, callList);
    333         }
    334 
    335         if (isActivityStarted()) {
    336             final boolean hasCall = callList.getActiveOrBackgroundCall() != null ||
    337                     callList.getOutgoingCall() != null;
    338             mInCallActivity.dismissKeyguard(hasCall);
    339         }
    340     }
    341 
    342     /**
    343      * Called when there is a new incoming call.
    344      *
    345      * @param call
    346      */
    347     @Override
    348     public void onIncomingCall(Call call) {
    349         InCallState newState = startOrFinishUi(InCallState.INCOMING);
    350         InCallState oldState = mInCallState;
    351 
    352         Log.i(this, "Phone switching state: " + oldState + " -> " + newState);
    353         mInCallState = newState;
    354 
    355         for (IncomingCallListener listener : mIncomingCallListeners) {
    356             listener.onIncomingCall(oldState, mInCallState, call);
    357         }
    358     }
    359 
    360     /**
    361      * Called when a call becomes disconnected. Called everytime an existing call
    362      * changes from being connected (incoming/outgoing/active) to disconnected.
    363      */
    364     @Override
    365     public void onDisconnect(Call call) {
    366         hideDialpadForDisconnect();
    367         maybeShowErrorDialogOnDisconnect(call);
    368 
    369         // We need to do the run the same code as onCallListChange.
    370         onCallListChange(CallList.getInstance());
    371 
    372         if (isActivityStarted()) {
    373             mInCallActivity.dismissKeyguard(false);
    374         }
    375     }
    376 
    377     /**
    378      * Given the call list, return the state in which the in-call screen should be.
    379      */
    380     public static InCallState getPotentialStateFromCallList(CallList callList) {
    381 
    382         InCallState newState = InCallState.NO_CALLS;
    383 
    384         if (callList == null) {
    385             return newState;
    386         }
    387         if (callList.getIncomingCall() != null) {
    388             newState = InCallState.INCOMING;
    389         } else if (callList.getWaitingForAccountCall() != null) {
    390             newState = InCallState.WAITING_FOR_ACCOUNT;
    391         } else if (callList.getPendingOutgoingCall() != null) {
    392             newState = InCallState.PENDING_OUTGOING;
    393         } else if (callList.getOutgoingCall() != null) {
    394             newState = InCallState.OUTGOING;
    395         } else if (callList.getActiveCall() != null ||
    396                 callList.getBackgroundCall() != null ||
    397                 callList.getDisconnectedCall() != null ||
    398                 callList.getDisconnectingCall() != null) {
    399             newState = InCallState.INCALL;
    400         }
    401 
    402         return newState;
    403     }
    404 
    405     public void addIncomingCallListener(IncomingCallListener listener) {
    406         Preconditions.checkNotNull(listener);
    407         mIncomingCallListeners.add(listener);
    408     }
    409 
    410     public void removeIncomingCallListener(IncomingCallListener listener) {
    411         if (listener != null) {
    412             mIncomingCallListeners.remove(listener);
    413         }
    414     }
    415 
    416     public void addListener(InCallStateListener listener) {
    417         Preconditions.checkNotNull(listener);
    418         mListeners.add(listener);
    419     }
    420 
    421     public void removeListener(InCallStateListener listener) {
    422         if (listener != null) {
    423             mListeners.remove(listener);
    424         }
    425     }
    426 
    427     public void addDetailsListener(InCallDetailsListener listener) {
    428         Preconditions.checkNotNull(listener);
    429         mDetailsListeners.add(listener);
    430     }
    431 
    432     public void removeDetailsListener(InCallDetailsListener listener) {
    433         if (listener != null) {
    434             mDetailsListeners.remove(listener);
    435         }
    436     }
    437 
    438     public void addOrientationListener(InCallOrientationListener listener) {
    439         Preconditions.checkNotNull(listener);
    440         mOrientationListeners.add(listener);
    441     }
    442 
    443     public void removeOrientationListener(InCallOrientationListener listener) {
    444         if (listener != null) {
    445             mOrientationListeners.remove(listener);
    446         }
    447     }
    448 
    449     public void addInCallEventListener(InCallEventListener listener) {
    450         Preconditions.checkNotNull(listener);
    451         mInCallEventListeners.add(listener);
    452     }
    453 
    454     public void removeInCallEventListener(InCallEventListener listener) {
    455         if (listener != null) {
    456             mInCallEventListeners.remove(listener);
    457         }
    458     }
    459 
    460     public ProximitySensor getProximitySensor() {
    461         return mProximitySensor;
    462     }
    463 
    464     public void handleAccountSelection(PhoneAccountHandle accountHandle) {
    465         Call call = mCallList.getWaitingForAccountCall();
    466         if (call != null) {
    467             String callId = call.getId();
    468             TelecomAdapter.getInstance().phoneAccountSelected(callId, accountHandle);
    469         }
    470     }
    471 
    472     public void cancelAccountSelection() {
    473         mAccountSelectionCancelled = true;
    474         Call call = mCallList.getWaitingForAccountCall();
    475         if (call != null) {
    476             String callId = call.getId();
    477             TelecomAdapter.getInstance().disconnectCall(callId);
    478         }
    479     }
    480 
    481     /**
    482      * Hangs up any active or outgoing calls.
    483      */
    484     public void hangUpOngoingCall(Context context) {
    485         // By the time we receive this intent, we could be shut down and call list
    486         // could be null.  Bail in those cases.
    487         if (mCallList == null) {
    488             if (mStatusBarNotifier == null) {
    489                 // The In Call UI has crashed but the notification still stayed up. We should not
    490                 // come to this stage.
    491                 StatusBarNotifier.clearInCallNotification(context);
    492             }
    493             return;
    494         }
    495 
    496         Call call = mCallList.getOutgoingCall();
    497         if (call == null) {
    498             call = mCallList.getActiveOrBackgroundCall();
    499         }
    500 
    501         if (call != null) {
    502             TelecomAdapter.getInstance().disconnectCall(call.getId());
    503             call.setState(Call.State.DISCONNECTING);
    504             mCallList.onUpdate(call);
    505         }
    506     }
    507 
    508     /**
    509      * Answers any incoming call.
    510      */
    511     public void answerIncomingCall(Context context, int videoState) {
    512         // By the time we receive this intent, we could be shut down and call list
    513         // could be null.  Bail in those cases.
    514         if (mCallList == null) {
    515             StatusBarNotifier.clearInCallNotification(context);
    516             return;
    517         }
    518 
    519         Call call = mCallList.getIncomingCall();
    520         if (call != null) {
    521             TelecomAdapter.getInstance().answerCall(call.getId(), videoState);
    522             showInCall(false, false/* newOutgoingCall */);
    523         }
    524     }
    525 
    526     /**
    527      * Declines any incoming call.
    528      */
    529     public void declineIncomingCall(Context context) {
    530         // By the time we receive this intent, we could be shut down and call list
    531         // could be null.  Bail in those cases.
    532         if (mCallList == null) {
    533             StatusBarNotifier.clearInCallNotification(context);
    534             return;
    535         }
    536 
    537         Call call = mCallList.getIncomingCall();
    538         if (call != null) {
    539             TelecomAdapter.getInstance().rejectCall(call.getId(), false, null);
    540         }
    541     }
    542 
    543     public void acceptUpgradeRequest(Context context) {
    544         // Bail if we have been shut down and the call list is null.
    545         if (mCallList == null) {
    546             StatusBarNotifier.clearInCallNotification(context);
    547             return;
    548         }
    549 
    550         Call call = mCallList.getVideoUpgradeRequestCall();
    551         if (call != null) {
    552             VideoProfile videoProfile =
    553                     new VideoProfile(VideoProfile.VideoState.BIDIRECTIONAL);
    554             call.getVideoCall().sendSessionModifyResponse(videoProfile);
    555             call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST);
    556         }
    557     }
    558 
    559     public void declineUpgradeRequest(Context context) {
    560         // Bail if we have been shut down and the call list is null.
    561         if (mCallList == null) {
    562             StatusBarNotifier.clearInCallNotification(context);
    563             return;
    564         }
    565 
    566         Call call = mCallList.getVideoUpgradeRequestCall();
    567         if (call != null) {
    568             VideoProfile videoProfile =
    569                     new VideoProfile(VideoProfile.VideoState.AUDIO_ONLY);
    570             call.getVideoCall().sendSessionModifyResponse(videoProfile);
    571             call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST);
    572         }
    573     }
    574 
    575     /**
    576      * Returns true if the incall app is the foreground application.
    577      */
    578     public boolean isShowingInCallUi() {
    579         return (isActivityStarted() && mInCallActivity.isForegroundActivity());
    580     }
    581 
    582     /**
    583      * Returns true if the activity has been created and is running.
    584      * Returns true as long as activity is not destroyed or finishing.  This ensures that we return
    585      * true even if the activity is paused (not in foreground).
    586      */
    587     public boolean isActivityStarted() {
    588         return (mInCallActivity != null &&
    589                 !mInCallActivity.isDestroyed() &&
    590                 !mInCallActivity.isFinishing());
    591     }
    592 
    593     public boolean isActivityPreviouslyStarted() {
    594         return mIsActivityPreviouslyStarted;
    595     }
    596 
    597     /**
    598      * Called when the activity goes in/out of the foreground.
    599      */
    600     public void onUiShowing(boolean showing) {
    601         // We need to update the notification bar when we leave the UI because that
    602         // could trigger it to show again.
    603         if (mStatusBarNotifier != null) {
    604             mStatusBarNotifier.updateNotification(mInCallState, mCallList);
    605         }
    606 
    607         if (mProximitySensor != null) {
    608             mProximitySensor.onInCallShowing(showing);
    609         }
    610 
    611         Intent broadcastIntent = ObjectFactory.getUiReadyBroadcastIntent(mContext);
    612         if (broadcastIntent != null) {
    613             broadcastIntent.putExtra(EXTRA_FIRST_TIME_SHOWN, !mIsActivityPreviouslyStarted);
    614 
    615             if (showing) {
    616                 Log.d(this, "Sending sticky broadcast: ", broadcastIntent);
    617                 mContext.sendStickyBroadcast(broadcastIntent);
    618             } else {
    619                 Log.d(this, "Removing sticky broadcast: ", broadcastIntent);
    620                 mContext.removeStickyBroadcast(broadcastIntent);
    621             }
    622         }
    623 
    624         if (showing) {
    625             mIsActivityPreviouslyStarted = true;
    626         }
    627     }
    628 
    629     /**
    630      * Brings the app into the foreground if possible.
    631      */
    632     public void bringToForeground(boolean showDialpad) {
    633         // Before we bring the incall UI to the foreground, we check to see if:
    634         // 1. It is not currently in the foreground
    635         // 2. We are in a state where we want to show the incall ui (i.e. there are calls to
    636         // be displayed)
    637         // If the activity hadn't actually been started previously, yet there are still calls
    638         // present (e.g. a call was accepted by a bluetooth or wired headset), we want to
    639         // bring it up the UI regardless.
    640         if (!isShowingInCallUi() && mInCallState != InCallState.NO_CALLS) {
    641             showInCall(showDialpad, false /* newOutgoingCall */);
    642         }
    643     }
    644 
    645     public void onPostDialCharWait(String callId, String chars) {
    646         if (isActivityStarted()) {
    647             mInCallActivity.showPostCharWaitDialog(callId, chars);
    648         }
    649     }
    650 
    651     /**
    652      * Handles the green CALL key while in-call.
    653      * @return true if we consumed the event.
    654      */
    655     public boolean handleCallKey() {
    656         Log.v(this, "handleCallKey");
    657 
    658         // The green CALL button means either "Answer", "Unhold", or
    659         // "Swap calls", or can be a no-op, depending on the current state
    660         // of the Phone.
    661 
    662         /**
    663          * INCOMING CALL
    664          */
    665         final CallList calls = CallList.getInstance();
    666         final Call incomingCall = calls.getIncomingCall();
    667         Log.v(this, "incomingCall: " + incomingCall);
    668 
    669         // (1) Attempt to answer a call
    670         if (incomingCall != null) {
    671             TelecomAdapter.getInstance().answerCall(
    672                     incomingCall.getId(), VideoProfile.VideoState.AUDIO_ONLY);
    673             return true;
    674         }
    675 
    676         /**
    677          * STATE_ACTIVE CALL
    678          */
    679         final Call activeCall = calls.getActiveCall();
    680         if (activeCall != null) {
    681             // TODO: This logic is repeated from CallButtonPresenter.java. We should
    682             // consolidate this logic.
    683             final boolean canMerge = activeCall.can(PhoneCapabilities.MERGE_CONFERENCE);
    684             final boolean canSwap = activeCall.can(PhoneCapabilities.SWAP_CONFERENCE);
    685 
    686             Log.v(this, "activeCall: " + activeCall + ", canMerge: " + canMerge +
    687                     ", canSwap: " + canSwap);
    688 
    689             // (2) Attempt actions on conference calls
    690             if (canMerge) {
    691                 TelecomAdapter.getInstance().merge(activeCall.getId());
    692                 return true;
    693             } else if (canSwap) {
    694                 TelecomAdapter.getInstance().swap(activeCall.getId());
    695                 return true;
    696             }
    697         }
    698 
    699         /**
    700          * BACKGROUND CALL
    701          */
    702         final Call heldCall = calls.getBackgroundCall();
    703         if (heldCall != null) {
    704             // We have a hold call so presumeable it will always support HOLD...but
    705             // there is no harm in double checking.
    706             final boolean canHold = heldCall.can(PhoneCapabilities.HOLD);
    707 
    708             Log.v(this, "heldCall: " + heldCall + ", canHold: " + canHold);
    709 
    710             // (4) unhold call
    711             if (heldCall.getState() == Call.State.ONHOLD && canHold) {
    712                 TelecomAdapter.getInstance().unholdCall(heldCall.getId());
    713                 return true;
    714             }
    715         }
    716 
    717         // Always consume hard keys
    718         return true;
    719     }
    720 
    721     /**
    722      * A dialog could have prevented in-call screen from being previously finished.
    723      * This function checks to see if there should be any UI left and if not attempts
    724      * to tear down the UI.
    725      */
    726     public void onDismissDialog() {
    727         Log.i(this, "Dialog dismissed");
    728         if (mInCallState == InCallState.NO_CALLS) {
    729             attemptFinishActivity();
    730             attemptCleanup();
    731         }
    732     }
    733 
    734     /**
    735      * Called by the {@link VideoCallPresenter} to inform of a change in full screen video status.
    736      *
    737      * @param isFullScreenVideo {@code True} if entering full screen video mode.
    738      */
    739     public void setFullScreenVideoState(boolean isFullScreenVideo) {
    740         for (InCallEventListener listener : mInCallEventListeners) {
    741             listener.onFullScreenVideoStateChanged(isFullScreenVideo);
    742         }
    743     }
    744 
    745     /**
    746      * For some disconnected causes, we show a dialog.  This calls into the activity to show
    747      * the dialog if appropriate for the call.
    748      */
    749     private void maybeShowErrorDialogOnDisconnect(Call call) {
    750         // For newly disconnected calls, we may want to show a dialog on specific error conditions
    751         if (isActivityStarted() && call.getState() == Call.State.DISCONNECTED) {
    752             if (call.getAccountHandle() == null && !call.isConferenceCall()) {
    753                 setDisconnectCauseForMissingAccounts(call);
    754             }
    755             mInCallActivity.maybeShowErrorDialogOnDisconnect(call.getDisconnectCause());
    756         }
    757     }
    758 
    759     /**
    760      * Hides the dialpad.  Called when a call is disconnected (Requires hiding dialpad).
    761      */
    762     private void hideDialpadForDisconnect() {
    763         if (isActivityStarted()) {
    764             mInCallActivity.hideDialpadForDisconnect();
    765         }
    766     }
    767 
    768     /**
    769      * When the state of in-call changes, this is the first method to get called. It determines if
    770      * the UI needs to be started or finished depending on the new state and does it.
    771      */
    772     private InCallState startOrFinishUi(InCallState newState) {
    773         Log.d(this, "startOrFinishUi: " + mInCallState + " -> " + newState);
    774 
    775         // TODO: Consider a proper state machine implementation
    776 
    777         // If the state isn't changing or if we're transitioning from pending outgoing to actual
    778         // outgoing, we have already done any starting/stopping of activities in a previous pass
    779         // ...so lets cut out early
    780         boolean alreadyOutgoing = mInCallState == InCallState.PENDING_OUTGOING &&
    781                 newState == InCallState.OUTGOING;
    782         if (newState == mInCallState || alreadyOutgoing) {
    783             return newState;
    784         }
    785 
    786         // A new Incoming call means that the user needs to be notified of the the call (since
    787         // it wasn't them who initiated it).  We do this through full screen notifications and
    788         // happens indirectly through {@link StatusBarNotifier}.
    789         //
    790         // The process for incoming calls is as follows:
    791         //
    792         // 1) CallList          - Announces existence of new INCOMING call
    793         // 2) InCallPresenter   - Gets announcement and calculates that the new InCallState
    794         //                      - should be set to INCOMING.
    795         // 3) InCallPresenter   - This method is called to see if we need to start or finish
    796         //                        the app given the new state.
    797         // 4) StatusBarNotifier - Listens to InCallState changes. InCallPresenter calls
    798         //                        StatusBarNotifier explicitly to issue a FullScreen Notification
    799         //                        that will either start the InCallActivity or show the user a
    800         //                        top-level notification dialog if the user is in an immersive app.
    801         //                        That notification can also start the InCallActivity.
    802         // 5) InCallActivity    - Main activity starts up and at the end of its onCreate will
    803         //                        call InCallPresenter::setActivity() to let the presenter
    804         //                        know that start-up is complete.
    805         //
    806         //          [ AND NOW YOU'RE IN THE CALL. voila! ]
    807         //
    808         // Our app is started using a fullScreen notification.  We need to do this whenever
    809         // we get an incoming call.
    810         final boolean startStartupSequence = (InCallState.INCOMING == newState);
    811 
    812         // A dialog to show on top of the InCallUI to select a PhoneAccount
    813         final boolean showAccountPicker = (InCallState.WAITING_FOR_ACCOUNT == newState);
    814 
    815         // A new outgoing call indicates that the user just now dialed a number and when that
    816         // happens we need to display the screen immediately or show an account picker dialog if
    817         // no default is set. However, if the main InCallUI is already visible, we do not want to
    818         // re-initiate the start-up animation, so we do not need to do anything here.
    819         //
    820         // It is also possible to go into an intermediate state where the call has been initiated
    821         // but Telecomm has not yet returned with the details of the call (handle, gateway, etc.).
    822         // This pending outgoing state can also launch the call screen.
    823         //
    824         // This is different from the incoming call sequence because we do not need to shock the
    825         // user with a top-level notification.  Just show the call UI normally.
    826         final boolean mainUiNotVisible = !isShowingInCallUi() || !getCallCardFragmentVisible();
    827         final boolean showCallUi = ((InCallState.PENDING_OUTGOING == newState ||
    828                 InCallState.OUTGOING == newState) && mainUiNotVisible);
    829 
    830         // TODO: Can we be suddenly in a call without it having been in the outgoing or incoming
    831         // state?  I havent seen that but if it can happen, the code below should be enabled.
    832         // showCallUi |= (InCallState.INCALL && !isActivityStarted());
    833 
    834         // The only time that we have an instance of mInCallActivity and it isn't started is
    835         // when it is being destroyed.  In that case, lets avoid bringing up another instance of
    836         // the activity.  When it is finally destroyed, we double check if we should bring it back
    837         // up so we aren't going to lose anything by avoiding a second startup here.
    838         boolean activityIsFinishing = mInCallActivity != null && !isActivityStarted();
    839         if (activityIsFinishing) {
    840             Log.i(this, "Undo the state change: " + newState + " -> " + mInCallState);
    841             return mInCallState;
    842         }
    843 
    844         if (showCallUi || showAccountPicker) {
    845             Log.i(this, "Start in call UI");
    846             showInCall(false /* showDialpad */, !showAccountPicker /* newOutgoingCall */);
    847         } else if (startStartupSequence) {
    848             Log.i(this, "Start Full Screen in call UI");
    849 
    850             // We're about the bring up the in-call UI for an incoming call. If we still have
    851             // dialogs up, we need to clear them out before showing incoming screen.
    852             if (isActivityStarted()) {
    853                 mInCallActivity.dismissPendingDialogs();
    854             }
    855             if (!startUi(newState)) {
    856                 // startUI refused to start the UI. This indicates that it needed to restart the
    857                 // activity.  When it finally restarts, it will call us back, so we do not actually
    858                 // change the state yet (we return mInCallState instead of newState).
    859                 return mInCallState;
    860             }
    861         } else if (newState == InCallState.NO_CALLS) {
    862             // The new state is the no calls state.  Tear everything down.
    863             attemptFinishActivity();
    864             attemptCleanup();
    865         }
    866 
    867         return newState;
    868     }
    869 
    870     /**
    871      * Sets the DisconnectCause for a call that was disconnected because it was missing a
    872      * PhoneAccount or PhoneAccounts to select from.
    873      * @param call
    874      */
    875     private void setDisconnectCauseForMissingAccounts(Call call) {
    876         android.telecom.Call telecomCall = call.getTelecommCall();
    877 
    878         Bundle extras = telecomCall.getDetails().getExtras();
    879         // Initialize the extras bundle to avoid NPE
    880         if (extras == null) {
    881             extras = new Bundle();
    882         }
    883 
    884         final List<PhoneAccountHandle> phoneAccountHandles = extras.getParcelableArrayList(
    885                 android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS);
    886 
    887         if (phoneAccountHandles == null || phoneAccountHandles.isEmpty()) {
    888             String scheme = telecomCall.getDetails().getHandle().getScheme();
    889             final String errorMsg = PhoneAccount.SCHEME_TEL.equals(scheme) ?
    890                     mContext.getString(R.string.callFailed_simError) :
    891                         mContext.getString(R.string.incall_error_supp_service_unknown);
    892             DisconnectCause disconnectCause =
    893                     new DisconnectCause(DisconnectCause.ERROR, null, errorMsg, errorMsg);
    894             call.setDisconnectCause(disconnectCause);
    895         }
    896     }
    897 
    898     private boolean startUi(InCallState inCallState) {
    899         boolean isCallWaiting = mCallList.getActiveCall() != null &&
    900                 mCallList.getIncomingCall() != null;
    901 
    902         // If the screen is off, we need to make sure it gets turned on for incoming calls.
    903         // This normally works just fine thanks to FLAG_TURN_SCREEN_ON but that only works
    904         // when the activity is first created. Therefore, to ensure the screen is turned on
    905         // for the call waiting case, we finish() the current activity and start a new one.
    906         // There should be no jank from this since the screen is already off and will remain so
    907         // until our new activity is up.
    908 
    909         if (isCallWaiting) {
    910             if (mProximitySensor.isScreenReallyOff() && isActivityStarted()) {
    911                 mInCallActivity.finish();
    912                 // When the activity actually finishes, we will start it again if there are
    913                 // any active calls, so we do not need to start it explicitly here. Note, we
    914                 // actually get called back on this function to restart it.
    915 
    916                 // We return false to indicate that we did not actually start the UI.
    917                 return false;
    918             } else {
    919                 showInCall(false, false);
    920             }
    921         } else {
    922             mStatusBarNotifier.updateNotification(inCallState, mCallList);
    923         }
    924         return true;
    925     }
    926 
    927     /**
    928      * Checks to see if both the UI is gone and the service is disconnected. If so, tear it all
    929      * down.
    930      */
    931     private void attemptCleanup() {
    932         boolean shouldCleanup = (mInCallActivity == null && !mServiceConnected &&
    933                 mInCallState == InCallState.NO_CALLS);
    934         Log.i(this, "attemptCleanup? " + shouldCleanup);
    935 
    936         if (shouldCleanup) {
    937             mIsActivityPreviouslyStarted = false;
    938 
    939             // blow away stale contact info so that we get fresh data on
    940             // the next set of calls
    941             if (mContactInfoCache != null) {
    942                 mContactInfoCache.clearCache();
    943             }
    944             mContactInfoCache = null;
    945 
    946             if (mProximitySensor != null) {
    947                 removeListener(mProximitySensor);
    948                 mProximitySensor.tearDown();
    949             }
    950             mProximitySensor = null;
    951 
    952             mAudioModeProvider = null;
    953 
    954             if (mStatusBarNotifier != null) {
    955                 removeListener(mStatusBarNotifier);
    956             }
    957             mStatusBarNotifier = null;
    958 
    959             if (mCallList != null) {
    960                 mCallList.removeListener(this);
    961             }
    962             mCallList = null;
    963 
    964             mContext = null;
    965             mInCallActivity = null;
    966 
    967             mListeners.clear();
    968             mIncomingCallListeners.clear();
    969 
    970             Log.d(this, "Finished InCallPresenter.CleanUp");
    971         }
    972     }
    973 
    974     private void showInCall(boolean showDialpad, boolean newOutgoingCall) {
    975         mContext.startActivity(getInCallIntent(showDialpad, newOutgoingCall));
    976     }
    977 
    978     public Intent getInCallIntent(boolean showDialpad, boolean newOutgoingCall) {
    979         final Intent intent = new Intent(Intent.ACTION_MAIN, null);
    980         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
    981                 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
    982                 | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
    983         intent.setClass(mContext, InCallActivity.class);
    984         if (showDialpad) {
    985             intent.putExtra(InCallActivity.SHOW_DIALPAD_EXTRA, true);
    986         }
    987 
    988         intent.putExtra(InCallActivity.NEW_OUTGOING_CALL, newOutgoingCall);
    989         return intent;
    990     }
    991 
    992     /**
    993      * Retrieves the current in-call camera manager instance, creating if necessary.
    994      *
    995      * @return The {@link InCallCameraManager}.
    996      */
    997     public InCallCameraManager getInCallCameraManager() {
    998         synchronized(this) {
    999             if (mInCallCameraManager == null) {
   1000                 mInCallCameraManager = new InCallCameraManager(mContext);
   1001             }
   1002 
   1003             return mInCallCameraManager;
   1004         }
   1005     }
   1006 
   1007     /**
   1008      * Handles changes to the device rotation.
   1009      *
   1010      * @param rotation The device rotation.
   1011      */
   1012     public void onDeviceRotationChange(int rotation) {
   1013         // First translate to rotation in degrees.
   1014         int rotationAngle;
   1015         switch (rotation) {
   1016             case Surface.ROTATION_0:
   1017                 rotationAngle = 0;
   1018                 break;
   1019             case Surface.ROTATION_90:
   1020                 rotationAngle = 90;
   1021                 break;
   1022             case Surface.ROTATION_180:
   1023                 rotationAngle = 180;
   1024                 break;
   1025             case Surface.ROTATION_270:
   1026                 rotationAngle = 270;
   1027                 break;
   1028             default:
   1029                 rotationAngle = 0;
   1030         }
   1031 
   1032         mCallList.notifyCallsOfDeviceRotation(rotationAngle);
   1033     }
   1034 
   1035     /**
   1036      * Notifies listeners of changes in orientation (e.g. portrait/landscape).
   1037      *
   1038      * @param orientation The orientation of the device.
   1039      */
   1040     public void onDeviceOrientationChange(int orientation) {
   1041         for (InCallOrientationListener listener : mOrientationListeners) {
   1042             listener.onDeviceOrientationChanged(orientation);
   1043         }
   1044     }
   1045 
   1046     /**
   1047      * Configures the in-call UI activity so it can change orientations or not.
   1048      *
   1049      * @param allowOrientationChange {@code True} if the in-call UI can change between portrait
   1050      *      and landscape.  {@Code False} if the in-call UI should be locked in portrait.
   1051      */
   1052     public void setInCallAllowsOrientationChange(boolean allowOrientationChange) {
   1053         if (!allowOrientationChange) {
   1054             mInCallActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
   1055         } else {
   1056             mInCallActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
   1057         }
   1058     }
   1059 
   1060     /**
   1061      * Returns the space available beside the call card.
   1062      *
   1063      * @return The space beside the call card.
   1064      */
   1065     public float getSpaceBesideCallCard() {
   1066         return mInCallActivity.getCallCardFragment().getSpaceBesideCallCard();
   1067     }
   1068 
   1069     /**
   1070      * Returns whether the call card fragment is currently visible.
   1071      *
   1072      * @return True if the call card fragment is visible.
   1073      */
   1074     public boolean getCallCardFragmentVisible() {
   1075         if (mInCallActivity != null) {
   1076             return mInCallActivity.getCallCardFragment().isVisible();
   1077         }
   1078         return false;
   1079     }
   1080 
   1081     /**
   1082      * @return True if the application is currently running in a right-to-left locale.
   1083      */
   1084     public static boolean isRtl() {
   1085         return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) ==
   1086                 View.LAYOUT_DIRECTION_RTL;
   1087     }
   1088 
   1089     /**
   1090      * Private constructor. Must use getInstance() to get this singleton.
   1091      */
   1092     private InCallPresenter() {
   1093     }
   1094 
   1095     /**
   1096      * All the main states of InCallActivity.
   1097      */
   1098     public enum InCallState {
   1099         // InCall Screen is off and there are no calls
   1100         NO_CALLS,
   1101 
   1102         // Incoming-call screen is up
   1103         INCOMING,
   1104 
   1105         // In-call experience is showing
   1106         INCALL,
   1107 
   1108         // Waiting for user input before placing outgoing call
   1109         WAITING_FOR_ACCOUNT,
   1110 
   1111         // UI is starting up but no call has been initiated yet.
   1112         // The UI is waiting for Telecomm to respond.
   1113         PENDING_OUTGOING,
   1114 
   1115         // User is dialing out
   1116         OUTGOING;
   1117 
   1118         public boolean isIncoming() {
   1119             return (this == INCOMING);
   1120         }
   1121 
   1122         public boolean isConnectingOrConnected() {
   1123             return (this == INCOMING ||
   1124                     this == OUTGOING ||
   1125                     this == INCALL);
   1126         }
   1127     }
   1128 
   1129     /**
   1130      * Interface implemented by classes that need to know about the InCall State.
   1131      */
   1132     public interface InCallStateListener {
   1133         // TODO: Enhance state to contain the call objects instead of passing CallList
   1134         public void onStateChange(InCallState oldState, InCallState newState, CallList callList);
   1135     }
   1136 
   1137     public interface IncomingCallListener {
   1138         public void onIncomingCall(InCallState oldState, InCallState newState, Call call);
   1139     }
   1140 
   1141     public interface InCallDetailsListener {
   1142         public void onDetailsChanged(Call call, android.telecom.Call.Details details);
   1143     }
   1144 
   1145     public interface InCallOrientationListener {
   1146         public void onDeviceOrientationChanged(int orientation);
   1147     }
   1148 
   1149     /**
   1150      * Interface implemented by classes that need to know about events which occur within the
   1151      * In-Call UI.  Used as a means of communicating between fragments that make up the UI.
   1152      */
   1153     public interface InCallEventListener {
   1154         public void onFullScreenVideoStateChanged(boolean isFullScreenVideo);
   1155     }
   1156 }
   1157