• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2025 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.providers.media.util;
18 
19 import android.content.ContentValues;
20 import android.content.Context;
21 import android.database.Cursor;
22 import android.database.sqlite.SQLiteDatabase;
23 import android.provider.MediaStore;
24 import android.util.Log;
25 
26 import com.android.providers.media.R;
27 
28 import java.io.BufferedReader;
29 import java.io.File;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.io.InputStreamReader;
33 import java.util.ArrayList;
34 import java.util.HashMap;
35 import java.util.List;
36 import java.util.Locale;
37 import java.util.Map;
38 import java.util.Optional;
39 
40 /**
41  * Utility class for handling MIME type mappings.
42  */
43 public final class MimeTypeFixHandler {
44 
45     private static final String TAG = "MimeTypeFixHandler";
46     private static final Map<String, String> sExtToMimeType = new HashMap<>();
47     private static final Map<String, String> sMimeTypeToExt = new HashMap<>();
48 
49     private static final Map<String, String> sCorruptedExtToMimeType = new HashMap<>();
50     private static final Map<String, String> sCorruptedMimeTypeToExt = new HashMap<>();
51 
52     /**
53      * Loads MIME type mappings from the classpath resource if not already loaded.
54      * <p>
55      * This method initializes both the standard and corrupted MIME type maps.
56      * </p>
57      */
loadMimeTypes(Context context)58     public static void loadMimeTypes(Context context) {
59         if (context == null) {
60             return;
61         }
62 
63         if (sExtToMimeType.isEmpty()) {
64             parseTypes(context, R.raw.mime_types, sExtToMimeType, sMimeTypeToExt);
65             // this will add or override the extension to mime type mapping
66             parseTypes(context, R.raw.android_mime_types, sExtToMimeType, sMimeTypeToExt);
67             Log.v(TAG, "MIME types loaded");
68         }
69         if (sCorruptedExtToMimeType.isEmpty()) {
70             parseTypes(context, R.raw.corrupted_mime_types, sCorruptedExtToMimeType,
71                     sCorruptedMimeTypeToExt);
72             Log.v(TAG, "Corrupted MIME types loaded");
73         }
74 
75     }
76 
77     /**
78      * Parses the specified mime types file and populates the provided mapping with file extension
79      * to MIME type entries.
80      *
81      * @param resource the mime.type resource
82      * @param mapping  the map to populate with file extension (key) to MIME type (value) mappings
83      */
parseTypes(Context context, int resource, Map<String, String> extToMimeType, Map<String, String> mimeTypeToExt)84     private static void parseTypes(Context context, int resource, Map<String, String> extToMimeType,
85             Map<String, String> mimeTypeToExt) {
86         try (InputStream inputStream = context.getResources().openRawResource(resource)) {
87             try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
88                 String line;
89                 while ((line = reader.readLine()) != null) {
90                     // Strip comments and normalize whitespace
91                     line = line.replaceAll("#.*$", "").trim().replaceAll("\\s+", " ");
92                     // Skip empty lines or lines without a space (i.e., no extension mapping)
93                     if (line.isEmpty() || !line.contains(" ")) {
94                         continue;
95                     }
96                     String[] tokens = line.split(" ");
97                     if (tokens.length < 2) {
98                         continue;
99                     }
100                     String mimeType = tokens[0].toLowerCase(Locale.ROOT);
101                     String firstExt = tokens[1].toLowerCase(Locale.ROOT);
102                     if (firstExt.startsWith("?")) {
103                         firstExt = firstExt.substring(1);
104                         if (firstExt.isEmpty()) {
105                             continue;
106                         }
107                     }
108 
109                     // ?mime ext1 ?ext2 ext3
110                     if (mimeType.toLowerCase(Locale.ROOT).startsWith("?")) {
111                         mimeType = mimeType.substring(1); // Remove the "?"
112                         if (mimeType.isEmpty()) {
113                             continue;
114                         }
115                         mimeTypeToExt.putIfAbsent(mimeType, firstExt);
116                     } else {
117                         mimeTypeToExt.put(mimeType, firstExt);
118                     }
119 
120                     for (int i = 1; i < tokens.length; i++) {
121                         String extension = tokens[i].toLowerCase(Locale.ROOT);
122                         boolean putIfAbsent = extension.startsWith("?");
123                         if (putIfAbsent) {
124                             extension = extension.substring(1); // Remove the "?"
125                             extToMimeType.putIfAbsent(extension, mimeType);
126                         } else {
127                             extToMimeType.put(extension, mimeType);
128                         }
129                     }
130                 }
131             }
132         } catch (IOException | RuntimeException e) {
133             Log.e(TAG, "Exception raised while parsing mime.types", e);
134         }
135     }
136 
137     /**
138      * Returns the MIME type for the given file extension from our internal mappings.
139      *
140      * @param extension The file extension to look up.
141      * @return The associated MIME type from the primary mapping if available, or
142      * {@link android.content.ClipDescription#MIMETYPE_UNKNOWN} if the extension is marked
143      * as corrupted
144      * Returns {@link Optional#empty()}  if not found in either mapping.
145      */
getMimeType(String extension)146     static Optional<String> getMimeType(String extension) {
147         String lowerExt = extension.toLowerCase(Locale.ROOT);
148         if (sExtToMimeType.containsKey(lowerExt)) {
149             return Optional.of(sExtToMimeType.get(lowerExt));
150         }
151 
152         if (sCorruptedExtToMimeType.containsKey(lowerExt)) {
153             return Optional.of(android.content.ClipDescription.MIMETYPE_UNKNOWN);
154         }
155 
156         return Optional.empty();
157     }
158 
159     /**
160      * Gets file extension from MIME type.
161      *
162      * @param mimeType The MIME type.
163      * @return Optional file extension, or empty.
164      */
getExtFromMimeType(String mimeType)165     static Optional<String> getExtFromMimeType(String mimeType) {
166         if (mimeType == null) {
167             return Optional.empty();
168         }
169 
170         mimeType = mimeType.toLowerCase(Locale.ROOT);
171         return Optional.ofNullable(sMimeTypeToExt.get(mimeType));
172     }
173 
174     /**
175      * Checks if a MIME type is corrupted.
176      *
177      * @param mimeType The MIME type.
178      * @return {@code true} if corrupted, {@code false} otherwise.
179      */
isCorruptedMimeType(String mimeType)180     static boolean isCorruptedMimeType(String mimeType) {
181         if (sMimeTypeToExt.containsKey(mimeType)) {
182             return false;
183         }
184 
185         return sCorruptedMimeTypeToExt.containsKey(mimeType);
186     }
187 
188 
189     /**
190      * Scans the database for files with unsupported or mismatched MIME types and updates them.
191      *
192      * @param db The SQLiteDatabase to update.
193      * @return true if all intended updates were successfully applied (or if there were no files),
194      * false otherwise.
195      */
updateUnsupportedMimeTypes(SQLiteDatabase db)196     public static boolean updateUnsupportedMimeTypes(SQLiteDatabase db) {
197         class FileMimeTypeUpdate {
198             final long mFileId;
199             final String mNewMimeType;
200 
201             FileMimeTypeUpdate(long fileId, String newMimeType) {
202                 this.mFileId = fileId;
203                 this.mNewMimeType = newMimeType;
204             }
205         }
206 
207         List<FileMimeTypeUpdate> filesToUpdate = new ArrayList<>();
208         String[] projections = new String[]{MediaStore.Files.FileColumns._ID,
209                 MediaStore.Files.FileColumns.DATA,
210                 MediaStore.Files.FileColumns.MIME_TYPE,
211                 MediaStore.Files.FileColumns.DISPLAY_NAME
212         };
213         try (Cursor cursor = db.query(MediaStore.Files.TABLE, projections,
214                 null, null, null, null, null)) {
215 
216             while (cursor != null && cursor.moveToNext()) {
217                 long fileId = cursor.getLong(cursor.getColumnIndexOrThrow(
218                         MediaStore.Files.FileColumns._ID));
219                 String data = cursor.getString(cursor.getColumnIndexOrThrow(
220                         MediaStore.Files.FileColumns.DATA));
221                 String currentMimeType = cursor.getString(
222                         cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MIME_TYPE));
223                 String displayName = cursor.getString(cursor.getColumnIndexOrThrow(
224                         MediaStore.Files.FileColumns.DISPLAY_NAME));
225 
226                 String extension = FileUtils.extractFileExtension(data);
227                 if (extension == null) {
228                     continue;
229                 }
230                 String newMimeType = MimeUtils.resolveMimeType(new File(displayName));
231                 if (!newMimeType.equalsIgnoreCase(currentMimeType)) {
232                     filesToUpdate.add(new FileMimeTypeUpdate(fileId, newMimeType));
233                 }
234             }
235         } catch (Exception e) {
236             Log.e(TAG, "Failed to fetch files for MIME type check", e);
237             return false;
238         }
239 
240         Log.v(TAG, "Identified " + filesToUpdate.size() + " files with incorrect MIME types.");
241         int updatedRows = 0;
242         for (FileMimeTypeUpdate fileUpdate : filesToUpdate) {
243             try {
244                 ContentValues contentValues = new ContentValues();
245                 contentValues.put(MediaStore.Files.FileColumns.MIME_TYPE, fileUpdate.mNewMimeType);
246                 contentValues.put(MediaStore.Files.FileColumns.MEDIA_TYPE,
247                         MimeUtils.resolveMediaType(fileUpdate.mNewMimeType));
248 
249                 String whereClause = MediaStore.Files.FileColumns._ID + " = ?";
250                 String[] whereArgs = new String[]{String.valueOf(fileUpdate.mFileId)};
251                 updatedRows += db.update(MediaStore.Files.TABLE, contentValues, whereClause,
252                         whereArgs);
253             } catch (Exception e) {
254                 Log.e(TAG, "Error updating file with id: " + fileUpdate.mFileId, e);
255             }
256         }
257         Log.v(TAG, "Updated MIME type and Media type for " + updatedRows + " rows");
258         return updatedRows == filesToUpdate.size();
259     }
260 }
261