blob: 43b102b8441be568b23a3d8235a3ec7e0d5f8e5e [file] [log] [blame]
Sailesh Nepal810735e2014-03-18 18:15:46 -07001/*
2 * Copyright (C) 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.telecomm;
18
19import android.content.Context;
20import android.media.AudioManager;
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070021import android.telecomm.CallAudioState;
Sailesh Nepal810735e2014-03-18 18:15:46 -070022import android.telecomm.CallState;
23
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070024import com.google.common.base.Preconditions;
Santos Cordon1ae2b852014-03-19 03:03:10 -070025
Sailesh Nepal810735e2014-03-18 18:15:46 -070026/**
27 * This class manages audio modes, streams and other properties.
28 */
29final class CallAudioManager extends CallsManagerListenerBase {
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070030 private static final int STREAM_NONE = -1;
Santos Cordon1ae2b852014-03-19 03:03:10 -070031
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070032 private final AudioManager mAudioManager;
33 private final WiredHeadsetManager mWiredHeadsetManager;
Santos Cordonc7e85d42014-05-22 02:51:48 -070034 private final BluetoothManager mBluetoothManager;
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070035 private CallAudioState mAudioState;
36 private int mAudioFocusStreamType;
37 private boolean mIsRinging;
Santos Cordona56f2762014-03-24 15:55:53 -070038 private boolean mIsTonePlaying;
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070039 private boolean mWasSpeakerOn;
Santos Cordon1ae2b852014-03-19 03:03:10 -070040
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070041 CallAudioManager() {
42 Context context = TelecommApp.getInstance();
43 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
44 mWiredHeadsetManager = new WiredHeadsetManager(this);
Santos Cordonc7e85d42014-05-22 02:51:48 -070045 mBluetoothManager = new BluetoothManager(context, this);
46 mAudioState = getInitialAudioState(null);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070047 mAudioFocusStreamType = STREAM_NONE;
48 }
Santos Cordon1ae2b852014-03-19 03:03:10 -070049
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070050 CallAudioState getAudioState() {
51 return mAudioState;
52 }
Santos Cordon1ae2b852014-03-19 03:03:10 -070053
54 @Override
55 public void onCallAdded(Call call) {
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070056 updateAudioStreamAndMode();
57 if (CallsManager.getInstance().getCalls().size() == 1) {
58 Log.v(this, "first call added, reseting system audio to default state");
Santos Cordonc7e85d42014-05-22 02:51:48 -070059 setInitialAudioState(call);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070060 } else if (!call.isIncoming()) {
61 // Unmute new outgoing call.
62 setSystemAudioState(false, mAudioState.route, mAudioState.supportedRouteMask);
Santos Cordon1ae2b852014-03-19 03:03:10 -070063 }
Santos Cordon1ae2b852014-03-19 03:03:10 -070064 }
65
66 @Override
67 public void onCallRemoved(Call call) {
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070068 if (CallsManager.getInstance().getCalls().isEmpty()) {
69 Log.v(this, "all calls removed, reseting system audio to default state");
Santos Cordonc7e85d42014-05-22 02:51:48 -070070 setInitialAudioState(null);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070071 }
72 updateAudioStreamAndMode();
Santos Cordon1ae2b852014-03-19 03:03:10 -070073 }
74
Sailesh Nepal810735e2014-03-18 18:15:46 -070075 @Override
76 public void onCallStateChanged(Call call, CallState oldState, CallState newState) {
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070077 updateAudioStreamAndMode();
Santos Cordon1ae2b852014-03-19 03:03:10 -070078 }
79
80 @Override
81 public void onIncomingCallAnswered(Call call) {
Santos Cordonc7e85d42014-05-22 02:51:48 -070082 int route = mAudioState.route;
83
84 // We do two things:
85 // (1) If this is the first call, then we can to turn on bluetooth if available.
86 // (2) Unmute the audio for the new incoming call.
87 boolean isOnlyCall = CallsManager.getInstance().getCalls().size() == 1;
88 if (isOnlyCall && mBluetoothManager.isBluetoothAvailable()) {
89 mBluetoothManager.connectBluetoothAudio();
90 route = CallAudioState.ROUTE_BLUETOOTH;
91 }
92
93 setSystemAudioState(false /* isMute */, route, mAudioState.supportedRouteMask);
Santos Cordon1ae2b852014-03-19 03:03:10 -070094 }
95
96 @Override
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070097 public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
98 updateAudioStreamAndMode();
99 // Ensure that the foreground call knows about the latest audio state.
100 updateAudioForForegroundCall();
Santos Cordon1ae2b852014-03-19 03:03:10 -0700101 }
102
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700103 void mute(boolean shouldMute) {
104 Log.v(this, "mute, shouldMute: %b", shouldMute);
Santos Cordon1ae2b852014-03-19 03:03:10 -0700105
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700106 // Don't mute if there are any emergency calls.
107 if (CallsManager.getInstance().hasEmergencyCall()) {
108 shouldMute = false;
109 Log.v(this, "ignoring mute for emergency call");
Santos Cordon1ae2b852014-03-19 03:03:10 -0700110 }
111
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700112 if (mAudioState.isMuted != shouldMute) {
113 setSystemAudioState(shouldMute, mAudioState.route, mAudioState.supportedRouteMask);
Sailesh Nepal810735e2014-03-18 18:15:46 -0700114 }
115 }
116
Santos Cordon1ae2b852014-03-19 03:03:10 -0700117 /**
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700118 * Changed the audio route, for example from earpiece to speaker phone.
Santos Cordon1ae2b852014-03-19 03:03:10 -0700119 *
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700120 * @param route The new audio route to use. See {@link CallAudioState}.
Santos Cordon1ae2b852014-03-19 03:03:10 -0700121 */
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700122 void setAudioRoute(int route) {
123 Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route));
Santos Cordon1ae2b852014-03-19 03:03:10 -0700124
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700125 // Change ROUTE_WIRED_OR_EARPIECE to a single entry.
126 int newRoute = selectWiredOrEarpiece(route, mAudioState.supportedRouteMask);
127
128 // If route is unsupported, do nothing.
129 if ((mAudioState.supportedRouteMask | newRoute) == 0) {
130 Log.wtf(this, "Asking to set to a route that is unsupported: %d", newRoute);
131 return;
132 }
133
134 if (mAudioState.route != newRoute) {
135 // Remember the new speaker state so it can be restored when the user plugs and unplugs
136 // a headset.
137 mWasSpeakerOn = newRoute == CallAudioState.ROUTE_SPEAKER;
138 setSystemAudioState(mAudioState.isMuted, newRoute, mAudioState.supportedRouteMask);
139 }
140 }
141
142 void setIsRinging(boolean isRinging) {
143 if (mIsRinging != isRinging) {
144 Log.v(this, "setIsRinging %b -> %b", mIsRinging, isRinging);
145 mIsRinging = isRinging;
146 updateAudioStreamAndMode();
Santos Cordon1ae2b852014-03-19 03:03:10 -0700147 }
Sailesh Nepal810735e2014-03-18 18:15:46 -0700148 }
149
Santos Cordon1ae2b852014-03-19 03:03:10 -0700150 /**
Santos Cordona56f2762014-03-24 15:55:53 -0700151 * Sets the tone playing status. Some tones can play even when there are no live calls and this
152 * status indicates that we should keep audio focus even for tones that play beyond the life of
153 * calls.
154 *
155 * @param isPlayingNew The status to set.
156 */
157 void setIsTonePlaying(boolean isPlayingNew) {
158 ThreadUtil.checkOnMainThread();
159
160 if (mIsTonePlaying != isPlayingNew) {
161 Log.v(this, "mIsTonePlaying %b -> %b.", mIsTonePlaying, isPlayingNew);
162 mIsTonePlaying = isPlayingNew;
163 updateAudioStreamAndMode();
164 }
165 }
166
167 /**
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700168 * Updates the audio route when the headset plugged in state changes. For example, if audio is
169 * being routed over speakerphone and a headset is plugged in then switch to wired headset.
170 */
171 void onHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
172 int newRoute = CallAudioState.ROUTE_EARPIECE;
173 if (newIsPluggedIn) {
174 newRoute = CallAudioState.ROUTE_WIRED_HEADSET;
175 } else if (mWasSpeakerOn) {
Santos Cordon5ba7f272014-05-28 13:59:49 -0700176 Call call = getForegroundCall();
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700177 if (call != null && call.isAlive()) {
178 // Restore the speaker state.
179 newRoute = CallAudioState.ROUTE_SPEAKER;
180 }
181 }
182 setSystemAudioState(mAudioState.isMuted, newRoute, calculateSupportedRoutes());
183 }
Santos Cordon1ae2b852014-03-19 03:03:10 -0700184
Santos Cordonc7e85d42014-05-22 02:51:48 -0700185 /**
186 * Updates the audio routing according to the bluetooth state.
187 */
188 void onBluetoothStateChange(BluetoothManager bluetoothManager) {
189 int newRoute = mAudioState.route;
190 if (bluetoothManager.isBluetoothAudioConnectedOrPending()) {
191 newRoute = CallAudioState.ROUTE_BLUETOOTH;
192 } else if (mAudioState.route == CallAudioState.ROUTE_BLUETOOTH) {
193 newRoute = CallAudioState.ROUTE_WIRED_OR_EARPIECE;
194 // Do not switch to speaker when bluetooth disconnects.
195 mWasSpeakerOn = false;
196 }
197
198 setSystemAudioState(mAudioState.isMuted, newRoute, calculateSupportedRoutes());
199 }
200
201 boolean isBluetoothAudioOn() {
202 return mBluetoothManager.isBluetoothAudioConnected();
203 }
204
205 boolean isBluetoothDeviceAvailable() {
206 return mBluetoothManager.isBluetoothAvailable();
207 }
208
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700209 private void setSystemAudioState(boolean isMuted, int route, int supportedRouteMask) {
210 CallAudioState oldAudioState = mAudioState;
211 mAudioState = new CallAudioState(isMuted, route, supportedRouteMask);
212 Log.i(this, "changing audio state from %s to %s", oldAudioState, mAudioState);
Santos Cordon1ae2b852014-03-19 03:03:10 -0700213
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700214 // Mute.
215 if (mAudioState.isMuted != mAudioManager.isMicrophoneMute()) {
216 Log.i(this, "changing microphone mute state to: %b", mAudioState.isMuted);
217 mAudioManager.setMicrophoneMute(mAudioState.isMuted);
Santos Cordon1ae2b852014-03-19 03:03:10 -0700218 }
219
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700220 // Audio route.
Santos Cordonc7e85d42014-05-22 02:51:48 -0700221 if (mAudioState.route == CallAudioState.ROUTE_BLUETOOTH) {
222 turnOnSpeaker(false);
223 turnOnBluetooth(true);
224 } else if (mAudioState.route == CallAudioState.ROUTE_SPEAKER) {
225 turnOnBluetooth(false);
226 turnOnSpeaker(true);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700227 } else if (mAudioState.route == CallAudioState.ROUTE_EARPIECE ||
228 mAudioState.route == CallAudioState.ROUTE_WIRED_HEADSET) {
Santos Cordonc7e85d42014-05-22 02:51:48 -0700229 turnOnBluetooth(false);
230 turnOnSpeaker(false);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700231 }
232
233 if (!oldAudioState.equals(mAudioState)) {
234 CallsManager.getInstance().onAudioStateChanged(oldAudioState, mAudioState);
235 updateAudioForForegroundCall();
236 }
237 }
238
Santos Cordonc7e85d42014-05-22 02:51:48 -0700239 private void turnOnSpeaker(boolean on) {
240 // Wired headset and earpiece work the same way
241 if (mAudioManager.isSpeakerphoneOn() != on) {
242 Log.i(this, "turning speaker phone off");
243 mAudioManager.setSpeakerphoneOn(on);
244 }
245 }
246
247 private void turnOnBluetooth(boolean on) {
248 if (mBluetoothManager.isBluetoothAvailable()) {
249 boolean isAlreadyOn = mBluetoothManager.isBluetoothAudioConnected();
250 if (on != isAlreadyOn) {
251 if (on) {
252 mBluetoothManager.connectBluetoothAudio();
253 } else {
254 mBluetoothManager.disconnectBluetoothAudio();
255 }
256 }
257 }
258 }
259
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700260 private void updateAudioStreamAndMode() {
Santos Cordona56f2762014-03-24 15:55:53 -0700261 Log.v(this, "updateAudioStreamAndMode, mIsRinging: %b, mIsTonePlaying: %b", mIsRinging,
262 mIsTonePlaying);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700263 if (mIsRinging) {
264 requestAudioFocusAndSetMode(AudioManager.STREAM_RING, AudioManager.MODE_RINGTONE);
265 } else {
Santos Cordon5ba7f272014-05-28 13:59:49 -0700266 Call call = getForegroundCall();
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700267 if (call != null) {
268 int mode = TelephonyUtil.isCurrentlyPSTNCall(call) ?
269 AudioManager.MODE_IN_CALL : AudioManager.MODE_IN_COMMUNICATION;
270 requestAudioFocusAndSetMode(AudioManager.STREAM_VOICE_CALL, mode);
Santos Cordona56f2762014-03-24 15:55:53 -0700271 } else if (mIsTonePlaying) {
272 // There is no call, however, we are still playing a tone, so keep focus.
273 requestAudioFocusAndSetMode(
274 AudioManager.STREAM_VOICE_CALL, AudioManager.MODE_IN_COMMUNICATION);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700275 } else {
276 abandonAudioFocus();
277 }
278 }
279 }
280
281 private void requestAudioFocusAndSetMode(int stream, int mode) {
282 Log.v(this, "setSystemAudioStreamAndMode, stream: %d -> %d", mAudioFocusStreamType, stream);
283 Preconditions.checkState(stream != STREAM_NONE);
284
Santos Cordon5ba7f272014-05-28 13:59:49 -0700285 // Even if we already have focus, if the stream is different we update audio manager to give
286 // it a hint about the purpose of our focus.
287 if (mAudioFocusStreamType != stream) {
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700288 Log.v(this, "requesting audio focus for stream: %d", stream);
289 mAudioManager.requestAudioFocusForCall(stream,
290 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
291 }
292 mAudioFocusStreamType = stream;
293 setMode(mode);
294 }
295
296 private void abandonAudioFocus() {
297 if (mAudioFocusStreamType != STREAM_NONE) {
298 setMode(AudioManager.MODE_NORMAL);
299 Log.v(this, "abandoning audio focus");
300 mAudioManager.abandonAudioFocusForCall();
301 mAudioFocusStreamType = STREAM_NONE;
302 }
Santos Cordon1ae2b852014-03-19 03:03:10 -0700303 }
304
305 /**
306 * Sets the audio mode.
307 *
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700308 * @param newMode Mode constant from AudioManager.MODE_*.
Santos Cordon1ae2b852014-03-19 03:03:10 -0700309 */
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700310 private void setMode(int newMode) {
311 Preconditions.checkState(mAudioFocusStreamType != STREAM_NONE);
312 int oldMode = mAudioManager.getMode();
313 Log.v(this, "Request to change audio mode from %d to %d", oldMode, newMode);
314 if (oldMode != newMode) {
315 mAudioManager.setMode(newMode);
316 }
317 }
318
319 private int selectWiredOrEarpiece(int route, int supportedRouteMask) {
320 // Since they are mutually exclusive and one is ALWAYS valid, we allow a special input of
321 // ROUTE_WIRED_OR_EARPIECE so that callers dont have to make a call to check which is
322 // supported before calling setAudioRoute.
323 if (route == CallAudioState.ROUTE_WIRED_OR_EARPIECE) {
324 route = CallAudioState.ROUTE_WIRED_OR_EARPIECE & supportedRouteMask;
325 if (route == 0) {
326 Log.wtf(this, "One of wired headset or earpiece should always be valid.");
327 // assume earpiece in this case.
328 route = CallAudioState.ROUTE_EARPIECE;
Santos Cordon1ae2b852014-03-19 03:03:10 -0700329 }
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700330 }
331 return route;
332 }
333
334 private int calculateSupportedRoutes() {
335 int routeMask = CallAudioState.ROUTE_SPEAKER;
336
337 if (mWiredHeadsetManager.isPluggedIn()) {
338 routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
Santos Cordon1ae2b852014-03-19 03:03:10 -0700339 } else {
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700340 routeMask |= CallAudioState.ROUTE_EARPIECE;
Santos Cordon1ae2b852014-03-19 03:03:10 -0700341 }
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700342
Santos Cordonc7e85d42014-05-22 02:51:48 -0700343 if (mBluetoothManager.isBluetoothAvailable()) {
344 routeMask |= CallAudioState.ROUTE_BLUETOOTH;
345 }
346
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700347 return routeMask;
Santos Cordon1ae2b852014-03-19 03:03:10 -0700348 }
349
Santos Cordonc7e85d42014-05-22 02:51:48 -0700350 private CallAudioState getInitialAudioState(Call call) {
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700351 int supportedRouteMask = calculateSupportedRoutes();
Santos Cordonc7e85d42014-05-22 02:51:48 -0700352 int route = selectWiredOrEarpiece(
353 CallAudioState.ROUTE_WIRED_OR_EARPIECE, supportedRouteMask);
354
355 // We want the UI to indicate that "bluetooth is in use" in two slightly different cases:
356 // (a) The obvious case: if a bluetooth headset is currently in use for an ongoing call.
357 // (b) The not-so-obvious case: if an incoming call is ringing, and we expect that audio
358 // *will* be routed to a bluetooth headset once the call is answered. In this case, just
359 // check if the headset is available. Note this only applies when we are dealing with
360 // the first call.
361 if (call != null && mBluetoothManager.isBluetoothAvailable()) {
362 switch(call.getState()) {
363 case ACTIVE:
364 case ON_HOLD:
365 if (mBluetoothManager.isBluetoothAudioConnectedOrPending()) {
366 route = CallAudioState.ROUTE_BLUETOOTH;
367 }
368 break;
369 case RINGING:
370 route = CallAudioState.ROUTE_BLUETOOTH;
371 break;
372 default:
373 break;
374 }
375 }
376
377 return new CallAudioState(false, route, supportedRouteMask);
Santos Cordon1ae2b852014-03-19 03:03:10 -0700378 }
379
Santos Cordonc7e85d42014-05-22 02:51:48 -0700380 private void setInitialAudioState(Call call) {
381 CallAudioState audioState = getInitialAudioState(call);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700382 setSystemAudioState(audioState.isMuted, audioState.route, audioState.supportedRouteMask);
383 }
384
385 private void updateAudioForForegroundCall() {
386 Call call = CallsManager.getInstance().getForegroundCall();
387 if (call != null && call.getCallService() != null) {
Sailesh Nepale59bb192014-04-01 18:33:59 -0700388 call.getCallService().onAudioStateChanged(call, mAudioState);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700389 }
Sailesh Nepal810735e2014-03-18 18:15:46 -0700390 }
Santos Cordon5ba7f272014-05-28 13:59:49 -0700391
392 /**
393 * Returns the current foreground call in order to properly set the audio mode.
394 */
395 private Call getForegroundCall() {
396 Call call = CallsManager.getInstance().getForegroundCall();
397
398 // We ignore any foreground call that is in the ringing state because we deal with ringing
399 // calls exclusively through the mIsRinging variable set by {@link Ringer}.
400 if (call != null && call.getState() == CallState.RINGING) {
401 call = null;
402 }
403 return call;
404 }
Sailesh Nepal810735e2014-03-18 18:15:46 -0700405}