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