package org.music.player;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.graphics.Color;
import android.net.Uri;
import android.provider.BaseColumns;
import android.provider.MediaStore;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.SectionIndexer;
import android.widget.TextView;
import java.util.regex.Pattern;
import org.music.player.R;
/**
* MediaAdapter provides an adapter backed by a MediaStore content provider.
* It generates simple one- or two-line text views to display each media
* element.
*
* Filtering is supported, as is a more specific type of filtering referred to
* as limiting. Limiting is separate from filtering; a new filter will not
* erase an active filter. Limiting is intended to allow only media belonging
* 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 BaseAdapter
implements SectionIndexer
, LibraryAdapter
, View.OnClickListener
{
private static final Pattern SPACE_SPLIT = Pattern.compile("\\s+");
/**
* A context to use.
*/
private final LibraryActivity mActivity;
/**
* A LayoutInflater to use.
*/
private final LayoutInflater mInflater;
/**
* The current data.
*/
private Cursor mCursor;
/**
* The type of media represented by this adapter. Must be one of the
* MediaUtils.FIELD_* constants. Determines which content provider to query for
* media and what fields to display.
*/
private final int mType;
/**
* The URI of the content provider backing this adapter.
*/
private Uri mStore;
/**
* The fields to use from the content provider. The last field will be
* displayed in the MediaView, as will the first field if there are
* multiple fields. Other fields will be used for searching.
*/
private String[] mFields;
/**
* The collation keys corresponding to each field. If provided, these are
* used to speed up sorting and filtering.
*/
private String[] mFieldKeys;
/**
* The columns to query from the content provider.
*/
private String[] mProjection;
/**
* A limiter is used for filtering. The intention is to restrict items
* displayed in the list to only those of a specific artist or album, as
* selected through an expander arrow in a broader MediaAdapter list.
*/
private Limiter mLimiter;
/**
* The constraint used for filtering, set by the search box.
*/
private String mConstraint;
/**
* The section indexer, for the letter pop-up when scrolling.
*/
private final MusicAlphabetIndexer mIndexer;
/**
* The sections used by the indexer.
*/
private Object[] mSections;
/**
* The sort order for use with buildSongQuery().
*/
private String mSongSort;
/**
* The human-readable descriptions for each sort mode.
*/
private int[] mSortEntries;
/**
* An array ORDER BY expressions for each sort mode. %1$s is replaced by
* ASC or DESC as appropriate before being passed to the query.
*/
private String[] mSortValues;
/**
* The index of the current of the current sort mode in mSortValues, or
* the inverse of the index (in which case sort should be descending
* instead of ascending).
*/
private int mSortMode;
/**
* If true, show the expander button on each row.
*/
private boolean mExpandable;
/**
* Construct a MediaAdapter representing the given <code>type</code> of
* media.
*
* @param activity The LibraryActivity that will contain this adapter.
* @param type The type of media to represent. Must be one of the
* Song.TYPE_* constants. This determines which content provider to query
* and what fields to display in the views.
* @param limiter An initial limiter to use
*/
public MediaAdapter(LibraryActivity activity, int type, Limiter limiter)
{
mActivity = activity;
mType = type;
mLimiter = limiter;
mIndexer = new MusicAlphabetIndexer(1);
mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
switch (type) {
case MediaUtils.TYPE_ARTIST:
mStore = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI;
mFields = new String[] { MediaStore.Audio.Artists.ARTIST };
mFieldKeys = new String[] { MediaStore.Audio.Artists.ARTIST_KEY };
mSongSort = MediaUtils.DEFAULT_SORT;
mSortEntries = new int[] { R.string.name, R.string.number_of_tracks };
mSortValues = new String[] { "artist_key %1$s", "number_of_tracks %1$s,artist_key %1$s" };
break;
case MediaUtils.TYPE_ALBUM:
mStore = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI;
mFields = new String[] { MediaStore.Audio.Albums.ARTIST, MediaStore.Audio.Albums.ALBUM };
// Why is there no artist_key column constant in the album MediaStore? The column does seem to exist.
mFieldKeys = new String[] { "artist_key", MediaStore.Audio.Albums.ALBUM_KEY };
mSongSort = "album_key,track";
mSortEntries = new int[] { R.string.name, R.string.artist_album, R.string.year, R.string.number_of_tracks };
mSortValues = new String[] { "album_key %1$s", "artist_key %1$s,album_key %1$s", "minyear %1$s,album_key %1$s", "numsongs %1$s,album_key %1$s" };
break;
case MediaUtils.TYPE_SONG:
mStore = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
mFields = new String[] { MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.TITLE };
mFieldKeys = new String[] { MediaStore.Audio.Media.ARTIST_KEY, MediaStore.Audio.Media.ALBUM_KEY, MediaStore.Audio.Media.TITLE_KEY };
mSortEntries = new int[] { R.string.name, R.string.artist_album_track, R.string.artist_album_title, R.string.artist_year, R.string.year };
mSortValues = new String[] { "title_key %1$s", "artist_key %1$s,album_key %1$s,track %1$s", "artist_key %1$s,album_key %1$s,title_key %1$s", "artist_key %1$s,year %1$s,track %1$s", "year %1$s,title_key %1$s" };
break;
case MediaUtils.TYPE_PLAYLIST:
mStore = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI;
mFields = new String[] { MediaStore.Audio.Playlists.NAME };
mFieldKeys = null;
mSortEntries = new int[] { R.string.name, R.string.date_added };
mSortValues = new String[] { "name %1$s", "date_added %1$s" };
mExpandable = true;
break;
case MediaUtils.TYPE_GENRE:
mStore = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI;
mFields = new String[] { MediaStore.Audio.Genres.NAME };
mFieldKeys = null;
mSortEntries = new int[] { R.string.name };
mSortValues = new String[] { "name %1$s" };
break;
default:
throw new IllegalArgumentException("Invalid value for type: " + type);
}
if (mFields.length == 1)
mProjection = new String[] { BaseColumns._ID, mFields[0] };
else
mProjection = new String[] { BaseColumns._ID, mFields[mFields.length - 1], mFields[0] };
}
/**
* Set whether or not the expander button should be shown in each row.
* Defaults to true for playlist adapter and false for all others.
*
* @param expandable True to show expander, false to hide.
*/
public void setExpandable(boolean expandable)
{
if (expandable != mExpandable) {
mExpandable = expandable;
notifyDataSetChanged();
}
}
@Override
public void setFilter(String filter)
{
mConstraint = filter;
}
/**
* Build the query to be run with runQuery().
*
* @param projection The columns to query.
* @param forceMusicCheck Force the is_music check to be added to the
* selection.
*/
public QueryTask buildQuery(String[] projection, boolean forceMusicCheck)
{
String constraint = mConstraint;
Limiter limiter = mLimiter;
StringBuilder selection = new StringBuilder();
String[] selectionArgs = null;
int mode = mSortMode;
String sortDir;
if (mode < 0) {
mode = ~mode;
sortDir = "DESC";
} else {
sortDir = "ASC";
}
String sort = String.format(mSortValues[mode], sortDir);
if (mType == MediaUtils.TYPE_SONG || forceMusicCheck)
selection.append("is_music!=0");
if (constraint != null && constraint.length() != 0) {
String[] needles;
String[] keySource;
// If we are using sorting keys, we need to change our constraint
// into a list of collation keys. Otherwise, just split the
// constraint with no modification.
if (mFieldKeys != null) {
String colKey = MediaStore.Audio.keyFor(constraint);
String spaceColKey = DatabaseUtils.getCollationKey(" ");
needles = colKey.split(spaceColKey);
keySource = mFieldKeys;
} else {
needles = SPACE_SPLIT.split(constraint);
keySource = mFields;
}
int size = needles.length;
selectionArgs = new String[size];
StringBuilder keys = new StringBuilder(20);
keys.append(keySource[0]);
for (int j = 1; j != keySource.length; ++j) {
keys.append("||");
keys.append(keySource[j]);
}
for (int j = 0; j != needles.length; ++j) {
selectionArgs[j] = '%' + needles[j] + '%';
// If we have something in the selection args (i.e. j > 0), we
// must have something in the selection, so we can skip the more
// costly direct check of the selection length.
if (j != 0 || selection.length() != 0)
selection.append(" AND ");
selection.append(keys);
selection.append(" LIKE ?");
}
}
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((Long)limiter.data, projection, selection.toString(), selectionArgs, sort);
} else {
if (limiter != null) {
if (selection.length() != 0)
selection.append(" AND ");
selection.append(limiter.data);
}
return new QueryTask(mStore, projection, selection.toString(), selectionArgs, sort);
}
}
@Override
public Object query()
{
return buildQuery(mProjection, false).runQuery(mActivity.getContentResolver());
}
@Override
public void commitQuery(Object data)
{
changeCursor((Cursor)data);
}
/**
* Build a query for all the songs represented by this adapter, for adding
* to the timeline.
*
* @param projection The columns to query.
*/
public QueryTask buildSongQuery(String[] projection)
{
QueryTask query = buildQuery(projection, true);
query.type = mType;
if (mType != MediaUtils.TYPE_SONG) {
query.uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
// Would be better to match the sort order in the adapter. This
// is likely to require significantly more work though.
query.sortOrder = mSongSort;
}
return query;
}
@Override
public void clear()
{
changeCursor(null);
}
@Override
public int getMediaType()
{
return mType;
}
@Override
public void setLimiter(Limiter limiter)
{
mLimiter = limiter;
}
@Override
public Limiter getLimiter()
{
return mLimiter;
}
@Override
public Limiter buildLimiter(long id)
{
String[] fields;
Object data;
Cursor cursor = mCursor;
if (cursor == null)
return null;
for (int i = 0, count = cursor.getCount(); i != count; ++i) {
cursor.moveToPosition(i);
if (cursor.getLong(0) == id)
break;
}
switch (mType) {
case MediaUtils.TYPE_ARTIST:
fields = new String[] { cursor.getString(1) };
data = String.format("%s=%d", MediaStore.Audio.Media.ARTIST_ID, id);
break;
case MediaUtils.TYPE_ALBUM:
fields = new String[] { cursor.getString(2), cursor.getString(1) };
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 Limiter(mType, fields, data);
}
/**
* Set a new cursor for this adapter. The old cursor will be closed.
*
* @param cursor The new cursor.
*/
public void changeCursor(Cursor cursor)
{
Cursor old = mCursor;
mCursor = cursor;
if (cursor == null) {
notifyDataSetInvalidated();
} else {
notifyDataSetChanged();
}
mIndexer.setCursor(cursor);
if (old != null) {
old.close();
}
}
@Override
public Object[] getSections()
{
if (mSections == null) {
if (mSortMode == 0)
mSections = MusicAlphabetIndexer.getSections();
else
mSections = new String[] { " " };
}
return mSections;
}
@Override
public int getPositionForSection(int section)
{
if (section == 0)
return 0;
if (section == getSections().length)
return getCount();
return mIndexer.getPositionForSection(section);
}
@Override
public int getSectionForPosition(int position)
{
if (mSortMode != 0)
return 0;
return mIndexer.getSectionForPosition(position);
}
private static class ViewHolder {
public long id;
public String title;
public TextView text;
public ImageView arrow;
}
@Override
public View getView(int position, View view, ViewGroup parent)
{
ViewHolder holder;
if (view == null || mExpandable != view instanceof LinearLayout) {
// We must create a new view if we're not given a recycle view or
// if the recycle view has the wrong layout.
int layout = mExpandable ? R.layout.library_row_expandable : R.layout.library_row;
view = mInflater.inflate(layout, null);
holder = new ViewHolder();
view.setTag(holder);
if (mExpandable) {
holder.text = (TextView)view.findViewById(R.id.text);
holder.arrow = (ImageView)view.findViewById(R.id.arrow);
holder.arrow.setOnClickListener(this);
} else {
holder.text = (TextView)view;
view.setLongClickable(true);
}
holder.text.setOnClickListener(this);
} else {
holder = (ViewHolder)view.getTag();
}
Cursor cursor = mCursor;
cursor.moveToPosition(position);
holder.id = cursor.getLong(0);
if (mFields.length > 1) {
String line1 = cursor.getString(1);
String line2 = cursor.getString(2);
SpannableStringBuilder sb = new SpannableStringBuilder(line1);
sb.append('\n');
sb.append(line2);
sb.setSpan(new ForegroundColorSpan(Color.GRAY), line1.length() + 1, sb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
holder.text.setText(sb);
holder.title = line1;
} else {
String title = cursor.getString(1);
holder.text.setText(title);
holder.title = title;
}
return view;
}
/**
* Returns the type of the current limiter.
*
* @return One of MediaUtils.TYPE_, or MediaUtils.TYPE_INVALID if there is
* no limiter set.
*/
public int getLimiterType()
{
Limiter limiter = mLimiter;
if (limiter != null)
return limiter.type;
return MediaUtils.TYPE_INVALID;
}
/**
* Return the available sort modes for this adapter.
*
* @return An array containing the resource ids of the sort mode strings.
*/
public int[] getSortEntries()
{
return mSortEntries;
}
/**
* Set the sorting mode. The adapter should be re-queried after changing
* this.
*
* @param i The index of the sort mode in the sort entries array. If this
* is negative, the inverse of the index will be used and sort order will
* be reversed.
*/
public void setSortMode(int i)
{
mSortMode = i;
mSections = null;
}
/**
* Returns the sort mode that should be used if no preference is saved. This
* may very based on the active limiter.
*/
public int getDefaultSortMode()
{
int type = mType;
if (type == MediaUtils.TYPE_ALBUM || type == MediaUtils.TYPE_SONG)
return 1; // aritst,album,track
return 0;
}
/**
* Return the current sort mode set on this adapter.
*/
public int getSortMode()
{
return mSortMode;
}
@Override
public Intent createData(View view)
{
ViewHolder holder = (ViewHolder)view.getTag();
Intent intent = new Intent();
intent.putExtra(LibraryAdapter.DATA_TYPE, mType);
intent.putExtra(LibraryAdapter.DATA_ID, holder.id);
intent.putExtra(LibraryAdapter.DATA_TITLE, holder.title);
intent.putExtra(LibraryAdapter.DATA_EXPANDABLE, mExpandable);
return intent;
}
@Override
public void onClick(View view)
{
int id = view.getId();
if (mExpandable)
view = (View)view.getParent();
Intent intent = createData(view);
if (id == R.id.arrow) {
mActivity.onItemExpanded(intent);
} else {
mActivity.onItemClicked(intent);
}
}
@Override
public int getCount()
{
Cursor cursor = mCursor;
if (cursor == null)
return 0;
return cursor.getCount();
}
@Override
public Object getItem(int position)
{
return null;
}
@Override
public long getItemId(int position)
{
Cursor cursor = mCursor;
if (cursor == null)
return 0;
cursor.moveToPosition(position);
return cursor.getLong(0);
}
@Override
public boolean hasStableIds()
{
return true;
}
}