diff --git a/res/drawable-hdpi/ic_launcher_folder.png b/res/drawable-hdpi/ic_launcher_folder.png
new file mode 100644
index 00000000..8d56e571
Binary files /dev/null and b/res/drawable-hdpi/ic_launcher_folder.png differ
diff --git a/res/drawable-hdpi/ic_tab_files_selected.png b/res/drawable-hdpi/ic_tab_files_selected.png
new file mode 100644
index 00000000..1032437e
Binary files /dev/null and b/res/drawable-hdpi/ic_tab_files_selected.png differ
diff --git a/res/drawable-hdpi/ic_tab_files_unselected.png b/res/drawable-hdpi/ic_tab_files_unselected.png
new file mode 100644
index 00000000..edb56e4a
Binary files /dev/null and b/res/drawable-hdpi/ic_tab_files_unselected.png differ
diff --git a/res/drawable-mdpi/ic_launcher_folder.png b/res/drawable-mdpi/ic_launcher_folder.png
new file mode 100644
index 00000000..917d3bff
Binary files /dev/null and b/res/drawable-mdpi/ic_launcher_folder.png differ
diff --git a/res/drawable-mdpi/ic_tab_files_selected.png b/res/drawable-mdpi/ic_tab_files_selected.png
new file mode 100644
index 00000000..c92ec219
Binary files /dev/null and b/res/drawable-mdpi/ic_tab_files_selected.png differ
diff --git a/res/drawable-mdpi/ic_tab_files_unselected.png b/res/drawable-mdpi/ic_tab_files_unselected.png
new file mode 100644
index 00000000..8446b240
Binary files /dev/null and b/res/drawable-mdpi/ic_tab_files_unselected.png differ
diff --git a/res/drawable/ic_tab_files.xml b/res/drawable/ic_tab_files.xml
new file mode 100644
index 00000000..26ab4370
--- /dev/null
+++ b/res/drawable/ic_tab_files.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/res/layout/library_content.xml b/res/layout/library_content.xml
index 106a5fad..a2d7b6d6 100644
--- a/res/layout/library_content.xml
+++ b/res/layout/library_content.xml
@@ -65,12 +65,21 @@ THE SOFTWARE.
android:visibility="gone"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
+
-
+ android:layout_gravity="bottom|left">
+
+
1 låt lagt til spilleliste %2$s.
- %1$d låter lagt til spilleliste %2$s.
- Spilleliste %s slettet.
- 1 låt slettet.
- %d låter slettet.
diff --git a/res/values-sk/translatable.xml b/res/values-sk/translatable.xml
index 7dc06b78..0853a6ab 100644
--- a/res/values-sk/translatable.xml
+++ b/res/values-sk/translatable.xml
@@ -65,7 +65,6 @@ THE SOFTWARE.
- %1$d skladby pridané do zoznamu skladieb %2$s.
- %1$d skladieb pridaných do zoznamu skladieb %2$s.
- Zoznam skladieb %s odstránený.
- 1 skladba odstránená.
- %d skladby odstránené.
diff --git a/res/values/translatable.xml b/res/values/translatable.xml
index f71d1c3b..700caf79 100644
--- a/res/values/translatable.xml
+++ b/res/values/translatable.xml
@@ -74,17 +74,19 @@ THE SOFTWARE.
- 1 song added to playlist %2$s.
- %1$d songs added to playlist %2$s.
- Playlist %s deleted.
+ %s deleted.
- 1 song deleted.
- %d songs deleted.
+ Failed to delete %s.
Artists
Albums
Songs
Playlists
Genres
+ Files
None
Unknown
diff --git a/src/org/kreed/vanilla/FileSystemAdapter.java b/src/org/kreed/vanilla/FileSystemAdapter.java
new file mode 100644
index 00000000..38c30c2c
--- /dev/null
+++ b/src/org/kreed/vanilla/FileSystemAdapter.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2010, 2011 Christopher Eby
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package org.kreed.vanilla;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Environment;
+import android.os.FileObserver;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import java.io.File;
+import java.io.FilenameFilter;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.regex.Pattern;
+
+/**
+ * A list adapter that provides a view of the filesystem. The active directory
+ * is set through a {@link Limiter} and rows are displayed using MediaViews.
+ */
+public class FileSystemAdapter extends BaseAdapter implements LibraryAdapter {
+ private static final Pattern SPACE_SPLIT = Pattern.compile("\\s+");
+ private static final Pattern FILE_SEPARATOR = Pattern.compile(File.separator);
+
+ /**
+ * The owner LibraryActivity.
+ */
+ final LibraryActivity mActivity;
+ /**
+ * The currently active limiter, set by a row expander being clicked.
+ */
+ private Limiter mLimiter;
+ /**
+ * The files and folders in the current directory.
+ */
+ private File[] mFiles;
+ /**
+ * The folder icon shown for folder rows.
+ */
+ private final Bitmap mFolderIcon;
+ /**
+ * The currently active filter, entered by the user from the search box.
+ */
+ String[] mFilter;
+ /**
+ * Excludes dot files and files not matching mFilter.
+ */
+ private final FilenameFilter mFileFilter = new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String filename)
+ {
+ if (filename.charAt(0) == '.')
+ return false;
+ if (mFilter != null) {
+ filename = filename.toLowerCase();
+ for (String term : mFilter) {
+ if (!filename.contains(term))
+ return false;
+ }
+ }
+ return true;
+ }
+ };
+ /**
+ * Sorts folders before files first, then sorts alphabetically by name.
+ */
+ private final Comparator mFileComparator = new Comparator() {
+ @Override
+ public int compare(File a, File b)
+ {
+ boolean aIsFolder = a.isDirectory();
+ boolean bIsFolder = b.isDirectory();
+ if (bIsFolder == aIsFolder) {
+ return a.getName().compareToIgnoreCase(b.getName());
+ } else if (bIsFolder) {
+ return 1;
+ }
+ return -1;
+ }
+ };
+ /**
+ * The Observer instance for the current directory.
+ */
+ private Observer mFileObserver;
+
+ /**
+ * Create a FileSystemAdapter.
+ *
+ * @param activity The LibraryActivity that will contain this adapter.
+ * Called on to requery this adapter when the contents of the directory
+ * change.
+ * @param limiter An initial limiter to set. If none is given, will be set
+ * to the external storage directory.
+ */
+ public FileSystemAdapter(LibraryActivity activity, Limiter limiter)
+ {
+ mActivity = activity;
+ mLimiter = limiter;
+ mFolderIcon = BitmapFactory.decodeResource(activity.getResources(), R.drawable.ic_launcher_folder);
+ if (limiter == null) {
+ limiter = buildLimiter(Environment.getExternalStorageDirectory());
+ }
+ setLimiter(limiter);
+ }
+
+ @Override
+ public Object query()
+ {
+ File file = mLimiter == null ? new File("/") : (File)mLimiter.data;
+
+ if (mFileObserver == null) {
+ mFileObserver = new Observer(file.getPath());
+ }
+
+ File[] files = file.listFiles(mFileFilter);
+ if (files != null)
+ Arrays.sort(files, mFileComparator);
+ return files;
+ }
+
+ @Override
+ public void commitQuery(Object data)
+ {
+ mFiles = (File[])data;
+ notifyDataSetInvalidated();
+ }
+
+ @Override
+ public void clear()
+ {
+ mFiles = null;
+ notifyDataSetInvalidated();
+ }
+
+ @Override
+ public int getCount()
+ {
+ if (mFiles == null)
+ return 0;
+ return mFiles.length;
+ }
+
+ @Override
+ public Object getItem(int pos)
+ {
+ return mFiles[pos];
+ }
+
+ @Override
+ public long getItemId(int pos)
+ {
+ return pos;
+ }
+
+ @Override
+ public View getView(int pos, View convertView, ViewGroup parent)
+ {
+ MediaView view;
+ if (convertView == null) {
+ view = new MediaView(mActivity, mFolderIcon, MediaView.sExpander);
+ } else {
+ view = (MediaView)convertView;
+ }
+ File file = mFiles[pos];
+ view.setData(pos, file.getName());
+ view.setShowBitmaps(file.isDirectory());
+ return view;
+ }
+
+ @Override
+ public void setFilter(String filter)
+ {
+ mFilter = SPACE_SPLIT.split(filter.toLowerCase());
+ }
+
+ @Override
+ public void setLimiter(Limiter limiter)
+ {
+ if (mFileObserver != null)
+ mFileObserver.stopWatching();
+ mFileObserver = null;
+ mLimiter = limiter;
+ }
+
+ @Override
+ public Limiter getLimiter()
+ {
+ return mLimiter;
+ }
+
+ /**
+ * Builds a limiter from the given folder. Only files contained in the
+ * given folder will be shown if the limiter is set on this adapter.
+ *
+ * @param file A File pointing to a folder.
+ * @return A limiter describing the given folder.
+ */
+ public static Limiter buildLimiter(File file)
+ {
+ String[] fields = FILE_SEPARATOR.split(file.getPath().substring(1));
+ return new Limiter(MediaUtils.TYPE_FILE, fields, file);
+ }
+
+ @Override
+ public Limiter buildLimiter(long id)
+ {
+ return buildLimiter(mFiles[(int)id]);
+ }
+
+ @Override
+ public int getMediaType()
+ {
+ return MediaUtils.TYPE_FILE;
+ }
+
+ /**
+ * FileObserver that reloads the files in this adapter.
+ */
+ private class Observer extends FileObserver {
+ public Observer(String path)
+ {
+ super(path, FileObserver.CREATE | FileObserver.DELETE | FileObserver.MOVED_TO | FileObserver.MOVED_FROM);
+ startWatching();
+ }
+
+ @Override
+ public void onEvent(int event, String path)
+ {
+ mActivity.postRequestRequery(FileSystemAdapter.this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/org/kreed/vanilla/LibraryActivity.java b/src/org/kreed/vanilla/LibraryActivity.java
index 3c751e8c..fdca43cf 100644
--- a/src/org/kreed/vanilla/LibraryActivity.java
+++ b/src/org/kreed/vanilla/LibraryActivity.java
@@ -26,8 +26,8 @@ import android.app.AlertDialog;
import android.content.ContentResolver;
import android.content.DialogInterface;
import android.content.Intent;
-import android.content.res.Resources;
import android.content.SharedPreferences;
+import android.content.res.Resources;
import android.database.ContentObserver;
import android.database.Cursor;
import android.graphics.Color;
@@ -42,6 +42,7 @@ import android.provider.MediaStore;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
+import android.util.Log;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@@ -59,6 +60,8 @@ import android.widget.RadioGroup;
import android.widget.TabHost;
import android.widget.TextView;
import android.widget.Toast;
+import java.io.File;
+import java.io.IOException;
import junit.framework.Assert;
/**
@@ -101,19 +104,35 @@ public class LibraryActivity
private int mLastAction = ACTION_PLAY;
private long mLastActedId;
- private MediaAdapter[] mAdapters;
- private MediaAdapter mArtistAdapter;
- private MediaAdapter mAlbumAdapter;
- private MediaAdapter mSongAdapter;
- private MediaAdapter mPlaylistAdapter;
- private MediaAdapter mGenreAdapter;
- private MediaAdapter mCurrentAdapter;
+ /**
+ * The number of adapters/lists (determines array sizes).
+ */
+ private static final int ADAPTER_COUNT = 6;
+ /**
+ * The ListView for each adapter, in the same order as MediaUtils.TYPE_*.
+ */
+ final ListView[] mLists = new ListView[ADAPTER_COUNT];
+ /**
+ * Whether the adapter corresponding to each index has stale data.
+ */
+ final boolean[] mRequeryNeeded = new boolean[ADAPTER_COUNT];
+ /**
+ * Each adapter, in the same order as MediaUtils.TYPE_*.
+ */
+ final LibraryAdapter[] mAdapters = new LibraryAdapter[ADAPTER_COUNT];
+ MediaAdapter mArtistAdapter;
+ MediaAdapter mAlbumAdapter;
+ MediaAdapter mSongAdapter;
+ MediaAdapter mPlaylistAdapter;
+ MediaAdapter mGenreAdapter;
+ FileSystemAdapter mFilesAdapter;
+ LibraryAdapter mCurrentAdapter;
private final ContentObserver mPlaylistObserver = new ContentObserver(null) {
@Override
public void onChange(boolean selfChange)
{
- mUiHandler.sendMessage(mUiHandler.obtainMessage(MSG_REQUEST_REQUERY, mPlaylistAdapter));
+ postRequestRequery(mPlaylistAdapter);
}
};
@@ -168,13 +187,19 @@ public class LibraryActivity
mTabHost = (TabHost)findViewById(R.id.tab_host);
mTabHost.setup();
- mArtistAdapter = setupView(R.id.artist_list, MediaUtils.TYPE_ARTIST, R.string.artists, R.drawable.ic_tab_artists, true, true, null);
- mAlbumAdapter = setupView(R.id.album_list, MediaUtils.TYPE_ALBUM, R.string.albums, R.drawable.ic_tab_albums, true, true, state == null ? null : (MediaAdapter.Limiter)state.getSerializable("limiter_albums"));
- mSongAdapter = setupView(R.id.song_list, MediaUtils.TYPE_SONG, R.string.songs, R.drawable.ic_tab_songs, false, true, state == null ? null : (MediaAdapter.Limiter)state.getSerializable("limiter_songs"));
- mPlaylistAdapter = setupView(R.id.playlist_list, MediaUtils.TYPE_PLAYLIST, R.string.playlists, R.drawable.ic_tab_playlists, true, false, null);
- mGenreAdapter = setupView(R.id.genre_list, MediaUtils.TYPE_GENRE, R.string.genres, R.drawable.ic_tab_genres, true, false, state == null ? null : (MediaAdapter.Limiter)state.getSerializable("limiter_genres"));
- // These should be in the same order as MediaUtils.TYPE_*
- mAdapters = new MediaAdapter[] { mArtistAdapter, mAlbumAdapter, mSongAdapter, mPlaylistAdapter, mGenreAdapter };
+ mArtistAdapter = new MediaAdapter(this, MediaUtils.TYPE_ARTIST, true, true, null);
+ mAlbumAdapter = new MediaAdapter(this, MediaUtils.TYPE_ALBUM, true, true, state == null ? null : (Limiter)state.getSerializable("limiter_albums"));
+ mSongAdapter = new MediaAdapter(this, MediaUtils.TYPE_SONG, false, true, state == null ? null : (Limiter)state.getSerializable("limiter_songs"));
+ mPlaylistAdapter = new MediaAdapter(this, MediaUtils.TYPE_PLAYLIST, true, false, null);
+ mGenreAdapter = new MediaAdapter(this, MediaUtils.TYPE_GENRE, true, false, null);
+ mFilesAdapter = new FileSystemAdapter(this, state == null ? null : (Limiter)state.getSerializable("limiter_files"));
+
+ setupView(0, R.id.artist_list, R.string.artists, R.drawable.ic_tab_artists, mArtistAdapter);
+ setupView(1, R.id.album_list, R.string.albums, R.drawable.ic_tab_albums, mAlbumAdapter);
+ setupView(2, R.id.song_list, R.string.songs, R.drawable.ic_tab_songs, mSongAdapter);
+ setupView(3, R.id.playlist_list, R.string.playlists, R.drawable.ic_tab_playlists, mPlaylistAdapter);
+ setupView(4, R.id.genre_list, R.string.genres, R.drawable.ic_tab_genres, mGenreAdapter);
+ setupView(5, R.id.file_list, R.string.files, R.drawable.ic_tab_files, mFilesAdapter);
getContentResolver().registerContentObserver(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, true, mPlaylistObserver);
@@ -219,7 +244,7 @@ public class LibraryActivity
out.putString("filter", mTextFilter.getText().toString());
out.putSerializable("limiter_albums", mAlbumAdapter.getLimiter());
out.putSerializable("limiter_songs", mSongAdapter.getLimiter());
- out.putSerializable("limiter_genres", mGenreAdapter.getLimiter());
+ out.putSerializable("limiter_files", mFilesAdapter.getLimiter());
}
@Override
@@ -282,7 +307,7 @@ public class LibraryActivity
* Adds songs matching the data from the given intent to the song timelime.
*
* @param intent An intent created with
- * {@link LibraryActivity#createClickIntent(MediaAdapter,MediaView)}.
+ * {@link LibraryActivity#createClickIntent(LibraryAdapter,MediaView)}.
* @param action One of LibraryActivity.ACTION_*
*/
private void pickSongs(Intent intent, int action)
@@ -295,7 +320,7 @@ public class LibraryActivity
boolean all = false;
int mode = action;
if (action == ACTION_PLAY_ALL || action == ACTION_ENQUEUE_ALL) {
- MediaAdapter adapter = mCurrentAdapter;
+ LibraryAdapter adapter = mCurrentAdapter;
boolean notPlayAllAdapter = (adapter != mSongAdapter && adapter != mAlbumAdapter
&& adapter != mArtistAdapter) || id == MediaView.HEADER_ID;
if (mode == ACTION_ENQUEUE_ALL && notPlayAllAdapter) {
@@ -324,13 +349,38 @@ public class LibraryActivity
* from the view and switching to the appropriate tab.
*
* @param intent An intent created with
- * {@link LibraryActivity#createClickIntent(MediaAdapter,MediaView)}.
+ * {@link LibraryActivity#createClickIntent(LibraryAdapter,MediaView)}.
*/
private void expand(Intent intent)
{
int type = intent.getIntExtra("type", 1);
long id = intent.getLongExtra("id", -1);
- mTabHost.setCurrentTab(setLimiter(mAdapters[type - 1].getLimiter(id)));
+ int tab = setLimiter(mAdapters[type - 1].buildLimiter(id));
+ if (tab == -1 || mTabHost.getCurrentTab() == tab) {
+ updateLimiterViews();
+ } else {
+ mTabHost.setCurrentTab(tab);
+ }
+ }
+
+ /**
+ * Clear a limiter.
+ *
+ * @param type Which type of limiter to clear.
+ */
+ private void clearLimiter(int type)
+ {
+ if (type == MediaUtils.TYPE_FILE) {
+ mFilesAdapter.setLimiter(null);
+ requestRequery(mFilesAdapter);
+ } else {
+ mAlbumAdapter.setLimiter(null);
+ mSongAdapter.setLimiter(null);
+ loadSortOrder(mSongAdapter);
+ loadSortOrder(mAlbumAdapter);
+ requestRequery(mSongAdapter);
+ requestRequery(mAlbumAdapter);
+ }
}
/**
@@ -338,34 +388,32 @@ public class LibraryActivity
*
* @return The tab to "expand" to
*/
- private int setLimiter(MediaAdapter.Limiter limiter)
+ private int setLimiter(Limiter limiter)
{
int tab;
- if (limiter == null) {
+ switch (limiter.type) {
+ case MediaUtils.TYPE_ALBUM:
+ mSongAdapter.setLimiter(limiter);
+ loadSortOrder(mSongAdapter);
+ requestRequery(mSongAdapter);
+ return 2;
+ case MediaUtils.TYPE_ARTIST:
+ mAlbumAdapter.setLimiter(limiter);
+ mSongAdapter.setLimiter(limiter);
+ tab = 1;
+ break;
+ case MediaUtils.TYPE_GENRE:
+ mSongAdapter.setLimiter(limiter);
mAlbumAdapter.setLimiter(null);
- mSongAdapter.setLimiter(null);
- tab = -1;
- } else {
- switch (limiter.type) {
- case MediaUtils.TYPE_ALBUM:
- mSongAdapter.setLimiter(limiter);
- loadSortOrder(mSongAdapter);
- requestRequery(mSongAdapter);
- return 2;
- case MediaUtils.TYPE_ARTIST:
- mAlbumAdapter.setLimiter(limiter);
- mSongAdapter.setLimiter(limiter);
- tab = 1;
- break;
- case MediaUtils.TYPE_GENRE:
- mSongAdapter.setLimiter(limiter);
- mAlbumAdapter.setLimiter(null);
- tab = 2;
- break;
- default:
- throw new IllegalArgumentException("Unsupported limiter type: " + limiter.type);
- }
+ tab = 2;
+ break;
+ case MediaUtils.TYPE_FILE:
+ mFilesAdapter.setLimiter(limiter);
+ requestRequery(mFilesAdapter);
+ return 5;
+ default:
+ throw new IllegalArgumentException("Unsupported limiter type: " + limiter.type);
}
loadSortOrder(mSongAdapter);
@@ -392,7 +440,7 @@ public class LibraryActivity
public void onItemClick(AdapterView> list, View view, int pos, long id)
{
MediaView mediaView = (MediaView)view;
- MediaAdapter adapter = (MediaAdapter)list.getAdapter();
+ LibraryAdapter adapter = (LibraryAdapter)list.getAdapter();
if (mediaView.isRightBitmapPressed()) {
if (adapter == mPlaylistAdapter)
editPlaylist(mediaView.getMediaId(), mediaView.getTitle());
@@ -419,7 +467,7 @@ public class LibraryActivity
public void onTextChanged(CharSequence text, int start, int before, int count)
{
String filter = text.toString();
- for (MediaAdapter adapter : mAdapters) {
+ for (LibraryAdapter adapter : mAdapters) {
adapter.setFilter(filter);
requestRequery(adapter);
}
@@ -432,11 +480,11 @@ public class LibraryActivity
mLimiterViews.removeAllViews();
- MediaAdapter adapter = mCurrentAdapter;
+ LibraryAdapter adapter = mCurrentAdapter;
if (adapter == null)
return;
- MediaAdapter.Limiter limiterData = adapter.getLimiter();
+ Limiter limiterData = adapter.getLimiter();
if (limiterData == null)
return;
String[] limiter = limiterData.names;
@@ -477,27 +525,31 @@ public class LibraryActivity
// a limiter view was clicked
int i = (Integer)view.getTag();
- if (i == 1) {
- // generate the artist limiter (we need to query the artist id)
- MediaAdapter.Limiter limiter = mSongAdapter.getLimiter();
- Assert.assertEquals(MediaUtils.TYPE_ALBUM, limiter.type);
-
+ Limiter limiter = mCurrentAdapter.getLimiter();
+ int type = limiter.type;
+ if (i == 1 && type == MediaUtils.TYPE_ALBUM) {
ContentResolver resolver = getContentResolver();
Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
String[] projection = new String[] { MediaStore.Audio.Media.ARTIST_ID };
- Cursor cursor = resolver.query(uri, projection, limiter.selection, null, null);
+ Cursor cursor = resolver.query(uri, projection, limiter.data.toString(), null, null);
if (cursor != null) {
if (cursor.moveToNext()) {
- setLimiter(mArtistAdapter.getLimiter(cursor.getLong(0)));
- updateLimiterViews();
- cursor.close();
- return;
+ setLimiter(mArtistAdapter.buildLimiter(cursor.getLong(0)));
}
cursor.close();
}
+ } else if (i > 0) {
+ Assert.assertEquals(MediaUtils.TYPE_FILE, limiter.type);
+ File file = (File)limiter.data;
+ int diff = limiter.names.length - i;
+ while (--diff != -1) {
+ file = file.getParentFile();
+ }
+ setLimiter(FileSystemAdapter.buildLimiter(file));
+ } else {
+ clearLimiter(type);
}
- setLimiter(null);
updateLimiterViews();
} else {
super.onClick(view);
@@ -510,12 +562,25 @@ public class LibraryActivity
* @param adapter The adapter that owns the view.
* @param view The MediaView to build from.
*/
- private static Intent createClickIntent(MediaAdapter adapter, MediaView view)
+ private static Intent createClickIntent(LibraryAdapter adapter, MediaView view)
{
+ int type = adapter.getMediaType();
+ long id = view.getMediaId();
Intent intent = new Intent();
- intent.putExtra("type", adapter.getMediaType());
- intent.putExtra("id", view.getMediaId());
+ intent.putExtra("type", type);
+ intent.putExtra("id", id);
intent.putExtra("title", view.getTitle());
+ if (type == MediaUtils.TYPE_FILE) {
+ File file = (File)adapter.getItem((int)id);
+ String path;
+ try {
+ path = file.getCanonicalPath();
+ } catch (IOException e) {
+ path = file.getAbsolutePath();
+ Log.e("VanillaMusic", "Failed to canonicalize path", e);
+ }
+ intent.putExtra("file", path);
+ }
return intent;
}
@@ -523,7 +588,7 @@ public class LibraryActivity
* Builds a media query based off the data stored in the given intent.
*
* @param intent An intent created with
- * {@link LibraryActivity#createClickIntent(MediaAdapter,MediaView)}.
+ * {@link LibraryActivity#createClickIntent(LibraryAdapter,MediaView)}.
* @param empty If true, use the empty projection (only query id).
* @param all If true query all songs in the adapter; otherwise query based
* on the row selected.
@@ -540,8 +605,10 @@ public class LibraryActivity
long id = intent.getLongExtra("id", -1);
QueryTask query;
- if (all || id == MediaView.HEADER_ID) {
- query = mAdapters[type - 1].buildSongQuery(projection);
+ if (type == MediaUtils.TYPE_FILE) {
+ query = MediaUtils.buildFileQuery(intent.getStringExtra("file"), projection);
+ } else if (all || id == MediaView.HEADER_ID) {
+ query = ((MediaAdapter)mAdapters[type - 1]).buildSongQuery(projection);
query.setExtra(id);
} else {
query = MediaUtils.buildQuery(type, id, projection, null);
@@ -570,7 +637,7 @@ public class LibraryActivity
return;
}
- MediaAdapter adapter = (MediaAdapter)((ListView)listView).getAdapter();
+ LibraryAdapter adapter = (LibraryAdapter)((ListView)listView).getAdapter();
MediaView view = (MediaView)((AdapterView.AdapterContextMenuInfo)absInfo).targetView;
// Store view data in intent to avoid problems when the view data changes
@@ -596,7 +663,7 @@ public class LibraryActivity
menu.add(0, MENU_EDIT, 0, R.string.edit).setIntent(intent);
}
menu.addSubMenu(0, MENU_ADD_TO_PLAYLIST, 0, R.string.add_to_playlist).getItem().setIntent(intent);
- if (adapter != mPlaylistAdapter && adapter != mSongAdapter)
+ if (view.hasRightBitmap())
menu.add(0, MENU_EXPAND, 0, R.string.expand).setIntent(intent);
if (!isHeader)
menu.add(0, MENU_DELETE, 0, R.string.delete).setIntent(intent);
@@ -608,7 +675,7 @@ public class LibraryActivity
*
* @param playlistId The id of the playlist to add to.
* @param intent An intent created with
- * {@link LibraryActivity#createClickIntent(MediaAdapter,MediaView)}.
+ * {@link LibraryActivity#createClickIntent(LibraryAdapter,MediaView)}.
*/
private void addToPlaylist(long playlistId, Intent intent)
{
@@ -635,22 +702,33 @@ public class LibraryActivity
* informing the user of this.
*
* @param intent An intent created with
- * {@link LibraryActivity#createClickIntent(MediaAdapter,MediaView)}.
+ * {@link LibraryActivity#createClickIntent(LibraryAdapter,MediaView)}.
*/
private void delete(Intent intent)
{
int type = intent.getIntExtra("type", 1);
long id = intent.getLongExtra("id", -1);
+ String message = null;
+ Resources res = getResources();
- if (type == MediaUtils.TYPE_PLAYLIST) {
+ if (type == MediaUtils.TYPE_FILE) {
+ String file = intent.getStringExtra("file");
+ boolean success = MediaUtils.deleteFile(new File(file));
+ if (!success) {
+ message = res.getString(R.string.delete_file_failed, file);
+ }
+ } else if (type == MediaUtils.TYPE_PLAYLIST) {
Playlist.deletePlaylist(getContentResolver(), id);
- String message = getResources().getString(R.string.playlist_deleted, intent.getStringExtra("title"));
- Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
} else {
int count = PlaybackService.get(this).deleteMedia(type, id);
- String message = getResources().getQuantityString(R.plurals.deleted, count, count);
- Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
+ message = res.getQuantityString(R.plurals.deleted, count, count);
}
+
+ if (message == null) {
+ message = res.getString(R.string.deleted, intent.getStringExtra("title"));
+ }
+
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
@Override
@@ -730,6 +808,13 @@ public class LibraryActivity
return super.onCreateOptionsMenu(menu);
}
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu)
+ {
+ menu.findItem(MENU_SORT).setEnabled(mCurrentAdapter != mFilesAdapter);
+ return super.onPrepareOptionsMenu(menu);
+ }
+
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
@@ -741,7 +826,7 @@ public class LibraryActivity
openPlaybackActivity();
return true;
case MENU_SORT: {
- MediaAdapter adapter = mCurrentAdapter;
+ MediaAdapter adapter = (MediaAdapter)mCurrentAdapter;
int mode = adapter.getSortMode();
int check;
if (mode < 0) {
@@ -780,15 +865,13 @@ public class LibraryActivity
/**
* Hook up a ListView to this Activity and the supplied adapter
*
+ * @param index Where to put the view and adapter in mLists/mAdapters.
* @param id The id of the ListView
- * @param type The media type for the adapter.
+ * @param adapter The adapter to hook up.
* @param label The text to show on the tab.
* @param icon The icon to show on the tab.
- * @param expandable True if the rows are expandable.
- * @param hasHeader True if the view should have a header row.
- * @param limiter The initial limiter to set on the adapter.
*/
- private MediaAdapter setupView(int id, int type, int label, int icon, boolean expandable, boolean hasHeader, MediaAdapter.Limiter limiter)
+ private void setupView(int index, int id, int label, int icon, LibraryAdapter adapter)
{
ListView view = (ListView)findViewById(id);
view.setOnItemClickListener(this);
@@ -797,9 +880,9 @@ public class LibraryActivity
view.setDivider(null);
view.setFastScrollEnabled(true);
- MediaAdapter adapter = new MediaAdapter(this, type, expandable, hasHeader, limiter);
view.setAdapter(adapter);
- loadSortOrder(adapter);
+ if (adapter instanceof MediaAdapter)
+ loadSortOrder((MediaAdapter)adapter);
Resources res = getResources();
String labelRes = res.getString(label);
@@ -809,7 +892,9 @@ public class LibraryActivity
else
mTabHost.addTab(mTabHost.newTabSpec(labelRes).setIndicator(labelRes, iconRes).setContent(id));
- return adapter;
+ mAdapters[index] = adapter;
+ mLists[index] = view;
+ mRequeryNeeded[index] = true;
}
/**
@@ -840,7 +925,7 @@ public class LibraryActivity
*/
private static final int MSG_SAVE_SORT = 16;
/**
- * Call {@link LibraryActivity#requestRequery(MediaAdapter)} on the adapter
+ * Call {@link LibraryActivity#requestRequery(LibraryAdapter)} on the adapter
* passed in obj.
*/
private static final int MSG_REQUEST_REQUERY = 17;
@@ -877,16 +962,18 @@ public class LibraryActivity
break;
}
case MSG_RUN_QUERY: {
- final MediaAdapter adapter = (MediaAdapter)message.obj;
- QueryTask query = adapter.buildQuery();
- final Cursor cursor = query.runQuery(getContentResolver());
+ final LibraryAdapter adapter = (LibraryAdapter)message.obj;
+ final Object data = adapter.query();
runOnUiThread(new Runnable() {
@Override
public void run()
{
- adapter.changeCursor(cursor);
+ adapter.commitQuery(data);
+ // scroll to the top of the list
+ mLists[adapter.getMediaType() - 1].setSelection(0);
}
});
+ mRequeryNeeded[adapter.getMediaType() - 1] = false;
break;
}
case MSG_SAVE_SORT: {
@@ -897,7 +984,7 @@ public class LibraryActivity
break;
}
case MSG_REQUEST_REQUERY:
- requestRequery((MediaAdapter)message.obj);
+ requestRequery((LibraryAdapter)message.obj);
break;
default:
return super.handleMessage(message);
@@ -913,15 +1000,15 @@ public class LibraryActivity
*
* Must be called on the UI thread.
*/
- public void requestRequery(MediaAdapter adapter)
+ public void requestRequery(LibraryAdapter adapter)
{
if (adapter == mCurrentAdapter) {
runQuery(adapter);
} else {
- adapter.requestRequery();
+ mRequeryNeeded[adapter.getMediaType() - 1] = true;
// Clear the data for non-visible adapters (so we don't show the old
// data briefly when we later switch to that adapter)
- adapter.changeCursor(null);
+ adapter.clear();
}
}
@@ -930,7 +1017,7 @@ public class LibraryActivity
*
* @param adapter The adapter to run the query for.
*/
- private void runQuery(MediaAdapter adapter)
+ private void runQuery(LibraryAdapter adapter)
{
mHandler.removeMessages(MSG_RUN_QUERY, adapter);
mHandler.sendMessage(mHandler.obtainMessage(MSG_RUN_QUERY, adapter));
@@ -939,12 +1026,23 @@ public class LibraryActivity
@Override
public void onMediaChange()
{
- Handler handler = mUiHandler;
- for (MediaAdapter adapter : mAdapters) {
- handler.sendMessage(handler.obtainMessage(MSG_REQUEST_REQUERY, adapter));
+ for (LibraryAdapter adapter : mAdapters) {
+ postRequestRequery(adapter);
}
}
+ /**
+ * Call {@link LibraryActivity#requestRequery(LibraryAdapter)} on the UI
+ * thread.
+ *
+ * @param adapter The adapter, passed to requestRequery.
+ */
+ public void postRequestRequery(LibraryAdapter adapter)
+ {
+ Handler handler = mUiHandler;
+ handler.sendMessage(handler.obtainMessage(MSG_REQUEST_REQUERY, adapter));
+ }
+
private void setSearchBoxVisible(boolean visible)
{
mSearchBoxVisible = visible;
@@ -1001,10 +1099,11 @@ public class LibraryActivity
@Override
public void onTabChanged(String tag)
{
- MediaAdapter adapter = mAdapters[mTabHost.getCurrentTab()];
+ LibraryAdapter adapter = mAdapters[mTabHost.getCurrentTab()];
mCurrentAdapter = adapter;
- if (adapter.isRequeryNeeded())
+ if (mRequeryNeeded[adapter.getMediaType() - 1]) {
runQuery(adapter);
+ }
updateLimiterViews();
}
@@ -1031,6 +1130,8 @@ public class LibraryActivity
@Override
public void onDismiss(DialogInterface dialog)
{
+ MediaAdapter adapter = (MediaAdapter)mCurrentAdapter;
+
ListView list = ((AlertDialog)dialog).getListView();
// subtract 1 for header
int which = list.getCheckedItemPosition() - 1;
@@ -1039,7 +1140,6 @@ public class LibraryActivity
if (group.getCheckedRadioButtonId() == R.id.descending)
which = ~which;
- MediaAdapter adapter = mCurrentAdapter;
adapter.setSortMode(which);
requestRequery(adapter);
diff --git a/src/org/kreed/vanilla/LibraryAdapter.java b/src/org/kreed/vanilla/LibraryAdapter.java
new file mode 100644
index 00000000..54455486
--- /dev/null
+++ b/src/org/kreed/vanilla/LibraryAdapter.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2011 Christopher Eby
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package org.kreed.vanilla;
+
+import android.widget.ListAdapter;
+
+/**
+ * Provides support for limiters and a few other methods LibraryActivity uses
+ * for its adapters.
+ */
+public interface LibraryAdapter extends ListAdapter {
+ /**
+ * Return the type of media represented by this adapter. One of
+ * MediaUtils.TYPE_*.
+ */
+ public int getMediaType();
+
+ /**
+ * Set the limiter for the adapter.
+ *
+ * A limiter is intended to restrict displayed media to only those that are
+ * children of a given parent media item.
+ *
+ * @param limiter The limiter, created by
+ * {@link LibraryAdapter#buildLimiter(long)}.
+ */
+ public void setLimiter(Limiter limiter);
+
+ /**
+ * Returns the limiter currently active on this adapter or null if none are
+ * active.
+ */
+ public Limiter getLimiter();
+
+ /**
+ * Builds a limiter based off of the media represented by the given row.
+ *
+ * @param id The id of the row.
+ * @see LibraryAdapter#getLimiter()
+ * @see LibraryAdapter#setLimiter(Limiter)
+ */
+ public Limiter buildLimiter(long id);
+
+ /**
+ * Set a new filter.
+ *
+ * The data should be requeried after calling this.
+ *
+ * @param filter The terms to filter on, separated by spaces. Only
+ * media that contain all of the terms (in any order) will be displayed
+ * after filtering is complete.
+ */
+ public void setFilter(String filter);
+
+ /**
+ * Retrieve the data for this adapter. The data must be set with
+ * {@link LibraryAdapter#commitQuery(Object)} before it takes effect.
+ *
+ * This should be called on a worker thread.
+ *
+ * @return The data. Contents depend on the sub-class.
+ */
+ public Object query();
+
+ /**
+ * Update the adapter with the given data.
+ *
+ * Must be called on the UI thread.
+ *
+ * @param data Data from {@link LibraryAdapter#query()}.
+ */
+ public void commitQuery(Object data);
+
+ /**
+ * Clear the data for this adapter.
+ *
+ * Must be called on the UI thread.
+ */
+ public void clear();
+}
\ No newline at end of file
diff --git a/src/org/kreed/vanilla/Limiter.java b/src/org/kreed/vanilla/Limiter.java
new file mode 100644
index 00000000..c6a0016c
--- /dev/null
+++ b/src/org/kreed/vanilla/Limiter.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2011 Christopher Eby
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package org.kreed.vanilla;
+
+import java.io.Serializable;
+
+/**
+ * Limiter is a constraint for MediaAdapter and FileSystemAdapter used when
+ * a row is "expanded".
+ */
+public class Limiter implements Serializable {
+ private static final long serialVersionUID = -4729694243900202614L;
+
+ /**
+ * The type of the limiter. One of MediaUtils.TYPE_ARTIST, TYPE_ALBUM,
+ * TYPE_GENRE, or TYPE_FILE.
+ */
+ public final int type;
+ /**
+ * Each element will be given a separate view each representing a higher
+ * different limiters. The first element is the broadest limiter, the last
+ * the most specific. For example, an album limiter would look like:
+ * { "Some Artist", "Some Album" }
+ * Or a file limiter:
+ * { "sdcard", "Music", "folder" }
+ */
+ public final String[] names;
+ /**
+ * The data for the limiter. This varies according to the type of the
+ * limiter.
+ */
+ public final Object data;
+
+ /**
+ * Create a limiter with the given data. All parameters initialize their
+ * corresponding fields in the class.
+ */
+ public Limiter(int type, String[] names, Object data)
+ {
+ this.type = type;
+ this.names = names;
+ this.data = data;
+ }
+}
\ No newline at end of file
diff --git a/src/org/kreed/vanilla/MediaAdapter.java b/src/org/kreed/vanilla/MediaAdapter.java
index e6229d24..1589a1ac 100644
--- a/src/org/kreed/vanilla/MediaAdapter.java
+++ b/src/org/kreed/vanilla/MediaAdapter.java
@@ -32,7 +32,6 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.SectionIndexer;
-import java.io.Serializable;
import java.util.regex.Pattern;
/**
@@ -46,7 +45,7 @@ import java.util.regex.Pattern;
* to a specific group to be displayed, e.g. only songs from a certain artist.
* See getLimiter and setLimiter for details.
*/
-public class MediaAdapter extends CursorAdapter implements SectionIndexer {
+public class MediaAdapter extends CursorAdapter implements SectionIndexer, LibraryAdapter {
private static final Pattern SPACE_SPLIT = Pattern.compile("\\s+");
/**
@@ -109,10 +108,6 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer {
* The sort order for use with buildSongQuery().
*/
private String mSongSort;
- /**
- * True if the data is stale and the query should be re-run.
- */
- private boolean mNeedsRequery;
/**
* The human-readable descriptions for each sort mode.
*/
@@ -152,7 +147,6 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer {
mHasHeader = hasHeader;
mLimiter = limiter;
mIndexer = new MusicAlphabetIndexer(1);
- mNeedsRequery = true;
switch (type) {
case MediaUtils.TYPE_ARTIST:
@@ -272,31 +266,7 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer {
notifyDataSetChanged();
}
- /**
- * Returns true if the data is stale and should be requeried.
- */
- public boolean isRequeryNeeded()
- {
- return mNeedsRequery;
- }
-
- /**
- * Mark the current data as requiring a requery.
- */
- public void requestRequery()
- {
- mNeedsRequery = true;
- }
-
- /**
- * Set a new filter.
- *
- * The data should be requeried after calling this.
- *
- * @param filter The terms to filter on, separated by spaces. Only
- * media that contain all of the terms (in any order) will be displayed
- * after filtering is complete.
- */
+ @Override
public void setFilter(String filter)
{
mConstraint = filter;
@@ -373,26 +343,28 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer {
if (limiter != null && limiter.type == MediaUtils.TYPE_GENRE) {
// Genre is not standard metadata for MediaStore.Audio.Media.
// We have to query it through a separate provider. : /
- return MediaUtils.buildGenreQuery(limiter.id, projection, selection.toString(), selectionArgs, sort);
+ return MediaUtils.buildGenreQuery((Long)limiter.data, projection, selection.toString(), selectionArgs, sort);
} else {
if (limiter != null) {
if (selection.length() != 0)
selection.append(" AND ");
- selection.append(limiter.selection);
+ selection.append(limiter.data);
}
return new QueryTask(mStore, projection, selection.toString(), selectionArgs, sort);
}
}
- /**
- * Build a query to populate the adapter with. The result should be set with
- * changeCursor().
- */
- public QueryTask buildQuery()
+ @Override
+ public Object query()
{
- mNeedsRequery = false;
- return buildQuery(mProjection, false);
+ return buildQuery(mProjection, false).runQuery(mContext.getContentResolver());
+ }
+
+ @Override
+ public void commitQuery(Object data)
+ {
+ changeCursor((Cursor)data);
}
/**
@@ -413,50 +385,35 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer {
return query;
}
- /**
- * Return the type of media represented by this adapter. One of
- * MediaUtils.TYPE_*.
- */
+ @Override
+ public void clear()
+ {
+ changeCursor(null);
+ }
+
+ @Override
public int getMediaType()
{
return mType;
}
- /**
- * Set the limiter for the adapter.
- *
- * A limiter is intended to restrict displayed media to only those that are
- * children of a given parent media item.
- *
- * The data should be requeried after calling this.
- *
- * @param limiter The limiter, created by {@link MediaAdapter#getLimiter(long)}.
- */
- public final void setLimiter(Limiter limiter)
+ @Override
+ public void setLimiter(Limiter limiter)
{
mLimiter = limiter;
}
- /**
- * Returns the limiter currently active on this adapter or null if none are
- * active.
- */
- public final Limiter getLimiter()
+ @Override
+ public Limiter getLimiter()
{
return mLimiter;
}
- /**
- * Builds a limiter based off of the media represented by the given row.
- *
- * @param id The id of the row.
- * @see MediaAdapter#getLimiter()
- * @see MediaAdapter#setLimiter(MediaAdapter.Limiter)
- */
- public Limiter getLimiter(long id)
+ @Override
+ public Limiter buildLimiter(long id)
{
String[] fields;
- String selection = null;
+ Object data;
Cursor cursor = getCursor();
if (cursor == null)
@@ -468,26 +425,23 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer {
}
switch (mType) {
- case MediaUtils.TYPE_ARTIST: {
+ case MediaUtils.TYPE_ARTIST:
fields = new String[] { cursor.getString(1) };
- String field = MediaStore.Audio.Media.ARTIST_ID;
- selection = String.format("%s=%d", field, id);
+ data = String.format("%s=%d", MediaStore.Audio.Media.ARTIST_ID, id);
break;
- }
- case MediaUtils.TYPE_ALBUM: {
+ case MediaUtils.TYPE_ALBUM:
fields = new String[] { cursor.getString(2), cursor.getString(1) };
- String field = MediaStore.Audio.Media.ALBUM_ID;
- selection = String.format("%s=%d", field, id);
+ data = String.format("%s=%d", MediaStore.Audio.Media.ALBUM_ID, id);
break;
- }
case MediaUtils.TYPE_GENRE:
fields = new String[] { cursor.getString(1) };
+ data = id;
break;
default:
throw new IllegalStateException("getLimiter() is not supported for media type: " + mType);
}
- return new MediaAdapter.Limiter(id, mType, selection, fields);
+ return new Limiter(mType, fields, data);
}
@Override
@@ -598,24 +552,4 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer {
{
return mSortMode;
}
-
- /**
- * Limiter is a constraint for MediaAdapters used when a row is "expanded".
- */
- public static class Limiter implements Serializable {
- private static final long serialVersionUID = -4729694243900202614L;
-
- public final String[] names;
- public final long id;
- public final int type;
- public final String selection;
-
- public Limiter(long id, int type, String selection, String[] names)
- {
- this.type = type;
- this.names = names;
- this.id = id;
- this.selection = selection;
- }
- }
}
diff --git a/src/org/kreed/vanilla/MediaUtils.java b/src/org/kreed/vanilla/MediaUtils.java
index d4cf8155..5c69e1a7 100644
--- a/src/org/kreed/vanilla/MediaUtils.java
+++ b/src/org/kreed/vanilla/MediaUtils.java
@@ -24,14 +24,19 @@ package org.kreed.vanilla;
import android.content.ContentResolver;
import android.database.Cursor;
+import android.database.DatabaseUtils;
import android.net.Uri;
import android.provider.MediaStore;
+import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import junit.framework.Assert;
+/**
+ * Provides some static Song/MediaStore-related utility functions.
+ */
public class MediaUtils {
/**
* A special invalid media type.
@@ -57,6 +62,11 @@ public class MediaUtils {
* Type indicating ids represent genres.
*/
public static final int TYPE_GENRE = 5;
+ /**
+ * Special type for files and folders. Most methods do not accept this type
+ * since files have no MediaStore id and require special handling.
+ */
+ public static final int TYPE_FILE = 6;
/**
* The default sort order for media queries. First artist, then album, then
@@ -488,4 +498,42 @@ public class MediaUtils {
return result;
}
+
+ /**
+ * Delete the given file or directory recursively.
+ *
+ * @return True if successful; false otherwise.
+ */
+ public static boolean deleteFile(File file)
+ {
+ File[] children = file.listFiles();
+ if (children != null) {
+ for (File child : children) {
+ deleteFile(child);
+ }
+ }
+ return file.delete();
+ }
+
+ /**
+ * Build a query that will contain all the media under the given path.
+ *
+ * @param path The path, e.g. /mnt/sdcard/music/
+ * @param projection The columns to query
+ * @return The initialized query.
+ */
+ public static QueryTask buildFileQuery(String path, String[] projection)
+ {
+ // It would be better to use selectionArgs to pass path here, but there
+ // doesn't appear to be any way to pass the * when using it.
+ StringBuilder selection = new StringBuilder();
+ selection.append("_data GLOB ");
+ DatabaseUtils.appendEscapedSQLString(selection, path);
+ // delete the quotation mark added by the escape method
+ selection.deleteCharAt(selection.length() - 1);
+ selection.append("*' AND is_music!=0");
+
+ Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+ return new QueryTask(media, projection, selection.toString(), null, DEFAULT_SORT);
+ }
}
diff --git a/src/org/kreed/vanilla/MediaView.java b/src/org/kreed/vanilla/MediaView.java
index 28840e87..738292c7 100644
--- a/src/org/kreed/vanilla/MediaView.java
+++ b/src/org/kreed/vanilla/MediaView.java
@@ -265,6 +265,14 @@ public final class MediaView extends View {
return mTitle;
}
+ /**
+ * Returns true if the view has a right bitmap that is visible.
+ */
+ public boolean hasRightBitmap()
+ {
+ return mRightBitmap != null && mShowBitmaps;
+ }
+
/**
* Returns true if the right bitmap was pressed in the last touch event.
*/
@@ -305,6 +313,19 @@ public final class MediaView extends View {
invalidate();
}
+ /**
+ * Set the id and title in this view.
+ *
+ * @param id The new id.
+ * @param title The new title for the view.
+ */
+ public void setData(long id, String title)
+ {
+ mId = id;
+ mTitle = title;
+ invalidate();
+ }
+
/**
* Update mExpanderPressed.
*/