package com.marverenic.music.model;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import com.marverenic.music.R;
import com.marverenic.music.data.store.MediaStoreUtil;
import com.marverenic.music.data.store.PlayCountStore;
import com.marverenic.music.utils.UriUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import static android.media.MediaMetadataRetriever.METADATA_KEY_ALBUM;
import static android.media.MediaMetadataRetriever.METADATA_KEY_ARTIST;
import static android.media.MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER;
import static android.media.MediaMetadataRetriever.METADATA_KEY_DATE;
import static android.media.MediaMetadataRetriever.METADATA_KEY_DURATION;
import static android.media.MediaMetadataRetriever.METADATA_KEY_TITLE;
import static com.marverenic.music.model.ModelUtil.compareLong;
import static com.marverenic.music.model.ModelUtil.compareTitle;
import static com.marverenic.music.model.ModelUtil.hashLong;
import static com.marverenic.music.model.ModelUtil.parseUnknown;
import static com.marverenic.music.model.ModelUtil.stringToInt;
import static com.marverenic.music.model.ModelUtil.stringToLong;
public class Song implements Parcelable, Comparable<Song> {
public static final Parcelable.Creator<Song> CREATOR = new Parcelable.Creator<Song>() {
public Song createFromParcel(Parcel in) {
return new Song(in);
}
public Song[] newArray(int size) {
return new Song[size];
}
};
protected String songName;
protected long songId;
protected String artistName;
protected String albumName;
protected long songDuration;
protected Uri location;
protected int year;
protected long dateAdded; // seconds since Jan 1, 1970
protected long albumId;
protected long artistId;
protected int trackNumber;
private boolean isInLibrary;
private Song() {
}
private Song(Parcel in) {
songName = in.readString();
songId = in.readLong();
albumName = in.readString();
artistName = in.readString();
songDuration = in.readLong();
location = in.readParcelable(Uri.class.getClassLoader());
year = in.readInt();
dateAdded = in.readLong();
albumId = in.readLong();
artistId = in.readLong();
isInLibrary = (in.readByte() != 0);
}
public Song(Song s) {
this.songName = s.songName;
this.songId = s.songId;
this.artistName = s.artistName;
this.albumName = s.albumName;
this.songDuration = s.songDuration;
this.location = s.location;
this.year = s.year;
this.dateAdded = s.dateAdded;
this.albumId = s.albumId;
this.artistId = s.artistId;
this.trackNumber = s.trackNumber;
this.isInLibrary = s.isInLibrary;
}
/**
* Builds a {@link List} of Songs from a Cursor
* @param cur A {@link Cursor} to use when reading the {@link MediaStore}. This Cursor may have
* any filters and sorting, but MUST have AT LEAST the columns in
* {@link MediaStoreUtil#SONG_PROJECTION}. The caller is responsible for closing
* this Cursor.
* @param res A {@link Resources} Object from {@link Context#getResources()} used to get the
* default values if an unknown value is encountered
* @return A List of songs populated by entries in the Cursor
*/
public static List<Song> buildSongList(Cursor cur, Resources res) {
List<Song> songs = new ArrayList<>(cur.getCount());
int titleIndex = cur.getColumnIndex(MediaStore.Audio.Media.TITLE);
int idIndex = cur.getColumnIndex(MediaStore.Audio.Media._ID);
int artistIndex = cur.getColumnIndex(MediaStore.Audio.Media.ARTIST);
int albumIndex = cur.getColumnIndex(MediaStore.Audio.Media.ALBUM);
int durationIndex = cur.getColumnIndex(MediaStore.Audio.Media.DURATION);
int dataIndex = cur.getColumnIndex(MediaStore.Audio.Media.DATA);
int yearIndex = cur.getColumnIndex(MediaStore.Audio.Media.YEAR);
int dateIndex = cur.getColumnIndex(MediaStore.Audio.Media.DATE_ADDED);
int albumIdIndex = cur.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID);
int artistIdIndex = cur.getColumnIndex(MediaStore.Audio.Media.ARTIST_ID);
int trackIndex = cur.getColumnIndex(MediaStore.Audio.Media.TRACK);
if (idIndex == -1) {
idIndex = cur.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID);
}
final String unknownSong = res.getString(R.string.unknown);
final String unknownArtist = res.getString(R.string.unknown_artist);
final String unknownAlbum = res.getString(R.string.unknown_album);
for (int i = 0; i < cur.getCount(); i++) {
cur.moveToPosition(i);
Song next = new Song();
next.songName = parseUnknown(cur.getString(titleIndex), unknownSong);
next.songId = cur.getLong(idIndex);
next.artistName = parseUnknown(cur.getString(artistIndex), unknownArtist);
next.albumName = parseUnknown(cur.getString(albumIndex), unknownAlbum);
next.songDuration = cur.getLong(durationIndex);
next.location = Uri.fromFile(new File(cur.getString(dataIndex)));
next.year = cur.getInt(yearIndex);
next.dateAdded = cur.getLong(dateIndex);
next.albumId = cur.getLong(albumIdIndex);
next.artistId = cur.getLong(artistIdIndex);
next.trackNumber = cur.getInt(trackIndex);
next.isInLibrary = true;
songs.add(next);
}
return songs;
}
/**
* Builds a Song that corresponds to a specific URI
* @param context A Context used to load information about the URI
* @param uri The URI to build a song from
* @return A Song with data corresponding to that of the given URI
*/
public static Song fromUri(Context context, Uri uri) {
Song song = new Song();
song.location = uri;
song.songName = UriUtils.getDisplayName(context, uri);
song.artistName = context.getResources().getString(R.string.unknown_artist);
song.albumName = context.getResources().getString(R.string.unknown_album);
song.songDuration = 0;
song.year = 0;
song.trackNumber = 0;
song.dateAdded = 0;
song.songId = -1 * Math.abs(uri.hashCode());
song.albumId = -1;
song.artistId = -1;
song.isInLibrary = false;
if (uri.getScheme().equals("content") || uri.getScheme().equals("file")) {
song.loadInfoFromMetadata(context);
}
return song;
}
private void loadInfoFromMetadata(Context context) {
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
mmr.setDataSource(context, location);
songName = parseUnknown(mmr.extractMetadata(METADATA_KEY_TITLE), songName);
artistName = parseUnknown(mmr.extractMetadata(METADATA_KEY_ARTIST), artistName);
albumName = parseUnknown(mmr.extractMetadata(METADATA_KEY_ALBUM), albumName);
songDuration = stringToLong(mmr.extractMetadata(METADATA_KEY_DURATION), songDuration);
year = stringToInt(mmr.extractMetadata(METADATA_KEY_DATE), year);
trackNumber = stringToInt(mmr.extractMetadata(METADATA_KEY_CD_TRACK_NUMBER), trackNumber);
mmr.release();
}
public String getSongName() {
return songName;
}
public long getSongId() {
return songId;
}
public String getArtistName() {
return artistName;
}
public String getAlbumName() {
return albumName;
}
public long getSongDuration() {
return songDuration;
}
public Uri getLocation() {
return location;
}
public int getYear() {
return year;
}
public long getDateAdded() {
return dateAdded;
}
public long getAlbumId() {
return albumId;
}
public long getArtistId() {
return artistId;
}
public int getTrackNumber() {
return trackNumber;
}
public boolean isInLibrary() {
return isInLibrary;
}
@Override
public int hashCode() {
return hashLong(songId);
}
@Override
public boolean equals(final Object obj) {
return this == obj
|| (obj != null && obj instanceof Song && songId == ((Song) obj).songId);
}
public String toString() {
return songName;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(songName);
dest.writeLong(songId);
dest.writeString(albumName);
dest.writeString(artistName);
dest.writeLong(songDuration);
dest.writeParcelable(location, 0);
dest.writeInt(year);
dest.writeLong(dateAdded);
dest.writeLong(albumId);
dest.writeLong(artistId);
dest.writeByte((byte) (isInLibrary ? 1 : 0));
}
@Override
public int compareTo(@NonNull Song another) {
return compareTitle(getSongName(), another.getSongName());
}
public static final Comparator<Song> ARTIST_COMPARATOR = (s1, s2) ->
compareTitle(s1.getArtistName(), s2.getArtistName());
public static final Comparator<Song> ALBUM_COMPARATOR = (o1, o2) ->
compareTitle(o1.getAlbumName(), o2.getAlbumName());
public static Comparator<Song> playCountComparator(PlayCountStore countStore) {
return (s1, s2) -> countStore.getPlayCount(s2) - countStore.getPlayCount(s1);
}
public static Comparator<Song> skipCountComparator(PlayCountStore countStore) {
return (s1, s2) -> countStore.getSkipCount(s2) - countStore.getSkipCount(s1);
}
public static final Comparator<Song> DATE_ADDED_COMPARATOR = (s1, s2) ->
compareLong(s2.getDateAdded(), s1.getDateAdded());
public static Comparator<Song> playDateComparator(PlayCountStore countStore) {
return (s1, s2) -> compareLong(countStore.getPlayDate(s2), countStore.getPlayDate(s1));
}
public static final Comparator<Song> YEAR_COMPARATOR = (s1, s2) ->
s2.getYear() - s1.getYear();
public static final Comparator<Song> TRACK_COMPARATOR = (s1, s2) -> {
int diff = s1.getTrackNumber() - s2.getTrackNumber();
if (diff == 0) {
// Sort by name when there's a conflict
diff = s1.compareTo(s2);
}
return diff;
};
public static class Builder {
private String songName;
private long songId;
private String artistName;
private String albumName;
private long songDuration;
private Uri location;
private int year;
private long dateAdded;
private long albumId;
private long artistId;
private int trackNumber;
private boolean isInLibrary;
public Builder setSongName(String songName) {
this.songName = songName;
return this;
}
public Builder setSongId(long songId) {
this.songId = songId;
return this;
}
public Builder setArtistName(String artistName) {
this.artistName = artistName;
return this;
}
public Builder setAlbumName(String albumName) {
this.albumName = albumName;
return this;
}
public Builder setSongDuration(long songDuration) {
this.songDuration = songDuration;
return this;
}
public Builder setLocation(Uri location) {
this.location = location;
return this;
}
public Builder setYear(int year) {
this.year = year;
return this;
}
public Builder setDateAdded(long dateAdded) {
this.dateAdded = dateAdded;
return this;
}
public Builder setAlbumId(long albumId) {
this.albumId = albumId;
return this;
}
public Builder setArtistId(long artistId) {
this.artistId = artistId;
return this;
}
public Builder setTrackNumber(int trackNumber) {
this.trackNumber = trackNumber;
return this;
}
public Builder setInLibrary(boolean inLibrary) {
isInLibrary = inLibrary;
return this;
}
public Song build() {
Song built = new Song();
built.songName = songName;
built.songId = songId;
built.artistName = artistName;
built.albumName = albumName;
built.songDuration = songDuration;
built.location = location;
built.year = year;
built.dateAdded = dateAdded;
built.albumId = albumId;
built.artistId = artistId;
built.trackNumber = trackNumber;
built.isInLibrary = isInLibrary;
return built;
}
}
}