blob: 8156db0daab154adf45783c729ce5fb1e709f3e3 [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
Santos Cordondeb8c892014-05-30 01:38:03 -070032 private final StatusBarNotifier mStatusBarNotifier;
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070033 private final AudioManager mAudioManager;
34 private final WiredHeadsetManager mWiredHeadsetManager;
Santos Cordonc7e85d42014-05-22 02:51:48 -070035 private final BluetoothManager mBluetoothManager;
Santos Cordondeb8c892014-05-30 01:38:03 -070036
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070037 private CallAudioState mAudioState;
38 private int mAudioFocusStreamType;
39 private boolean mIsRinging;
Santos Cordona56f2762014-03-24 15:55:53 -070040 private boolean mIsTonePlaying;
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070041 private boolean mWasSpeakerOn;
Santos Cordon1ae2b852014-03-19 03:03:10 -070042
Santos Cordondeb8c892014-05-30 01:38:03 -070043 CallAudioManager(Context context, StatusBarNotifier statusBarNotifier) {
44 mStatusBarNotifier = statusBarNotifier;
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070045 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
46 mWiredHeadsetManager = new WiredHeadsetManager(this);
Santos Cordonc7e85d42014-05-22 02:51:48 -070047 mBluetoothManager = new BluetoothManager(context, this);
Santos Cordondeb8c892014-05-30 01:38:03 -070048 saveAudioState(getInitialAudioState(null));
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070049 mAudioFocusStreamType = STREAM_NONE;
50 }
Santos Cordon1ae2b852014-03-19 03:03:10 -070051
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070052 CallAudioState getAudioState() {
53 return mAudioState;
54 }
Santos Cordon1ae2b852014-03-19 03:03:10 -070055
56 @Override
57 public void onCallAdded(Call call) {
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070058 updateAudioStreamAndMode();
59 if (CallsManager.getInstance().getCalls().size() == 1) {
60 Log.v(this, "first call added, reseting system audio to default state");
Santos Cordonc7e85d42014-05-22 02:51:48 -070061 setInitialAudioState(call);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070062 } else if (!call.isIncoming()) {
63 // Unmute new outgoing call.
64 setSystemAudioState(false, mAudioState.route, mAudioState.supportedRouteMask);
Santos Cordon1ae2b852014-03-19 03:03:10 -070065 }
Santos Cordon1ae2b852014-03-19 03:03:10 -070066 }
67
68 @Override
69 public void onCallRemoved(Call call) {
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070070 if (CallsManager.getInstance().getCalls().isEmpty()) {
71 Log.v(this, "all calls removed, reseting system audio to default state");
Santos Cordonc7e85d42014-05-22 02:51:48 -070072 setInitialAudioState(null);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070073 }
74 updateAudioStreamAndMode();
Santos Cordon1ae2b852014-03-19 03:03:10 -070075 }
76
Sailesh Nepal810735e2014-03-18 18:15:46 -070077 @Override
78 public void onCallStateChanged(Call call, CallState oldState, CallState newState) {
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070079 updateAudioStreamAndMode();
Santos Cordon1ae2b852014-03-19 03:03:10 -070080 }
81
82 @Override
83 public void onIncomingCallAnswered(Call call) {
Santos Cordonc7e85d42014-05-22 02:51:48 -070084 int route = mAudioState.route;
85
86 // We do two things:
87 // (1) If this is the first call, then we can to turn on bluetooth if available.
88 // (2) Unmute the audio for the new incoming call.
89 boolean isOnlyCall = CallsManager.getInstance().getCalls().size() == 1;
90 if (isOnlyCall && mBluetoothManager.isBluetoothAvailable()) {
91 mBluetoothManager.connectBluetoothAudio();
92 route = CallAudioState.ROUTE_BLUETOOTH;
93 }
94
95 setSystemAudioState(false /* isMute */, route, mAudioState.supportedRouteMask);
Santos Cordon1ae2b852014-03-19 03:03:10 -070096 }
97
98 @Override
Sailesh Nepal6aca10a2014-03-24 16:11:02 -070099 public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
100 updateAudioStreamAndMode();
101 // Ensure that the foreground call knows about the latest audio state.
102 updateAudioForForegroundCall();
Santos Cordon1ae2b852014-03-19 03:03:10 -0700103 }
104
Sailesh Nepal7e669572014-07-08 21:29:12 -0700105 @Override
106 public void onAudioModeIsVoipChanged(Call call) {
107 updateAudioStreamAndMode();
108 }
109
Santos Cordondeb8c892014-05-30 01:38:03 -0700110 void toggleMute() {
111 mute(!mAudioState.isMuted);
112 }
113
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700114 void mute(boolean shouldMute) {
115 Log.v(this, "mute, shouldMute: %b", shouldMute);
Santos Cordon1ae2b852014-03-19 03:03:10 -0700116
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700117 // Don't mute if there are any emergency calls.
118 if (CallsManager.getInstance().hasEmergencyCall()) {
119 shouldMute = false;
120 Log.v(this, "ignoring mute for emergency call");
Santos Cordon1ae2b852014-03-19 03:03:10 -0700121 }
122
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700123 if (mAudioState.isMuted != shouldMute) {
124 setSystemAudioState(shouldMute, mAudioState.route, mAudioState.supportedRouteMask);
Sailesh Nepal810735e2014-03-18 18:15:46 -0700125 }
126 }
127
Santos Cordon1ae2b852014-03-19 03:03:10 -0700128 /**
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700129 * Changed the audio route, for example from earpiece to speaker phone.
Santos Cordon1ae2b852014-03-19 03:03:10 -0700130 *
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700131 * @param route The new audio route to use. See {@link CallAudioState}.
Santos Cordon1ae2b852014-03-19 03:03:10 -0700132 */
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700133 void setAudioRoute(int route) {
134 Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route));
Santos Cordon1ae2b852014-03-19 03:03:10 -0700135
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700136 // Change ROUTE_WIRED_OR_EARPIECE to a single entry.
137 int newRoute = selectWiredOrEarpiece(route, mAudioState.supportedRouteMask);
138
139 // If route is unsupported, do nothing.
140 if ((mAudioState.supportedRouteMask | newRoute) == 0) {
141 Log.wtf(this, "Asking to set to a route that is unsupported: %d", newRoute);
142 return;
143 }
144
145 if (mAudioState.route != newRoute) {
146 // Remember the new speaker state so it can be restored when the user plugs and unplugs
147 // a headset.
148 mWasSpeakerOn = newRoute == CallAudioState.ROUTE_SPEAKER;
149 setSystemAudioState(mAudioState.isMuted, newRoute, mAudioState.supportedRouteMask);
150 }
151 }
152
153 void setIsRinging(boolean isRinging) {
154 if (mIsRinging != isRinging) {
155 Log.v(this, "setIsRinging %b -> %b", mIsRinging, isRinging);
156 mIsRinging = isRinging;
157 updateAudioStreamAndMode();
Santos Cordon1ae2b852014-03-19 03:03:10 -0700158 }
Sailesh Nepal810735e2014-03-18 18:15:46 -0700159 }
160
Santos Cordon1ae2b852014-03-19 03:03:10 -0700161 /**
Santos Cordona56f2762014-03-24 15:55:53 -0700162 * Sets the tone playing status. Some tones can play even when there are no live calls and this
163 * status indicates that we should keep audio focus even for tones that play beyond the life of
164 * calls.
165 *
166 * @param isPlayingNew The status to set.
167 */
168 void setIsTonePlaying(boolean isPlayingNew) {
169 ThreadUtil.checkOnMainThread();
170
171 if (mIsTonePlaying != isPlayingNew) {
172 Log.v(this, "mIsTonePlaying %b -> %b.", mIsTonePlaying, isPlayingNew);
173 mIsTonePlaying = isPlayingNew;
174 updateAudioStreamAndMode();
175 }
176 }
177
178 /**
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700179 * Updates the audio route when the headset plugged in state changes. For example, if audio is
180 * being routed over speakerphone and a headset is plugged in then switch to wired headset.
181 */
182 void onHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
183 int newRoute = CallAudioState.ROUTE_EARPIECE;
184 if (newIsPluggedIn) {
185 newRoute = CallAudioState.ROUTE_WIRED_HEADSET;
186 } else if (mWasSpeakerOn) {
Santos Cordon5ba7f272014-05-28 13:59:49 -0700187 Call call = getForegroundCall();
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700188 if (call != null && call.isAlive()) {
189 // Restore the speaker state.
190 newRoute = CallAudioState.ROUTE_SPEAKER;
191 }
192 }
193 setSystemAudioState(mAudioState.isMuted, newRoute, calculateSupportedRoutes());
194 }
Santos Cordon1ae2b852014-03-19 03:03:10 -0700195
Santos Cordonc7e85d42014-05-22 02:51:48 -0700196 /**
197 * Updates the audio routing according to the bluetooth state.
198 */
199 void onBluetoothStateChange(BluetoothManager bluetoothManager) {
200 int newRoute = mAudioState.route;
201 if (bluetoothManager.isBluetoothAudioConnectedOrPending()) {
202 newRoute = CallAudioState.ROUTE_BLUETOOTH;
203 } else if (mAudioState.route == CallAudioState.ROUTE_BLUETOOTH) {
204 newRoute = CallAudioState.ROUTE_WIRED_OR_EARPIECE;
205 // Do not switch to speaker when bluetooth disconnects.
206 mWasSpeakerOn = false;
207 }
208
209 setSystemAudioState(mAudioState.isMuted, newRoute, calculateSupportedRoutes());
210 }
211
212 boolean isBluetoothAudioOn() {
213 return mBluetoothManager.isBluetoothAudioConnected();
214 }
215
216 boolean isBluetoothDeviceAvailable() {
217 return mBluetoothManager.isBluetoothAvailable();
218 }
219
Santos Cordondeb8c892014-05-30 01:38:03 -0700220 private void saveAudioState(CallAudioState audioState) {
221 mAudioState = audioState;
222 mStatusBarNotifier.notifyMute(mAudioState.isMuted);
223 mStatusBarNotifier.notifySpeakerphone(mAudioState.route == CallAudioState.ROUTE_SPEAKER);
224 }
225
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700226 private void setSystemAudioState(boolean isMuted, int route, int supportedRouteMask) {
227 CallAudioState oldAudioState = mAudioState;
Santos Cordondeb8c892014-05-30 01:38:03 -0700228 saveAudioState(new CallAudioState(isMuted, route, supportedRouteMask));
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700229 Log.i(this, "changing audio state from %s to %s", oldAudioState, mAudioState);
Santos Cordon1ae2b852014-03-19 03:03:10 -0700230
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700231 // Mute.
232 if (mAudioState.isMuted != mAudioManager.isMicrophoneMute()) {
233 Log.i(this, "changing microphone mute state to: %b", mAudioState.isMuted);
234 mAudioManager.setMicrophoneMute(mAudioState.isMuted);
Santos Cordon1ae2b852014-03-19 03:03:10 -0700235 }
236
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700237 // Audio route.
Santos Cordonc7e85d42014-05-22 02:51:48 -0700238 if (mAudioState.route == CallAudioState.ROUTE_BLUETOOTH) {
239 turnOnSpeaker(false);
240 turnOnBluetooth(true);
241 } else if (mAudioState.route == CallAudioState.ROUTE_SPEAKER) {
242 turnOnBluetooth(false);
243 turnOnSpeaker(true);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700244 } else if (mAudioState.route == CallAudioState.ROUTE_EARPIECE ||
245 mAudioState.route == CallAudioState.ROUTE_WIRED_HEADSET) {
Santos Cordonc7e85d42014-05-22 02:51:48 -0700246 turnOnBluetooth(false);
247 turnOnSpeaker(false);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700248 }
249
250 if (!oldAudioState.equals(mAudioState)) {
251 CallsManager.getInstance().onAudioStateChanged(oldAudioState, mAudioState);
252 updateAudioForForegroundCall();
253 }
254 }
255
Santos Cordonc7e85d42014-05-22 02:51:48 -0700256 private void turnOnSpeaker(boolean on) {
257 // Wired headset and earpiece work the same way
258 if (mAudioManager.isSpeakerphoneOn() != on) {
259 Log.i(this, "turning speaker phone off");
260 mAudioManager.setSpeakerphoneOn(on);
261 }
262 }
263
264 private void turnOnBluetooth(boolean on) {
265 if (mBluetoothManager.isBluetoothAvailable()) {
266 boolean isAlreadyOn = mBluetoothManager.isBluetoothAudioConnected();
267 if (on != isAlreadyOn) {
268 if (on) {
269 mBluetoothManager.connectBluetoothAudio();
270 } else {
271 mBluetoothManager.disconnectBluetoothAudio();
272 }
273 }
274 }
275 }
276
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700277 private void updateAudioStreamAndMode() {
Santos Cordona56f2762014-03-24 15:55:53 -0700278 Log.v(this, "updateAudioStreamAndMode, mIsRinging: %b, mIsTonePlaying: %b", mIsRinging,
279 mIsTonePlaying);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700280 if (mIsRinging) {
281 requestAudioFocusAndSetMode(AudioManager.STREAM_RING, AudioManager.MODE_RINGTONE);
282 } else {
Santos Cordon5ba7f272014-05-28 13:59:49 -0700283 Call call = getForegroundCall();
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700284 if (call != null) {
Sailesh Nepal7e669572014-07-08 21:29:12 -0700285 int mode = call.getAudioModeIsVoip() ?
286 AudioManager.MODE_IN_COMMUNICATION : AudioManager.MODE_IN_CALL;
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700287 requestAudioFocusAndSetMode(AudioManager.STREAM_VOICE_CALL, mode);
Santos Cordona56f2762014-03-24 15:55:53 -0700288 } else if (mIsTonePlaying) {
289 // There is no call, however, we are still playing a tone, so keep focus.
290 requestAudioFocusAndSetMode(
291 AudioManager.STREAM_VOICE_CALL, AudioManager.MODE_IN_COMMUNICATION);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700292 } else {
293 abandonAudioFocus();
294 }
295 }
296 }
297
298 private void requestAudioFocusAndSetMode(int stream, int mode) {
299 Log.v(this, "setSystemAudioStreamAndMode, stream: %d -> %d", mAudioFocusStreamType, stream);
300 Preconditions.checkState(stream != STREAM_NONE);
301
Santos Cordon5ba7f272014-05-28 13:59:49 -0700302 // Even if we already have focus, if the stream is different we update audio manager to give
303 // it a hint about the purpose of our focus.
304 if (mAudioFocusStreamType != stream) {
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700305 Log.v(this, "requesting audio focus for stream: %d", stream);
306 mAudioManager.requestAudioFocusForCall(stream,
307 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
308 }
309 mAudioFocusStreamType = stream;
310 setMode(mode);
311 }
312
313 private void abandonAudioFocus() {
314 if (mAudioFocusStreamType != STREAM_NONE) {
315 setMode(AudioManager.MODE_NORMAL);
316 Log.v(this, "abandoning audio focus");
317 mAudioManager.abandonAudioFocusForCall();
318 mAudioFocusStreamType = STREAM_NONE;
319 }
Santos Cordon1ae2b852014-03-19 03:03:10 -0700320 }
321
322 /**
323 * Sets the audio mode.
324 *
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700325 * @param newMode Mode constant from AudioManager.MODE_*.
Santos Cordon1ae2b852014-03-19 03:03:10 -0700326 */
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700327 private void setMode(int newMode) {
328 Preconditions.checkState(mAudioFocusStreamType != STREAM_NONE);
329 int oldMode = mAudioManager.getMode();
330 Log.v(this, "Request to change audio mode from %d to %d", oldMode, newMode);
331 if (oldMode != newMode) {
332 mAudioManager.setMode(newMode);
333 }
334 }
335
336 private int selectWiredOrEarpiece(int route, int supportedRouteMask) {
337 // Since they are mutually exclusive and one is ALWAYS valid, we allow a special input of
338 // ROUTE_WIRED_OR_EARPIECE so that callers dont have to make a call to check which is
339 // supported before calling setAudioRoute.
340 if (route == CallAudioState.ROUTE_WIRED_OR_EARPIECE) {
341 route = CallAudioState.ROUTE_WIRED_OR_EARPIECE & supportedRouteMask;
342 if (route == 0) {
343 Log.wtf(this, "One of wired headset or earpiece should always be valid.");
344 // assume earpiece in this case.
345 route = CallAudioState.ROUTE_EARPIECE;
Santos Cordon1ae2b852014-03-19 03:03:10 -0700346 }
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700347 }
348 return route;
349 }
350
351 private int calculateSupportedRoutes() {
352 int routeMask = CallAudioState.ROUTE_SPEAKER;
353
354 if (mWiredHeadsetManager.isPluggedIn()) {
355 routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
Santos Cordon1ae2b852014-03-19 03:03:10 -0700356 } else {
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700357 routeMask |= CallAudioState.ROUTE_EARPIECE;
Santos Cordon1ae2b852014-03-19 03:03:10 -0700358 }
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700359
Santos Cordonc7e85d42014-05-22 02:51:48 -0700360 if (mBluetoothManager.isBluetoothAvailable()) {
361 routeMask |= CallAudioState.ROUTE_BLUETOOTH;
362 }
363
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700364 return routeMask;
Santos Cordon1ae2b852014-03-19 03:03:10 -0700365 }
366
Santos Cordonc7e85d42014-05-22 02:51:48 -0700367 private CallAudioState getInitialAudioState(Call call) {
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700368 int supportedRouteMask = calculateSupportedRoutes();
Santos Cordonc7e85d42014-05-22 02:51:48 -0700369 int route = selectWiredOrEarpiece(
370 CallAudioState.ROUTE_WIRED_OR_EARPIECE, supportedRouteMask);
371
372 // We want the UI to indicate that "bluetooth is in use" in two slightly different cases:
373 // (a) The obvious case: if a bluetooth headset is currently in use for an ongoing call.
374 // (b) The not-so-obvious case: if an incoming call is ringing, and we expect that audio
375 // *will* be routed to a bluetooth headset once the call is answered. In this case, just
376 // check if the headset is available. Note this only applies when we are dealing with
377 // the first call.
378 if (call != null && mBluetoothManager.isBluetoothAvailable()) {
379 switch(call.getState()) {
380 case ACTIVE:
381 case ON_HOLD:
382 if (mBluetoothManager.isBluetoothAudioConnectedOrPending()) {
383 route = CallAudioState.ROUTE_BLUETOOTH;
384 }
385 break;
386 case RINGING:
387 route = CallAudioState.ROUTE_BLUETOOTH;
388 break;
389 default:
390 break;
391 }
392 }
393
394 return new CallAudioState(false, route, supportedRouteMask);
Santos Cordon1ae2b852014-03-19 03:03:10 -0700395 }
396
Santos Cordonc7e85d42014-05-22 02:51:48 -0700397 private void setInitialAudioState(Call call) {
398 CallAudioState audioState = getInitialAudioState(call);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700399 setSystemAudioState(audioState.isMuted, audioState.route, audioState.supportedRouteMask);
400 }
401
402 private void updateAudioForForegroundCall() {
403 Call call = CallsManager.getInstance().getForegroundCall();
Sailesh Nepalc92c4362014-07-04 18:33:21 -0700404 if (call != null && call.getConnectionService() != null) {
405 call.getConnectionService().onAudioStateChanged(call, mAudioState);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700406 }
Sailesh Nepal810735e2014-03-18 18:15:46 -0700407 }
Santos Cordon5ba7f272014-05-28 13:59:49 -0700408
409 /**
410 * Returns the current foreground call in order to properly set the audio mode.
411 */
412 private Call getForegroundCall() {
413 Call call = CallsManager.getInstance().getForegroundCall();
414
415 // We ignore any foreground call that is in the ringing state because we deal with ringing
416 // calls exclusively through the mIsRinging variable set by {@link Ringer}.
417 if (call != null && call.getState() == CallState.RINGING) {
418 call = null;
419 }
420 return call;
421 }
Sailesh Nepal810735e2014-03-18 18:15:46 -0700422}