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