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