summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/com/android/providers/media/MediaProvider.java70
-rw-r--r--src/com/android/providers/media/util/MimeTypeFixHandler.java214
-rw-r--r--src/com/android/providers/media/util/MimeUtils.java19
3 files changed, 303 insertions, 0 deletions
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 0beb07b8d..d0d5d345d 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -329,6 +329,7 @@ import com.android.providers.media.util.ForegroundThread;
import com.android.providers.media.util.Logging;
import com.android.providers.media.util.LongArray;
import com.android.providers.media.util.Metrics;
+import com.android.providers.media.util.MimeTypeFixHandler;
import com.android.providers.media.util.MimeUtils;
import com.android.providers.media.util.PermissionUtils;
import com.android.providers.media.util.Preconditions;
@@ -546,6 +547,11 @@ public class MediaProvider extends ContentProvider {
*/
private static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary";
+ private static final String MEDIAPROVIDER_PREFS = "mediaprovider_prefs";
+
+ private static final String IS_MIME_TYPE_FIXED_IN_ANDROID_15 =
+ "is_mime_type_fixed_in_android_15";
+
/**
* Updates the MediaStore versioning schema and format to reduce identifying properties.
*/
@@ -1621,6 +1627,9 @@ public class MediaProvider extends ContentProvider {
PulledMetrics.initialize(context);
mMaliciousAppDetector = createMaliciousAppDetector();
+
+ initializeMimeTypeFixHandlerForAndroid15(getContext());
+
return true;
}
@@ -1829,6 +1838,10 @@ public class MediaProvider extends ContentProvider {
mExternalPrimaryBackupExecutor.doBackup(signal);
+ // In Android 15, certain MIME types were introduced that are not supported, this fixes
+ // existing data with these unsupported MIME types
+ fixUnsupportedMimeTypesForAndroid15(getContext());
+
final long durationMillis = (SystemClock.elapsedRealtime() - startTime);
Metrics.logIdleMaintenance(MediaStore.VOLUME_EXTERNAL, itemCount,
durationMillis, staleThumbnails, deletedExpiredMedia);
@@ -1966,6 +1979,38 @@ public class MediaProvider extends ContentProvider {
}
}
+ private void fixUnsupportedMimeTypesForAndroid15(Context context) {
+ if (!Flags.enableMimeTypeFixForAndroid15()) {
+ return;
+ }
+
+ if (context == null) {
+ return;
+ }
+
+ if (Build.VERSION.SDK_INT != Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ return;
+ }
+
+ SharedPreferences prefs = context.getSharedPreferences(MEDIAPROVIDER_PREFS,
+ Context.MODE_PRIVATE);
+ if (prefs.getBoolean(IS_MIME_TYPE_FIXED_IN_ANDROID_15, false)) {
+ Log.v(TAG, "Mime type already corrected");
+ return;
+ }
+
+ mExternalDatabase.runWithTransaction(db -> {
+ boolean isSuccess = MimeTypeFixHandler.updateUnsupportedMimeTypes(db);
+ // if success then update the shared pref value
+ if (isSuccess) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(IS_MIME_TYPE_FIXED_IN_ANDROID_15, true);
+ editor.apply();
+ }
+ return null;
+ });
+ }
+
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.
@@ -12286,6 +12331,31 @@ public class MediaProvider extends ContentProvider {
}
/**
+ * Initializes the MimeTypeFixHandler for Android 15, running only for Android 15
+ * This method loads the mime types from the res/raw directory
+ * This is necessary to ensure that MediaProvider can handle mime types correctly on Android 15
+ */
+ private void initializeMimeTypeFixHandlerForAndroid15(Context context) {
+ if (Build.VERSION.SDK_INT != Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ return;
+ }
+
+ if (!Flags.enableMimeTypeFixForAndroid15()) {
+ return;
+ }
+
+ // Load all the MIME types from various files in the background to reduce the latency
+ // caused when this method is called from onCreate
+ BackgroundThread.getExecutor().execute(() -> {
+ try {
+ MimeTypeFixHandler.loadMimeTypes(context);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to initialize MimeTypeFixHandler: ", e);
+ }
+ });
+ }
+
+ /**
* <b>FOT TESTING PURPOSES ONLY</b>
* <p>
* Allows injecting alternative {@link ConfigStore} implementation.
diff --git a/src/com/android/providers/media/util/MimeTypeFixHandler.java b/src/com/android/providers/media/util/MimeTypeFixHandler.java
new file mode 100644
index 000000000..bf0aa535e
--- /dev/null
+++ b/src/com/android/providers/media/util/MimeTypeFixHandler.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2025 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.util;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.provider.MediaStore;
+import android.util.Log;
+
+import com.android.providers.media.R;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Utility class for handling MIME type mappings.
+ */
+public final class MimeTypeFixHandler {
+
+ private static final String TAG = "MimeTypeFixHandler";
+ private static final Map<String, String> sExtToMimeType = new HashMap<>();
+ private static final Map<String, String> sCorruptedExtToMimeType = new HashMap<>();
+
+ /**
+ * Loads MIME type mappings from the classpath resource if not already loaded.
+ * <p>
+ * This method initializes both the standard and corrupted MIME type maps.
+ * </p>
+ */
+ public static void loadMimeTypes(Context context) {
+ if (context == null) {
+ return;
+ }
+
+ if (sExtToMimeType.isEmpty()) {
+ parseTypes(context, R.raw.mime_types, sExtToMimeType);
+ // this will add or override the extension to mime type mapping
+ parseTypes(context, R.raw.android_mime_types, sExtToMimeType);
+ Log.v(TAG, "MIME types loaded");
+ }
+ if (sCorruptedExtToMimeType.isEmpty()) {
+ parseTypes(context, R.raw.corrupted_mime_types, sCorruptedExtToMimeType);
+ Log.v(TAG, "Corrupted MIME types loaded");
+ }
+
+ }
+
+ /**
+ * Parses the specified mime types file and populates the provided mapping with file extension
+ * to MIME type entries.
+ *
+ * @param resource the mime.type resource
+ * @param mapping the map to populate with file extension (key) to MIME type (value) mappings
+ */
+ private static void parseTypes(Context context, int resource, Map<String, String> mapping) {
+ try (InputStream inputStream = context.getResources().openRawResource(resource)) {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ // Strip comments and normalize whitespace
+ line = line.replaceAll("#.*$", "").trim().replaceAll("\\s+", " ");
+ // Skip empty lines or lines without a space (i.e., no extension mapping)
+ if (line.isEmpty() || !line.contains(" ")) {
+ continue;
+ }
+ String[] tokens = line.split(" ");
+ if (tokens.length < 2) {
+ continue;
+ }
+ String mimeType = tokens[0];
+ // ?mime ext1 ?ext2 ext3
+ if (mimeType.toLowerCase(Locale.ROOT).startsWith("?")) {
+ mimeType = mimeType.substring(1); // Remove the "?"
+ }
+
+ for (int i = 1; i < tokens.length; i++) {
+ String extension = tokens[i].toLowerCase(Locale.ROOT);
+ boolean putIfAbsent = extension.startsWith("?");
+ if (putIfAbsent) {
+ extension = extension.substring(1); // Remove the "?"
+ if (!mapping.containsKey(extension)) {
+ mapping.put(extension, mimeType);
+ }
+ } else {
+ mapping.put(extension, mimeType);
+ }
+ }
+ }
+ }
+ } catch (IOException | RuntimeException e) {
+ Log.e(TAG, "Exception raised while parsing mime.types", e);
+ }
+ }
+
+ /**
+ * Returns the MIME type for the given file extension from our internal mappings.
+ *
+ * @param extension The file extension to look up.
+ * @return The associated MIME type from the primary mapping if available, or
+ * {@link android.content.ClipDescription#MIMETYPE_UNKNOWN} if the extension is marked
+ * as corrupted
+ * Returns {@link Optional#empty()} if not found in either mapping.
+ */
+ static Optional<String> getMimeType(String extension) {
+ String lowerExt = extension.toLowerCase(Locale.ROOT);
+ if (sExtToMimeType.containsKey(lowerExt)) {
+ return Optional.of(sExtToMimeType.get(lowerExt));
+ }
+
+ if (sCorruptedExtToMimeType.containsKey(lowerExt)) {
+ return Optional.of(android.content.ClipDescription.MIMETYPE_UNKNOWN);
+ }
+
+ return Optional.empty();
+ }
+
+
+ /**
+ * Scans the database for files with unsupported or mismatched MIME types and updates them.
+ *
+ * @param db The SQLiteDatabase to update.
+ * @return true if all intended updates were successfully applied (or if there were no files),
+ * false otherwise.
+ */
+ public static boolean updateUnsupportedMimeTypes(SQLiteDatabase db) {
+ class FileMimeTypeUpdate {
+ final long mFileId;
+ final String mNewMimeType;
+
+ FileMimeTypeUpdate(long fileId, String newMimeType) {
+ this.mFileId = fileId;
+ this.mNewMimeType = newMimeType;
+ }
+ }
+
+ List<FileMimeTypeUpdate> filesToUpdate = new ArrayList<>();
+ String[] projections = new String[]{MediaStore.Files.FileColumns._ID,
+ MediaStore.Files.FileColumns.DATA,
+ MediaStore.Files.FileColumns.MIME_TYPE,
+ MediaStore.Files.FileColumns.DISPLAY_NAME
+ };
+ try (Cursor cursor = db.query(MediaStore.Files.TABLE, projections,
+ null, null, null, null, null)) {
+
+ while (cursor != null && cursor.moveToNext()) {
+ long fileId = cursor.getLong(cursor.getColumnIndexOrThrow(
+ MediaStore.Files.FileColumns._ID));
+ String data = cursor.getString(cursor.getColumnIndexOrThrow(
+ MediaStore.Files.FileColumns.DATA));
+ String currentMimeType = cursor.getString(
+ cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MIME_TYPE));
+ String displayName = cursor.getString(cursor.getColumnIndexOrThrow(
+ MediaStore.Files.FileColumns.DISPLAY_NAME));
+
+ String extension = FileUtils.extractFileExtension(data);
+ if (extension == null) {
+ continue;
+ }
+ String newMimeType = MimeUtils.resolveMimeType(new File(displayName));
+ if (!newMimeType.equalsIgnoreCase(currentMimeType)) {
+ filesToUpdate.add(new FileMimeTypeUpdate(fileId, newMimeType));
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to fetch files for MIME type check", e);
+ return false;
+ }
+
+ Log.v(TAG, "Identified " + filesToUpdate.size() + " files with incorrect MIME types.");
+ int updatedRows = 0;
+ for (FileMimeTypeUpdate fileUpdate : filesToUpdate) {
+ try {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(MediaStore.Files.FileColumns.MIME_TYPE, fileUpdate.mNewMimeType);
+ contentValues.put(MediaStore.Files.FileColumns.MEDIA_TYPE,
+ MimeUtils.resolveMediaType(fileUpdate.mNewMimeType));
+
+ String whereClause = MediaStore.Files.FileColumns._ID + " = ?";
+ String[] whereArgs = new String[]{String.valueOf(fileUpdate.mFileId)};
+ updatedRows += db.update(MediaStore.Files.TABLE, contentValues, whereClause,
+ whereArgs);
+ } catch (Exception e) {
+ Log.e(TAG, "Error updating file with id: " + fileUpdate.mFileId, e);
+ }
+ }
+ Log.v(TAG, "Updated MIME type and Media type for " + updatedRows + " rows");
+ return updatedRows == filesToUpdate.size();
+ }
+}
diff --git a/src/com/android/providers/media/util/MimeUtils.java b/src/com/android/providers/media/util/MimeUtils.java
index cdd2c8287..354ceb193 100644
--- a/src/com/android/providers/media/util/MimeUtils.java
+++ b/src/com/android/providers/media/util/MimeUtils.java
@@ -18,6 +18,7 @@ package com.android.providers.media.util;
import android.content.ClipDescription;
import android.mtp.MtpConstants;
+import android.os.Build;
import android.provider.MediaStore.Files.FileColumns;
import android.util.Log;
import android.webkit.MimeTypeMap;
@@ -26,8 +27,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.providers.media.flags.Flags;
+
import java.io.File;
import java.util.Locale;
+import java.util.Optional;
public class MimeUtils {
private static final String TAG = "MimeUtils";
@@ -46,6 +50,13 @@ public class MimeUtils {
final String extension = FileUtils.extractFileExtension(file.getPath());
if (extension == null) return ClipDescription.MIMETYPE_UNKNOWN;
+ // In Android 15, certain unsupported MIME types were introduced
+ // This ensures new files with these MIME types are handled with the correct MIME type
+ Optional<String> android15MimeType = getMimeTypeForAndroid15(extension);
+ if (android15MimeType.isPresent()) {
+ return android15MimeType.get();
+ }
+
final String mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(extension.toLowerCase(Locale.ROOT));
if (mimeType == null) return ClipDescription.MIMETYPE_UNKNOWN;
@@ -283,4 +294,12 @@ public class MimeUtils {
return "";
}
+
+ private static Optional<String> getMimeTypeForAndroid15(String extension) {
+ if (Flags.enableMimeTypeFixForAndroid15()
+ && Build.VERSION.SDK_INT == Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ return MimeTypeFixHandler.getMimeType(extension);
+ }
+ return Optional.empty();
+ }
}