summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apex/framework/java/android/provider/ExportedSince.java41
-rw-r--r--apex/framework/java/android/provider/MediaStore.java1
-rw-r--r--legacy/src/com/android/providers/media/LegacyMediaProvider.java4
-rw-r--r--src/com/android/providers/media/DatabaseHelper.java48
-rw-r--r--src/com/android/providers/media/MediaProvider.java9
-rw-r--r--src/com/android/providers/media/MediaUpgradeReceiver.java3
-rw-r--r--tests/src/com/android/providers/media/DatabaseHelperTest.java28
-rw-r--r--tests/src/com/android/providers/media/MediaProviderTest.java106
-rw-r--r--tests/src/com/android/providers/media/photopicker/data/ExternalDbFacadeTest.java3
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);
}
}
}