cccbaa70f8e7df03f72c2876e1be4684d69c6967
[pub/Android/ownCloud.git] / src / com / owncloud / android / media / MediaService.java
1 /* ownCloud Android client application
2 * Copyright 2013 ownCloud Inc.
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 */
18
19 package com.owncloud.android.media;
20
21 import android.accounts.Account;
22 import android.app.Notification;
23 import android.app.NotificationManager;
24 import android.app.PendingIntent;
25 import android.app.Service;
26 import android.content.ContentUris;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.media.AudioManager;
30 import android.media.MediaPlayer;
31 import android.media.MediaPlayer.OnCompletionListener;
32 import android.media.MediaPlayer.OnErrorListener;
33 import android.media.MediaPlayer.OnPreparedListener;
34 import android.net.Uri;
35 import android.net.wifi.WifiManager;
36 import android.net.wifi.WifiManager.WifiLock;
37 import android.os.IBinder;
38 import android.os.PowerManager;
39 import android.util.Log;
40 import android.widget.Toast;
41
42 import java.io.IOException;
43
44 import com.owncloud.android.AccountUtils;
45 import com.owncloud.android.R;
46 import com.owncloud.android.datamodel.OCFile;
47 import com.owncloud.android.ui.activity.FileDetailActivity;
48 import com.owncloud.android.ui.fragment.FileDetailFragment;
49
50 /**
51 * Service that handles media playback, both audio and video.
52 *
53 * Waits for Intents which signal the service to perform specific operations: Play, Pause,
54 * Rewind, etc.
55 *
56 * @author David A. Velasco
57 */
58 public class MediaService extends Service implements OnCompletionListener, OnPreparedListener,
59 OnErrorListener, AudioManager.OnAudioFocusChangeListener {
60
61 private static final String TAG = MediaService.class.getSimpleName();
62
63 private static final String MY_PACKAGE = MediaService.class.getPackage() != null ? MediaService.class.getPackage().getName() : "com.owncloud.android.media";
64
65 /// Intent actions that we are prepared to handle
66 public static final String ACTION_TOGGLE_PLAYBACK = MY_PACKAGE + ".action.TOGGLE_PLAYBACK";
67 public static final String ACTION_PLAY = MY_PACKAGE + ".action.PLAY";
68 public static final String ACTION_PAUSE = MY_PACKAGE + ".android.media.action.PAUSE";
69 public static final String ACTION_STOP = MY_PACKAGE + ".android.media.action.STOP";
70 public static final String ACTION_REWIND = MY_PACKAGE + ".android.media.action.REWIND";
71 public static final String ACTION_PLAY_FILE = MY_PACKAGE + ".android.media.action.URL";
72
73 /// Keys to add extras to the action
74 public static final String EXTRA_FILE = MY_PACKAGE + ".extra.FILE";
75 public static final String EXTRA_ACCOUNT = MY_PACKAGE + ".extra.ACCOUNT";
76
77 /**
78 * Volume to set when audio focus is lost and ducking is allowed
79 */
80 private static final float DUCK_VOLUME = 0.1f;
81
82 /**
83 * Media player instance
84 */
85 private MediaPlayer mPlayer = null;
86
87
88 // our AudioFocusHelper object, if it's available (it's available on SDK level >= 8)
89 // If not available, this will be null. Always check for null before using!
90 //AudioFocusHelper mAudioFocusHelper = null;
91 /**
92 * Reference to the system AudioManager
93 */
94 private AudioManager mAudioManager = null;
95
96
97 /**
98 * Values to indicate the state of the service
99 */
100 enum State {
101 STOPPED,
102 PREPARING,
103 PLAYING,
104 PAUSED
105 };
106
107
108 /**
109 * Current state
110 */
111 private State mState = State.STOPPED;
112
113
114 enum PauseReason {
115 UserRequest, // paused by user request
116 FocusLoss, // paused because of audio focus loss
117 };
118
119
120 /**
121 * Possible focus values
122 */
123 enum AudioFocus {
124 NO_FOCUS,
125 NO_FOCUS_CAN_DUCK,
126 FOCUS
127 }
128
129 /**
130 * Current focus state
131 */
132 private AudioFocus mAudioFocus = AudioFocus.NO_FOCUS;
133
134
135 /**
136 * 'True' when the current song is streaming from the network
137 */
138 private boolean mIsStreaming = false;
139
140 /**
141 * Wifi lock kept to prevents the device from shutting off the radio when streaming a file.
142 */
143 private WifiLock mWifiLock;
144 private static final String MEDIA_WIFI_LOCK_TAG = MY_PACKAGE + ".WIFI_LOCK";
145
146
147
148 /**
149 * Id for the notification to keep in the notification bar while a song is playing
150 */
151 private final int NOTIFICATION_ID = 1;
152 private NotificationManager mNotificationManager;
153 private Notification mNotification = null;
154
155 private OCFile mFile;
156 private Account mAccount;
157
158 private IBinder mBinder;
159
160
161
162 /**
163 * Initialize a service instance
164 *
165 * {@inheritDoc}
166 */
167 @Override
168 public void onCreate() {
169 Log.d(TAG, "Creating ownCloud media service");
170
171 mWifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE)).
172 createWifiLock(WifiManager.WIFI_MODE_FULL, MEDIA_WIFI_LOCK_TAG);
173
174 mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
175 mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
176 mBinder = new MediaServiceBinder(this);
177 }
178
179
180 /**
181 * Entry point for Intents requesting actions, sent here via startService.
182 *
183 * TODO maybe, replace by an API based in binding
184 */
185 @Override
186 public int onStartCommand(Intent intent, int flags, int startId) {
187 String action = intent.getAction();
188 if (action.equals(ACTION_PLAY_FILE)) {
189 processPlayFileRequest(intent);
190 } else if (action.equals(ACTION_PLAY)) {
191 processPlayRequest();
192 } else if (action.equals(ACTION_TOGGLE_PLAYBACK)) {
193 processTogglePlaybackRequest();
194 } else if (action.equals(ACTION_PAUSE)) {
195 processPauseRequest();
196 } else if (action.equals(ACTION_STOP)) {
197 processStopRequest();
198 } else if (action.equals(ACTION_REWIND)) {
199 processRewindRequest();
200 }
201
202 return START_NOT_STICKY; // don't want it to restart in case it's killed.
203 }
204
205
206 /**
207 * Processes a request to play a media file received as a parameter
208 *
209 * TODO If a new request is received when a file is being prepared, it is ignored. Is this what we want?
210 *
211 * @param intent Intent received in the request with the data to identify the file to play.
212 */
213 private void processPlayFileRequest(Intent intent) {
214 if (mState == State.PLAYING || mState == State.PAUSED || mState == State.STOPPED) {
215 mFile = intent.getExtras().getParcelable(EXTRA_FILE);
216 mAccount = intent.getExtras().getParcelable(EXTRA_ACCOUNT);
217 tryToGetAudioFocus();
218 playMedia();
219 }
220 }
221
222
223 /**
224 * Processes a request to play a media file.
225 */
226 protected void processPlayRequest() {
227 // request audio focus
228 tryToGetAudioFocus();
229
230 // actually play the song
231 if (mState == State.STOPPED) {
232 // (re)start playback
233 playMedia();
234
235 } else if (mState == State.PAUSED) {
236 // continue playback
237 mState = State.PLAYING;
238 setUpAsForeground(String.format(getString(R.string.media_state_playing), mFile.getFileName()));
239 configAndStartMediaPlayer();
240
241 }
242 }
243
244
245 /**
246 * Makes sure the media player exists and has been reset. This will create the media player
247 * if needed, or reset the existing media player if one already exists.
248 */
249 protected void createMediaPlayerIfNeeded() {
250 if (mPlayer == null) {
251 mPlayer = new MediaPlayer();
252
253 // make sure the CPU won't go to sleep while media is playing
254 mPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
255
256 // the media player will notify the service when it's ready preparing, and when it's done playing
257 mPlayer.setOnPreparedListener(this);
258 mPlayer.setOnCompletionListener(this);
259 mPlayer.setOnErrorListener(this);
260
261 } else {
262 mPlayer.reset();
263 }
264 }
265
266 /**
267 * Processes a request to toggle from PLAY to PAUSE, or from PAUSE to PLAY
268 */
269 private void processTogglePlaybackRequest() {
270 if (mState == State.PAUSED || mState == State.STOPPED) {
271 processPlayRequest();
272
273 } else {
274 processPauseRequest();
275 }
276 }
277
278 /**
279 * Processes a request to pause the current playback
280 */
281 protected void processPauseRequest() {
282 if (mState == State.PLAYING) {
283 mState = State.PAUSED;
284 mPlayer.pause();
285 releaseResources(false); // retain media player in pause
286 // TODO polite audio focus, instead of keep it owned; or not?
287 }
288 }
289
290
291 /**
292 * Process a request to rewind the current media playback to the start point.
293 */
294 private void processRewindRequest() {
295 if (mState == State.PLAYING || mState == State.PAUSED) {
296 mPlayer.seekTo(0);
297 }
298 }
299
300 /**
301 * Processes a request to stop the playback
302 */
303 private void processStopRequest() {
304 processStopRequest(false);
305 }
306
307
308 /**
309 * Processes a request to stop the playback.
310 *
311 * @param force When 'true', the playback is stopped no matter the value of mState
312 */
313 void processStopRequest(boolean force) {
314 if (mState == State.PLAYING || mState == State.PAUSED || mState == State.STOPPED || force) {
315 mState = State.STOPPED;
316
317 mFile = null;
318 mAccount = null;
319
320 releaseResources(true);
321 giveUpAudioFocus();
322 stopSelf(); // service is no longer necessary
323 }
324 }
325
326
327 /**
328 * Releases resources used by the service for playback. This includes the "foreground service"
329 * status and notification, the wake locks and possibly the MediaPlayer.
330 *
331 * @param releaseMediaPlayer Indicates whether the Media Player should also be released or not
332 */
333 void releaseResources(boolean releaseMediaPlayer) {
334 // stop being a foreground service
335 stopForeground(true);
336
337 // stop and release the Media Player, if it's available
338 if (releaseMediaPlayer && mPlayer != null) {
339 mPlayer.reset();
340 mPlayer.release();
341 mPlayer = null;
342 }
343
344 // release the Wifi lock, if holding it
345 if (mWifiLock.isHeld()) {
346 mWifiLock.release();
347 }
348 }
349
350
351 /**
352 * Fully releases the audio focus.
353 */
354 private void giveUpAudioFocus() {
355 if (mAudioFocus == AudioFocus.FOCUS
356 && mAudioManager != null
357 && AudioManager.AUDIOFOCUS_REQUEST_GRANTED == mAudioManager.abandonAudioFocus(this)) {
358
359 mAudioFocus = AudioFocus.NO_FOCUS;
360 }
361 }
362
363
364 /**
365 * Reconfigures MediaPlayer according to audio focus settings and starts/restarts it.
366 */
367 void configAndStartMediaPlayer() {
368 if (mPlayer == null) {
369 throw new IllegalStateException("mPlayer is NULL");
370 }
371
372 if (mAudioFocus == AudioFocus.NO_FOCUS) {
373 if (mPlayer.isPlaying()) {
374 mPlayer.pause(); // have to be polite; but mState is not changed, to resume when focus is received again
375 }
376
377 } else {
378 if (mAudioFocus == AudioFocus.NO_FOCUS_CAN_DUCK) {
379 mPlayer.setVolume(DUCK_VOLUME, DUCK_VOLUME);
380
381 } else {
382 mPlayer.setVolume(1.0f, 1.0f); // full volume
383 }
384
385 if (!mPlayer.isPlaying()) {
386 mPlayer.start();
387 }
388 }
389 }
390
391
392 /**
393 * Requests the audio focus to the Audio Manager
394 */
395 private void tryToGetAudioFocus() {
396 if (mAudioFocus != AudioFocus.FOCUS
397 && mAudioManager != null
398 && (AudioManager.AUDIOFOCUS_REQUEST_GRANTED == mAudioManager.requestAudioFocus( this,
399 AudioManager.STREAM_MUSIC,
400 AudioManager.AUDIOFOCUS_GAIN))
401 ) {
402 mAudioFocus = AudioFocus.FOCUS;
403 }
404 }
405
406
407 public static class Item {
408 long id;
409 String artist;
410 String title;
411 String album;
412 long duration;
413
414 public Item(long id, String artist, String title, String album, long duration) {
415 this.id = id;
416 this.artist = artist;
417 this.title = title;
418 this.album = album;
419 this.duration = duration;
420 }
421
422 public long getId() {
423 return id;
424 }
425
426 public String getArtist() {
427 return artist;
428 }
429
430 public String getTitle() {
431 return title;
432 }
433
434 public String getAlbum() {
435 return album;
436 }
437
438 public long getDuration() {
439 return duration;
440 }
441
442 public Uri getURI() {
443 return ContentUris.withAppendedId(
444 android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
445 }
446 }
447
448
449
450 /**
451 * Starts playing the current media file.
452 */
453 void playMedia() {
454 mState = State.STOPPED;
455 releaseResources(false); // release everything except MediaPlayer
456
457 try {
458 if (mFile == null) {
459 Toast.makeText(this, R.string.media_err_nothing_to_play, Toast.LENGTH_LONG).show();
460 processStopRequest(true);
461 return;
462
463 } else if (mAccount == null) {
464 Toast.makeText(this, R.string.media_err_not_in_owncloud, Toast.LENGTH_LONG).show();
465 processStopRequest(true);
466 return;
467 }
468
469 createMediaPlayerIfNeeded();
470 mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
471 String url = mFile.getStoragePath();
472 if (url == null || url.length() <= 0) {
473 url = AccountUtils.constructFullURLForAccount(this, mAccount) + mFile.getRemotePath();
474 }
475 mIsStreaming = url.startsWith("http:") || url.startsWith("https:");
476
477 mPlayer.setDataSource(url);
478
479 mState = State.PREPARING;
480 setUpAsForeground(String.format(getString(R.string.media_state_loading), mFile.getFileName()));
481
482 // starts preparing the media player in background
483 mPlayer.prepareAsync();
484
485 // prevent the Wifi from going to sleep when streaming
486 if (mIsStreaming) {
487 mWifiLock.acquire();
488 } else if (mWifiLock.isHeld()) {
489 mWifiLock.release();
490 }
491
492 } catch (SecurityException e) {
493 Log.e(TAG, "SecurityException playing " + mAccount.name + mFile.getRemotePath(), e);
494 // TODO message to the user
495
496 } catch (IOException e) {
497 Log.e(TAG, "IOException playing " + mAccount.name + mFile.getRemotePath(), e);
498 // TODO message to the user
499
500 } catch (IllegalStateException e) {
501 Log.e(TAG, "IllegalStateException " + mAccount.name + mFile.getRemotePath(), e);
502
503 } catch (IllegalArgumentException e) {
504 Log.e(TAG, "IllegalArgumentException " + mAccount.name + mFile.getRemotePath(), e);
505 e.printStackTrace();
506 }
507 }
508
509
510 /** Called when media player is done playing current song. */
511 public void onCompletion(MediaPlayer player) {
512 Toast.makeText(this, String.format(getString(R.string.media_event_done, mFile.getFileName())), Toast.LENGTH_LONG).show();
513 processStopRequest(true);
514 return;
515 }
516
517
518 /**
519 * Called when media player is done preparing.
520 *
521 * Time to start.
522 */
523 public void onPrepared(MediaPlayer player) {
524 mState = State.PLAYING;
525 updateNotification(String.format(getString(R.string.media_state_playing), mFile.getFileName()));
526 configAndStartMediaPlayer();
527 }
528
529
530 /**
531 * Updates the status notification
532 */
533 @SuppressWarnings("deprecation")
534 private void updateNotification(String content) {
535 // TODO check if updating the Intent is really necessary
536 Intent showDetailsIntent = new Intent(this, FileDetailActivity.class);
537 showDetailsIntent.putExtra(FileDetailFragment.EXTRA_FILE, mFile);
538 showDetailsIntent.putExtra(FileDetailFragment.EXTRA_ACCOUNT, mAccount);
539 showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
540 mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(),
541 (int)System.currentTimeMillis(),
542 showDetailsIntent,
543 PendingIntent.FLAG_UPDATE_CURRENT);
544 mNotification.when = System.currentTimeMillis();
545 //mNotification.contentView.setTextViewText(R.id.status_text, content);
546 String ticker = "ownCloud MusicPlayer";
547 mNotification.setLatestEventInfo(getApplicationContext(), ticker, content, mNotification.contentIntent);
548 mNotificationManager.notify(NOTIFICATION_ID, mNotification);
549 }
550
551
552 /**
553 * Configures the service as a foreground service.
554 *
555 * The system will avoid finishing the service as much as possible when resources as low.
556 *
557 * A notification must be created to keep the user aware of the existance of the service.
558 */
559 @SuppressWarnings("deprecation")
560 private void setUpAsForeground(String content) {
561 /// creates status notification
562 // TODO put a progress bar to follow the playback progress
563 mNotification = new Notification();
564 mNotification.icon = android.R.drawable.ic_media_play;
565 //mNotification.tickerText = text;
566 mNotification.when = System.currentTimeMillis();
567 mNotification.flags |= Notification.FLAG_ONGOING_EVENT;
568 //mNotification.contentView.setTextViewText(R.id.status_text, "ownCloud Music Player"); // NULL POINTER
569 //mNotification.contentView.setTextViewText(R.id.status_text, getString(R.string.downloader_download_in_progress_content));
570
571
572 /// includes a pending intent in the notification showing the details view of the file
573 Intent showDetailsIntent = new Intent(this, FileDetailActivity.class);
574 showDetailsIntent.putExtra(FileDetailFragment.EXTRA_FILE, mFile);
575 showDetailsIntent.putExtra(FileDetailFragment.EXTRA_ACCOUNT, mAccount);
576 showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
577 mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(),
578 (int)System.currentTimeMillis(),
579 showDetailsIntent,
580 PendingIntent.FLAG_UPDATE_CURRENT);
581
582
583 //mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotification);
584 String ticker = "ownCloud MusicPlayer";
585 mNotification.setLatestEventInfo(getApplicationContext(), ticker, content, mNotification.contentIntent);
586 startForeground(NOTIFICATION_ID, mNotification);
587
588 }
589
590 /**
591 * Called when there's an error playing media.
592 *
593 * Warns the user about the error and resets the media player.
594 */
595 public boolean onError(MediaPlayer mp, int what, int extra) {
596 // TODO FOLLOW HERE!!!!!!
597
598 Toast.makeText(getApplicationContext(), "Media player error! Resetting.",
599 Toast.LENGTH_SHORT).show();
600 Log.e(TAG, "Error: what=" + String.valueOf(what) + ", extra=" + String.valueOf(extra));
601
602 mState = State.STOPPED;
603 releaseResources(true);
604 giveUpAudioFocus();
605 return true;
606 }
607
608
609 /**
610 * Called by the system when another app tries to play some sound.
611 *
612 * {@inheritDoc}
613 */
614 @Override
615 public void onAudioFocusChange(int focusChange) {
616 if (focusChange > 0) {
617 // focus gain; check AudioManager.AUDIOFOCUS_* values
618 Toast.makeText(getApplicationContext(), "gained audio focus.", Toast.LENGTH_SHORT).show();
619 mAudioFocus = AudioFocus.FOCUS;
620
621 // restart media player with new focus settings
622 if (mState == State.PLAYING)
623 configAndStartMediaPlayer();
624
625 } else if (focusChange < 0) {
626 // focus loss; check AudioManager.AUDIOFOCUS_* values
627 boolean canDuck = AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK == focusChange;
628 Toast.makeText(getApplicationContext(), "lost audio focus." + (canDuck ? "can duck" :
629 "no duck"), Toast.LENGTH_SHORT).show();
630 mAudioFocus = canDuck ? AudioFocus.NO_FOCUS_CAN_DUCK : AudioFocus.NO_FOCUS;
631
632 // start/restart/pause media player with new focus settings
633 if (mPlayer != null && mPlayer.isPlaying())
634 configAndStartMediaPlayer();
635 }
636
637 }
638
639 /**
640 * Called when the service is finished for final clean-up.
641 *
642 * {@inheritDoc}
643 */
644 @Override
645 public void onDestroy() {
646 mState = State.STOPPED;
647 releaseResources(true);
648 giveUpAudioFocus();
649 }
650
651
652 /**
653 * Provides a binder object that clients can use to perform operations on the MediaPlayer managed by the MediaService.
654 */
655 @Override
656 public IBinder onBind(Intent arg) {
657 return mBinder;
658 }
659
660
661 /**
662 * Called when ALL the bound clients were onbound.
663 *
664 * The service is destroyed if playback stopped or paused
665 */
666 @Override
667 public boolean onUnbind(Intent intent) {
668 if (mState == State.PAUSED || mState == State.STOPPED) {
669 Log.d(TAG, "Stopping service due to unbind in pause");
670 processStopRequest(false);
671 }
672 return false; // not accepting rebinding (default behaviour)
673 }
674
675
676 /**
677 * Accesses the current MediaPlayer instance in the service.
678 *
679 * To be handled carefully. Visibility is protected to be accessed only
680 *
681 * @return Current MediaPlayer instance handled by MediaService.
682 */
683 protected MediaPlayer getPlayer() {
684 return mPlayer;
685 }
686
687
688 /**
689 * Accesses the current OCFile loaded in the service.
690 *
691 * @return The current OCFile loaded in the service.
692 */
693 protected OCFile getCurrentFile() {
694 return mFile;
695 }
696
697
698 /**
699 * Accesses the current {@link State} of the MediaService.
700 *
701 * @return The current {@link State} of the MediaService.
702 */
703 public State getState() {
704 return mState;
705 }
706
707 }