package org.wordpress.android.util; import android.annotation.TargetApi; import android.app.Activity; import android.content.ContentUris; import android.content.Context; import android.content.CursorLoader; import android.database.Cursor; import android.graphics.BitmapFactory; import android.media.MediaMetadataRetriever; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.provider.DocumentsContract; import android.provider.MediaStore; import android.provider.OpenableColumns; import android.support.annotation.Nullable; import android.text.TextUtils; import android.webkit.MimeTypeMap; import org.wordpress.android.util.AppLog.T; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; import static android.os.Environment.isExternalStorageRemovable; public class MediaUtils { private static final int DEFAULT_MAX_IMAGE_WIDTH = 1024; private static final Pattern FILE_EXISTS_PATTERN = Pattern.compile("(.*?)(-([0-9]+))?(\\..*$)?"); public static boolean isValidImage(String url) { if (url == null) { return false; } url = url.toLowerCase(); return url.endsWith(".png") || url.endsWith(".jpg") || url.endsWith(".jpeg") || url.endsWith(".gif"); } public static boolean isDocument(String url) { if (url == null) { return false; } url = url.toLowerCase(); return url.endsWith(".doc") || url.endsWith(".docx") || url.endsWith(".odt") || url.endsWith(".pdf"); } public static boolean isPowerpoint(String url) { if (url == null) { return false; } url = url.toLowerCase(); return url.endsWith(".ppt") || url.endsWith(".pptx") || url.endsWith(".pps") || url.endsWith(".ppsx") || url.endsWith(".key"); } public static boolean isSpreadsheet(String url) { if (url == null) { return false; } url = url.toLowerCase(); return url.endsWith(".xls") || url.endsWith(".xlsx"); } public static boolean isVideo(String url) { if (url == null) { return false; } url = url.toLowerCase(); return url.endsWith(".ogv") || url.endsWith(".mp4") || url.endsWith(".m4v") || url.endsWith(".mov") || url.endsWith(".wmv") || url.endsWith(".avi") || url.endsWith(".mpg") || url.endsWith(".3gp") || url.endsWith(".3g2") || url.contains("video"); } public static boolean isAudio(String url) { if (url == null) { return false; } url = url.toLowerCase(); return url.endsWith(".mp3") || url.endsWith(".ogg") || url.endsWith(".wav") || url.endsWith(".wma") || url.endsWith(".aiff") || url.endsWith(".aif") || url.endsWith(".aac") || url.endsWith(".m4a"); } /** * E.g. Jul 2, 2013 @ 21:57 */ public static String getDate(long ms) { Date date = new Date(ms); SimpleDateFormat sdf = new SimpleDateFormat("MMM d, yyyy '@' HH:mm", Locale.ENGLISH); // The timezone on the website is at GMT sdf.setTimeZone(TimeZone.getTimeZone("GMT")); return sdf.format(date); } public static boolean isLocalFile(String state) { if (state == null) { return false; } return state.equalsIgnoreCase("queued") || state.equalsIgnoreCase("uploading") || state.equalsIgnoreCase("retry") || state.equalsIgnoreCase("failed"); } public static Uri getLastRecordedVideoUri(Activity activity) { String[] proj = { MediaStore.Video.Media._ID }; Uri contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; String sortOrder = MediaStore.Video.VideoColumns.DATE_TAKEN + " DESC"; CursorLoader loader = new CursorLoader(activity, contentUri, proj, null, null, sortOrder); Cursor cursor = loader.loadInBackground(); cursor.moveToFirst(); long value = cursor.getLong(0); SqlUtils.closeCursor(cursor); return Uri.parse(contentUri.toString() + "/" + value); } /** * Get image width setting from the image width site setting string. This string can be an int, in this case it's * the maximum image width defined by the site. * Examples: * "1000" will return 1000 * "Original Size" will return Integer.MAX_VALUE * "Largeur originale" will return Integer.MAX_VALUE * null will return Integer.MAX_VALUE * @param imageWidthSiteSettingString Image width site setting string * @return Integer.MAX_VALUE if image width is not defined or invalid, maximum image width in other cases. */ public static int getImageWidthSettingFromString(String imageWidthSiteSettingString) { if (imageWidthSiteSettingString == null) { return Integer.MAX_VALUE; } try { return Integer.valueOf(imageWidthSiteSettingString); } catch (NumberFormatException e) { return Integer.MAX_VALUE; } } /** * Calculate and return the maximum allowed image width by comparing the width of the image at its full size with * the maximum upload width set in the blog settings * @param imageWidth the image's natural (full) width * @param imageWidthSiteSettingString the maximum upload width set in the site settings * @return maximum allowed image width */ public static int getMaximumImageWidth(int imageWidth, String imageWidthSiteSettingString) { int imageWidthBlogSetting = getImageWidthSettingFromString(imageWidthSiteSettingString); int imageWidthPictureSetting = imageWidth == 0 ? Integer.MAX_VALUE : imageWidth; if (Math.min(imageWidthPictureSetting, imageWidthBlogSetting) == Integer.MAX_VALUE) { // Default value in case of errors reading the picture size or the blog settings is set to Original size return DEFAULT_MAX_IMAGE_WIDTH; } else { return Math.min(imageWidthPictureSetting, imageWidthBlogSetting); } } public static int getMaximumImageWidth(Context context, Uri curStream, String imageWidthBlogSettingString) { int[] dimensions = ImageUtils.getImageSize(curStream, context); return getMaximumImageWidth(dimensions[0], imageWidthBlogSettingString); } public static boolean isInMediaStore(Uri mediaUri) { // Check if the image is externally hosted (Picasa/Google Photos for example) if (mediaUri != null && mediaUri.toString().startsWith("content://media/")) { return true; } else { return false; } } public static @Nullable String getFilenameFromURI(Context context, Uri uri) { Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); try { String result = null; if (cursor != null && cursor.moveToFirst()) { int columnIndexDisplayName = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); if (columnIndexDisplayName == -1) { return null; } result = cursor.getString(columnIndexDisplayName); } return result; } finally { if (cursor != null) { cursor.close(); } } } public static File getDiskCacheDir(Context context) { // Check if media is mounted or storage is built-in, if so, try and use external cache dir // otherwise use internal cache dir return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !isExternalStorageRemovable() ? context.getApplicationContext().getExternalCacheDir() : context.getCacheDir(); } public static Uri downloadExternalMedia(Context context, Uri imageUri) { if (context == null || imageUri == null) { return null; } String mimeType = context.getContentResolver().getType(imageUri); File cacheDir = getDiskCacheDir(context); if (cacheDir != null && !cacheDir.exists()) { cacheDir.mkdirs(); } try { InputStream input; // Download the file if (imageUri.toString().startsWith("content://")) { input = context.getContentResolver().openInputStream(imageUri); if (input == null) { AppLog.e(T.UTILS, "openInputStream returned null"); return null; } } else { input = new URL(imageUri.toString()).openStream(); } String fileName = getFilenameFromURI(context, imageUri); if (TextUtils.isEmpty(fileName)) { fileName = generateTimeStampedFileName(mimeType); } File f = getUniqueCacheFileForName(fileName, cacheDir, mimeType); OutputStream output = new FileOutputStream(f); byte data[] = new byte[1024]; int count; while ((count = input.read(data)) != -1) { output.write(data, 0, count); } output.flush(); output.close(); input.close(); return Uri.fromFile(f); } catch (IOException e) { AppLog.e(T.UTILS, e); } return null; } private static File getUniqueCacheFileForName(String fileName, File cacheDir, String mimeType) { File file = new File(cacheDir, fileName); while (file.exists()) { Matcher matcher = FILE_EXISTS_PATTERN.matcher(fileName); if (matcher.matches()) { String baseFileName = matcher.group(1); String existingDuplicationNumber = matcher.group(3); String fileType = StringUtils.notNullStr(matcher.group(4)); if (existingDuplicationNumber == null) { // Not a copy already fileName = baseFileName + "-1" + fileType; } else { fileName = baseFileName + "-" + (StringUtils.stringToInt(existingDuplicationNumber) + 1) + fileType; } } else { // Shouldn't happen, but in case our match fails fall back to timestamped file name fileName = generateTimeStampedFileName(mimeType); } file = new File(cacheDir, fileName); } return file; } private static String generateTimeStampedFileName(String mimeType) { return "wp-" + System.currentTimeMillis() + "." + getExtensionForMimeType(mimeType); } public static String getMimeTypeOfInputStream(InputStream stream) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeStream(stream, null, options); return options.outMimeType; } public static String getMediaFileMimeType(File mediaFile) { String originalFileName = mediaFile.getName().toLowerCase(); String mimeType = UrlUtils.getUrlMimeType(originalFileName); if (TextUtils.isEmpty(mimeType)) { try { String filePathForGuessingMime; if (mediaFile.getPath().contains("://")) { filePathForGuessingMime = Uri.encode(mediaFile.getPath(), ":/"); } else { filePathForGuessingMime = "file://"+ Uri.encode(mediaFile.getPath(), "/"); } URL urlForGuessingMime = new URL(filePathForGuessingMime); URLConnection uc = urlForGuessingMime.openConnection(); String guessedContentType = null; try { guessedContentType = uc.getContentType(); //internally calls guessContentTypeFromName(url.getFile()); and guessContentTypeFromStream(is); } catch (StringIndexOutOfBoundsException e) { // Ref: https://github.com/wordpress-mobile/WordPress-Android/issues/5699 AppLog.e(AppLog.T.MEDIA, "Error getting the content type for " + mediaFile.getPath() +" by using URLConnection.getContentType", e); } // check if returned "content/unknown" if (!TextUtils.isEmpty(guessedContentType) && !guessedContentType.equals("content/unknown")) { mimeType = guessedContentType; } } catch (MalformedURLException e) { AppLog.e(AppLog.T.MEDIA, "MalformedURLException while trying to guess the content type for the file here " + mediaFile.getPath() + " with URLConnection", e); } catch (IOException e) { AppLog.e(AppLog.T.MEDIA, "Error while trying to guess the content type for the file here " + mediaFile.getPath() +" with URLConnection", e); } } // No mimeType yet? Try to decode the image and get the mimeType from there if (TextUtils.isEmpty(mimeType)) { try { DataInputStream inputStream = new DataInputStream(new FileInputStream(mediaFile)); String mimeTypeFromStream = getMimeTypeOfInputStream(inputStream); if (!TextUtils.isEmpty(mimeTypeFromStream)) { mimeType = mimeTypeFromStream; } inputStream.close(); } catch (FileNotFoundException e) { AppLog.e(AppLog.T.MEDIA, "FileNotFoundException while trying to guess the content type for the file " + mediaFile.getPath(), e); } catch (IOException e) { AppLog.e(AppLog.T.MEDIA, "IOException while trying to guess the content type for the file " + mediaFile.getPath(), e); } } if (TextUtils.isEmpty(mimeType)) { mimeType = ""; } else { if (mimeType.equalsIgnoreCase("video/mp4v-es")) { //Fixes #533. See: http://tools.ietf.org/html/rfc3016 mimeType = "video/mp4"; } } return mimeType; } public static String getMediaFileName(File mediaFile, String mimeType) { String originalFileName = mediaFile.getName().toLowerCase(); String extension = MimeTypeMap.getFileExtensionFromUrl(originalFileName); if (!TextUtils.isEmpty(extension)) //File name already has the extension in it return originalFileName; if (!TextUtils.isEmpty(mimeType)) { //try to get the extension from mimeType String fileExtension = getExtensionForMimeType(mimeType); if (!TextUtils.isEmpty(fileExtension)) { originalFileName += "." + fileExtension; } } else { //No mimetype and no extension!! AppLog.e(AppLog.T.API, "No mimetype and no extension for " + mediaFile.getPath()); } return originalFileName; } public static String getExtensionForMimeType(String mimeType) { if (TextUtils.isEmpty(mimeType)) return ""; MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); String fileExtensionFromMimeType = mimeTypeMap.getExtensionFromMimeType(mimeType); if (TextUtils.isEmpty(fileExtensionFromMimeType)) { // We're still without an extension - split the mime type and retrieve it String[] split = mimeType.split("/"); fileExtensionFromMimeType = split.length > 1 ? split[1] : split[0]; } return fileExtensionFromMimeType.toLowerCase(); } public static String getRealPathFromURI(final Context context, Uri uri) { String path; if ("content".equals(uri.getScheme())) { path = MediaUtils.getPath(context, uri); } else if ("file".equals(uri.getScheme())) { path = uri.getPath(); } else { path = uri.toString(); } return path; } /** * Get a file path from a Uri. This will get the the path for Storage Access * Framework Documents, as well as the _data field for the MediaStore and * other file-based ContentProviders. * * Based on paulburke's solution for aFileChooser - https://github.com/iPaulPro/aFileChooser * * @param context The context. * @param uri The Uri to query. */ private static String getPath(final Context context, final Uri uri) { String path = getDocumentProviderPathKitkatOrHigher(context, uri); if (path != null) { return path; } // MediaStore (and general) if ("content".equalsIgnoreCase(uri.getScheme())) { return getDataColumn(context, uri, null, null); } // File else if ("file".equalsIgnoreCase(uri.getScheme())) { return uri.getPath(); } return null; } @TargetApi(Build.VERSION_CODES.KITKAT) private static String getDocumentProviderPathKitkatOrHigher(final Context context, final Uri uri) { final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; // DocumentProvider if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider if (isExternalStorageDocument(uri)) { final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; if ("primary".equalsIgnoreCase(type)) { return Environment.getExternalStorageDirectory() + "/" + split[1]; } // TODO handle non-primary volumes } // DownloadsProvider else if (isDownloadsDocument(uri)) { final String id = DocumentsContract.getDocumentId(uri); final Uri contentUri = ContentUris.withAppendedId( Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); return getDataColumn(context, contentUri, null, null); } // MediaProvider else if (isMediaDocument(uri)) { final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; Uri contentUri = null; if ("image".equals(type)) { contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; } else if ("video".equals(type)) { contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; } else if ("audio".equals(type)) { contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; } final String selection = MediaStore.MediaColumns._ID + "=?"; final String[] selectionArgs = new String[] { split[1] }; return getDataColumn(context, contentUri, selection, selectionArgs); } } return null; } /** * Get the value of the data column for this Uri. This is useful for * MediaStore Uris, and other file-based ContentProviders. * * @param context The context. * @param uri The Uri to query. * @param selection (Optional) Filter used in the query. * @param selectionArgs (Optional) Selection arguments used in the query. * @return The value of the _data column, which is typically a file path. */ public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { Cursor cursor = null; final String column = MediaStore.MediaColumns.DATA; final String[] projection = { column }; try { cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); if (cursor != null && cursor.moveToFirst()) { final int column_index = cursor.getColumnIndex(column); if (column_index != -1) { return cursor.getString(column_index); } } } finally { if (cursor != null) { cursor.close(); } } return null; } /** * @param uri The Uri to check. * @return Whether the Uri authority is ExternalStorageProvider. */ public static boolean isExternalStorageDocument(Uri uri) { return "com.android.externalstorage.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is DownloadsProvider. */ public static boolean isDownloadsDocument(Uri uri) { return "com.android.providers.downloads.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is MediaProvider. */ public static boolean isMediaDocument(Uri uri) { return "com.android.providers.media.documents".equals(uri.getAuthority()); } public static long getVideoDurationMS(Context context, File file) { if(context == null || file == null) { AppLog.e(AppLog.T.MEDIA, "context and file can't be null."); return 0L; } return getVideoDurationMS(context, Uri.fromFile(file)); } public static long getVideoDurationMS(Context context, Uri videoUri) { if(context == null || videoUri == null) { AppLog.e(AppLog.T.MEDIA, "context and videoUri can't be null."); return 0L; } MediaMetadataRetriever retriever = new MediaMetadataRetriever(); try { retriever.setDataSource(context, videoUri); } catch (IllegalArgumentException | SecurityException e) { AppLog.e(AppLog.T.MEDIA, "Can't read duration of the video.", e); return 0L; } catch (RuntimeException e) { // Ref: https://github.com/wordpress-mobile/WordPress-Android/issues/5431 AppLog.e(AppLog.T.MEDIA, "Can't read duration of the video due to a Runtime Exception happened setting the datasource", e); return 0L; } String time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); if (time == null) { return 0L; } return Long.parseLong(time); } }