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