• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.pump.provider;
18 
19 import android.net.Uri;
20 
21 import androidx.annotation.AnyThread;
22 import androidx.annotation.NonNull;
23 import androidx.annotation.Nullable;
24 import androidx.annotation.WorkerThread;
25 
26 import com.android.pump.db.Album;
27 import com.android.pump.db.Artist;
28 import com.android.pump.db.DataProvider;
29 import com.android.pump.db.Episode;
30 import com.android.pump.db.Movie;
31 import com.android.pump.db.Series;
32 import com.android.pump.util.Clog;
33 import com.android.pump.util.Http;
34 
35 import java.io.IOException;
36 import java.nio.charset.StandardCharsets;
37 
38 import org.json.JSONArray;
39 import org.json.JSONException;
40 import org.json.JSONObject;
41 import org.json.JSONTokener;
42 
43 @WorkerThread
44 public final class KnowledgeGraph implements DataProvider {
45     private static final String TAG = Clog.tag(KnowledgeGraph.class);
46 
47     private static final DataProvider INSTANCE = new KnowledgeGraph();
48 
KnowledgeGraph()49     private KnowledgeGraph() { }
50 
51     @AnyThread
getInstance()52     public static @NonNull DataProvider getInstance() {
53         return INSTANCE;
54     }
55 
56     @Override
populateArtist(@onNull Artist artist)57     public boolean populateArtist(@NonNull Artist artist) throws IOException {
58         boolean updated = false;
59         // Artist may be of type "Person" or "MusicGroup"
60         JSONObject result = getResultFromKG(artist.getName(), "Person", "MusicGroup");
61 
62         String imageUrl = getImageUrl(result);
63         if (imageUrl != null) {
64             updated |= artist.setHeadshotUri(Uri.parse(imageUrl));
65         }
66         String detailedDescription = getDetailedDescription(result);
67         if (detailedDescription != null) {
68             updated |= artist.setDescription(detailedDescription);
69         }
70         return updated;
71     }
72 
73     @Override
populateAlbum(@onNull Album album)74     public boolean populateAlbum(@NonNull Album album) throws IOException {
75         // Return if album art is already retrieved from the media file
76         if (album.getAlbumArtUri() != null) {
77             return false;
78         }
79 
80         boolean updated = false;
81         JSONObject result = getResultFromKG(album.getTitle(), "MusicAlbum");
82 
83         // TODO: (b/128383917) Investigate how to filter search results
84         String imageUrl = getImageUrl(result);
85         if (imageUrl != null) {
86             updated |= album.setAlbumArtUri(Uri.parse(imageUrl));
87         }
88         String detailedDescription = getDetailedDescription(result);
89         if (detailedDescription != null) {
90             updated |= album.setDescription(detailedDescription);
91         }
92         return updated;
93     }
94 
95     @Override
populateMovie(@onNull Movie movie)96     public boolean populateMovie(@NonNull Movie movie) throws IOException {
97         boolean updated = false;
98         JSONObject result = getResultFromKG(movie.getTitle(), "Movie");
99 
100         String imageUrl = getImageUrl(result);
101         if (imageUrl != null) {
102             updated |= movie.setPosterUri(Uri.parse(imageUrl));
103         }
104         String detailedDescription = getDetailedDescription(result);
105         if (detailedDescription != null) {
106             updated |= movie.setDescription(detailedDescription);
107         }
108         return updated;
109     }
110 
111     @Override
populateSeries(@onNull Series series)112     public boolean populateSeries(@NonNull Series series) throws IOException {
113         boolean updated = false;
114         JSONObject result = getResultFromKG(series.getTitle(), "TVSeries");
115 
116         String imageUrl = getImageUrl(result);
117         if (imageUrl != null) {
118             updated |= series.setPosterUri(Uri.parse(imageUrl));
119         }
120         String detailedDescription = getDetailedDescription(result);
121         if (detailedDescription != null) {
122             updated |= series.setDescription(detailedDescription);
123         }
124         return updated;
125     }
126 
127     @Override
populateEpisode(@onNull Episode episode)128     public boolean populateEpisode(@NonNull Episode episode) throws IOException {
129         boolean updated = false;
130         JSONObject result = getResultFromKG(episode.getSeries().getTitle(), "TVEpisode");
131 
132         String imageUrl = getImageUrl(result);
133         if (imageUrl != null) {
134             updated |= episode.setPosterUri(Uri.parse(imageUrl));
135         }
136         String detailedDescription = getDetailedDescription(result);
137         if (detailedDescription != null) {
138             updated |= episode.setDescription(detailedDescription);
139         }
140         return updated;
141     }
142 
getResultFromKG(String title, String... types)143     private @NonNull JSONObject getResultFromKG(String title, String... types) throws IOException {
144         try {
145             JSONObject root = (JSONObject) getContent(getContentUri(title, types));
146             JSONArray items = root.getJSONArray("itemListElement");
147             JSONObject item = (JSONObject) items.get(0);
148             JSONObject result = item.getJSONObject("result");
149             if (!title.equals(result.getString("name"))) {
150                 throw new IOException("Failed to find result for " + title);
151             }
152             return result;
153         } catch (JSONException e) {
154             throw new IOException("Failed to find result for " + title);
155         }
156     }
157 
getImageUrl(@onNull JSONObject result)158     private @Nullable String getImageUrl(@NonNull JSONObject result) {
159         String imageUrl = null;
160         try {
161             JSONObject imageObj = result.optJSONObject("image");
162             if (imageObj != null) {
163                 String url = imageObj.getString("contentUrl");
164                 if (url != null) {
165                     // TODO (b/125143807): Remove once HTTPS scheme urls are retrieved.
166                     imageUrl = url.replaceFirst("^http://", "https://");
167                 }
168             }
169         } catch (JSONException e) {
170             Clog.w(TAG, "Failed to parse image url", e);
171         }
172         return imageUrl;
173     }
174 
getDescription(@onNull JSONObject result)175     private @Nullable String getDescription(@NonNull JSONObject result) {
176         String description = null;
177         try {
178             description = result.getString("description");
179         } catch (JSONException e) {
180             Clog.w(TAG, "Failed to parse description", e);
181         }
182         return description;
183     }
184 
getDetailedDescription(@onNull JSONObject result)185     private @Nullable String getDetailedDescription(@NonNull JSONObject result) {
186         String detailedDescription = null;
187         try {
188             JSONObject descriptionObj = result.optJSONObject("detailedDescription");
189             if (descriptionObj != null) {
190                 detailedDescription = descriptionObj.getString("articleBody");
191             }
192         } catch (JSONException e) {
193             Clog.w(TAG, "Failed to parse detailed description", e);
194         }
195         return detailedDescription;
196     }
197 
getContentUri(@onNull String title, @NonNull String... types)198     private static @NonNull Uri getContentUri(@NonNull String title, @NonNull String... types) {
199         Uri.Builder ub = new Uri.Builder();
200         ub.scheme("https");
201         ub.authority("kgsearch.googleapis.com");
202         ub.appendPath("v1");
203         ub.appendEncodedPath("entities:search");
204         ub.appendQueryParameter("key", ApiKeys.KG_API);
205         ub.appendQueryParameter("limit", "1");
206         ub.appendQueryParameter("query", title);
207         for (String type : types) {
208             ub.appendQueryParameter("types", type);
209         }
210         return ub.build();
211     }
212 
getContent(@onNull Uri uri)213     private static @NonNull Object getContent(@NonNull Uri uri) throws IOException, JSONException {
214         return new JSONTokener(new String(Http.get(uri.toString()), StandardCharsets.UTF_8))
215                 .nextValue();
216     }
217 }
218