Java tutorial
/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.murati.oszk.audiobook.playback; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; import android.net.Uri; import android.net.wifi.WifiManager; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.text.TextUtils; import com.murati.oszk.audiobook.MusicService; import com.murati.oszk.audiobook.OfflineBookService; import com.murati.oszk.audiobook.model.MusicProvider; import com.murati.oszk.audiobook.model.MusicProviderSource; import com.murati.oszk.audiobook.utils.LogHelper; import com.murati.oszk.audiobook.utils.MediaIDHelper; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.util.Util; import static android.support.v4.media.session.MediaSessionCompat.QueueItem; import static com.google.android.exoplayer2.C.CONTENT_TYPE_MUSIC; import static com.google.android.exoplayer2.C.USAGE_MEDIA; /** * A class that implements local media playback using {@link * com.google.android.exoplayer2.ExoPlayer} */ public final class LocalPlayback implements Playback { private static final String TAG = LogHelper.makeLogTag(LocalPlayback.class); // The volume we set the media player to when we lose audio focus, but are // allowed to reduce the volume instead of stopping playback. public static final float VOLUME_DUCK = 0.2f; // The volume we set the media player when we have audio focus. public static final float VOLUME_NORMAL = 1.0f; // we don't have audio focus, and can't duck (play at a low volume) private static final int AUDIO_NO_FOCUS_NO_DUCK = 0; // we don't have focus, but can duck (play at a low volume) private static final int AUDIO_NO_FOCUS_CAN_DUCK = 1; // we have full audio focus private static final int AUDIO_FOCUSED = 2; private final Context mContext; private final WifiManager.WifiLock mWifiLock; private boolean mPlayOnFocusGain; private Callback mCallback; private final MusicProvider mMusicProvider; private boolean mAudioNoisyReceiverRegistered; private String mCurrentMediaId; private int mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK; private final AudioManager mAudioManager; private SimpleExoPlayer mExoPlayer; private final ExoPlayerEventListener mEventListener = new ExoPlayerEventListener(); // Whether to return STATE_NONE or STATE_STOPPED when mExoPlayer is null; private boolean mExoPlayerNullIsStopped = false; private final IntentFilter mAudioNoisyIntentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY); private final BroadcastReceiver mAudioNoisyReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) { LogHelper.d(TAG, "Headphones disconnected."); if (isPlaying()) { Intent i = new Intent(context, MusicService.class); i.setAction(MusicService.ACTION_CMD); i.putExtra(MusicService.CMD_NAME, MusicService.CMD_PAUSE); mContext.startService(i); } } } }; public LocalPlayback(Context context, MusicProvider musicProvider) { Context applicationContext = context.getApplicationContext(); this.mContext = applicationContext; this.mMusicProvider = musicProvider; this.mAudioManager = (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE); // Create the Wifi lock (this does not acquire the lock, this just creates it) this.mWifiLock = ((WifiManager) applicationContext.getSystemService(Context.WIFI_SERVICE)) .createWifiLock(WifiManager.WIFI_MODE_FULL, "uAmp_lock"); } @Override public void start() { // Nothing to do } @Override public void stop(boolean notifyListeners) { giveUpAudioFocus(); unregisterAudioNoisyReceiver(); releaseResources(true); } @Override public void setState(int state) { // Nothing to do (mExoPlayer holds its own state). } @Override public int getState() { if (mExoPlayer == null) { return mExoPlayerNullIsStopped ? PlaybackStateCompat.STATE_STOPPED : PlaybackStateCompat.STATE_NONE; } switch (mExoPlayer.getPlaybackState()) { case ExoPlayer.STATE_IDLE: return PlaybackStateCompat.STATE_PAUSED; case ExoPlayer.STATE_BUFFERING: return PlaybackStateCompat.STATE_BUFFERING; case ExoPlayer.STATE_READY: return mExoPlayer.getPlayWhenReady() ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED; case ExoPlayer.STATE_ENDED: return PlaybackStateCompat.STATE_PAUSED; default: return PlaybackStateCompat.STATE_NONE; } } @Override public boolean isConnected() { return true; } @Override public boolean isPlaying() { return mPlayOnFocusGain || (mExoPlayer != null && mExoPlayer.getPlayWhenReady()); } @Override public long getCurrentStreamPosition() { return mExoPlayer != null ? mExoPlayer.getCurrentPosition() : 0; } @Override public void updateLastKnownStreamPosition() { // Nothing to do. Position maintained by ExoPlayer. } @Override public void play(QueueItem item) { mPlayOnFocusGain = true; tryToGetAudioFocus(); registerAudioNoisyReceiver(); String mediaId = item.getDescription().getMediaId(); boolean mediaHasChanged = !TextUtils.equals(mediaId, mCurrentMediaId); if (mediaHasChanged) { mCurrentMediaId = mediaId; } if (mediaHasChanged || mExoPlayer == null) { releaseResources(false); // release everything except the player MediaMetadataCompat track = mMusicProvider .getTrack(MediaIDHelper.extractMusicIDFromMediaID(item.getDescription().getMediaId())); String source = OfflineBookService.getTrackSource(track); if (mExoPlayer == null) { mExoPlayer = ExoPlayerFactory.newSimpleInstance(mContext, new DefaultTrackSelector(), new DefaultLoadControl()); mExoPlayer.addListener(mEventListener); } // Android "O" makes much greater use of AudioAttributes, especially // with regards to AudioFocus. All of UAMP's tracks are music, but // if your content includes spoken word such as audiobooks or podcasts // then the content type should be set to CONTENT_TYPE_SPEECH for those // tracks. final AudioAttributes audioAttributes = new AudioAttributes.Builder().setContentType(CONTENT_TYPE_MUSIC) .setUsage(USAGE_MEDIA).build(); mExoPlayer.setAudioAttributes(audioAttributes); // Produces DataSource instances through which media data is loaded. DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(mContext, Util.getUserAgent(mContext, "uamp"), null); // Produces Extractor instances for parsing the media data. ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); // The MediaSource represents the media to be played. MediaSource mediaSource = new ExtractorMediaSource(Uri.parse(source), dataSourceFactory, extractorsFactory, null, null); // Prepares media to play (happens on background thread) and triggers // {@code onPlayerStateChanged} callback when the stream is ready to play. mExoPlayer.prepare(mediaSource); // If we are streaming from the internet, we want to hold a // Wifi lock, which prevents the Wifi radio from going to // sleep while the song is playing. mWifiLock.acquire(); } configurePlayerState(); } @Override public void pause() { // Pause player and cancel the 'foreground service' state. if (mExoPlayer != null) { mExoPlayer.setPlayWhenReady(false); } // While paused, retain the player instance, but give up audio focus. releaseResources(false); unregisterAudioNoisyReceiver(); } @Override public void seekTo(long position) { LogHelper.d(TAG, "seekTo called with ", position); if (mExoPlayer != null) { registerAudioNoisyReceiver(); mExoPlayer.seekTo(position); } } @Override public void setCallback(Callback callback) { this.mCallback = callback; } @Override public void setCurrentMediaId(String mediaId) { this.mCurrentMediaId = mediaId; } @Override public String getCurrentMediaId() { return mCurrentMediaId; } private void tryToGetAudioFocus() { LogHelper.d(TAG, "tryToGetAudioFocus"); int result = mAudioManager.requestAudioFocus(mOnAudioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { mCurrentAudioFocusState = AUDIO_FOCUSED; } else { mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK; } } private void giveUpAudioFocus() { LogHelper.d(TAG, "giveUpAudioFocus"); if (mAudioManager .abandonAudioFocus(mOnAudioFocusChangeListener) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK; } } /** * Reconfigures the player according to audio focus settings and starts/restarts it. This method * starts/restarts the ExoPlayer instance respecting the current audio focus state. So if we * have focus, it will play normally; if we don't have focus, it will either leave the player * paused or set it to a low volume, depending on what is permitted by the current focus * settings. */ private void configurePlayerState() { LogHelper.d(TAG, "configurePlayerState. mCurrentAudioFocusState=", mCurrentAudioFocusState); if (mCurrentAudioFocusState == AUDIO_NO_FOCUS_NO_DUCK) { // We don't have audio focus and can't duck, so we have to pause pause(); } else { registerAudioNoisyReceiver(); if (mCurrentAudioFocusState == AUDIO_NO_FOCUS_CAN_DUCK) { // We're permitted to play, but only if we 'duck', ie: play softly mExoPlayer.setVolume(VOLUME_DUCK); } else { mExoPlayer.setVolume(VOLUME_NORMAL); } // If we were playing when we lost focus, we need to resume playing. if (mPlayOnFocusGain) { mExoPlayer.setPlayWhenReady(true); mPlayOnFocusGain = false; } } } private final AudioManager.OnAudioFocusChangeListener mOnAudioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { @Override public void onAudioFocusChange(int focusChange) { LogHelper.d(TAG, "onAudioFocusChange. focusChange=", focusChange); switch (focusChange) { case AudioManager.AUDIOFOCUS_GAIN: mCurrentAudioFocusState = AUDIO_FOCUSED; break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: // Audio focus was lost, but it's possible to duck (i.e.: play quietly) mCurrentAudioFocusState = AUDIO_NO_FOCUS_CAN_DUCK; break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: // Lost audio focus, but will gain it back (shortly), so note whether // playback should resume mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK; mPlayOnFocusGain = mExoPlayer != null && mExoPlayer.getPlayWhenReady(); break; case AudioManager.AUDIOFOCUS_LOSS: // Lost audio focus, probably "permanently" mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK; break; } if (mExoPlayer != null) { // Update the player state based on the change configurePlayerState(); } } }; /** * Releases resources used by the service for playback, which is mostly just the WiFi lock for * local playback. If requested, the ExoPlayer instance is also released. * * @param releasePlayer Indicates whether the player should also be released */ private void releaseResources(boolean releasePlayer) { LogHelper.d(TAG, "releaseResources. releasePlayer=", releasePlayer); // Stops and releases player (if requested and available). if (releasePlayer && mExoPlayer != null) { mExoPlayer.release(); mExoPlayer.removeListener(mEventListener); mExoPlayer = null; mExoPlayerNullIsStopped = true; mPlayOnFocusGain = false; } if (mWifiLock.isHeld()) { mWifiLock.release(); } } private void registerAudioNoisyReceiver() { if (!mAudioNoisyReceiverRegistered) { mContext.registerReceiver(mAudioNoisyReceiver, mAudioNoisyIntentFilter); mAudioNoisyReceiverRegistered = true; } } private void unregisterAudioNoisyReceiver() { if (mAudioNoisyReceiverRegistered) { mContext.unregisterReceiver(mAudioNoisyReceiver); mAudioNoisyReceiverRegistered = false; } } private final class ExoPlayerEventListener implements ExoPlayer.EventListener { @Override public void onTimelineChanged(Timeline timeline, Object manifest) { // Nothing to do. } @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { // Nothing to do. } @Override public void onLoadingChanged(boolean isLoading) { // Nothing to do. } @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { switch (playbackState) { case ExoPlayer.STATE_IDLE: case ExoPlayer.STATE_BUFFERING: case ExoPlayer.STATE_READY: if (mCallback != null) { mCallback.onPlaybackStatusChanged(getState()); } break; case ExoPlayer.STATE_ENDED: // The media player finished playing the current song. if (mCallback != null) { mCallback.onCompletion(); } break; } } @Override public void onPlayerError(ExoPlaybackException error) { final String what; switch (error.type) { case ExoPlaybackException.TYPE_SOURCE: what = error.getSourceException().getMessage(); break; case ExoPlaybackException.TYPE_RENDERER: what = error.getRendererException().getMessage(); break; case ExoPlaybackException.TYPE_UNEXPECTED: what = error.getUnexpectedException().getMessage(); break; default: what = "Unknown: " + error; } LogHelper.e(TAG, "ExoPlayer error: what=" + what); if (mCallback != null) { mCallback.onError("ExoPlayer error " + what); } } @Override public void onPositionDiscontinuity() { // Nothing to do. } @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { // Nothing to do. } @Override public void onRepeatModeChanged(int repeatMode) { // Nothing to do. } } }