1/*
2 * Copyright 2014, 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.media.AudioManager;
20import android.media.ToneGenerator;
21import android.os.Handler;
22import android.os.Looper;
23
24import com.android.internal.annotations.VisibleForTesting;
25
26/**
27 * Play a call-related tone (ringback, busy signal, etc.) through ToneGenerator. To use, create an
28 * instance using InCallTonePlayer.Factory (passing in the TONE_* constant for the tone you want)
29 * and start() it. Implemented on top of {@link Thread} so that the tone plays in its own thread.
30 */
31public class InCallTonePlayer extends Thread {
32
33    /**
34     * Factory used to create InCallTonePlayers. Exists to aid with testing mocks.
35     */
36    public static class Factory {
37        private CallAudioManager mCallAudioManager;
38        private final CallAudioRoutePeripheralAdapter mCallAudioRoutePeripheralAdapter;
39        private final TelecomSystem.SyncRoot mLock;
40
41        Factory(CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter,
42                TelecomSystem.SyncRoot lock) {
43            mCallAudioRoutePeripheralAdapter = callAudioRoutePeripheralAdapter;
44            mLock = lock;
45        }
46
47        public void setCallAudioManager(CallAudioManager callAudioManager) {
48            mCallAudioManager = callAudioManager;
49        }
50
51        public InCallTonePlayer createPlayer(int tone) {
52            return new InCallTonePlayer(tone, mCallAudioManager,
53                    mCallAudioRoutePeripheralAdapter, mLock);
54        }
55    }
56
57    // The possible tones that we can play.
58    public static final int TONE_INVALID = 0;
59    public static final int TONE_BUSY = 1;
60    public static final int TONE_CALL_ENDED = 2;
61    public static final int TONE_OTA_CALL_ENDED = 3;
62    public static final int TONE_CALL_WAITING = 4;
63    public static final int TONE_CDMA_DROP = 5;
64    public static final int TONE_CONGESTION = 6;
65    public static final int TONE_INTERCEPT = 7;
66    public static final int TONE_OUT_OF_SERVICE = 8;
67    public static final int TONE_REDIAL = 9;
68    public static final int TONE_REORDER = 10;
69    public static final int TONE_RING_BACK = 11;
70    public static final int TONE_UNOBTAINABLE_NUMBER = 12;
71    public static final int TONE_VOICE_PRIVACY = 13;
72    public static final int TONE_VIDEO_UPGRADE = 14;
73
74    private static final int RELATIVE_VOLUME_EMERGENCY = 100;
75    private static final int RELATIVE_VOLUME_HIPRI = 80;
76    private static final int RELATIVE_VOLUME_LOPRI = 50;
77
78    // Buffer time (in msec) to add on to the tone timeout value. Needed mainly when the timeout
79    // value for a tone is exact duration of the tone itself.
80    private static final int TIMEOUT_BUFFER_MILLIS = 20;
81
82    // The tone state.
83    private static final int STATE_OFF = 0;
84    private static final int STATE_ON = 1;
85    private static final int STATE_STOPPED = 2;
86
87    /**
88     * Keeps count of the number of actively playing tones so that we can notify CallAudioManager
89     * when we need focus and when it can be release. This should only be manipulated from the main
90     * thread.
91     */
92    private static int sTonesPlaying = 0;
93
94    private final CallAudioManager mCallAudioManager;
95    private final CallAudioRoutePeripheralAdapter mCallAudioRoutePeripheralAdapter;
96
97    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
98
99    /** The ID of the tone to play. */
100    private final int mToneId;
101
102    /** Current state of the tone player. */
103    private int mState;
104
105    /** Telecom lock object. */
106    private final TelecomSystem.SyncRoot mLock;
107
108    private Session mSession;
109    private final Object mSessionLock = new Object();
110
111    /**
112     * Initializes the tone player. Private; use the {@link Factory} to create tone players.
113     *
114     * @param toneId ID of the tone to play, see TONE_* constants.
115     */
116    private InCallTonePlayer(
117            int toneId,
118            CallAudioManager callAudioManager,
119            CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter,
120            TelecomSystem.SyncRoot lock) {
121        mState = STATE_OFF;
122        mToneId = toneId;
123        mCallAudioManager = callAudioManager;
124        mCallAudioRoutePeripheralAdapter = callAudioRoutePeripheralAdapter;
125        mLock = lock;
126    }
127
128    /** {@inheritDoc} */
129    @Override
130    public void run() {
131        ToneGenerator toneGenerator = null;
132        try {
133            synchronized (mSessionLock) {
134                if (mSession != null) {
135                    Log.continueSession(mSession, "ICTP.r");
136                    mSession = null;
137                }
138            }
139            Log.d(this, "run(toneId = %s)", mToneId);
140
141            final int toneType;  // Passed to ToneGenerator.startTone.
142            final int toneVolume;  // Passed to the ToneGenerator constructor.
143            final int toneLengthMillis;
144
145            switch (mToneId) {
146                case TONE_BUSY:
147                    // TODO: CDMA-specific tones
148                    toneType = ToneGenerator.TONE_SUP_BUSY;
149                    toneVolume = RELATIVE_VOLUME_HIPRI;
150                    toneLengthMillis = 4000;
151                    break;
152                case TONE_CALL_ENDED:
153                    toneType = ToneGenerator.TONE_PROP_PROMPT;
154                    toneVolume = RELATIVE_VOLUME_HIPRI;
155                    toneLengthMillis = 200;
156                    break;
157                case TONE_OTA_CALL_ENDED:
158                    // TODO: fill in
159                    throw new IllegalStateException("OTA Call ended NYI.");
160                case TONE_CALL_WAITING:
161                    toneType = ToneGenerator.TONE_SUP_CALL_WAITING;
162                    toneVolume = RELATIVE_VOLUME_HIPRI;
163                    toneLengthMillis = Integer.MAX_VALUE - TIMEOUT_BUFFER_MILLIS;
164                    break;
165                case TONE_CDMA_DROP:
166                    toneType = ToneGenerator.TONE_CDMA_CALLDROP_LITE;
167                    toneVolume = RELATIVE_VOLUME_LOPRI;
168                    toneLengthMillis = 375;
169                    break;
170                case TONE_CONGESTION:
171                    toneType = ToneGenerator.TONE_SUP_CONGESTION;
172                    toneVolume = RELATIVE_VOLUME_HIPRI;
173                    toneLengthMillis = 4000;
174                    break;
175                case TONE_INTERCEPT:
176                    toneType = ToneGenerator.TONE_CDMA_ABBR_INTERCEPT;
177                    toneVolume = RELATIVE_VOLUME_LOPRI;
178                    toneLengthMillis = 500;
179                    break;
180                case TONE_OUT_OF_SERVICE:
181                    toneType = ToneGenerator.TONE_CDMA_CALLDROP_LITE;
182                    toneVolume = RELATIVE_VOLUME_LOPRI;
183                    toneLengthMillis = 375;
184                    break;
185                case TONE_REDIAL:
186                    toneType = ToneGenerator.TONE_CDMA_ALERT_AUTOREDIAL_LITE;
187                    toneVolume = RELATIVE_VOLUME_LOPRI;
188                    toneLengthMillis = 5000;
189                    break;
190                case TONE_REORDER:
191                    toneType = ToneGenerator.TONE_CDMA_REORDER;
192                    toneVolume = RELATIVE_VOLUME_HIPRI;
193                    toneLengthMillis = 4000;
194                    break;
195                case TONE_RING_BACK:
196                    toneType = ToneGenerator.TONE_SUP_RINGTONE;
197                    toneVolume = RELATIVE_VOLUME_HIPRI;
198                    toneLengthMillis = Integer.MAX_VALUE - TIMEOUT_BUFFER_MILLIS;
199                    break;
200                case TONE_UNOBTAINABLE_NUMBER:
201                    toneType = ToneGenerator.TONE_SUP_ERROR;
202                    toneVolume = RELATIVE_VOLUME_HIPRI;
203                    toneLengthMillis = 4000;
204                    break;
205                case TONE_VOICE_PRIVACY:
206                    // TODO: fill in.
207                    throw new IllegalStateException("Voice privacy tone NYI.");
208                case TONE_VIDEO_UPGRADE:
209                    // Similar to the call waiting tone, but does not repeat.
210                    toneType = ToneGenerator.TONE_SUP_CALL_WAITING;
211                    toneVolume = RELATIVE_VOLUME_HIPRI;
212                    toneLengthMillis = 4000;
213                    break;
214                default:
215                    throw new IllegalStateException("Bad toneId: " + mToneId);
216            }
217
218            int stream = AudioManager.STREAM_VOICE_CALL;
219            if (mCallAudioRoutePeripheralAdapter.isBluetoothAudioOn()) {
220                stream = AudioManager.STREAM_BLUETOOTH_SCO;
221            }
222
223            // If the ToneGenerator creation fails, just continue without it. It is a local audio
224            // signal, and is not as important.
225            try {
226                Log.v(this, "Creating generator");
227                toneGenerator = new ToneGenerator(stream, toneVolume);
228            } catch (RuntimeException e) {
229                Log.w(this, "Failed to create ToneGenerator.", e);
230                return;
231            }
232
233            // TODO: Certain CDMA tones need to check the ringer-volume state before
234            // playing. See CallNotifier.InCallTonePlayer.
235
236            // TODO: Some tones play through the end of a call so we need to inform
237            // CallAudioManager that we want focus the same way that Ringer does.
238
239            synchronized (this) {
240                if (mState != STATE_STOPPED) {
241                    mState = STATE_ON;
242                    toneGenerator.startTone(toneType);
243                    try {
244                        Log.v(this, "Starting tone %d...waiting for %d ms.", mToneId,
245                                toneLengthMillis + TIMEOUT_BUFFER_MILLIS);
246                        wait(toneLengthMillis + TIMEOUT_BUFFER_MILLIS);
247                    } catch (InterruptedException e) {
248                        Log.w(this, "wait interrupted", e);
249                    }
250                }
251            }
252            mState = STATE_OFF;
253        } finally {
254            if (toneGenerator != null) {
255                toneGenerator.release();
256            }
257            cleanUpTonePlayer();
258            Log.endSession();
259        }
260    }
261
262    @VisibleForTesting
263    public void startTone() {
264        sTonesPlaying++;
265        if (sTonesPlaying == 1) {
266            mCallAudioManager.setIsTonePlaying(true);
267        }
268
269        synchronized (mSessionLock) {
270            if (mSession != null) {
271                Log.cancelSubsession(mSession);
272            }
273            mSession = Log.createSubsession();
274        }
275
276        super.start();
277    }
278
279    @Override
280    public void start() {
281        Log.w(this, "Do not call the start method directly; use startTone instead.");
282    }
283
284    /**
285     * Stops the tone.
286     */
287    @VisibleForTesting
288    public void stopTone() {
289        synchronized (this) {
290            if (mState == STATE_ON) {
291                Log.d(this, "Stopping the tone %d.", mToneId);
292                notify();
293            }
294            mState = STATE_STOPPED;
295        }
296    }
297
298    private void cleanUpTonePlayer() {
299        // Release focus on the main thread.
300        mMainThreadHandler.post(new Runnable("ICTP.cUTP", mLock) {
301            @Override
302            public void loggedRun() {
303                if (sTonesPlaying == 0) {
304                    Log.wtf(this, "Over-releasing focus for tone player.");
305                } else if (--sTonesPlaying == 0) {
306                    mCallAudioManager.setIsTonePlaying(false);
307                }
308            }
309        }.prepare());
310    }
311}
312