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 17 package com.android.dialer.app.voicemail; 18 19 import android.content.Context; 20 import android.media.AudioDeviceInfo; 21 import android.media.AudioManager; 22 import android.media.AudioManager.OnAudioFocusChangeListener; 23 import android.telecom.CallAudioState; 24 import com.android.dialer.common.LogUtil; 25 import java.util.concurrent.RejectedExecutionException; 26 27 /** This class manages all audio changes for voicemail playback. */ 28 public final class VoicemailAudioManager 29 implements OnAudioFocusChangeListener, WiredHeadsetManager.Listener { 30 31 private static final String TAG = "VoicemailAudioManager"; 32 33 public static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL; 34 35 private AudioManager mAudioManager; 36 private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; 37 private WiredHeadsetManager mWiredHeadsetManager; 38 private boolean mWasSpeakerOn; 39 private CallAudioState mCallAudioState; 40 private boolean mBluetoothScoEnabled; 41 VoicemailAudioManager( Context context, VoicemailPlaybackPresenter voicemailPlaybackPresenter)42 public VoicemailAudioManager( 43 Context context, VoicemailPlaybackPresenter voicemailPlaybackPresenter) { 44 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 45 mVoicemailPlaybackPresenter = voicemailPlaybackPresenter; 46 mWiredHeadsetManager = new WiredHeadsetManager(context); 47 mWiredHeadsetManager.setListener(this); 48 49 mCallAudioState = getInitialAudioState(); 50 LogUtil.i( 51 "VoicemailAudioManager.VoicemailAudioManager", "Initial audioState = " + mCallAudioState); 52 } 53 requestAudioFocus()54 public void requestAudioFocus() { 55 int result = 56 mAudioManager.requestAudioFocus( 57 this, PLAYBACK_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); 58 if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 59 throw new RejectedExecutionException("Could not capture audio focus."); 60 } 61 updateBluetoothScoState(true); 62 } 63 abandonAudioFocus()64 public void abandonAudioFocus() { 65 updateBluetoothScoState(false); 66 mAudioManager.abandonAudioFocus(this); 67 } 68 69 @Override onAudioFocusChange(int focusChange)70 public void onAudioFocusChange(int focusChange) { 71 LogUtil.d("VoicemailAudioManager.onAudioFocusChange", "focusChange=" + focusChange); 72 mVoicemailPlaybackPresenter.onAudioFocusChange(focusChange == AudioManager.AUDIOFOCUS_GAIN); 73 } 74 75 @Override onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn)76 public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) { 77 LogUtil.i( 78 "VoicemailAudioManager.onWiredHeadsetPluggedInChanged", 79 "wired headset was plugged in changed: " + oldIsPluggedIn + " -> " + newIsPluggedIn); 80 81 if (oldIsPluggedIn == newIsPluggedIn) { 82 return; 83 } 84 85 int newRoute = mCallAudioState.getRoute(); // start out with existing route 86 if (newIsPluggedIn) { 87 newRoute = CallAudioState.ROUTE_WIRED_HEADSET; 88 } else { 89 if (mWasSpeakerOn) { 90 newRoute = CallAudioState.ROUTE_SPEAKER; 91 } else { 92 newRoute = CallAudioState.ROUTE_EARPIECE; 93 } 94 } 95 96 mVoicemailPlaybackPresenter.setSpeakerphoneOn(newRoute == CallAudioState.ROUTE_SPEAKER); 97 98 // We need to call this every time even if we do not change the route because the supported 99 // routes changed either to include or not include WIRED_HEADSET. 100 setSystemAudioState( 101 new CallAudioState(false /* muted */, newRoute, calculateSupportedRoutes())); 102 } 103 setSpeakerphoneOn(boolean on)104 public void setSpeakerphoneOn(boolean on) { 105 setAudioRoute(on ? CallAudioState.ROUTE_SPEAKER : CallAudioState.ROUTE_WIRED_OR_EARPIECE); 106 } 107 isWiredHeadsetPluggedIn()108 public boolean isWiredHeadsetPluggedIn() { 109 return mWiredHeadsetManager.isPluggedIn(); 110 } 111 registerReceivers()112 public void registerReceivers() { 113 // Receivers is plural because we expect to add bluetooth support. 114 mWiredHeadsetManager.registerReceiver(); 115 } 116 unregisterReceivers()117 public void unregisterReceivers() { 118 mWiredHeadsetManager.unregisterReceiver(); 119 } 120 121 /** 122 * Bluetooth SCO (Synchronous Connection-Oriented) is the "phone" bluetooth audio. The system will 123 * route to the bluetooth headset automatically if A2DP ("media") is available, but if the headset 124 * only supports SCO then dialer must route it manually. 125 */ updateBluetoothScoState(boolean hasAudioFocus)126 private void updateBluetoothScoState(boolean hasAudioFocus) { 127 if (hasAudioFocus) { 128 if (hasMediaAudioCapability()) { 129 mBluetoothScoEnabled = false; 130 } else { 131 mBluetoothScoEnabled = true; 132 LogUtil.i( 133 "VoicemailAudioManager.updateBluetoothScoState", 134 "bluetooth device doesn't support media, using SCO instead"); 135 } 136 } else { 137 mBluetoothScoEnabled = false; 138 } 139 applyBluetoothScoState(); 140 } 141 applyBluetoothScoState()142 private void applyBluetoothScoState() { 143 if (mBluetoothScoEnabled) { 144 mAudioManager.startBluetoothSco(); 145 // The doc for startBluetoothSco() states it could take seconds to establish the SCO 146 // connection, so we should probably resume the playback after we've acquired SCO. 147 // In practice the delay is unnoticeable so this is ignored for simplicity. 148 mAudioManager.setBluetoothScoOn(true); 149 } else { 150 mAudioManager.setBluetoothScoOn(false); 151 mAudioManager.stopBluetoothSco(); 152 } 153 } 154 hasMediaAudioCapability()155 private boolean hasMediaAudioCapability() { 156 for (AudioDeviceInfo info : mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) { 157 if (info.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP) { 158 return true; 159 } 160 } 161 return false; 162 } 163 164 /** 165 * Change the audio route, for example from earpiece to speakerphone. 166 * 167 * @param route The new audio route to use. See {@link CallAudioState}. 168 */ setAudioRoute(int route)169 void setAudioRoute(int route) { 170 LogUtil.v( 171 "VoicemailAudioManager.setAudioRoute", 172 "route: " + CallAudioState.audioRouteToString(route)); 173 174 // Change ROUTE_WIRED_OR_EARPIECE to a single entry. 175 int newRoute = selectWiredOrEarpiece(route, mCallAudioState.getSupportedRouteMask()); 176 177 // If route is unsupported, do nothing. 178 if ((mCallAudioState.getSupportedRouteMask() | newRoute) == 0) { 179 LogUtil.w( 180 "VoicemailAudioManager.setAudioRoute", 181 "Asking to set to a route that is unsupported: " + newRoute); 182 return; 183 } 184 185 // Remember the new speaker state so it can be restored when the user plugs and unplugs 186 // a headset. 187 mWasSpeakerOn = newRoute == CallAudioState.ROUTE_SPEAKER; 188 setSystemAudioState( 189 new CallAudioState(false /* muted */, newRoute, mCallAudioState.getSupportedRouteMask())); 190 } 191 getInitialAudioState()192 private CallAudioState getInitialAudioState() { 193 int supportedRouteMask = calculateSupportedRoutes(); 194 int route = selectWiredOrEarpiece(CallAudioState.ROUTE_WIRED_OR_EARPIECE, supportedRouteMask); 195 return new CallAudioState(false /* muted */, route, supportedRouteMask); 196 } 197 calculateSupportedRoutes()198 private int calculateSupportedRoutes() { 199 int routeMask = CallAudioState.ROUTE_SPEAKER; 200 if (mWiredHeadsetManager.isPluggedIn()) { 201 routeMask |= CallAudioState.ROUTE_WIRED_HEADSET; 202 } else { 203 routeMask |= CallAudioState.ROUTE_EARPIECE; 204 } 205 return routeMask; 206 } 207 selectWiredOrEarpiece(int route, int supportedRouteMask)208 private int selectWiredOrEarpiece(int route, int supportedRouteMask) { 209 // Since they are mutually exclusive and one is ALWAYS valid, we allow a special input of 210 // ROUTE_WIRED_OR_EARPIECE so that callers don't have to make a call to check which is 211 // supported before calling setAudioRoute. 212 if (route == CallAudioState.ROUTE_WIRED_OR_EARPIECE) { 213 route = CallAudioState.ROUTE_WIRED_OR_EARPIECE & supportedRouteMask; 214 if (route == 0) { 215 LogUtil.e( 216 "VoicemailAudioManager.selectWiredOrEarpiece", 217 "One of wired headset or earpiece should always be valid."); 218 // assume earpiece in this case. 219 route = CallAudioState.ROUTE_EARPIECE; 220 } 221 } 222 return route; 223 } 224 setSystemAudioState(CallAudioState callAudioState)225 private void setSystemAudioState(CallAudioState callAudioState) { 226 CallAudioState oldAudioState = mCallAudioState; 227 mCallAudioState = callAudioState; 228 229 LogUtil.i( 230 "VoicemailAudioManager.setSystemAudioState", 231 "changing from " + oldAudioState + " to " + mCallAudioState); 232 233 // Audio route. 234 if (mCallAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) { 235 turnOnSpeaker(true); 236 } else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_EARPIECE 237 || mCallAudioState.getRoute() == CallAudioState.ROUTE_WIRED_HEADSET) { 238 // Just handle turning off the speaker, the system will handle switching between wired 239 // headset and earpiece. 240 turnOnSpeaker(false); 241 // BluetoothSco is not handled by the system so it has to be reset. 242 applyBluetoothScoState(); 243 } 244 } 245 turnOnSpeaker(boolean on)246 private void turnOnSpeaker(boolean on) { 247 if (mAudioManager.isSpeakerphoneOn() != on) { 248 LogUtil.i("VoicemailAudioManager.turnOnSpeaker", "turning speaker phone on: " + on); 249 mAudioManager.setSpeakerphoneOn(on); 250 } 251 } 252 } 253