diff options
author | 2021-10-08 16:04:41 +0800 | |
---|---|---|
committer | 2021-10-20 14:18:00 +0800 | |
commit | 1fb4d347768512ee5cf0f08e505fb0adb1209e9c (patch) | |
tree | aa77a087d651179f622898973d119a15d3720086 | |
parent | ccda642bbd4416cc465564a954e4fe6b6837587e (diff) |
Hide the column in query result by @ExportedSince value
- Add the @ExportedSince Build.VERSION_CODES.S on is_recording
- If the column is added @ExportedSince by X version, if the os
version is less than X, the query result won't include the column.
Test: atest MediaProviderTest
Test: atest DatabaseHelperTest
Bug: 202040814
Change-Id: Ib019feb0ef715ac1b828ae5c39140bcfb80cf51e
9 files changed, 213 insertions, 30 deletions
diff --git a/apex/framework/java/android/provider/ExportedSince.java b/apex/framework/java/android/provider/ExportedSince.java new file mode 100644 index 000000000..20a1a116a --- /dev/null +++ b/apex/framework/java/android/provider/ExportedSince.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2021 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 android.provider; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import androidx.annotation.IntRange; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Denotes that a field is exported starting with the OS version + * @hide + */ +@Documented +@Retention(RUNTIME) +@Target({FIELD}) +public @interface ExportedSince { + /** + * The API level of the OS version to require. + */ + @IntRange(from = 1) + int osVersion() default 1; +} diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java index a27378bfe..be447f40e 100644 --- a/apex/framework/java/android/provider/MediaStore.java +++ b/apex/framework/java/android/provider/MediaStore.java @@ -2979,6 +2979,7 @@ public final class MediaStore { * {@link #IS_NOTIFICATION}, {@link #IS_PODCAST}, * and {@link #IS_RINGTONE}. */ + @ExportedSince(osVersion = Build.VERSION_CODES.S) @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true) public static final String IS_RECORDING = "is_recording"; diff --git a/legacy/src/com/android/providers/media/LegacyMediaProvider.java b/legacy/src/com/android/providers/media/LegacyMediaProvider.java index 4c7e74155..d51414b6b 100644 --- a/legacy/src/com/android/providers/media/LegacyMediaProvider.java +++ b/legacy/src/com/android/providers/media/LegacyMediaProvider.java @@ -80,9 +80,9 @@ public class LegacyMediaProvider extends ContentProvider { Logging.initPersistent(persistentDir); mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, false, true, null, - null, null, null, null); + null, null, null, null, null); mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME, false, true, null, - null, null, null, null); + null, null, null, null, null); return true; } diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java index 81968f6b1..e8831e086 100644 --- a/src/com/android/providers/media/DatabaseHelper.java +++ b/src/com/android/providers/media/DatabaseHelper.java @@ -34,6 +34,7 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.mtp.MtpConstants; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.RemoteException; @@ -122,6 +123,7 @@ public class DatabaseHelper extends SQLiteOpenHelper implements AutoCloseable { final boolean mEarlyUpgrade; final boolean mLegacyProvider; final @Nullable Class<? extends Annotation> mColumnAnnotation; + final @Nullable Class<? extends Annotation> mExportedSinceAnnotation; final @Nullable OnSchemaChangeListener mSchemaListener; final @Nullable OnFilesChangeListener mFilesListener; final @Nullable OnLegacyMigrationListener mMigrationListener; @@ -155,6 +157,7 @@ public class DatabaseHelper extends SQLiteOpenHelper implements AutoCloseable { public interface OnFilesChangeListener { public void onInsert(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id, int mediaType, boolean isDownload, boolean isPending); + public void onUpdate(@NonNull DatabaseHelper helper, @NonNull String volumeName, long oldId, int oldMediaType, boolean oldIsDownload, long newId, int newMediaType, boolean newIsDownload, @@ -162,31 +165,37 @@ public class DatabaseHelper extends SQLiteOpenHelper implements AutoCloseable { boolean oldIsPending, boolean newIsPending, boolean oldIsFavorite, boolean newIsFavorite, String oldOwnerPackage, String newOwnerPackage, String oldPath); + public void onDelete(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id, int mediaType, boolean isDownload, String ownerPackage, String path); } public interface OnLegacyMigrationListener { public void onStarted(ContentProviderClient client, String volumeName); + public void onProgress(ContentProviderClient client, String volumeName, long progress, long total); + public void onFinished(ContentProviderClient client, String volumeName); } public DatabaseHelper(Context context, String name, boolean earlyUpgrade, boolean legacyProvider, @Nullable Class<? extends Annotation> columnAnnotation, + @Nullable Class<? extends Annotation> exportedSinceAnnotation, @Nullable OnSchemaChangeListener schemaListener, @Nullable OnFilesChangeListener filesListener, @NonNull OnLegacyMigrationListener migrationListener, @Nullable UnaryOperator<String> idGenerator) { this(context, name, getDatabaseVersion(context), earlyUpgrade, legacyProvider, - columnAnnotation, schemaListener, filesListener, migrationListener, idGenerator); + columnAnnotation, exportedSinceAnnotation, schemaListener, filesListener, + migrationListener, idGenerator); } public DatabaseHelper(Context context, String name, int version, boolean earlyUpgrade, boolean legacyProvider, @Nullable Class<? extends Annotation> columnAnnotation, + @Nullable Class<? extends Annotation> exportedSinceAnnotation, @Nullable OnSchemaChangeListener schemaListener, @Nullable OnFilesChangeListener filesListener, @NonNull OnLegacyMigrationListener migrationListener, @@ -205,6 +214,7 @@ public class DatabaseHelper extends SQLiteOpenHelper implements AutoCloseable { mEarlyUpgrade = earlyUpgrade; mLegacyProvider = legacyProvider; mColumnAnnotation = columnAnnotation; + mExportedSinceAnnotation = exportedSinceAnnotation; mSchemaListener = schemaListener; mFilesListener = filesListener; mMigrationListener = migrationListener; @@ -454,10 +464,13 @@ public class DatabaseHelper extends SQLiteOpenHelper implements AutoCloseable { map = new ArrayMap<>(); try { for (Field field : clazz.getFields()) { - if (Objects.equals(field.getName(), "_ID") - || field.isAnnotationPresent(mColumnAnnotation)) { - final String column = (String) field.get(null); - map.put(column, column); + if (Objects.equals(field.getName(), "_ID") || (mColumnAnnotation != null + && field.isAnnotationPresent(mColumnAnnotation))) { + boolean shouldIgnoreByOsVersion = shouldBeIgnoredByOsVersion(field); + if (!shouldIgnoreByOsVersion) { + final String column = (String) field.get(null); + map.put(column, column); + } } } } catch (ReflectiveOperationException e) { @@ -1986,6 +1999,31 @@ public class DatabaseHelper extends SQLiteOpenHelper implements AutoCloseable { } } + private boolean shouldBeIgnoredByOsVersion(@NonNull Field field) { + if (mExportedSinceAnnotation == null) { + return false; + } + + if (!field.isAnnotationPresent(mExportedSinceAnnotation)) { + return false; + } + + try { + final Annotation annotation = field.getAnnotation(mExportedSinceAnnotation); + final int exportedSinceOSVersion = (int) annotation.annotationType().getMethod( + "osVersion").invoke(annotation); + final boolean shouldIgnore = exportedSinceOSVersion > Build.VERSION.SDK_INT; + if (shouldIgnore) { + Log.d(TAG, "Ignoring column " + field.get(null) + " with version " + + exportedSinceOSVersion + " in OS version " + Build.VERSION.SDK_INT); + } + return shouldIgnore; + } catch (Exception e) { + Log.e(TAG, "Can't parse the OS version in ExportedSince annotation", e); + return false; + } + } + /** * Return the current generation that will be populated into * {@link MediaColumns#GENERATION_ADDED} or diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java index beaa4378c..c559b5374 100644 --- a/src/com/android/providers/media/MediaProvider.java +++ b/src/com/android/providers/media/MediaProvider.java @@ -169,6 +169,7 @@ import android.provider.Column; import android.provider.DeviceConfig; import android.provider.DeviceConfig.OnPropertiesChangedListener; import android.provider.DocumentsContract; +import android.provider.ExportedSince; import android.provider.MediaStore; import android.provider.MediaStore.Audio; import android.provider.MediaStore.Audio.AudioColumns; @@ -951,11 +952,11 @@ public class MediaProvider extends ContentProvider { mMediaScanner = new ModernMediaScanner(context); mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, false, false, - Column.class, Metrics::logSchemaChange, mFilesListener, MIGRATION_LISTENER, - mIdGenerator); + Column.class, ExportedSince.class, Metrics::logSchemaChange, mFilesListener, + MIGRATION_LISTENER, mIdGenerator); mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME, false, false, - Column.class, Metrics::logSchemaChange, mFilesListener, MIGRATION_LISTENER, - mIdGenerator); + Column.class, ExportedSince.class, Metrics::logSchemaChange, mFilesListener, + MIGRATION_LISTENER, mIdGenerator); mExternalDbFacade = new ExternalDbFacade(getContext(), mExternalDatabase); mPickerDbFacade = new PickerDbFacade(context); mPickerSyncController = new PickerSyncController(context, mPickerDbFacade); diff --git a/src/com/android/providers/media/MediaUpgradeReceiver.java b/src/com/android/providers/media/MediaUpgradeReceiver.java index ea7233026..983892a8d 100644 --- a/src/com/android/providers/media/MediaUpgradeReceiver.java +++ b/src/com/android/providers/media/MediaUpgradeReceiver.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.provider.Column; +import android.provider.ExportedSince; import android.util.Log; import com.android.providers.media.util.ForegroundThread; @@ -74,7 +75,7 @@ public class MediaUpgradeReceiver extends BroadcastReceiver { Log.i(TAG, "---> Start upgrade of media database " + file); try { DatabaseHelper helper = new DatabaseHelper(context, file, false, false, - Column.class, Metrics::logSchemaChange, null, + Column.class, ExportedSince.class, Metrics::logSchemaChange, null, MediaProvider.MIGRATION_LISTENER, null); helper.runWithTransaction((db) -> { // Perform just enough to force database upgrade diff --git a/tests/src/com/android/providers/media/DatabaseHelperTest.java b/tests/src/com/android/providers/media/DatabaseHelperTest.java index 07be69c1b..08324d6e0 100644 --- a/tests/src/com/android/providers/media/DatabaseHelperTest.java +++ b/tests/src/com/android/providers/media/DatabaseHelperTest.java @@ -42,6 +42,7 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.os.UserHandle; import android.provider.Column; +import android.provider.ExportedSince; import android.provider.MediaStore.Audio.AudioColumns; import android.provider.MediaStore.Audio; import android.provider.MediaStore.Files.FileColumns; @@ -635,8 +636,8 @@ public class DatabaseHelperTest { private static class DatabaseHelperO extends DatabaseHelper { public DatabaseHelperO(Context context, String name) { - super(context, name, DatabaseHelper.VERSION_O, - false, false, Column.class, null, null, null, null); + super(context, name, DatabaseHelper.VERSION_O, false, false, Column.class, + ExportedSince.class, null, null, null, null); } @Override @@ -647,8 +648,8 @@ public class DatabaseHelperTest { private static class DatabaseHelperP extends DatabaseHelper { public DatabaseHelperP(Context context, String name) { - super(context, name, DatabaseHelper.VERSION_P, - false, false, Column.class, null, null, null, null); + super(context, name, DatabaseHelper.VERSION_P, false, false, Column.class, + ExportedSince.class, null, null, null, null); } @Override @@ -659,8 +660,8 @@ public class DatabaseHelperTest { private static class DatabaseHelperQ extends DatabaseHelper { public DatabaseHelperQ(Context context, String name) { - super(context, name, DatabaseHelper.VERSION_Q, - false, false, Column.class, null, null, null, null); + super(context, name, DatabaseHelper.VERSION_Q, false, false, Column.class, + ExportedSince.class, null, null, null, null); } @Override @@ -671,9 +672,8 @@ public class DatabaseHelperTest { private static class DatabaseHelperR extends DatabaseHelper { public DatabaseHelperR(Context context, String name) { - super(context, name, DatabaseHelper.VERSION_R, - false, false, Column.class, null, null, - MediaProvider.MIGRATION_LISTENER, null); + super(context, name, DatabaseHelper.VERSION_R, false, false, Column.class, + ExportedSince.class, null, null, MediaProvider.MIGRATION_LISTENER, null); } @Override @@ -684,9 +684,8 @@ public class DatabaseHelperTest { private static class DatabaseHelperS extends DatabaseHelper { public DatabaseHelperS(Context context, String name) { - super(context, name, VERSION_S, - false, false, Column.class, null, null, - MediaProvider.MIGRATION_LISTENER, null); + super(context, name, VERSION_S, false, false, Column.class, ExportedSince.class, null, + null, MediaProvider.MIGRATION_LISTENER, null); } @@ -698,9 +697,8 @@ public class DatabaseHelperTest { private static class DatabaseHelperT extends DatabaseHelper { public DatabaseHelperT(Context context, String name) { - super(context, name, DatabaseHelper.VERSION_T, - false, false, Column.class, null, null, - MediaProvider.MIGRATION_LISTENER, null); + super(context, name, DatabaseHelper.VERSION_T, false, false, Column.class, + ExportedSince.class, null, null, MediaProvider.MIGRATION_LISTENER, null); } } diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java index 558930c11..6e6f3b83f 100644 --- a/tests/src/com/android/providers/media/MediaProviderTest.java +++ b/tests/src/com/android/providers/media/MediaProviderTest.java @@ -46,6 +46,7 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.database.Cursor; +import android.database.sqlite.SQLiteException; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -61,6 +62,7 @@ import android.util.ArrayMap; import android.util.Log; import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SdkSuppress; import androidx.test.runner.AndroidJUnit4; import com.android.providers.media.MediaProvider.FallbackException; @@ -81,7 +83,6 @@ import org.junit.runner.RunWith; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.PrintWriter; -import java.sql.Array; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -1278,6 +1279,109 @@ public class MediaProviderTest { assertQueryResultNoItems(MediaStore.Audio.Genres.getContentUri(volume)); } + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R, maxSdkVersion = Build.VERSION_CODES.R) + public void testQueryAudioTableNoIsRecordingColumnInR() throws Exception { + final File file = createAudioRecordingFile(); + final Uri audioUri = + MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); + + try (Cursor c = sIsolatedResolver.query(audioUri, null, null, null, null)) { + assertThat(c).isNotNull(); + assertThat(c.getCount()).isEqualTo(1); + assertThat(c.getColumnIndex("is_recording")).isEqualTo(-1); + } finally { + file.delete(); + final File dir = file.getParentFile(); + dir.delete(); + } + } + + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R, maxSdkVersion = Build.VERSION_CODES.R) + public void testQueryIsRecordingInAudioTableExceptionInR() throws Exception { + final File file = createAudioRecordingFile(); + final Uri audioUri = + MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); + final String[] projection = new String[]{"is_recording"}; + + try (Cursor c = sIsolatedResolver.query(audioUri, projection, null, null, null)) { + fail("Expected exception with the is_recording is not a column in Audio table"); + } catch (IllegalArgumentException | SQLiteException expected) { + } finally { + file.delete(); + final File dir = file.getParentFile(); + dir.delete(); + } + } + + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S) + public void testQueryAudioTableHasIsRecordingColumnAfterR() throws Exception { + final File file = createAudioRecordingFile(); + final Uri audioUri = + MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); + + try (Cursor c = sIsolatedResolver.query(audioUri, null, null, null, null)) { + assertThat(c).isNotNull(); + assertThat(c.getCount()).isEqualTo(1); + final int columnIndex = c.getColumnIndex(AudioColumns.IS_RECORDING); + assertThat(columnIndex).isNotEqualTo(-1); + assertThat(c.moveToFirst()).isTrue(); + assertThat(c.getInt(columnIndex)).isEqualTo(1); + } finally { + file.delete(); + final File dir = file.getParentFile(); + dir.delete(); + } + } + + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S) + public void testQueryIsRecordingInAudioTableAfterR() throws Exception { + final File file = createAudioRecordingFile(); + final Uri audioUri = + MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); + final String[] projection = new String[]{AudioColumns.IS_RECORDING}; + + try (Cursor c = sIsolatedResolver.query(audioUri, projection, null, null, null)) { + assertThat(c).isNotNull(); + assertThat(c.getCount()).isEqualTo(1); + assertThat(c.moveToFirst()).isTrue(); + assertThat(c.getInt(0)).isEqualTo(1); + } finally { + file.delete(); + final File dir = file.getParentFile(); + dir.delete(); + } + } + + private File createAudioRecordingFile() throws Exception { + // We might have old files lurking, so force a clean slate + final Context context = InstrumentationRegistry.getTargetContext(); + sIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false); + sIsolatedResolver = sIsolatedContext.getContentResolver(); + final File dir = Environment + .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + final File recordingDir = new File(dir, "Recordings"); + recordingDir.mkdirs(); + final String displayName = "test" + System.nanoTime() + ".mp3"; + final File audio = new File(recordingDir, displayName); + stage(R.raw.test_audio, audio); + final Uri result = MediaStore.scanFile(sIsolatedResolver, audio); + + // Check the audio music file exists + try (Cursor c = sIsolatedResolver.query(result, + new String[]{MediaColumns.DISPLAY_NAME, AudioColumns.IS_MUSIC}, null, null)) { + assertThat(c).isNotNull(); + assertThat(c.getCount()).isEqualTo(1); + assertThat(c.moveToFirst()).isTrue(); + assertThat(c.getString(0)).isEqualTo(displayName); + assertThat(c.getInt(1)).isEqualTo(0); + } + return audio; + } + private static void assertQueryResultNoItems(Uri uri) throws Exception { try (Cursor c = sIsolatedResolver.query(uri, null, null, null, null)) { assertNotNull(c); diff --git a/tests/src/com/android/providers/media/photopicker/data/ExternalDbFacadeTest.java b/tests/src/com/android/providers/media/photopicker/data/ExternalDbFacadeTest.java index a25bd0096..15058f54e 100644 --- a/tests/src/com/android/providers/media/photopicker/data/ExternalDbFacadeTest.java +++ b/tests/src/com/android/providers/media/photopicker/data/ExternalDbFacadeTest.java @@ -870,8 +870,7 @@ public class ExternalDbFacadeTest { private static class TestDatabaseHelper extends DatabaseHelper { public TestDatabaseHelper(Context context) { - super(context, TEST_CLEAN_DB, 1, - false, false, null, null, null, null, null); + super(context, TEST_CLEAN_DB, 1, false, false, null, null, null, null, null, null); } } } |