• 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.content.Context;
20 import android.media.AudioAttributes;
21 import android.media.AudioFocusRequest;
22 import android.media.AudioManager;
23 import android.media.session.PlaybackState;
24 import android.util.IndentingPrintWriter;
25 
26 import androidx.annotation.IntDef;
27 import androidx.annotation.NonNull;
28 import androidx.annotation.Nullable;
29 
30 import com.android.car.radio.platform.RadioTunerExt;
31 import com.android.car.radio.platform.RadioTunerExt.TuneCallback;
32 import com.android.car.radio.util.Log;
33 
34 import java.lang.annotation.Retention;
35 import java.lang.annotation.RetentionPolicy;
36 import java.util.Objects;
37 
38 /**
39  * Manages radio's audio stream.
40  */
41 public class AudioStreamController {
42     private static final String TAG = "BcRadioApp.audio";
43 
44     /** Tune operation. */
45     public static final int OPERATION_TUNE = 1;
46 
47     /** Seek forward operation. */
48     public static final int OPERATION_SEEK_FWD = 2;
49 
50     /** Seek backwards operation. */
51     public static final int OPERATION_SEEK_BKW = 3;
52 
53     /** Step forwards operation. */
54     public static final int OPERATION_STEP_FWD = 4;
55 
56     /** Step backwards operation. */
57     public static final int OPERATION_STEP_BKW = 5;
58 
59     /**
60      * Operation types for {@link #preparePlayback}.
61      */
62     @IntDef(value = {
63         OPERATION_TUNE,
64         OPERATION_SEEK_FWD,
65         OPERATION_SEEK_BKW,
66         OPERATION_STEP_FWD,
67         OPERATION_STEP_BKW,
68     })
69     @Retention(RetentionPolicy.SOURCE)
70     public @interface PlaybackOperation {}
71 
72     private final Object mLock = new Object();
73     private final AudioManager mAudioManager;
74     private final RadioTunerExt mRadioTunerExt;
75 
76     private final PlaybackStateCallback mCallback;
77     private final AudioFocusRequest mGainFocusReq;
78 
79     /**
80      * Indicates that the app has *some* focus or a promise of it.
81      *
82      * It may be ducked, transiently lost or delayed.
83      */
84     private boolean mHasSomeFocus;
85 
86     private int mCurrentPlaybackState = PlaybackState.STATE_NONE;
87     private Object mTuningToken;
88 
89     /**
90      * Callback for playback state changes.
91      */
92     public interface PlaybackStateCallback {
93         /**
94          * Called when playback state changes.
95          */
onPlaybackStateChanged(int newState)96         void onPlaybackStateChanged(int newState);
97     }
98 
99     /**
100      * New (and only) instance of Audio stream controller.
101      *
102      * This is a part of RadioAppService that handles audio streams and playback status.
103      *
104      * @param context Context
105      * @param radioManager tuner hardware manager
106      * @param callback Callback for playback state changes
107      */
AudioStreamController(@onNull Context context, @NonNull RadioTunerExt tuner, @NonNull PlaybackStateCallback callback)108     public AudioStreamController(@NonNull Context context, @NonNull RadioTunerExt tuner,
109             @NonNull PlaybackStateCallback callback) {
110         mAudioManager = Objects.requireNonNull(
111                 (AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
112         mRadioTunerExt = Objects.requireNonNull(tuner);
113         mCallback = Objects.requireNonNull(callback);
114 
115         AudioAttributes playbackAttr = new AudioAttributes.Builder()
116                 .setUsage(AudioAttributes.USAGE_MEDIA)
117                 .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
118                 .build();
119         mGainFocusReq = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
120                 .setAudioAttributes(playbackAttr)
121                 .setAcceptsDelayedFocusGain(true)
122                 .setWillPauseWhenDucked(true)
123                 .setOnAudioFocusChangeListener(this::onAudioFocusChange)
124                 .build();
125     }
126 
unmuteLocked()127     private boolean unmuteLocked() {
128         if (mRadioTunerExt.setMuted(false)) return true;
129         Log.w(TAG, "Failed to unmute, dropping audio focus");
130         abandonAudioFocusLocked();
131         return false;
132     }
133 
requestAudioFocusLocked()134     private boolean requestAudioFocusLocked() {
135         if (mHasSomeFocus) return true;
136         int res = mAudioManager.requestAudioFocus(mGainFocusReq);
137         if (res == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
138             Log.d(TAG, "Audio focus request is delayed");
139             mHasSomeFocus = true;
140             return true;
141         }
142         if (res != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
143             Log.w(TAG, "Couldn't obtain audio focus, res=" + res);
144             return false;
145         }
146 
147         Log.v(TAG, "Audio focus request succeeded");
148         mHasSomeFocus = true;
149 
150         // we assume that audio focus was requested only when we mean to unmute
151         if (!unmuteLocked()) return false;
152 
153         return true;
154     }
155 
abandonAudioFocusLocked()156     private boolean abandonAudioFocusLocked() {
157         if (!mHasSomeFocus) return true;
158         if (!mRadioTunerExt.setMuted(true)) return false;
159 
160         int res = mAudioManager.abandonAudioFocusRequest(mGainFocusReq);
161         if (res != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
162             Log.e(TAG, "Couldn't abandon audio focus, res=" + res);
163             return false;
164         }
165 
166         Log.v(TAG, "Audio focus abandoned");
167         mHasSomeFocus = false;
168 
169         return true;
170     }
171 
notifyPlaybackStateLocked(int newState)172     private void notifyPlaybackStateLocked(int newState) {
173         if (mCurrentPlaybackState == newState) return;
174         mCurrentPlaybackState = newState;
175         Log.v(TAG, "Playback state changed to " + newState);
176         mCallback.onPlaybackStateChanged(newState);
177     }
178 
179     /**
180      * Prepare playback for ongoing tune/scan operation.
181      *
182      * @param operation Playback operation type
183      * @return result callback to be passed to {@link RadioTunerExt#tune} call,
184      *         or {@code null} if couldn't get focus.
185      */
186     @Nullable
preparePlayback(@laybackOperation int operation)187     public TuneCallback preparePlayback(@PlaybackOperation int operation) {
188         synchronized (mLock) {
189             Object token = new Object();
190             mTuningToken = token;
191 
192             if (!requestAudioFocusLocked()) {
193                 mTuningToken = null;
194                 return null;
195             }
196 
197             int state;
198             switch (operation) {
199                 case OPERATION_TUNE:
200                     state = PlaybackState.STATE_CONNECTING;
201                     break;
202                 case OPERATION_SEEK_FWD:
203                 case OPERATION_STEP_FWD:
204                     state = PlaybackState.STATE_SKIPPING_TO_NEXT;
205                     break;
206                 case OPERATION_SEEK_BKW:
207                 case OPERATION_STEP_BKW:
208                     state = PlaybackState.STATE_SKIPPING_TO_PREVIOUS;
209                     break;
210                 default:
211                     throw new IllegalArgumentException("Invalid operation: " + operation);
212             }
213             notifyPlaybackStateLocked(state);
214 
215             return succeeded -> onTuneCompleted(token, succeeded);
216         }
217     }
218 
onTuneCompleted(@onNull Object token, boolean succeeded)219     private void onTuneCompleted(@NonNull Object token, boolean succeeded) {
220         synchronized (mLock) {
221             if (mTuningToken != token) return;
222             mTuningToken = null;
223             notifyPlaybackStateLocked(succeeded
224                     ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_ERROR);
225         }
226     }
227 
228     /**
229      * Request audio stream muted or unmuted.
230      *
231      * @param muted true, if audio stream should be muted, false if unmuted
232      * @return true, if request has succeeded (maybe delayed)
233      */
requestMuted(boolean muted)234     public boolean requestMuted(boolean muted) {
235         Log.v(TAG, "requestMuted(" + muted + ")");
236         synchronized (mLock) {
237             if (muted) {
238                 if (mTuningToken == null) {
239                     notifyPlaybackStateLocked(PlaybackState.STATE_STOPPED);
240                 }
241                 return abandonAudioFocusLocked();
242             } else {
243                 if (!requestAudioFocusLocked()) return false;
244                 if (mTuningToken == null) {
245                     notifyPlaybackStateLocked(PlaybackState.STATE_PLAYING);
246                 }
247                 return true;
248             }
249         }
250     }
251 
onAudioFocusChange(int focusChange)252     private void onAudioFocusChange(int focusChange) {
253         Log.v(TAG, "onAudioFocusChange(" + focusChange + ")");
254 
255         synchronized (mLock) {
256             switch (focusChange) {
257                 case AudioManager.AUDIOFOCUS_GAIN:
258                     mHasSomeFocus = true;
259                     // we assume that audio focus was requested only when we mean to unmute
260                     unmuteLocked();
261                     break;
262                 case AudioManager.AUDIOFOCUS_LOSS:
263                     Log.i(TAG, "Unexpected audio focus loss");
264                     mHasSomeFocus = false;
265                     mRadioTunerExt.setMuted(true);
266                     notifyPlaybackStateLocked(PlaybackState.STATE_STOPPED);
267                     break;
268                 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
269                 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
270                     mRadioTunerExt.setMuted(true);
271                     break;
272                 default:
273                     Log.w(TAG, "Unexpected audio focus state: " + focusChange);
274             }
275         }
276     }
277 
278     /**
279      * Dumps the current audio stream controller state
280      */
dump(IndentingPrintWriter writer)281     public void dump(IndentingPrintWriter writer) {
282         writer.println("AudioStreamController");
283         writer.increaseIndent();
284         synchronized (mLock) {
285             writer.printf("Focus Request: %s\n", mGainFocusReq);
286             writer.printf("Has Some Focus: %b\n", mHasSomeFocus);
287             writer.printf("PlayBack State: %d\n", mCurrentPlaybackState);
288             writer.printf("Is Tuning Token Available: %s\n", mTuningToken != null);
289         }
290         writer.decreaseIndent();
291     }
292 }
293