mp.teardrop.Song.java Source code

Java tutorial

Introduction

Here is the source code for mp.teardrop.Song.java

Source

/*
 * Copyright (C) 2010, 2011 Christopher Eby <kreed@kreed.org>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package mp.teardrop;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;

import org.json.JSONException;
import org.json.JSONObject;

import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.MediaStore;
import android.util.Log;
import android.util.LruCache;

/**
 * Represents a Song backed by the MediaStore. Includes basic metadata and
 * utilities to retrieve songs from the MediaStore.
 */
public class Song implements Comparable<Song> {
    /**
     * Indicates that this song was randomly selected from all songs.
     */
    public static final int FLAG_RANDOM = 0x1;
    /**
     * If set, this song has no cover art. If not set, this song may or may not
     * have cover art.
     */
    public static final int FLAG_NO_COVER = 0x2;
    /**
     * The number of flags.
     */
    public static final int FLAG_COUNT = 2;
    /**
     * Use all cover providers to load cover art
     */
    public static final int COVER_MODE_ALL = 0xF;
    /**
     * Use androids builtin cover mechanism to load covers
     */
    public static final int COVER_MODE_ANDROID = 0x1;
    /**
     * Use vanilla musics cover load mechanism
     */
    public static final int COVER_MODE_VANILLA = 0x2;
    /**
     * Use vanilla musics SHADOW cover load mechanism
     */
    public static final int COVER_MODE_SHADOW = 0x4;

    public static final String[] EMPTY_PROJECTION = { MediaStore.Audio.Media._ID, };

    public static final String[] FILLED_PROJECTION = { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA,
            MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.ARTIST,
            MediaStore.Audio.Media.ALBUM_ID, MediaStore.Audio.Media.ARTIST_ID, MediaStore.Audio.Media.DURATION,
            MediaStore.Audio.Media.TRACK, };

    public static final String[] EMPTY_PLAYLIST_PROJECTION = { MediaStore.Audio.Playlists.Members.AUDIO_ID, };

    public static final String[] FILLED_PLAYLIST_PROJECTION = { MediaStore.Audio.Playlists.Members.AUDIO_ID,
            MediaStore.Audio.Playlists.Members.DATA, MediaStore.Audio.Playlists.Members.TITLE,
            MediaStore.Audio.Playlists.Members.ALBUM, MediaStore.Audio.Playlists.Members.ARTIST,
            MediaStore.Audio.Playlists.Members.ALBUM_ID, MediaStore.Audio.Playlists.Members.ARTIST_ID,
            MediaStore.Audio.Playlists.Members.DURATION, MediaStore.Audio.Playlists.Members.TRACK, };

    private class LruCacheKey {
        long id;
        long artistId;
        long albumId;
        String path;

        public LruCacheKey(long id, long artistId, long albumId, String path) {
            this.id = id;
            this.artistId = artistId;
            this.albumId = albumId;
            this.path = path;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof LruCacheKey && this.albumId == ((LruCacheKey) obj).albumId
                    && this.artistId == ((LruCacheKey) obj).artistId) {
                return true;
            }
            return false;
        }

        @Override
        public int hashCode() {
            return (int) (0xFFFFFF & (this.artistId + this.albumId));
        }

        @Override
        public String toString() {
            return "LruCacheKey<" + this.id + "> = " + this.path;
        }

    }

    /**
     * A cache of 6 MiB of covers.
     */
    private static class CoverCache extends LruCache<LruCacheKey, Bitmap> {
        private final Context mContext;

        // Possible coverart names if we are going to load the cover on our own
        private static String[] coverNames = { "cover.jpg", "cover.png", "album.jpg", "album.png", "artwork.jpg",
                "artwork.png", "art.jpg", "art.png" };

        public CoverCache(Context context) {
            super(6 * 1024 * 1024);
            mContext = context;
        }

        @Override
        public Bitmap create(LruCacheKey key) {
            try {
                InputStream inputStream = null;
                InputStream sampleInputStream = null; // same as inputStream but used for getSampleSize

                if ((mCoverLoadMode & COVER_MODE_VANILLA) != 0) {
                    String basePath = (new File(key.path)).getParentFile().getAbsolutePath(); // ../ of the currently playing file
                    for (String coverFile : coverNames) {
                        File guessedFile = new File(basePath + "/" + coverFile);
                        if (guessedFile.exists() && !guessedFile.isDirectory()) {
                            inputStream = new FileInputStream(guessedFile);
                            sampleInputStream = new FileInputStream(guessedFile);
                            break;
                        }
                    }
                }

                if (inputStream == null && (mCoverLoadMode & COVER_MODE_SHADOW) != 0) {
                    String[] projection = new String[] { MediaStore.Audio.Media.ARTIST,
                            MediaStore.Audio.Media.ALBUM };
                    QueryTask query = MediaUtils.buildQuery(UnifiedAdapter.ITEM_TYPE_SONG, key.id, projection,
                            null);
                    Cursor cursor = query.runQuery(mContext.getContentResolver());
                    if (cursor != null) {
                        if (cursor.getCount() > 0) {
                            cursor.moveToNext();
                            String thisArtist = cursor.getString(0);
                            String thisAlbum = cursor.getString(1);
                            String shadowPath = "/sdcard/Music/.vanilla/" + (thisArtist.replaceAll("/", "_")) + "/"
                                    + (thisAlbum.replaceAll("/", "_")) + ".jpg";

                            File guessedFile = new File(shadowPath);
                            if (guessedFile.exists() && !guessedFile.isDirectory()) {
                                inputStream = new FileInputStream(guessedFile);
                                sampleInputStream = new FileInputStream(guessedFile);
                            }
                        }
                        cursor.close();
                    }
                }

                if (inputStream == null && (mCoverLoadMode & COVER_MODE_ANDROID) != 0) {
                    Uri uri = Uri.parse("content://media/external/audio/media/" + key.id + "/albumart");
                    ContentResolver res = mContext.getContentResolver();
                    inputStream = res.openInputStream(uri);
                    sampleInputStream = res.openInputStream(uri);
                }

                if (inputStream != null) {
                    BitmapFactory.Options bopts = new BitmapFactory.Options();
                    bopts.inPreferredConfig = Bitmap.Config.RGB_565;
                    bopts.inJustDecodeBounds = true;

                    final int inSampleSize = getSampleSize(sampleInputStream, bopts);

                    /* reuse bopts: we are now REALLY going to decode the image */
                    bopts.inJustDecodeBounds = false;
                    bopts.inSampleSize = inSampleSize;
                    return BitmapFactory.decodeStream(inputStream, null, bopts);
                }
            } catch (IOException e) {
                // no cover art found
                Log.v("OrchidMP", "Loading coverart for " + key + " failed with exception " + e);
            }

            return null;
        }

        /**
         * Guess a good sampleSize value for given inputStream
         */
        private static int getSampleSize(InputStream inputStream, BitmapFactory.Options bopts) {
            int sampleSize = 1; /* default sample size                   */
            long maxVal = 600 * 600; /* max number of pixels we are accepting */

            BitmapFactory.decodeStream(inputStream, null, bopts);
            long hasPixels = bopts.outHeight * bopts.outWidth;
            if (hasPixels > maxVal) {
                sampleSize = Math.round((int) Math.sqrt((float) hasPixels / (float) maxVal));
            }
            return sampleSize;
        }

        @Override
        protected int sizeOf(LruCacheKey key, Bitmap value) {
            return value.getRowBytes() * value.getHeight();
        }
    }

    /**
     * The cache instance.
     */
    private static CoverCache sCoverCache = null;

    /**
     * Bitmask on how we are going to load coverart
     */
    public static int mCoverLoadMode = 0;

    /**
     * We will evict our own cache if set to true
     */
    public static boolean mFlushCoverCache = false;

    /**
     * Id of this song in the MediaStore
     */
    public long id;
    /**
     * Id of this song's album in the MediaStore
     */
    public long albumId;
    /**
     * Id of this song's artist in the MediaStore
     */
    public long artistId;
    /**
     * True if the song is a file in Dropbox, false if it's on the local file system.
     */
    public boolean isCloudSong;
    /**
     * Used to determine whether the Dropbox streaming link is still usable (they are valid for 4
      * hours). This is not preserved in JSON because if a song is being recreated from JSON, the
      * it's probably a new app session and all links are invalid anyway.
     */
    public Date dropboxLinkCreated;

    public String cloudRevision;

    public Date cloudLinkExpires;

    /**
     * Path to the data for this song, or streaming URL if it's in Dropbox
     */
    public String path;

    /**
     * Path to this song in Dropbox, null if it's a local file.
     */
    String dbPath = null;

    /**
     * Song title
     */
    public String title;
    /**
     * Album name
     */
    public String album;
    /**
     * Artist name
     */
    public String artist;

    /**
     * The position of the song in its album.
     */
    public int trackNumber;

    /**
     * Song flags. Currently {@link #FLAG_RANDOM} or {@link #FLAG_NO_COVER}.
     */
    public int flags;

    /**
     * Pre-fetched track RG info. If a song lacks this and is a local file,
     * the file will be checked for RG info when the MediaPlayer is prepared.
     */
    Float rgTrack = null;
    /**
     * Pre-fetched album RG info. If a song lacks this and is a local file,
     * the file will be checked for RG info when the MediaPlayer is prepared.
     */
    Float rgAlbum = null;

    /**
     * Initialize the song with the specified id. Call populate to fill fields
     * in the song.
     */
    public Song(long id) {
        this.id = id;
        this.isCloudSong = false;
    }

    /**
     * Initialize the song with the specified id and flags. Call populate to
     * fill fields in the song.
     */
    public Song(long id, int flags) {
        this.id = id;
        this.flags = flags;
        this.isCloudSong = false;
    }

    /**
     * Initialize and instantly populate a song.
     * If the song is not a cloud song (and thus has ids),
     * they will have to be set manually after using this constructor.
     */
    public Song(boolean isCloudSong, String dropboxPath, String title, String album, String artist,
            int trackNumber) {
        this.isCloudSong = isCloudSong;
        id = -1337;
        this.path = dropboxPath;
        this.title = title;
        this.album = album;
        this.artist = artist;
        albumId = -1337;
        artistId = -1337;
        this.trackNumber = trackNumber;
    }

    static Song fromJsonObject(JSONObject jsonBourne) {

        try {

            Song song = new Song(jsonBourne.getBoolean("isCloudSong"), jsonBourne.getString("path"),
                    jsonBourne.getString("title"), jsonBourne.getString("album"), jsonBourne.getString("artist"),
                    jsonBourne.getInt("trackNumber"));

            if (song.isCloudSong) {
                song.id = -1337;
                song.albumId = -1337;
                song.artistId = -1337;
                song.dbPath = jsonBourne.getString("dbPath");
                song.rgTrack = //TODO: make sure there isn't any loss of precision
                        jsonBourne.has("rgTrack") ? new Float(jsonBourne.getDouble("rgTrack")) : null;
                song.rgAlbum = jsonBourne.has("rgAlbum") ? new Float(jsonBourne.getDouble("rgAlbum")) : null;
            } else {
                song.id = jsonBourne.getLong("id");
                song.artistId = jsonBourne.getLong("artistId");
                song.albumId = jsonBourne.getLong("albumId");
                song.dbPath = null;
            }

            return song;

        } catch (JSONException e) {
            return null;
        }

    }

    JSONObject toJsonObject() {
        JSONObject jsonBourne = new JSONObject();

        try {

            jsonBourne.put("path", this.path);
            jsonBourne.put("title", this.title);
            jsonBourne.put("album", this.album);
            jsonBourne.put("artist", this.artist);
            jsonBourne.put("trackNumber", this.trackNumber);

            if (this.isCloudSong) {
                jsonBourne.put("isCloudSong", true);
                jsonBourne.put("dbPath", this.dbPath);
                //currently, only cloud songs need to store RG here as local songs
                //retrieve their RG info on the fly in playbackservice
                //TODO: unify?
                jsonBourne.put("rgTrack", this.rgTrack);
                jsonBourne.put("rgAlbum", this.rgAlbum);
            } else {
                jsonBourne.put("isCloudSong", false);
                jsonBourne.put("id", this.id);
                jsonBourne.put("artistId", this.id);
                jsonBourne.put("albumId", this.id);
            }

            return jsonBourne;

        } catch (JSONException e) {
            return null;
        }

    }

    /**
     * Return true if this song was retrieved from randomSong().
     */
    public boolean isRandom() {
        return (flags & FLAG_RANDOM) != 0;
    }

    /**
     * Populate fields with data from the supplied cursor.
     *
     * @param cursor Cursor queried with FILLED_PROJECTION projection
     */
    public void populate(Cursor cursor) {
        id = cursor.getLong(0);
        path = cursor.getString(1);
        title = cursor.getString(2);
        album = cursor.getString(3);
        artist = cursor.getString(4);
        albumId = cursor.getLong(5);
        artistId = cursor.getLong(6);
        //duration = cursor.getLong(7);
        trackNumber = cursor.getInt(8);
    }

    /**
     * Get the id of the given song.
     *
     * @param song The Song to get the id from.
     * @return The id, or 0 if the given song is null.
     */
    public static long getId(Song song) {
        if (song == null)
            return 0;
        return song.id;
    }

    /**
     * Get the Dropbox path of the given song.
     *
     * @param song The Song.
     * @return The path, or null if the song is a local file.
     */
    static String getDropboxPath(Song song) {
        if (song == null)
            return null;
        return song.dbPath;
    }

    /**
     * Query the album art for this song.
     *
     * @param context A context to use.
     * @return The album art or null if no album art could be found
     */
    public Bitmap getCover(Context context) {
        if (isCloudSong)
            return null;

        /* if (mCoverLoadMode == 0 || id == -1 || (flags & FLAG_NO_COVER) != 0) //TODO: restore this
           return null; */

        if (mCoverLoadMode == 0 || id == -1)
            return null;

        if (sCoverCache == null)
            sCoverCache = new CoverCache(context.getApplicationContext());

        if (mFlushCoverCache) {
            mFlushCoverCache = false;
            sCoverCache.evictAll();
        }

        LruCacheKey key = new LruCacheKey(id, artistId, albumId, path);
        Bitmap cover = sCoverCache.get(key);

        if (cover == null)
            flags |= FLAG_NO_COVER;
        return cover;
    }

    @Override
    public String toString() {
        return String.format("%d %d %s", id, albumId, path);
    }

    /**
     * Compares the album ids of the two songs; if equal, compares track order.
     */
    @Override
    public int compareTo(Song other) {
        if (albumId == other.albumId)
            return trackNumber - other.trackNumber;
        if (albumId > other.albumId)
            return 1;
        return -1;
    }
}