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.db;
18 
19 import android.content.ContentResolver;
20 import android.database.ContentObserver;
21 import android.database.Cursor;
22 import android.net.Uri;
23 import android.os.Build;
24 import android.provider.MediaStore;
25 
26 import androidx.annotation.AnyThread;
27 import androidx.annotation.NonNull;
28 import androidx.annotation.Nullable;
29 import androidx.annotation.WorkerThread;
30 
31 import com.android.pump.provider.Query;
32 import com.android.pump.util.Clog;
33 
34 import java.io.File;
35 import java.util.ArrayList;
36 import java.util.Collection;
37 
38 @WorkerThread
39 class VideoStore extends ContentObserver {
40     private static final String TAG = Clog.tag(VideoStore.class);
41 
42     // TODO Replace the following with MediaStore.Video.Media.RELATIVE_PATH throughout the code.
43     private static final String RELATIVE_PATH = "relative_path";
44 
45     // TODO Replace with Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q throughout the code.
isRunningQ()46     private static boolean isRunningQ() {
47         return Build.VERSION.SDK_INT > Build.VERSION_CODES.P
48                 || (Build.VERSION.SDK_INT == Build.VERSION_CODES.P
49                 && Build.VERSION.PREVIEW_SDK_INT > 0);
50     }
51 
52     private final ContentResolver mContentResolver;
53     private final ChangeListener mChangeListener;
54     private final MediaProvider mMediaProvider;
55 
56     interface ChangeListener {
onMoviesAdded(@onNull Collection<Movie> movies)57         void onMoviesAdded(@NonNull Collection<Movie> movies);
onSeriesAdded(@onNull Collection<Series> series)58         void onSeriesAdded(@NonNull Collection<Series> series);
onEpisodesAdded(@onNull Collection<Episode> episodes)59         void onEpisodesAdded(@NonNull Collection<Episode> episodes);
onOthersAdded(@onNull Collection<Other> others)60         void onOthersAdded(@NonNull Collection<Other> others);
61     }
62 
63     @AnyThread
VideoStore(@onNull ContentResolver contentResolver, @NonNull ChangeListener changeListener, @NonNull MediaProvider mediaProvider)64     VideoStore(@NonNull ContentResolver contentResolver, @NonNull ChangeListener changeListener,
65             @NonNull MediaProvider mediaProvider) {
66         super(null);
67 
68         Clog.i(TAG, "VideoStore(" + contentResolver + ", " + changeListener
69                 + ", " + mediaProvider + ")");
70         mContentResolver = contentResolver;
71         mChangeListener = changeListener;
72         mMediaProvider = mediaProvider;
73 
74         // TODO(b/123706961) Do we need content observer for other content uris? (E.g. thumbnail)
75         mContentResolver.registerContentObserver(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
76                 true, this);
77 
78         // TODO(b/123706961) When to call unregisterContentObserver?
79         // mContentResolver.unregisterContentObserver(this);
80     }
81 
load()82     void load() {
83         Clog.i(TAG, "load()");
84         Collection<Movie> movies = new ArrayList<>();
85         Collection<Series> series = new ArrayList<>();
86         Collection<Episode> episodes = new ArrayList<>();
87         Collection<Other> others = new ArrayList<>();
88 
89         /* TODO get via count instead?
90                 Cursor countCursor = mContentResolver.query(CONTENT_URI,
91                 new String[] { "count(*) AS count" },
92                 null,
93                 null,
94                 null);
95         countCursor.moveToFirst();
96         int count = countCursor.getInt(0);
97         Clog.i(TAG, "count = " + count);
98         countCursor.close();
99         */
100 
101         {
102             Uri contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
103             String[] projection;
104             if (isRunningQ()) {
105                 projection = new String[] {
106                     MediaStore.Video.Media._ID,
107                     MediaStore.Video.Media.MIME_TYPE,
108                     RELATIVE_PATH,
109                     MediaStore.Video.Media.DISPLAY_NAME
110                 };
111             } else {
112                 projection = new String[] {
113                     MediaStore.Video.Media._ID,
114                     MediaStore.Video.Media.MIME_TYPE,
115                     MediaStore.Video.Media.DATA
116                 };
117             }
118             String sortOrder = MediaStore.Video.Media._ID;
119             Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder);
120             if (cursor != null) {
121                 try {
122                     int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID);
123                     int dataColumn;
124                     int relativePathColumn;
125                     int displayNameColumn;
126                     int mimeTypeColumn = cursor.getColumnIndexOrThrow(
127                             MediaStore.Video.Media.MIME_TYPE);
128 
129                     if (isRunningQ()) {
130                         dataColumn = -1;
131                         relativePathColumn = cursor.getColumnIndexOrThrow(RELATIVE_PATH);
132                         displayNameColumn = cursor.getColumnIndexOrThrow(
133                                 MediaStore.Video.Media.DISPLAY_NAME);
134                     } else {
135                         dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA);
136                         relativePathColumn = -1;
137                         displayNameColumn = -1;
138                     }
139 
140                     for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
141                         long id = cursor.getLong(idColumn);
142                         String mimeType = cursor.getString(mimeTypeColumn);
143 
144                         File file;
145                         if (isRunningQ()) {
146                             String relativePath = cursor.getString(relativePathColumn);
147                             String displayName = cursor.getString(displayNameColumn);
148                             file = new File(relativePath, displayName);
149                         } else {
150                             String data = cursor.getString(dataColumn);
151                             file = new File(data);
152                         }
153                         Query query = Query.parse(Uri.fromFile(file));
154                         if (query.isMovie()) {
155                             Movie movie;
156                             if (query.hasYear()) {
157                                 movie = new Movie(id, mimeType, query.getName(), query.getYear());
158                             } else {
159                                 movie = new Movie(id, mimeType, query.getName());
160                             }
161                             movies.add(movie);
162                         } else if (query.isEpisode()) {
163                             Series serie = null;
164                             for (Series s : series) {
165                                 if (s.getTitle().equals(query.getName())
166                                         && s.hasYear() == query.hasYear()
167                                         && (!s.hasYear() || s.getYear() == query.getYear())) {
168                                     serie = s;
169                                     break;
170                                 }
171                             }
172                             if (serie == null) {
173                                 if (query.hasYear()) {
174                                     serie = new Series(query.getName(), query.getYear());
175                                 } else {
176                                     serie = new Series(query.getName());
177                                 }
178                                 series.add(serie);
179                             }
180 
181                             Episode episode = new Episode(id, mimeType, serie,
182                                     query.getSeason(), query.getEpisode());
183                             episodes.add(episode);
184 
185                             serie.addEpisode(episode);
186                         } else {
187                             Other other = new Other(id, mimeType, query.getName());
188                             others.add(other);
189                         }
190                     }
191                 } finally {
192                     cursor.close();
193                 }
194             }
195         }
196 
197         mChangeListener.onMoviesAdded(movies);
198         mChangeListener.onSeriesAdded(series);
199         mChangeListener.onEpisodesAdded(episodes);
200         mChangeListener.onOthersAdded(others);
201     }
202 
loadData(@onNull Movie movie)203     boolean loadData(@NonNull Movie movie) {
204         Uri thumbnailUri = getThumbnailUri(movie.getId());
205         if (thumbnailUri != null) {
206             return movie.setThumbnailUri(thumbnailUri);
207         }
208         return false;
209     }
210 
loadData(@onNull Series series)211     boolean loadData(@NonNull Series series) {
212         return false;
213     }
214 
loadData(@onNull Episode episode)215     boolean loadData(@NonNull Episode episode) {
216         Uri thumbnailUri = getThumbnailUri(episode.getId());
217         if (thumbnailUri != null) {
218             return episode.setThumbnailUri(thumbnailUri);
219         }
220         return false;
221     }
222 
loadData(@onNull Other other)223     boolean loadData(@NonNull Other other) {
224         boolean updated = false;
225 
226         Uri contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
227         String[] projection = {
228             MediaStore.Video.Media.TITLE,
229             MediaStore.Video.Media.DURATION,
230             MediaStore.Video.Media.DATE_TAKEN,
231             MediaStore.Video.Media.LATITUDE,
232             MediaStore.Video.Media.LONGITUDE
233         };
234         String selection = MediaStore.Video.Media._ID + " = ?";
235         String[] selectionArgs = { Long.toString(other.getId()) };
236         Cursor cursor = mContentResolver.query(
237                 contentUri, projection, selection, selectionArgs, null);
238         if (cursor != null) {
239             try {
240                 int titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.TITLE);
241                 int durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION);
242                 int dateTakenColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_TAKEN);
243                 int latitudeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.LATITUDE);
244                 int longitudeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.LONGITUDE);
245 
246                 if (cursor.moveToFirst()) {
247                     if (!cursor.isNull(titleColumn)) {
248                         String title = cursor.getString(titleColumn);
249                         updated |= other.setTitle(title);
250                     }
251                     if (!cursor.isNull(durationColumn)) {
252                         long duration = cursor.getLong(durationColumn);
253                         updated |= other.setDuration(duration);
254                     }
255                     if (!cursor.isNull(dateTakenColumn)) {
256                         long dateTaken = cursor.getLong(dateTakenColumn);
257                         updated |= other.setDateTaken(dateTaken);
258                     }
259                     if (!cursor.isNull(latitudeColumn) && !cursor.isNull(longitudeColumn)) {
260                         double latitude = cursor.getDouble(latitudeColumn);
261                         double longitude = cursor.getDouble(longitudeColumn);
262                         updated |= other.setLatLong(latitude, longitude);
263                     }
264                 }
265             } finally {
266                 cursor.close();
267             }
268         }
269 
270         Uri thumbnailUri = getThumbnailUri(other.getId());
271         if (thumbnailUri != null) {
272             updated |= other.setThumbnailUri(thumbnailUri);
273         }
274 
275         return updated;
276     }
277 
getThumbnailUri(long id)278     private @Nullable Uri getThumbnailUri(long id) {
279         int thumbKind = MediaStore.Video.Thumbnails.MINI_KIND;
280 
281         // TODO(b/123707512) The following line is required to generate thumbnails -- is there a better way?
282         MediaStore.Video.Thumbnails.getThumbnail(mContentResolver, id, thumbKind, null);
283 
284         Uri thumbnailUri = null;
285         Uri contentUri = MediaStore.Video.Thumbnails.EXTERNAL_CONTENT_URI;
286         String[] projection = {
287             MediaStore.Video.Thumbnails.DATA
288         };
289         String selection = MediaStore.Video.Thumbnails.KIND + " = " + thumbKind + " AND " +
290                 MediaStore.Video.Thumbnails.VIDEO_ID + " = ?";
291         String[] selectionArgs = { Long.toString(id) };
292         Cursor cursor = mContentResolver.query(
293                 contentUri, projection, selection, selectionArgs, null);
294         if (cursor != null) {
295             try {
296                 int dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Thumbnails.DATA);
297 
298                 if (cursor.moveToFirst()) {
299                     String data = cursor.getString(dataColumn);
300 
301                     thumbnailUri = Uri.fromFile(new File(data));
302                 }
303             } finally {
304                 cursor.close();
305             }
306         }
307         return thumbnailUri;
308     }
309 
310     @Override
onChange(boolean selfChange)311     public void onChange(boolean selfChange) {
312         Clog.i(TAG, "onChange(" + selfChange + ")");
313         onChange(selfChange, null);
314     }
315 
316     @Override
onChange(boolean selfChange, @Nullable Uri uri)317     public void onChange(boolean selfChange, @Nullable Uri uri) {
318         Clog.i(TAG, "onChange(" + selfChange + ", " + uri + ")");
319         // TODO(b/123706961) Figure out what changed
320         // onChange(false, content://media)
321         // onChange(false, content://media/external)
322         // onChange(false, content://media/external/audio/media/444)
323         // onChange(false, content://media/external/video/media/328?blocking=1&orig_id=328&group_id=0)
324 
325         // TODO(b/123706961) Notify listener about changes
326         // mChangeListener.xxx();
327     }
328 }
329