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