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