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