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        maybeStopRingingAndCallWaitingForAnsweredOrRejectedCall(call);
235    }
236
237    @Override
238    public void onSessionModifyRequestReceived(Call call, VideoProfile videoProfile) {
239        if (videoProfile == null) {
240            return;
241        }
242
243        if (call != mForegroundCall) {
244            // We only play tones for foreground calls.
245            return;
246        }
247
248        int previousVideoState = call.getVideoState();
249        int newVideoState = videoProfile.getVideoState();
250        Log.v(this, "onSessionModifyRequestReceived : videoProfile = " + VideoProfile
251                .videoStateToString(newVideoState));
252
253        boolean isUpgradeRequest = !VideoProfile.isReceptionEnabled(previousVideoState) &&
254                VideoProfile.isReceptionEnabled(newVideoState);
255
256        if (isUpgradeRequest) {
257            mPlayerFactory.createPlayer(InCallTonePlayer.TONE_VIDEO_UPGRADE).startTone();
258        }
259    }
260
261    /**
262     * Play or stop a call hold tone for a call.  Triggered via
263     * {@link Connection#sendConnectionEvent(String)} when the
264     * {@link Connection#EVENT_ON_HOLD_TONE_START} event or
265     * {@link Connection#EVENT_ON_HOLD_TONE_STOP} event is passed through to the
266     *
267     * @param call The call which requested the hold tone.
268     */
269    @Override
270    public void onHoldToneRequested(Call call) {
271        maybePlayHoldTone();
272    }
273
274    @Override
275    public void onIsVoipAudioModeChanged(Call call) {
276        if (call != mForegroundCall) {
277            return;
278        }
279        mCallAudioModeStateMachine.sendMessageWithArgs(
280                CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE,
281                makeArgsForModeStateMachine());
282    }
283
284    @Override
285    public void onRingbackRequested(Call call, boolean shouldRingback) {
286        if (call == mForegroundCall && shouldRingback) {
287            mRingbackPlayer.startRingbackForCall(call);
288        } else {
289            mRingbackPlayer.stopRingbackForCall(call);
290        }
291    }
292
293    @Override
294    public void onIncomingCallRejected(Call call, boolean rejectWithMessage, String message) {
295        maybeStopRingingAndCallWaitingForAnsweredOrRejectedCall(call);
296    }
297
298    @Override
299    public void onIsConferencedChanged(Call call) {
300        // This indicates a conferencing change, which shouldn't impact any audio mode stuff.
301        Call parentCall = call.getParentCall();
302        if (parentCall == null) {
303            // Indicates that the call should be tracked for audio purposes. Treat it as if it were
304            // just added.
305            Log.i(LOG_TAG, "Call TC@" + call.getId() + " left conference and will" +
306                            " now be tracked by CallAudioManager.");
307            onCallAdded(call);
308        } else {
309            // The call joined a conference, so stop tracking it.
310            if (mCallStateToCalls.get(call.getState()) != null) {
311                mCallStateToCalls.get(call.getState()).remove(call);
312            }
313
314            updateForegroundCall();
315            mCalls.remove(call);
316        }
317    }
318
319    @Override
320    public void onConnectionServiceChanged(Call call, ConnectionServiceWrapper oldCs,
321            ConnectionServiceWrapper newCs) {
322        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
323                CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
324    }
325
326    @Override
327    public void onVideoStateChanged(Call call, int previousVideoState, int newVideoState) {
328        if (call != getForegroundCall()) {
329            Log.d(LOG_TAG, "Ignoring video state change from %s to %s for call %s -- not " +
330                    "foreground.", VideoProfile.videoStateToString(previousVideoState),
331                    VideoProfile.videoStateToString(newVideoState), call.getId());
332            return;
333        }
334
335        if (!VideoProfile.isVideo(previousVideoState) &&
336                mCallsManager.isSpeakerphoneAutoEnabledForVideoCalls(newVideoState)) {
337            Log.d(LOG_TAG, "Switching to speaker because call %s transitioned video state from %s" +
338                    " to %s", call.getId(), VideoProfile.videoStateToString(previousVideoState),
339                    VideoProfile.videoStateToString(newVideoState));
340            mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
341                    CallAudioRouteStateMachine.SWITCH_SPEAKER);
342        }
343    }
344
345    public CallAudioState getCallAudioState() {
346        return mCallAudioRouteStateMachine.getCurrentCallAudioState();
347    }
348
349    public Call getPossiblyHeldForegroundCall() {
350        return mForegroundCall;
351    }
352
353    public Call getForegroundCall() {
354        if (mForegroundCall != null && mForegroundCall.getState() != CallState.ON_HOLD) {
355            return mForegroundCall;
356        }
357        return null;
358    }
359
360    void toggleMute() {
361        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
362                CallAudioRouteStateMachine.TOGGLE_MUTE);
363    }
364
365    void mute(boolean shouldMute) {
366        Log.v(this, "mute, shouldMute: %b", shouldMute);
367
368        // Don't mute if there are any emergency calls.
369        if (mCallsManager.hasEmergencyCall()) {
370            shouldMute = false;
371            Log.v(this, "ignoring mute for emergency call");
372        }
373
374        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(shouldMute
375                ? CallAudioRouteStateMachine.MUTE_ON : CallAudioRouteStateMachine.MUTE_OFF);
376    }
377
378    /**
379     * Changed the audio route, for example from earpiece to speaker phone.
380     *
381     * @param route The new audio route to use. See {@link CallAudioState}.
382     */
383    void setAudioRoute(int route) {
384        Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route));
385        switch (route) {
386            case CallAudioState.ROUTE_BLUETOOTH:
387                mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
388                        CallAudioRouteStateMachine.USER_SWITCH_BLUETOOTH);
389                return;
390            case CallAudioState.ROUTE_SPEAKER:
391                mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
392                        CallAudioRouteStateMachine.USER_SWITCH_SPEAKER);
393                return;
394            case CallAudioState.ROUTE_WIRED_HEADSET:
395                mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
396                        CallAudioRouteStateMachine.USER_SWITCH_HEADSET);
397                return;
398            case CallAudioState.ROUTE_EARPIECE:
399                mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
400                        CallAudioRouteStateMachine.USER_SWITCH_EARPIECE);
401                return;
402            case CallAudioState.ROUTE_WIRED_OR_EARPIECE:
403                mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
404                        CallAudioRouteStateMachine.USER_SWITCH_BASELINE_ROUTE);
405                return;
406            default:
407                Log.wtf(this, "Invalid route specified: %d", route);
408        }
409    }
410
411    void silenceRingers() {
412        for (Call call : mRingingCalls) {
413            call.silence();
414        }
415
416        mRingingCalls.clear();
417        mRinger.stopRinging();
418        mRinger.stopCallWaiting();
419        mCallAudioModeStateMachine.sendMessageWithArgs(
420                CallAudioModeStateMachine.NO_MORE_RINGING_CALLS,
421                makeArgsForModeStateMachine());
422    }
423
424    @VisibleForTesting
425    public boolean startRinging() {
426        return mRinger.startRinging(mForegroundCall);
427    }
428
429    @VisibleForTesting
430    public void startCallWaiting() {
431        mRinger.startCallWaiting(mRingingCalls.iterator().next());
432    }
433
434    @VisibleForTesting
435    public void stopRinging() {
436        mRinger.stopRinging();
437    }
438
439    @VisibleForTesting
440    public void stopCallWaiting() {
441        mRinger.stopCallWaiting();
442    }
443
444    @VisibleForTesting
445    public void setCallAudioRouteFocusState(int focusState) {
446        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
447                CallAudioRouteStateMachine.SWITCH_FOCUS, focusState);
448    }
449
450    @VisibleForTesting
451    public CallAudioRouteStateMachine getCallAudioRouteStateMachine() {
452        return mCallAudioRouteStateMachine;
453    }
454
455    @VisibleForTesting
456    public CallAudioModeStateMachine getCallAudioModeStateMachine() {
457        return mCallAudioModeStateMachine;
458    }
459
460    void dump(IndentingPrintWriter pw) {
461        pw.println("All calls:");
462        pw.increaseIndent();
463        dumpCallsInCollection(pw, mCalls);
464        pw.decreaseIndent();
465
466        pw.println("Active dialing, or connecting calls:");
467        pw.increaseIndent();
468        dumpCallsInCollection(pw, mActiveDialingOrConnectingCalls);
469        pw.decreaseIndent();
470
471        pw.println("Ringing calls:");
472        pw.increaseIndent();
473        dumpCallsInCollection(pw, mRingingCalls);
474        pw.decreaseIndent();
475
476        pw.println("Holding calls:");
477        pw.increaseIndent();
478        dumpCallsInCollection(pw, mHoldingCalls);
479        pw.decreaseIndent();
480
481        pw.println("Foreground call:");
482        pw.println(mForegroundCall);
483    }
484
485    @VisibleForTesting
486    public void setIsTonePlaying(boolean isTonePlaying) {
487        mIsTonePlaying = isTonePlaying;
488        mCallAudioModeStateMachine.sendMessageWithArgs(
489                isTonePlaying ? CallAudioModeStateMachine.TONE_STARTED_PLAYING
490                        : CallAudioModeStateMachine.TONE_STOPPED_PLAYING,
491                makeArgsForModeStateMachine());
492    }
493
494    private void onCallLeavingState(Call call, int state) {
495        switch (state) {
496            case CallState.ACTIVE:
497            case CallState.CONNECTING:
498                onCallLeavingActiveDialingOrConnecting();
499                break;
500            case CallState.RINGING:
501                onCallLeavingRinging();
502                break;
503            case CallState.ON_HOLD:
504                onCallLeavingHold();
505                break;
506            case CallState.PULLING:
507                onCallLeavingActiveDialingOrConnecting();
508                break;
509            case CallState.DIALING:
510                stopRingbackForCall(call);
511                onCallLeavingActiveDialingOrConnecting();
512                break;
513        }
514    }
515
516    private void onCallEnteringState(Call call, int state) {
517        switch (state) {
518            case CallState.ACTIVE:
519            case CallState.CONNECTING:
520                onCallEnteringActiveDialingOrConnecting();
521                break;
522            case CallState.RINGING:
523                onCallEnteringRinging();
524                break;
525            case CallState.ON_HOLD:
526                onCallEnteringHold();
527                break;
528            case CallState.PULLING:
529                onCallEnteringActiveDialingOrConnecting();
530                break;
531            case CallState.DIALING:
532                onCallEnteringActiveDialingOrConnecting();
533                playRingbackForCall(call);
534                break;
535        }
536    }
537
538    private void onCallLeavingActiveDialingOrConnecting() {
539        if (mActiveDialingOrConnectingCalls.size() == 0) {
540            mCallAudioModeStateMachine.sendMessageWithArgs(
541                    CallAudioModeStateMachine.NO_MORE_ACTIVE_OR_DIALING_CALLS,
542                    makeArgsForModeStateMachine());
543        }
544    }
545
546    private void onCallLeavingRinging() {
547        if (mRingingCalls.size() == 0) {
548            mCallAudioModeStateMachine.sendMessageWithArgs(
549                    CallAudioModeStateMachine.NO_MORE_RINGING_CALLS,
550                    makeArgsForModeStateMachine());
551        }
552    }
553
554    private void onCallLeavingHold() {
555        if (mHoldingCalls.size() == 0) {
556            mCallAudioModeStateMachine.sendMessageWithArgs(
557                    CallAudioModeStateMachine.NO_MORE_HOLDING_CALLS,
558                    makeArgsForModeStateMachine());
559        }
560    }
561
562    private void onCallEnteringActiveDialingOrConnecting() {
563        if (mActiveDialingOrConnectingCalls.size() == 1) {
564            mCallAudioModeStateMachine.sendMessageWithArgs(
565                    CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL,
566                    makeArgsForModeStateMachine());
567        }
568    }
569
570    private void onCallEnteringRinging() {
571        if (mRingingCalls.size() == 1) {
572            mCallAudioModeStateMachine.sendMessageWithArgs(
573                    CallAudioModeStateMachine.NEW_RINGING_CALL,
574                    makeArgsForModeStateMachine());
575        }
576    }
577
578    private void onCallEnteringHold() {
579        if (mHoldingCalls.size() == 1) {
580            mCallAudioModeStateMachine.sendMessageWithArgs(
581                    CallAudioModeStateMachine.NEW_HOLDING_CALL,
582                    makeArgsForModeStateMachine());
583        }
584    }
585
586    private void updateForegroundCall() {
587        Call oldForegroundCall = mForegroundCall;
588        if (mActiveDialingOrConnectingCalls.size() > 0) {
589            // Give preference for connecting calls over active/dialing for foreground-ness.
590            Call possibleConnectingCall = null;
591            for (Call call : mActiveDialingOrConnectingCalls) {
592                if (call.getState() == CallState.CONNECTING) {
593                    possibleConnectingCall = call;
594                }
595            }
596            mForegroundCall = possibleConnectingCall == null ?
597                    mActiveDialingOrConnectingCalls.iterator().next() : possibleConnectingCall;
598        } else if (mRingingCalls.size() > 0) {
599            mForegroundCall = mRingingCalls.iterator().next();
600        } else if (mHoldingCalls.size() > 0) {
601            mForegroundCall = mHoldingCalls.iterator().next();
602        } else {
603            mForegroundCall = null;
604        }
605
606        if (mForegroundCall != oldForegroundCall) {
607            mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
608                    CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
609            mDtmfLocalTonePlayer.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
610            maybePlayHoldTone();
611        }
612    }
613
614    @NonNull
615    private CallAudioModeStateMachine.MessageArgs makeArgsForModeStateMachine() {
616        return new CallAudioModeStateMachine.MessageArgs(
617                mActiveDialingOrConnectingCalls.size() > 0,
618                mRingingCalls.size() > 0,
619                mHoldingCalls.size() > 0,
620                mIsTonePlaying,
621                mForegroundCall != null && mForegroundCall.getIsVoipAudioMode(),
622                Log.createSubsession());
623    }
624
625    private void playToneForDisconnectedCall(Call call) {
626        if (mForegroundCall != null && call != mForegroundCall && mCalls.size() > 1) {
627            Log.v(LOG_TAG, "Omitting tone because we are not foreground" +
628                    " and there is another call.");
629            return;
630        }
631
632        if (call.getDisconnectCause() != null) {
633            int toneToPlay = InCallTonePlayer.TONE_INVALID;
634
635            Log.v(this, "Disconnect cause: %s.", call.getDisconnectCause());
636
637            switch(call.getDisconnectCause().getTone()) {
638                case ToneGenerator.TONE_SUP_BUSY:
639                    toneToPlay = InCallTonePlayer.TONE_BUSY;
640                    break;
641                case ToneGenerator.TONE_SUP_CONGESTION:
642                    toneToPlay = InCallTonePlayer.TONE_CONGESTION;
643                    break;
644                case ToneGenerator.TONE_CDMA_REORDER:
645                    toneToPlay = InCallTonePlayer.TONE_REORDER;
646                    break;
647                case ToneGenerator.TONE_CDMA_ABBR_INTERCEPT:
648                    toneToPlay = InCallTonePlayer.TONE_INTERCEPT;
649                    break;
650                case ToneGenerator.TONE_CDMA_CALLDROP_LITE:
651                    toneToPlay = InCallTonePlayer.TONE_CDMA_DROP;
652                    break;
653                case ToneGenerator.TONE_SUP_ERROR:
654                    toneToPlay = InCallTonePlayer.TONE_UNOBTAINABLE_NUMBER;
655                    break;
656                case ToneGenerator.TONE_PROP_PROMPT:
657                    toneToPlay = InCallTonePlayer.TONE_CALL_ENDED;
658                    break;
659            }
660
661            Log.d(this, "Found a disconnected call with tone to play %d.", toneToPlay);
662
663            if (toneToPlay != InCallTonePlayer.TONE_INVALID) {
664                mPlayerFactory.createPlayer(toneToPlay).startTone();
665            }
666        }
667    }
668
669    private void playRingbackForCall(Call call) {
670        if (call == mForegroundCall && call.isRingbackRequested()) {
671            mRingbackPlayer.startRingbackForCall(call);
672        }
673    }
674
675    private void stopRingbackForCall(Call call) {
676        mRingbackPlayer.stopRingbackForCall(call);
677    }
678
679    /**
680     * Determines if a hold tone should be played and then starts or stops it accordingly.
681     */
682    private void maybePlayHoldTone() {
683        if (shouldPlayHoldTone()) {
684            if (mHoldTonePlayer == null) {
685                mHoldTonePlayer = mPlayerFactory.createPlayer(InCallTonePlayer.TONE_CALL_WAITING);
686                mHoldTonePlayer.startTone();
687            }
688        } else {
689            if (mHoldTonePlayer != null) {
690                mHoldTonePlayer.stopTone();
691                mHoldTonePlayer = null;
692            }
693        }
694    }
695
696    /**
697     * Determines if a hold tone should be played.
698     * A hold tone should be played only if foreground call is equals with call which is
699     * remotely held.
700     *
701     * @return {@code true} if the the hold tone should be played, {@code false} otherwise.
702     */
703    private boolean shouldPlayHoldTone() {
704        Call foregroundCall = getForegroundCall();
705        // If there is no foreground call, no hold tone should play.
706        if (foregroundCall == null) {
707            return false;
708        }
709
710        // If another call is ringing, no hold tone should play.
711        if (mCallsManager.hasRingingCall()) {
712            return false;
713        }
714
715        // If the foreground call isn't active, no hold tone should play. This might happen, for
716        // example, if the user puts a remotely held call on hold itself.
717        if (!foregroundCall.isActive()) {
718            return false;
719        }
720
721        return foregroundCall.isRemotelyHeld();
722    }
723
724    private void dumpCallsInCollection(IndentingPrintWriter pw, Collection<Call> calls) {
725        for (Call call : calls) {
726            if (call != null) pw.println(call.getId());
727        }
728    }
729
730    private void maybeStopRingingAndCallWaitingForAnsweredOrRejectedCall(Call call) {
731        // Check to see if the call being answered/rejected is the only ringing call, since this
732        // will be called before the connection service acknowledges the state change.
733        if (mRingingCalls.size() == 0 ||
734                (mRingingCalls.size() == 1 && call == mRingingCalls.iterator().next())) {
735            mRinger.stopRinging();
736            mRinger.stopCallWaiting();
737        }
738    }
739
740    private boolean shouldPlayDisconnectTone(int oldState, int newState) {
741        if (newState != CallState.DISCONNECTED) {
742            return false;
743        }
744        return oldState == CallState.ACTIVE ||
745                oldState == CallState.DIALING ||
746                oldState == CallState.ON_HOLD;
747    }
748
749    @VisibleForTesting
750    public Set<Call> getTrackedCalls() {
751        return mCalls;
752    }
753
754    @VisibleForTesting
755    public SparseArray<LinkedHashSet<Call>> getCallStateToCalls() {
756        return mCallStateToCalls;
757    }
758}