From ca32dd98de9c7767f11cd44f4e91225a5bfb3dbc Mon Sep 17 00:00:00 2001 From: Himanshu Arora Date: Sun, 22 Dec 2024 18:15:50 +0000 Subject: Fix handling of unsupported MIME types in Android 15 In Android 15, certain unsupported MIME types were introduced. This ensures new files with these MIME types are handled with the correct MIME type also fixed the exsiting incorrect mime type and media type Test: atest MimeTypeFixHandlerTest Flag: com.android.providers.media.flags.enable_mime_type_fix_for_android_15 Bug: 376910932 Change-Id: I642ab9a03cf8cd059d28fe258b80fe05ab31c8ce --- src/com/android/providers/media/MediaProvider.java | 70 +++++++ .../providers/media/util/MimeTypeFixHandler.java | 214 +++++++++++++++++++++ .../android/providers/media/util/MimeUtils.java | 19 ++ 3 files changed, 303 insertions(+) create mode 100644 src/com/android/providers/media/util/MimeTypeFixHandler.java (limited to 'src') 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. @@ -12285,6 +12330,31 @@ public class MediaProvider extends ContentProvider { return configStore; } + /** + * 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); + } + }); + } + /** * FOT TESTING PURPOSES ONLY *

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 sExtToMimeType = new HashMap<>(); + private static final Map sCorruptedExtToMimeType = new HashMap<>(); + + /** + * Loads MIME type mappings from the classpath resource if not already loaded. + *

+ * This method initializes both the standard and corrupted MIME type maps. + *

+ */ + 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 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 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 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 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 getMimeTypeForAndroid15(String extension) { + if (Flags.enableMimeTypeFixForAndroid15() + && Build.VERSION.SDK_INT == Build.VERSION_CODES.VANILLA_ICE_CREAM) { + return MimeTypeFixHandler.getMimeType(extension); + } + return Optional.empty(); + } } -- cgit v1.2.3-59-g8ed1b