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