/* * Copyright 2008-2013, ETH Zürich, Samuel Welten, Michael Kuhn, Tobias Langner, * Sandro Affentranger, Lukas Bossard, Michael Grob, Rahul Jain, * Dominic Langenegger, Sonia Mayor Alonso, Roger Odermatt, Tobias Schlueter, * Yannick Stucki, Sebastian Wendland, Samuel Zehnder, Samuel Zihlmann, * Samuel Zweifel * * This file is part of Jukefox. * * Jukefox 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 any later version. Jukefox 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 * Jukefox. If not, see <http://www.gnu.org/licenses/>. */ package ch.ethz.dcg.pancho3.tablet.model; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map.Entry; import java.util.TreeSet; import android.database.Cursor; import android.graphics.Bitmap; import android.os.AsyncTask; import ch.ethz.dcg.jukefox.commons.DataUnavailableException; import ch.ethz.dcg.jukefox.commons.utils.Log; import ch.ethz.dcg.jukefox.commons.utils.MathUtils; import ch.ethz.dcg.jukefox.commons.utils.Pair; import ch.ethz.dcg.jukefox.commons.utils.kdtree.KdTreePoint; import ch.ethz.dcg.jukefox.model.AndroidCollectionModelManager; import ch.ethz.dcg.jukefox.model.collection.BaseAlbum; import ch.ethz.dcg.jukefox.model.collection.BaseArtist; import ch.ethz.dcg.jukefox.model.collection.BaseSong; import ch.ethz.dcg.jukefox.model.collection.BaseTag; import ch.ethz.dcg.jukefox.model.collection.CompleteArtist; import ch.ethz.dcg.jukefox.model.collection.CompleteTag; import ch.ethz.dcg.jukefox.model.collection.Genre; import ch.ethz.dcg.jukefox.model.collection.IBaseListItem; import ch.ethz.dcg.jukefox.model.collection.ListAlbum; import ch.ethz.dcg.jukefox.model.collection.MapAlbum; import ch.ethz.dcg.jukefox.model.collection.PlaylistSong; import ch.ethz.dcg.jukefox.model.collection.PlaylistSong.SongSource; import ch.ethz.dcg.jukefox.model.commons.NoAlbumArtException; import ch.ethz.dcg.jukefox.model.providers.AlbumProvider; import ch.ethz.dcg.pancho3.view.commons.BitmapReflection; /** * Class to fetch data asynchronously. Each fetch method expects a listener for * callback. */ public class DataFetcher { private static final String TAG = DataFetcher.class.getSimpleName(); private final AndroidCollectionModelManager model; /** * The constructor sets the model reference needed to obtain all the data. */ public DataFetcher(AndroidCollectionModelManager model) { this.model = model; } /** * The universal listener for all the callbacks performed by DataFetcher. * * @param <T> * The type of the value fetched. */ public static interface OnDataFetchedListener<T> { void onDataFetched(T data); } // Each method has its own private class to perform the asynchronous data // fetching. // Those classes all subclass this one. private abstract class Fetcher<Input, Result> extends AsyncTask<Input, Void, Result> { private final OnDataFetchedListener<Result> listener; public Fetcher(OnDataFetchedListener<Result> listener) { this.listener = listener; } // This happens on the UI thread. @Override protected void onPostExecute(Result result) { if (result != null) { listener.onDataFetched(result); } } } // Fetching related albums. /** * Returns a list of related albums for a given artist. Each album comes * with a float, which is the negative distance of this album to the closest * artist's album which is was similar to. * * The album/float pairs come in descending order by the floats, i.e. the * album with the shortest distance comes first (since the values are * negative). */ public void fetchRelatedAlbums(BaseArtist artist, OnDataFetchedListener<List<Pair<MapAlbum, Float>>> listener) { new RelatedAlbumFetcher(listener).execute(artist); } private class RelatedAlbumFetcher extends Fetcher<BaseArtist, List<Pair<MapAlbum, Float>>> { public RelatedAlbumFetcher(OnDataFetchedListener<List<Pair<MapAlbum, Float>>> listener) { super(listener); } @Override protected List<Pair<MapAlbum, Float>> doInBackground(BaseArtist... artist) { List<ListAlbum> albums = model.getAlbumProvider().getAllListAlbums(artist[0]); HashSet<ListAlbum> albumSet = new HashSet<ListAlbum>(albums); HashMap<MapAlbum, Float> allAlbums = new HashMap<MapAlbum, Float>(); try { for (ListAlbum album : albums) { for (Pair<MapAlbum, Float> pair : model.getAlbumProvider().getSimilarAlbums(album, 20)) { Float value = allAlbums.get(pair.first); if (value == null) { value = -pair.second; } else { value = Math.max(value, -pair.second); } allAlbums.put(pair.first, value); } } } catch (DataUnavailableException e) { } List<Pair<MapAlbum, Float>> result = new ArrayList<Pair<MapAlbum, Float>>(); for (Entry<MapAlbum, Float> entry : allAlbums.entrySet()) { if (!albumSet.contains(entry.getKey())) { result.add(new Pair<MapAlbum, Float>(entry.getKey(), entry.getValue())); } } Collections.sort(result, new WeightedItemComparator<Float>()); return result; } } public void fetchRelatedAlbums2(BaseArtist artist, OnDataFetchedListener<List<MapAlbum>> listener) { new RelatedAlbumFetcher2(listener).execute(artist); } private class RelatedAlbumFetcher2 extends Fetcher<BaseArtist, List<MapAlbum>> { public RelatedAlbumFetcher2(OnDataFetchedListener<List<MapAlbum>> listener) { super(listener); } @Override protected ArrayList<MapAlbum> doInBackground(BaseArtist... artist) { TreeSet<MapAlbum> set = new TreeSet<MapAlbum>(); try { // TODO: provide functionality lower in db AlbumProvider provider = model.getAlbumProvider(); CompleteArtist completeArtist = model.getArtistProvider().getCompleteArtist(artist[0]); for (Pair<BaseSong<BaseArtist, BaseAlbum>, KdTreePoint<Integer>> pair : model.getSongProvider() .getClosestSongsToPosition2(completeArtist.getCoords(), 50)) { set.add(provider.getMapAlbum(pair.first)); } } catch (DataUnavailableException e) { } ArrayList<MapAlbum> list = new ArrayList<MapAlbum>(); for (MapAlbum album : set) { if (!album.getArtists().contains(artist[0])) { list.add(album); } } return list; } } // Fetching albums /** * Returns a list of all albums. */ public void fetchAllAlbums(OnDataFetchedListener<List<MapAlbum>> listener) { new FetchAlbums(listener).execute(); } /** * Returns a list of the albums of the given artist. */ public void fetchAlbumsOfArtist(BaseArtist artist, OnDataFetchedListener<List<MapAlbum>> listener) { new FetchAlbums(listener).execute(artist); } private class FetchAlbums extends Fetcher<BaseArtist, List<MapAlbum>> { public FetchAlbums(OnDataFetchedListener<List<MapAlbum>> listener) { super(listener); } @Override protected List<MapAlbum> doInBackground(BaseArtist... artist) { AlbumProvider provider = model.getAlbumProvider(); List<MapAlbum> list; try { if (artist.length == 0) { list = provider.getAllMapAlbums(); } else { // TODO: Provide this functionality at a lower level in the data base. list = new ArrayList<MapAlbum>(); List<ListAlbum> listAlbums = provider.getAllListAlbums(artist[0]); for (ListAlbum album : listAlbums) { list.add(provider.getMapAlbum(album)); } } Collections.sort(list); } catch (DataUnavailableException e) { list = new ArrayList<MapAlbum>(); } return list; } } // Fetching songs public void fetchAllPlaylistSongs(final OnDataFetchedListener<List<PlaylistSong<BaseArtist, BaseAlbum>>> listener, final SongSource songSource) { new FetchSongs(new OnDataFetchedListener<List<BaseSong<BaseArtist, BaseAlbum>>>() { @Override public void onDataFetched(List<BaseSong<BaseArtist, BaseAlbum>> songs) { listener.onDataFetched(toPlaylistSongs(songs, songSource)); } }).execute(); } /** * Returns a list of all songs. */ public void fetchAllSongs(OnDataFetchedListener<List<BaseSong<BaseArtist, BaseAlbum>>> listener) { new FetchSongs(listener).execute(); } /** * Returns a list of the songs of a given artist. */ public void fetchSongsOfArtist(BaseArtist artist, OnDataFetchedListener<List<BaseSong<BaseArtist, BaseAlbum>>> listener) { new FetchSongs(listener).execute(artist); } public void fetchPlaylistSongsOfArtist(BaseArtist artist, final OnDataFetchedListener<List<PlaylistSong<BaseArtist, BaseAlbum>>> listener, final SongSource songSource) { new FetchSongs(new OnDataFetchedListener<List<BaseSong<BaseArtist, BaseAlbum>>>() { @Override public void onDataFetched(List<BaseSong<BaseArtist, BaseAlbum>> songs) { listener.onDataFetched(toPlaylistSongs(songs, songSource)); } }).execute(artist); } /** * Returns a list of all the songs of a given album. */ public void fetchSongsOfAlbum(ListAlbum album, OnDataFetchedListener<List<BaseSong<BaseArtist, BaseAlbum>>> listener) { new FetchSongs(listener).execute(album); } public void fetchPlaylistSongsOfAlbum(ListAlbum album, final OnDataFetchedListener<List<PlaylistSong<BaseArtist, BaseAlbum>>> listener, final SongSource songSource) { new FetchSongs(new OnDataFetchedListener<List<BaseSong<BaseArtist, BaseAlbum>>>() { @Override public void onDataFetched(List<BaseSong<BaseArtist, BaseAlbum>> songs) { listener.onDataFetched(toPlaylistSongs(songs, songSource)); } }).execute(album); } private class FetchSongs extends Fetcher<IBaseListItem, List<BaseSong<BaseArtist, BaseAlbum>>> { public FetchSongs(OnDataFetchedListener<List<BaseSong<BaseArtist, BaseAlbum>>> listener) { super(listener); } // This happens asynchronously. @Override protected List<BaseSong<BaseArtist, BaseAlbum>> doInBackground(IBaseListItem... restriction) { if (restriction.length == 0) { // Display all the songs. List<BaseSong<BaseArtist, BaseAlbum>> songs = model.getSongProvider().getAllBaseSongs(); Collections.sort(songs); // Sort alphabetically. return songs; } else if (restriction[0] instanceof BaseArtist) { BaseArtist artist = (BaseArtist) restriction[0]; // Display all the songs of the artist. List<BaseSong<BaseArtist, BaseAlbum>> songs = model.getSongProvider().getAllBaseSongs(artist); Collections.sort(songs); // Sort alphabetically. return songs; } else if (restriction[0] instanceof ListAlbum) { // Display all the songs of the album ListAlbum album = (ListAlbum) restriction[0]; try { // We don't sort here; the album already has a natural // order. return model.getAlbumProvider().getCompleteAlbum(album).getSongs(); } catch (DataUnavailableException e) { return new ArrayList<BaseSong<BaseArtist, BaseAlbum>>(); } } // We shouldn't end up down here. return null; } } public void fetchSongsOfAlbums(List<? extends ListAlbum> albums, OnDataFetchedListener<List<BaseSong<BaseArtist, BaseAlbum>>> listener) { new FetchSongsFromAlbums(listener).execute(albums); } public void fetchPlaylistSongsOfAlbums(List<? extends ListAlbum> albums, final OnDataFetchedListener<List<PlaylistSong<BaseArtist, BaseAlbum>>> listener, final SongSource songSource) { new FetchSongsFromAlbums(new OnDataFetchedListener<List<BaseSong<BaseArtist, BaseAlbum>>>() { @Override public void onDataFetched(List<BaseSong<BaseArtist, BaseAlbum>> songs) { listener.onDataFetched(toPlaylistSongs(songs, songSource)); } }).execute(albums); } private class FetchSongsFromAlbums extends Fetcher<List<? extends ListAlbum>, List<BaseSong<BaseArtist, BaseAlbum>>> { public FetchSongsFromAlbums( OnDataFetchedListener<List<BaseSong<BaseArtist, BaseAlbum>>> listener) { super(listener); } // This happens asynchronously. @Override protected List<BaseSong<BaseArtist, BaseAlbum>> doInBackground( List<? extends ListAlbum>... restriction) { List<BaseSong<BaseArtist, BaseAlbum>> list = new ArrayList<BaseSong<BaseArtist, BaseAlbum>>(); for (ListAlbum album : restriction[0]) { // We don't sort here; the album already has a natural // order. try { list.addAll(model.getAlbumProvider().getCompleteAlbum(album).getSongs()); } catch (DataUnavailableException e) { } } return list; } } // Fetching artists /** * Returns a list of all the artists. */ public void fetchAllArtists(OnDataFetchedListener<List<BaseArtist>> listener) { new ArtistFetcher(listener).execute(); } private class ArtistFetcher extends Fetcher<Void, List<BaseArtist>> { public ArtistFetcher(OnDataFetchedListener<List<BaseArtist>> listener) { super(listener); } // This happens asynchronously. @Override protected List<BaseArtist> doInBackground(Void... parameter) { List<BaseArtist> artists = model.getArtistProvider().getAllArtists(); Collections.sort(artists); // Sort artists alphabetically. return artists; } } // Fetching Genres /** * Returns a list of all the genres of a given artist where each genre comes * with an integer which TODO: what exactly? * * The genre/integer pairs come in descending order by the integer. */ public void fetchGenresForArtist(BaseArtist artist, OnDataFetchedListener<List<Pair<Genre, Integer>>> listener) { new GenreFetcher(listener).execute(artist); } private class GenreFetcher extends Fetcher<BaseArtist, List<Pair<Genre, Integer>>> { public GenreFetcher(OnDataFetchedListener<List<Pair<Genre, Integer>>> listener) { super(listener); } @Override protected List<Pair<Genre, Integer>> doInBackground(BaseArtist... artist) { try { List<Pair<Genre, Integer>> genres = model.getGenreProvider().getGenresForArtist(artist[0]); Collections.sort(genres, new WeightedItemComparator<Integer>()); return genres; } catch (DataUnavailableException e) { return new ArrayList<Pair<Genre, Integer>>(); } } } // Fetching Tags /** * Returns a list of all the tags for a given artist. Each tag comes with a * float which acts as a weight that this tag has for the given artist. * * The tag/float pairs come sorted in descending order by the float. */ public void fetchTagsForArtist(BaseArtist artist, OnDataFetchedListener<List<Pair<CompleteTag, Float>>> listener) { new TagFetcher(listener).execute(artist); } private class TagFetcher extends Fetcher<BaseArtist, List<Pair<CompleteTag, Float>>> { public TagFetcher(OnDataFetchedListener<List<Pair<CompleteTag, Float>>> listener) { super(listener); } @Override protected List<Pair<CompleteTag, Float>> doInBackground(BaseArtist... artist) { List<Pair<CompleteTag, Float>> tags = new ArrayList<Pair<CompleteTag, Float>>(); try { tags = model.getTagProvider() .getAllCompleteTags(model.getArtistProvider().getCompleteArtist(artist[0])); Collections.sort(tags, new WeightedItemComparator<Float>()); } catch (DataUnavailableException e) { return new ArrayList<Pair<CompleteTag, Float>>(); } return tags; } } public void fetchAlbumArt(OnDataFetchedListener<List<Pair<Bitmap, BaseAlbum>>> listener, boolean forceLowResolution, boolean addReflection, BaseAlbum... albums) { new AlbumArtFetcher(listener, forceLowResolution, addReflection).execute(albums); } private class AlbumArtFetcher extends Fetcher<BaseAlbum, List<Pair<Bitmap, BaseAlbum>>> { private final boolean forceLowResolution; private final boolean addReflection; public AlbumArtFetcher(OnDataFetchedListener<List<Pair<Bitmap, BaseAlbum>>> listener, boolean forceLowResolution, boolean addReflection) { super(listener); this.forceLowResolution = forceLowResolution; this.addReflection = addReflection; } @Override protected List<Pair<Bitmap, BaseAlbum>> doInBackground(BaseAlbum... albums) { try { List<Pair<Bitmap, BaseAlbum>> list = new ArrayList<Pair<Bitmap, BaseAlbum>>(); for (BaseAlbum album : albums) { if (album != null) { Bitmap albumArt = model.getAlbumArtProvider().getAlbumArt(album, forceLowResolution); if (addReflection) { albumArt = BitmapReflection.getReflection(albumArt); } list.add(new Pair<Bitmap, BaseAlbum>(albumArt, album)); } } return list; } catch (NoAlbumArtException e) { return null; } } } public void fetchQueriedSongs(String query, OnDataFetchedListener<Pair<String, Cursor>> listener) { new QueriedSongsFetcher(listener).execute(query); } private class QueriedSongsFetcher extends Fetcher<String, Pair<String, Cursor>> { public QueriedSongsFetcher(OnDataFetchedListener<Pair<String, Cursor>> listener) { super(listener); } @Override protected Pair<String, Cursor> doInBackground(String... query) { return new Pair<String, Cursor>(query[0], model.getCursorProvider().findTitleBySearchStringCursor( query[0], 100)); } } public void fetchAlbumsForTag(BaseTag tag, OnDataFetchedListener<List<MapAlbum>> listener) { new AlbumsForTagFetcher(listener).execute(tag); } private class AlbumsForTagFetcher extends Fetcher<BaseTag, List<MapAlbum>> { public AlbumsForTagFetcher(OnDataFetchedListener<List<MapAlbum>> listener) { super(listener); } @Override protected List<MapAlbum> doInBackground(BaseTag... tags) { CompleteTag tag = getCompleteTag(tags[0]); try { // TODO: provide this functionality lower in the db. AlbumProvider provider = model.getAlbumProvider(); TreeSet<MapAlbum> albums = new TreeSet<MapAlbum>(); for (PlaylistSong<BaseArtist, BaseAlbum> song : model.getTagPlaylistGenerator().generatePlaylist(tag, 30, 20)) { albums.add(provider.getMapAlbum(song)); } return new ArrayList<MapAlbum>(albums); } catch (DataUnavailableException e) { return null; } } } public void fetchRelatedTagsForTag(BaseTag tag, OnDataFetchedListener<List<Pair<CompleteTag, Float>>> listener) { new TagForTagFetcher(listener).execute(tag); } private class TagForTagFetcher extends Fetcher<BaseTag, List<Pair<CompleteTag, Float>>> { public TagForTagFetcher(OnDataFetchedListener<List<Pair<CompleteTag, Float>>> listener) { super(listener); } @Override protected List<Pair<CompleteTag, Float>> doInBackground(BaseTag... tags) { ArrayList<Pair<CompleteTag, Float>> tagList = new ArrayList<Pair<CompleteTag, Float>>(); try { Collection<CompleteTag> allTags = model.getTagProvider().getAllCompleteTags(200); CompleteTag tag = getCompleteTag(tags[0]); final float[] tagCoords = tag.getPlsaCoords(); final float length = MathUtils.norm2(tagCoords); for (CompleteTag currentTag : allTags) { float[] currentCoords = currentTag.getPlsaCoords(); if (currentCoords != null) { float value = (float) Math .acos(MathUtils.dotProduct(currentCoords, tagCoords) / (MathUtils.norm2(currentCoords) * length)); tagList.add(new Pair<CompleteTag, Float>(currentTag, value)); } } Collections.sort(tagList, new Comparator<Pair<CompleteTag, Float>>() { @Override public int compare(Pair<CompleteTag, Float> object1, Pair<CompleteTag, Float> object2) { return Float.compare(object1.second, object2.second); } }); } catch (DataUnavailableException e) { } return tagList.subList(0, Math.min(30, tagList.size())); } } private CompleteTag getCompleteTag(BaseTag tag) { final CompleteTag completeTag; if (tag instanceof CompleteTag) { completeTag = (CompleteTag) tag; } else { try { completeTag = model.getTagProvider().getCompleteTag(tag.getId()); } catch (DataUnavailableException e) { Log.w(TAG, e); return null; } } return completeTag; } /** * Comparator to sort pairs by descending order of the second element, a * weight. If this weight is equal it sorts by the ascending order of the * title of the first element (a base list item). * * @param <T> * the class of the weights. */ private static class WeightedItemComparator<T extends Comparable<T>> implements Comparator<Pair<? extends IBaseListItem, T>> { @Override public int compare(Pair<? extends IBaseListItem, T> item1, Pair<? extends IBaseListItem, T> item2) { if (item1.second == item2.second) { return item1.first.getTitle().compareTo(item2.first.getTitle()); } return item2.second.compareTo(item1.second); } } public AndroidCollectionModelManager getData() { return model; } private List<PlaylistSong<BaseArtist, BaseAlbum>> toPlaylistSongs(List<BaseSong<BaseArtist, BaseAlbum>> songs, SongSource songSource) { List<PlaylistSong<BaseArtist, BaseAlbum>> list = new ArrayList<PlaylistSong<BaseArtist, BaseAlbum>>(); for (BaseSong<BaseArtist, BaseAlbum> song : songs) { list.add(new PlaylistSong<BaseArtist, BaseAlbum>(song, songSource)); } return list; } }