• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * Copyright (C) 2018 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.car.radio.audio;
18 
19 import android.annotation.NonNull;
20 import android.content.Context;
21 import android.media.AudioAttributes;
22 import android.media.AudioFocusRequest;
23 import android.media.AudioManager;
24 import android.os.RemoteException;
25 import android.support.v4.media.session.PlaybackStateCompat;
26 import android.util.Log;
27 
28 import com.android.car.radio.platform.RadioManagerExt;
29 import com.android.car.radio.platform.RadioTunerExt;
30 
31 import java.util.ArrayList;
32 import java.util.List;
33 import java.util.Objects;
34 import java.util.Optional;
35 
36 /**
37  * Manages radio's audio stream.
38  */
39 public class AudioStreamController {
40     private static final String TAG = "BcRadioApp.AudioSCntrl";
41 
42     private final Object mLock = new Object();
43     private final AudioManager mAudioManager;
44     private final RadioTunerExt mRadioTunerExt;
45 
46     private final AudioFocusRequest mGainFocusReq;
47 
48     /**
49      * Indicates that the app has *some* focus or a promise of it.
50      *
51      * It may be ducked, transiently lost or delayed.
52      */
53     private boolean mHasSomeFocus = false;
54 
55     private boolean mIsTuning = false;
56 
57     private int mCurrentPlaybackState = PlaybackStateCompat.STATE_NONE;
58     private final List<IPlaybackStateListener> mPlaybackStateListeners = new ArrayList<>();
59 
AudioStreamController(@onNull Context context, @NonNull RadioManagerExt radioManager)60     public AudioStreamController(@NonNull Context context, @NonNull RadioManagerExt radioManager) {
61         mAudioManager = Objects.requireNonNull(
62                 (AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
63         mRadioTunerExt = Objects.requireNonNull(radioManager.getRadioTunerExt());
64 
65         AudioAttributes playbackAttr = new AudioAttributes.Builder()
66                 .setUsage(AudioAttributes.USAGE_MEDIA)
67                 .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
68                 .build();
69         mGainFocusReq = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
70                 .setAudioAttributes(playbackAttr)
71                 .setAcceptsDelayedFocusGain(true)
72                 .setWillPauseWhenDucked(true)
73                 .setOnAudioFocusChangeListener(this::onAudioFocusChange)
74                 .build();
75     }
76 
77     /**
78      * Add playback state listener.
79      *
80      * @param listener listener to add
81      */
addPlaybackStateListener(@onNull IPlaybackStateListener listener)82     public void addPlaybackStateListener(@NonNull IPlaybackStateListener listener) {
83         synchronized (mLock) {
84             mPlaybackStateListeners.add(Objects.requireNonNull(listener));
85             try {
86                 listener.onPlaybackStateChanged(mCurrentPlaybackState);
87             } catch (RemoteException e) {
88                 Log.e(TAG, "Couldn't notify new listener about current playback state", e);
89             }
90         }
91     }
92 
93     /**
94      * Remove playback state listener.
95      *
96      * @param listener listener to remove
97      */
removePlaybackStateListener(IPlaybackStateListener listener)98     public void removePlaybackStateListener(IPlaybackStateListener listener) {
99         synchronized (mLock) {
100             mPlaybackStateListeners.remove(listener);
101         }
102     }
103 
notifyPlaybackStateChangedLocked(@laybackStateCompat.State int state)104     private void notifyPlaybackStateChangedLocked(@PlaybackStateCompat.State int state) {
105         if (mCurrentPlaybackState == state) return;
106         mCurrentPlaybackState = state;
107         for (IPlaybackStateListener listener : mPlaybackStateListeners) {
108             try {
109                 listener.onPlaybackStateChanged(state);
110             } catch (RemoteException e) {
111                 Log.e(TAG, "Couldn't notify listener about playback state change", e);
112             }
113         }
114     }
115 
requestAudioFocusLocked()116     private boolean requestAudioFocusLocked() {
117         if (mHasSomeFocus) return true;
118         int res = mAudioManager.requestAudioFocus(mGainFocusReq);
119         if (res == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
120             Log.i(TAG, "Audio focus request is delayed");
121             mHasSomeFocus = true;
122             return true;
123         }
124         if (res != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
125             Log.w(TAG, "Couldn't obtain audio focus, res=" + res);
126             return false;
127         }
128 
129         Log.v(TAG, "Audio focus request succeeded");
130         mHasSomeFocus = true;
131 
132         // we assume that audio focus was requested only when we mean to unmute
133         if (!mRadioTunerExt.setMuted(false)) return false;
134 
135         return true;
136     }
137 
abandonAudioFocusLocked()138     private boolean abandonAudioFocusLocked() {
139         if (!mHasSomeFocus) return true;
140         if (!mRadioTunerExt.setMuted(true)) return false;
141 
142         int res = mAudioManager.abandonAudioFocusRequest(mGainFocusReq);
143         if (res != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
144             Log.e(TAG, "Couldn't abandon audio focus, res=" + res);
145             return false;
146         }
147 
148         Log.v(TAG, "Audio focus abandoned");
149         mHasSomeFocus = false;
150 
151         return true;
152     }
153 
154     /**
155      * Prepare playback for ongoing tune/scan operation.
156      *
157      * @param skipDirectionNext true if it's skipping to next station;
158      *                          false if skipping to previous;
159      *                          empty if tuning to arbitrary selector.
160      */
preparePlayback(Optional<Boolean> skipDirectionNext)161     public boolean preparePlayback(Optional<Boolean> skipDirectionNext) {
162         synchronized (mLock) {
163             if (!requestAudioFocusLocked()) return false;
164 
165             int state = PlaybackStateCompat.STATE_CONNECTING;
166             if (skipDirectionNext.isPresent()) {
167                 state = skipDirectionNext.get() ? PlaybackStateCompat.STATE_SKIPPING_TO_NEXT
168                         : PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS;
169             }
170             notifyPlaybackStateChangedLocked(state);
171 
172             mIsTuning = true;
173             return true;
174         }
175     }
176 
177     /**
178      * Notifies AudioStreamController that radio hardware is done with tune/scan operation.
179      *
180      * TODO(b/73950974): use callbacks, don't hardcode
181      *
182      * @see #preparePlayback
183      */
notifyProgramInfoChanged()184     public void notifyProgramInfoChanged() {
185         synchronized (mLock) {
186             if (!mIsTuning) return;
187             mIsTuning = false;
188             notifyPlaybackStateChangedLocked(PlaybackStateCompat.STATE_PLAYING);
189         }
190     }
191 
192     /**
193      * Request audio stream muted or unmuted.
194      *
195      * @param muted true, if audio stream should be muted, false if unmuted
196      * @return true, if request has succeeded (maybe delayed)
197      */
requestMuted(boolean muted)198     public boolean requestMuted(boolean muted) {
199         synchronized (mLock) {
200             if (muted) {
201                 notifyPlaybackStateChangedLocked(PlaybackStateCompat.STATE_STOPPED);
202                 return abandonAudioFocusLocked();
203             } else {
204                 if (!requestAudioFocusLocked()) return false;
205                 notifyPlaybackStateChangedLocked(PlaybackStateCompat.STATE_PLAYING);
206                 return true;
207             }
208         }
209     }
210 
211     // TODO(b/73950974): depend on callbacks only
isMuted()212     public boolean isMuted() {
213         return !mHasSomeFocus;
214     }
215 
onAudioFocusChange(int focusChange)216     private void onAudioFocusChange(int focusChange) {
217         Log.v(TAG, "onAudioFocusChange(" + focusChange + ")");
218 
219         synchronized (mLock) {
220             switch (focusChange) {
221                 case AudioManager.AUDIOFOCUS_GAIN:
222                     mHasSomeFocus = true;
223                     // we assume that audio focus was requested only when we mean to unmute
224                     mRadioTunerExt.setMuted(false);
225                     break;
226                 case AudioManager.AUDIOFOCUS_LOSS:
227                     Log.i(TAG, "Unexpected audio focus loss");
228                     mHasSomeFocus = false;
229                     mRadioTunerExt.setMuted(true);
230                     notifyPlaybackStateChangedLocked(PlaybackStateCompat.STATE_STOPPED);
231                     break;
232                 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
233                 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
234                     mRadioTunerExt.setMuted(true);
235                     break;
236                 default:
237                     Log.w(TAG, "Unexpected audio focus state: " + focusChange);
238             }
239         }
240     }
241 }
242