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