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