/*
* Copyright (C) 2014 Fastboot Mobile, LLC.
*
* 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 3 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, see <http://www.gnu.org/licenses>.
*/
package com.fastbootmobile.encore.app.adapters;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.RemoteException;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.TextView;
import com.fastbootmobile.encore.app.AlbumActivity;
import com.fastbootmobile.encore.app.ArtistActivity;
import com.fastbootmobile.encore.app.R;
import com.fastbootmobile.encore.app.fragments.PlaylistChooserFragment;
import com.fastbootmobile.encore.app.fragments.SearchFragment;
import com.fastbootmobile.encore.app.ui.AlbumArtImageView;
import com.fastbootmobile.encore.framework.PlaybackProxy;
import com.fastbootmobile.encore.framework.PluginsLookup;
import com.fastbootmobile.encore.art.RecyclingBitmapDrawable;
import com.fastbootmobile.encore.framework.Suggestor;
import com.fastbootmobile.encore.model.Album;
import com.fastbootmobile.encore.model.Artist;
import com.fastbootmobile.encore.model.BoundEntity;
import com.fastbootmobile.encore.model.Playlist;
import com.fastbootmobile.encore.model.SearchResult;
import com.fastbootmobile.encore.model.Song;
import com.fastbootmobile.encore.providers.IMusicProvider;
import com.fastbootmobile.encore.providers.ProviderAggregator;
import com.fastbootmobile.encore.providers.ProviderConnection;
import com.fastbootmobile.encore.providers.ProviderIdentifier;
import com.fastbootmobile.encore.utils.Utils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Adapter that displays Search results in an Expandable ListView
*/
public class SearchAdapter extends BaseExpandableListAdapter {
private static final String TAG = "SearchAdapter";
public final static int ARTIST = 0;
public final static int ALBUM = 1;
public final static int SONG = 2;
public final static int PLAYLIST = 3;
public final static int COUNT = 4;
private int mMaxCount[] = new int[COUNT];
private List<SearchResult> mSearchResults;
private List<SearchEntry> mAllSongs;
private List<SearchEntry> mAllArtists;
private List<SearchEntry> mAllPlaylists;
private List<SearchEntry> mAllAlbums;
private List<SearchEntry> mSortedSongs;
private List<SearchEntry> mSortedArtists;
private List<SearchEntry> mSortedPlaylists;
private List<SearchEntry> mSortedAlbums;
private final View.OnClickListener mOverflowArtistClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
ViewHolder vh = (ViewHolder) v.getTag();
Artist artist = (Artist) vh.content;
showArtistOverflow(v.getContext(), v, artist);
}
};
private final View.OnClickListener mOverflowAlbumClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
ViewHolder vh = (ViewHolder) v.getTag();
Album album = (Album) vh.content;
showAlbumOverflow(v.getContext(), v, album);
}
};
private final View.OnClickListener mOverflowPlaylistClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
ViewHolder vh = (ViewHolder) v.getTag();
Playlist playlist = (Playlist) vh.content;
showPlaylistOverflow(v.getContext(), v, playlist);
}
};
private final View.OnClickListener mOverflowSongClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
ViewHolder vh = (ViewHolder) v.getTag();
Song song = (Song) vh.content;
showSongOverflow(v.getContext(), v, song);
}
};
/**
* Class representing search entries
*/
public class SearchEntry {
SearchEntry(String ref, ProviderIdentifier id) {
this.ref = ref;
this.identifier = id;
}
public String ref;
public ProviderIdentifier identifier;
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
} else if (o instanceof String) {
return o.equals(ref);
} else if (o instanceof BoundEntity) {
return ref.equals(((BoundEntity) o).getRef());
} else if (o instanceof SearchEntry) {
return ((SearchEntry) o).ref.equals(ref) &&
((SearchEntry) o).identifier.equals(identifier);
}
return false;
}
}
/**
* Default constructor
*/
public SearchAdapter() {
mSearchResults = new ArrayList<>();
mAllSongs = new ArrayList<>();
mAllArtists = new ArrayList<>();
mAllPlaylists = new ArrayList<>();
mAllAlbums = new ArrayList<>();
mSortedSongs = new ArrayList<>();
mSortedArtists = new ArrayList<>();
mSortedPlaylists = new ArrayList<>();
mSortedAlbums = new ArrayList<>();
for (int i = 0; i < COUNT; ++i) {
mMaxCount[i] = 5;
}
}
/**
* Clear all the results from the adapter
*/
public void clear() {
mSearchResults.clear();
mAllSongs.clear();
mAllArtists.clear();
mAllPlaylists.clear();
mAllAlbums.clear();
mSortedSongs.clear();
mSortedArtists.clear();
mSortedPlaylists.clear();
mSortedAlbums.clear();
}
/**
* {@inheritDoc}
*/
@Override
public int getGroupCount() {
if (mSearchResults.size() > 0) {
return COUNT;
} else {
return 0;
}
}
public void setGroupMaxCount(int group, int count) {
mMaxCount[group] = count;
computeResultsList();
notifyDataSetChanged();
}
public int getGroupMaxCount(int group) {
return mMaxCount[group];
}
/**
* Add the results to the current adapter's results
*
* @param searchResults The results to append
*/
public void appendResults(List<SearchResult> searchResults) {
mSearchResults.addAll(searchResults);
for (SearchResult searchResult : searchResults) {
final ProviderIdentifier id = searchResult.getIdentifier();
final List<String> songs = searchResult.getSongsList();
final List<String> artists = searchResult.getArtistList();
final List<String> playlists = searchResult.getPlaylistList();
final List<String> albums = searchResult.getAlbumsList();
for (String song : songs) {
SearchEntry entry = new SearchEntry(song, id);
if (!mAllSongs.contains(entry)) {
mAllSongs.add(entry);
}
}
for (String artist : artists) {
SearchEntry entry = new SearchEntry(artist, id);
if (!mAllArtists.contains(entry)) {
mAllArtists.add(entry);
}
}
for (String playlist : playlists) {
SearchEntry entry = new SearchEntry(playlist, id);
if (!mAllPlaylists.contains(entry)) {
mAllPlaylists.add(entry);
}
}
for (String album : albums) {
SearchEntry entry = new SearchEntry(album, id);
if (!mAllAlbums.contains(entry)) {
mAllAlbums.add(entry);
}
}
}
computeResultsList();
}
private void computeResultsList() {
mSortedArtists.clear();
mSortedAlbums.clear();
mSortedPlaylists.clear();
mSortedSongs.clear();
Map<ProviderIdentifier, Integer> songsPerProvider = new HashMap<>();
Map<ProviderIdentifier, Integer> albumsPerProvider = new HashMap<>();
Map<ProviderIdentifier, Integer> playlistsPerProvider = new HashMap<>();
Map<ProviderIdentifier, Integer> artistsPerProvider = new HashMap<>();
for (SearchEntry song : mAllSongs) {
Integer providerCountInteger = songsPerProvider.get(song.identifier);
int providerCount = providerCountInteger == null ? 0 : providerCountInteger;
if (providerCount < mMaxCount[SONG]) {
mSortedSongs.add(song);
songsPerProvider.put(song.identifier, providerCount + 1);
}
}
for (SearchEntry album : mAllAlbums) {
Integer providerCountInteger = albumsPerProvider.get(album.identifier);
int providerCount = providerCountInteger == null ? 0 : providerCountInteger;
if (providerCount < mMaxCount[ALBUM]) {
mSortedAlbums.add(album);
albumsPerProvider.put(album.identifier, providerCount + 1);
}
}
for (SearchEntry artist : mAllArtists) {
Integer providerCountInteger = artistsPerProvider.get(artist.identifier);
int providerCount = providerCountInteger == null ? 0 : providerCountInteger;
if (providerCount < mMaxCount[ARTIST]) {
mSortedArtists.add(artist);
artistsPerProvider.put(artist.identifier, providerCount + 1);
}
}
for (SearchEntry playlist : mAllPlaylists) {
Integer providerCountInteger = playlistsPerProvider.get(playlist.identifier);
int providerCount = providerCountInteger == null ? 0 : providerCountInteger;
if (providerCount < mMaxCount[PLAYLIST]) {
mSortedPlaylists.add(playlist);
playlistsPerProvider.put(playlist.identifier, providerCount + 1);
}
}
}
/**
* Returns whether or not the search results contains the provided entity
*
* @param ent The entity to check
* @return True if the search results contains the entity, false otherwise
*/
public boolean contains(BoundEntity ent) {
SearchEntry compare = new SearchEntry(ent.getRef(), ent.getProvider());
if (ent instanceof Song) {
return mAllSongs.contains(compare);
} else if (ent instanceof Artist) {
return mAllArtists.contains(compare);
} else if (ent instanceof Album) {
return mAllAlbums.contains(compare);
} else if (ent instanceof Playlist) {
return mAllPlaylists.contains(compare);
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public int getChildrenCount(int i) {
List children = getGroup(i);
if (children != null) {
return children.size() + 1;
} else {
return 0;
}
}
/**
* {@inheritDoc}
*/
@Override
public List<SearchEntry> getGroup(int i) {
switch (i) {
case ARTIST:
return mSortedArtists;
case ALBUM:
return mSortedAlbums;
case SONG:
return mSortedSongs;
case PLAYLIST:
return mSortedPlaylists;
default:
return null;
}
}
/**
* {@inheritDoc}
*/
@Override
public SearchEntry getChild(int i, int i2) {
try {
return getGroup(i).get(i2);
} catch (IndexOutOfBoundsException e) {
// TODO: Better heuristic
// This is the "More" item in search
return new SearchEntry(SearchFragment.KEY_SPECIAL_MORE, null);
}
}
/**
* {@inheritDoc}
*/
@Override
public long getGroupId(int i) {
return i;
}
/**
* {@inheritDoc}
*/
@Override
public long getChildId(int i, int i2) {
if (i < getGroupCount() && i2 < getChildrenCount(i)) {
try {
return (long) getChild(i, i2).hashCode();
} catch (IndexOutOfBoundsException e) {
// It's the 'more' item
// TODO: Better heuristic for that
return -2;
}
} else {
return -1;
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasStableIds() {
return true;
}
/**
* {@inheritDoc}
*/
@Override
public View getGroupView(int i, boolean b, View view, ViewGroup parent) {
final Context ctx = parent.getContext();
assert ctx != null;
GroupViewHolder holder;
if (view == null) {
LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(R.layout.item_group_separator, parent, false);
holder = new GroupViewHolder();
holder.tvSearchSeparator = (TextView) view.findViewById(R.id.tv_search_separator);
view.setTag(holder);
} else {
holder = (GroupViewHolder) view.getTag();
}
int title;
switch (i) {
case ARTIST:
title = R.string.tab_artists;
break;
case ALBUM:
title = R.string.albums;
break;
case SONG:
title = R.string.songs;
break;
case PLAYLIST:
title = R.string.tab_playlists;
break;
default:
throw new RuntimeException("Unknown group index: " + i);
}
holder.tvSearchSeparator.setText(title);
if (getChildrenCount(i) == 0) {
view.setVisibility(View.GONE);
} else {
view.setVisibility(View.VISIBLE);
}
return view;
}
/**
* {@inheritDoc}
*/
@Override
public View getChildView(int i, int i2, boolean b, View root, ViewGroup parent) {
final Context ctx = parent.getContext();
assert ctx != null;
if (root == null) {
LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
root = inflater.inflate(R.layout.item_search_element, parent, false);
ViewHolder holder = new ViewHolder();
holder.albumArtImageView = (AlbumArtImageView) root.findViewById(R.id.ivCover);
holder.tvTitle = (TextView) root.findViewById(R.id.tvTitle);
holder.tvSubtitle = (TextView) root.findViewById(R.id.tvSubTitle);
holder.divider = (TextView) root.findViewById(R.id.divider);
holder.ivSource = (ImageView) root.findViewById(R.id.ivSource);
holder.ivOverflow = (ImageView) root.findViewById(R.id.ivOverflow);
holder.vRoot = root;
holder.ivOverflow.setTag(holder);
root.setTag(holder);
}
final ViewHolder tag = (ViewHolder) root.getTag();
if (i2 == getChildrenCount(i) - 1) {
tag.albumArtImageView.setVisibility(View.INVISIBLE);
tag.ivSource.setVisibility(View.GONE);
if (getChildrenCount(i) == 1) {
tag.tvTitle.setText(R.string.search_no_results);
} else {
tag.tvTitle.setText(R.string.more);
}
tag.tvSubtitle.setText(null);
tag.ivOverflow.setVisibility(View.GONE);
tag.content = null;
} else {
tag.albumArtImageView.setVisibility(View.VISIBLE);
tag.ivSource.setVisibility(View.VISIBLE);
tag.ivOverflow.setVisibility(View.VISIBLE);
switch (i) {
case ARTIST:
updateArtistTag(i2, tag);
break;
case ALBUM:
updateAlbumTag(i2, tag);
break;
case SONG:
updateSongTag(i2, tag);
break;
case PLAYLIST:
updatePlaylistTag(i2, tag);
break;
default:
Log.e(TAG, "Unknown group " + i);
break;
}
}
return root;
}
/**
* Updates the tag fields considering the entry is a song
*
* @param i The item index
* @param tag The tag of the view
*/
private void updateSongTag(int i, ViewHolder tag) {
final SearchEntry entry = mSortedSongs.get(i);
final ProviderAggregator aggregator = ProviderAggregator.getDefault();
Song song = aggregator.retrieveSong(entry.ref, entry.identifier);
if (song != null && song.equals(tag.content)) {
// We're already displaying it
return;
}
if (song != null && song.isLoaded()) {
tag.tvTitle.setText(song.getTitle());
Artist artist = aggregator.retrieveArtist(song.getArtist(), song.getProvider());
if (artist != null) {
tag.tvSubtitle.setText(artist.getName());
} else {
tag.tvSubtitle.setText(null);
}
tag.albumArtImageView.loadArtForSong(song);
tag.sourceLogo = PluginsLookup.getDefault().getCachedLogo(tag.vRoot.getResources(), song);
tag.ivSource.setImageDrawable(tag.sourceLogo);
tag.content = song;
tag.ivOverflow.setOnClickListener(mOverflowSongClickListener);
} else {
tag.tvTitle.setText(R.string.loading);
tag.tvSubtitle.setText(null);
tag.ivSource.setImageDrawable(null);
tag.albumArtImageView.setDefaultArt();
tag.ivOverflow.setOnClickListener(null);
}
}
/**
* Updates the tag fields considering the entry is an artist
*
* @param i The item index
* @param tag The tag of the view
*/
private void updateArtistTag(int i, ViewHolder tag) {
final SearchEntry entry = mSortedArtists.get(i);
final ProviderAggregator aggregator = ProviderAggregator.getDefault();
Artist artist = aggregator.retrieveArtist(entry.ref, entry.identifier);
if (artist != null && artist.equals(tag.content)) {
// We're already displaying it
return;
}
if (artist != null && (artist.isLoaded() || artist.getName() != null)) {
tag.tvTitle.setText(artist.getName());
tag.tvSubtitle.setText(null);
tag.albumArtImageView.loadArtForArtist(artist);
tag.content = artist;
tag.sourceLogo = PluginsLookup.getDefault().getCachedLogo(tag.vRoot.getResources(), artist);
tag.ivSource.setImageDrawable(tag.sourceLogo);
tag.ivOverflow.setOnClickListener(mOverflowArtistClickListener);
} else {
tag.tvTitle.setText(R.string.loading);
tag.tvSubtitle.setText(null);
tag.ivSource.setImageDrawable(null);
tag.albumArtImageView.setDefaultArt();
tag.ivOverflow.setOnClickListener(null);
}
}
/**
* Updates the tag fields considering the entry is an album
*
* @param i The item index
* @param tag The tag of the view
*/
private void updateAlbumTag(int i, ViewHolder tag) {
final SearchEntry entry = mSortedAlbums.get(i);
ProviderAggregator aggregator = ProviderAggregator.getDefault();
Album album = aggregator.retrieveAlbum(entry.ref, entry.identifier);
if (album != null && album.equals(tag.content)) {
// We're already displaying it
return;
}
if (album != null) {
tag.tvTitle.setText(album.getName());
if (album.getYear() > 0) {
tag.tvSubtitle.setText("" + album.getYear());
} else {
tag.tvSubtitle.setText("");
}
tag.albumArtImageView.loadArtForAlbum(album);
tag.sourceLogo = PluginsLookup.getDefault().getCachedLogo(tag.vRoot.getResources(), album);
tag.ivSource.setImageDrawable(tag.sourceLogo);
tag.content = album;
tag.ivOverflow.setOnClickListener(mOverflowAlbumClickListener);
// Ensure album contents are fetched so that we can do something with it
ProviderConnection conn = PluginsLookup.getDefault().getProvider(album.getProvider());
if (conn != null) {
IMusicProvider binder = conn.getBinder();
if (binder != null) {
try {
binder.fetchAlbumTracks(album.getRef());
} catch (RemoteException e) {
Log.e(TAG, "Cannot fetch album tracks");
}
}
}
} else {
tag.tvTitle.setText(R.string.loading);
tag.tvSubtitle.setText(null);
tag.albumArtImageView.setDefaultArt();
tag.ivOverflow.setOnClickListener(null);
}
}
/**
* Updates the tag fields considering the entry is a playlist
*
* @param i The item index
* @param tag The tag of the view
*/
private void updatePlaylistTag(int i, ViewHolder tag) {
final SearchEntry entry = mSortedPlaylists.get(i);
final Playlist playlist = ProviderAggregator.getDefault().retrievePlaylist(entry.ref, entry.identifier);
final Resources res = tag.vRoot.getResources();
if (playlist != null && playlist.equals(tag.content)) {
// We're already displaying it
return;
}
if (playlist != null && (playlist.isLoaded() || playlist.getName() != null)) {
tag.tvTitle.setText(playlist.getName());
tag.tvSubtitle.setText(res.getQuantityString(R.plurals.xx_songs, playlist.getSongsCount()));
tag.content = playlist;
tag.sourceLogo = PluginsLookup.getDefault().getCachedLogo(tag.vRoot.getResources(), playlist);
tag.ivSource.setImageDrawable(tag.sourceLogo);
tag.ivOverflow.setOnClickListener(mOverflowPlaylistClickListener);
} else {
tag.tvTitle.setText(R.string.loading);
tag.tvSubtitle.setText(null);
tag.ivSource.setImageDrawable(null);
tag.albumArtImageView.setDefaultArt();
tag.ivOverflow.setOnClickListener(null);
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean isChildSelectable(int i, int i2) {
return mSearchResults.size() > 0 && getGroup(i) != null;
}
private void showArtistOverflow(final Context context, View parent, final Artist artist) {
PopupMenu popupMenu = new PopupMenu(context, parent);
popupMenu.inflate(R.menu.search_res_artist);
popupMenu.show();
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
switch (menuItem.getItemId()) {
case R.id.menu_play_now:
List<Song> radio = Suggestor.getInstance().buildArtistRadio(artist);
PlaybackProxy.clearQueue();
for (Song song : radio) {
PlaybackProxy.queueSong(song, false);
}
PlaybackProxy.playAtIndex(0);
break;
default:
return false;
}
return true;
}
});
}
private void showAlbumOverflow(final Context context, View parent, final Album album) {
PopupMenu popupMenu = new PopupMenu(context, parent);
popupMenu.inflate(R.menu.search_res_album);
popupMenu.show();
final String artist = Utils.getMainArtist(album);
if (artist == null) {
// No artist could be found for this album, don't show the entry
popupMenu.getMenu().removeItem(R.id.menu_open_artist_page);
}
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
switch (menuItem.getItemId()) {
case R.id.menu_play_now:
PlaybackProxy.playAlbum(album);
break;
case R.id.menu_add_to_queue:
PlaybackProxy.queueAlbum(album, false);
break;
case R.id.menu_open_artist_page:
if (artist != null) {
Intent intent = ArtistActivity.craftIntent(context, null, artist,
album.getProvider(), 0xFF333333);
context.startActivity(intent);
}
break;
case R.id.menu_add_to_playlist:
PlaylistChooserFragment fragment = PlaylistChooserFragment.newInstance(album);
fragment.show(((FragmentActivity) context).getSupportFragmentManager(),
album.getRef());
break;
default:
return false;
}
return true;
}
});
}
private void showSongOverflow(final Context context, View parent, final Song song) {
PopupMenu popupMenu = new PopupMenu(context, parent);
popupMenu.inflate(R.menu.search_res_song);
popupMenu.show();
if (song.getArtist() == null) {
// No attached artist, don't show the menu entry
popupMenu.getMenu().removeItem(R.id.menu_open_artist_page);
}
if (song.getAlbum() == null) {
// No attached album, don't show the menu entry
popupMenu.getMenu().removeItem(R.id.menu_open_album_page);
}
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
Intent intent;
switch (menuItem.getItemId()) {
case R.id.menu_play_now:
PlaybackProxy.playSong(song);
break;
case R.id.menu_add_to_queue:
PlaybackProxy.queueSong(song, false);
break;
case R.id.menu_open_artist_page:
if (song.getArtist() != null) {
intent = ArtistActivity.craftIntent(context, null, song.getArtist(),
song.getProvider(), 0xFF333333);
context.startActivity(intent);
}
break;
case R.id.menu_open_album_page:
if (song.getAlbum() != null) {
intent = AlbumActivity.craftIntent(context, null, song.getAlbum(),
song.getProvider(), 0xFF333333);
context.startActivity(intent);
}
break;
case R.id.menu_add_to_playlist:
PlaylistChooserFragment fragment = PlaylistChooserFragment.newInstance(song);
fragment.show(((FragmentActivity) context).getSupportFragmentManager(),
song.getRef());
break;
default:
return false;
}
return true;
}
});
}
private void showPlaylistOverflow(final Context context, View parent, final Playlist playlist) {
PopupMenu popupMenu = new PopupMenu(context, parent);
popupMenu.inflate(R.menu.search_res_playlist);
popupMenu.show();
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
switch (menuItem.getItemId()) {
case R.id.menu_play_now:
PlaybackProxy.playPlaylist(playlist);
break;
case R.id.menu_add_to_queue:
PlaybackProxy.queuePlaylist(playlist, false);
break;
case R.id.menu_add_to_playlist:
PlaylistChooserFragment fragment = PlaylistChooserFragment.newInstance(playlist);
fragment.show(((FragmentActivity) context).getSupportFragmentManager(),
playlist.getRef());
break;
default:
return false;
}
return true;
}
});
}
/**
* ViewHolder for list items
*/
public static class ViewHolder {
public AlbumArtImageView albumArtImageView;
public TextView tvTitle;
public TextView tvSubtitle;
public Object content;
public TextView divider;
public ImageView ivSource;
public View vRoot;
public RecyclingBitmapDrawable sourceLogo;
public ImageView ivOverflow;
}
/**
* ViewHolder for group headers
*/
private static class GroupViewHolder {
public TextView tvSearchSeparator;
}
}