Apollo doesn't have an activity registered for support "file" schemas, that are used by file
managers when using the method setDataAndType of Intent class.
This changes adds a new activity (PlayExternal) that registers this kind of schema, and pass
the request to AudioPlayerHolder. Allow direct play the file, or enqueue in the current track list
Patch 2: Remove trailing white-spaces
Rename removeAllTrack to removeAllTracks
Change-Id: I25b728700bd0d2d58d31e4de4bed81ee3e368e0a
<category android:name="android.intent.category.DEFAULT" />\r
</intent-filter>\r
</activity>\r
+ <!-- Play External File -->\r
+ <activity\r
+ android:name=".activities.PlayExternal"\r
+ android:clearTaskOnLaunch="true"\r
+ android:excludeFromRecents="true"\r
+ android:noHistory="true"\r
+ android:launchMode="singleTask"\r
+ android:theme="@style/Theme.Light.Translucent"\r
+ android:label="@string/app_name" >\r
+ <intent-filter>\r
+ <action android:name="android.intent.action.VIEW" />\r
+\r
+ <category android:name="android.intent.category.DEFAULT" />\r
+\r
+ <data android:scheme="file" />\r
+ <data android:mimeType="audio/*" />\r
+ <data android:mimeType="application/ogg" />\r
+ <data android:mimeType="application/x-ogg" />\r
+ <data android:mimeType="application/itunes" />\r
+ </intent-filter>\r
+ </activity>\r
<!-- Track browser -->\r
<activity\r
android:name=".activities.TracksBrowser"\r
<string name="tab_playlists">PLAYLISTS</string>\r
<string name="tab_genres">GENRES</string>\r
\r
+ <!-- External file dialog -->\r
+ <string name="play_external_question_msg">Select the action to apply to: \n\n<b><xliff:g id="name">%s</xliff:g></b></string>\r
+ <string name="play_external_question_button_play">Play</string>\r
+ <string name="play_external_question_button_queue">Enqueue</string>\r
+ <string name="play_external_question_button_cancel">Cancel</string>\r
+ <string name="play_external_error">Failed to play file</string>\r
+\r
<!-- Something went wrong -->\r
<string name="error">Error</string>\r
\r
<?xml version="1.0" encoding="utf-8"?>\r
<resources>\r
\r
+ <!-- Translucent dialog (for @PlayExternal) -->\r
+ <style name="Theme.Light.Translucent" parent="android:style/Theme.Light.NoTitleBar">\r
+ <item name="android:windowIsTranslucent">true</item>\r
+ <item name="android:windowBackground">@android:color/transparent</item>\r
+ <item name="android:windowContentOverlay">@null</item>\r
+ <item name="android:windowNoTitle">true</item>\r
+ <item name="android:windowIsFloating">true</item>\r
+ <item name="android:backgroundDimEnabled">false</item>\r
+ </style>\r
+ <style name="Theme.Light.Translucent.Dialog" parent="@android:style/Theme.Holo.Light.Dialog">\r
+ <item name="android:windowBackground">@android:color/transparent</item>\r
+ <item name="android:windowContentOverlay">@null</item>\r
+ </style>\r
+\r
<!-- Custom tabs -->\r
<style name="Tabs">\r
<item name="android:layout_width">wrap_content</item>\r
{\r
void openFile(String path);\r
void open(in long [] list, int position);\r
+ long getIdFromPath(String path);\r
int getQueuePosition();\r
boolean isPlaying();\r
void stop();\r
\r
private static final int EFFECTS_PANEL = 0;\r
\r
+ private PagerAdapter mPagerAdapter;\r
+\r
@Override\r
protected void onCreate(Bundle icicle) {\r
// For the theme chooser and overflow MenuItem\r
}\r
\r
@Override\r
+ protected void onNewIntent(Intent intent) {\r
+ // If an activity is requesting access to this activity, and\r
+ // the activity is in the stack, the the fragments may need\r
+ // be refreshed. Update the page adapter\r
+ if (mPagerAdapter != null) {\r
+ mPagerAdapter.refresh();\r
+ }\r
+ super.onNewIntent(intent);\r
+ }\r
+\r
+ @Override\r
public void onServiceConnected(ComponentName name, IBinder obj) {\r
MusicUtils.mService = IApolloService.Stub.asInterface(obj);\r
}\r
*/\r
public void initPager() {\r
// Initiate PagerAdapter\r
- PagerAdapter mPagerAdapter = new PagerAdapter(getSupportFragmentManager());\r
+ mPagerAdapter = new PagerAdapter(getSupportFragmentManager());\r
Bundle bundle = new Bundle();\r
bundle.putString(MIME_TYPE, Audio.Playlists.CONTENT_TYPE);\r
bundle.putLong(BaseColumns._ID, PLAYLIST_QUEUE);\r
--- /dev/null
+/*
+ * Copyright (C) 2012 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.andrew.apollo.activities;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ComponentName;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.support.v4.app.FragmentActivity;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.andrew.apollo.IApolloService;
+import com.andrew.apollo.R;
+import com.andrew.apollo.service.ServiceToken;
+import com.andrew.apollo.utils.MusicUtils;
+
+import java.io.File;
+import java.net.URLDecoder;
+
+/**
+ * An activity that lets external browsers launching music inside Apollo
+ */
+public class PlayExternal extends FragmentActivity
+ implements ServiceConnection, DialogInterface.OnCancelListener {
+
+ private static final String TAG = "PlayExternal";
+
+ private ServiceToken mToken;
+ private Uri mUri;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Get the external file to play
+ Intent intent = getIntent();
+ if (intent == null) {
+ finish();
+ return;
+ }
+ mUri = intent.getData();
+ if (mUri == null) {
+ finish();
+ return;
+ }
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder obj) {
+ MusicUtils.mService = IApolloService.Stub.asInterface(obj);
+ play(this.mUri);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ MusicUtils.mService = null;
+ }
+
+ @Override
+ protected void onStart() {
+ // Bind to Service
+ mToken = MusicUtils.bindToService(this, this);
+ super.onStart();
+ }
+
+ @Override
+ protected void onStop() {
+ // Unbind
+ if (MusicUtils.mService != null)
+ MusicUtils.unbindFromService(mToken);
+ super.onStop();
+ }
+
+ private void play(Uri uri) {
+ try {
+ final String file = URLDecoder.decode( uri.toString(), "UTF-8");
+ final String name = new File(file).getName();
+
+ // Try to resolve the file to a media id
+ final long id = MusicUtils.mService.getIdFromPath(file);
+ if( id == -1 ) {
+ // Open the stream, But we will not have album information
+ openFile(file);
+ }
+ else {
+ // Show a dialog asking the user for play or queue the song
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(this, R.style.Theme_Light_Translucent_Dialog);
+ builder.setTitle(R.string.app_name);
+ builder.setMessage(getString(R.string.play_external_question_msg, name));
+
+ DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ try {
+ switch (which) {
+ case DialogInterface.BUTTON_POSITIVE:
+ playOrEnqueuFile(file, id, false);
+ break;
+
+ case DialogInterface.BUTTON_NEUTRAL:
+ playOrEnqueuFile(file, id, true);
+ break;
+
+ case DialogInterface.BUTTON_NEGATIVE:
+ break;
+
+ default:
+ break;
+ }
+ } finally {
+ finish();
+ }
+ }
+ };
+ builder.setPositiveButton(R.string.play_external_question_button_play, listener);
+ builder.setNeutralButton(R.string.play_external_question_button_queue, listener);
+ builder.setNegativeButton(R.string.play_external_question_button_cancel, listener);
+
+ Dialog dialog = builder.create();
+ dialog.setOnCancelListener(this);
+ dialog.show();
+ }
+
+ } catch (Exception e) {
+ Toast.makeText(
+ getApplicationContext(),
+ R.string.play_external_error,
+ Toast.LENGTH_SHORT);
+ Log.e(TAG, String.format("Failed to play external file: ", uri.toString()), e);
+ try {
+ Thread.sleep(1000L);
+ }catch (Exception e2) {}
+ finish();
+ }
+
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ finish();
+ }
+
+ private void playOrEnqueuFile(String file, long id, boolean enqueue) {
+ final long[] list = new long[] {id};
+ if (!enqueue) {
+ //Remove the actual queue
+ MusicUtils.removeAllTracks();
+ MusicUtils.playAll(getApplicationContext(), list, 0);
+ }
+ else {
+ MusicUtils.addToCurrentPlaylist(getApplicationContext(), list);
+ }
+
+ // Show now playing
+ Intent intent = new Intent(this, AudioPlayerHolder.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ }
+
+ private void openFile(String file) throws RemoteException {
+ // Stop, load and play
+ MusicUtils.mService.stop();
+ MusicUtils.mService.openFile(file);
+ MusicUtils.mService.play();
+
+ // Show now playing
+ Intent nowPlayingIntent = new Intent(this, AudioPlayerHolder.class);
+ startActivity(nowPlayingIntent);
+ }
+
+}
import android.support.v4.app.FragmentManager;\r
import android.support.v4.app.FragmentPagerAdapter;\r
\r
+import com.andrew.apollo.utils.RefreshableFragment;\r
+\r
/**\r
* @author Andrew Neal\r
*/\r
return mFragments.get(position);\r
}\r
\r
+ /**\r
+ * This method update the fragments that extends the {@link RefreshableFragment} class\r
+ */\r
+ public void refresh() {\r
+ for (int i = 0; i < mFragments.size(); i++) {\r
+ if( mFragments.get(i) instanceof RefreshableFragment ) {\r
+ ((RefreshableFragment)mFragments.get(i)).refresh();\r
+ }\r
+ }\r
+ }\r
+\r
}\r
import android.provider.MediaStore.Audio.Genres;\r
import android.provider.MediaStore.Audio.Playlists;\r
import android.provider.MediaStore.MediaColumns;\r
-import android.support.v4.app.Fragment;\r
import android.support.v4.app.LoaderManager.LoaderCallbacks;\r
import android.support.v4.content.CursorLoader;\r
import android.support.v4.content.Loader;\r
import com.andrew.apollo.service.ApolloService;\r
import com.andrew.apollo.utils.ApolloUtils;\r
import com.andrew.apollo.utils.MusicUtils;\r
+import com.andrew.apollo.utils.RefreshableFragment;\r
\r
import static com.andrew.apollo.Constants.EXTERNAL;\r
import static com.andrew.apollo.Constants.INTENT_ADD_TO_PLAYLIST;\r
/**\r
* @author Andrew Neal\r
*/\r
-public class TracksFragment extends Fragment implements LoaderCallbacks<Cursor>,\r
+public class TracksFragment extends RefreshableFragment implements LoaderCallbacks<Cursor>,\r
OnItemClickListener {\r
\r
// Adapter\r
}\r
\r
@Override\r
+ public void refresh() {\r
+ // The data need to be refreshed\r
+ if( mListView != null ) {\r
+ getLoaderManager().restartLoader(0, null, this);\r
+ }\r
+ }\r
+\r
+ @Override\r
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {\r
View root = inflater.inflate(R.layout.listview, container, false);\r
mListView = (ListView)root.findViewById(android.R.id.list);\r
mAlbumIndex = data.getColumnIndexOrThrow(AudioColumns.ALBUM);\r
}\r
mTrackAdapter.changeCursor(data);\r
+ mListView.invalidateViews();\r
mCursor = data;\r
}\r
\r
where = null;\r
selectionArgs = null;\r
} else {\r
+ // Remove schema for search in the database\r
+ // Otherwise the file will not found\r
+ String data = path;\r
+ if( data.startsWith("file://") ){\r
+ data = data.substring(7);\r
+ }\r
uri = MediaStore.Audio.Media.getContentUriForPath(path);\r
where = MediaColumns.DATA + "=?";\r
selectionArgs = new String[] {\r
- path\r
+ data\r
};\r
}\r
\r
}\r
\r
/**\r
+ * Method that query the media database for search a path an translate\r
+ * to the internal media id\r
+ *\r
+ * @param path The path to search\r
+ * @return long The id of the resource, or -1 if not found\r
+ */\r
+ public long getIdFromPath(String path) {\r
+ try {\r
+ // Remove schema for search in the database\r
+ // Otherwise the file will not found\r
+ String data = path;\r
+ if( data.startsWith("file://") ){\r
+ data = data.substring(7);\r
+ }\r
+ ContentResolver resolver = getContentResolver();\r
+ String where = MediaColumns.DATA + "=?";\r
+ String selectionArgs[] = new String[] {\r
+ data\r
+ };\r
+ Cursor cursor =\r
+ resolver.query(\r
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,\r
+ mCursorCols, where, selectionArgs, null);\r
+ try {\r
+ if (cursor == null || cursor.getCount() == 0) {\r
+ return -1;\r
+ }\r
+ cursor.moveToNext();\r
+ return cursor.getLong(IDCOLIDX);\r
+ } finally {\r
+ try {\r
+ if( cursor != null )\r
+ cursor.close();\r
+ } catch (Exception ex) {\r
+ }\r
+ }\r
+ } catch (UnsupportedOperationException ex) {\r
+ }\r
+ return -1;\r
+ }\r
+\r
+ /**\r
* Starts playback of a previously opened file.\r
*/\r
public void play() {\r
public String getArtistName() {\r
synchronized (this) {\r
if (mCursor == null) {\r
- return null;\r
+ return getString(R.string.unknown);\r
}\r
return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST));\r
}\r
public String getAlbumName() {\r
synchronized (this) {\r
if (mCursor == null) {\r
- return null;\r
+ return getString(R.string.unknown);\r
}\r
return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM));\r
}\r
public String getTrackName() {\r
synchronized (this) {\r
if (mCursor == null) {\r
- return null;\r
+ return getString(R.string.unknown);\r
}\r
return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaColumns.TITLE));\r
}\r
}\r
\r
@Override\r
+ public long getIdFromPath(String path) {\r
+ return mService.get().getIdFromPath(path);\r
+ }\r
+\r
+ @Override\r
public int getQueuePosition() {\r
return mService.get().getQueuePosition();\r
}\r
}\r
\r
/**\r
+ * Method that removes all tracks from the current queue\r
+ */\r
+ public static void removeAllTracks() {\r
+ try {\r
+ if (mService == null) {\r
+ long[] current = MusicUtils.getQueue();\r
+ if (current != null) {\r
+ mService.removeTracks(0, current.length-1);\r
+ }\r
+ }\r
+ } catch (RemoteException e) {\r
+ }\r
+ }\r
+\r
+ /**\r
* @param id\r
* @return removes track from a playlist\r
*/\r
--- /dev/null
+/*\r
+ * Copyright (C) 2012 The CyanogenMod Project\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ * http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package com.andrew.apollo.utils;\r
+\r
+\r
+import android.support.v4.app.Fragment;\r
+\r
+/**\r
+ * An abstract class that defines a {@link Fragment} like refreshable\r
+ */\r
+public abstract class RefreshableFragment extends Fragment {\r
+\r
+ /**\r
+ * Method invoked when the fragment need to be refreshed\r
+ */\r
+ public abstract void refresh();\r
+}\r