Create a builder for audio_util Metadata and migrate off bundles

The Util class in audio_util provides several static utilties for
converting between media framework objects and AVRCP flavored objects.
These conversion methods utilize bundles to do the translations, which
can be limiting when using non-parcelable objects.

While I would love to move off of AVRCP flavored objects for this
generic audio/media interface, that's going to be a meaty change thats
best done when a new version of the media framework is locked in and
supports everything we need. This change serves to make what we have
more flexible in the meantime.

Tag: #refactor
Bug: 153076316
Test: atest BluetoothInstrumentationTests
Change-Id: I0f29291cc4e8c92836bd1777c30dee184ac1b4e8
diff --git a/src/com/android/bluetooth/audio_util/helpers/Metadata.java b/src/com/android/bluetooth/audio_util/helpers/Metadata.java
index 1b59b3e..68bd8da 100644
--- a/src/com/android/bluetooth/audio_util/helpers/Metadata.java
+++ b/src/com/android/bluetooth/audio_util/helpers/Metadata.java
@@ -16,6 +16,12 @@
 
 package com.android.bluetooth.audio_util;
 
+import android.media.MediaDescription;
+import android.media.MediaMetadata;
+import android.media.browse.MediaBrowser.MediaItem;
+import android.media.session.MediaSession;
+import android.os.Bundle;
+
 import java.util.Objects;
 
 public class Metadata implements Cloneable {
@@ -61,6 +67,143 @@
         return "{ mediaId=\"" + mediaId + "\" title=\"" + title + "\" artist=\"" + artist
                 + "\" album=\"" + album + "\" duration=" + duration
                 + " trackPosition=" + trackNum + "/" + numTracks + " }";
+    }
 
+    /**
+     * A Builder object to populate a Metadata from various different Media Framework objects
+     */
+    public static class Builder {
+        private Metadata mMetadata = new Metadata();
+
+        /**
+         * Set the Media ID fot the Metadata Object
+         */
+        public Builder setMediaId(String id) {
+            mMetadata.mediaId = id;
+            return this;
+        }
+
+        /**
+         * Extract the fields from a MediaMetadata object into a Metadata, if they exist
+         */
+        public Builder fromMediaMetadata(MediaMetadata data) {
+            if (data == null) return this;
+
+            // First, use the basic description available with the MediaMetadata
+            fromMediaDescription(data.getDescription());
+
+            // Then, replace with better data if available on the MediaMetadata
+            if (data.containsKey(MediaMetadata.METADATA_KEY_MEDIA_ID)) {
+                mMetadata.mediaId = data.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
+            }
+            if (data.containsKey(MediaMetadata.METADATA_KEY_TITLE)) {
+                mMetadata.title = data.getString(MediaMetadata.METADATA_KEY_TITLE);
+            }
+            if (data.containsKey(MediaMetadata.METADATA_KEY_ARTIST)) {
+                mMetadata.artist = data.getString(MediaMetadata.METADATA_KEY_ARTIST);
+            }
+            if (data.containsKey(MediaMetadata.METADATA_KEY_ALBUM)) {
+                mMetadata.album = data.getString(MediaMetadata.METADATA_KEY_ALBUM);
+            }
+            if (data.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER)) {
+                mMetadata.trackNum = "" + data.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER);
+            }
+            if (data.containsKey(MediaMetadata.METADATA_KEY_NUM_TRACKS)) {
+                mMetadata.numTracks = "" + data.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS);
+            }
+            if (data.containsKey(MediaMetadata.METADATA_KEY_GENRE)) {
+                mMetadata.genre = data.getString(MediaMetadata.METADATA_KEY_GENRE);
+            }
+            if (data.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
+                mMetadata.duration = "" + data.getLong(MediaMetadata.METADATA_KEY_DURATION);
+            }
+            return this;
+        }
+
+        /**
+         * Extract the fields from a MediaItem object into a Metadata, if they exist
+         */
+        public Builder fromMediaItem(MediaItem item) {
+            if (item == null) return this;
+            return fromMediaDescription(item.getDescription()).setMediaId(item.getMediaId());
+        }
+
+        /**
+         * Extract the fields from a MediaDescription object into a Metadata, if they exist
+         */
+        public Builder fromMediaDescription(MediaDescription desc) {
+            if (desc == null) return this;
+
+            // Default the following mapping if they exist
+            if (desc.getTitle() != null) mMetadata.title = desc.getTitle().toString();
+            if (desc.getSubtitle() != null) mMetadata.artist = desc.getSubtitle().toString();
+            if (desc.getDescription() != null) mMetadata.album = desc.getDescription().toString();
+
+            // Then, check the extras in the description for even better data
+            return fromBundle(desc.getExtras()).setMediaId(desc.getMediaId());
+        }
+
+        /**
+         * Extract the fields from a MediaSession.QueueItem object into a Metadata, if they exist
+         */
+        public Builder fromQueueItem(MediaSession.QueueItem item) {
+            if (item == null) return this;
+            return fromMediaDescription(item.getDescription());
+        }
+
+        /**
+         * Extract the fields from a Bundle of MediaMetadata constants into a Metadata, if they
+         * exist
+         */
+        public Builder fromBundle(Bundle bundle) {
+            if (bundle == null) return this;
+            if (bundle.containsKey(MediaMetadata.METADATA_KEY_MEDIA_ID)) {
+                mMetadata.mediaId = bundle.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
+            }
+            if (bundle.containsKey(MediaMetadata.METADATA_KEY_TITLE)) {
+                mMetadata.title = bundle.getString(MediaMetadata.METADATA_KEY_TITLE);
+            }
+            if (bundle.containsKey(MediaMetadata.METADATA_KEY_ARTIST)) {
+                mMetadata.artist = bundle.getString(MediaMetadata.METADATA_KEY_ARTIST);
+            }
+            if (bundle.containsKey(MediaMetadata.METADATA_KEY_ALBUM)) {
+                mMetadata.album = bundle.getString(MediaMetadata.METADATA_KEY_ALBUM);
+            }
+            if (bundle.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER)) {
+                mMetadata.trackNum = "" + bundle.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER);
+            }
+            if (bundle.containsKey(MediaMetadata.METADATA_KEY_NUM_TRACKS)) {
+                mMetadata.numTracks = "" + bundle.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS);
+            }
+            if (bundle.containsKey(MediaMetadata.METADATA_KEY_GENRE)) {
+                mMetadata.genre = bundle.getString(MediaMetadata.METADATA_KEY_GENRE);
+            }
+            if (bundle.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
+                mMetadata.duration = "" + bundle.getLong(MediaMetadata.METADATA_KEY_DURATION);
+            }
+            return this;
+        }
+
+        /**
+         * Elect to use default values in the Metadata in place of any missing values
+         */
+        public Builder useDefaults() {
+            if (mMetadata.mediaId == null) mMetadata.mediaId = "Not Provided";
+            if (mMetadata.title == null) mMetadata.title = "Not Provided";
+            if (mMetadata.artist == null) mMetadata.artist = "";
+            if (mMetadata.album == null) mMetadata.album = "";
+            if (mMetadata.trackNum == null) mMetadata.trackNum = "1";
+            if (mMetadata.numTracks == null) mMetadata.numTracks = "1";
+            if (mMetadata.genre == null) mMetadata.genre = "";
+            if (mMetadata.duration == null) mMetadata.duration = "0";
+            return this;
+        }
+
+        /**
+         * Get the final Metadata objects you're building
+         */
+        public Metadata build() {
+            return mMetadata.clone();
+        }
     }
 }
diff --git a/src/com/android/bluetooth/audio_util/helpers/Util.java b/src/com/android/bluetooth/audio_util/helpers/Util.java
index 31101bf..e48318d 100644
--- a/src/com/android/bluetooth/audio_util/helpers/Util.java
+++ b/src/com/android/bluetooth/audio_util/helpers/Util.java
@@ -29,7 +29,7 @@
 import java.util.List;
 
 class Util {
-    public static String TAG = "AvrcpUtil";
+    public static String TAG = "audio_util.Util";
     public static boolean DEBUG = false;
 
     private static final String GPM_KEY = "com.google.android.music.mediasession.music_metadata";
@@ -37,6 +37,9 @@
     // TODO (apanicke): Remove this prefix later, for now it makes debugging easier.
     public static final String NOW_PLAYING_PREFIX = "NowPlayingId";
 
+    /**
+     * Get an empty set of Metadata
+     */
     public static final Metadata empty_data() {
         Metadata ret = new Metadata();
         ret.mediaId = "Not Provided";
@@ -50,160 +53,63 @@
         return ret;
     }
 
-    public static Metadata bundleToMetadata(Bundle bundle) {
-        if (bundle == null) return empty_data();
-
-        Metadata temp = new Metadata();
-        temp.title = bundle.getString(MediaMetadata.METADATA_KEY_TITLE, "Not Provided");
-        temp.artist = bundle.getString(MediaMetadata.METADATA_KEY_ARTIST, "");
-        temp.album = bundle.getString(MediaMetadata.METADATA_KEY_ALBUM, "");
-        temp.trackNum = "" + bundle.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, 1);
-        temp.numTracks = "" + bundle.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, 1);
-        temp.genre = bundle.getString(MediaMetadata.METADATA_KEY_GENRE, "");
-        temp.duration = "" + bundle.getLong(MediaMetadata.METADATA_KEY_DURATION, 0);
-        return temp;
+    /**
+     * Translate a bundle of MediaMetadata keys to audio_util's Metadata
+     */
+    public static Metadata toMetadata(Bundle bundle) {
+        Metadata.Builder builder = new Metadata.Builder();
+        return builder.useDefaults().fromBundle(bundle).build();
     }
 
-    public static Bundle descriptionToBundle(MediaDescription desc) {
-        Bundle ret = new Bundle();
-        if (desc == null) return ret;
-
-        if (desc.getTitle() != null) {
-            ret.putString(MediaMetadata.METADATA_KEY_TITLE, desc.getTitle().toString());
+    /**
+     * Translate a MediaDescription to audio_util's Metadata
+     */
+    public static Metadata toMetadata(MediaDescription desc) {
+        // Find GPM_KEY data if it exists
+        MediaMetadata data = null;
+        Bundle extras = (desc != null ? desc.getExtras() : null);
+        if (extras != null && extras.containsKey(GPM_KEY)) {
+            data = (MediaMetadata) extras.get(GPM_KEY);
         }
 
-        if (desc.getSubtitle() != null) {
-            ret.putString(MediaMetadata.METADATA_KEY_ARTIST, desc.getSubtitle().toString());
-        }
-
-        if (desc.getDescription() != null) {
-            ret.putString(MediaMetadata.METADATA_KEY_ALBUM, desc.getDescription().toString());
-        }
-
-        // If the bundle has title or artist use those over the description title or subtitle.
-        if (desc.getExtras() != null) ret.putAll(desc.getExtras());
-
-        if (ret.containsKey(GPM_KEY)) {
-            if (DEBUG) Log.d(TAG, "MediaDescription contains GPM data");
-            ret.putAll(mediaMetadataToBundle((MediaMetadata) ret.get(GPM_KEY)));
-        }
-
-        return ret;
+        Metadata.Builder builder = new Metadata.Builder();
+        return builder.useDefaults().fromMediaDescription(desc).fromMediaMetadata(data).build();
     }
 
-    public static Bundle mediaMetadataToBundle(MediaMetadata data) {
-        Bundle bundle = new Bundle();
-        if (data == null) return bundle;
-
-        if (data.containsKey(MediaMetadata.METADATA_KEY_TITLE)) {
-            bundle.putString(MediaMetadata.METADATA_KEY_TITLE,
-                    data.getString(MediaMetadata.METADATA_KEY_TITLE));
-        }
-
-        if (data.containsKey(MediaMetadata.METADATA_KEY_ARTIST)) {
-            bundle.putString(MediaMetadata.METADATA_KEY_ARTIST,
-                    data.getString(MediaMetadata.METADATA_KEY_ARTIST));
-        }
-
-        if (data.containsKey(MediaMetadata.METADATA_KEY_ALBUM)) {
-            bundle.putString(MediaMetadata.METADATA_KEY_ALBUM,
-                    data.getString(MediaMetadata.METADATA_KEY_ALBUM));
-        }
-
-        if (data.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER)) {
-            bundle.putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER,
-                    data.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER));
-        }
-
-        if (data.containsKey(MediaMetadata.METADATA_KEY_NUM_TRACKS)) {
-            bundle.putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS,
-                    data.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS));
-        }
-
-        if (data.containsKey(MediaMetadata.METADATA_KEY_GENRE)) {
-            bundle.putString(MediaMetadata.METADATA_KEY_GENRE,
-                    data.getString(MediaMetadata.METADATA_KEY_GENRE));
-        }
-
-        if (data.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
-            bundle.putLong(MediaMetadata.METADATA_KEY_DURATION,
-                    data.getLong(MediaMetadata.METADATA_KEY_DURATION));
-        }
-
-        return bundle;
+    /**
+     * Translate a MediaItem to audio_util's Metadata
+     */
+    public static Metadata toMetadata(MediaItem item) {
+        Metadata.Builder builder = new Metadata.Builder();
+        return builder.useDefaults().fromMediaItem(item).build();
     }
 
+    /**
+     * Translate a MediaSession.QueueItem to audio_util's Metadata
+     */
     public static Metadata toMetadata(MediaSession.QueueItem item) {
-        if (item == null) {
-            return empty_data();
-        }
-
-        Bundle bundle = descriptionToBundle(item.getDescription());
-
-        if (DEBUG) {
-            for (String key : bundle.keySet()) {
-                Log.d(TAG, "toMetadata: QueueItem: ContainsKey: " + key);
-            }
-        }
-
-        Metadata ret = bundleToMetadata(bundle);
-
+        Metadata.Builder builder = new Metadata.Builder().useDefaults().fromQueueItem(item);
         // For Queue Items, the Media Id will always be just its Queue ID
         // We don't need to use its actual ID since we don't promise UIDS being valid
         // between a file system and it's now playing list.
-        ret.mediaId = NOW_PLAYING_PREFIX + item.getQueueId();
-
-        return ret;
+        if (item != null) builder.setMediaId(NOW_PLAYING_PREFIX + item.getQueueId());
+        return builder.build();
     }
 
+    /**
+     * Translate a MediaMetadata to audio_util's Metadata
+     */
     public static Metadata toMetadata(MediaMetadata data) {
-        if (data == null) {
-            return empty_data();
-        }
-
-        MediaDescription desc = data.getDescription();
-
-        Bundle dataBundle = mediaMetadataToBundle(data);
-        Bundle bundle = descriptionToBundle(data.getDescription());
-
-        // Prioritize the media metadata over the media description
-        bundle.putAll(dataBundle);
-
-        if (DEBUG) {
-            for (String key : bundle.keySet()) {
-                Log.d(TAG, "toMetadata: MediaMetadata: ContainsKey: " + key);
-            }
-        }
-
-        Metadata ret = bundleToMetadata(bundle);
-
+        Metadata.Builder builder = new Metadata.Builder();
         // This will always be currsong. The AVRCP service will overwrite the mediaId if it needs to
         // TODO (apanicke): Remove when the service is ready, right now it makes debugging much more
         // convenient
-        ret.mediaId = "currsong";
-
-        return ret;
+        return builder.useDefaults().fromMediaMetadata(data).setMediaId("currsong").build();
     }
 
-    public static Metadata toMetadata(MediaItem item) {
-        if (item == null) {
-            return empty_data();
-        }
-
-        Bundle bundle = descriptionToBundle(item.getDescription());
-
-        if (DEBUG) {
-            for (String key : bundle.keySet()) {
-                Log.d(TAG, "toMetadata: MediaItem: ContainsKey: " + key);
-            }
-        }
-
-        Metadata ret = bundleToMetadata(bundle);
-        ret.mediaId = item.getMediaId();
-
-        return ret;
-    }
-
+    /**
+     * Translate a list of MediaSession.QueueItem to a list of audio_util's Metadata
+     */
     public static List<Metadata> toMetadataList(List<MediaSession.QueueItem> items) {
         ArrayList<Metadata> list = new ArrayList<Metadata>();