48aa841710622fd563ebfa42acc6525ebfdf9682
[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 * Initialize a service instance
137 *
138 * {@inheritDoc}
139 */
140 @Override
141 public void onCreate() {
142 Log.d(TAG, "Creating ownCloud media service");
143
144 mWifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE)).
145 createWifiLock(WifiManager.WIFI_MODE_FULL, MEDIA_WIFI_LOCK_TAG);
146
147 mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
148 mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
149 mBinder = new MediaServiceBinder(this);
150 }
151
152
153 /**
154 * Entry point for Intents requesting actions, sent here via startService.
155 *
156 * {@inheritDoc}
157 */
158 @Override
159 public int onStartCommand(Intent intent, int flags, int startId) {
160 String action = intent.getAction();
161 if (action.equals(ACTION_PLAY_FILE)) {
162 processPlayFileRequest(intent);
163 }
164
165 return START_NOT_STICKY; // don't want it to restart in case it's killed.
166 }
167
168
169 /**
170 * Processes a request to play a media file received as a parameter
171 *
172 * TODO If a new request is received when a file is being prepared, it is ignored. Is this what we want?
173 *
174 * @param intent Intent received in the request with the data to identify the file to play.
175 */
176 private void processPlayFileRequest(Intent intent) {
177 if (mState != State.PREPARING) {
178 mFile = intent.getExtras().getParcelable(EXTRA_FILE);
179 mAccount = intent.getExtras().getParcelable(EXTRA_ACCOUNT);
180 tryToGetAudioFocus();
181 playMedia();
182 }
183 }
184
185
186 /**
187 * Processes a request to play a media file.
188 */
189 protected void processPlayRequest() {
190 // request audio focus
191 tryToGetAudioFocus();
192
193 // actually play the song
194 if (mState == State.STOPPED) {
195 // (re)start playback
196 playMedia();
197
198 } else if (mState == State.PAUSED) {
199 // continue playback
200 mState = State.PLAYING;
201 setUpAsForeground(String.format(getString(R.string.media_state_playing), mFile.getFileName()));
202 configAndStartMediaPlayer();
203
204 }
205 }
206
207
208 /**
209 * Makes sure the media player exists and has been reset. This will create the media player
210 * if needed, or reset the existing media player if one already exists.
211 */
212 protected void createMediaPlayerIfNeeded() {
213 if (mPlayer == null) {
214 mPlayer = new MediaPlayer();
215
216 // make sure the CPU won't go to sleep while media is playing
217 mPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
218
219 // the media player will notify the service when it's ready preparing, and when it's done playing
220 mPlayer.setOnPreparedListener(this);
221 mPlayer.setOnCompletionListener(this);
222 mPlayer.setOnErrorListener(this);
223
224 } else {
225 mPlayer.reset();
226 }
227 }
228
229 /**
230 * Processes a request to pause the current playback
231 */
232 protected void processPauseRequest() {
233 if (mState == State.PLAYING) {
234 mState = State.PAUSED;
235 mPlayer.pause();
236 releaseResources(false); // retain media player in pause
237 // TODO polite audio focus, instead of keep it owned; or not?
238 }
239 }
240
241
242 /**
243 * Processes a request to stop the playback.
244 *
245 * @param force When 'true', the playback is stopped no matter the value of mState
246 */
247 protected void processStopRequest(boolean force) {
248 if (mState != State.PREPARING || force) {
249 mState = State.STOPPED;
250 mFile = null;
251 mAccount = null;
252 releaseResources(true);
253 giveUpAudioFocus();
254 stopSelf(); // service is no longer necessary
255 }
256 }
257
258
259 /**
260 * Releases resources used by the service for playback. This includes the "foreground service"
261 * status and notification, the wake locks and possibly the MediaPlayer.
262 *
263 * @param releaseMediaPlayer Indicates whether the Media Player should also be released or not
264 */
265 protected void releaseResources(boolean releaseMediaPlayer) {
266 // stop being a foreground service
267 stopForeground(true);
268
269 // stop and release the Media Player, if it's available
270 if (releaseMediaPlayer && mPlayer != null) {
271 mPlayer.reset();
272 mPlayer.release();
273 mPlayer = null;
274 }
275
276 // release the Wifi lock, if holding it
277 if (mWifiLock.isHeld()) {
278 mWifiLock.release();
279 }
280 }
281
282
283 /**
284 * Fully releases the audio focus.
285 */
286 private void giveUpAudioFocus() {
287 if (mAudioFocus == AudioFocus.FOCUS
288 && mAudioManager != null
289 && AudioManager.AUDIOFOCUS_REQUEST_GRANTED == mAudioManager.abandonAudioFocus(this)) {
290
291 mAudioFocus = AudioFocus.NO_FOCUS;
292 }
293 }
294
295
296 /**
297 * Reconfigures MediaPlayer according to audio focus settings and starts/restarts it.
298 */
299 protected void configAndStartMediaPlayer() {
300 if (mPlayer == null) {
301 throw new IllegalStateException("mPlayer is NULL");
302 }
303
304 if (mAudioFocus == AudioFocus.NO_FOCUS) {
305 if (mPlayer.isPlaying()) {
306 mPlayer.pause(); // have to be polite; but mState is not changed, to resume when focus is received again
307 }
308
309 } else {
310 if (mAudioFocus == AudioFocus.NO_FOCUS_CAN_DUCK) {
311 mPlayer.setVolume(DUCK_VOLUME, DUCK_VOLUME);
312
313 } else {
314 mPlayer.setVolume(1.0f, 1.0f); // full volume
315 }
316
317 if (!mPlayer.isPlaying()) {
318 mPlayer.start();
319 }
320 }
321 }
322
323
324 /**
325 * Requests the audio focus to the Audio Manager
326 */
327 private void tryToGetAudioFocus() {
328 if (mAudioFocus != AudioFocus.FOCUS
329 && mAudioManager != null
330 && (AudioManager.AUDIOFOCUS_REQUEST_GRANTED == mAudioManager.requestAudioFocus( this,
331 AudioManager.STREAM_MUSIC,
332 AudioManager.AUDIOFOCUS_GAIN))
333 ) {
334 mAudioFocus = AudioFocus.FOCUS;
335 }
336 }
337
338
339 /**
340 * Starts playing the current media file.
341 */
342 protected void playMedia() {
343 mState = State.STOPPED;
344 releaseResources(false); // release everything except MediaPlayer
345
346 try {
347 if (mFile == null) {
348 Toast.makeText(this, R.string.media_err_nothing_to_play, Toast.LENGTH_LONG).show();
349 processStopRequest(true);
350 return;
351
352 } else if (mAccount == null) {
353 Toast.makeText(this, R.string.media_err_not_in_owncloud, Toast.LENGTH_LONG).show();
354 processStopRequest(true);
355 return;
356 }
357
358 createMediaPlayerIfNeeded();
359 mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
360 String url = mFile.getStoragePath();
361 if (url == null || url.length() <= 0) {
362 url = AccountUtils.constructFullURLForAccount(this, mAccount) + mFile.getRemotePath();
363 }
364 mIsStreaming = url.startsWith("http:") || url.startsWith("https:");
365
366 mPlayer.setDataSource(url);
367
368 mState = State.PREPARING;
369 setUpAsForeground(String.format(getString(R.string.media_state_loading), mFile.getFileName()));
370
371 // starts preparing the media player in background
372 mPlayer.prepareAsync();
373
374 // prevent the Wifi from going to sleep when streaming
375 if (mIsStreaming) {
376 mWifiLock.acquire();
377 } else if (mWifiLock.isHeld()) {
378 mWifiLock.release();
379 }
380
381 } catch (SecurityException e) {
382 Log.e(TAG, "SecurityException playing " + mAccount.name + mFile.getRemotePath(), e);
383 Toast.makeText(this, String.format(getString(R.string.media_err_security_ex), mFile.getFileName()), Toast.LENGTH_LONG).show();
384 processStopRequest(true);
385
386 } catch (IOException e) {
387 Log.e(TAG, "IOException playing " + mAccount.name + mFile.getRemotePath(), e);
388 Toast.makeText(this, String.format(getString(R.string.media_err_io_ex), mFile.getFileName()), Toast.LENGTH_LONG).show();
389 processStopRequest(true);
390
391 } catch (IllegalStateException e) {
392 Log.e(TAG, "IllegalStateException " + mAccount.name + mFile.getRemotePath(), e);
393 Toast.makeText(this, String.format(getString(R.string.media_err_unexpected), mFile.getFileName()), Toast.LENGTH_LONG).show();
394 processStopRequest(true);
395
396 } catch (IllegalArgumentException e) {
397 Log.e(TAG, "IllegalArgumentException " + mAccount.name + mFile.getRemotePath(), e);
398 Toast.makeText(this, String.format(getString(R.string.media_err_unexpected), mFile.getFileName()), Toast.LENGTH_LONG).show();
399 processStopRequest(true);
400 }
401 }
402
403
404 /** Called when media player is done playing current song. */
405 public void onCompletion(MediaPlayer player) {
406 if (mMediaController != null) {
407 mMediaController.hide();
408 }
409 Toast.makeText(this, String.format(getString(R.string.media_event_done, mFile.getFileName())), Toast.LENGTH_LONG).show();
410 processStopRequest(true);
411 return;
412 }
413
414
415 /**
416 * Called when media player is done preparing.
417 *
418 * Time to start.
419 */
420 public void onPrepared(MediaPlayer player) {
421 mState = State.PLAYING;
422 updateNotification(String.format(getString(R.string.media_state_playing), mFile.getFileName()));
423 if (mMediaController != null) {
424 mMediaController.setEnabled(true);
425 }
426 configAndStartMediaPlayer();
427 if (mMediaController != null) {
428 mMediaController.show(MEDIA_CONTROL_LIFE);
429 }
430 }
431
432
433 /**
434 * Updates the status notification
435 */
436 @SuppressWarnings("deprecation")
437 private void updateNotification(String content) {
438 // TODO check if updating the Intent is really necessary
439 Intent showDetailsIntent = new Intent(this, FileDetailActivity.class);
440 showDetailsIntent.putExtra(FileDetailFragment.EXTRA_FILE, mFile);
441 showDetailsIntent.putExtra(FileDetailFragment.EXTRA_ACCOUNT, mAccount);
442 showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
443 mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(),
444 (int)System.currentTimeMillis(),
445 showDetailsIntent,
446 PendingIntent.FLAG_UPDATE_CURRENT);
447 mNotification.when = System.currentTimeMillis();
448 //mNotification.contentView.setTextViewText(R.id.status_text, content);
449 String ticker = String.format(getString(R.string.media_notif_ticker), getString(R.string.app_name));
450 mNotification.setLatestEventInfo(getApplicationContext(), ticker, content, mNotification.contentIntent);
451 mNotificationManager.notify(R.string.media_notif_ticker, mNotification);
452 }
453
454
455 /**
456 * Configures the service as a foreground service.
457 *
458 * The system will avoid finishing the service as much as possible when resources as low.
459 *
460 * A notification must be created to keep the user aware of the existance of the service.
461 */
462 @SuppressWarnings("deprecation")
463 private void setUpAsForeground(String content) {
464 /// creates status notification
465 // TODO put a progress bar to follow the playback progress
466 mNotification = new Notification();
467 mNotification.icon = android.R.drawable.ic_media_play;
468 //mNotification.tickerText = text;
469 mNotification.when = System.currentTimeMillis();
470 mNotification.flags |= Notification.FLAG_ONGOING_EVENT;
471 //mNotification.contentView.setTextViewText(R.id.status_text, "ownCloud Music Player"); // NULL POINTER
472 //mNotification.contentView.setTextViewText(R.id.status_text, getString(R.string.downloader_download_in_progress_content));
473
474
475 /// includes a pending intent in the notification showing the details view of the file
476 Intent showDetailsIntent = new Intent(this, FileDetailActivity.class);
477 showDetailsIntent.putExtra(FileDetailFragment.EXTRA_FILE, mFile);
478 showDetailsIntent.putExtra(FileDetailFragment.EXTRA_ACCOUNT, mAccount);
479 showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
480 mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(),
481 (int)System.currentTimeMillis(),
482 showDetailsIntent,
483 PendingIntent.FLAG_UPDATE_CURRENT);
484
485
486 //mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotification);
487 String ticker = String.format(getString(R.string.media_notif_ticker), getString(R.string.app_name));
488 mNotification.setLatestEventInfo(getApplicationContext(), ticker, content, mNotification.contentIntent);
489 startForeground(R.string.media_notif_ticker, mNotification);
490
491 }
492
493 /**
494 * Called when there's an error playing media.
495 *
496 * Warns the user about the error and resets the media player.
497 */
498 public boolean onError(MediaPlayer mp, int what, int extra) {
499 Log.e(TAG, "Error in audio playback, what = " + what + ", extra = " + extra);
500
501 if (mMediaController != null) {
502 mMediaController.hide();
503 }
504
505 int messageId;
506 if (what == OC_MEDIA_ERROR) {
507 messageId = extra;
508
509 } else if (what == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
510 messageId = R.string.media_err_invalid_progressive_playback;
511
512 } else {
513 // what == MediaPlayer.MEDIA_ERROR_UNKNOWN or MEDIA_ERROR_SERVER_DIED
514 messageId = R.string.media_err_unknown;
515
516 }
517 Toast.makeText(getApplicationContext(), messageId, Toast.LENGTH_SHORT).show();
518
519 processStopRequest(true);
520 return true;
521 }
522
523
524 /**
525 * Called by the system when another app tries to play some sound.
526 *
527 * {@inheritDoc}
528 */
529 @Override
530 public void onAudioFocusChange(int focusChange) {
531 if (focusChange > 0) {
532 // focus gain; check AudioManager.AUDIOFOCUS_* values
533 mAudioFocus = AudioFocus.FOCUS;
534 // restart media player with new focus settings
535 if (mState == State.PLAYING)
536 configAndStartMediaPlayer();
537
538 } else if (focusChange < 0) {
539 // focus loss; check AudioManager.AUDIOFOCUS_* values
540 boolean canDuck = AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK == focusChange;
541 mAudioFocus = canDuck ? AudioFocus.NO_FOCUS_CAN_DUCK : AudioFocus.NO_FOCUS;
542 // start/restart/pause media player with new focus settings
543 if (mPlayer != null && mPlayer.isPlaying())
544 configAndStartMediaPlayer();
545 }
546
547 }
548
549 /**
550 * Called when the service is finished for final clean-up.
551 *
552 * {@inheritDoc}
553 */
554 @Override
555 public void onDestroy() {
556 mState = State.STOPPED;
557 releaseResources(true);
558 giveUpAudioFocus();
559 }
560
561
562 /**
563 * Provides a binder object that clients can use to perform operations on the MediaPlayer managed by the MediaService.
564 */
565 @Override
566 public IBinder onBind(Intent arg) {
567 return mBinder;
568 }
569
570
571 /**
572 * Called when ALL the bound clients were onbound.
573 *
574 * The service is destroyed if playback stopped or paused
575 */
576 @Override
577 public boolean onUnbind(Intent intent) {
578 if (mState == State.PAUSED || mState == State.STOPPED) {
579 processStopRequest(false);
580 }
581 return false; // not accepting rebinding (default behaviour)
582 }
583
584
585 /**
586 * Accesses the current MediaPlayer instance in the service.
587 *
588 * To be handled carefully. Visibility is protected to be accessed only
589 *
590 * @return Current MediaPlayer instance handled by MediaService.
591 */
592 protected MediaPlayer getPlayer() {
593 return mPlayer;
594 }
595
596
597 /**
598 * Accesses the current OCFile loaded in the service.
599 *
600 * @return The current OCFile loaded in the service.
601 */
602 protected OCFile getCurrentFile() {
603 return mFile;
604 }
605
606
607 /**
608 * Accesses the current {@link State} of the MediaService.
609 *
610 * @return The current {@link State} of the MediaService.
611 */
612 protected State getState() {
613 return mState;
614 }
615
616
617 protected void setMediaContoller(MediaController mediaController) {
618 if (mMediaController != null) {
619 mMediaController.hide();
620 }
621 mMediaController = mediaController;
622
623 }
624
625 protected MediaController getMediaController() {
626 return mMediaController;
627 }
628
629 }