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