1 /* 2 * Copyright (C) 2015 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.google.android.car.kitchensink.audio; 18 19 import static android.media.AudioManager.AUDIOFOCUS_GAIN; 20 import static android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT; 21 import static android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE; 22 import static android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK; 23 import static android.media.AudioManager.AUDIOFOCUS_LOSS; 24 import static android.media.AudioManager.AUDIOFOCUS_LOSS_TRANSIENT; 25 import static android.media.AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK; 26 import static android.media.AudioManager.AUDIOFOCUS_REQUEST_DELAYED; 27 import static android.media.AudioManager.AUDIOFOCUS_REQUEST_FAILED; 28 import static android.media.AudioManager.AUDIOFOCUS_REQUEST_GRANTED; 29 30 import static com.google.android.car.kitchensink.audio.AudioTestFragment.getAudioLogTag; 31 32 import android.annotation.IntDef; 33 import android.content.Context; 34 import android.content.res.AssetFileDescriptor; 35 import android.media.AudioAttributes; 36 import android.media.AudioDeviceInfo; 37 import android.media.AudioFocusRequest; 38 import android.media.AudioManager; 39 import android.media.AudioRouting.OnRoutingChangedListener; 40 import android.media.MediaPlayer; 41 import android.os.Handler; 42 import android.util.Log; 43 44 import androidx.annotation.Nullable; 45 46 import com.android.internal.annotations.GuardedBy; 47 48 import java.io.IOException; 49 import java.lang.annotation.Retention; 50 import java.lang.annotation.RetentionPolicy; 51 import java.util.concurrent.atomic.AtomicBoolean; 52 53 /** 54 * Class for playing music. 55 * 56 * This is not thread safe and all public calls should be made from main thread. 57 * 58 * MP3 used is from 59 * http://freemusicarchive 60 * .org/music/John_Harrison_with_the_Wichita_State_University_Chamber_Players 61 * /The_Four_Seasons_Vivaldi/05_-_Vivaldi_Summer_mvt_2_Adagio_-_John_Harrison_violin 62 * from John Harrison with the Wichita State University Chamber Players 63 * Copyright under Create Commons license. 64 */ 65 public class AudioPlayer { 66 67 private static final String TAG = getAudioLogTag(AudioPlayer.class); 68 69 @IntDef(flag = false, prefix = "PLAYER_STATE", value = { 70 PLAYER_STATE_COMPLETED, 71 PLAYER_STATE_STARTED, 72 PLAYER_STATE_STOPPED, 73 PLAYER_STATE_DELAYED, 74 PLAYER_STATE_PAUSED, 75 }) 76 @Retention(RetentionPolicy.SOURCE) 77 public @interface AudioPlayerState { 78 } 79 80 public static final int PLAYER_STATE_COMPLETED = 0; 81 public static final int PLAYER_STATE_STARTED = 1; 82 public static final int PLAYER_STATE_STOPPED = 2; 83 public static final int PLAYER_STATE_DELAYED = 3; 84 public static final int PLAYER_STATE_PAUSED = 4; 85 86 public interface PlayStateListener { onStateChange(@udioPlayerState int state)87 void onStateChange(@AudioPlayerState int state); 88 } 89 90 private final AudioManager.OnAudioFocusChangeListener mFocusListener = 91 new AudioManager.OnAudioFocusChangeListener() { 92 93 @Override 94 public void onAudioFocusChange(int focusChange) { 95 if (mPlayer == null) { 96 Log.e(TAG, "mPlayer is null"); 97 return; 98 } 99 switch (focusChange) { 100 case AUDIOFOCUS_GAIN: 101 case AUDIOFOCUS_GAIN_TRANSIENT: 102 case AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE: 103 case AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: 104 Log.i(TAG, "Audio focus change AUDIOFOCUS_GAIN for usage " 105 + mAttrib.getUsage()); 106 mPlayer.setVolume(1.0f, 1.0f); 107 if (mRepeat && isPlaying()) { 108 // Resume 109 Log.i(TAG, "resuming player"); 110 mPlayer.start(); 111 sendPlayerStateChanged(PLAYER_STATE_STARTED); 112 } 113 return; 114 case AUDIOFOCUS_LOSS_TRANSIENT: 115 Log.i(TAG, "Audio focus change AUDIOFOCUS_LOSS_TRANSIENT for usage " 116 + mAttrib.getUsage()); 117 if (mRepeat && isPlaying()) { 118 Log.i(TAG, "pausing repeating player"); 119 mPlayer.pause(); 120 sendPlayerStateChanged(PLAYER_STATE_PAUSED); 121 } else { 122 Log.i(TAG, "stopping one shot player"); 123 stop(); 124 } 125 return; 126 case AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 127 // While we used to setVolume on the player to 20%, we don't do this 128 // anymore 129 // because we expect the car's audio hal do handle ducking as it sees 130 // fit. 131 Log.i(TAG, "Audio focus change AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK " 132 + "-> do nothing"); 133 return; 134 case AUDIOFOCUS_LOSS: 135 default: 136 if (isPlaying()) { 137 Log.i(TAG, "stopping player"); 138 stop(); 139 } 140 } 141 } 142 }; 143 144 private final Object mLock = new Object(); 145 private AudioManager mAudioManager; 146 private MediaPlayer mPlayer; 147 148 private final Context mContext; 149 private final int mResourceId; 150 private final AudioAttributes mAttrib; 151 private final AudioDeviceInfo mPreferredDeviceInfo; 152 153 private final AtomicBoolean mPlaying = new AtomicBoolean(false); 154 155 @GuardedBy("mLock") 156 @Nullable 157 private OnRoutingChangedListener mAudioRoutingListener; 158 159 private volatile boolean mHandleFocus; 160 private volatile boolean mRepeat; 161 private PlayStateListener mListener; 162 private AudioFocusRequest mFocusRequest; 163 164 AudioPlayer(Context context, int resourceId, AudioAttributes attrib)165 public AudioPlayer(Context context, int resourceId, AudioAttributes attrib) { 166 this(context, resourceId, attrib, /* preferredDeviceInfo= */ null, 167 /* routingListener= */ null); 168 } 169 AudioPlayer(Context context, int resourceId, AudioAttributes attrib, @Nullable AudioDeviceInfo preferredDeviceInfo, @Nullable OnRoutingChangedListener routingListener)170 public AudioPlayer(Context context, int resourceId, AudioAttributes attrib, 171 @Nullable AudioDeviceInfo preferredDeviceInfo, 172 @Nullable OnRoutingChangedListener routingListener) { 173 mContext = context; 174 mResourceId = resourceId; 175 mAttrib = attrib; 176 mPreferredDeviceInfo = preferredDeviceInfo; 177 mAudioRoutingListener = routingListener; 178 } 179 getUsage()180 public int getUsage() { 181 return mAttrib.getSystemUsage(); 182 } 183 184 /** 185 * Starts player 186 * 187 * @param handleFocus {@code true} to handle focus 188 * @param repeat {@code true} to repeat track 189 * @param focusRequest type of focus to request 190 */ start(boolean handleFocus, boolean repeat, int focusRequest)191 public void start(boolean handleFocus, boolean repeat, int focusRequest) { 192 mHandleFocus = handleFocus; 193 mRepeat = repeat; 194 mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); 195 int ret = AUDIOFOCUS_REQUEST_GRANTED; 196 if (mHandleFocus) { 197 // NOTE: We are CONSCIOUSLY asking for focus again even if already playing in order 198 // exercise the framework's focus logic when faced with a (sloppy) application which 199 // might do this. 200 Log.i(TAG, "Asking for focus for usage " + mAttrib.getUsage()); 201 mFocusRequest = new AudioFocusRequest 202 .Builder(focusRequest) 203 .setAudioAttributes(mAttrib) 204 .setOnAudioFocusChangeListener(mFocusListener) 205 .setForceDucking(false) 206 .setWillPauseWhenDucked(false) 207 .setAcceptsDelayedFocusGain(false) 208 .build(); 209 ret = mAudioManager.requestAudioFocus(mFocusRequest); 210 } 211 switch (ret) { 212 case AUDIOFOCUS_REQUEST_GRANTED: 213 Log.i(TAG, "MediaPlayer got focus for usage " + mAttrib.getUsage()); 214 doStart(); 215 break; 216 case AUDIOFOCUS_REQUEST_DELAYED: 217 Log.i(TAG, "MediaPlayer delayed focus for usage " + mAttrib.getUsage()); 218 sendPlayerStateChanged(PLAYER_STATE_DELAYED); 219 break; 220 case AUDIOFOCUS_REQUEST_FAILED: 221 default: 222 Log.i(TAG, "MediaPlayer denied focus for usage " + mAttrib.getUsage()); 223 } 224 } 225 start(boolean handleFocus, boolean repeat, int focusRequest, PlayStateListener listener)226 public void start(boolean handleFocus, boolean repeat, int focusRequest, 227 PlayStateListener listener) { 228 mListener = listener; 229 start(handleFocus, repeat, focusRequest); 230 } 231 232 /** 233 * Sets the audio routing listener, runs in main handler 234 */ setAudioRoutingListener( @ullable OnRoutingChangedListener audioRoutingListener)235 void setAudioRoutingListener( 236 @Nullable OnRoutingChangedListener audioRoutingListener) { 237 synchronized (mLock) { 238 mAudioRoutingListener = audioRoutingListener; 239 } 240 } 241 doStart()242 private void doStart() { 243 if (mPlaying.getAndSet(true)) { 244 Log.i(TAG, "already playing"); 245 return; 246 } 247 Log.i(TAG, "doStart audio"); 248 mPlayer = new MediaPlayer(); 249 mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { 250 251 @Override 252 public boolean onError(MediaPlayer mp, int what, int extra) { 253 Log.e(TAG, "audio error what " + what + " extra " + extra); 254 mPlaying.set(false); 255 if (!mRepeat && mHandleFocus) { 256 mPlayer.stop(); 257 mPlayer.release(); 258 mPlayer = null; 259 mAudioManager.abandonAudioFocus(mFocusListener); 260 sendPlayerStateChanged(PLAYER_STATE_STOPPED); 261 } 262 return false; 263 } 264 265 }); 266 mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { 267 @Override 268 public void onCompletion(MediaPlayer mp) { 269 Log.i(TAG, "AudioPlayer onCompletion"); 270 mPlaying.set(false); 271 if (!mRepeat && mHandleFocus) { 272 mPlayer.stop(); 273 mPlayer = null; 274 mAudioManager.abandonAudioFocus(mFocusListener); 275 sendPlayerStateChanged(PLAYER_STATE_COMPLETED); 276 } 277 } 278 }); 279 mPlayer.setAudioAttributes(mAttrib); 280 mPlayer.setLooping(mRepeat); 281 mPlayer.setVolume(1.0f, 1.0f); 282 try { 283 AssetFileDescriptor afd = 284 mContext.getResources().openRawResourceFd(mResourceId); 285 if (afd == null) { 286 throw new RuntimeException("resource not found"); 287 } 288 mPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), 289 afd.getLength()); 290 afd.close(); 291 mPlayer.prepare(); 292 } catch (IOException e) { 293 throw new RuntimeException(e); 294 } 295 296 // Search for preferred device 297 if (mPreferredDeviceInfo != null) { 298 mPlayer.setPreferredDevice(mPreferredDeviceInfo); 299 Log.d(TAG, "doStart preferred device address: " + mPreferredDeviceInfo.getAddress()); 300 } 301 302 mPlayer.start(); 303 OnRoutingChangedListener routingChangedListener; 304 synchronized (mLock) { 305 routingChangedListener = mAudioRoutingListener; 306 } 307 if (routingChangedListener != null) { 308 Log.i(TAG, "doStart addOnRoutingChangedListener " + routingChangedListener); 309 mPlayer.addOnRoutingChangedListener(routingChangedListener, Handler.getMain()); 310 } 311 sendPlayerStateChanged(PLAYER_STATE_STARTED); 312 } 313 stop()314 public void stop() { 315 if (!mPlaying.getAndSet(false)) { 316 Log.i(TAG, "already stopped"); 317 if (mPlayer == null) { 318 return; 319 } 320 mPlayer.release(); 321 mPlayer = null; 322 return; 323 } 324 Log.i(TAG, "stop"); 325 326 mPlayer.stop(); 327 mPlayer.release(); 328 mPlayer = null; 329 sendPlayerStateChanged(PLAYER_STATE_STOPPED); 330 331 if (mHandleFocus) { 332 mAudioManager.abandonAudioFocusRequest(mFocusRequest); 333 } 334 } 335 isPlaying()336 public boolean isPlaying() { 337 return mPlaying.get(); 338 } 339 sendPlayerStateChanged(@udioPlayerState int state)340 private void sendPlayerStateChanged(@AudioPlayerState int state) { 341 if (mListener != null) { 342 mListener.onStateChange(state); 343 } 344 } 345 } 346