Merge "[KG Integration] Retrieve album metadata"
diff --git a/java/com/android/pump/db/Album.java b/java/com/android/pump/db/Album.java
index 984a5de..43f6d20 100644
--- a/java/com/android/pump/db/Album.java
+++ b/java/com/android/pump/db/Album.java
@@ -32,6 +32,7 @@
 
     // TODO(b/123706949) Lock mutable fields to ensure consistent updates
     private String mTitle;
+    private String mDescription;
     private Uri mAlbumArtUri;
     private Artist mArtist;
     private final List<Audio> mAudios = new ArrayList<>();
@@ -61,6 +62,26 @@
         return Collections.unmodifiableList(mAudios);
     }
 
+    public @Nullable String getDescription() {
+        return mDescription;
+    }
+
+    public boolean setAlbumArtUri(@NonNull Uri albumArtUri) {
+        if (albumArtUri.equals(mAlbumArtUri)) {
+            return false;
+        }
+        mAlbumArtUri = albumArtUri;
+        return true;
+    }
+
+    public boolean setDescription(@NonNull String description) {
+        if (description.equals(mDescription)) {
+            return false;
+        }
+        mDescription = description;
+        return true;
+    }
+
     boolean setTitle(@NonNull String title) {
         if (title.equals(mTitle)) {
             return false;
@@ -69,14 +90,6 @@
         return true;
     }
 
-    boolean setAlbumArtUri(@NonNull Uri albumArtUri) {
-        if (albumArtUri.equals(mAlbumArtUri)) {
-            return false;
-        }
-        mAlbumArtUri = albumArtUri;
-        return true;
-    }
-
     boolean setArtist(@NonNull Artist artist) {
         if (artist.equals(mArtist)) {
             return false;
diff --git a/java/com/android/pump/db/Artist.java b/java/com/android/pump/db/Artist.java
index 0270bda..82468d7 100644
--- a/java/com/android/pump/db/Artist.java
+++ b/java/com/android/pump/db/Artist.java
@@ -62,6 +62,10 @@
         return mHeadshotUri;
     }
 
+    public @Nullable String getDescription() {
+        return mDescription;
+    }
+
     public boolean setHeadshotUri(@NonNull Uri headshotUri) {
         if (headshotUri.equals(mHeadshotUri)) {
             return false;
@@ -70,10 +74,6 @@
         return true;
     }
 
-    public @Nullable String getDescription() {
-        return mDescription;
-    }
-
     public boolean setDescription(@NonNull String description) {
         if (description.equals(mDescription)) {
             return false;
diff --git a/java/com/android/pump/db/DataProvider.java b/java/com/android/pump/db/DataProvider.java
index c4511a7..28a6dc8 100644
--- a/java/com/android/pump/db/DataProvider.java
+++ b/java/com/android/pump/db/DataProvider.java
@@ -23,6 +23,7 @@
 // TODO (b/126977959): Split DataProvider into Audio/VideoDataProvider interfaces.
 public interface DataProvider {
     boolean populateArtist(@NonNull Artist artist) throws IOException;
+    boolean populateAlbum(@NonNull Album album) throws IOException;
     boolean populateMovie(@NonNull Movie movie) throws IOException;
     boolean populateSeries(@NonNull Series series) throws IOException;
     boolean populateEpisode(@NonNull Episode episode) throws IOException;
diff --git a/java/com/android/pump/db/MediaDb.java b/java/com/android/pump/db/MediaDb.java
index 3c45531..b0434af 100644
--- a/java/com/android/pump/db/MediaDb.java
+++ b/java/com/android/pump/db/MediaDb.java
@@ -275,7 +275,6 @@
             } catch (IOException e) {
                 Clog.e(TAG, "Search for " + artist + " failed", e);
             }
-
         });
     }
 
@@ -284,11 +283,17 @@
         if (album.isLoaded()) return;
 
         mExecutor.execute(() -> {
-            boolean updated = mAudioStore.loadData(album);
+            try {
+                boolean updated = mDataProvider.populateAlbum(album);
 
-            album.setLoaded();
-            if (updated) {
-                Executors.uiThreadExecutor().execute(() -> updateAlbum(album));
+                updated |= mAudioStore.loadData(album);
+
+                album.setLoaded();
+                if (updated) {
+                    Executors.uiThreadExecutor().execute(() -> updateAlbum(album));
+                }
+            } catch (IOException e) {
+                Clog.e(TAG, "Search for " + album + " failed", e);
             }
         });
     }
diff --git a/java/com/android/pump/provider/KnowledgeGraph.java b/java/com/android/pump/provider/KnowledgeGraph.java
index 33dc300..2a8b168 100644
--- a/java/com/android/pump/provider/KnowledgeGraph.java
+++ b/java/com/android/pump/provider/KnowledgeGraph.java
@@ -17,14 +17,13 @@
 package com.android.pump.provider;
 
 import android.net.Uri;
-import android.util.Log;
-import android.util.Pair;
 
 import androidx.annotation.AnyThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
+import com.android.pump.db.Album;
 import com.android.pump.db.Artist;
 import com.android.pump.db.DataProvider;
 import com.android.pump.db.Episode;
@@ -57,20 +56,48 @@
     @Override
     public boolean populateArtist(@NonNull Artist artist) throws IOException {
         boolean updated = false;
+        // TODO: Merge multiple types into one request.
         // Artist may be of type "Person" or "MusicGroup"
         JSONObject result = getResultFromKG(artist.getName(), "Person");
         if (result == null) {
             result = getResultFromKG(artist.getName(), "MusicGroup");
         }
+        if (result == null) {
+            throw new IOException("Failed to find search result");
+        }
 
-        Pair<String, String> metadata = getMetadataFromResult(result);
-        if (metadata != null) {
-            if (metadata.first != null) {
-                updated |= artist.setHeadshotUri(Uri.parse(metadata.first));
-            }
-            if (metadata.second != null) {
-                updated |= artist.setDescription(metadata.second);
-            }
+        String imageUrl = getImageUrl(result);
+        if (imageUrl != null) {
+            updated |= artist.setHeadshotUri(Uri.parse(imageUrl));
+        }
+        String detailedDescription = getDetailedDescription(result);
+        if (detailedDescription != null) {
+            updated |= artist.setDescription(detailedDescription);
+        }
+        return updated;
+    }
+
+    @Override
+    public boolean populateAlbum(@NonNull Album album) throws IOException {
+        // Return if album art is already retrieved from the media file
+        if (album.getAlbumArtUri() != null) {
+            return false;
+        }
+
+        boolean updated = false;
+        JSONObject result = getResultFromKG(album.getTitle(), "MusicAlbum");
+        if (result == null) {
+            throw new IOException("Failed to find search result");
+        }
+
+        // TODO: (b/128383917) Investigate how to filter search results
+        String imageUrl = getImageUrl(result);
+        if (imageUrl != null) {
+            updated |= album.setAlbumArtUri(Uri.parse(imageUrl));
+        }
+        String detailedDescription = getDetailedDescription(result);
+        if (detailedDescription != null) {
+            updated |= album.setDescription(detailedDescription);
         }
         return updated;
     }
@@ -78,15 +105,18 @@
     @Override
     public boolean populateMovie(@NonNull Movie movie) throws IOException {
         boolean updated = false;
-        Pair<String, String> metadata = getMetadataFromResult(
-                getResultFromKG(movie.getTitle(), "Movie"));
-        if (metadata != null) {
-            if (metadata.first != null) {
-                updated |= movie.setPosterUri(Uri.parse(metadata.first));
-            }
-            if (metadata.second != null) {
-                updated |= movie.setDescription(metadata.second);
-            }
+        JSONObject result = getResultFromKG(movie.getTitle(), "Movie");
+        if (result == null) {
+            throw new IOException("Failed to find search result");
+        }
+
+        String imageUrl = getImageUrl(result);
+        if (imageUrl != null) {
+            updated |= movie.setPosterUri(Uri.parse(imageUrl));
+        }
+        String detailedDescription = getDetailedDescription(result);
+        if (detailedDescription != null) {
+            updated |= movie.setDescription(detailedDescription);
         }
         return updated;
     }
@@ -94,15 +124,18 @@
     @Override
     public boolean populateSeries(@NonNull Series series) throws IOException {
         boolean updated = false;
-        Pair<String, String> metadata = getMetadataFromResult(
-                getResultFromKG(series.getTitle(), "TVSeries"));
-        if (metadata != null) {
-            if (metadata.first != null) {
-                updated |= series.setPosterUri(Uri.parse(metadata.first));
-            }
-            if (metadata.second != null) {
-                updated |= series.setDescription(metadata.second);
-            }
+        JSONObject result = getResultFromKG(series.getTitle(), "TVSeries");
+        if (result == null) {
+            throw new IOException("Failed to find search result");
+        }
+
+        String imageUrl = getImageUrl(result);
+        if (imageUrl != null) {
+            updated |= series.setPosterUri(Uri.parse(imageUrl));
+        }
+        String detailedDescription = getDetailedDescription(result);
+        if (detailedDescription != null) {
+            updated |= series.setDescription(detailedDescription);
         }
         return updated;
     }
@@ -110,20 +143,23 @@
     @Override
     public boolean populateEpisode(@NonNull Episode episode) throws IOException {
         boolean updated = false;
-        Pair<String, String> metadata = getMetadataFromResult(
-                getResultFromKG(episode.getSeries().getTitle(), "TVEpisode"));
-        if (metadata != null) {
-            if (metadata.first != null) {
-                updated |= episode.setPosterUri(Uri.parse(metadata.first));
-            }
-            if (metadata.second != null) {
-                updated |= episode.setDescription(metadata.second);
-            }
+        JSONObject result = getResultFromKG(episode.getSeries().getTitle(), "TVEpisode");
+        if (result == null) {
+            throw new IOException("Failed to find search result");
+        }
+
+        String imageUrl = getImageUrl(result);
+        if (imageUrl != null) {
+            updated |= episode.setPosterUri(Uri.parse(imageUrl));
+        }
+        String detailedDescription = getDetailedDescription(result);
+        if (detailedDescription != null) {
+            updated |= episode.setDescription(detailedDescription);
         }
         return updated;
     }
 
-    private JSONObject getResultFromKG(String title, String type) throws IOException {
+    private @Nullable JSONObject getResultFromKG(String title, String type) throws IOException {
         try {
             JSONObject root = (JSONObject) getContent(getContentUri(title, type));
             JSONArray items = root.getJSONArray("itemListElement");
@@ -139,32 +175,44 @@
         }
     }
 
-    private Pair<String, String> getMetadataFromResult(@NonNull JSONObject result)
-            throws IOException {
-        if (result == null) {
-            throw new IOException("Failed to find search result");
-        }
-
+    private @Nullable String getImageUrl(@NonNull JSONObject result) {
         String imageUrl = null;
-        String description = null;
         try {
-            JSONObject image = result.optJSONObject("image");
-            if (image != null) {
-                String url = image.getString("contentUrl");
+            JSONObject imageObj = result.optJSONObject("image");
+            if (imageObj != null) {
+                String url = imageObj.getString("contentUrl");
                 if (url != null) {
                     // TODO (b/125143807): Remove once HTTPS scheme urls are retrieved.
                     imageUrl = url.replaceFirst("^http://", "https://");
                 }
             }
-            JSONObject detailedDescription = result.optJSONObject("detailedDescription");
-            if (detailedDescription != null) {
-                description = detailedDescription.getString("articleBody");
+        } catch (JSONException e) {
+            Clog.w(TAG, "Failed to parse image url", e);
+        }
+        return imageUrl;
+    }
+
+    private @Nullable String getDescription(@NonNull JSONObject result) {
+        String description = null;
+        try {
+            description = result.getString("description");
+        } catch (JSONException e) {
+            Clog.w(TAG, "Failed to parse description", e);
+        }
+        return description;
+    }
+
+    private @Nullable String getDetailedDescription(@NonNull JSONObject result) {
+        String detailedDescription = null;
+        try {
+            JSONObject descriptionObj = result.optJSONObject("detailedDescription");
+            if (descriptionObj != null) {
+                detailedDescription = descriptionObj.getString("articleBody");
             }
         } catch (JSONException e) {
-            Clog.w(TAG, "Failed to parse search result", e);
-            throw new IOException(e);
+            Clog.w(TAG, "Failed to parse detailed description", e);
         }
-        return new Pair<>(imageUrl, description);
+        return detailedDescription;
     }
 
     private static @NonNull Uri getContentUri(@NonNull String title, @Nullable String type) {
diff --git a/java/com/android/pump/provider/OmdbApi.java b/java/com/android/pump/provider/OmdbApi.java
index ac7a6cd..f68c2b5 100644
--- a/java/com/android/pump/provider/OmdbApi.java
+++ b/java/com/android/pump/provider/OmdbApi.java
@@ -22,6 +22,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.WorkerThread;
 
+import com.android.pump.db.Album;
 import com.android.pump.db.Artist;
 import com.android.pump.db.DataProvider;
 import com.android.pump.db.Episode;
@@ -57,6 +58,12 @@
     }
 
     @Override
+    public boolean populateAlbum(@NonNull Album album) throws IOException {
+        // NO-OP
+        return false;
+    }
+
+    @Override
     public boolean populateMovie(@NonNull Movie movie) throws IOException {
         boolean updated = false;
         try {