| /* |
| * Copyright 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.pump.db; |
| |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.database.ContentObserver; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.provider.MediaStore; |
| |
| import androidx.annotation.AnyThread; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.WorkerThread; |
| |
| import com.android.pump.provider.Query; |
| import com.android.pump.util.Clog; |
| |
| import java.io.File; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| |
| @WorkerThread |
| class VideoStore extends ContentObserver { |
| private static final String TAG = Clog.tag(VideoStore.class); |
| |
| // TODO Replace the following with MediaStore.Video.Media.RELATIVE_PATH throughout the code. |
| private static final String RELATIVE_PATH = "relative_path"; |
| |
| // TODO Replace with Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q throughout the code. |
| private static boolean isAtLeastRunningQ() { |
| return Build.VERSION.SDK_INT > Build.VERSION_CODES.P |
| || (Build.VERSION.SDK_INT == Build.VERSION_CODES.P |
| && Build.VERSION.PREVIEW_SDK_INT > 0); |
| } |
| |
| private final ContentResolver mContentResolver; |
| private final ChangeListener mChangeListener; |
| private final MediaProvider mMediaProvider; |
| |
| interface ChangeListener { |
| void onMoviesAdded(@NonNull Collection<Movie> movies); |
| void onSeriesAdded(@NonNull Collection<Series> series); |
| void onEpisodesAdded(@NonNull Collection<Episode> episodes); |
| void onOthersAdded(@NonNull Collection<Other> others); |
| } |
| |
| @AnyThread |
| VideoStore(@NonNull ContentResolver contentResolver, @NonNull ChangeListener changeListener, |
| @NonNull MediaProvider mediaProvider) { |
| super(null); |
| |
| Clog.i(TAG, "VideoStore(" + contentResolver + ", " + changeListener |
| + ", " + mediaProvider + ")"); |
| mContentResolver = contentResolver; |
| mChangeListener = changeListener; |
| mMediaProvider = mediaProvider; |
| |
| // TODO(b/123706961) Do we need content observer for other content uris? (E.g. thumbnail) |
| mContentResolver.registerContentObserver(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, |
| true, this); |
| |
| // TODO(b/123706961) When to call unregisterContentObserver? |
| // mContentResolver.unregisterContentObserver(this); |
| } |
| |
| void load() { |
| Clog.i(TAG, "load()"); |
| Collection<Movie> movies = new ArrayList<>(); |
| Collection<Series> series = new ArrayList<>(); |
| Collection<Episode> episodes = new ArrayList<>(); |
| Collection<Other> others = new ArrayList<>(); |
| |
| /* TODO get via count instead? |
| Cursor countCursor = mContentResolver.query(CONTENT_URI, |
| new String[] { "count(*) AS count" }, |
| null, |
| null, |
| null); |
| countCursor.moveToFirst(); |
| int count = countCursor.getInt(0); |
| Clog.i(TAG, "count = " + count); |
| countCursor.close(); |
| */ |
| |
| { |
| Uri contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; |
| String[] projection; |
| if (isAtLeastRunningQ()) { |
| projection = new String[] { |
| MediaStore.Video.Media._ID, |
| MediaStore.Video.Media.MIME_TYPE, |
| RELATIVE_PATH, |
| MediaStore.Video.Media.DISPLAY_NAME |
| }; |
| } else { |
| projection = new String[] { |
| MediaStore.Video.Media._ID, |
| MediaStore.Video.Media.MIME_TYPE, |
| MediaStore.Video.Media.DATA |
| }; |
| } |
| String sortOrder = MediaStore.Video.Media._ID; |
| Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder); |
| if (cursor != null) { |
| try { |
| int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID); |
| int dataColumn; |
| int relativePathColumn; |
| int displayNameColumn; |
| int mimeTypeColumn = cursor.getColumnIndexOrThrow( |
| MediaStore.Video.Media.MIME_TYPE); |
| |
| if (isAtLeastRunningQ()) { |
| dataColumn = -1; |
| relativePathColumn = cursor.getColumnIndexOrThrow(RELATIVE_PATH); |
| displayNameColumn = cursor.getColumnIndexOrThrow( |
| MediaStore.Video.Media.DISPLAY_NAME); |
| } else { |
| dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA); |
| relativePathColumn = -1; |
| displayNameColumn = -1; |
| } |
| |
| for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { |
| long id = cursor.getLong(idColumn); |
| String mimeType = cursor.getString(mimeTypeColumn); |
| |
| File file; |
| if (isAtLeastRunningQ()) { |
| String relativePath = cursor.getString(relativePathColumn); |
| String displayName = cursor.getString(displayNameColumn); |
| file = new File(relativePath, displayName); |
| } else { |
| String data = cursor.getString(dataColumn); |
| file = new File(data); |
| } |
| Query query = Query.parse(Uri.fromFile(file)); |
| if (query.isMovie()) { |
| Movie movie; |
| if (query.hasYear()) { |
| movie = new Movie(id, mimeType, query.getName(), query.getYear()); |
| } else { |
| movie = new Movie(id, mimeType, query.getName()); |
| } |
| movies.add(movie); |
| } else if (query.isEpisode()) { |
| Series serie = null; |
| for (Series s : series) { |
| if (s.getTitle().equals(query.getName()) |
| && s.hasYear() == query.hasYear() |
| && (!s.hasYear() || s.getYear() == query.getYear())) { |
| serie = s; |
| break; |
| } |
| } |
| if (serie == null) { |
| if (query.hasYear()) { |
| serie = new Series(query.getName(), query.getYear()); |
| } else { |
| serie = new Series(query.getName()); |
| } |
| series.add(serie); |
| } |
| |
| Episode episode = new Episode(id, mimeType, serie, |
| query.getSeason(), query.getEpisode()); |
| episodes.add(episode); |
| |
| serie.addEpisode(episode); |
| } else { |
| Other other = new Other(id, mimeType, query.getName()); |
| others.add(other); |
| } |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| } |
| |
| mChangeListener.onMoviesAdded(movies); |
| mChangeListener.onSeriesAdded(series); |
| mChangeListener.onEpisodesAdded(episodes); |
| mChangeListener.onOthersAdded(others); |
| } |
| |
| boolean loadData(@NonNull Movie movie) { |
| Uri thumbnailUri = getThumbnailUri(movie.getId()); |
| if (thumbnailUri != null) { |
| return movie.setThumbnailUri(thumbnailUri); |
| } |
| return false; |
| } |
| |
| boolean loadData(@NonNull Series series) { |
| return false; |
| } |
| |
| boolean loadData(@NonNull Episode episode) { |
| Uri thumbnailUri = getThumbnailUri(episode.getId()); |
| if (thumbnailUri != null) { |
| return episode.setThumbnailUri(thumbnailUri); |
| } |
| return false; |
| } |
| |
| boolean loadData(@NonNull Other other) { |
| boolean updated = false; |
| |
| Uri contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; |
| String[] projection = { |
| MediaStore.Video.Media.TITLE, |
| MediaStore.Video.Media.DURATION, |
| MediaStore.Video.Media.DATE_TAKEN, |
| MediaStore.Video.Media.LATITUDE, |
| MediaStore.Video.Media.LONGITUDE |
| }; |
| String selection = MediaStore.Video.Media._ID + " = ?"; |
| String[] selectionArgs = { Long.toString(other.getId()) }; |
| Cursor cursor = mContentResolver.query( |
| contentUri, projection, selection, selectionArgs, null); |
| if (cursor != null) { |
| try { |
| int titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.TITLE); |
| int durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION); |
| int dateTakenColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_TAKEN); |
| int latitudeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.LATITUDE); |
| int longitudeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.LONGITUDE); |
| |
| if (cursor.moveToFirst()) { |
| if (!cursor.isNull(titleColumn)) { |
| String title = cursor.getString(titleColumn); |
| updated |= other.setTitle(title); |
| } |
| if (!cursor.isNull(durationColumn)) { |
| long duration = cursor.getLong(durationColumn); |
| updated |= other.setDuration(duration); |
| } |
| if (!cursor.isNull(dateTakenColumn)) { |
| long dateTaken = cursor.getLong(dateTakenColumn); |
| updated |= other.setDateTaken(dateTaken); |
| } |
| if (!cursor.isNull(latitudeColumn) && !cursor.isNull(longitudeColumn)) { |
| double latitude = cursor.getDouble(latitudeColumn); |
| double longitude = cursor.getDouble(longitudeColumn); |
| updated |= other.setLatLong(latitude, longitude); |
| } |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| Uri thumbnailUri = getThumbnailUri(other.getId()); |
| if (thumbnailUri != null) { |
| updated |= other.setThumbnailUri(thumbnailUri); |
| } |
| |
| return updated; |
| } |
| |
| private @Nullable Uri getThumbnailUri(long id) { |
| // TODO(b/130363861) No need to store the URI -- generate when requested instead |
| return ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id) |
| .buildUpon().appendPath("thumbnail").build(); |
| } |
| |
| @Override |
| public void onChange(boolean selfChange) { |
| Clog.i(TAG, "onChange(" + selfChange + ")"); |
| onChange(selfChange, null); |
| } |
| |
| @Override |
| public void onChange(boolean selfChange, @Nullable Uri uri) { |
| Clog.i(TAG, "onChange(" + selfChange + ", " + uri + ")"); |
| // TODO(b/123706961) Figure out what changed |
| // onChange(false, content://media) |
| // onChange(false, content://media/external) |
| // onChange(false, content://media/external/audio/media/444) |
| // onChange(false, content://media/external/video/media/328?blocking=1&orig_id=328&group_id=0) |
| |
| // TODO(b/123706961) Notify listener about changes |
| // mChangeListener.xxx(); |
| } |
| } |