blob: 35de0cc2ac49e220da7c374e573c30a659688cb1 [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
Santos Cordondeb8c892014-05-30 01:38:03 -0700105 void toggleMute() {
106 mute(!mAudioState.isMuted);
107 }
108
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700109 void mute(boolean shouldMute) {
110 Log.v(this, "mute, shouldMute: %b", shouldMute);
Santos Cordon1ae2b852014-03-19 03:03:10 -0700111
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700112 // Don't mute if there are any emergency calls.
113 if (CallsManager.getInstance().hasEmergencyCall()) {
114 shouldMute = false;
115 Log.v(this, "ignoring mute for emergency call");
Santos Cordon1ae2b852014-03-19 03:03:10 -0700116 }
117
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700118 if (mAudioState.isMuted != shouldMute) {
119 setSystemAudioState(shouldMute, mAudioState.route, mAudioState.supportedRouteMask);
Sailesh Nepal810735e2014-03-18 18:15:46 -0700120 }
121 }
122
Santos Cordon1ae2b852014-03-19 03:03:10 -0700123 /**
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700124 * Changed the audio route, for example from earpiece to speaker phone.
Santos Cordon1ae2b852014-03-19 03:03:10 -0700125 *
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700126 * @param route The new audio route to use. See {@link CallAudioState}.
Santos Cordon1ae2b852014-03-19 03:03:10 -0700127 */
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700128 void setAudioRoute(int route) {
129 Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route));
Santos Cordon1ae2b852014-03-19 03:03:10 -0700130
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700131 // Change ROUTE_WIRED_OR_EARPIECE to a single entry.
132 int newRoute = selectWiredOrEarpiece(route, mAudioState.supportedRouteMask);
133
134 // If route is unsupported, do nothing.
135 if ((mAudioState.supportedRouteMask | newRoute) == 0) {
136 Log.wtf(this, "Asking to set to a route that is unsupported: %d", newRoute);
137 return;
138 }
139
140 if (mAudioState.route != newRoute) {
141 // Remember the new speaker state so it can be restored when the user plugs and unplugs
142 // a headset.
143 mWasSpeakerOn = newRoute == CallAudioState.ROUTE_SPEAKER;
144 setSystemAudioState(mAudioState.isMuted, newRoute, mAudioState.supportedRouteMask);
145 }
146 }
147
148 void setIsRinging(boolean isRinging) {
149 if (mIsRinging != isRinging) {
150 Log.v(this, "setIsRinging %b -> %b", mIsRinging, isRinging);
151 mIsRinging = isRinging;
152 updateAudioStreamAndMode();
Santos Cordon1ae2b852014-03-19 03:03:10 -0700153 }
Sailesh Nepal810735e2014-03-18 18:15:46 -0700154 }
155
Santos Cordon1ae2b852014-03-19 03:03:10 -0700156 /**
Santos Cordona56f2762014-03-24 15:55:53 -0700157 * Sets the tone playing status. Some tones can play even when there are no live calls and this
158 * status indicates that we should keep audio focus even for tones that play beyond the life of
159 * calls.
160 *
161 * @param isPlayingNew The status to set.
162 */
163 void setIsTonePlaying(boolean isPlayingNew) {
164 ThreadUtil.checkOnMainThread();
165
166 if (mIsTonePlaying != isPlayingNew) {
167 Log.v(this, "mIsTonePlaying %b -> %b.", mIsTonePlaying, isPlayingNew);
168 mIsTonePlaying = isPlayingNew;
169 updateAudioStreamAndMode();
170 }
171 }
172
173 /**
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700174 * Updates the audio route when the headset plugged in state changes. For example, if audio is
175 * being routed over speakerphone and a headset is plugged in then switch to wired headset.
176 */
177 void onHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
178 int newRoute = CallAudioState.ROUTE_EARPIECE;
179 if (newIsPluggedIn) {
180 newRoute = CallAudioState.ROUTE_WIRED_HEADSET;
181 } else if (mWasSpeakerOn) {
Santos Cordon5ba7f272014-05-28 13:59:49 -0700182 Call call = getForegroundCall();
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700183 if (call != null && call.isAlive()) {
184 // Restore the speaker state.
185 newRoute = CallAudioState.ROUTE_SPEAKER;
186 }
187 }
188 setSystemAudioState(mAudioState.isMuted, newRoute, calculateSupportedRoutes());
189 }
Santos Cordon1ae2b852014-03-19 03:03:10 -0700190
Santos Cordonc7e85d42014-05-22 02:51:48 -0700191 /**
192 * Updates the audio routing according to the bluetooth state.
193 */
194 void onBluetoothStateChange(BluetoothManager bluetoothManager) {
195 int newRoute = mAudioState.route;
196 if (bluetoothManager.isBluetoothAudioConnectedOrPending()) {
197 newRoute = CallAudioState.ROUTE_BLUETOOTH;
198 } else if (mAudioState.route == CallAudioState.ROUTE_BLUETOOTH) {
199 newRoute = CallAudioState.ROUTE_WIRED_OR_EARPIECE;
200 // Do not switch to speaker when bluetooth disconnects.
201 mWasSpeakerOn = false;
202 }
203
204 setSystemAudioState(mAudioState.isMuted, newRoute, calculateSupportedRoutes());
205 }
206
207 boolean isBluetoothAudioOn() {
208 return mBluetoothManager.isBluetoothAudioConnected();
209 }
210
211 boolean isBluetoothDeviceAvailable() {
212 return mBluetoothManager.isBluetoothAvailable();
213 }
214
Santos Cordondeb8c892014-05-30 01:38:03 -0700215 private void saveAudioState(CallAudioState audioState) {
216 mAudioState = audioState;
217 mStatusBarNotifier.notifyMute(mAudioState.isMuted);
218 mStatusBarNotifier.notifySpeakerphone(mAudioState.route == CallAudioState.ROUTE_SPEAKER);
219 }
220
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700221 private void setSystemAudioState(boolean isMuted, int route, int supportedRouteMask) {
222 CallAudioState oldAudioState = mAudioState;
Santos Cordondeb8c892014-05-30 01:38:03 -0700223 saveAudioState(new CallAudioState(isMuted, route, supportedRouteMask));
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700224 Log.i(this, "changing audio state from %s to %s", oldAudioState, mAudioState);
Santos Cordon1ae2b852014-03-19 03:03:10 -0700225
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700226 // Mute.
227 if (mAudioState.isMuted != mAudioManager.isMicrophoneMute()) {
228 Log.i(this, "changing microphone mute state to: %b", mAudioState.isMuted);
229 mAudioManager.setMicrophoneMute(mAudioState.isMuted);
Santos Cordon1ae2b852014-03-19 03:03:10 -0700230 }
231
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700232 // Audio route.
Santos Cordonc7e85d42014-05-22 02:51:48 -0700233 if (mAudioState.route == CallAudioState.ROUTE_BLUETOOTH) {
234 turnOnSpeaker(false);
235 turnOnBluetooth(true);
236 } else if (mAudioState.route == CallAudioState.ROUTE_SPEAKER) {
237 turnOnBluetooth(false);
238 turnOnSpeaker(true);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700239 } else if (mAudioState.route == CallAudioState.ROUTE_EARPIECE ||
240 mAudioState.route == CallAudioState.ROUTE_WIRED_HEADSET) {
Santos Cordonc7e85d42014-05-22 02:51:48 -0700241 turnOnBluetooth(false);
242 turnOnSpeaker(false);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700243 }
244
245 if (!oldAudioState.equals(mAudioState)) {
246 CallsManager.getInstance().onAudioStateChanged(oldAudioState, mAudioState);
247 updateAudioForForegroundCall();
248 }
249 }
250
Santos Cordonc7e85d42014-05-22 02:51:48 -0700251 private void turnOnSpeaker(boolean on) {
252 // Wired headset and earpiece work the same way
253 if (mAudioManager.isSpeakerphoneOn() != on) {
254 Log.i(this, "turning speaker phone off");
255 mAudioManager.setSpeakerphoneOn(on);
256 }
257 }
258
259 private void turnOnBluetooth(boolean on) {
260 if (mBluetoothManager.isBluetoothAvailable()) {
261 boolean isAlreadyOn = mBluetoothManager.isBluetoothAudioConnected();
262 if (on != isAlreadyOn) {
263 if (on) {
264 mBluetoothManager.connectBluetoothAudio();
265 } else {
266 mBluetoothManager.disconnectBluetoothAudio();
267 }
268 }
269 }
270 }
271
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700272 private void updateAudioStreamAndMode() {
Santos Cordona56f2762014-03-24 15:55:53 -0700273 Log.v(this, "updateAudioStreamAndMode, mIsRinging: %b, mIsTonePlaying: %b", mIsRinging,
274 mIsTonePlaying);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700275 if (mIsRinging) {
276 requestAudioFocusAndSetMode(AudioManager.STREAM_RING, AudioManager.MODE_RINGTONE);
277 } else {
Santos Cordon5ba7f272014-05-28 13:59:49 -0700278 Call call = getForegroundCall();
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700279 if (call != null) {
280 int mode = TelephonyUtil.isCurrentlyPSTNCall(call) ?
281 AudioManager.MODE_IN_CALL : AudioManager.MODE_IN_COMMUNICATION;
282 requestAudioFocusAndSetMode(AudioManager.STREAM_VOICE_CALL, mode);
Santos Cordona56f2762014-03-24 15:55:53 -0700283 } else if (mIsTonePlaying) {
284 // There is no call, however, we are still playing a tone, so keep focus.
285 requestAudioFocusAndSetMode(
286 AudioManager.STREAM_VOICE_CALL, AudioManager.MODE_IN_COMMUNICATION);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700287 } else {
288 abandonAudioFocus();
289 }
290 }
291 }
292
293 private void requestAudioFocusAndSetMode(int stream, int mode) {
294 Log.v(this, "setSystemAudioStreamAndMode, stream: %d -> %d", mAudioFocusStreamType, stream);
295 Preconditions.checkState(stream != STREAM_NONE);
296
Santos Cordon5ba7f272014-05-28 13:59:49 -0700297 // Even if we already have focus, if the stream is different we update audio manager to give
298 // it a hint about the purpose of our focus.
299 if (mAudioFocusStreamType != stream) {
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700300 Log.v(this, "requesting audio focus for stream: %d", stream);
301 mAudioManager.requestAudioFocusForCall(stream,
302 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
303 }
304 mAudioFocusStreamType = stream;
305 setMode(mode);
306 }
307
308 private void abandonAudioFocus() {
309 if (mAudioFocusStreamType != STREAM_NONE) {
310 setMode(AudioManager.MODE_NORMAL);
311 Log.v(this, "abandoning audio focus");
312 mAudioManager.abandonAudioFocusForCall();
313 mAudioFocusStreamType = STREAM_NONE;
314 }
Santos Cordon1ae2b852014-03-19 03:03:10 -0700315 }
316
317 /**
318 * Sets the audio mode.
319 *
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700320 * @param newMode Mode constant from AudioManager.MODE_*.
Santos Cordon1ae2b852014-03-19 03:03:10 -0700321 */
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700322 private void setMode(int newMode) {
323 Preconditions.checkState(mAudioFocusStreamType != STREAM_NONE);
324 int oldMode = mAudioManager.getMode();
325 Log.v(this, "Request to change audio mode from %d to %d", oldMode, newMode);
326 if (oldMode != newMode) {
327 mAudioManager.setMode(newMode);
328 }
329 }
330
331 private int selectWiredOrEarpiece(int route, int supportedRouteMask) {
332 // Since they are mutually exclusive and one is ALWAYS valid, we allow a special input of
333 // ROUTE_WIRED_OR_EARPIECE so that callers dont have to make a call to check which is
334 // supported before calling setAudioRoute.
335 if (route == CallAudioState.ROUTE_WIRED_OR_EARPIECE) {
336 route = CallAudioState.ROUTE_WIRED_OR_EARPIECE & supportedRouteMask;
337 if (route == 0) {
338 Log.wtf(this, "One of wired headset or earpiece should always be valid.");
339 // assume earpiece in this case.
340 route = CallAudioState.ROUTE_EARPIECE;
Santos Cordon1ae2b852014-03-19 03:03:10 -0700341 }
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700342 }
343 return route;
344 }
345
346 private int calculateSupportedRoutes() {
347 int routeMask = CallAudioState.ROUTE_SPEAKER;
348
349 if (mWiredHeadsetManager.isPluggedIn()) {
350 routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
Santos Cordon1ae2b852014-03-19 03:03:10 -0700351 } else {
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700352 routeMask |= CallAudioState.ROUTE_EARPIECE;
Santos Cordon1ae2b852014-03-19 03:03:10 -0700353 }
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700354
Santos Cordonc7e85d42014-05-22 02:51:48 -0700355 if (mBluetoothManager.isBluetoothAvailable()) {
356 routeMask |= CallAudioState.ROUTE_BLUETOOTH;
357 }
358
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700359 return routeMask;
Santos Cordon1ae2b852014-03-19 03:03:10 -0700360 }
361
Santos Cordonc7e85d42014-05-22 02:51:48 -0700362 private CallAudioState getInitialAudioState(Call call) {
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700363 int supportedRouteMask = calculateSupportedRoutes();
Santos Cordonc7e85d42014-05-22 02:51:48 -0700364 int route = selectWiredOrEarpiece(
365 CallAudioState.ROUTE_WIRED_OR_EARPIECE, supportedRouteMask);
366
367 // We want the UI to indicate that "bluetooth is in use" in two slightly different cases:
368 // (a) The obvious case: if a bluetooth headset is currently in use for an ongoing call.
369 // (b) The not-so-obvious case: if an incoming call is ringing, and we expect that audio
370 // *will* be routed to a bluetooth headset once the call is answered. In this case, just
371 // check if the headset is available. Note this only applies when we are dealing with
372 // the first call.
373 if (call != null && mBluetoothManager.isBluetoothAvailable()) {
374 switch(call.getState()) {
375 case ACTIVE:
376 case ON_HOLD:
377 if (mBluetoothManager.isBluetoothAudioConnectedOrPending()) {
378 route = CallAudioState.ROUTE_BLUETOOTH;
379 }
380 break;
381 case RINGING:
382 route = CallAudioState.ROUTE_BLUETOOTH;
383 break;
384 default:
385 break;
386 }
387 }
388
389 return new CallAudioState(false, route, supportedRouteMask);
Santos Cordon1ae2b852014-03-19 03:03:10 -0700390 }
391
Santos Cordonc7e85d42014-05-22 02:51:48 -0700392 private void setInitialAudioState(Call call) {
393 CallAudioState audioState = getInitialAudioState(call);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700394 setSystemAudioState(audioState.isMuted, audioState.route, audioState.supportedRouteMask);
395 }
396
397 private void updateAudioForForegroundCall() {
398 Call call = CallsManager.getInstance().getForegroundCall();
Sailesh Nepalc92c4362014-07-04 18:33:21 -0700399 if (call != null && call.getConnectionService() != null) {
400 call.getConnectionService().onAudioStateChanged(call, mAudioState);
Sailesh Nepal6aca10a2014-03-24 16:11:02 -0700401 }
Sailesh Nepal810735e2014-03-18 18:15:46 -0700402 }
Santos Cordon5ba7f272014-05-28 13:59:49 -0700403
404 /**
405 * Returns the current foreground call in order to properly set the audio mode.
406 */
407 private Call getForegroundCall() {
408 Call call = CallsManager.getInstance().getForegroundCall();
409
410 // We ignore any foreground call that is in the ringing state because we deal with ringing
411 // calls exclusively through the mIsRinging variable set by {@link Ringer}.
412 if (call != null && call.getState() == CallState.RINGING) {
413 call = null;
414 }
415 return call;
416 }
Sailesh Nepal810735e2014-03-18 18:15:46 -0700417}