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);