package me.barrasso.android.volume.media;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.MediaMetadataEditor;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Property;
import static android.media.MediaMetadataRetriever.*;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import me.barrasso.android.volume.utils.Utils;
import static me.barrasso.android.volume.LogUtils.LOGE;
/**
* Proxy to {@link android.media.MediaMetadataEditor} to allow for
* easier access and storage of media metadata information.
*/
public final class Metadata {
public static final String TAG = Metadata.class.getSimpleName();
public static class PlaybackEvent {
public PlaybackInfo mPlaybackInfo;
public Metadata mMetadata;
}
public static class PlaybackPosition {
public long position;
public PlaybackPosition(long pos) { position = pos; }
}
public static class SeekCommand {
public long position;
public SeekCommand(long pos) { position = pos; }
}
// RemoteControlClient.MetadataEditor#BITMAP_KEY_ARTWORK
public final static int _BITMAP_KEY_ARTWORK = 100;
// Localized constant to avoid issues with SDK.
@TargetApi(Build.VERSION_CODES.KITKAT)
public static int BITMAP_KEY_ARTWORK() {
return ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) ?
MediaMetadataEditor.BITMAP_KEY_ARTWORK : _BITMAP_KEY_ARTWORK);
}
public static final int KEY_HAS_ARTWORK = 1984; // Thanks George Orwell
public static final int KEY_HAS_REMOTE = 2014; // @Copyright
public static final String ARTWORK_FILE = "artwork.png"; // Default file name
public static final int KEY_REMOTE_PACKAGE = 1776; // Independence!
public static final int KEY_POSITION = 1994; // BEST YEAR EVER!!!
private static final int[] METADATA_STRING_KEYS = new int[] {
METADATA_KEY_ALBUM, METADATA_KEY_ALBUMARTIST,
METADATA_KEY_ARTIST, METADATA_KEY_AUTHOR,
METADATA_KEY_COMPOSER, METADATA_KEY_GENRE,
METADATA_KEY_TITLE, METADATA_KEY_WRITER
};
private static final int[] METADATA_LONG_KEYS = new int[] {
METADATA_KEY_CD_TRACK_NUMBER, METADATA_KEY_DISC_NUMBER,
METADATA_KEY_YEAR, METADATA_KEY_DURATION
};
public static final Property<Metadata, Bundle> BUNDLE = Property.of(Metadata.class, Bundle.class, "bundle");
private final Bundle bundle;
public Metadata(MediaMetadataEditor editor) {
bundle = new Bundle();
bundle.putBoolean(TAG, true); // Leave our mark!
initFromMetadata(editor);
}
@TargetApi(Build.VERSION_CODES.KITKAT)
protected void initFromMetadata(MediaMetadataEditor editor) {
// KitKat and above supports retrieval of album art.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
setArtwork(editor.getBitmap(BITMAP_KEY_ARTWORK(), null));
}
for (final int intKey : METADATA_STRING_KEYS) {
String ret = editor.getString(intKey, null);
if (!TextUtils.isEmpty(ret)) {
putString(intKey, ret);
}
}
for (final int intKey : METADATA_LONG_KEYS) {
long ret = editor.getLong(intKey, Long.MIN_VALUE);
if (ret != Long.MIN_VALUE) {
putLong(intKey, ret);
}
}
}
public Metadata(Bundle iBundle){
this.bundle = iBundle;
}
/** Constructor for partial implementation of metadata information. */
public Metadata() {
bundle = new Bundle();
bundle.putBoolean(TAG, true); // Leave our mark!
}
public Bundle getBundle() { return bundle; }
/**
* Like {@link #getBundle()}, but does not contain a Bitmap to avoid
* the chances of a {@link android.os.TransactionTooLargeException}.
* @return A {@link android.os.Bundle} of primitives and strings.
*/
public Bundle getLightweightBundle() {
boolean hasArtwork = hasArtwork();
if (hasArtwork)
remove(BITMAP_KEY_ARTWORK());
putBoolean(KEY_HAS_ARTWORK, true);
return bundle;
}
/**
* Read the stored artwork from disk.
* @see #ARTWORK_FILE
* @return The stored artwork, or null if none was found.
*/
public Bitmap getArtwork(Context context) {
if (hasArtwork() && bundle.containsKey(File.class.getSimpleName())) {
File artFile = new File(bundle.getString(File.class.getSimpleName()));
if (!artFile.exists()) return null;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
options.inPreferQualityOverSpeed = false;
return BitmapFactory.decodeFile(artFile.getAbsolutePath(), options);
}
return null;
}
/**
* Write the stored artwork to disk.
* @see #ARTWORK_FILE
* @return True if the artwork was written to disk successfully.
*/
public boolean writeArtwork(Context context) {
OutputStream fos = null;
try {
if (hasArtwork()) {
File dir = context.getCacheDir();
File artFile = new File(dir, ARTWORK_FILE);
fos = new BufferedOutputStream(new FileOutputStream(artFile));
Bitmap artwork = getArtwork();
if (null != artwork) {
artwork.compress(Bitmap.CompressFormat.PNG, 100, fos);
artwork.recycle();
putBoolean(KEY_HAS_ARTWORK, true);
bundle.putString(File.class.getSimpleName(), artFile.getAbsolutePath());
return true;
}
}
} catch (FileNotFoundException fne) {
LOGE(TAG, "Error creating " + ARTWORK_FILE, fne);
} finally {
try {
if (null != fos) {
fos.close();
}
} catch (IOException ioe) {
LOGE(TAG, "Error closing " + FileOutputStream.class.getSimpleName(), ioe);
}
}
return false;
}
// Convenience methods for extracting info from metadata.
public String getAlbum() { return getString(METADATA_KEY_ALBUM, null); }
public String getArtist() { return getString(METADATA_KEY_ARTIST, null); }
public String getTitle() { return getString(METADATA_KEY_TITLE, null); }
public long getDuration() { return getLong(METADATA_KEY_DURATION, 0); }
public void setAlbum(String album) { putString(METADATA_KEY_ALBUM, album); }
public void setArtist(String artist) { putString(METADATA_KEY_ARTIST, artist); }
public void setTitle(String title) { putString(METADATA_KEY_TITLE, title); }
public void setDuration(long duration) { putLong(METADATA_KEY_DURATION, duration); }
public void setPlayState(boolean playing) { bundle.putBoolean("playstate", playing); }
public boolean isPlaying() { return bundle.getBoolean("playstate", false); }
public boolean hasPlayState() { return bundle.containsKey("playstate"); }
public void hasRemote(boolean hazIt) { putBoolean(KEY_HAS_REMOTE, hazIt); }
public boolean hasRemote() { return containsKey(KEY_HAS_REMOTE); }
public void setRemotePackage(String remotePackage) { putString(KEY_REMOTE_PACKAGE, remotePackage); }
public String getRemotePackage() { return getString(KEY_REMOTE_PACKAGE, null); }
/** @return True if this metadata only has the current play state. */
public boolean isPlayState() {
return (null != bundle.keySet()) && (bundle.keySet().size() == 1) && hasPlayState();
}
public boolean hasArtwork() {
return containsKey(BITMAP_KEY_ARTWORK()) ||
containsKey(KEY_HAS_ARTWORK);
}
public Bitmap getArtwork() {
final int key = BITMAP_KEY_ARTWORK();
if (!containsKey(key)) return null;
try {
return (Bitmap) getParcelable(key);
} catch (ClassCastException cce) {
LOGE(TAG, "Artwork could not be obtained, not a Bitmap.", cce);
return null;
}
}
public void setArtwork(Bitmap artwork) {
putParcelable(BITMAP_KEY_ARTWORK(), artwork);
}
public void clearArtwork() {
// Recycle the old artwork if it exists.
// Bitmap artwork = getArtwork();
// if (null != artwork) {
// artwork.recycle();
// }
remove(BITMAP_KEY_ARTWORK());
}
public void recycle() {
clearArtwork();
bundle.clear();
}
@Override
public String toString() {
return TAG + '@' + Utils.bundle2string(bundle);
}
/** @see android.os.Bundle#remove(String) */
public void remove(int key) {
bundle.remove(String.valueOf(key));
}
/** @see android.os.Bundle#containsKey(String) */
public boolean containsKey(int key) {
return bundle.containsKey(String.valueOf(key));
}
/** @see android.os.Bundle#putBoolean(String, boolean) */
public void putBoolean(int key, boolean value) {
bundle.putBoolean(String.valueOf(key), value);
}
/** @see android.os.Bundle#putByte(String, byte) */
public void putByte(int key, byte value) {
bundle.putByte(String.valueOf(key), value);
}
/** @see android.os.Bundle#putChar(String, char) */
public void putChar(int key, char value) {
bundle.putChar(String.valueOf(key), value);
}
/** @see android.os.Bundle#putShort(String, short) */
public void putShort(int key, short value) {
bundle.putShort(String.valueOf(key), value);
}
/** @see android.os.Bundle#putInt(String, int) */
public void putInt(int key, int value) {
bundle.putInt(String.valueOf(key), value);
}
/** @see android.os.Bundle#putLong(String, long) */
public void putLong(int key, long value) {
bundle.putLong(String.valueOf(key), value);
}
/** @see android.os.Bundle#putFloat(String, float) */
public void putFloat(int key, float value) {
bundle.putFloat(String.valueOf(key), value);
}
/** @see android.os.Bundle#putDouble(String, double) */
public void putDouble(int key, double value) {
bundle.putDouble(String.valueOf(key), value);
}
/** @see android.os.Bundle#putString(String, String) */
public void putString(int key, String value) {
bundle.putString(String.valueOf(key), value);
}
/** @see android.os.Bundle#putCharSequence(String, CharSequence) */
public void putCharSequence(int key, CharSequence value) {
bundle.putCharSequence(String.valueOf(key), value);
}
/** @see android.os.Bundle#putParcelable(String, Parcelable) */
public void putParcelable(int key, Parcelable value) {
bundle.putParcelable(String.valueOf(key), value);
}
/** @see android.os.Bundle#getBoolean(String, boolean) */
public boolean getBoolean(int key, boolean defaultValue) {
return bundle.getBoolean(String.valueOf(key), defaultValue);
}
/** @see android.os.Bundle#getByte(String, byte) */
public byte getByte(int key, byte defaultValue) {
return bundle.getByte(String.valueOf(key), defaultValue);
}
/** @see android.os.Bundle#getChar(String, char) */
public char getChar(int key, char defaultValue) {
return bundle.getChar(String.valueOf(key), defaultValue);
}
/** @see android.os.Bundle#getShort(String, short) */
public short getShort(int key, short defaultValue) {
return bundle.getShort(String.valueOf(key), defaultValue);
}
/** @see android.os.Bundle#getInt(String, int) */
public int getInt(int key, int defaultValue) {
return bundle.getInt(String.valueOf(key), defaultValue);
}
/** @see android.os.Bundle#getLong(String, long) */
public long getLong(int key, long defaultValue) {
return bundle.getLong(String.valueOf(key), defaultValue);
}
/** @see android.os.Bundle#getFloat(String, float) */
public float getFloat(int key, float defaultValue) {
return bundle.getFloat(String.valueOf(key), defaultValue);
}
/** @see android.os.Bundle#getDouble(String, double) */
public double getDouble(int key, double defaultValue) {
return bundle.getDouble(String.valueOf(key), defaultValue);
}
/** @see android.os.Bundle#getString(String, String) */
public String getString(int key, String defaultValue) {
return bundle.getString(String.valueOf(key), defaultValue);
}
/** @see android.os.Bundle#getCharSequence(String, CharSequence) */
public CharSequence getCharSequence(int key, CharSequence defaultValue) {
return bundle.getCharSequence(String.valueOf(key), defaultValue);
}
/** @see android.os.Bundle#getBundle(String) */
public Bundle getBundle(int key) {
return bundle.getBundle(String.valueOf(key));
}
/** @see android.os.Bundle#getParcelable(String) */
public Parcelable getParcelable(int key) {
return bundle.getParcelable(String.valueOf(key));
}
}