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