Files tab
This commit is contained in:
parent
2569247cbf
commit
3f26fd0965
BIN
res/drawable-hdpi/ic_launcher_folder.png
Normal file
BIN
res/drawable-hdpi/ic_launcher_folder.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
BIN
res/drawable-hdpi/ic_tab_files_selected.png
Normal file
BIN
res/drawable-hdpi/ic_tab_files_selected.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
BIN
res/drawable-hdpi/ic_tab_files_unselected.png
Normal file
BIN
res/drawable-hdpi/ic_tab_files_unselected.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1003 B |
BIN
res/drawable-mdpi/ic_launcher_folder.png
Normal file
BIN
res/drawable-mdpi/ic_launcher_folder.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
res/drawable-mdpi/ic_tab_files_selected.png
Normal file
BIN
res/drawable-mdpi/ic_tab_files_selected.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
res/drawable-mdpi/ic_tab_files_unselected.png
Normal file
BIN
res/drawable-mdpi/ic_tab_files_unselected.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 764 B |
5
res/drawable/ic_tab_files.xml
Normal file
5
res/drawable/ic_tab_files.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/ic_tab_files_selected" android:state_selected="true" android:state_pressed="false" />
|
||||
<item android:drawable="@drawable/ic_tab_files_unselected" />
|
||||
</selector>
|
@ -65,12 +65,21 @@ THE SOFTWARE.
|
||||
android:visibility="gone"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent" />
|
||||
<ListView
|
||||
android:id="@+id/file_list"
|
||||
android:visibility="gone"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent" />
|
||||
</FrameLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/limiter_layout"
|
||||
android:layout_width="wrap_content"
|
||||
<HorizontalScrollView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|left" />
|
||||
android:layout_gravity="bottom|left">
|
||||
<LinearLayout
|
||||
android:id="@+id/limiter_layout"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent" />
|
||||
</HorizontalScrollView>
|
||||
<LinearLayout
|
||||
android:id="@+id/search_box"
|
||||
android:layout_width="fill_parent"
|
||||
|
@ -62,7 +62,6 @@ THE SOFTWARE.
|
||||
<item quantity="one">1 låt lagt til spilleliste %2$s.</item>
|
||||
<item quantity="other">%1$d låter lagt til spilleliste %2$s.</item>
|
||||
</plurals>
|
||||
<string name="playlist_deleted">Spilleliste %s slettet.</string>
|
||||
<plurals name="deleted">
|
||||
<item quantity="one">1 låt slettet.</item>
|
||||
<item quantity="other">%d låter slettet.</item>
|
||||
|
@ -65,7 +65,6 @@ THE SOFTWARE.
|
||||
<item quantity="few">%1$d skladby pridané do zoznamu skladieb %2$s.</item>
|
||||
<item quantity="other">%1$d skladieb pridaných do zoznamu skladieb %2$s.</item>
|
||||
</plurals>
|
||||
<string name="playlist_deleted">Zoznam skladieb %s odstránený.</string>
|
||||
<plurals name="deleted">
|
||||
<item quantity="one">1 skladba odstránená.</item>
|
||||
<item quantity="few">%d skladby odstránené.</item>
|
||||
|
@ -74,17 +74,19 @@ THE SOFTWARE.
|
||||
<item quantity="one">1 song added to playlist %2$s.</item>
|
||||
<item quantity="other">%1$d songs added to playlist %2$s.</item>
|
||||
</plurals>
|
||||
<string name="playlist_deleted">Playlist %s deleted.</string>
|
||||
<string name="deleted">%s deleted.</string>
|
||||
<plurals name="deleted">
|
||||
<item quantity="one">1 song deleted.</item>
|
||||
<item quantity="other">%d songs deleted.</item>
|
||||
</plurals>
|
||||
<string name="delete_file_failed">Failed to delete %s.</string>
|
||||
|
||||
<string name="artists">Artists</string>
|
||||
<string name="albums">Albums</string>
|
||||
<string name="songs">Songs</string>
|
||||
<string name="playlists">Playlists</string>
|
||||
<string name="genres">Genres</string>
|
||||
<string name="files">Files</string>
|
||||
|
||||
<string name="none">None</string>
|
||||
<string name="unknown">Unknown</string>
|
||||
|
253
src/org/kreed/vanilla/FileSystemAdapter.java
Normal file
253
src/org/kreed/vanilla/FileSystemAdapter.java
Normal file
@ -0,0 +1,253 @@
|
||||
/*
|
||||
* Copyright (C) 2010, 2011 Christopher Eby <kreed@kreed.org>
|
||||
*
|
||||
* 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<File> mFileComparator = new Comparator<File>() {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
100
src/org/kreed/vanilla/LibraryAdapter.java
Normal file
100
src/org/kreed/vanilla/LibraryAdapter.java
Normal file
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Christopher Eby <kreed@kreed.org>
|
||||
*
|
||||
* 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();
|
||||
}
|
64
src/org/kreed/vanilla/Limiter.java
Normal file
64
src/org/kreed/vanilla/Limiter.java
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Christopher Eby <kreed@kreed.org>
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user