From 18bf35a8099ce585ef30767a9f1f7e595c866b45 Mon Sep 17 00:00:00 2001 From: "David A. Velasco" Date: Wed, 6 Feb 2013 14:14:55 +0100 Subject: [PATCH] Added MediaController user interface to handle audio files in the MediaService --- res/layout/audio_player.xml | 29 +++ res/layout/file_details_fragment.xml | 2 +- .../owncloud/android/datamodel/OCFile.java | 4 + .../owncloud/android/media/MediaService.java | 75 ++++++-- .../android/media/MediaServiceBinder.java | 174 ++++++++++++++++++ .../ui/activity/FileDisplayActivity.java | 2 +- .../ui/fragment/FileDetailFragment.java | 149 +++++++++++++-- 7 files changed, 408 insertions(+), 27 deletions(-) create mode 100644 res/layout/audio_player.xml create mode 100644 src/com/owncloud/android/media/MediaServiceBinder.java diff --git a/res/layout/audio_player.xml b/res/layout/audio_player.xml new file mode 100644 index 0000000000..bee0f87098 --- /dev/null +++ b/res/layout/audio_player.xml @@ -0,0 +1,29 @@ + + + + + + \ No newline at end of file diff --git a/res/layout/file_details_fragment.xml b/res/layout/file_details_fragment.xml index 3903677833..cee45a9ebf 100644 --- a/res/layout/file_details_fragment.xml +++ b/res/layout/file_details_fragment.xml @@ -57,7 +57,7 @@ android:id="@+id/fdDetailsContainer" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_below="@+id/fdFileHeaderContainer" > + android:layout_below="@id/fdFileHeaderContainer" > { return 0; } + public boolean isAudio() { + return (mMimeType.startsWith("audio/")); + } + } diff --git a/src/com/owncloud/android/media/MediaService.java b/src/com/owncloud/android/media/MediaService.java index 8020f12116..cccbaa70f8 100644 --- a/src/com/owncloud/android/media/MediaService.java +++ b/src/com/owncloud/android/media/MediaService.java @@ -37,10 +37,8 @@ import android.net.wifi.WifiManager.WifiLock; import android.os.IBinder; import android.os.PowerManager; import android.util.Log; -import android.widget.RemoteViews; import android.widget.Toast; -import java.io.File; import java.io.IOException; import com.owncloud.android.AccountUtils; @@ -156,6 +154,8 @@ public class MediaService extends Service implements OnCompletionListener, OnPre private OCFile mFile; private Account mAccount; + + private IBinder mBinder; @@ -173,7 +173,7 @@ public class MediaService extends Service implements OnCompletionListener, OnPre mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE); - + mBinder = new MediaServiceBinder(this); } @@ -206,6 +206,8 @@ public class MediaService extends Service implements OnCompletionListener, OnPre /** * Processes a request to play a media file received as a parameter * + * TODO If a new request is received when a file is being prepared, it is ignored. Is this what we want? + * * @param intent Intent received in the request with the data to identify the file to play. */ private void processPlayFileRequest(Intent intent) { @@ -215,14 +217,13 @@ public class MediaService extends Service implements OnCompletionListener, OnPre tryToGetAudioFocus(); playMedia(); } - // TODO think what happens if mState == State.PREPARING } /** * Processes a request to play a media file. */ - void processPlayRequest() { + protected void processPlayRequest() { // request audio focus tryToGetAudioFocus(); @@ -245,7 +246,7 @@ public class MediaService extends Service implements OnCompletionListener, OnPre * Makes sure the media player exists and has been reset. This will create the media player * if needed, or reset the existing media player if one already exists. */ - void createMediaPlayerIfNeeded() { + protected void createMediaPlayerIfNeeded() { if (mPlayer == null) { mPlayer = new MediaPlayer(); @@ -277,7 +278,7 @@ public class MediaService extends Service implements OnCompletionListener, OnPre /** * Processes a request to pause the current playback */ - private void processPauseRequest() { + protected void processPauseRequest() { if (mState == State.PLAYING) { mState = State.PAUSED; mPlayer.pause(); @@ -310,9 +311,12 @@ public class MediaService extends Service implements OnCompletionListener, OnPre * @param force When 'true', the playback is stopped no matter the value of mState */ void processStopRequest(boolean force) { - if (mState == State.PLAYING || mState == State.PAUSED || force) { + if (mState == State.PLAYING || mState == State.PAUSED || mState == State.STOPPED || force) { mState = State.STOPPED; + mFile = null; + mAccount = null; + releaseResources(true); giveUpAudioFocus(); stopSelf(); // service is no longer necessary @@ -645,10 +649,59 @@ public class MediaService extends Service implements OnCompletionListener, OnPre } + /** + * Provides a binder object that clients can use to perform operations on the MediaPlayer managed by the MediaService. + */ @Override - public IBinder onBind(Intent arg0) { - // TODO provide a binding API? may we use a service to play VIDEO? - return null; + public IBinder onBind(Intent arg) { + return mBinder; + } + + + /** + * Called when ALL the bound clients were onbound. + * + * The service is destroyed if playback stopped or paused + */ + @Override + public boolean onUnbind(Intent intent) { + if (mState == State.PAUSED || mState == State.STOPPED) { + Log.d(TAG, "Stopping service due to unbind in pause"); + processStopRequest(false); + } + return false; // not accepting rebinding (default behaviour) + } + + + /** + * Accesses the current MediaPlayer instance in the service. + * + * To be handled carefully. Visibility is protected to be accessed only + * + * @return Current MediaPlayer instance handled by MediaService. + */ + protected MediaPlayer getPlayer() { + return mPlayer; + } + + + /** + * Accesses the current OCFile loaded in the service. + * + * @return The current OCFile loaded in the service. + */ + protected OCFile getCurrentFile() { + return mFile; + } + + + /** + * Accesses the current {@link State} of the MediaService. + * + * @return The current {@link State} of the MediaService. + */ + public State getState() { + return mState; } } diff --git a/src/com/owncloud/android/media/MediaServiceBinder.java b/src/com/owncloud/android/media/MediaServiceBinder.java new file mode 100644 index 0000000000..98b506c44c --- /dev/null +++ b/src/com/owncloud/android/media/MediaServiceBinder.java @@ -0,0 +1,174 @@ +/* ownCloud Android client application + * Copyright (C) 2013 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.owncloud.android.media; + + +import com.owncloud.android.datamodel.OCFile; +import com.owncloud.android.media.MediaService.State; + +import android.accounts.Account; +import android.content.Intent; +import android.media.MediaPlayer; +import android.os.Binder; +import android.util.Log; +import android.widget.MediaController; + + +/** + * Binder allowing client components to perform operations on on the MediaPlayer managed by a MediaService instance. + * + * Provides the operations of {@link MediaController.MediaPlayerControl}, and an extra method to check if + * an {@link OCFile} instance is handled by the MediaService. + * + * @author David A. Velasco + */ +public class MediaServiceBinder extends Binder implements MediaController.MediaPlayerControl { + + private static final String TAG = MediaServiceBinder.class.getSimpleName(); + /** + * {@link MediaService} instance to access with the binder + */ + private MediaService mService = null; + + /** + * Public constructor + * + * @param service A {@link MediaService} instance to access with the binder + */ + public MediaServiceBinder(MediaService service) { + if (service == null) { + throw new IllegalArgumentException("Argument 'service' can not be null"); + } + mService = service; + } + + + public boolean isPlaying(OCFile mFile) { + return (mFile != null && mFile.equals(mService.getCurrentFile())); + } + + + @Override + public boolean canPause() { + //Log.e(TAG, TAG + " - canPause -> true"); + return true; + } + + @Override + public boolean canSeekBackward() { + //Log.e(TAG, TAG + " - canSeekBackward -> true"); + return true; + } + + @Override + public boolean canSeekForward() { + //Log.e(TAG, TAG + " - canSeekForward -> true"); + return true; + } + + @Override + public int getBufferPercentage() { + MediaPlayer currentPlayer = mService.getPlayer(); + if (currentPlayer != null) { + //Log.e(TAG, TAG + " - getBufferPercentage -> 100"); + return 100; + // TODO update for streamed playback; add OnBufferUpdateListener in MediaService + } else { + //Log.e(TAG, TAG + " - getBufferPercentage -> 0"); + return 0; + } + } + + @Override + public int getCurrentPosition() { + MediaPlayer currentPlayer = mService.getPlayer(); + if (currentPlayer != null) { + int pos = currentPlayer.getCurrentPosition(); + //Log.e(TAG, TAG + " - getCurrentPosition -> " + pos); + return pos; + } else { + //Log.e(TAG, TAG + " - getCurrentPosition -> 0"); + return 0; + } + } + + @Override + public int getDuration() { + MediaPlayer currentPlayer = mService.getPlayer(); + if (currentPlayer != null) { + int dur = currentPlayer.getDuration(); + //Log.e(TAG, TAG + " - getDuration -> " + dur); + return dur; + } else { + //Log.e(TAG, TAG + " - getDuration -> 0"); + return 0; + } + } + + + /** + * Reports if the MediaService is playing a file or not. + * + * Considers that the file is being played when it is in preparation because the expected + * client of this method is a {@link MediaController} , and we do not want that the 'play' + * button is shown when the file is being prepared by the MediaService. + */ + @Override + public boolean isPlaying() { + MediaService.State currentState = mService.getState(); + //Log.e(TAG, TAG + " - isPlaying -> " + (currentState == State.PLAYING || currentState == State.PREPARING)); + return (currentState == State.PLAYING || currentState == State.PREPARING); + } + + + @Override + public void pause() { + Log.d(TAG, "Pausing through binder..."); + mService.processPauseRequest(); + } + + @Override + public void seekTo(int pos) { + Log.d(TAG, "Seeking " + pos + " through binder..."); + MediaPlayer currentPlayer = mService.getPlayer(); + MediaService.State currentState = mService.getState(); + if (currentPlayer != null && currentState != State.PREPARING && currentState != State.STOPPED) { + currentPlayer.seekTo(pos); + } + } + + @Override + public void start() { + Log.d(TAG, "Starting through binder..."); + mService.processPlayRequest(); // this will finish the service if there is no file preloaded to play + } + + + public void start(Account account, OCFile file) { + Log.d(TAG, "Loading and starting through binder..."); + Intent i = new Intent(mService, MediaService.class); + i.putExtra(MediaService.EXTRA_ACCOUNT, account); + i.putExtra(MediaService.EXTRA_FILE, file); + i.setAction(MediaService.ACTION_PLAY_FILE); + mService.startService(i); + } + +} + + diff --git a/src/com/owncloud/android/ui/activity/FileDisplayActivity.java b/src/com/owncloud/android/ui/activity/FileDisplayActivity.java index cfd9ae5efc..8eafbf7978 100644 --- a/src/com/owncloud/android/ui/activity/FileDisplayActivity.java +++ b/src/com/owncloud/android/ui/activity/FileDisplayActivity.java @@ -1015,7 +1015,7 @@ public class FileDisplayActivity extends SherlockFragmentActivity implements */ @Override public void onFileClick(OCFile file) { - + // If we are on a large device -> update fragment if (mDualPane) { // buttons in the details view are problematic when trying to reuse an existing fragment; create always a new one solves some of them, BUT no all; downloads are 'dangerous' diff --git a/src/com/owncloud/android/ui/fragment/FileDetailFragment.java b/src/com/owncloud/android/ui/fragment/FileDetailFragment.java index 4d6fb483fa..0d07c44823 100644 --- a/src/com/owncloud/android/ui/fragment/FileDetailFragment.java +++ b/src/com/owncloud/android/ui/fragment/FileDetailFragment.java @@ -28,6 +28,7 @@ import org.apache.commons.httpclient.params.HttpConnectionManagerParams; import org.apache.http.HttpStatus; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.entity.FileEntity; import org.apache.http.message.BasicNameValuePair; import org.apache.http.protocol.HTTP; import org.apache.jackrabbit.webdav.client.methods.PropFindMethod; @@ -39,9 +40,11 @@ import android.annotation.SuppressLint; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.ServiceConnection; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapFactory.Options; @@ -50,18 +53,22 @@ import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; +import android.os.IBinder; import android.support.v4.app.DialogFragment; import android.support.v4.app.FragmentTransaction; import android.util.Log; import android.view.Display; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; +import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.webkit.MimeTypeMap; import android.widget.Button; import android.widget.CheckBox; import android.widget.ImageView; +import android.widget.MediaController; import android.widget.TextView; import android.widget.Toast; @@ -77,6 +84,7 @@ import com.owncloud.android.files.services.FileUploader; import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder; import com.owncloud.android.files.services.FileUploader.FileUploaderBinder; import com.owncloud.android.media.MediaService; +import com.owncloud.android.media.MediaServiceBinder; import com.owncloud.android.network.OwnCloudClientUtils; import com.owncloud.android.operations.OnRemoteOperationListener; import com.owncloud.android.operations.RemoteOperation; @@ -104,7 +112,8 @@ import eu.alefzero.webdav.WebdavUtils; * */ public class FileDetailFragment extends SherlockFragment implements - OnClickListener, ConfirmationDialogFragment.ConfirmationDialogFragmentListener, OnRemoteOperationListener, EditNameDialogListener { + OnClickListener, OnTouchListener, + ConfirmationDialogFragment.ConfirmationDialogFragmentListener, OnRemoteOperationListener, EditNameDialogListener { public static final String EXTRA_FILE = "FILE"; public static final String EXTRA_ACCOUNT = "ACCOUNT"; @@ -124,6 +133,9 @@ public class FileDetailFragment extends SherlockFragment implements private Handler mHandler; private RemoteOperation mLastRemoteOperation; private DialogFragment mCurrentDialog; + private MediaServiceBinder mMediaServiceBinder = null; + private MediaController mMediaController = null; + private MediaServiceConnection mMediaServiceConnection = null; private static final String TAG = FileDetailFragment.class.getSimpleName(); public static final String FTAG = "FileDetails"; @@ -192,6 +204,7 @@ public class FileDetailFragment extends SherlockFragment implements mView.findViewById(R.id.fdRemoveBtn).setOnClickListener(this); //mView.findViewById(R.id.fdShareBtn).setOnClickListener(this); mPreview = (ImageView)mView.findViewById(R.id.fdPreview); + mPreview.setOnTouchListener(this); } updateFileDetails(false); @@ -234,6 +247,13 @@ public class FileDetailFragment extends SherlockFragment implements Log.i(getClass().toString(), "onSaveInstanceState() end"); } + @Override + public void onStart() { + super.onStart(); + if (mFile != null && mFile.isAudio()) { + bindMediaService(); + } + } @Override public void onResume() { @@ -247,10 +267,15 @@ public class FileDetailFragment extends SherlockFragment implements mUploadFinishReceiver = new UploadFinishReceiver(); filter = new IntentFilter(FileUploader.UPLOAD_FINISH_MESSAGE); getActivity().registerReceiver(mUploadFinishReceiver, filter); + + mPreview = (ImageView)mView.findViewById(R.id.fdPreview); // this is here just because it is nullified in onPause() - mPreview = (ImageView)mView.findViewById(R.id.fdPreview); + if (mMediaController != null) { + mMediaController.show(); + } } + @Override public void onPause() { super.onPause(); @@ -261,18 +286,34 @@ public class FileDetailFragment extends SherlockFragment implements getActivity().unregisterReceiver(mUploadFinishReceiver); mUploadFinishReceiver = null; - if (mPreview != null) { + if (mPreview != null) { // why? mPreview = null; } + + if (mMediaController != null) { + mMediaController.hide(); + } } + + @Override + public void onStop() { + super.onStop(); + if (mMediaServiceConnection != null) { + Log.d(TAG, "Unbinding from MediaService ..."); + getActivity().unbindService(mMediaServiceConnection); + mMediaServiceBinder = null; + mMediaController = null; + } + } + + @Override public View getView() { return super.getView() == null ? mView : super.getView(); } - @Override public void onClick(View v) { switch (v.getId()) { @@ -374,18 +415,84 @@ public class FileDetailFragment extends SherlockFragment implements }*/ } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (v == mPreview && event.getAction() == MotionEvent.ACTION_DOWN && mFile != null && mFile.isDown() && mFile.isAudio()) { + if (!mMediaServiceBinder.isPlaying(mFile)) { + Log.d(TAG, "starting playback of " + mFile.getStoragePath()); + mMediaServiceBinder.start(mAccount, mFile); + // this is a patch; need to synchronize this with the onPrepared() coming from MediaPlayer in the MediaService + mMediaController.postDelayed(new Runnable() { + @Override + public void run() { + mMediaController.show(0); + } + } , 300); + } else { + mMediaController.show(0); + } + } + return false; + } + + + private void bindMediaService() { + Log.d(TAG, "Binding to MediaService..."); + if (mMediaServiceConnection == null) { + mMediaServiceConnection = new MediaServiceConnection(); + } + getActivity().bindService( new Intent(getActivity(), + MediaService.class), + mMediaServiceConnection, + Context.BIND_AUTO_CREATE); + } + + /** Defines callbacks for service binding, passed to bindService() */ + private class MediaServiceConnection implements ServiceConnection { + + @Override + public void onServiceConnected(ComponentName component, IBinder service) { + if (component.equals(new ComponentName(getActivity(), MediaService.class))) { + Log.d(TAG, "Media service connected"); + mMediaServiceBinder = (MediaServiceBinder) service; + if (mMediaServiceBinder != null) { + if (mMediaController == null) { + mMediaController = new MediaController(getSherlockActivity()); + } + mMediaController.setMediaPlayer(mMediaServiceBinder); + mMediaController.setAnchorView(mPreview); + mMediaController.setEnabled(true); + + Log.d(TAG, "Successfully bound to MediaService, MediaController ready"); + + } else { + Log.e(TAG, "Unexpected response from MediaService while binding"); + } + } + } + + @Override + public void onServiceDisconnected(ComponentName component) { + if (component.equals(new ComponentName(getActivity(), MediaService.class))) { + Log.d(TAG, "Media service suddenly disconnected"); + if (mMediaController != null) { + mMediaController.hide(); + mMediaController.setMediaPlayer(null); // TODO check this is not an error + mMediaController = null; + } + mMediaServiceBinder = null; + mMediaServiceConnection = null; + } + } + } + + /** * Opens mFile. */ private void openFile() { - Intent i = new Intent(getActivity(), MediaService.class); - i.putExtra(MediaService.EXTRA_ACCOUNT, mAccount); - i.putExtra(MediaService.EXTRA_FILE, mFile); - i.setAction(MediaService.ACTION_PLAY_FILE); - getActivity().startService(i); - - /* String storagePath = mFile.getStoragePath(); String encodedStoragePath = WebdavUtils.encodePath(storagePath); try { @@ -406,7 +513,7 @@ public class FileDetailFragment extends SherlockFragment implements i.setDataAndType(Uri.parse("file://"+ encodedStoragePath), mimeType); } else { // desperate try - i.setDataAndType(Uri.parse("file://"+ encodedStoragePath), "*-/*"); + i.setDataAndType(Uri.parse("file://"+ encodedStoragePath), "*/*"); } i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); startActivity(i); @@ -428,7 +535,7 @@ public class FileDetailFragment extends SherlockFragment implements } } - }*/ + } } @@ -512,6 +619,8 @@ public class FileDetailFragment extends SherlockFragment implements * * TODO Remove parameter when the transferring state of files is kept in database. * + * TODO REFACTORING! this method called 5 times before every time the fragment is shown! + * * @param transferring Flag signaling if the file should be considered as downloading or uploading, * although {@link FileDownloaderBinder#isDownloading(Account, OCFile)} and * {@link FileUploaderBinder#isUploading(Account, OCFile)} return false. @@ -519,7 +628,7 @@ public class FileDetailFragment extends SherlockFragment implements */ public void updateFileDetails(boolean transferring) { - if (mFile != null && mAccount != null && mLayout == R.layout.file_details_fragment) { + if (readyToShow()) { // set file details setFilename(mFile.getFileName()); @@ -559,6 +668,17 @@ public class FileDetailFragment extends SherlockFragment implements } + /** + * Checks if the fragment is ready to show details of a OCFile + * + * @return 'True' when the fragment is ready to show details of a file + */ + private boolean readyToShow() { + return (mFile != null && mAccount != null && mLayout == R.layout.file_details_fragment); + } + + + /** * Updates the filename in view * @param filename to set @@ -1074,4 +1194,5 @@ public class FileDetailFragment extends SherlockFragment implements } } + }