blob: a9d623c73e4ea766e8c41568d0e2eaadbbf88e50 [file] [log] [blame]
/*
* Copyright (C) 2016 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.android.sample.musicplayer;
import android.app.PendingIntent;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.net.Uri;
import android.os.IBinder;
import android.os.PowerManager;
import android.support.annotation.Nullable;
import android.support.annotation.RawRes;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v7.app.NotificationCompat;
import com.android.sample.musicplayer.MusicRepository.TrackMetadata;
import com.android.support.lifecycle.LifecycleService;
import com.android.support.lifecycle.LiveData;
import com.android.support.lifecycle.Observer;
import java.io.IOException;
import java.util.List;
/**
* Music playback service.
*/
public class MusicService extends LifecycleService implements OnCompletionListener,
OnPreparedListener {
// Note that only START action is an entry point "exposed" to the rest of the
// application. The rest are actions set on the notification intents fired off
// by this service itself.
public static final String ACTION_START = "com.android.sample.musicplayer.action.START";
private static final String ACTION_PLAY = "com.android.sample.musicplayer.action.PLAY";
private static final String ACTION_PAUSE = "com.android.sample.musicplayer.action.PAUSE";
private static final String ACTION_STOP = "com.android.sample.musicplayer.action.STOP";
private static final String ACTION_NEXT = "com.android.sample.musicplayer.action.NEXT";
private static final String ACTION_PREV = "com.android.sample.musicplayer.action.PREV";
private static final String RESOURCE_PREFIX =
"android.resource://com.android.sample.musicplayer/";
// The ID we use for the notification (the onscreen alert that appears at the notification
// area at the top of the screen as an icon -- and as text as well if the user expands the
// notification area).
private static final int NOTIFICATION_ID = 1;
private MediaSessionCompat mMediaSession;
private MediaPlayer mMediaPlayer = null;
private NotificationManagerCompat mNotificationManager;
private NotificationCompat.Builder mNotificationBuilder;
private MusicRepository mMusicRepository;
private int mCurrPlaybackState;
private int mCurrActiveTrackIndex;
private List<TrackMetadata> mTracks;
@Override
public void onCreate() {
super.onCreate();
mMediaSession = new MediaSessionCompat(this, MusicService.class.getSimpleName());
mMediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
mMediaSession.setActive(true);
mMusicRepository = MusicRepository.getInstance();
mTracks = mMusicRepository.getTracks();
// Attach Callback to receive MediaSession updates
mMediaSession.setCallback(new MediaSessionCompat.Callback() {
// Implement callbacks
@Override
public void onPlay() {
super.onPlay();
processPlayRequest();
}
@Override
public void onPause() {
super.onPause();
processPauseRequest();
}
@Override
public void onSkipToNext() {
super.onSkipToNext();
processNextRequest();
}
@Override
public void onSkipToPrevious() {
super.onSkipToPrevious();
processPreviousRequest();
}
@Override
public void onStop() {
super.onStop();
processStopRequest();
}
});
mNotificationManager = NotificationManagerCompat.from(this);
// Register self as the observer on the LiveData object that wraps the currently
// active track index.
LiveData<Integer> currentlyActiveTrackData = mMusicRepository.getCurrentlyActiveTrackData();
mCurrActiveTrackIndex = currentlyActiveTrackData.getValue();
currentlyActiveTrackData.observe(this, new Observer<Integer>() {
@Override
public void onChanged(@Nullable Integer integer) {
mCurrActiveTrackIndex = integer;
if (mCurrActiveTrackIndex < 0) {
return;
}
// Create the media player if necessary, set its data to the currently active track
// and call prepare(). This will eventually result in an asynchronous call to
// our onPrepared() method which will transition from PREPARING into PLAYING state.
createMediaPlayerIfNeeded();
try {
mMusicRepository.setState(MusicRepository.STATE_PREPARING);
@RawRes int trackRawRes = mTracks.get(mCurrActiveTrackIndex).getTrackRes();
mMediaPlayer.setDataSource(getBaseContext(),
Uri.parse(RESOURCE_PREFIX + trackRawRes));
mMediaPlayer.prepare();
} catch (IOException ioe) {
}
// As the media player is preparing the track, update the media session and the
// notification with the metadata of that track.
updateAudioMetadata();
updateNotification();
}
});
// Register self as the observer on the LiveData object that wraps the playback state.
LiveData<Integer> stateData = mMusicRepository.getStateData();
mCurrPlaybackState = stateData.getValue();
stateData.observe(this, new Observer<Integer>() {
@Override
public void onChanged(@Nullable Integer integer) {
mCurrPlaybackState = integer;
switch (mCurrPlaybackState) {
case MusicRepository.STATE_INITIAL:
createMediaPlayerIfNeeded();
break;
case MusicRepository.STATE_PLAYING:
// Start the media player and update the ongoing notification
configAndStartMediaPlayer();
updateNotification();
break;
case MusicRepository.STATE_PAUSED:
// Pause the media player and update the ongoing notification
mMediaPlayer.pause();
updateNotification();
}
}
});
}
private void createMediaPlayerIfNeeded() {
if (mMediaPlayer == null) {
mMediaPlayer = new MediaPlayer();
// Make sure the media player will acquire a wake-lock while playing. If we don't do
// that, the CPU might go to sleep while the song is playing, causing playback to stop.
//
// Remember that to use this, we have to declare the android.permission.WAKE_LOCK
// permission in AndroidManifest.xml.
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
// we want the media player to notify us when it's ready preparing, and when it's done
// playing:
mMediaPlayer.setOnPreparedListener(this);
mMediaPlayer.setOnCompletionListener(this);
} else {
mMediaPlayer.reset();
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
// Note that we don't do anything for the START action. The purpose of that action
// is to start the service. As the service registers itself to observe changes to
// playback state and current track, it will start the matching flows as a response
// to those changes.
// Here we handle service-internal actions that are registered on notification intents.
if (intent.getAction().equals(ACTION_PLAY)) {
processPlayRequest();
} else if (intent.getAction().equals(ACTION_PAUSE)) {
processPauseRequest();
} else if (intent.getAction().equals(ACTION_STOP)) {
processStopRequest();
} else if (intent.getAction().equals(ACTION_NEXT)) {
processNextRequest();
} else if (intent.getAction().equals(ACTION_PREV)) {
processPreviousRequest();
}
return START_NOT_STICKY;
}
private void processPlayRequest() {
// The logic here is different depending on our current state
if (mCurrPlaybackState == MusicRepository.STATE_STOPPED) {
// If we're stopped, just go ahead to the next song and start playing.
playNextSong();
} else if (mCurrPlaybackState == MusicRepository.STATE_PAUSED) {
// If we're paused, just continue playback. We are registered to listen to the changes
// in LiveData that tracks the playback state, and that observer will update our ongoing
// notification and resume the playback.
mMusicRepository.setState(MusicRepository.STATE_PLAYING);
}
}
private void processPauseRequest() {
if (mCurrPlaybackState == MusicRepository.STATE_PLAYING) {
// Move to the paused state. We are registered
// to listen to the changes in LiveData that tracks the playback state,
// and that observer will update our ongoing notification and pause the media
// player.
mMusicRepository.setState(MusicRepository.STATE_PAUSED);
}
}
private void processStopRequest() {
processStopRequest(false);
}
private void processStopRequest(boolean force) {
if (mCurrPlaybackState != MusicRepository.STATE_STOPPED || force) {
mMusicRepository.setState(MusicRepository.STATE_STOPPED);
// let go of all resources...
relaxResources(true);
// cancel the notification
mNotificationManager.cancel(NOTIFICATION_ID);
// service is no longer necessary. Will be started again if needed.
stopSelf();
}
}
private void processNextRequest() {
if (mCurrPlaybackState != MusicRepository.STATE_STOPPED) {
playNextSong();
}
}
private void processPreviousRequest() {
if (mCurrPlaybackState != MusicRepository.STATE_STOPPED) {
playPrevSong();
}
}
/**
* Releases resources used by the service for playback. This includes the "foreground service"
* status and notification, the wake locks and possibly the MediaPlayer.
*
* @param releaseMediaPlayer Indicates whether the Media Player should also be released or not
*/
private void relaxResources(boolean releaseMediaPlayer) {
// stop being a foreground service
//stopForeground(true);
// stop and release the Media Player, if it's available
if (releaseMediaPlayer && mMediaPlayer != null) {
mMediaPlayer.reset();
mMediaPlayer.release();
mMediaPlayer = null;
}
}
/**
* Reconfigures MediaPlayer according to audio focus settings and starts/restarts it. This
* method starts/restarts the MediaPlayer 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
* MediaPlayer paused or set it to a low volume, depending on what is allowed by the
* current focus settings. This method assumes mPlayer != null, so if you are calling it,
* you have to do so from a context where you are sure this is the case.
*/
private void configAndStartMediaPlayer() {
mMediaPlayer.setVolume(1.0f, 1.0f); // we can be loud
if (!mMediaPlayer.isPlaying()) {
mMediaPlayer.start();
}
}
/**
* Starts playing the next song in our repository.
*/
private void playNextSong() {
relaxResources(false); // release everything except MediaPlayer
// Ask the repository to go to the next track. We are registered to listen to the
// changes in LiveData that tracks the current track, and that observer will point the
// media player to the right URI
mMusicRepository.goToNextTrack();
}
/**
* Starts playing the previous song in our repository.
*/
private void playPrevSong() {
relaxResources(false); // release everything except MediaPlayer
// Ask the repository to go to the next track. We are registered to listen to the
// changes in LiveData that tracks the current track, and that observer will point the
// media player to the right URI
mMusicRepository.goToPreviousTrack();
}
/**
* Called when media player is done playing current song.
*/
public void onCompletion(MediaPlayer player) {
// The media player finished playing the current song, so we go ahead and start the next.
playNextSong();
}
/** Called when media player is done preparing. */
public void onPrepared(MediaPlayer player) {
// The media player is done preparing. That means we can start playing!
// We are registered to listen to the changes in LiveData that tracks the playback state,
// and that observer will update our ongoing notification
mMusicRepository.setState(MusicRepository.STATE_PLAYING);
}
/**
* Configures service as a foreground service. A foreground service is a service that's doing
* something the user is actively aware of (such as playing music), and must appear to the
* user as a notification. That's why we create the notification here.
*/
private void populateNotificationBuilderContent(String text) {
PendingIntent pi = PendingIntent.getActivity(getApplicationContext(),
(int) (System.currentTimeMillis() & 0xfffffff),
new Intent().setClass(getApplicationContext(), MainActivity.class)
.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
PendingIntent.FLAG_UPDATE_CURRENT);
boolean isPlaying = (mCurrPlaybackState == MusicRepository.STATE_PLAYING);
// Build the notification object.
mNotificationBuilder = new NotificationCompat.Builder(getApplicationContext());
mNotificationBuilder.setSmallIcon(R.drawable.ic_play_arrow_white_24dp);
mNotificationBuilder.setTicker(text);
mNotificationBuilder.setWhen(System.currentTimeMillis());
mNotificationBuilder.setContentTitle("RandomMusicPlayer");
mNotificationBuilder.setContentText(text);
mNotificationBuilder.setContentIntent(pi);
mNotificationBuilder.setOngoing(isPlaying);
int primaryActionDrawable = isPlaying ? android.R.drawable.ic_media_pause
: android.R.drawable.ic_media_play;
PendingIntent primaryActionIntent = isPlaying
? PendingIntent.getService(this, 12,
new Intent(this, MusicService.class).setAction(ACTION_PAUSE), 0)
: PendingIntent.getService(this, 13,
new Intent(this, MusicService.class).setAction(ACTION_PLAY), 0);
String primaryActionName = isPlaying ? "pause" : "play";
mNotificationBuilder.addAction(android.R.drawable.ic_media_previous, "previous",
PendingIntent.getService(this, 10,
new Intent(this, MusicService.class).setAction(ACTION_PREV), 0));
mNotificationBuilder.addAction(primaryActionDrawable, primaryActionName,
primaryActionIntent);
mNotificationBuilder.addAction(android.R.drawable.ic_media_next, "next",
PendingIntent.getService(this, 11,
new Intent(this, MusicService.class).setAction(ACTION_NEXT), 0));
mNotificationBuilder.setStyle(new NotificationCompat.MediaStyle()
.setShowActionsInCompactView(0, 1, 2)
.setMediaSession(mMediaSession.getSessionToken()));
}
private void updateNotification() {
if (mCurrPlaybackState == MusicRepository.STATE_INITIAL) {
return;
}
if (mNotificationBuilder == null) {
// This is the very first time we're creating our ongoing notification, and marking
// the service to be in the foreground.
populateNotificationBuilderContent("Initializing...");
startForeground(NOTIFICATION_ID, mNotificationBuilder.build());
return;
}
TrackMetadata currTrack = mTracks.get(mCurrActiveTrackIndex);
populateNotificationBuilderContent(currTrack.getTitle()
+ " by " + currTrack.getArtist());
mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
}
private void updateAudioMetadata() {
if (mCurrPlaybackState == MusicRepository.STATE_INITIAL) {
return;
}
Bitmap albumArt = BitmapFactory.decodeResource(getResources(), R.drawable.nougat_bg_2x);
// Update the current metadata
TrackMetadata current = mTracks.get(mCurrActiveTrackIndex);
mMediaSession.setMetadata(new MediaMetadataCompat.Builder()
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt)
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, current.getArtist())
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Track #" + current.getTitle())
.build());
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
super.onBind(intent);
return null;
}
@Override
public void onDestroy() {
super.onDestroy();
if (mMediaPlayer != null) {
mMediaPlayer.release();
mNotificationManager.cancel(NOTIFICATION_ID);
stopForeground(true);
}
}
}