123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705 |
- /**
- * ownCloud Android client application
- *
- * @author David A. Velasco
- * Copyright (C) 2016 ownCloud Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * 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
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
- package com.owncloud.android.media;
- import android.accounts.Account;
- import android.app.NotificationManager;
- import android.app.PendingIntent;
- import android.app.Service;
- import android.content.Context;
- import android.content.Intent;
- import android.media.AudioManager;
- import android.media.MediaPlayer;
- import android.media.MediaPlayer.OnCompletionListener;
- import android.media.MediaPlayer.OnErrorListener;
- import android.media.MediaPlayer.OnPreparedListener;
- import android.net.wifi.WifiManager;
- import android.net.wifi.WifiManager.WifiLock;
- import android.os.IBinder;
- import android.os.PowerManager;
- import android.support.v4.app.NotificationCompat;
- import android.widget.Toast;
- import com.owncloud.android.R;
- import com.owncloud.android.datamodel.OCFile;
- import com.owncloud.android.lib.common.utils.Log_OC;
- import com.owncloud.android.ui.activity.FileActivity;
- import com.owncloud.android.ui.activity.FileDisplayActivity;
- import com.owncloud.android.ui.notifications.NotificationUtils;
- import com.owncloud.android.utils.ThemeUtils;
- import java.io.IOException;
- /**
- * Service that handles media playback, both audio and video.
- *
- * Waits for Intents which signal the service to perform specific operations: Play, Pause,
- * Rewind, etc.
- */
- public class MediaService extends Service implements OnCompletionListener, OnPreparedListener,
- OnErrorListener, AudioManager.OnAudioFocusChangeListener {
- private static final String TAG = MediaService.class.getSimpleName();
- 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_PLAY_FILE = MY_PACKAGE + ".action.PLAY_FILE";
- public static final String ACTION_STOP_ALL = MY_PACKAGE + ".action.STOP_ALL";
- /// PreferenceKeys 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 final 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;
- /** 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 */
- private MediaPlayer mPlayer = null;
- /** Reference to the system AudioManager */
- private AudioManager mAudioManager = null;
- /** Values to indicate the state of the service */
- enum State {
- STOPPED,
- PREPARING,
- PLAYING,
- PAUSED
- }
- /** Current state */
- private State mState = State.STOPPED;
- /** Possible focus values */
- enum AudioFocus {
- NO_FOCUS,
- NO_FOCUS_CAN_DUCK,
- FOCUS
- }
- /** Current focus state */
- private AudioFocus mAudioFocus = AudioFocus.NO_FOCUS;
- /** 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";
- /** Notification to keep in the notification bar while a song is playing */
- private NotificationManager mNotificationManager;
- /** 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 milliseconds, 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;
- /** Notification builder to create notifications, new reuse way since Android 6 */
- private NotificationCompat.Builder mNotificationBuilder;
- /**
- * 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
- *
- * {@inheritDoc}
- */
- @Override
- public void onCreate() {
- super.onCreate();
- Log_OC.d(TAG, "Creating ownCloud media service");
- mWifiLock = ((WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE)).
- createWifiLock(WifiManager.WIFI_MODE_FULL, MEDIA_WIFI_LOCK_TAG);
- mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
- mNotificationBuilder = new NotificationCompat.Builder(this);
- mNotificationBuilder.setColor(ThemeUtils.primaryColor(this));
- mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
- mBinder = new MediaServiceBinder(this);
- }
- /**
- * Entry point for Intents requesting actions, sent here via startService.
- *
- * {@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_STOP_ALL)) {
- processStopRequest(true);
- }
- return START_NOT_STICKY; // don't want it to restart in case it's killed.
- }
- /**
- * Processes a request to play a media file received as a parameter
- *
- * TODO If a new request is received when a file is being prepared, it is ignored. Is this what we want?
- *
- * @param intent Intent received in the request with the data to identify the file to play.
- */
- private void processPlayFileRequest(Intent intent) {
- 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();
- }
- }
- /**
- * Processes a request to play a media file.
- */
- protected void processPlayRequest() {
- // request audio focus
- tryToGetAudioFocus();
- // actually play the song
- if (mState == State.STOPPED) {
- // (re)start playback
- playMedia();
- } else if (mState == State.PAUSED) {
- // continue playback
- mState = State.PLAYING;
- setUpAsForeground(String.format(getString(R.string.media_state_playing), mFile.getFileName()));
- configAndStartMediaPlayer();
- }
- }
- /**
- * Makes sure the media player exists and has been reset. This will create the media player
- * if needed. reset the existing media player if one already exists.
- */
- protected void createMediaPlayerIfNeeded() {
- if (mPlayer == null) {
- mPlayer = new MediaPlayer();
- // make sure the CPU won't go to sleep while media is playing
- mPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
- // the media player will notify the service when it's ready preparing, and when it's done playing
- mPlayer.setOnPreparedListener(this);
- mPlayer.setOnCompletionListener(this);
- mPlayer.setOnErrorListener(this);
- } else {
- mPlayer.reset();
- }
- }
- /**
- * Processes a request to pause the current playback
- */
- protected void processPauseRequest() {
- if (mState == State.PLAYING) {
- mState = State.PAUSED;
- mPlayer.pause();
- releaseResources(false); // retain media player in pause
- // TODO polite audio focus, instead of keep it owned; or not?
- }
- }
- /**
- * Processes a request to stop the playback.
- *
- * @param force When 'true', the playback is stopped no matter the value of mState
- */
- 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
- }
- }
- /**
- * 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
- */
- protected void releaseResources(boolean releaseMediaPlayer) {
- // stop being a foreground service
- stopForeground(true);
- // stop and release the Media Player, if it's available
- if (releaseMediaPlayer && mPlayer != null) {
- mPlayer.reset();
- mPlayer.release();
- mPlayer = null;
- }
- // release the Wifi lock, if holding it
- if (mWifiLock.isHeld()) {
- mWifiLock.release();
- }
- }
- /**
- * Fully releases the audio focus.
- */
- private void giveUpAudioFocus() {
- if (mAudioFocus == AudioFocus.FOCUS
- && mAudioManager != null
- && AudioManager.AUDIOFOCUS_REQUEST_GRANTED == mAudioManager.abandonAudioFocus(this)) {
- mAudioFocus = AudioFocus.NO_FOCUS;
- }
- }
- /**
- * Reconfigures MediaPlayer according to audio focus settings and starts/restarts it.
- */
- protected void configAndStartMediaPlayer() {
- if (mPlayer == null) {
- throw new IllegalStateException("mPlayer is NULL");
- }
- if (mAudioFocus == AudioFocus.NO_FOCUS) {
- if (mPlayer.isPlaying()) {
- mPlayer.pause(); // have to be polite; but mState is not changed, to resume when focus is received again
- }
- } else {
- if (mAudioFocus == AudioFocus.NO_FOCUS_CAN_DUCK) {
- mPlayer.setVolume(DUCK_VOLUME, DUCK_VOLUME);
- } else {
- mPlayer.setVolume(1.0f, 1.0f); // full volume
- }
- if (!mPlayer.isPlaying()) {
- mPlayer.start();
- }
- }
- }
- /**
- * Requests the audio focus to the Audio Manager
- */
- private void tryToGetAudioFocus() {
- if (mAudioFocus != AudioFocus.FOCUS
- && mAudioManager != null
- && (AudioManager.AUDIOFOCUS_REQUEST_GRANTED == mAudioManager.requestAudioFocus(this,
- AudioManager.STREAM_MUSIC,
- AudioManager.AUDIOFOCUS_GAIN))
- ) {
- mAudioFocus = AudioFocus.FOCUS;
- }
- }
- /**
- * Starts playing the current media file.
- */
- protected void playMedia() {
- mState = State.STOPPED;
- releaseResources(false); // release everything except MediaPlayer
- try {
- if (mFile == null) {
- Toast.makeText(this, R.string.media_err_nothing_to_play, Toast.LENGTH_LONG).show();
- processStopRequest(true);
- return;
- } else if (mAccount == null) {
- Toast.makeText(this, R.string.media_err_not_in_owncloud, Toast.LENGTH_LONG).show();
- processStopRequest(true);
- return;
- }
- 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);
- mState = State.PREPARING;
- setUpAsForeground(String.format(getString(R.string.media_state_loading), mFile.getFileName()));
- // starts preparing the media player in background
- mPlayer.prepareAsync();
- // prevent the Wifi from going to sleep when streaming
- /*
- if (mIsStreaming) {
- mWifiLock.acquire();
- } else
- */
- if (mWifiLock.isHeld()) {
- mWifiLock.release();
- }
- } catch (SecurityException | IOException | IllegalStateException | IllegalArgumentException e) {
- Log_OC.e(TAG, e.getClass().getSimpleName() + " 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);
- }
- }
- /** 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();
- if (mMediaController != null) {
- // somebody is still bound to the service
- player.seekTo(0);
- processPauseRequest();
- mMediaController.updatePausePlay();
- } else {
- // nobody is bound
- processStopRequest(true);
- }
- }
- /**
- * Called when media player is done preparing.
- *
- * Time to start.
- */
- 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();
- }
- }
- /**
- * Updates the status notification
- */
- private void updateNotification(String content) {
- String ticker = String.format(getString(R.string.media_notif_ticker), getString(R.string.app_name));
- // TODO check if updating the Intent is really necessary
- 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);
- mNotificationBuilder.setContentIntent(PendingIntent.getActivity(getApplicationContext(),
- (int) System.currentTimeMillis(),
- showDetailsIntent,
- PendingIntent.FLAG_UPDATE_CURRENT));
- mNotificationBuilder.setWhen(System.currentTimeMillis());
- mNotificationBuilder.setTicker(ticker);
- mNotificationBuilder.setContentTitle(ticker);
- mNotificationBuilder.setContentText(content);
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- mNotificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_MEDIA);
- }
- mNotificationManager.notify(R.string.media_notif_ticker, mNotificationBuilder.build());
- }
- /**
- * Configures the service as a foreground service.
- *
- * The system will avoid finishing the service as much as possible when resources as low.
- *
- * A notification must be created to keep the user aware of the existence of the service.
- */
- private void setUpAsForeground(String content) {
- String ticker = String.format(getString(R.string.media_notif_ticker), getString(R.string.app_name));
- /// creates status notification
- // TODO put a progress bar to follow the playback progress
- mNotificationBuilder.setSmallIcon(R.drawable.ic_play_arrow);
- //mNotification.tickerText = text;
- mNotificationBuilder.setWhen(System.currentTimeMillis());
- mNotificationBuilder.setOngoing(true);
- /// includes a pending intent in the notification showing the details view of the file
- 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);
- mNotificationBuilder.setContentIntent(PendingIntent.getActivity(getApplicationContext(),
- (int) System.currentTimeMillis(),
- showDetailsIntent,
- PendingIntent.FLAG_UPDATE_CURRENT));
- mNotificationBuilder.setContentTitle(ticker);
- mNotificationBuilder.setContentText(content);
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- mNotificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_MEDIA);
- }
- startForeground(R.string.media_notif_ticker, mNotificationBuilder.build());
- }
- /**
- * Called when there's an error playing media.
- *
- * Warns the user about the error and resets the media player.
- */
- public boolean onError(MediaPlayer mp, int what, int extra) {
- Log_OC.e(TAG, "Error in audio playback, what = " + what + ", extra = " + extra);
- 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.
- *
- * {@inheritDoc}
- */
- @Override
- public void onAudioFocusChange(int focusChange) {
- if (focusChange > 0) {
- // focus gain; check AudioManager.AUDIOFOCUS_* values
- 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;
- 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();
- }
- }
- }
- /**
- * Called when the service is finished for final clean-up.
- *
- * {@inheritDoc}
- */
- @Override
- public void onDestroy() {
- mState = State.STOPPED;
- releaseResources(true);
- giveUpAudioFocus();
- stopForeground(true);
- super.onDestroy();
- }
- /**
- * Provides a binder object that clients can use to perform operations on the MediaPlayer managed by the MediaService.
- */
- @Override
- public IBinder onBind(Intent arg) {
- return mBinder;
- }
- /**
- * Called when ALL the bound clients were onbound.
- *
- * The service is destroyed if playback stopped or paused
- */
- @Override
- public boolean onUnbind(Intent intent) {
- if (mState == State.PAUSED || mState == State.STOPPED) {
- processStopRequest(false);
- }
- return false; // not accepting rebinding (default behaviour)
- }
- /**
- * Accesses the current MediaPlayer instance in the service.
- *
- * To be handled carefully. Visibility is protected to be accessed only
- *
- * @return Current MediaPlayer instance handled by MediaService.
- */
- protected MediaPlayer getPlayer() {
- return mPlayer;
- }
- /**
- * Accesses the current OCFile loaded in the service.
- *
- * @return The current OCFile loaded in the service.
- */
- protected OCFile getCurrentFile() {
- return mFile;
- }
- /**
- * Accesses the current {@link State} of the MediaService.
- *
- * @return The current {@link State} of the MediaService.
- */
- protected State getState() {
- return mState;
- }
- protected void setMediaContoller(MediaControlView mediaController) {
- mMediaController = mediaController;
- }
- protected MediaControlView getMediaController() {
- return mMediaController;
- }
- }
|