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