Added music player service in background; 'open' button in details used to test ...
[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.RemoteViews;
41 import android.widget.Toast;
42
43 import java.io.File;
44 import java.io.IOException;
45
46 import com.owncloud.android.AccountUtils;
47 import com.owncloud.android.R;
48 import com.owncloud.android.datamodel.OCFile;
49 import com.owncloud.android.ui.activity.FileDetailActivity;
50 import com.owncloud.android.ui.fragment.FileDetailFragment;
51
52 /**
53 * Service that handles media playback, both audio and video.
54 *
55 * Waits for Intents which signal the service to perform specific operations: Play, Pause,
56 * Rewind, etc.
57 *
58 * @author David A. Velasco
59 */
60 public class MediaService extends Service implements OnCompletionListener, OnPreparedListener,
61 OnErrorListener, AudioManager.OnAudioFocusChangeListener {
62
63 private static final String TAG = MediaService.class.getSimpleName();
64
65 private static final String MY_PACKAGE = MediaService.class.getPackage() != null ? MediaService.class.getPackage().getName() : "com.owncloud.android.media";
66
67 /// Intent actions that we are prepared to handle
68 public static final String ACTION_TOGGLE_PLAYBACK = MY_PACKAGE + ".action.TOGGLE_PLAYBACK";
69 public static final String ACTION_PLAY = MY_PACKAGE + ".action.PLAY";
70 public static final String ACTION_PAUSE = MY_PACKAGE + ".android.media.action.PAUSE";
71 public static final String ACTION_STOP = MY_PACKAGE + ".android.media.action.STOP";
72 public static final String ACTION_REWIND = MY_PACKAGE + ".android.media.action.REWIND";
73 public static final String ACTION_PLAY_FILE = MY_PACKAGE + ".android.media.action.URL";
74
75 /// Keys to add extras to the action
76 public static final String EXTRA_FILE = MY_PACKAGE + ".extra.FILE";
77 public static final String EXTRA_ACCOUNT = MY_PACKAGE + ".extra.ACCOUNT";
78
79 /**
80 * Volume to set when audio focus is lost and ducking is allowed
81 */
82 private static final float DUCK_VOLUME = 0.1f;
83
84 /**
85 * Media player instance
86 */
87 private MediaPlayer mPlayer = null;
88
89
90 // our AudioFocusHelper object, if it's available (it's available on SDK level >= 8)
91 // If not available, this will be null. Always check for null before using!
92 //AudioFocusHelper mAudioFocusHelper = null;
93 /**
94 * Reference to the system AudioManager
95 */
96 private AudioManager mAudioManager = null;
97
98
99 /**
100 * Values to indicate the state of the service
101 */
102 enum State {
103 STOPPED,
104 PREPARING,
105 PLAYING,
106 PAUSED
107 };
108
109
110 /**
111 * Current state
112 */
113 private State mState = State.STOPPED;
114
115
116 enum PauseReason {
117 UserRequest, // paused by user request
118 FocusLoss, // paused because of audio focus loss
119 };
120
121
122 /**
123 * Possible focus values
124 */
125 enum AudioFocus {
126 NO_FOCUS,
127 NO_FOCUS_CAN_DUCK,
128 FOCUS
129 }
130
131 /**
132 * Current focus state
133 */
134 private AudioFocus mAudioFocus = AudioFocus.NO_FOCUS;
135
136
137 /**
138 * 'True' when the current song is streaming from the network
139 */
140 private boolean mIsStreaming = false;
141
142 /**
143 * Wifi lock kept to prevents the device from shutting off the radio when streaming a file.
144 */
145 private WifiLock mWifiLock;
146 private static final String MEDIA_WIFI_LOCK_TAG = MY_PACKAGE + ".WIFI_LOCK";
147
148
149
150 /**
151 * Id for the notification to keep in the notification bar while a song is playing
152 */
153 private final int NOTIFICATION_ID = 1;
154 private NotificationManager mNotificationManager;
155 private Notification mNotification = null;
156
157 private OCFile mFile;
158 private Account mAccount;
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
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 * @param intent Intent received in the request with the data to identify the file to play.
210 */
211 private void processPlayFileRequest(Intent intent) {
212 if (mState == State.PLAYING || mState == State.PAUSED || mState == State.STOPPED) {
213 mFile = intent.getExtras().getParcelable(EXTRA_FILE);
214 mAccount = intent.getExtras().getParcelable(EXTRA_ACCOUNT);
215 tryToGetAudioFocus();
216 playMedia();
217 }
218 // TODO think what happens if mState == State.PREPARING
219 }
220
221
222 /**
223 * Processes a request to play a media file.
224 */
225 void processPlayRequest() {
226 // request audio focus
227 tryToGetAudioFocus();
228
229 // actually play the song
230 if (mState == State.STOPPED) {
231 // (re)start playback
232 playMedia();
233
234 } else if (mState == State.PAUSED) {
235 // continue playback
236 mState = State.PLAYING;
237 setUpAsForeground(String.format(getString(R.string.media_state_playing), mFile.getFileName()));
238 configAndStartMediaPlayer();
239
240 }
241 }
242
243
244 /**
245 * Makes sure the media player exists and has been reset. This will create the media player
246 * if needed, or reset the existing media player if one already exists.
247 */
248 void createMediaPlayerIfNeeded() {
249 if (mPlayer == null) {
250 mPlayer = new MediaPlayer();
251
252 // make sure the CPU won't go to sleep while media is playing
253 mPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
254
255 // the media player will notify the service when it's ready preparing, and when it's done playing
256 mPlayer.setOnPreparedListener(this);
257 mPlayer.setOnCompletionListener(this);
258 mPlayer.setOnErrorListener(this);
259
260 } else {
261 mPlayer.reset();
262 }
263 }
264
265 /**
266 * Processes a request to toggle from PLAY to PAUSE, or from PAUSE to PLAY
267 */
268 private void processTogglePlaybackRequest() {
269 if (mState == State.PAUSED || mState == State.STOPPED) {
270 processPlayRequest();
271
272 } else {
273 processPauseRequest();
274 }
275 }
276
277 /**
278 * Processes a request to pause the current playback
279 */
280 private void processPauseRequest() {
281 if (mState == State.PLAYING) {
282 mState = State.PAUSED;
283 mPlayer.pause();
284 releaseResources(false); // retain media player in pause
285 // TODO polite audio focus, instead of keep it owned; or not?
286 }
287 }
288
289
290 /**
291 * Process a request to rewind the current media playback to the start point.
292 */
293 private void processRewindRequest() {
294 if (mState == State.PLAYING || mState == State.PAUSED) {
295 mPlayer.seekTo(0);
296 }
297 }
298
299 /**
300 * Processes a request to stop the playback
301 */
302 private void processStopRequest() {
303 processStopRequest(false);
304 }
305
306
307 /**
308 * Processes a request to stop the playback.
309 *
310 * @param force When 'true', the playback is stopped no matter the value of mState
311 */
312 void processStopRequest(boolean force) {
313 if (mState == State.PLAYING || mState == State.PAUSED || force) {
314 mState = State.STOPPED;
315
316 releaseResources(true);
317 giveUpAudioFocus();
318 stopSelf(); // service is no longer necessary
319 }
320 }
321
322
323 /**
324 * Releases resources used by the service for playback. This includes the "foreground service"
325 * status and notification, the wake locks and possibly the MediaPlayer.
326 *
327 * @param releaseMediaPlayer Indicates whether the Media Player should also be released or not
328 */
329 void releaseResources(boolean releaseMediaPlayer) {
330 // stop being a foreground service
331 stopForeground(true);
332
333 // stop and release the Media Player, if it's available
334 if (releaseMediaPlayer && mPlayer != null) {
335 mPlayer.reset();
336 mPlayer.release();
337 mPlayer = null;
338 }
339
340 // release the Wifi lock, if holding it
341 if (mWifiLock.isHeld()) {
342 mWifiLock.release();
343 }
344 }
345
346
347 /**
348 * Fully releases the audio focus.
349 */
350 private void giveUpAudioFocus() {
351 if (mAudioFocus == AudioFocus.FOCUS
352 && mAudioManager != null
353 && AudioManager.AUDIOFOCUS_REQUEST_GRANTED == mAudioManager.abandonAudioFocus(this)) {
354
355 mAudioFocus = AudioFocus.NO_FOCUS;
356 }
357 }
358
359
360 /**
361 * Reconfigures MediaPlayer according to audio focus settings and starts/restarts it.
362 */
363 void configAndStartMediaPlayer() {
364 if (mPlayer == null) {
365 throw new IllegalStateException("mPlayer is NULL");
366 }
367
368 if (mAudioFocus == AudioFocus.NO_FOCUS) {
369 if (mPlayer.isPlaying()) {
370 mPlayer.pause(); // have to be polite; but mState is not changed, to resume when focus is received again
371 }
372
373 } else {
374 if (mAudioFocus == AudioFocus.NO_FOCUS_CAN_DUCK) {
375 mPlayer.setVolume(DUCK_VOLUME, DUCK_VOLUME);
376
377 } else {
378 mPlayer.setVolume(1.0f, 1.0f); // full volume
379 }
380
381 if (!mPlayer.isPlaying()) {
382 mPlayer.start();
383 }
384 }
385 }
386
387
388 /**
389 * Requests the audio focus to the Audio Manager
390 */
391 private void tryToGetAudioFocus() {
392 if (mAudioFocus != AudioFocus.FOCUS
393 && mAudioManager != null
394 && (AudioManager.AUDIOFOCUS_REQUEST_GRANTED == mAudioManager.requestAudioFocus( this,
395 AudioManager.STREAM_MUSIC,
396 AudioManager.AUDIOFOCUS_GAIN))
397 ) {
398 mAudioFocus = AudioFocus.FOCUS;
399 }
400 }
401
402
403 public static class Item {
404 long id;
405 String artist;
406 String title;
407 String album;
408 long duration;
409
410 public Item(long id, String artist, String title, String album, long duration) {
411 this.id = id;
412 this.artist = artist;
413 this.title = title;
414 this.album = album;
415 this.duration = duration;
416 }
417
418 public long getId() {
419 return id;
420 }
421
422 public String getArtist() {
423 return artist;
424 }
425
426 public String getTitle() {
427 return title;
428 }
429
430 public String getAlbum() {
431 return album;
432 }
433
434 public long getDuration() {
435 return duration;
436 }
437
438 public Uri getURI() {
439 return ContentUris.withAppendedId(
440 android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
441 }
442 }
443
444
445
446 /**
447 * Starts playing the current media file.
448 */
449 void playMedia() {
450 mState = State.STOPPED;
451 releaseResources(false); // release everything except MediaPlayer
452
453 try {
454 if (mFile == null) {
455 Toast.makeText(this, R.string.media_err_nothing_to_play, Toast.LENGTH_LONG).show();
456 processStopRequest(true);
457 return;
458
459 } else if (mAccount == null) {
460 Toast.makeText(this, R.string.media_err_not_in_owncloud, Toast.LENGTH_LONG).show();
461 processStopRequest(true);
462 return;
463 }
464
465 createMediaPlayerIfNeeded();
466 mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
467 String url = mFile.getStoragePath();
468 if (url == null || url.length() <= 0) {
469 url = AccountUtils.constructFullURLForAccount(this, mAccount) + mFile.getRemotePath();
470 }
471 mIsStreaming = url.startsWith("http:") || url.startsWith("https:");
472
473 mPlayer.setDataSource(url);
474
475 mState = State.PREPARING;
476 setUpAsForeground(String.format(getString(R.string.media_state_loading), mFile.getFileName()));
477
478 // starts preparing the media player in background
479 mPlayer.prepareAsync();
480
481 // prevent the Wifi from going to sleep when streaming
482 if (mIsStreaming) {
483 mWifiLock.acquire();
484 } else if (mWifiLock.isHeld()) {
485 mWifiLock.release();
486 }
487
488 } catch (SecurityException e) {
489 Log.e(TAG, "SecurityException playing " + mAccount.name + mFile.getRemotePath(), e);
490 // TODO message to the user
491
492 } catch (IOException e) {
493 Log.e(TAG, "IOException playing " + mAccount.name + mFile.getRemotePath(), e);
494 // TODO message to the user
495
496 } catch (IllegalStateException e) {
497 Log.e(TAG, "IllegalStateException " + mAccount.name + mFile.getRemotePath(), e);
498
499 } catch (IllegalArgumentException e) {
500 Log.e(TAG, "IllegalArgumentException " + mAccount.name + mFile.getRemotePath(), e);
501 e.printStackTrace();
502 }
503 }
504
505
506 /** Called when media player is done playing current song. */
507 public void onCompletion(MediaPlayer player) {
508 Toast.makeText(this, String.format(getString(R.string.media_event_done, mFile.getFileName())), Toast.LENGTH_LONG).show();
509 processStopRequest(true);
510 return;
511 }
512
513
514 /**
515 * Called when media player is done preparing.
516 *
517 * Time to start.
518 */
519 public void onPrepared(MediaPlayer player) {
520 mState = State.PLAYING;
521 updateNotification(String.format(getString(R.string.media_state_playing), mFile.getFileName()));
522 configAndStartMediaPlayer();
523 }
524
525
526 /**
527 * Updates the status notification
528 */
529 @SuppressWarnings("deprecation")
530 private void updateNotification(String content) {
531 // TODO check if updating the Intent is really necessary
532 Intent showDetailsIntent = new Intent(this, FileDetailActivity.class);
533 showDetailsIntent.putExtra(FileDetailFragment.EXTRA_FILE, mFile);
534 showDetailsIntent.putExtra(FileDetailFragment.EXTRA_ACCOUNT, mAccount);
535 showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
536 mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(),
537 (int)System.currentTimeMillis(),
538 showDetailsIntent,
539 PendingIntent.FLAG_UPDATE_CURRENT);
540 mNotification.when = System.currentTimeMillis();
541 //mNotification.contentView.setTextViewText(R.id.status_text, content);
542 String ticker = "ownCloud MusicPlayer";
543 mNotification.setLatestEventInfo(getApplicationContext(), ticker, content, mNotification.contentIntent);
544 mNotificationManager.notify(NOTIFICATION_ID, mNotification);
545 }
546
547
548 /**
549 * Configures the service as a foreground service.
550 *
551 * The system will avoid finishing the service as much as possible when resources as low.
552 *
553 * A notification must be created to keep the user aware of the existance of the service.
554 */
555 @SuppressWarnings("deprecation")
556 private void setUpAsForeground(String content) {
557 /// creates status notification
558 // TODO put a progress bar to follow the playback progress
559 mNotification = new Notification();
560 mNotification.icon = android.R.drawable.ic_media_play;
561 //mNotification.tickerText = text;
562 mNotification.when = System.currentTimeMillis();
563 mNotification.flags |= Notification.FLAG_ONGOING_EVENT;
564 //mNotification.contentView.setTextViewText(R.id.status_text, "ownCloud Music Player"); // NULL POINTER
565 //mNotification.contentView.setTextViewText(R.id.status_text, getString(R.string.downloader_download_in_progress_content));
566
567
568 /// includes a pending intent in the notification showing the details view of the file
569 Intent showDetailsIntent = new Intent(this, FileDetailActivity.class);
570 showDetailsIntent.putExtra(FileDetailFragment.EXTRA_FILE, mFile);
571 showDetailsIntent.putExtra(FileDetailFragment.EXTRA_ACCOUNT, mAccount);
572 showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
573 mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(),
574 (int)System.currentTimeMillis(),
575 showDetailsIntent,
576 PendingIntent.FLAG_UPDATE_CURRENT);
577
578
579 //mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotification);
580 String ticker = "ownCloud MusicPlayer";
581 mNotification.setLatestEventInfo(getApplicationContext(), ticker, content, mNotification.contentIntent);
582 startForeground(NOTIFICATION_ID, mNotification);
583
584 }
585
586 /**
587 * Called when there's an error playing media.
588 *
589 * Warns the user about the error and resets the media player.
590 */
591 public boolean onError(MediaPlayer mp, int what, int extra) {
592 // TODO FOLLOW HERE!!!!!!
593
594 Toast.makeText(getApplicationContext(), "Media player error! Resetting.",
595 Toast.LENGTH_SHORT).show();
596 Log.e(TAG, "Error: what=" + String.valueOf(what) + ", extra=" + String.valueOf(extra));
597
598 mState = State.STOPPED;
599 releaseResources(true);
600 giveUpAudioFocus();
601 return true;
602 }
603
604
605 /**
606 * Called by the system when another app tries to play some sound.
607 *
608 * {@inheritDoc}
609 */
610 @Override
611 public void onAudioFocusChange(int focusChange) {
612 if (focusChange > 0) {
613 // focus gain; check AudioManager.AUDIOFOCUS_* values
614 Toast.makeText(getApplicationContext(), "gained audio focus.", Toast.LENGTH_SHORT).show();
615 mAudioFocus = AudioFocus.FOCUS;
616
617 // restart media player with new focus settings
618 if (mState == State.PLAYING)
619 configAndStartMediaPlayer();
620
621 } else if (focusChange < 0) {
622 // focus loss; check AudioManager.AUDIOFOCUS_* values
623 boolean canDuck = AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK == focusChange;
624 Toast.makeText(getApplicationContext(), "lost audio focus." + (canDuck ? "can duck" :
625 "no duck"), Toast.LENGTH_SHORT).show();
626 mAudioFocus = canDuck ? AudioFocus.NO_FOCUS_CAN_DUCK : AudioFocus.NO_FOCUS;
627
628 // start/restart/pause media player with new focus settings
629 if (mPlayer != null && mPlayer.isPlaying())
630 configAndStartMediaPlayer();
631 }
632
633 }
634
635 /**
636 * Called when the service is finished for final clean-up.
637 *
638 * {@inheritDoc}
639 */
640 @Override
641 public void onDestroy() {
642 mState = State.STOPPED;
643 releaseResources(true);
644 giveUpAudioFocus();
645 }
646
647
648 @Override
649 public IBinder onBind(Intent arg0) {
650 // TODO provide a binding API? may we use a service to play VIDEO?
651 return null;
652 }
653
654 }