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