Consistent treatment of boolean-ish values.

The public API clearly states that our various IS_FOO columns are
of type Cursor.FIELD_TYPE_INTEGER, but developers often rely on how
SQLite automatically converts booleans and literal strings into
integer "1" and "0" values on disk.

This change adjusts all Java logic to parse all variants of these
boolean values to match what SQLite does internally.

Bug: 154054403
Test: atest --test-mapping packages/providers/MediaProvider
Change-Id: I1f3ec650c957a6c5207c5803fb667ea006666cd0
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 9158204..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;
@@ -728,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);
     }
 
     /**
@@ -758,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);
     }
 
     /**
@@ -3744,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/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 49bb111..abd3bdf 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -2444,7 +2444,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);
@@ -2791,7 +2791,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;
@@ -3107,7 +3107,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) {
@@ -3191,14 +3191,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();
@@ -3247,7 +3239,7 @@
         }
 
         final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
-        if (parseBoolean(uri.getQueryParameter("distinct"))) {
+        if (uri.getBooleanQueryParameter("distinct", false)) {
             qb.setDistinct(true);
         }
         qb.setStrict(true);
@@ -4555,7 +4547,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;
diff --git a/src/com/android/providers/media/PermissionActivity.java b/src/com/android/providers/media/PermissionActivity.java
index 85bcbe5..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;
@@ -291,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 78f2c6f..d7269ec 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 {
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/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();