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