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