1/*
2 * Copyright (C) 2015 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.server.telecom;
18
19import android.annotation.NonNull;
20import android.media.IAudioService;
21import android.media.ToneGenerator;
22import android.telecom.CallAudioState;
23import android.telecom.VideoProfile;
24import android.util.SparseArray;
25
26import com.android.internal.annotations.VisibleForTesting;
27import com.android.internal.util.IndentingPrintWriter;
28
29import java.util.Collection;
30import java.util.HashSet;
31import java.util.Set;
32import java.util.LinkedHashSet;
33
34public class CallAudioManager extends CallsManagerListenerBase {
35
36    public interface AudioServiceFactory {
37        IAudioService getAudioService();
38    }
39
40    private final String LOG_TAG = CallAudioManager.class.getSimpleName();
41
42    private final LinkedHashSet<Call> mActiveDialingOrConnectingCalls;
43    private final LinkedHashSet<Call> mRingingCalls;
44    private final LinkedHashSet<Call> mHoldingCalls;
45    private final Set<Call> mCalls;
46    private final SparseArray<LinkedHashSet<Call>> mCallStateToCalls;
47
48    private final CallAudioRouteStateMachine mCallAudioRouteStateMachine;
49    private final CallAudioModeStateMachine mCallAudioModeStateMachine;
50    private final CallsManager mCallsManager;
51    private final InCallTonePlayer.Factory mPlayerFactory;
52    private final Ringer mRinger;
53    private final RingbackPlayer mRingbackPlayer;
54    private final DtmfLocalTonePlayer mDtmfLocalTonePlayer;
55
56    private Call mForegroundCall;
57    private boolean mIsTonePlaying = false;
58    private InCallTonePlayer mHoldTonePlayer;
59
60    public CallAudioManager(CallAudioRouteStateMachine callAudioRouteStateMachine,
61            CallsManager callsManager,
62            CallAudioModeStateMachine callAudioModeStateMachine,
63            InCallTonePlayer.Factory playerFactory,
64            Ringer ringer,
65            RingbackPlayer ringbackPlayer,
66            DtmfLocalTonePlayer dtmfLocalTonePlayer) {
67        mActiveDialingOrConnectingCalls = new LinkedHashSet<>();
68        mRingingCalls = new LinkedHashSet<>();
69        mHoldingCalls = new LinkedHashSet<>();
70        mCalls = new HashSet<>();
71        mCallStateToCalls = new SparseArray<LinkedHashSet<Call>>() {{
72            put(CallState.CONNECTING, mActiveDialingOrConnectingCalls);
73            put(CallState.ACTIVE, mActiveDialingOrConnectingCalls);
74            put(CallState.DIALING, mActiveDialingOrConnectingCalls);
75            put(CallState.PULLING, mActiveDialingOrConnectingCalls);
76            put(CallState.RINGING, mRingingCalls);
77            put(CallState.ON_HOLD, mHoldingCalls);
78        }};
79
80        mCallAudioRouteStateMachine = callAudioRouteStateMachine;
81        mCallAudioModeStateMachine = callAudioModeStateMachine;
82        mCallsManager = callsManager;
83        mPlayerFactory = playerFactory;
84        mRinger = ringer;
85        mRingbackPlayer = ringbackPlayer;
86        mDtmfLocalTonePlayer = dtmfLocalTonePlayer;
87
88        mPlayerFactory.setCallAudioManager(this);
89        mCallAudioModeStateMachine.setCallAudioManager(this);
90    }
91
92    @Override
93    public void onCallStateChanged(Call call, int oldState, int newState) {
94        if (shouldIgnoreCallForAudio(call)) {
95            // No audio management for calls in a conference, or external calls.
96            return;
97        }
98        Log.d(LOG_TAG, "Call state changed for TC@%s: %s -> %s", call.getId(),
99                CallState.toString(oldState), CallState.toString(newState));
100
101        for (int i = 0; i < mCallStateToCalls.size(); i++) {
102            mCallStateToCalls.valueAt(i).remove(call);
103        }
104        if (mCallStateToCalls.get(newState) != null) {
105            mCallStateToCalls.get(newState).add(call);
106        }
107
108        updateForegroundCall();
109        if (shouldPlayDisconnectTone(oldState, newState)) {
110            playToneForDisconnectedCall(call);
111        }
112
113        onCallLeavingState(call, oldState);
114        onCallEnteringState(call, newState);
115    }
116
117    @Override
118    public void onCallAdded(Call call) {
119        if (shouldIgnoreCallForAudio(call)) {
120            return; // Don't do audio handling for calls in a conference, or external calls.
121        }
122
123        addCall(call);
124    }
125
126    @Override
127    public void onCallRemoved(Call call) {
128        if (shouldIgnoreCallForAudio(call)) {
129            return; // Don't do audio handling for calls in a conference, or external calls.
130        }
131
132        removeCall(call);
133    }
134
135    private void addCall(Call call) {
136        if (mCalls.contains(call)) {
137            Log.w(LOG_TAG, "Call TC@%s is being added twice.", call.getId());
138            return; // No guarantees that the same call won't get added twice.
139        }
140
141        Log.d(LOG_TAG, "Call added with id TC@%s in state %s", call.getId(),
142                CallState.toString(call.getState()));
143
144        if (mCallStateToCalls.get(call.getState()) != null) {
145            mCallStateToCalls.get(call.getState()).add(call);
146        }
147        updateForegroundCall();
148        mCalls.add(call);
149
150        onCallEnteringState(call, call.getState());
151    }
152
153    private void removeCall(Call call) {
154        if (!mCalls.contains(call)) {
155            return; // No guarantees that the same call won't get removed twice.
156        }
157
158        Log.d(LOG_TAG, "Call removed with id TC@%s in state %s", call.getId(),
159                CallState.toString(call.getState()));
160
161        for (int i = 0; i < mCallStateToCalls.size(); i++) {
162            mCallStateToCalls.valueAt(i).remove(call);
163        }
164
165        updateForegroundCall();
166        mCalls.remove(call);
167
168        onCallLeavingState(call, call.getState());
169    }
170
171    /**
172     * Handles changes to the external state of a call.  External calls which become regular calls
173     * should be tracked, and regular calls which become external should no longer be tracked.
174     *
175     * @param call The call.
176     * @param isExternalCall {@code True} if the call is now external, {@code false} if it is now
177     *      a regular call.
178     */
179    @Override
180    public void onExternalCallChanged(Call call, boolean isExternalCall) {
181        if (isExternalCall) {
182            Log.d(LOG_TAG, "Removing call which became external ID %s", call.getId());
183            removeCall(call);
184        } else if (!isExternalCall) {
185            Log.d(LOG_TAG, "Adding external call which was pulled with ID %s", call.getId());
186            addCall(call);
187
188            if (mCallsManager.isSpeakerphoneAutoEnabledForVideoCalls(call.getVideoState())) {
189                // When pulling a video call, automatically enable the speakerphone.
190                Log.d(LOG_TAG, "Switching to speaker because external video call %s was pulled." +
191                        call.getId());
192                mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
193                        CallAudioRouteStateMachine.SWITCH_SPEAKER);
194            }
195        }
196    }
197
198    /**
199     * Determines if {@link CallAudioManager} should do any audio routing operations for a call.
200     * We ignore child calls of a conference and external calls for audio routing purposes.
201     *
202     * @param call The call to check.
203     * @return {@code true} if the call should be ignored for audio routing, {@code false}
204     * otherwise
205     */
206    private boolean shouldIgnoreCallForAudio(Call call) {
207        return call.getParentCall() != null || call.isExternalCall();
208    }
209
210    @Override
211    public void onIncomingCallAnswered(Call call) {
212        if (!mCalls.contains(call)) {
213            return;
214        }
215
216        // This is called after the UI answers the call, but before the connection service
217        // sets the call to active. Only thing to handle for mode here is the audio speedup thing.
218
219        if (call.can(android.telecom.Call.Details.CAPABILITY_SPEED_UP_MT_AUDIO)) {
220            if (mForegroundCall == call) {
221                Log.i(LOG_TAG, "Invoking the MT_AUDIO_SPEEDUP mechanism. Transitioning into " +
222                        "an active in-call audio state before connection service has " +
223                        "connected the call.");
224                if (mCallStateToCalls.get(call.getState()) != null) {
225                    mCallStateToCalls.get(call.getState()).remove(call);
226                }
227                mActiveDialingOrConnectingCalls.add(call);
228                mCallAudioModeStateMachine.sendMessageWithArgs(
229                        CallAudioModeStateMachine.MT_AUDIO_SPEEDUP_FOR_RINGING_CALL,
230                        makeArgsForModeStateMachine());
231            }
232        }
233
234        // Turn off mute when a new incoming call is answered.
235        mute(false /* shouldMute */);
236
237        maybeStopRingingAndCallWaitingForAnsweredOrRejectedCall(call);
238    }
239
240    @Override
241    public void onSessionModifyRequestReceived(Call call, VideoProfile videoProfile) {
242        if (videoProfile == null) {
243            return;
244        }
245
246        if (call != mForegroundCall) {
247            // We only play tones for foreground calls.
248            return;
249        }
250
251        int previousVideoState = call.getVideoState();
252        int newVideoState = videoProfile.getVideoState();
253        Log.v(this, "onSessionModifyRequestReceived : videoProfile = " + VideoProfile
254                .videoStateToString(newVideoState));
255
256        boolean isUpgradeRequest = !VideoProfile.isReceptionEnabled(previousVideoState) &&
257                VideoProfile.isReceptionEnabled(newVideoState);
258
259        if (isUpgradeRequest) {
260            mPlayerFactory.createPlayer(InCallTonePlayer.TONE_VIDEO_UPGRADE).startTone();
261        }
262    }
263
264    /**
265     * Play or stop a call hold tone for a call.  Triggered via
266     * {@link Connection#sendConnectionEvent(String)} when the
267     * {@link Connection#EVENT_ON_HOLD_TONE_START} event or
268     * {@link Connection#EVENT_ON_HOLD_TONE_STOP} event is passed through to the
269     *
270     * @param call The call which requested the hold tone.
271     */
272    @Override
273    public void onHoldToneRequested(Call call) {
274        maybePlayHoldTone();
275    }
276
277    @Override
278    public void onIsVoipAudioModeChanged(Call call) {
279        if (call != mForegroundCall) {
280            return;
281        }
282        mCallAudioModeStateMachine.sendMessageWithArgs(
283                CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE,
284                makeArgsForModeStateMachine());
285    }
286
287    @Override
288    public void onRingbackRequested(Call call, boolean shouldRingback) {
289        if (call == mForegroundCall && shouldRingback) {
290            mRingbackPlayer.startRingbackForCall(call);
291        } else {
292            mRingbackPlayer.stopRingbackForCall(call);
293        }
294    }
295
296    @Override
297    public void onIncomingCallRejected(Call call, boolean rejectWithMessage, String message) {
298        maybeStopRingingAndCallWaitingForAnsweredOrRejectedCall(call);
299    }
300
301    @Override
302    public void onIsConferencedChanged(Call call) {
303        // This indicates a conferencing change, which shouldn't impact any audio mode stuff.
304        Call parentCall = call.getParentCall();
305        if (parentCall == null) {
306            // Indicates that the call should be tracked for audio purposes. Treat it as if it were
307            // just added.
308            Log.i(LOG_TAG, "Call TC@" + call.getId() + " left conference and will" +
309                            " now be tracked by CallAudioManager.");
310            onCallAdded(call);
311        } else {
312            // The call joined a conference, so stop tracking it.
313            if (mCallStateToCalls.get(call.getState()) != null) {
314                mCallStateToCalls.get(call.getState()).remove(call);
315            }
316
317            updateForegroundCall();
318            mCalls.remove(call);
319        }
320    }
321
322    @Override
323    public void onConnectionServiceChanged(Call call, ConnectionServiceWrapper oldCs,
324            ConnectionServiceWrapper newCs) {
325        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
326                CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
327    }
328
329    @Override
330    public void onVideoStateChanged(Call call, int previousVideoState, int newVideoState) {
331        if (call != getForegroundCall()) {
332            Log.d(LOG_TAG, "Ignoring video state change from %s to %s for call %s -- not " +
333                    "foreground.", VideoProfile.videoStateToString(previousVideoState),
334                    VideoProfile.videoStateToString(newVideoState), call.getId());
335            return;
336        }
337
338        if (!VideoProfile.isVideo(previousVideoState) &&
339                mCallsManager.isSpeakerphoneAutoEnabledForVideoCalls(newVideoState)) {
340            Log.d(LOG_TAG, "Switching to speaker because call %s transitioned video state from %s" +
341                    " to %s", call.getId(), VideoProfile.videoStateToString(previousVideoState),
342                    VideoProfile.videoStateToString(newVideoState));
343            mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
344                    CallAudioRouteStateMachine.SWITCH_SPEAKER);
345        }
346    }
347
348    public CallAudioState getCallAudioState() {
349        return mCallAudioRouteStateMachine.getCurrentCallAudioState();
350    }
351
352    public Call getPossiblyHeldForegroundCall() {
353        return mForegroundCall;
354    }
355
356    public Call getForegroundCall() {
357        if (mForegroundCall != null && mForegroundCall.getState() != CallState.ON_HOLD) {
358            return mForegroundCall;
359        }
360        return null;
361    }
362
363    void toggleMute() {
364        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
365                CallAudioRouteStateMachine.TOGGLE_MUTE);
366    }
367
368    @VisibleForTesting
369    public void mute(boolean shouldMute) {
370        Log.v(this, "mute, shouldMute: %b", shouldMute);
371
372        // Don't mute if there are any emergency calls.
373        if (mCallsManager.hasEmergencyCall()) {
374            shouldMute = false;
375            Log.v(this, "ignoring mute for emergency call");
376        }
377
378        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(shouldMute
379                ? CallAudioRouteStateMachine.MUTE_ON : CallAudioRouteStateMachine.MUTE_OFF);
380    }
381
382    /**
383     * Changed the audio route, for example from earpiece to speaker phone.
384     *
385     * @param route The new audio route to use. See {@link CallAudioState}.
386     */
387    void setAudioRoute(int route) {
388        Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route));
389        switch (route) {
390            case CallAudioState.ROUTE_BLUETOOTH:
391                mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
392                        CallAudioRouteStateMachine.USER_SWITCH_BLUETOOTH);
393                return;
394            case CallAudioState.ROUTE_SPEAKER:
395                mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
396                        CallAudioRouteStateMachine.USER_SWITCH_SPEAKER);
397                return;
398            case CallAudioState.ROUTE_WIRED_HEADSET:
399                mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
400                        CallAudioRouteStateMachine.USER_SWITCH_HEADSET);
401                return;
402            case CallAudioState.ROUTE_EARPIECE:
403                mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
404                        CallAudioRouteStateMachine.USER_SWITCH_EARPIECE);
405                return;
406            case CallAudioState.ROUTE_WIRED_OR_EARPIECE:
407                mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
408                        CallAudioRouteStateMachine.USER_SWITCH_BASELINE_ROUTE);
409                return;
410            default:
411                Log.wtf(this, "Invalid route specified: %d", route);
412        }
413    }
414
415    void silenceRingers() {
416        for (Call call : mRingingCalls) {
417            call.silence();
418        }
419
420        mRingingCalls.clear();
421        mRinger.stopRinging();
422        mRinger.stopCallWaiting();
423        mCallAudioModeStateMachine.sendMessageWithArgs(
424                CallAudioModeStateMachine.NO_MORE_RINGING_CALLS,
425                makeArgsForModeStateMachine());
426    }
427
428    @VisibleForTesting
429    public boolean startRinging() {
430        return mRinger.startRinging(mForegroundCall);
431    }
432
433    @VisibleForTesting
434    public void startCallWaiting() {
435        mRinger.startCallWaiting(mRingingCalls.iterator().next());
436    }
437
438    @VisibleForTesting
439    public void stopRinging() {
440        mRinger.stopRinging();
441    }
442
443    @VisibleForTesting
444    public void stopCallWaiting() {
445        mRinger.stopCallWaiting();
446    }
447
448    @VisibleForTesting
449    public void setCallAudioRouteFocusState(int focusState) {
450        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
451                CallAudioRouteStateMachine.SWITCH_FOCUS, focusState);
452    }
453
454    @VisibleForTesting
455    public CallAudioRouteStateMachine getCallAudioRouteStateMachine() {
456        return mCallAudioRouteStateMachine;
457    }
458
459    @VisibleForTesting
460    public CallAudioModeStateMachine getCallAudioModeStateMachine() {
461        return mCallAudioModeStateMachine;
462    }
463
464    void dump(IndentingPrintWriter pw) {
465        pw.println("All calls:");
466        pw.increaseIndent();
467        dumpCallsInCollection(pw, mCalls);
468        pw.decreaseIndent();
469
470        pw.println("Active dialing, or connecting calls:");
471        pw.increaseIndent();
472        dumpCallsInCollection(pw, mActiveDialingOrConnectingCalls);
473        pw.decreaseIndent();
474
475        pw.println("Ringing calls:");
476        pw.increaseIndent();
477        dumpCallsInCollection(pw, mRingingCalls);
478        pw.decreaseIndent();
479
480        pw.println("Holding calls:");
481        pw.increaseIndent();
482        dumpCallsInCollection(pw, mHoldingCalls);
483        pw.decreaseIndent();
484
485        pw.println("Foreground call:");
486        pw.println(mForegroundCall);
487    }
488
489    @VisibleForTesting
490    public void setIsTonePlaying(boolean isTonePlaying) {
491        mIsTonePlaying = isTonePlaying;
492        mCallAudioModeStateMachine.sendMessageWithArgs(
493                isTonePlaying ? CallAudioModeStateMachine.TONE_STARTED_PLAYING
494                        : CallAudioModeStateMachine.TONE_STOPPED_PLAYING,
495                makeArgsForModeStateMachine());
496    }
497
498    private void onCallLeavingState(Call call, int state) {
499        switch (state) {
500            case CallState.ACTIVE:
501            case CallState.CONNECTING:
502                onCallLeavingActiveDialingOrConnecting();
503                break;
504            case CallState.RINGING:
505                onCallLeavingRinging();
506                break;
507            case CallState.ON_HOLD:
508                onCallLeavingHold();
509                break;
510            case CallState.PULLING:
511                onCallLeavingActiveDialingOrConnecting();
512                break;
513            case CallState.DIALING:
514                stopRingbackForCall(call);
515                onCallLeavingActiveDialingOrConnecting();
516                break;
517        }
518    }
519
520    private void onCallEnteringState(Call call, int state) {
521        switch (state) {
522            case CallState.ACTIVE:
523            case CallState.CONNECTING:
524                onCallEnteringActiveDialingOrConnecting();
525                break;
526            case CallState.RINGING:
527                onCallEnteringRinging();
528                break;
529            case CallState.ON_HOLD:
530                onCallEnteringHold();
531                break;
532            case CallState.PULLING:
533                onCallEnteringActiveDialingOrConnecting();
534                break;
535            case CallState.DIALING:
536                onCallEnteringActiveDialingOrConnecting();
537                playRingbackForCall(call);
538                break;
539        }
540    }
541
542    private void onCallLeavingActiveDialingOrConnecting() {
543        if (mActiveDialingOrConnectingCalls.size() == 0) {
544            mCallAudioModeStateMachine.sendMessageWithArgs(
545                    CallAudioModeStateMachine.NO_MORE_ACTIVE_OR_DIALING_CALLS,
546                    makeArgsForModeStateMachine());
547        }
548    }
549
550    private void onCallLeavingRinging() {
551        if (mRingingCalls.size() == 0) {
552            mCallAudioModeStateMachine.sendMessageWithArgs(
553                    CallAudioModeStateMachine.NO_MORE_RINGING_CALLS,
554                    makeArgsForModeStateMachine());
555        }
556    }
557
558    private void onCallLeavingHold() {
559        if (mHoldingCalls.size() == 0) {
560            mCallAudioModeStateMachine.sendMessageWithArgs(
561                    CallAudioModeStateMachine.NO_MORE_HOLDING_CALLS,
562                    makeArgsForModeStateMachine());
563        }
564    }
565
566    private void onCallEnteringActiveDialingOrConnecting() {
567        if (mActiveDialingOrConnectingCalls.size() == 1) {
568            mCallAudioModeStateMachine.sendMessageWithArgs(
569                    CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL,
570                    makeArgsForModeStateMachine());
571        }
572    }
573
574    private void onCallEnteringRinging() {
575        if (mRingingCalls.size() == 1) {
576            mCallAudioModeStateMachine.sendMessageWithArgs(
577                    CallAudioModeStateMachine.NEW_RINGING_CALL,
578                    makeArgsForModeStateMachine());
579        }
580    }
581
582    private void onCallEnteringHold() {
583        if (mHoldingCalls.size() == 1) {
584            mCallAudioModeStateMachine.sendMessageWithArgs(
585                    CallAudioModeStateMachine.NEW_HOLDING_CALL,
586                    makeArgsForModeStateMachine());
587        }
588    }
589
590    private void updateForegroundCall() {
591        Call oldForegroundCall = mForegroundCall;
592        if (mActiveDialingOrConnectingCalls.size() > 0) {
593            // Give preference for connecting calls over active/dialing for foreground-ness.
594            Call possibleConnectingCall = null;
595            for (Call call : mActiveDialingOrConnectingCalls) {
596                if (call.getState() == CallState.CONNECTING) {
597                    possibleConnectingCall = call;
598                }
599            }
600            mForegroundCall = possibleConnectingCall == null ?
601                    mActiveDialingOrConnectingCalls.iterator().next() : possibleConnectingCall;
602        } else if (mRingingCalls.size() > 0) {
603            mForegroundCall = mRingingCalls.iterator().next();
604        } else if (mHoldingCalls.size() > 0) {
605            mForegroundCall = mHoldingCalls.iterator().next();
606        } else {
607            mForegroundCall = null;
608        }
609
610        if (mForegroundCall != oldForegroundCall) {
611            mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
612                    CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
613            mDtmfLocalTonePlayer.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
614            maybePlayHoldTone();
615        }
616    }
617
618    @NonNull
619    private CallAudioModeStateMachine.MessageArgs makeArgsForModeStateMachine() {
620        return new CallAudioModeStateMachine.MessageArgs(
621                mActiveDialingOrConnectingCalls.size() > 0,
622                mRingingCalls.size() > 0,
623                mHoldingCalls.size() > 0,
624                mIsTonePlaying,
625                mForegroundCall != null && mForegroundCall.getIsVoipAudioMode(),
626                Log.createSubsession());
627    }
628
629    private void playToneForDisconnectedCall(Call call) {
630        if (mForegroundCall != null && call != mForegroundCall && mCalls.size() > 1) {
631            Log.v(LOG_TAG, "Omitting tone because we are not foreground" +
632                    " and there is another call.");
633            return;
634        }
635
636        if (call.getDisconnectCause() != null) {
637            int toneToPlay = InCallTonePlayer.TONE_INVALID;
638
639            Log.v(this, "Disconnect cause: %s.", call.getDisconnectCause());
640
641            switch(call.getDisconnectCause().getTone()) {
642                case ToneGenerator.TONE_SUP_BUSY:
643                    toneToPlay = InCallTonePlayer.TONE_BUSY;
644                    break;
645                case ToneGenerator.TONE_SUP_CONGESTION:
646                    toneToPlay = InCallTonePlayer.TONE_CONGESTION;
647                    break;
648                case ToneGenerator.TONE_CDMA_REORDER:
649                    toneToPlay = InCallTonePlayer.TONE_REORDER;
650                    break;
651                case ToneGenerator.TONE_CDMA_ABBR_INTERCEPT:
652                    toneToPlay = InCallTonePlayer.TONE_INTERCEPT;
653                    break;
654                case ToneGenerator.TONE_CDMA_CALLDROP_LITE:
655                    toneToPlay = InCallTonePlayer.TONE_CDMA_DROP;
656                    break;
657                case ToneGenerator.TONE_SUP_ERROR:
658                    toneToPlay = InCallTonePlayer.TONE_UNOBTAINABLE_NUMBER;
659                    break;
660                case ToneGenerator.TONE_PROP_PROMPT:
661                    toneToPlay = InCallTonePlayer.TONE_CALL_ENDED;
662                    break;
663            }
664
665            Log.d(this, "Found a disconnected call with tone to play %d.", toneToPlay);
666
667            if (toneToPlay != InCallTonePlayer.TONE_INVALID) {
668                mPlayerFactory.createPlayer(toneToPlay).startTone();
669            }
670        }
671    }
672
673    private void playRingbackForCall(Call call) {
674        if (call == mForegroundCall && call.isRingbackRequested()) {
675            mRingbackPlayer.startRingbackForCall(call);
676        }
677    }
678
679    private void stopRingbackForCall(Call call) {
680        mRingbackPlayer.stopRingbackForCall(call);
681    }
682
683    /**
684     * Determines if a hold tone should be played and then starts or stops it accordingly.
685     */
686    private void maybePlayHoldTone() {
687        if (shouldPlayHoldTone()) {
688            if (mHoldTonePlayer == null) {
689                mHoldTonePlayer = mPlayerFactory.createPlayer(InCallTonePlayer.TONE_CALL_WAITING);
690                mHoldTonePlayer.startTone();
691            }
692        } else {
693            if (mHoldTonePlayer != null) {
694                mHoldTonePlayer.stopTone();
695                mHoldTonePlayer = null;
696            }
697        }
698    }
699
700    /**
701     * Determines if a hold tone should be played.
702     * A hold tone should be played only if foreground call is equals with call which is
703     * remotely held.
704     *
705     * @return {@code true} if the the hold tone should be played, {@code false} otherwise.
706     */
707    private boolean shouldPlayHoldTone() {
708        Call foregroundCall = getForegroundCall();
709        // If there is no foreground call, no hold tone should play.
710        if (foregroundCall == null) {
711            return false;
712        }
713
714        // If another call is ringing, no hold tone should play.
715        if (mCallsManager.hasRingingCall()) {
716            return false;
717        }
718
719        // If the foreground call isn't active, no hold tone should play. This might happen, for
720        // example, if the user puts a remotely held call on hold itself.
721        if (!foregroundCall.isActive()) {
722            return false;
723        }
724
725        return foregroundCall.isRemotelyHeld();
726    }
727
728    private void dumpCallsInCollection(IndentingPrintWriter pw, Collection<Call> calls) {
729        for (Call call : calls) {
730            if (call != null) pw.println(call.getId());
731        }
732    }
733
734    private void maybeStopRingingAndCallWaitingForAnsweredOrRejectedCall(Call call) {
735        // Check to see if the call being answered/rejected is the only ringing call, since this
736        // will be called before the connection service acknowledges the state change.
737        if (mRingingCalls.size() == 0 ||
738                (mRingingCalls.size() == 1 && call == mRingingCalls.iterator().next())) {
739            mRinger.stopRinging();
740            mRinger.stopCallWaiting();
741        }
742    }
743
744    private boolean shouldPlayDisconnectTone(int oldState, int newState) {
745        if (newState != CallState.DISCONNECTED) {
746            return false;
747        }
748        return oldState == CallState.ACTIVE ||
749                oldState == CallState.DIALING ||
750                oldState == CallState.ON_HOLD;
751    }
752
753    @VisibleForTesting
754    public Set<Call> getTrackedCalls() {
755        return mCalls;
756    }
757
758    @VisibleForTesting
759    public SparseArray<LinkedHashSet<Call>> getCallStateToCalls() {
760        return mCallStateToCalls;
761    }
762}