-/* ownCloud Android client application
- * Copyright 2013 ownCloud Inc.
+/**
+ * ownCloud Android client application
+ *
+ * @author David A. Velasco
+ * Copyright (C) 2015 ownCloud Inc.
*
* This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
-import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener;
import android.media.MediaPlayer.OnPreparedListener;
-import android.net.Uri;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.WifiLock;
import android.os.IBinder;
import android.os.PowerManager;
-import android.util.Log;
import android.widget.Toast;
import java.io.IOException;
-import com.owncloud.android.AccountUtils;
import com.owncloud.android.R;
import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.ui.activity.FileDetailActivity;
-import com.owncloud.android.ui.fragment.FileDetailFragment;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.ui.activity.FileActivity;
+import com.owncloud.android.ui.activity.FileDisplayActivity;
+
/**
* Service that handles media playback, both audio and video.
*
* Waits for Intents which signal the service to perform specific operations: Play, Pause,
* Rewind, etc.
- *
- * @author David A. Velasco
*/
public class MediaService extends Service implements OnCompletionListener, OnPreparedListener,
OnErrorListener, AudioManager.OnAudioFocusChangeListener {
private static final String MY_PACKAGE = MediaService.class.getPackage() != null ? MediaService.class.getPackage().getName() : "com.owncloud.android.media";
/// Intent actions that we are prepared to handle
- public static final String ACTION_TOGGLE_PLAYBACK = MY_PACKAGE + ".action.TOGGLE_PLAYBACK";
- public static final String ACTION_PLAY = MY_PACKAGE + ".action.PLAY";
- public static final String ACTION_PAUSE = MY_PACKAGE + ".android.media.action.PAUSE";
- public static final String ACTION_STOP = MY_PACKAGE + ".android.media.action.STOP";
- public static final String ACTION_REWIND = MY_PACKAGE + ".android.media.action.REWIND";
- public static final String ACTION_PLAY_FILE = MY_PACKAGE + ".android.media.action.URL";
+ public static final String ACTION_PLAY_FILE = MY_PACKAGE + ".action.PLAY_FILE";
+ public static final String ACTION_STOP_ALL = MY_PACKAGE + ".action.STOP_ALL";
/// Keys to add extras to the action
public static final String EXTRA_FILE = MY_PACKAGE + ".extra.FILE";
public static final String EXTRA_ACCOUNT = MY_PACKAGE + ".extra.ACCOUNT";
+ public static String EXTRA_START_POSITION = MY_PACKAGE + ".extra.START_POSITION";
+ public static final String EXTRA_PLAY_ON_LOAD = MY_PACKAGE + ".extra.PLAY_ON_LOAD";
+
+
+ /** Error code for specific messages - see regular error codes at {@link MediaPlayer} */
+ public static final int OC_MEDIA_ERROR = 0;
+
+ /** Time To keep the control panel visible when the user does not use it */
+ public static final int MEDIA_CONTROL_SHORT_LIFE = 4000;
- /**
- * Volume to set when audio focus is lost and ducking is allowed
- */
+ /** Time To keep the control panel visible when the user does not use it */
+ public static final int MEDIA_CONTROL_PERMANENT = 0;
+
+ /** Volume to set when audio focus is lost and ducking is allowed */
private static final float DUCK_VOLUME = 0.1f;
- /**
- * Media player instance
- */
+ /** Media player instance */
private MediaPlayer mPlayer = null;
-
- // our AudioFocusHelper object, if it's available (it's available on SDK level >= 8)
- // If not available, this will be null. Always check for null before using!
- //AudioFocusHelper mAudioFocusHelper = null;
- /**
- * Reference to the system AudioManager
- */
+ /** Reference to the system AudioManager */
private AudioManager mAudioManager = null;
- /**
- * Values to indicate the state of the service
- */
+ /** Values to indicate the state of the service */
enum State {
STOPPED,
PREPARING,
};
- /**
- * Current state
- */
+ /** Current state */
private State mState = State.STOPPED;
-
- enum PauseReason {
- UserRequest, // paused by user request
- FocusLoss, // paused because of audio focus loss
- };
-
-
- /**
- * Possible focus values
- */
+ /** Possible focus values */
enum AudioFocus {
NO_FOCUS,
NO_FOCUS_CAN_DUCK,
FOCUS
}
- /**
- * Current focus state
- */
+ /** Current focus state */
private AudioFocus mAudioFocus = AudioFocus.NO_FOCUS;
- /**
- * 'True' when the current song is streaming from the network
- */
+ /** 'True' when the current song is streaming from the network */
private boolean mIsStreaming = false;
- /**
- * Wifi lock kept to prevents the device from shutting off the radio when streaming a file.
- */
+ /** Wifi lock kept to prevents the device from shutting off the radio when streaming a file. */
private WifiLock mWifiLock;
+
private static final String MEDIA_WIFI_LOCK_TAG = MY_PACKAGE + ".WIFI_LOCK";
-
-
- /**
- * Id for the notification to keep in the notification bar while a song is playing
- */
- private final int NOTIFICATION_ID = 1;
+ /** Notification to keep in the notification bar while a song is playing */
private NotificationManager mNotificationManager;
private Notification mNotification = null;
-
+
+ /** File being played */
private OCFile mFile;
+
+ /** Account holding the file being played */
private Account mAccount;
+ /** Flag signaling if the audio should be played immediately when the file is prepared */
+ protected boolean mPlayOnPrepared;
+
+ /** Position, in miliseconds, where the audio should be started */
+ private int mStartPosition;
+
+ /** Interface to access the service through binding */
private IBinder mBinder;
+
+ /** Control panel shown to the user to control the playback, to register through binding */
+ private MediaControlView mMediaController;
+
+ /**
+ * Helper method to get an error message suitable to show to users for errors occurred in media playback,
+ *
+ * @param context A context to access string resources.
+ * @param what See {@link MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)
+ * @param extra See {@link MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)
+ * @return Message suitable to users.
+ */
+ public static String getMessageForMediaError(Context context, int what, int extra) {
+ int messageId;
+
+ if (what == OC_MEDIA_ERROR) {
+ messageId = extra;
+
+ } else if (extra == MediaPlayer.MEDIA_ERROR_UNSUPPORTED) {
+ /* Added in API level 17
+ Bitstream is conforming to the related coding standard or file spec, but the media framework does not support the feature.
+ Constant Value: -1010 (0xfffffc0e)
+ */
+ messageId = R.string.media_err_unsupported;
+
+ } else if (extra == MediaPlayer.MEDIA_ERROR_IO) {
+ /* Added in API level 17
+ File or network related operation errors.
+ Constant Value: -1004 (0xfffffc14)
+ */
+ messageId = R.string.media_err_io;
+
+ } else if (extra == MediaPlayer.MEDIA_ERROR_MALFORMED) {
+ /* Added in API level 17
+ Bitstream is not conforming to the related coding standard or file spec.
+ Constant Value: -1007 (0xfffffc11)
+ */
+ messageId = R.string.media_err_malformed;
+
+ } else if (extra == MediaPlayer.MEDIA_ERROR_TIMED_OUT) {
+ /* Added in API level 17
+ Some operation takes too long to complete, usually more than 3-5 seconds.
+ Constant Value: -110 (0xffffff92)
+ */
+ messageId = R.string.media_err_timeout;
+
+ } else if (what == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
+ /* Added in API level 3
+ The video is streamed and its container is not valid for progressive playback i.e the video's index (e.g moov atom) is not at the start of the file.
+ Constant Value: 200 (0x000000c8)
+ */
+ messageId = R.string.media_err_invalid_progressive_playback;
+
+ } else {
+ /* MediaPlayer.MEDIA_ERROR_UNKNOWN
+ Added in API level 1
+ Unspecified media player error.
+ Constant Value: 1 (0x00000001)
+ */
+ /* MediaPlayer.MEDIA_ERROR_SERVER_DIED)
+ Added in API level 1
+ Media server died. In this case, the application must release the MediaPlayer object and instantiate a new one.
+ Constant Value: 100 (0x00000064)
+ */
+ messageId = R.string.media_err_unknown;
+ }
+ return context.getString(messageId);
+ }
+
+
/**
* Initialize a service instance
*
*/
@Override
public void onCreate() {
- Log.d(TAG, "Creating ownCloud media service");
+ super.onCreate();
+ Log_OC.d(TAG, "Creating ownCloud media service");
mWifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE)).
createWifiLock(WifiManager.WIFI_MODE_FULL, MEDIA_WIFI_LOCK_TAG);
/**
* Entry point for Intents requesting actions, sent here via startService.
*
- * TODO maybe, replace by an API based in binding
+ * {@inheritDoc}
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent.getAction();
if (action.equals(ACTION_PLAY_FILE)) {
processPlayFileRequest(intent);
- } else if (action.equals(ACTION_PLAY)) {
- processPlayRequest();
- } else if (action.equals(ACTION_TOGGLE_PLAYBACK)) {
- processTogglePlaybackRequest();
- } else if (action.equals(ACTION_PAUSE)) {
- processPauseRequest();
- } else if (action.equals(ACTION_STOP)) {
- processStopRequest();
- } else if (action.equals(ACTION_REWIND)) {
- processRewindRequest();
+
+ } else if (action.equals(ACTION_STOP_ALL)) {
+ processStopRequest(true);
}
return START_NOT_STICKY; // don't want it to restart in case it's killed.
* @param intent Intent received in the request with the data to identify the file to play.
*/
private void processPlayFileRequest(Intent intent) {
- if (mState == State.PLAYING || mState == State.PAUSED || mState == State.STOPPED) {
+ if (mState != State.PREPARING) {
mFile = intent.getExtras().getParcelable(EXTRA_FILE);
mAccount = intent.getExtras().getParcelable(EXTRA_ACCOUNT);
+ mPlayOnPrepared = intent.getExtras().getBoolean(EXTRA_PLAY_ON_LOAD, false);
+ mStartPosition = intent.getExtras().getInt(EXTRA_START_POSITION, 0);
tryToGetAudioFocus();
playMedia();
}
/**
* Makes sure the media player exists and has been reset. This will create the media player
- * if needed, or reset the existing media player if one already exists.
+ * if needed. reset the existing media player if one already exists.
*/
protected void createMediaPlayerIfNeeded() {
if (mPlayer == null) {
}
/**
- * Processes a request to toggle from PLAY to PAUSE, or from PAUSE to PLAY
- */
- private void processTogglePlaybackRequest() {
- if (mState == State.PAUSED || mState == State.STOPPED) {
- processPlayRequest();
-
- } else {
- processPauseRequest();
- }
- }
-
- /**
* Processes a request to pause the current playback
*/
protected void processPauseRequest() {
/**
- * Process a request to rewind the current media playback to the start point.
- */
- private void processRewindRequest() {
- if (mState == State.PLAYING || mState == State.PAUSED) {
- mPlayer.seekTo(0);
- }
- }
-
- /**
- * Processes a request to stop the playback
- */
- private void processStopRequest() {
- processStopRequest(false);
- }
-
-
- /**
* Processes a request to stop the playback.
*
* @param force When 'true', the playback is stopped no matter the value of mState
*/
- void processStopRequest(boolean force) {
- if (mState == State.PLAYING || mState == State.PAUSED || mState == State.STOPPED || force) {
+ protected void processStopRequest(boolean force) {
+ if (mState != State.PREPARING || force) {
mState = State.STOPPED;
-
mFile = null;
mAccount = null;
-
releaseResources(true);
giveUpAudioFocus();
stopSelf(); // service is no longer necessary
*
* @param releaseMediaPlayer Indicates whether the Media Player should also be released or not
*/
- void releaseResources(boolean releaseMediaPlayer) {
+ protected void releaseResources(boolean releaseMediaPlayer) {
// stop being a foreground service
stopForeground(true);
/**
* Reconfigures MediaPlayer according to audio focus settings and starts/restarts it.
*/
- void configAndStartMediaPlayer() {
+ protected void configAndStartMediaPlayer() {
if (mPlayer == null) {
throw new IllegalStateException("mPlayer is NULL");
}
}
- public static class Item {
- long id;
- String artist;
- String title;
- String album;
- long duration;
-
- public Item(long id, String artist, String title, String album, long duration) {
- this.id = id;
- this.artist = artist;
- this.title = title;
- this.album = album;
- this.duration = duration;
- }
-
- public long getId() {
- return id;
- }
-
- public String getArtist() {
- return artist;
- }
-
- public String getTitle() {
- return title;
- }
-
- public String getAlbum() {
- return album;
- }
-
- public long getDuration() {
- return duration;
- }
-
- public Uri getURI() {
- return ContentUris.withAppendedId(
- android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
- }
- }
-
-
-
/**
* Starts playing the current media file.
*/
- void playMedia() {
+ protected void playMedia() {
mState = State.STOPPED;
releaseResources(false); // release everything except MediaPlayer
createMediaPlayerIfNeeded();
mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
String url = mFile.getStoragePath();
+ /* Streaming is not possible right now
if (url == null || url.length() <= 0) {
url = AccountUtils.constructFullURLForAccount(this, mAccount) + mFile.getRemotePath();
}
mIsStreaming = url.startsWith("http:") || url.startsWith("https:");
+ */
+ mIsStreaming = false;
mPlayer.setDataSource(url);
}
} catch (SecurityException e) {
- Log.e(TAG, "SecurityException playing " + mAccount.name + mFile.getRemotePath(), e);
- // TODO message to the user
+ Log_OC.e(TAG, "SecurityException playing " + mAccount.name + mFile.getRemotePath(), e);
+ Toast.makeText(this, String.format(getString(R.string.media_err_security_ex), mFile.getFileName()), Toast.LENGTH_LONG).show();
+ processStopRequest(true);
} catch (IOException e) {
- Log.e(TAG, "IOException playing " + mAccount.name + mFile.getRemotePath(), e);
- // TODO message to the user
+ Log_OC.e(TAG, "IOException playing " + mAccount.name + mFile.getRemotePath(), e);
+ Toast.makeText(this, String.format(getString(R.string.media_err_io_ex), mFile.getFileName()), Toast.LENGTH_LONG).show();
+ processStopRequest(true);
} catch (IllegalStateException e) {
- Log.e(TAG, "IllegalStateException " + mAccount.name + mFile.getRemotePath(), e);
+ Log_OC.e(TAG, "IllegalStateException " + mAccount.name + mFile.getRemotePath(), e);
+ Toast.makeText(this, String.format(getString(R.string.media_err_unexpected), mFile.getFileName()), Toast.LENGTH_LONG).show();
+ processStopRequest(true);
} catch (IllegalArgumentException e) {
- Log.e(TAG, "IllegalArgumentException " + mAccount.name + mFile.getRemotePath(), e);
- e.printStackTrace();
+ Log_OC.e(TAG, "IllegalArgumentException " + mAccount.name + mFile.getRemotePath(), e);
+ Toast.makeText(this, String.format(getString(R.string.media_err_unexpected), mFile.getFileName()), Toast.LENGTH_LONG).show();
+ processStopRequest(true);
}
}
/** Called when media player is done playing current song. */
public void onCompletion(MediaPlayer player) {
Toast.makeText(this, String.format(getString(R.string.media_event_done, mFile.getFileName())), Toast.LENGTH_LONG).show();
- processStopRequest(true);
+ if (mMediaController != null) {
+ // somebody is still bound to the service
+ player.seekTo(0);
+ processPauseRequest();
+ mMediaController.updatePausePlay();
+ } else {
+ // nobody is bound
+ processStopRequest(true);
+ }
return;
}
public void onPrepared(MediaPlayer player) {
mState = State.PLAYING;
updateNotification(String.format(getString(R.string.media_state_playing), mFile.getFileName()));
+ if (mMediaController != null) {
+ mMediaController.setEnabled(true);
+ }
+ player.seekTo(mStartPosition);
configAndStartMediaPlayer();
+ if (!mPlayOnPrepared) {
+ processPauseRequest();
+ }
+
+ if (mMediaController != null) {
+ mMediaController.updatePausePlay();
+ }
}
@SuppressWarnings("deprecation")
private void updateNotification(String content) {
// TODO check if updating the Intent is really necessary
- Intent showDetailsIntent = new Intent(this, FileDetailActivity.class);
- showDetailsIntent.putExtra(FileDetailFragment.EXTRA_FILE, mFile);
- showDetailsIntent.putExtra(FileDetailFragment.EXTRA_ACCOUNT, mAccount);
+ Intent showDetailsIntent = new Intent(this, FileDisplayActivity.class);
+ showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, mFile);
+ showDetailsIntent.putExtra(FileActivity.EXTRA_ACCOUNT, mAccount);
showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(),
(int)System.currentTimeMillis(),
PendingIntent.FLAG_UPDATE_CURRENT);
mNotification.when = System.currentTimeMillis();
//mNotification.contentView.setTextViewText(R.id.status_text, content);
- String ticker = "ownCloud MusicPlayer";
+ String ticker = String.format(getString(R.string.media_notif_ticker), getString(R.string.app_name));
mNotification.setLatestEventInfo(getApplicationContext(), ticker, content, mNotification.contentIntent);
- mNotificationManager.notify(NOTIFICATION_ID, mNotification);
+ mNotificationManager.notify(R.string.media_notif_ticker, mNotification);
}
/// includes a pending intent in the notification showing the details view of the file
- Intent showDetailsIntent = new Intent(this, FileDetailActivity.class);
- showDetailsIntent.putExtra(FileDetailFragment.EXTRA_FILE, mFile);
- showDetailsIntent.putExtra(FileDetailFragment.EXTRA_ACCOUNT, mAccount);
+ Intent showDetailsIntent = new Intent(this, FileDisplayActivity.class);
+ showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, mFile);
+ showDetailsIntent.putExtra(FileActivity.EXTRA_ACCOUNT, mAccount);
showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(),
(int)System.currentTimeMillis(),
//mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotification);
- String ticker = "ownCloud MusicPlayer";
+ String ticker = String.format(getString(R.string.media_notif_ticker), getString(R.string.app_name));
mNotification.setLatestEventInfo(getApplicationContext(), ticker, content, mNotification.contentIntent);
- startForeground(NOTIFICATION_ID, mNotification);
+ startForeground(R.string.media_notif_ticker, mNotification);
}
* Warns the user about the error and resets the media player.
*/
public boolean onError(MediaPlayer mp, int what, int extra) {
- // TODO FOLLOW HERE!!!!!!
+ Log_OC.e(TAG, "Error in audio playback, what = " + what + ", extra = " + extra);
- Toast.makeText(getApplicationContext(), "Media player error! Resetting.",
- Toast.LENGTH_SHORT).show();
- Log.e(TAG, "Error: what=" + String.valueOf(what) + ", extra=" + String.valueOf(extra));
-
- mState = State.STOPPED;
- releaseResources(true);
- giveUpAudioFocus();
+ String message = getMessageForMediaError(this, what, extra);
+ Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show();
+
+ processStopRequest(true);
return true;
}
-
/**
* Called by the system when another app tries to play some sound.
*
public void onAudioFocusChange(int focusChange) {
if (focusChange > 0) {
// focus gain; check AudioManager.AUDIOFOCUS_* values
- Toast.makeText(getApplicationContext(), "gained audio focus.", Toast.LENGTH_SHORT).show();
mAudioFocus = AudioFocus.FOCUS;
-
// restart media player with new focus settings
if (mState == State.PLAYING)
configAndStartMediaPlayer();
} else if (focusChange < 0) {
// focus loss; check AudioManager.AUDIOFOCUS_* values
boolean canDuck = AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK == focusChange;
- Toast.makeText(getApplicationContext(), "lost audio focus." + (canDuck ? "can duck" :
- "no duck"), Toast.LENGTH_SHORT).show();
mAudioFocus = canDuck ? AudioFocus.NO_FOCUS_CAN_DUCK : AudioFocus.NO_FOCUS;
-
// start/restart/pause media player with new focus settings
if (mPlayer != null && mPlayer.isPlaying())
configAndStartMediaPlayer();
mState = State.STOPPED;
releaseResources(true);
giveUpAudioFocus();
+ super.onDestroy();
}
@Override
public boolean onUnbind(Intent intent) {
if (mState == State.PAUSED || mState == State.STOPPED) {
- Log.d(TAG, "Stopping service due to unbind in pause");
processStopRequest(false);
}
return false; // not accepting rebinding (default behaviour)
*
* @return The current {@link State} of the MediaService.
*/
- public State getState() {
+ protected State getState() {
return mState;
}
+
+ protected void setMediaContoller(MediaControlView mediaController) {
+ mMediaController = mediaController;
+ }
+
+ protected MediaControlView getMediaController() {
+ return mMediaController;
+ }
+
}