Merge "Return ENOENT if app tries to access other packages fuse path" into rvc-dev
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 16a0892..84b2b10 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -57,7 +57,6 @@
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.text.TextUtils;
-import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
@@ -609,6 +608,15 @@
public static final String QUERY_ARG_RELATED_URI = "android:query-arg-related-uri";
/**
+ * Flag that can be used to enable movement of media items on disk through
+ * {@link ContentResolver#update} calls. This is typically true for
+ * third-party apps, but false for system components.
+ *
+ * @hide
+ */
+ public static final String QUERY_ARG_ALLOW_MOVEMENT = "android:query-arg-allow-movement";
+
+ /**
* Specify how {@link MediaColumns#IS_PENDING} items should be filtered when
* performing a {@link MediaStore} operation.
* <p>
@@ -719,7 +727,7 @@
/** @hide */
@Deprecated
public static boolean getIncludePending(@NonNull Uri uri) {
- return parseBoolean(uri.getQueryParameter(MediaStore.PARAM_INCLUDE_PENDING));
+ return uri.getBooleanQueryParameter(MediaStore.PARAM_INCLUDE_PENDING, false);
}
/**
@@ -749,7 +757,7 @@
* @see MediaStore#setRequireOriginal(Uri)
*/
public static boolean getRequireOriginal(@NonNull Uri uri) {
- return parseBoolean(uri.getQueryParameter(MediaStore.PARAM_REQUIRE_ORIGINAL));
+ return uri.getBooleanQueryParameter(MediaStore.PARAM_REQUIRE_ORIGINAL, false);
}
/**
@@ -3735,13 +3743,6 @@
return volumeName;
}
- private static boolean parseBoolean(@Nullable String value) {
- if (value == null) return false;
- if ("1".equals(value)) return true;
- if ("true".equalsIgnoreCase(value)) return true;
- return false;
- }
-
/**
* Uri for querying the state of the media scanner.
*/
diff --git a/legacy/src/com/android/providers/media/LegacyMediaProvider.java b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
index a505ee1..6e152d4 100644
--- a/legacy/src/com/android/providers/media/LegacyMediaProvider.java
+++ b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
@@ -20,15 +20,19 @@
import static com.android.providers.media.DatabaseHelper.INTERNAL_DATABASE_NAME;
import android.content.ContentProvider;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
+import android.content.OperationApplicationException;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.provider.MediaStore.MediaColumns;
+import android.util.ArraySet;
import androidx.annotation.NonNull;
@@ -38,7 +42,9 @@
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.Objects;
+import java.util.Set;
/**
* Very limited subset of {@link MediaProvider} which only surfaces
@@ -106,13 +112,41 @@
}
@Override
- public Uri insert(Uri uri, ContentValues values) {
+ public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
+ throws OperationApplicationException {
+ // Open transactions on databases for requested volumes
+ final Set<DatabaseHelper> transactions = new ArraySet<>();
try {
- final File file = new File(values.getAsString(MediaColumns.DATA));
- file.getParentFile().mkdirs();
- file.createNewFile();
- } catch (IOException e) {
- throw new IllegalStateException(e);
+ for (ContentProviderOperation op : operations) {
+ final DatabaseHelper helper = getDatabaseForUri(op.getUri());
+ if (!transactions.contains(helper)) {
+ helper.beginTransaction();
+ transactions.add(helper);
+ }
+ }
+
+ final ContentProviderResult[] result = super.applyBatch(operations);
+ for (DatabaseHelper helper : transactions) {
+ helper.setTransactionSuccessful();
+ }
+ return result;
+ } finally {
+ for (DatabaseHelper helper : transactions) {
+ helper.endTransaction();
+ }
+ }
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ if (!uri.getBooleanQueryParameter("silent", false)) {
+ try {
+ final File file = new File(values.getAsString(MediaColumns.DATA));
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
}
final DatabaseHelper helper = getDatabaseForUri(uri);
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 47be764..2d36456 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -23,7 +23,7 @@
<string name="unknown" msgid="2059049215682829375">"અજાણ"</string>
<string name="root_images" msgid="5861633549189045666">"છબીઓ"</string>
<string name="root_videos" msgid="8792703517064649453">"વિડિઓઝ"</string>
- <string name="root_audio" msgid="3505830755201326018">"ઑડિઓ"</string>
+ <string name="root_audio" msgid="3505830755201326018">"ઑડિયો"</string>
<string name="root_documents" msgid="3829103301363849237">"દસ્તાવેજો"</string>
<string name="permission_required" msgid="1460820436132943754">"આ આઇટમમાં ફેરફાર કરવા માટે અથવા તેને ડિલીટ કરવા માટે પરવાનગી હોવી જરૂરી છે."</string>
<string name="permission_required_action" msgid="706370952366113539">"આગળ વધો"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 09eac9f..80999fa 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -21,7 +21,7 @@
<string name="app_label" msgid="9035307001052716210">"Przechowywanie multimediów"</string>
<string name="artist_label" msgid="8105600993099120273">"Wykonawca"</string>
<string name="unknown" msgid="2059049215682829375">"Nieznany"</string>
- <string name="root_images" msgid="5861633549189045666">"Grafika"</string>
+ <string name="root_images" msgid="5861633549189045666">"Obrazy"</string>
<string name="root_videos" msgid="8792703517064649453">"Filmy"</string>
<string name="root_audio" msgid="3505830755201326018">"Dźwięk"</string>
<string name="root_documents" msgid="3829103301363849237">"Dokumenty"</string>
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index afa3d63..a61075b 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -69,7 +69,6 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;
@@ -112,6 +111,13 @@
long mScanStopTime;
/**
+ * Flag indicating that this database should invoke
+ * {@link #migrateFromLegacy} to migrate from a legacy database, typically
+ * only set when this database is starting from scratch.
+ */
+ boolean mMigrateFromLegacy;
+
+ /**
* Lock used to guard against deadlocks in SQLite; the write lock is used to
* guard any schema changes, and the read lock is used for all other
* database operations.
@@ -242,7 +248,7 @@
db.setCustomScalarFunction("_INSERT", (arg) -> {
if (arg != null && mFilesListener != null
&& !mSchemaLock.isWriteLockedByCurrentThread()) {
- final String[] split = arg.split(":");
+ final String[] split = arg.split(":", 4);
final String volumeName = split[0];
final long id = Long.parseLong(split[1]);
final int mediaType = Integer.parseInt(split[2]);
@@ -261,7 +267,7 @@
db.setCustomScalarFunction("_UPDATE", (arg) -> {
if (arg != null && mFilesListener != null
&& !mSchemaLock.isWriteLockedByCurrentThread()) {
- final String[] split = arg.split(":");
+ final String[] split = arg.split(":", 10);
final String volumeName = split[0];
final long oldId = Long.parseLong(split[1]);
final int oldMediaType = Integer.parseInt(split[2]);
@@ -271,8 +277,7 @@
final boolean newIsDownload = Integer.parseInt(split[6]) != 0;
final String oldOwnerPackage = split[7];
final String newOwnerPackage = split[8];
- // Path can include ':', assume rest of split[9..length] is path.
- final String oldPath = String.join(":", Arrays.copyOfRange(split, 9, split.length));
+ final String oldPath = split[9];
Trace.beginSection("_UPDATE");
try {
@@ -288,14 +293,13 @@
db.setCustomScalarFunction("_DELETE", (arg) -> {
if (arg != null && mFilesListener != null
&& !mSchemaLock.isWriteLockedByCurrentThread()) {
- final String[] split = arg.split(":");
+ final String[] split = arg.split(":", 6);
final String volumeName = split[0];
final long id = Long.parseLong(split[1]);
final int mediaType = Integer.parseInt(split[2]);
final boolean isDownload = Integer.parseInt(split[3]) != 0;
final String ownerPackage = split[4];
- // Path can include ':', assume rest of split[5..length] is path.
- final String path = String.join(":", Arrays.copyOfRange(split, 5, split.length));
+ final String path = split[5];
Trace.beginSection("_DELETE");
try {
@@ -353,6 +357,22 @@
}
}
+ @Override
+ public void onOpen(final SQLiteDatabase db) {
+ Log.v(TAG, "onOpen() for " + mName);
+ if (mMigrateFromLegacy) {
+ // Clear flag, since we should only attempt once
+ mMigrateFromLegacy = false;
+
+ mSchemaLock.writeLock().lock();
+ try {
+ migrateFromLegacy(db);
+ } finally {
+ mSchemaLock.writeLock().unlock();
+ }
+ }
+ }
+
@GuardedBy("mProjectionMapCache")
private final ArrayMap<Class<?>, ArrayMap<String, String>>
mProjectionMapCache = new ArrayMap<>();
@@ -760,7 +780,7 @@
// Since this code is used by both the legacy and modern providers, we
// only want to migrate when we're running as the modern provider
if (!mLegacyProvider) {
- migrateFromLegacy(db);
+ mMigrateFromLegacy = true;
}
}
@@ -789,8 +809,8 @@
extras.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE);
extras.putInt(MediaStore.QUERY_ARG_MATCH_FAVORITE, MediaStore.MATCH_INCLUDE);
- db.execSQL("SAVEPOINT before_migrate");
- Log.d(TAG, "Starting migration from legacy provider for " + mName);
+ db.beginTransaction();
+ Log.d(TAG, "Starting migration from legacy provider");
mMigrationListener.onStarted(client, mVolumeName);
try (Cursor c = client.query(queryUri, sMigrateColumns.toArray(new String[0]),
extras, null)) {
@@ -809,17 +829,19 @@
// When migrating pending or trashed files, we might need to
// rename them on disk to match new schema
- FileUtils.computeDataFromValues(values,
- new File(FileUtils.extractVolumePath(data)));
- final String recomputedData = values.getAsString(MediaColumns.DATA);
- if (!Objects.equals(data, recomputedData)) {
- try {
- Os.rename(data, recomputedData);
- } catch (ErrnoException e) {
- // We only have one shot to migrate data, so log and
- // keep marching forward
- Log.w(TAG, "Failed to rename " + values + "; continuing");
- FileUtils.computeValuesFromData(values);
+ final String volumePath = FileUtils.extractVolumePath(data);
+ if (volumePath != null) {
+ FileUtils.computeDataFromValues(values, new File(volumePath));
+ final String recomputedData = values.getAsString(MediaColumns.DATA);
+ if (!Objects.equals(data, recomputedData)) {
+ try {
+ Os.rename(data, recomputedData);
+ } catch (ErrnoException e) {
+ // We only have one shot to migrate data, so log and
+ // keep marching forward
+ Log.w(TAG, "Failed to rename " + values + "; continuing");
+ FileUtils.computeValuesFromData(values);
+ }
}
}
@@ -828,18 +850,28 @@
// keep marching forward
Log.w(TAG, "Failed to insert " + values + "; continuing");
}
+
+ // To avoid SQLITE_NOMEM errors, we need to periodically
+ // flush the current transaction and start another one
+ if ((c.getPosition() % 1_000) == 0) {
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ db.beginTransaction();
+ }
}
- db.execSQL("RELEASE before_migrate");
- Log.d(TAG, "Finished migration from legacy provider for " + mName);
- mMigrationListener.onFinished(client, mVolumeName);
+ Log.d(TAG, "Finished migration from legacy provider");
} catch (Exception e) {
// We have to guard ourselves against any weird behavior of the
// legacy provider by trying to catch everything
- db.execSQL("ROLLBACK TO before_migrate");
Log.wtf(TAG, "Failed migration from legacy provider", e);
- mMigrationListener.onFinished(client, mVolumeName);
}
+
+ // We tried our best above to migrate everything we could, and we
+ // only have one possible shot, so mark everything successful
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ mMigrationListener.onFinished(client, mVolumeName);
}
}
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 84a0a56..40325cb 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -2445,7 +2445,7 @@
values.put(FileColumns.VOLUME_NAME, extractVolumeName(path));
values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path));
values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path));
- values.put(FileColumns.IS_DOWNLOAD, isDownload(path));
+ values.put(FileColumns.IS_DOWNLOAD, isDownload(path) ? 1 : 0);
File file = new File(path);
if (file.exists()) {
values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000);
@@ -2746,12 +2746,13 @@
try {
return qb.insert(helper, values);
} catch (SQLiteConstraintException e) {
- final long rowId = getIdIfPathExistsForCallingPackage(qb, helper, path);
+ SQLiteQueryBuilder qbForUpsert = getQueryBuilderForUpsert(path);
+ final long rowId = getIdIfPathExistsForCallingPackage(qbForUpsert, helper, path);
// Apps sometimes create a file via direct path and then insert it into
// MediaStore via ContentResolver. The former should create a database entry,
// so we have to treat the latter as an upsert.
// TODO(b/149917493) Perform all INSERT operations as UPSERT.
- if (rowId != -1 && qb.update(helper, values, "_id=?",
+ if (rowId != -1 && qbForUpsert.update(helper, values, "_id=?",
new String[]{Long.toString(rowId)}) == 1) {
return rowId;
}
@@ -2782,6 +2783,28 @@
return -1;
}
+ /**
+ * @return {@link SQLiteQueryBuilder} for upsert with Files uri. This disables strict columns
+ * check to allow upsert to update any column with Files uri.
+ */
+ private SQLiteQueryBuilder getQueryBuilderForUpsert(@NonNull String path) {
+ final Uri uri = Files.getContentUriForPath(path);
+ final boolean allowHidden = isCallingPackageAllowedHidden();
+ // When Fuse inserts a file to database it doesn't set is_download column. When app tries
+ // insert with Downloads uri, upsert fails because getIdIfPathExistsForCallingPackage can't
+ // find a row ID with is_download=1. Use Files uri to query & update any existing row
+ // irrespective of is_download=1.
+ SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, matchUri(uri, allowHidden), uri,
+ Bundle.EMPTY, null);
+
+ // We won't be able to update columns that are not part of projection map of Files table. We
+ // have already checked strict columns in previous insert operation which failed with
+ // exception. Any malicious column usage would have got caught in insert operation, hence we
+ // can safely disable strict column check for upsert.
+ qb.setStrictColumns(false);
+ return qb;
+ }
+
private void maybePut(@NonNull ContentValues values, @NonNull String key,
@Nullable String value) {
if (value != null) {
@@ -2792,7 +2815,7 @@
private boolean maybeMarkAsDownload(@NonNull ContentValues values) {
final String path = values.getAsString(MediaColumns.DATA);
if (path != null && isDownload(path)) {
- values.put(FileColumns.IS_DOWNLOAD, true);
+ values.put(FileColumns.IS_DOWNLOAD, 1);
return true;
}
return false;
@@ -3108,7 +3131,7 @@
case DOWNLOADS:
maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
- initialValues.put(FileColumns.IS_DOWNLOAD, true);
+ initialValues.put(FileColumns.IS_DOWNLOAD, 1);
rowId = insertFile(qb, helper, match, uri, extras, initialValues,
FileColumns.MEDIA_TYPE_NONE, false);
if (rowId > 0) {
@@ -3192,14 +3215,6 @@
}
}
- @VisibleForTesting
- static boolean parseBoolean(String value) {
- if (value == null) return false;
- if ("1".equals(value)) return true;
- if ("true".equalsIgnoreCase(value)) return true;
- return false;
- }
-
@Deprecated
private String getSharedPackages(String callingPackage) {
final String[] sharedPackageNames = mCallingIdentity.get().getSharedPackageNames();
@@ -3248,7 +3263,7 @@
}
final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- if (parseBoolean(uri.getQueryParameter("distinct"))) {
+ if (uri.getBooleanQueryParameter("distinct", false)) {
qb.setDistinct(true);
}
qb.setStrict(true);
@@ -4556,7 +4571,7 @@
final Uri playlistUri = ContentUris.withAppendedId(
MediaStore.Audio.Playlists.getContentUri(volumeName), playlistId);
- if (parseBoolean(uri.getQueryParameter("move"))) {
+ if (uri.getBooleanQueryParameter("move", false)) {
// Convert explicit request into query; sigh, moveItem()
// uses zero-based indexing instead of one-based indexing
final int from = Integer.parseInt(uri.getPathSegments().get(5)) + 1;
@@ -4713,16 +4728,14 @@
break;
}
- // TODO: remove this as part of fixing b/151768142
- final boolean isCallingPackageSystem = isCallingPackageSystem()
- && !"com.android.systemui".equals(getCallingPackageOrSelf());
-
// If we're touching columns that would change placement of a file,
// blend in current values and recalculate path
+ final boolean allowMovement = extras.getBoolean(MediaStore.QUERY_ARG_ALLOW_MOVEMENT,
+ !isCallingPackageSystem());
if (containsAny(initialValues.keySet(), sPlacementColumns)
&& !initialValues.containsKey(MediaColumns.DATA)
- && !isCallingPackageSystem
- && !isThumbnail) {
+ && !isThumbnail
+ && allowMovement) {
Trace.beginSection("movement");
// We only support movement under well-defined collections
diff --git a/src/com/android/providers/media/PermissionActivity.java b/src/com/android/providers/media/PermissionActivity.java
index 0c398dd..0696ef5 100644
--- a/src/com/android/providers/media/PermissionActivity.java
+++ b/src/com/android/providers/media/PermissionActivity.java
@@ -20,6 +20,7 @@
import static com.android.providers.media.MediaProvider.IMAGES_MEDIA_ID;
import static com.android.providers.media.MediaProvider.VIDEO_MEDIA_ID;
import static com.android.providers.media.MediaProvider.collectUris;
+import static com.android.providers.media.util.DatabaseUtils.getAsBoolean;
import static com.android.providers.media.util.Logging.TAG;
import android.app.Activity;
@@ -198,6 +199,7 @@
for (Uri uri : uris) {
ops.add(ContentProviderOperation.newUpdate(uri)
.withValues(values)
+ .withExtra(MediaStore.QUERY_ARG_ALLOW_MOVEMENT, true)
.withExceptionAllowed(true)
.build());
}
@@ -290,10 +292,10 @@
case MediaStore.CREATE_WRITE_REQUEST_CALL:
return VERB_WRITE;
case MediaStore.CREATE_TRASH_REQUEST_CALL:
- return (values.getAsInteger(MediaColumns.IS_TRASHED) != 0)
+ return getAsBoolean(values, MediaColumns.IS_TRASHED, false)
? VERB_TRASH : VERB_UNTRASH;
case MediaStore.CREATE_FAVORITE_REQUEST_CALL:
- return (values.getAsInteger(MediaColumns.IS_FAVORITE) != 0)
+ return getAsBoolean(values, MediaColumns.IS_FAVORITE, false)
? VERB_FAVORITE : VERB_UNFAVORITE;
case MediaStore.CREATE_DELETE_REQUEST_CALL:
return VERB_DELETE;
diff --git a/src/com/android/providers/media/util/DatabaseUtils.java b/src/com/android/providers/media/util/DatabaseUtils.java
index ef33b04..a5ab700 100644
--- a/src/com/android/providers/media/util/DatabaseUtils.java
+++ b/src/com/android/providers/media/util/DatabaseUtils.java
@@ -48,7 +48,9 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import java.util.Locale;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -532,10 +534,27 @@
return sb.toString();
}
+ public static boolean parseBoolean(@Nullable Object value, boolean def) {
+ if (value instanceof Boolean) {
+ return (Boolean) value;
+ } else if (value instanceof Number) {
+ return ((Number) value).intValue() != 0;
+ } else if (value instanceof String) {
+ final String stringValue = ((String) value).toLowerCase(Locale.ROOT);
+ return (!"false".equals(stringValue) && !"0".equals(stringValue));
+ } else {
+ return def;
+ }
+ }
+
+ public static boolean getAsBoolean(@NonNull Bundle extras,
+ @NonNull String key, boolean def) {
+ return parseBoolean(extras.get(key), def);
+ }
+
public static boolean getAsBoolean(@NonNull ContentValues values,
@NonNull String key, boolean def) {
- final Integer value = values.getAsInteger(key);
- return (value != null) ? (value != 0) : def;
+ return parseBoolean(values.get(key), def);
}
public static long getAsLong(@NonNull ContentValues values,
diff --git a/src/com/android/providers/media/util/FileUtils.java b/src/com/android/providers/media/util/FileUtils.java
index d96b465..6b0cd80 100644
--- a/src/com/android/providers/media/util/FileUtils.java
+++ b/src/com/android/providers/media/util/FileUtils.java
@@ -35,6 +35,7 @@
import static com.android.providers.media.util.DatabaseUtils.getAsBoolean;
import static com.android.providers.media.util.DatabaseUtils.getAsLong;
+import static com.android.providers.media.util.DatabaseUtils.parseBoolean;
import static com.android.providers.media.util.Logging.TAG;
import android.content.ClipDescription;
@@ -957,18 +958,18 @@
// Only define the field when this modification is actually adjusting
// one of the flags that should influence the expiration
- final Integer pending = values.getAsInteger(MediaColumns.IS_PENDING);
+ final Object pending = values.get(MediaColumns.IS_PENDING);
if (pending != null) {
- if (pending != 0) {
+ if (parseBoolean(pending, false)) {
values.put(MediaColumns.DATE_EXPIRES,
(System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000);
} else {
values.putNull(MediaColumns.DATE_EXPIRES);
}
}
- final Integer trashed = values.getAsInteger(MediaColumns.IS_TRASHED);
+ final Object trashed = values.get(MediaColumns.IS_TRASHED);
if (trashed != null) {
- if (trashed != 0) {
+ if (parseBoolean(trashed, false)) {
values.put(MediaColumns.DATE_EXPIRES,
(System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000);
} else {
@@ -986,7 +987,6 @@
// Worst case we have to assume no bucket details
values.remove(MediaColumns.VOLUME_NAME);
values.remove(MediaColumns.RELATIVE_PATH);
- values.remove(MediaColumns.IS_DOWNLOAD);
values.remove(MediaColumns.IS_PENDING);
values.remove(MediaColumns.IS_TRASHED);
values.remove(MediaColumns.DATE_EXPIRES);
@@ -1002,8 +1002,6 @@
values.put(MediaColumns.VOLUME_NAME, extractVolumeName(data));
values.put(MediaColumns.RELATIVE_PATH, extractRelativePath(data));
- values.put(MediaColumns.IS_DOWNLOAD, isDownload(data));
-
final String displayName = extractDisplayName(data);
final Matcher matcher = FileUtils.PATTERN_EXPIRES_FILE.matcher(displayName);
if (matcher.matches()) {
diff --git a/tests/client/Android.bp b/tests/client/Android.bp
index 9d5be41..0d29bbf 100644
--- a/tests/client/Android.bp
+++ b/tests/client/Android.bp
@@ -21,6 +21,7 @@
static_libs: [
"androidx.test.rules",
"mockito-target",
+ "truth-prebuilt",
],
certificate: "media",
diff --git a/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java b/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
index fa2de15..71f79a1 100644
--- a/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
+++ b/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
@@ -24,6 +24,7 @@
import android.app.UiAutomation;
import android.content.ContentProviderClient;
+import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
@@ -49,6 +50,8 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import com.google.common.truth.Truth;
+
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
@@ -61,6 +64,7 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
@@ -80,6 +84,11 @@
private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
private static final long POLLING_SLEEP_MILLIS = 100;
+ /**
+ * Number of media items to insert for {@link #testLegacy_Extreme()}.
+ */
+ private static final int EXTREME_COUNT = 10_000;
+
private Uri mExternalAudio;
private Uri mExternalVideo;
private Uri mExternalImages;
@@ -182,6 +191,67 @@
doLegacy(mExternalDownloads, values);
}
+ /**
+ * Verify that a legacy database with thousands of media entries can be
+ * successfully migrated.
+ */
+ @Test
+ public void testLegacy_Extreme() throws Exception {
+ final Context context = InstrumentationRegistry.getTargetContext();
+ final UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+
+ final ProviderInfo legacyProvider = context.getPackageManager()
+ .resolveContentProvider(MediaStore.AUTHORITY_LEGACY, 0);
+ final ProviderInfo modernProvider = context.getPackageManager()
+ .resolveContentProvider(MediaStore.AUTHORITY, 0);
+
+ // Only continue if we have both providers to test against
+ Assume.assumeNotNull(legacyProvider);
+ Assume.assumeNotNull(modernProvider);
+
+ // Wait until everything calms down
+ MediaStore.waitForIdle(context.getContentResolver());
+
+ // Clear data on the legacy provider so that we create a database
+ executeShellCommand("pm clear " + legacyProvider.applicationInfo.packageName, ui);
+
+ // Create thousands of items in the legacy provider
+ try (ContentProviderClient legacy = context.getContentResolver()
+ .acquireContentProviderClient(MediaStore.AUTHORITY_LEGACY)) {
+ // We're purposefully "silent" to avoid creating the raw file on
+ // disk, since otherwise this test would take several minutes
+ final Uri insertTarget = rewriteToLegacy(
+ mExternalImages.buildUpon().appendQueryParameter("silent", "true").build());
+
+ final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+ for (int i = 0; i < EXTREME_COUNT; i++) {
+ ops.add(ContentProviderOperation.newInsert(insertTarget)
+ .withValues(generateValues(FileColumns.MEDIA_TYPE_IMAGE, "image/png",
+ Environment.DIRECTORY_PICTURES))
+ .build());
+
+ if ((ops.size() > 1_000) || (i == (EXTREME_COUNT - 1))) {
+ Log.v(TAG, "Inserting items...");
+ legacy.applyBatch(MediaStore.AUTHORITY_LEGACY, ops);
+ ops.clear();
+ }
+ }
+ }
+
+ // Clear data on the modern provider so that the initial scan recovers
+ // metadata from the legacy provider
+ executeShellCommand("pm clear " + modernProvider.applicationInfo.packageName, ui);
+ pollForExternalStorageState();
+
+ // Confirm that details from legacy provider have migrated
+ try (ContentProviderClient modern = context.getContentResolver()
+ .acquireContentProviderClient(MediaStore.AUTHORITY)) {
+ try (Cursor cursor = modern.query(mExternalImages, null, null, null)) {
+ Truth.assertThat(cursor.getCount()).isAtLeast(EXTREME_COUNT);
+ }
+ }
+ }
+
private void doLegacy(Uri collectionUri, ContentValues values) throws Exception {
final Context context = InstrumentationRegistry.getTargetContext();
final UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation();
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index 0f0ed64..6e9fda9 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -741,18 +741,6 @@
}
@Test
- public void testParseBoolean() throws Exception {
- assertTrue(MediaProvider.parseBoolean("TRUE"));
- assertTrue(MediaProvider.parseBoolean("true"));
- assertTrue(MediaProvider.parseBoolean("1"));
-
- assertFalse(MediaProvider.parseBoolean("FALSE"));
- assertFalse(MediaProvider.parseBoolean("false"));
- assertFalse(MediaProvider.parseBoolean("0"));
- assertFalse(MediaProvider.parseBoolean(null));
- }
-
- @Test
public void testIsDownload() throws Exception {
assertTrue(isDownload("/storage/emulated/0/Download/colors.png"));
assertTrue(isDownload("/storage/emulated/0/Download/test.pdf"));
diff --git a/tests/src/com/android/providers/media/scan/DrmTest.java b/tests/src/com/android/providers/media/scan/DrmTest.java
index 038a93e..c7ed122 100644
--- a/tests/src/com/android/providers/media/scan/DrmTest.java
+++ b/tests/src/com/android/providers/media/scan/DrmTest.java
@@ -97,6 +97,7 @@
@Test
public void testForwardLock_Audio() throws Exception {
+ Assume.assumeTrue(isForwardLockSupported());
doForwardLock("audio/mpeg", R.raw.test_audio, (values) -> {
assertEquals(1_045L, (long) values.getAsLong(FileColumns.DURATION));
assertEquals(FileColumns.MEDIA_TYPE_AUDIO,
@@ -106,6 +107,7 @@
@Test
public void testForwardLock_Video() throws Exception {
+ Assume.assumeTrue(isForwardLockSupported());
doForwardLock("video/mp4", R.raw.test_video, (values) -> {
assertEquals(40_000L, (long) values.getAsLong(FileColumns.DURATION));
assertEquals(FileColumns.MEDIA_TYPE_VIDEO,
@@ -115,6 +117,7 @@
@Test
public void testForwardLock_Image() throws Exception {
+ Assume.assumeTrue(isForwardLockSupported());
doForwardLock("image/jpeg", R.raw.test_image, (values) -> {
// ExifInterface currently doesn't know how to scan DRM images, so
// the best we can do is verify the base test metadata
@@ -125,6 +128,7 @@
@Test
public void testForwardLock_Binary() throws Exception {
+ Assume.assumeTrue(isForwardLockSupported());
doForwardLock("application/octet-stream", R.raw.test_image, null);
}
@@ -134,6 +138,8 @@
*/
@Test
public void testForwardLock_130680734() throws Exception {
+ Assume.assumeTrue(isForwardLockSupported());
+
final ContentValues values = new ContentValues();
values.put(MediaColumns.DISPLAY_NAME, "temp" + System.nanoTime() + ".fl");
values.put(MediaColumns.MIME_TYPE, MIME_FORWARD_LOCKED);
@@ -185,8 +191,6 @@
private void doForwardLock(String mimeType, int resId,
@Nullable Consumer<ContentValues> verifier) throws Exception {
- Assume.assumeTrue(isForwardLockSupported());
-
InputStream dmStream = createDmStream(mimeType, resId);
File flPath = new File(mContext.getExternalMediaDirs()[0],
diff --git a/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java b/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java
index 3a0ea75..d4ff968 100644
--- a/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java
@@ -32,6 +32,7 @@
import static android.database.DatabaseUtils.escapeForLike;
import static com.android.providers.media.util.DatabaseUtils.maybeBalance;
+import static com.android.providers.media.util.DatabaseUtils.parseBoolean;
import static com.android.providers.media.util.DatabaseUtils.recoverAbusiveLimit;
import static com.android.providers.media.util.DatabaseUtils.recoverAbusiveSortOrder;
import static com.android.providers.media.util.DatabaseUtils.resolveQueryArgs;
@@ -368,6 +369,24 @@
escapeForLike("/path/to/fi%le.bin"));
}
+ @Test
+ public void testParseBoolean() throws Exception {
+ assertTrue(parseBoolean("TRUE", false));
+ assertTrue(parseBoolean("true", false));
+ assertTrue(parseBoolean("1", false));
+ assertTrue(parseBoolean(1, false));
+ assertTrue(parseBoolean(true, false));
+
+ assertFalse(parseBoolean("FALSE", true));
+ assertFalse(parseBoolean("false", true));
+ assertFalse(parseBoolean("0", true));
+ assertFalse(parseBoolean(0, true));
+ assertFalse(parseBoolean(false, true));
+
+ assertFalse(parseBoolean(null, false));
+ assertTrue(parseBoolean(null, true));
+ }
+
private static Pair<String, String> recoverAbusiveGroupBy(
Pair<String, String> selectionAndGroupBy) {
final Bundle queryArgs = new Bundle();