/* * 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(); } }