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