/*****************************************************************************
* AudioBrowserListAdapter.java
*****************************************************************************
* Copyright © 2011-2014 VLC authors and VideoLAN
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
package org.videolan.vlc.gui.audio;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.DataSetObserver;
import android.databinding.DataBindingUtil;
import android.databinding.ViewDataBinding;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.preference.PreferenceManager;
import android.support.v4.util.ArrayMap;
import android.util.SparseArray;
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 org.videolan.vlc.BR;
import org.videolan.vlc.MediaWrapper;
import org.videolan.vlc.R;
import org.videolan.vlc.VLCApplication;
import org.videolan.vlc.gui.AsyncImageLoader;
import org.videolan.vlc.interfaces.IAudioClickHandler;
import org.videolan.vlc.util.BitmapUtil;
import org.videolan.vlc.util.Util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class AudioBrowserListAdapter extends BaseAdapter implements SectionIndexer, IAudioClickHandler {
public final static String TAG = "VLC/AudioBrowserListAdapter";
public final static int TYPE_ARTISTS = 0;
public final static int TYPE_ALBUMS = 1;
public final static int TYPE_SONGS = 2;
public final static int TYPE_GENRES = 3;
public final static int TYPE_PLAYLISTS = 4;
// Key: the item title, value: ListItem of only media item (no separator).
private Map<String, ListItem> mMediaItemMap;
private Map<String, ListItem> mSeparatorItemMap;
// A list of all the list items: media items and separators.
private ArrayList<ListItem> mItems;
// A list of all the sections in the list; better performance than searching the whole list
private SparseArray<String> mSections;
private int mAlignMode; // align mode from prefs
private Activity mContext;
// The types of the item views: media and separator.
private static final int VIEW_MEDIA = 0;
private static final int VIEW_SEPARATOR = 1;
// The types of the media views.
public static final int ITEM_WITHOUT_COVER = 0;
public static final int ITEM_WITH_COVER = 1;
private int mItemType;
private ContextPopupMenuListener mContextPopupMenuListener;
// An item of the list: a media or a separator.
public static class ListItem {
final public String mTitle;
final public String mSubTitle;
final public ArrayList<MediaWrapper> mMediaList;
final public boolean mIsSeparator;
public ListItem(String title, String subTitle, MediaWrapper media, boolean isSeparator) {
mMediaList = new ArrayList<MediaWrapper>();
if (media != null)
mMediaList.add(media);
mTitle = title;
mSubTitle = subTitle;
mIsSeparator = isSeparator;
}
}
public AudioBrowserListAdapter(Activity context, int itemType) {
mMediaItemMap = new ArrayMap<String, ListItem>();
mSeparatorItemMap = new ArrayMap<String, ListItem>();
mItems = new ArrayList<ListItem>();
mSections = new SparseArray<String>();
mContext = context;
if (itemType != ITEM_WITHOUT_COVER && itemType != ITEM_WITH_COVER)
throw new IllegalArgumentException();
mItemType = itemType;
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
mAlignMode = Integer.valueOf(preferences.getString("audio_title_alignment", "0"));
}
public void addAll(final List<ListItem> items) {
mContext.runOnUiThread(new Runnable() {
@Override
public void run() {
for (ListItem item : items) {
mMediaItemMap.put(item.mTitle, item);
mItems.add(item);
}
Collections.sort(mItems, mItemsComparator);
}
});
}
public void add(String title, String subTitle, MediaWrapper media) {
add(title, subTitle, media, null);
}
public void add(String title, String subTitle, MediaWrapper media, String key) {
if(title == null) return;
title = title.trim();
String mediaKey;
if (key == null)
mediaKey = title.toLowerCase(Locale.getDefault());
else
mediaKey = key.trim().toLowerCase(Locale.getDefault());
if(subTitle != null) subTitle = subTitle.trim();
if (mMediaItemMap.containsKey(mediaKey))
mMediaItemMap.get(mediaKey).mMediaList.add(media);
else {
ListItem item = new ListItem(title, subTitle, media, false);
mMediaItemMap.put(mediaKey, item);
mItems.add(item);
}
}
public void addAll(List<MediaWrapper> mediaList, final int type) {
final LinkedList<MediaWrapper> list = new LinkedList<MediaWrapper>(mediaList);
mContext.runOnUiThread(new Runnable() {
@Override
public void run() {
clear();
String title, subTitle, key;
for (MediaWrapper media : list) {
switch (type){
case TYPE_ALBUMS:
title = Util.getMediaAlbum(mContext, media);
subTitle = Util.getMediaReferenceArtist(mContext, media);
key = null;
break;
case TYPE_ARTISTS:
title = Util.getMediaReferenceArtist(mContext, media);
subTitle = null;
key = null;
break;
case TYPE_GENRES:
title = Util.getMediaGenre(mContext, media);
subTitle = null;
key = null;
break;
case TYPE_PLAYLISTS:
title = media.getTitle();
subTitle = null;
key = null;
break;
case TYPE_SONGS:
default:
title = media.getTitle();
subTitle = Util.getMediaArtist(mContext, media);
key = media.getLocation();
}
add(title, subTitle, media, key);
}
calculateSections(type);
}
});
}
/**
* Calculate sections of the list
*
* @param type Type of the audio file sort.
*/
private void calculateSections(int type) {
char prevFirstChar = 'a';
boolean firstSeparator = true;
for (int i = 0; i < mItems.size(); ++i) {
String title = mItems.get(i).mTitle;
String unknown;
switch (type){
case TYPE_ALBUMS:
unknown = mContext.getString(R.string.unknown_album);
break;
case TYPE_GENRES:
unknown = mContext.getString(R.string.unknown_genre);
break;
case TYPE_ARTISTS:
unknown = mContext.getString(R.string.unknown_artist);
break;
default:
unknown = null;
}
char firstChar;
if(title.length() > 0 && (unknown == null || !unknown.equals(title)))
firstChar = title.toUpperCase(Locale.ENGLISH).charAt(0);
else
firstChar = '#'; // Blank / spaces-only song title.
if (Character.isLetter(firstChar)) {
if (firstSeparator || firstChar != prevFirstChar) {
ListItem item = new ListItem(String.valueOf(firstChar), null, null, true);
mItems.add(i, item);
mSections.put(i, String.valueOf(firstChar));
i++;
prevFirstChar = firstChar;
firstSeparator = false;
}
}
else if (firstSeparator) {
ListItem item = new ListItem("#", null, null, true);
mItems.add(i, item);
mSections.put(i, "#");
i++;
prevFirstChar = firstChar;
firstSeparator = false;
}
}
}
public void addSeparator(String title, MediaWrapper media) {
if(title == null) return;
title = title.trim();
final String titleKey = title.toLowerCase(Locale.getDefault());
if (mSeparatorItemMap.containsKey(titleKey))
mSeparatorItemMap.get(titleKey).mMediaList.add(media);
else {
ListItem item = new ListItem(title, null, media, true);
mSeparatorItemMap.put(titleKey, item);
mItems.add(item);
}
}
public void sortByAlbum(){
mItems.clear();
for (ListItem album : mSeparatorItemMap.values()){
mItems.add(album);
Collections.sort(album.mMediaList, MediaComparators.byTrackNumber);
for (MediaWrapper media : album.mMediaList)
add(media.getTitle(), null, media, media.getLocation());
}
}
/**
* Remove all the reference to a media in the list items.
* Remove also all the list items that contain only this media.
* @param media the media to remove
*/
public void removeMedia(MediaWrapper media) {
for (int i = 0; i < mItems.size(); ++i) {
ListItem item = mItems.get(i);
if (item.mMediaList == null)
continue;
for (int j = 0; j < item.mMediaList.size(); ++j)
if (item.mMediaList.get(j).getLocation().equals(media.getLocation())) {
item.mMediaList.remove(j);
j--;
}
if (item.mMediaList.isEmpty() && !item.mIsSeparator) {
mItems.remove(i);
i--;
}
}
notifyDataSetChanged();
}
public void clear() {
mMediaItemMap.clear();
mSeparatorItemMap.clear();
mItems.clear();
mSections.clear();
notifyDataSetChanged();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (getItemViewType(position) == VIEW_MEDIA)
return getViewMedia(position, convertView, parent);
else // == VIEW_SEPARATOR
return getViewSeparator(position, convertView, parent);
}
public View getViewMedia(int position, View convertView, ViewGroup parent) {
View v = convertView;
ViewHolder holder = null;
/* convertView may be a recycled view but we must recreate it
* if it does not correspond to a media view. */
boolean b_createView = true;
if (v != null) {
holder = (ViewHolder) v.getTag();
if (holder.viewType == VIEW_MEDIA)
b_createView = false;
}
if (b_createView) {
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
holder = new ViewHolder();
holder.binding = DataBindingUtil.inflate(inflater, R.layout.audio_browser_item, parent, false);
v = holder.binding.getRoot();
Util.setAlignModeByPref(mAlignMode, (TextView) v.findViewById(R.id.title));
holder.viewType = VIEW_MEDIA;
v.setTag(holder);
}
ListItem item = getItem(position);
holder.binding.setVariable(BR.item, item);
holder.binding.setVariable(BR.position, position);
final ArrayList<MediaWrapper> mediaList = mItems.get(position).mMediaList;
boolean asyncLoad = true;
if (mItemType == ITEM_WITH_COVER) {
Bitmap bitmap = AudioUtil.getCoverFromMemCache(mContext, mediaList, 64);
if (bitmap != null) {
asyncLoad = false;
holder.binding.setVariable(BR.cover, new BitmapDrawable(VLCApplication.getAppResources(), bitmap));
} else {
holder.binding.setVariable(BR.cover, AudioUtil.DEFAULT_COVER);
}
} else
holder.binding.setVariable(BR.cover, AudioUtil.DEFAULT_COVER);
holder.binding.setVariable(BR.footer, !isMediaItemAboveASeparator(position));
holder.binding.setVariable(BR.clickable, mContextPopupMenuListener != null);
holder.binding.setVariable(BR.handler, this);
holder.binding.executePendingBindings();
if (asyncLoad)
AsyncImageLoader.LoadImage(new AudioCoverFetcher(holder.binding, mContext, mediaList), null);
return v;
}
public View getViewSeparator(int position, View convertView, ViewGroup parent) {
View v = convertView;
ViewHolder holder = null;
/* convertView may be a recycled view but we must recreate it
* if it does not correspond to a separator view. */
boolean b_createView = true;
if (v != null) {
holder = (ViewHolder) v.getTag();
if (holder.viewType == VIEW_SEPARATOR)
b_createView = false;
}
if (b_createView) {
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
holder = new ViewHolder();
holder.binding = DataBindingUtil.inflate(inflater, R.layout.audio_browser_separator, parent, false);
v = holder.binding.getRoot();
holder.viewType = VIEW_SEPARATOR;
v.setTag(holder);
}
ListItem item = getItem(position);
holder.binding.setVariable(BR.item, item);
holder.binding.executePendingBindings();
return v;
}
static class ViewHolder {
int viewType;
ViewDataBinding binding;
}
@Override
public int getCount() {
return mItems.size();
}
@Override
public ListItem getItem(int position) {
return mItems.get(position);
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public int getItemViewType(int position) {
int viewType = VIEW_MEDIA;
if (mItems.get(position).mIsSeparator)
viewType = VIEW_SEPARATOR;
return viewType;
}
@Override
public int getViewTypeCount() {
return 2;
}
@Override
public boolean hasStableIds() {
return false;
}
@Override
public boolean isEmpty() {
return getCount() == 0;
}
@Override
public boolean areAllItemsEnabled() {
return false;
}
@Override
public boolean isEnabled(int position) {
return position < mItems.size() && mItems.get(position).mMediaList.size() > 0;
}
@Override
public int getPositionForSection(int sectionIndex) {
int index;
if(mSections.size() == 0)
index = 0;
else if(sectionIndex >= mSections.size())
index = mSections.size() - 1;
else if(sectionIndex <= 0)
index = 0;
else
index = sectionIndex;
return mSections.keyAt(index);
}
@Override
public int getSectionForPosition(int position) {
for(int i = 0; i < mSections.size(); i++) {
if(position > mSections.keyAt(i))
return i;
}
return mSections.size()-1; // default to last section
}
@Override
public Object[] getSections() {
ArrayList<String> sections = new ArrayList<String>();
for(int i = 0; i < mSections.size(); i++) {
sections.add(mSections.valueAt(i));
}
return sections.toArray();
}
public ArrayList<MediaWrapper> getMedias(int position) {
// Return all the media of a list item list.
ArrayList<MediaWrapper> mediaList = new ArrayList<MediaWrapper>();
ListItem item = mItems.get(position);
if (!item.mIsSeparator || !item.mMediaList.isEmpty())
mediaList.addAll(item.mMediaList);
return mediaList;
}
public ArrayList<MediaWrapper> getMedias(int position, boolean sortByTrackNumber) {
ArrayList<MediaWrapper> mediaList = getMedias(position);
if (isEnabled(position)) {
if (sortByTrackNumber)
Collections.sort(mediaList, MediaComparators.byTrackNumber);
}
return mediaList;
}
public String getTitle(int position) {
return getItem(position).mTitle;
}
// public ArrayList<String> getLocations(int position) {
// return getLocations(position, false);
// }
//
// public ArrayList<String> getLocations(int position, boolean sortByTrackNumber) {
// // Return all the media locations of a list item list.
// ArrayList<String> locations = new ArrayList<String>();
// if (isEnabled(position)) {
// ArrayList<MediaWrapper> mediaList = mItems.get(position).mMediaList;
// if (sortByTrackNumber)
// Collections.sort(mediaList, MediaComparators.byTrackNumber);
// for (int i = 0; i < mediaList.size(); ++i)
// locations.add(mediaList.get(i).getLocation());
// }
// return locations;
// }
/**
* Returns a single list containing all media, along with the position of
* the first media in 'position' in the _new_ single list.
*
* @param outputList The list to be written to.
* @param position Position to retrieve in to _this_ adapter.
* @return The position of 'position' in the new single list, or 0 if not found.
*/
public int getListWithPosition(List<MediaWrapper> outputList, int position) {
int outputPosition = 0;
outputList.clear();
for(int i = 0; i < mItems.size(); i++) {
if(!mItems.get(i).mIsSeparator) {
if(position == i && !mItems.get(i).mMediaList.isEmpty())
outputPosition = outputList.size();
for(MediaWrapper mediaWrapper : mItems.get(i).mMediaList) {
outputList.add(mediaWrapper);
}
}
}
return outputPosition;
}
private boolean isMediaItemAboveASeparator(int position) {
// Test if a media item if above or not a separator.
if (mItems.get(position).mIsSeparator)
throw new IllegalArgumentException("Tested item must be a media item and not a separator.");
//consider end of list as a separator. Nicer to display
return (position == mItems.size() - 1 || mItems.get(position + 1).mIsSeparator);
}
public interface ContextPopupMenuListener {
void onPopupMenu(View anchor, final int position);
}
void setContextPopupMenuListener(ContextPopupMenuListener l) {
mContextPopupMenuListener = l;
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
if (observer != null)
super.unregisterDataSetObserver(observer);
}
private Comparator<ListItem> mItemsComparator = new Comparator<ListItem>() {
@Override
public int compare(ListItem lhs, ListItem rhs) {
return String.CASE_INSENSITIVE_ORDER.compare(lhs.mTitle, rhs.mTitle);
}
};
@Override
public void onMoreClick(View v) {
if (mContextPopupMenuListener != null)
mContextPopupMenuListener.onPopupMenu(v, ((Integer)v.getTag()).intValue());
}
private static class AudioCoverFetcher extends AsyncImageLoader.CoverFetcher {
final ArrayList<MediaWrapper> list;
final Context context;
AudioCoverFetcher(ViewDataBinding binding, Context context, ArrayList<MediaWrapper> list) {
super(binding);
this.context = context;
this.list = list;
}
@Override
public Bitmap getImage() {
return AudioUtil.getCover(context, list, 64);
}
@Override
public void updateBindImage(Bitmap bitmap, View target) {
if (bitmap != null && (bitmap.getWidth() != 1 && bitmap.getHeight() != 1)) {
binding.setVariable(BR.scaleType, ImageView.ScaleType.FIT_CENTER);
binding.setVariable(BR.cover, new BitmapDrawable(VLCApplication.getAppResources(), bitmap));
}
}
}
}