Snap for 8857176 from c59555d54e39f47325456ae4607b721a6203d860 to mainline-go-wifi-release

Change-Id: I4eeae78902e01ea62a48a8df45a311c49538bfd1
diff --git a/apex/Android.bp b/apex/Android.bp
index c735880..ccc26f5 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -40,7 +40,11 @@
 
 sdk {
     name: "mediaprovider-module-sdk",
-    bootclasspath_fragments: ["com.android.mediaprovider-bootclasspath-fragment"],
+    apexes: [
+        // Adds exportable dependencies of the APEX to the sdk,
+        // e.g. *classpath_fragments.
+        "com.android.mediaprovider",
+    ],
 }
 
 // Encapsulate the contributions made by the com.android.mediaprovider to the bootclasspath.
diff --git a/apex/apex_manifest.json b/apex/apex_manifest.json
index fe2ed11..527b9f7 100644
--- a/apex/apex_manifest.json
+++ b/apex/apex_manifest.json
@@ -1,4 +1,7 @@
 {
   "name": "com.android.mediaprovider",
-  "version": 339990000
+
+  // Placeholder module version to be replaced during build.
+  // Do not change!
+  "version": 0
 }
diff --git a/res/layout-v33/safety_protection_section.xml b/res/layout-v33/safety_protection_section.xml
new file mode 100644
index 0000000..155132f
--- /dev/null
+++ b/res/layout-v33/safety_protection_section.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 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.
+  -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+    <ImageView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:src="@android:drawable/ic_safety_protection"
+        android:contentDescription="@string/safety_protection_icon_label"/>
+
+    <TextView
+        android:id="@+id/safety_protection_display_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:paddingStart="8dp"
+        android:paddingEnd="0dp"
+        android:paddingTop="3dp"
+        android:textColor="?android:attr/textColorSecondary"
+        android:textSize="12sp" />
+</merge>
\ No newline at end of file
diff --git a/res/layout/activity_photo_picker.xml b/res/layout/activity_photo_picker.xml
index 1680806..fc73702 100644
--- a/res/layout/activity_photo_picker.xml
+++ b/res/layout/activity_photo_picker.xml
@@ -47,6 +47,12 @@
             android:src="@drawable/ic_drag"
             android:contentDescription="@null"/>
 
+        <com.android.providers.media.photopicker.ui.SafetyProtectionSectionView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="5dp"
+            android:layout_gravity="center" />
+
         <TextView
             android:id="@+id/privacy_text"
             android:layout_width="wrap_content"
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 05511da..c3dd711 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -118,6 +118,6 @@
     <string name="transcode_processing_success" msgid="447288876429730122">"ମିଡିଆ ପ୍ରକ୍ରିୟାକରଣ ସଫଳ ହୋଇଛି"</string>
     <string name="transcode_processing_started" msgid="7789086308155361523">"ମିଡିଆ ପ୍ରକ୍ରିୟାକରଣ ଆରମ୍ଭ କରାଯାଇଛି"</string>
     <string name="transcode_processing" msgid="6753136468864077258">"ମିଡିଆ ପ୍ରକ୍ରିୟାକରଣ କରାଯାଉଛି…"</string>
-    <string name="transcode_cancel" msgid="8555752601907598192">"ବାତିଲ୍ କରନ୍ତୁ"</string>
+    <string name="transcode_cancel" msgid="8555752601907598192">"ବାତିଲ କରନ୍ତୁ"</string>
     <string name="transcode_wait" msgid="8909773149560697501">"ଅପେକ୍ଷା କରନ୍ତୁ"</string>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d6b8deb..4b134d3 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -406,4 +406,7 @@
 
     <!-- Transcode intermediate ANR dialog wait button. -->
     <string name="transcode_wait">Wait</string>
+
+    <!-- Safety Protection shield icon. -->
+    <string name="safety_protection_icon_label">Safety protection</string>
 </resources>
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 6354dcb..5e10099 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -44,7 +44,6 @@
 import static android.provider.MediaStore.QUERY_ARG_MATCH_TRASHED;
 import static android.provider.MediaStore.QUERY_ARG_REDACTED_URI;
 import static android.provider.MediaStore.QUERY_ARG_RELATED_URI;
-import static android.provider.MediaStore.VOLUME_EXTERNAL;
 import static android.provider.MediaStore.getVolumeName;
 import static android.system.OsConstants.F_GETFL;
 
@@ -1313,9 +1312,9 @@
         // Cleaning media files for users that have been removed
         cleanMediaFilesForRemovedUser(signal);
 
-        // Populate _SPECIAL_FORMAT column for files which have column value as NULL
-        // TODO(b/236620024): Do not update generation_modified for special_format value update
-        // detectSpecialFormat(signal);
+        // Calculate standard_mime_type_extension column for files which have SPECIAL_FORMAT column
+        // value as NULL, and update the same in the picker db
+        detectSpecialFormat(signal);
 
         final long durationMillis = (SystemClock.elapsedRealtime() - startTime);
         Metrics.logIdleMaintenance(MediaStore.VOLUME_EXTERNAL, itemCount,
@@ -1428,9 +1427,14 @@
     private void updateSpecialFormatColumn(SQLiteDatabase db, @NonNull CancellationSignal signal) {
         // This is to ensure we only do a bounded iteration over the rows as updates can fail, and
         // we don't want to keep running the query/update indefinitely.
-        final int totalRowsToUpdate = getPendingSpecialFormatRowsCount(db,signal);
-        for (int i = 0 ; i < totalRowsToUpdate ; i += IDLE_MAINTENANCE_ROWS_LIMIT) {
-            updateSpecialFormatForLimitedRows(db, signal);
+        final int totalRowsToUpdate = getPendingSpecialFormatRowsCount(db, signal);
+        for (int i = 0; i < totalRowsToUpdate; i += IDLE_MAINTENANCE_ROWS_LIMIT) {
+            try (PickerDbFacade.UpdateMediaOperation operation =
+                         mPickerDbFacade.beginUpdateMediaOperation(
+                                 PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY)) {
+                updateSpecialFormatForLimitedRows(db, signal, operation);
+                operation.setSuccess();
+            }
         }
     }
 
@@ -1444,14 +1448,12 @@
         }
     }
 
-    private void updateSpecialFormatForLimitedRows(SQLiteDatabase db,
-            @NonNull CancellationSignal signal) {
-        final SQLiteQueryBuilder qbForUpdate = getQueryBuilder(TYPE_UPDATE, FILES,
-                Files.getContentUri(VOLUME_EXTERNAL), Bundle.EMPTY, null);
+    private void updateSpecialFormatForLimitedRows(SQLiteDatabase externalDb,
+            @NonNull CancellationSignal signal, PickerDbFacade.UpdateMediaOperation operation) {
         // Accumulate all the new SPECIAL_FORMAT updates with their ids
         ArrayMap<Long, Integer> newSpecialFormatValues = new ArrayMap<>();
         final String limit = String.valueOf(IDLE_MAINTENANCE_ROWS_LIMIT);
-        try (Cursor c = queryForPendingSpecialFormatColumns(db, limit, signal)) {
+        try (Cursor c = queryForPendingSpecialFormatColumns(externalDb, limit, signal)) {
             while (c.moveToNext() && !signal.isCanceled()) {
                 final long id = c.getLong(0);
                 final String path = c.getString(1);
@@ -1459,25 +1461,35 @@
             }
         }
 
-        // Now, update all the new SPECIAL_FORMAT values.
-        final ContentValues values = new ContentValues();
+        // Now, update all the new SPECIAL_FORMAT values in both external db and picker db.
+        final ContentValues pickerDbValues = new ContentValues();
+        final ContentValues externalDbValues = new ContentValues();
         int count = 0;
-        for (long id: newSpecialFormatValues.keySet()) {
+        for (long id : newSpecialFormatValues.keySet()) {
             if (signal.isCanceled()) {
                 return;
             }
 
-            values.clear();
-            values.put(_SPECIAL_FORMAT, newSpecialFormatValues.get(id));
-            final String selection = MediaColumns._ID + "=?";
-            final String[] selectionArgs = new String[]{String.valueOf(id)};
-            if (qbForUpdate.update(db, values, selection, selectionArgs) == 1) {
+            int specialFormat = newSpecialFormatValues.get(id);
+
+            pickerDbValues.clear();
+            pickerDbValues.put(PickerDbFacade.KEY_STANDARD_MIME_TYPE_EXTENSION, specialFormat);
+            boolean pickerDbWriteSuccess = operation.execute(String.valueOf(id), pickerDbValues);
+
+            externalDbValues.clear();
+            externalDbValues.put(_SPECIAL_FORMAT, specialFormat);
+            final String externalDbSelection = MediaColumns._ID + "=?";
+            final String[] externalDbSelectionArgs = new String[]{String.valueOf(id)};
+            boolean externalDbWriteSuccess =
+                    externalDb.update("files", externalDbValues, externalDbSelection,
+                            externalDbSelectionArgs)
+                            == 1;
+
+            if (pickerDbWriteSuccess && externalDbWriteSuccess) {
                 count++;
-            } else {
-                Log.e(TAG, "Unable to update _SPECIAL_FORMAT for id = " + id);
             }
         }
-        Log.d(TAG, "Updated _SPECIAL_FORMAT for " + count + " items");
+        Log.d(TAG, "Updated standard_mime_type_extension for " + count + " items");
     }
 
     private int getSpecialFormatValue(String path) {
@@ -3130,27 +3142,31 @@
                 return OsConstants.EPERM;
             }
 
-            // TODO(b/177049768): We shouldn't use getExternalStorageDirectory for these checks.
-            final File directoryAndroid = new File(Environment.getExternalStorageDirectory(),
-                    DIRECTORY_ANDROID_LOWER_CASE);
+            final File directoryAndroid = new File(
+                    extractVolumePath(oldPath).toLowerCase(Locale.ROOT),
+                    DIRECTORY_ANDROID_LOWER_CASE
+            );
             final File directoryAndroidMedia = new File(directoryAndroid, DIRECTORY_MEDIA);
+            String newPathLowerCase = newPath.toLowerCase(Locale.ROOT);
             if (directoryAndroidMedia.getAbsolutePath().equalsIgnoreCase(oldPath)) {
                 // Don't allow renaming 'Android/media' directory.
                 // Android/[data|obb] are bind mounted and these paths don't go through FUSE.
                 Log.e(TAG, errorMessage +  oldPath + " is a default folder in app external "
                         + "directory. Renaming a default folder is not allowed.");
                 return OsConstants.EPERM;
-            } else if (FileUtils.contains(directoryAndroid, new File(newPath))) {
-                if (newRelativePath.length == 1) {
-                    // New path is Android/*. Path is directly under Android. Don't allow moving
-                    // files and directories to Android/.
+            } else if (FileUtils.contains(directoryAndroid, new File(newPathLowerCase))) {
+                if (newRelativePath.length <= 2) {
+                    // Path is directly under Android, Android/media, Android/data, Android/obb or
+                    // some other directory under Android. Don't allow moving files and directories
+                    // in these paths. Files and directories are only allowed to move to path
+                    // Android/media/<app_specific_directory>/*
                     Log.e(TAG, errorMessage +  newPath + " is in app external directory. "
                             + "Renaming a file/directory to app external directory is not "
                             + "allowed.");
                     return OsConstants.EPERM;
-                } else if(!FileUtils.contains(directoryAndroidMedia, new File(newPath))) {
-                    // New path is  Android/*/*. Don't allow moving of files or directories
-                    // to app external directory other than media directory.
+                } else if (!FileUtils.contains(directoryAndroidMedia, new File(newPathLowerCase))) {
+                    // New path is not in Android/media/*. Don't allow moving of files or
+                    // directories to app external directory other than media directory.
                     Log.e(TAG, errorMessage +  newPath + " is not in external media directory."
                             + "File/directory can only be renamed to a path in external media "
                             + "directory. Renaming file/directory to path in other external "
diff --git a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
index 1a0dda2..0f8953e 100644
--- a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
+++ b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
@@ -37,8 +37,6 @@
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
-import android.os.SystemProperties;
-import android.provider.DeviceConfig;
 import android.provider.CloudMediaProviderContract;
 import android.provider.MediaStore;
 import android.text.TextUtils;
@@ -115,7 +113,6 @@
     public static final String KEY_DURATION_MS = "duration_ms";
     @VisibleForTesting
     public static final String KEY_MIME_TYPE = "mime_type";
-    @VisibleForTesting
     public static final String KEY_STANDARD_MIME_TYPE_EXTENSION = "standard_mime_type_extension";
     @VisibleForTesting
     public static final String KEY_IS_FAVORITE = "is_favorite";
@@ -240,6 +237,10 @@
      * Returns {@link DbWriteOperation} to clear album media for a given albumId from the picker
      * db.
      *
+     * <p>The {@link DbWriteOperation} clears local or cloud album based on {@code authority} and
+     * {@code albumId}. If {@code albumId} is null, it clears all local or cloud albums based on
+     * {@code authority}.
+     *
      * @param authority to determine whether local or cloud media should be cleared
      */
     public DbWriteOperation beginResetAlbumMediaOperation(String authority, String albumId) {
@@ -247,37 +248,38 @@
     }
 
     /**
+     * Returns {@link UpdateMediaOperation} to update media belonging to {@code authority} in the
+     * picker db.
+     *
+     * @param authority to determine whether local or cloud media should be updated
+     */
+    public UpdateMediaOperation beginUpdateMediaOperation(String authority) {
+        return new UpdateMediaOperation(mDatabase, isLocal(authority));
+    }
+
+    /**
      * Represents an atomic write operation to the picker database.
      *
      * <p>This class is not thread-safe and is meant to be used within a single thread only.
      */
-    public static abstract class DbWriteOperation implements AutoCloseable {
+    public abstract static class DbWriteOperation implements AutoCloseable {
 
         private final SQLiteDatabase mDatabase;
         private final boolean mIsLocal;
-        private final String mAlbumId;
 
         private boolean mIsSuccess = false;
 
-        // Needed for Album Media Write operations.
         private DbWriteOperation(SQLiteDatabase database, boolean isLocal) {
-            this(database, isLocal, "");
-        }
-
-        // Needed for Album Media Write operations.
-        private DbWriteOperation(SQLiteDatabase database, boolean isLocal, String albumId) {
             mDatabase = database;
             mIsLocal = isLocal;
-            mAlbumId = albumId;
             mDatabase.beginTransaction();
         }
 
-        /*
-         * Execute the write operation.
+        /**
+         * Execute a write operation.
          *
          * @param cursor containing items to add/remove
-         * @return {@link WriteResult} indicating success/failure and the number of {@code cursor}
-         *          items that were inserted/updated/deleted in the picker db
+         * @return number of {@code cursor} items that were inserted/updated/deleted in the db
          * @throws {@link IllegalStateException} if no DB transaction is active
          */
         public int execute(@Nullable Cursor cursor) {
@@ -315,21 +317,17 @@
             return mIsLocal;
         }
 
-        String albumId() {
-            return mAlbumId;
-        }
-
         int updateMedia(SQLiteQueryBuilder qb, ContentValues values,
                 String[] selectionArgs) {
             try {
                 if (qb.update(mDatabase, values, /* selection */ null, selectionArgs) > 0) {
                     return SUCCESS;
                 } else {
-                    Log.d(TAG, "Failed to update picker db media. ContentValues: " + values);
+                    Log.v(TAG, "Failed to update picker db media. ContentValues: " + values);
                     return FAIL;
                 }
             } catch (SQLiteConstraintException e) {
-                Log.d(TAG, "Failed to update picker db media. ContentValues: " + values, e);
+                Log.v(TAG, "Failed to update picker db media. ContentValues: " + values, e);
                 return RETRY;
             }
         }
@@ -348,6 +346,41 @@
         }
     }
 
+    /**
+     * Represents an atomic media update operation to the picker database.
+     *
+     * <p>This class is not thread-safe and is meant to be used within a single thread only.
+     */
+    public static final class UpdateMediaOperation extends DbWriteOperation {
+
+        private UpdateMediaOperation(SQLiteDatabase database, boolean isLocal) {
+            super(database, isLocal);
+        }
+
+        /**
+         * Execute a media update operation.
+         *
+         * @param id id of the media to be updated
+         * @param contentValues key-value pairs indicating fields to be updated for the media
+         * @return boolean indicating success/failure of the update
+         * @throws {@link IllegalStateException} if no DB transaction is active
+         */
+        public boolean execute(String id, ContentValues contentValues) {
+            final SQLiteDatabase database = getDatabase();
+            if (!database.inTransaction()) {
+                throw new IllegalStateException("No ongoing DB transaction.");
+            }
+
+            final SQLiteQueryBuilder qb = isLocal() ? QB_MATCH_LOCAL_ONLY : QB_MATCH_CLOUD;
+            return qb.update(database, contentValues, /* selection */ null, new String[] {id}) > 0;
+        }
+
+        @Override
+        int executeInternal(@Nullable Cursor cursor) {
+            throw new UnsupportedOperationException("Cursor updates are not supported.");
+        }
+    }
+
     private static final class AddMediaOperation extends DbWriteOperation {
 
         private AddMediaOperation(SQLiteDatabase database, boolean isLocal) {
@@ -394,11 +427,11 @@
                 if (QB_MATCH_ALL.insert(getDatabase(), values) > 0) {
                     return SUCCESS;
                 } else {
-                    Log.d(TAG, "Failed to insert picker db media. ContentValues: " + values);
+                    Log.v(TAG, "Failed to insert picker db media. ContentValues: " + values);
                     return FAIL;
                 }
             } catch (SQLiteConstraintException e) {
-                Log.d(TAG, "Failed to insert picker db media. ContentValues: " + values, e);
+                Log.v(TAG, "Failed to insert picker db media. ContentValues: " + values, e);
                 return RETRY;
             }
         }
@@ -408,7 +441,7 @@
             int res = insertMedia(values);
             if (res == RETRY) {
                 // Attempt equivalent of CONFLICT_REPLACE resolution
-                Log.d(TAG, "Retrying failed insert as update. ContentValues: " + values);
+                Log.v(TAG, "Retrying failed insert as update. ContentValues: " + values);
                 res = updateMedia(qb, values, selectionArgs);
             }
 
@@ -906,7 +939,7 @@
     private static ContentValues cursorToContentValue(Cursor cursor, boolean isLocal,
             String albumId) {
         final ContentValues values = new ContentValues();
-        if(TextUtils.isEmpty(albumId)) {
+        if (TextUtils.isEmpty(albumId)) {
             values.put(KEY_IS_VISIBLE, 1);
         }
         else {
@@ -955,7 +988,7 @@
                     values.put(KEY_DURATION_MS, cursor.getLong(index));
                     break;
                 case CloudMediaProviderContract.MediaColumns.IS_FAVORITE:
-                    if(TextUtils.isEmpty(albumId)) {
+                    if (TextUtils.isEmpty(albumId)) {
                         values.put(KEY_IS_FAVORITE, cursor.getInt(index));
                     }
                     break;
@@ -1104,25 +1137,35 @@
         return qb;
     }
 
-    private static final class ResetAlbumOperation extends DbWriteOperation {
-        /**
-         * Resets the given cloud or local album_media identified by {@code isLocal} and
-         * {@code albumId}. If {@code albumId} is null, resets all the respective cloud or
-         * local albums.
-         */
+    private abstract static class AlbumWriteOperation extends DbWriteOperation {
+
+        private final String mAlbumId;
+
+        private AlbumWriteOperation(SQLiteDatabase database, boolean isLocal, String albumId) {
+            super(database, isLocal);
+            mAlbumId = albumId;
+        }
+
+        String getAlbumId() {
+            return mAlbumId;
+        }
+    }
+
+    private static final class ResetAlbumOperation extends AlbumWriteOperation {
+
         private ResetAlbumOperation(SQLiteDatabase database, boolean isLocal, String albumId) {
             super(database, isLocal, albumId);
         }
 
         @Override
         int executeInternal(@Nullable Cursor unused) {
-            final String albumId = albumId();
+            final String albumId = getAlbumId();
             final boolean isLocal = isLocal();
 
             final SQLiteQueryBuilder qb = createAlbumMediaQueryBuilder(isLocal);
 
             String[] selectionArgs = null;
-            if(!TextUtils.isEmpty(albumId)) {
+            if (!TextUtils.isEmpty(albumId)) {
                 qb.appendWhereStandalone(WHERE_ALBUM_ID);
                 selectionArgs = new String[]{albumId};
             }
@@ -1132,10 +1175,12 @@
         }
     }
 
-    private static final class AddAlbumMediaOperation extends DbWriteOperation {
+    private static final class AddAlbumMediaOperation extends AlbumWriteOperation {
+
         private AddAlbumMediaOperation(SQLiteDatabase database, boolean isLocal, String albumId) {
             super(database, isLocal, albumId);
-            if(TextUtils.isEmpty(albumId)) {
+
+            if (TextUtils.isEmpty(albumId)) {
                 throw new IllegalArgumentException("Missing albumId.");
             }
         }
@@ -1143,7 +1188,7 @@
         @Override
         int executeInternal(@Nullable Cursor cursor) {
             final boolean isLocal = isLocal();
-            final String albumId = albumId();
+            final String albumId = getAlbumId();
             final SQLiteQueryBuilder qb = createAlbumMediaQueryBuilder(isLocal);
             int counter = 0;
 
@@ -1153,10 +1198,10 @@
                     if (qb.insert(getDatabase(), values) > 0) {
                         counter++;
                     } else {
-                        Log.d(TAG, "Failed to insert album_media. ContentValues: " + values);
+                        Log.v(TAG, "Failed to insert album_media. ContentValues: " + values);
                     }
                 } catch (SQLiteConstraintException e) {
-                    Log.d(TAG, "Failed to insert album_media. ContentValues: " + values, e);
+                    Log.v(TAG, "Failed to insert album_media. ContentValues: " + values, e);
                 }
             }
 
diff --git a/src/com/android/providers/media/photopicker/ui/SafetyProtectionSectionView.java b/src/com/android/providers/media/photopicker/ui/SafetyProtectionSectionView.java
new file mode 100644
index 0000000..21dd503
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/ui/SafetyProtectionSectionView.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2022 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.providers.media.photopicker.ui;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.text.Html;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.providers.media.R;
+import com.android.providers.media.photopicker.util.SafetyProtectionUtils;
+
+/**
+ * A custom view class for Safety Protection widget.
+ */
+public class SafetyProtectionSectionView extends LinearLayout {
+    public SafetyProtectionSectionView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public SafetyProtectionSectionView(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public SafetyProtectionSectionView(Context context, @Nullable AttributeSet attrs,
+            int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context);
+    }
+
+    public SafetyProtectionSectionView(Context context, AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init(context);
+    }
+
+    private void init(Context context) {
+        setGravity(Gravity.CENTER);
+        setOrientation(HORIZONTAL);
+        int visibility = SafetyProtectionUtils.shouldShowSafetyProtectionResources(context)
+                ? View.VISIBLE : View.GONE;
+        setVisibility(visibility);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        Context context = getContext();
+        if (SafetyProtectionUtils.shouldShowSafetyProtectionResources(context)) {
+            LayoutInflater.from(context).inflate(R.layout.safety_protection_section, this);
+            TextView safetyProtectionDisplayTextView =
+                    requireViewById(R.id.safety_protection_display_text);
+            safetyProtectionDisplayTextView.setText(Html.fromHtml(
+                    context.getString(android.R.string.safety_protection_display_text), 0));
+        }
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/util/SafetyProtectionUtils.java b/src/com/android/providers/media/photopicker/util/SafetyProtectionUtils.java
new file mode 100644
index 0000000..e3f459a
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/util/SafetyProtectionUtils.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2022 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.providers.media.photopicker.util;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Build;
+import android.provider.DeviceConfig;
+
+import androidx.annotation.ChecksSdkIntAtLeast;
+
+import com.android.modules.utils.build.SdkLevel;
+
+/**
+ * Util class for whether we should show the safety protection resources.
+ */
+public class SafetyProtectionUtils {
+    private static final String SAFETY_PROTECTION_RESOURCES_ENABLED = "safety_protection_enabled";
+
+    /**
+     * Determines whether we should show the safety protection resources.
+     * We show the resources only if
+     * (1) the build version is T or after and
+     * (2) the feature flag safety_protection_enabled is enabled and
+     * (3) the config value config_safetyProtectionEnabled is enabled/true and
+     * (4) the resources exist (currently the resources only exist on GMS devices)
+     */
+    @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
+    public static boolean shouldShowSafetyProtectionResources(Context context) {
+        return SdkLevel.isAtLeastT()
+                && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
+                        SAFETY_PROTECTION_RESOURCES_ENABLED, false)
+                && context.getResources().getBoolean(
+                        Resources.getSystem()
+                                .getIdentifier("config_safetyProtectionEnabled",
+                                        "bool", "android"))
+                && context.getDrawable(android.R.drawable.ic_safety_protection) != null
+                && context.getString(android.R.string.safety_protection_display_text) != null
+                && !context.getString(android.R.string.safety_protection_display_text).isEmpty();
+    }
+}
diff --git a/src/com/android/providers/media/util/IsoInterface.java b/src/com/android/providers/media/util/IsoInterface.java
index 03b46c9..8da64b9 100644
--- a/src/com/android/providers/media/util/IsoInterface.java
+++ b/src/com/android/providers/media/util/IsoInterface.java
@@ -48,6 +48,7 @@
     private static final String TAG = "IsoInterface";
     private static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE);
 
+    public static final int BOX_ILST = 0x696c7374;
     public static final int BOX_FTYP = 0x66747970;
     public static final int BOX_HDLR = 0x68646c72;
     public static final int BOX_UUID = 0x75756964;
@@ -96,7 +97,7 @@
 
     private static class Box {
         public final int type;
-        public final long[] range;
+        public long[] range;
         public UUID uuid;
         public byte[] data;
         public List<Box> children;
@@ -132,7 +133,7 @@
         return new UUID(high, low);
     }
 
-    private static @Nullable Box parseNextBox(@NonNull FileDescriptor fd, long end,
+    private static @Nullable Box parseNextBox(@NonNull FileDescriptor fd, long end, int parentType,
             @NonNull String prefix) throws ErrnoException, IOException {
         final long pos = Os.lseek(fd, 0, OsConstants.SEEK_CUR);
 
@@ -210,6 +211,9 @@
                 box.headerSize += 4;
             }
             Os.lseek(fd, pos + box.headerSize, OsConstants.SEEK_SET);
+        } else if (type == BOX_XYZ && parentType == BOX_ILST) {
+            box.range = new long[] {box.range[0], headerSize,
+                    box.range[0] + headerSize, box.range[1] - headerSize};
         }
 
         if (LOGV) {
@@ -222,7 +226,7 @@
             box.children = new ArrayList<>();
 
             Box child;
-            while ((child = parseNextBox(fd, pos + len, prefix + "  ")) != null) {
+            while ((child = parseNextBox(fd, pos + len, type, prefix + "  ")) != null) {
                 box.children.add(child);
             }
         }
@@ -252,7 +256,7 @@
             final long end = Os.lseek(fd, 0, OsConstants.SEEK_END);
             Os.lseek(fd, 0, OsConstants.SEEK_SET);
             Box box;
-            while ((box = parseNextBox(fd, end, "")) != null) {
+            while ((box = parseNextBox(fd, end, -1, "")) != null) {
                 mRoots.add(box);
             }
         } catch (ErrnoException e) {
@@ -291,9 +295,11 @@
     public @NonNull long[] getBoxRanges(int type) {
         LongArray res = new LongArray();
         for (Box box : mFlattened) {
-            if (box.type == type) {
-                res.add(box.range[0] + box.headerSize);
-                res.add(box.range[0] + box.range[1]);
+            for (int i = 0; i < box.range.length; i += 2) {
+                if (box.type == type) {
+                    res.add(box.range[i] + box.headerSize);
+                    res.add(box.range[i] + box.range[i + 1]);
+                }
             }
         }
         return res.toArray();
@@ -302,9 +308,11 @@
     public @NonNull long[] getBoxRanges(@NonNull UUID uuid) {
         LongArray res = new LongArray();
         for (Box box : mFlattened) {
-            if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) {
-                res.add(box.range[0] + box.headerSize);
-                res.add(box.range[0] + box.range[1]);
+            for (int i = 0; i < box.range.length; i += 2) {
+                if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) {
+                    res.add(box.range[i] + box.headerSize);
+                    res.add(box.range[i] + box.range[i + 1]);
+                }
             }
         }
         return res.toArray();
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 15c2be4..a3c81af 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -9,6 +9,7 @@
     <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
     <uses-permission android:name="android.permission.MANAGE_USERS" />
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+    <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
     <application android:label="MediaProvider Tests">
         <uses-library android:name="android.test.runner" />
 
diff --git a/tests/res/raw/test_video_gps_ilst_tag.mp4 b/tests/res/raw/test_video_gps_ilst_tag.mp4
new file mode 100644
index 0000000..d65ff15
--- /dev/null
+++ b/tests/res/raw/test_video_gps_ilst_tag.mp4
Binary files differ
diff --git a/tests/src/com/android/providers/media/IdleServiceTest.java b/tests/src/com/android/providers/media/IdleServiceTest.java
index 39f7e6e..3d81b25 100644
--- a/tests/src/com/android/providers/media/IdleServiceTest.java
+++ b/tests/src/com/android/providers/media/IdleServiceTest.java
@@ -249,9 +249,8 @@
                 assertThat(cr.getCount()).isEqualTo(1);
                 assertThat(cr.moveToFirst()).isNotNull();
                 assertThat(cr.getInt(0)).isEqualTo(_SPECIAL_FORMAT_NONE);
-                // Make sure updating special format column updates GENERATION_MODIFIED;
-                // This is essential for picker db to know which rows were modified.
-                assertThat(cr.getInt(1)).isGreaterThan(initialGenerationModified);
+                // Make sure that updating special format column doesn't update GENERATION_MODIFIED
+                assertThat(cr.getInt(1)).isEqualTo(initialGenerationModified);
             }
         } finally {
             file.delete();
diff --git a/tests/src/com/android/providers/media/photopicker/SafetyProtectionUtilsTest.java b/tests/src/com/android/providers/media/photopicker/SafetyProtectionUtilsTest.java
new file mode 100644
index 0000000..1c15d97
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/SafetyProtectionUtilsTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2022 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.providers.media.photopicker;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.provider.DeviceConfig;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.providers.media.photopicker.util.SafetyProtectionUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class SafetyProtectionUtilsTest {
+    private final Context mContext = InstrumentationRegistry.getTargetContext();
+    private static final String SAFETY_PROTECTION_RESOURCES_ENABLED = "safety_protection_enabled";
+    private static final String TRUE_STRING = "true";
+    private static final String FALSE_STRING = "false";
+    private String mOriginalSafetyProtectionResourcesFlagStatus = "false";
+
+    @Before
+    public void setUp() {
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            mOriginalSafetyProtectionResourcesFlagStatus = DeviceConfig.getProperty(
+                    DeviceConfig.NAMESPACE_PRIVACY, SAFETY_PROTECTION_RESOURCES_ENABLED);
+        });
+    }
+
+    @After
+    public void tearDown() {
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY,
+                    SAFETY_PROTECTION_RESOURCES_ENABLED,
+                    mOriginalSafetyProtectionResourcesFlagStatus, false);
+        });
+    }
+
+    @Test
+    public void testShouldNotUseSafetyProtectionResourcesWhenSOrBelow() {
+        assumeFalse(SdkLevel.isAtLeastT());
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY,
+                    SAFETY_PROTECTION_RESOURCES_ENABLED, TRUE_STRING, false);
+            assertThat(SafetyProtectionUtils.shouldShowSafetyProtectionResources(mContext))
+                    .isFalse();
+        });
+    }
+
+    @Test
+    public void testWhetherShouldUseSafetyProtectionResourcesWhenTOrAboveAndFeatureFlagOn() {
+        assumeTrue(SdkLevel.isAtLeastT());
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY,
+                    SAFETY_PROTECTION_RESOURCES_ENABLED, TRUE_STRING, false);
+            assertThat(SafetyProtectionUtils.shouldShowSafetyProtectionResources(mContext))
+                    .isEqualTo(isSafetyProtectionConfigEnabled());
+        });
+    }
+
+    @Test
+    public void testWhetherShouldUseSafetyProtectionResourcesWhenTOrAboveAndFeatureFlagOff() {
+        assumeTrue(SdkLevel.isAtLeastT());
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY,
+                    SAFETY_PROTECTION_RESOURCES_ENABLED, FALSE_STRING, false);
+            assertThat(SafetyProtectionUtils.shouldShowSafetyProtectionResources(mContext))
+                    .isFalse();
+        });
+    }
+
+    protected boolean isSafetyProtectionConfigEnabled() {
+        try {
+            return mContext.getResources().getBoolean(
+                    Resources.getSystem()
+                            .getIdentifier("config_safetyProtectionEnabled", "bool",
+                            "android"));
+        } catch (Resources.NotFoundException e) {
+            return false;
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java b/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
index 5e13bec..0305bbb 100644
--- a/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
@@ -23,6 +23,7 @@
 
 import static org.junit.Assert.assertThrows;
 
+import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
 import android.database.MatrixCursor;
@@ -1014,6 +1015,68 @@
         }
     }
 
+    @Test
+    public void testUpdateMediaSuccess() throws Exception {
+        Cursor localCursor = getMediaCursor(LOCAL_ID, DATE_TAKEN_MS, GENERATION_MODIFIED,
+                /* mediaStoreUri */ null, SIZE_BYTES, VIDEO_MIME_TYPE,
+                STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ true);
+        try (PickerDbFacade.DbWriteOperation operation =
+                     mFacade.beginAddMediaOperation(LOCAL_PROVIDER)) {
+            operation.execute(localCursor);
+            operation.setSuccess();
+        }
+
+        try (PickerDbFacade.UpdateMediaOperation operation =
+                     mFacade.beginUpdateMediaOperation(LOCAL_PROVIDER)) {
+            ContentValues values = new ContentValues();
+            values.put(PickerDbFacade.KEY_STANDARD_MIME_TYPE_EXTENSION,
+                    MediaColumns.STANDARD_MIME_TYPE_EXTENSION_ANIMATED_WEBP);
+            assertThat(operation.execute(LOCAL_ID, values)).isTrue();
+            operation.setSuccess();
+        }
+
+        try (Cursor cursor = queryMediaAll()) {
+            assertThat(cursor.getCount()).isEqualTo(1);
+
+            // Assert that STANDARD_MIME_TYPE_EXTENSION has been updated
+            cursor.moveToFirst();
+            assertThat(cursor.getInt(cursor.getColumnIndex(
+                    MediaColumns.STANDARD_MIME_TYPE_EXTENSION)))
+                    .isEqualTo(MediaColumns.STANDARD_MIME_TYPE_EXTENSION_ANIMATED_WEBP);
+        }
+    }
+
+    @Test
+    public void testUpdateMediaFailure() throws Exception {
+        Cursor localCursor = getMediaCursor(LOCAL_ID, DATE_TAKEN_MS, GENERATION_MODIFIED,
+                /* mediaStoreUri */ null, SIZE_BYTES, VIDEO_MIME_TYPE,
+                STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ true);
+        try (PickerDbFacade.DbWriteOperation operation =
+                     mFacade.beginAddMediaOperation(LOCAL_PROVIDER)) {
+            operation.execute(localCursor);
+            operation.setSuccess();
+        }
+
+        try (PickerDbFacade.UpdateMediaOperation operation =
+                     mFacade.beginUpdateMediaOperation(LOCAL_PROVIDER)) {
+            ContentValues values = new ContentValues();
+            values.put(PickerDbFacade.KEY_STANDARD_MIME_TYPE_EXTENSION,
+                    MediaColumns.STANDARD_MIME_TYPE_EXTENSION_ANIMATED_WEBP);
+            assertThat(operation.execute(CLOUD_ID, values)).isFalse();
+            operation.setSuccess();
+        }
+
+        try (Cursor cursor = queryMediaAll()) {
+            assertThat(cursor.getCount()).isEqualTo(1);
+
+            // Assert that STANDARD_MIME_TYPE_EXTENSION is same as before
+            cursor.moveToFirst();
+            assertThat(cursor.getInt(cursor.getColumnIndex(
+                    MediaColumns.STANDARD_MIME_TYPE_EXTENSION)))
+                    .isEqualTo(STANDARD_MIME_TYPE_EXTENSION);
+        }
+    }
+
     private Cursor queryMediaAll() {
         return mFacade.queryMediaForUi(
                 new PickerDbFacade.QueryFilterBuilder(1000).build());
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
index f2c80e7..91c309b 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
@@ -155,7 +155,8 @@
         InstrumentationRegistry.getInstrumentation().getUiAutomation()
                 .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
                         Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
-                        Manifest.permission.INTERACT_ACROSS_USERS);
+                        Manifest.permission.INTERACT_ACROSS_USERS,
+                        Manifest.permission.READ_DEVICE_CONFIG);
 
         sIsolatedContext = new IsolatedContext(getTargetContext(), "modern",
                 /* asFuseThread */ false);
diff --git a/tests/src/com/android/providers/media/util/IsoInterfaceTest.java b/tests/src/com/android/providers/media/util/IsoInterfaceTest.java
index 7783957..37dd48f 100644
--- a/tests/src/com/android/providers/media/util/IsoInterfaceTest.java
+++ b/tests/src/com/android/providers/media/util/IsoInterfaceTest.java
@@ -73,6 +73,19 @@
     }
 
     @Test
+    public void testGpsInIlst() throws Exception {
+        final File file = stageFile(R.raw.test_video_gps_ilst_tag);
+        final IsoInterface mp4 = IsoInterface.fromFile(file);
+
+        final long[] ranges = mp4.getBoxRanges(0xa978797a); // ?xyz
+        assertEquals(4, ranges.length);
+        assertEquals(2267, ranges[0]);
+        assertEquals(2267, ranges[1]);
+        assertEquals(2275, ranges[2]);
+        assertEquals(2309, ranges[3]);
+    }
+
+    @Test
     public void testXmp() throws Exception {
         final File file = stageFile(R.raw.test_video_xmp);
         final IsoInterface mp4 = IsoInterface.fromFile(file);