• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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.ClipDescription;
20 import android.mtp.MtpConstants;
21 import android.os.Build;
22 import android.provider.MediaStore.Files.FileColumns;
23 import android.util.Log;
24 import android.webkit.MimeTypeMap;
25 
26 import androidx.annotation.NonNull;
27 import androidx.annotation.Nullable;
28 import androidx.annotation.VisibleForTesting;
29 
30 import com.android.providers.media.flags.Flags;
31 
32 import java.io.File;
33 import java.util.Locale;
34 import java.util.Optional;
35 
36 public class MimeUtils {
37     private static final String TAG = "MimeUtils";
38     private static final String ALL_IMAGES_MIME_TYPE = "image/*";
39     private static final String ALL_VIDEOS_MIME_TYPE = "video/*";
40     @VisibleForTesting
41     static final String DEFAULT_IMAGE_FILE_EXTENSION = ".jpg";
42     @VisibleForTesting
43     static final String DEFAULT_VIDEO_FILE_EXTENSION = ".mp4";
44 
45     /**
46      * Resolve the MIME type of the given file, returning
47      * {@code application/octet-stream} if the type cannot be determined.
48      */
resolveMimeType(@onNull File file)49     public static @NonNull String resolveMimeType(@NonNull File file) {
50         final String extension = FileUtils.extractFileExtension(file.getPath());
51         if (extension == null) return ClipDescription.MIMETYPE_UNKNOWN;
52 
53         // In Android 15, certain unsupported MIME types were introduced
54         // This ensures new files with these MIME types are handled with the correct MIME type
55         Optional<String> android15MimeType = getMimeTypeForAndroid15(extension);
56         if (android15MimeType.isPresent()) {
57             return android15MimeType.get();
58         }
59 
60         final String mimeType = MimeTypeMap.getSingleton()
61                 .getMimeTypeFromExtension(extension.toLowerCase(Locale.ROOT));
62         if (mimeType == null) return ClipDescription.MIMETYPE_UNKNOWN;
63 
64         return mimeType;
65     }
66 
67     /**
68      * Resolve the {@link FileColumns#MEDIA_TYPE} of the given MIME type. This
69      * carefully checks for more specific types before generic ones, such as
70      * treating {@code audio/mpegurl} as a playlist instead of an audio file.
71      */
resolveMediaType(@onNull String mimeType)72     public static int resolveMediaType(@NonNull String mimeType) {
73         if (isPlaylistMimeType(mimeType)) {
74             return FileColumns.MEDIA_TYPE_PLAYLIST;
75         } else if (isSubtitleMimeType(mimeType)) {
76             return FileColumns.MEDIA_TYPE_SUBTITLE;
77         } else if (isAudioMimeType(mimeType)) {
78             return FileColumns.MEDIA_TYPE_AUDIO;
79         } else if (isVideoMimeType(mimeType)) {
80             return FileColumns.MEDIA_TYPE_VIDEO;
81         } else if (isImageMimeType(mimeType)) {
82             return FileColumns.MEDIA_TYPE_IMAGE;
83         } else if (isDocumentMimeType(mimeType)) {
84             return FileColumns.MEDIA_TYPE_DOCUMENT;
85         } else {
86             return FileColumns.MEDIA_TYPE_NONE;
87         }
88     }
89 
90     /**
91      * Resolve the {@link FileColumns#FORMAT} of the given MIME type. Note that
92      * since this column isn't public API, we're okay only getting very rough
93      * values in place, and it's not worthwhile to build out complex matching.
94      */
resolveFormatCode(@ullable String mimeType)95     public static int resolveFormatCode(@Nullable String mimeType) {
96         final int mediaType = resolveMediaType(mimeType);
97         switch (mediaType) {
98             case FileColumns.MEDIA_TYPE_AUDIO:
99                 return MtpConstants.FORMAT_UNDEFINED_AUDIO;
100             case FileColumns.MEDIA_TYPE_VIDEO:
101                 return MtpConstants.FORMAT_UNDEFINED_VIDEO;
102             case FileColumns.MEDIA_TYPE_IMAGE:
103                 return MtpConstants.FORMAT_DEFINED;
104             default:
105                 return MtpConstants.FORMAT_UNDEFINED;
106         }
107     }
108 
extractPrimaryType(@onNull String mimeType)109     public static @NonNull String extractPrimaryType(@NonNull String mimeType) {
110         final int slash = mimeType.indexOf('/');
111         if (slash == -1) {
112             throw new IllegalArgumentException();
113         }
114         return mimeType.substring(0, slash);
115     }
116 
isAudioMimeType(@ullable String mimeType)117     public static boolean isAudioMimeType(@Nullable String mimeType) {
118         if (mimeType == null) return false;
119         return StringUtils.startsWithIgnoreCase(mimeType, "audio/");
120     }
121 
isVideoMimeType(@ullable String mimeType)122     public static boolean isVideoMimeType(@Nullable String mimeType) {
123         if (mimeType == null) return false;
124 
125         // Handle ASF files as videos
126         if (mimeType.equalsIgnoreCase("application/vnd.ms-asf")) {
127             return true;
128         } else {
129             return StringUtils.startsWithIgnoreCase(mimeType, "video/");
130         }
131     }
132 
133     /**
134      * Check whether a mime type is all videos
135      * @param mimeType the mime type {@link String} to be checked
136      * @return {@code true} if the given mime type is {@link ALL_VIDEOS_MIME_TYPE},
137      * {@code false} otherwise
138      */
isAllVideosMimeType(@ullable String mimeType)139     public static boolean isAllVideosMimeType(@Nullable String mimeType) {
140         return ALL_VIDEOS_MIME_TYPE.equalsIgnoreCase(mimeType);
141     }
142 
isImageMimeType(@ullable String mimeType)143     public static boolean isImageMimeType(@Nullable String mimeType) {
144         if (mimeType == null) return false;
145         return StringUtils.startsWithIgnoreCase(mimeType, "image/");
146     }
147 
148     /**
149      * Check whether a mime type is all images
150      * @param mimeType the mime type {@link String} to be checked
151      * @return {@code true} if the given mime type is {@link ALL_IMAGES_MIME_TYPE},
152      * {@code false} otherwise
153      */
isAllImagesMimeType(@ullable String mimeType)154     public static boolean isAllImagesMimeType(@Nullable String mimeType) {
155         return ALL_IMAGES_MIME_TYPE.equalsIgnoreCase(mimeType);
156     }
157 
isImageOrVideoMediaType(int mediaType)158     public static boolean isImageOrVideoMediaType(int mediaType) {
159         return FileColumns.MEDIA_TYPE_IMAGE == mediaType
160                 || FileColumns.MEDIA_TYPE_VIDEO == mediaType;
161     }
162 
isPlaylistMimeType(@ullable String mimeType)163     public static boolean isPlaylistMimeType(@Nullable String mimeType) {
164         if (mimeType == null) return false;
165         switch (mimeType.toLowerCase(Locale.ROOT)) {
166             case "application/vnd.apple.mpegurl":
167             case "application/vnd.ms-wpl":
168             case "application/x-extension-smpl":
169             case "application/x-mpegurl":
170             case "application/xspf+xml":
171             case "audio/mpegurl":
172             case "audio/x-mpegurl":
173             case "audio/x-scpls":
174                 return true;
175             default:
176                 return false;
177         }
178     }
179 
isSubtitleMimeType(@ullable String mimeType)180     public static boolean isSubtitleMimeType(@Nullable String mimeType) {
181         if (mimeType == null) return false;
182         switch (mimeType.toLowerCase(Locale.ROOT)) {
183             case "application/lrc":
184             case "application/smil+xml":
185             case "application/ttml+xml":
186             case "application/x-extension-cap":
187             case "application/x-extension-srt":
188             case "application/x-extension-sub":
189             case "application/x-extension-vtt":
190             case "application/x-subrip":
191             case "text/vtt":
192                 return true;
193             default:
194                 return false;
195         }
196     }
197 
isDocumentMimeType(@ullable String mimeType)198     public static boolean isDocumentMimeType(@Nullable String mimeType) {
199         if (mimeType == null) return false;
200 
201         if (StringUtils.startsWithIgnoreCase(mimeType, "text/")) return true;
202 
203         switch (mimeType.toLowerCase(Locale.ROOT)) {
204             case "application/epub+zip":
205             case "application/msword":
206             case "application/pdf":
207             case "application/rtf":
208             case "application/vnd.ms-excel":
209             case "application/vnd.ms-excel.addin.macroenabled.12":
210             case "application/vnd.ms-excel.sheet.binary.macroenabled.12":
211             case "application/vnd.ms-excel.sheet.macroenabled.12":
212             case "application/vnd.ms-excel.template.macroenabled.12":
213             case "application/vnd.ms-powerpoint":
214             case "application/vnd.ms-powerpoint.addin.macroenabled.12":
215             case "application/vnd.ms-powerpoint.presentation.macroenabled.12":
216             case "application/vnd.ms-powerpoint.slideshow.macroenabled.12":
217             case "application/vnd.ms-powerpoint.template.macroenabled.12":
218             case "application/vnd.ms-word.document.macroenabled.12":
219             case "application/vnd.ms-word.template.macroenabled.12":
220             case "application/vnd.oasis.opendocument.chart":
221             case "application/vnd.oasis.opendocument.database":
222             case "application/vnd.oasis.opendocument.formula":
223             case "application/vnd.oasis.opendocument.graphics":
224             case "application/vnd.oasis.opendocument.graphics-template":
225             case "application/vnd.oasis.opendocument.presentation":
226             case "application/vnd.oasis.opendocument.presentation-template":
227             case "application/vnd.oasis.opendocument.spreadsheet":
228             case "application/vnd.oasis.opendocument.spreadsheet-template":
229             case "application/vnd.oasis.opendocument.text":
230             case "application/vnd.oasis.opendocument.text-master":
231             case "application/vnd.oasis.opendocument.text-template":
232             case "application/vnd.oasis.opendocument.text-web":
233             case "application/vnd.openxmlformats-officedocument.presentationml.presentation":
234             case "application/vnd.openxmlformats-officedocument.presentationml.slideshow":
235             case "application/vnd.openxmlformats-officedocument.presentationml.template":
236             case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
237             case "application/vnd.openxmlformats-officedocument.spreadsheetml.template":
238             case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
239             case "application/vnd.openxmlformats-officedocument.wordprocessingml.template":
240             case "application/vnd.stardivision.calc":
241             case "application/vnd.stardivision.chart":
242             case "application/vnd.stardivision.draw":
243             case "application/vnd.stardivision.impress":
244             case "application/vnd.stardivision.impress-packed":
245             case "application/vnd.stardivision.mail":
246             case "application/vnd.stardivision.math":
247             case "application/vnd.stardivision.writer":
248             case "application/vnd.stardivision.writer-global":
249             case "application/vnd.sun.xml.calc":
250             case "application/vnd.sun.xml.calc.template":
251             case "application/vnd.sun.xml.draw":
252             case "application/vnd.sun.xml.draw.template":
253             case "application/vnd.sun.xml.impress":
254             case "application/vnd.sun.xml.impress.template":
255             case "application/vnd.sun.xml.math":
256             case "application/vnd.sun.xml.writer":
257             case "application/vnd.sun.xml.writer.global":
258             case "application/vnd.sun.xml.writer.template":
259             case "application/x-mspublisher":
260                 return true;
261             default:
262                 return false;
263         }
264     }
265 
266     /**
267      * Get the file extension from the mime type.
268      *
269      * @param mimeType A MIME type (i.e. text/plain)
270      *
271      * @return -
272      *      {@link MimeTypeMap#getExtensionFromMimeType} if not {@code null} or
273      *      {@link #DEFAULT_IMAGE_FILE_EXTENSION} if the mimeType is {@link #isImageMimeType} or
274      *      {@link #DEFAULT_VIDEO_FILE_EXTENSION} if the mimeType is {@link #isVideoMimeType} or
275      *      {@code ""} otherwise.
276      */
277     @NonNull
getExtensionFromMimeType(@ullable String mimeType)278     public static String getExtensionFromMimeType(@Nullable String mimeType) {
279         Optional<String> android15Extension = getExtFromMimeTypeForAndroid15(mimeType);
280         if (android15Extension.isPresent()) {
281             return android15Extension.get();
282         }
283 
284         final String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
285         if (extension != null) {
286             return "." + extension;
287         }
288 
289         Log.d(TAG, "No extension found for the mime type " + mimeType
290                 + ", returning the default file extension.");
291         // TODO(b/269614462): Eliminate the image and video extension hard codes for picker uri
292         //  display names
293         if (isImageMimeType(mimeType)) {
294             return DEFAULT_IMAGE_FILE_EXTENSION;
295         }
296         if (isVideoMimeType(mimeType)) {
297             return DEFAULT_VIDEO_FILE_EXTENSION;
298         }
299 
300         return "";
301     }
302 
getMimeTypeForAndroid15(String extension)303     private static Optional<String> getMimeTypeForAndroid15(String extension) {
304         if (Flags.enableMimeTypeFixForAndroid15()
305                 && Build.VERSION.SDK_INT == Build.VERSION_CODES.VANILLA_ICE_CREAM) {
306             return MimeTypeFixHandler.getMimeType(extension);
307         }
308         return Optional.empty();
309     }
310 
311     /**
312      * Gets file extension from MIME type for Android 15.
313      * Handles Android 15 specific MIME type to extension mapping. If the mime-type is corrupted,
314      * then return the default one with respect to mime type.
315      *
316      * @param mimeType The MIME type.
317      * @return Optional file extension (with dot), or empty.
318      */
getExtFromMimeTypeForAndroid15(String mimeType)319     private static Optional<String> getExtFromMimeTypeForAndroid15(String mimeType) {
320         if (Flags.enableMimeTypeFixForAndroid15()
321                 && Build.VERSION.SDK_INT == Build.VERSION_CODES.VANILLA_ICE_CREAM) {
322             Optional<String> value = MimeTypeFixHandler.getExtFromMimeType(mimeType);
323             if (value.isPresent()) {
324                 return Optional.of("." + value.get());
325             } else if (MimeTypeFixHandler.isCorruptedMimeType(mimeType)) {
326                 if (isImageMimeType(mimeType)) return Optional.of(DEFAULT_IMAGE_FILE_EXTENSION);
327                 if (isVideoMimeType(mimeType)) return Optional.of(DEFAULT_VIDEO_FILE_EXTENSION);
328                 return Optional.of("");
329             }
330         }
331         return Optional.empty();
332     }
333 
334 }
335