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