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