diff options
Diffstat (limited to 'src')
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(); + } } |