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