| /* |
| * Copyright (C) 2004-2020 Savoir-faire Linux Inc. |
| * |
| * Author: Aline Bonnet <aline.bonnet@savoirfairelinux.com> |
| * Author: Adrien BĂ©raud <adrien.beraud@savoirfairelinux.com> |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 3 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program. If not, see <http://www.gnu.org/licenses/>. |
| */ |
| package cx.ring.utils; |
| |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.Context; |
| import android.content.res.AssetManager; |
| import android.database.Cursor; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.Matrix; |
| import android.media.ExifInterface; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Environment; |
| import android.os.StatFs; |
| import android.provider.DocumentsContract; |
| import android.provider.MediaStore; |
| import android.provider.OpenableColumns; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.webkit.MimeTypeMap; |
| |
| 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.text.SimpleDateFormat; |
| import java.util.Date; |
| import java.util.Locale; |
| |
| import androidx.annotation.NonNull; |
| |
| import io.reactivex.Completable; |
| import io.reactivex.Single; |
| import io.reactivex.schedulers.Schedulers; |
| |
| public class AndroidFileUtils { |
| |
| private static final String TAG = AndroidFileUtils.class.getSimpleName(); |
| private final static int ORIENTATION_LEFT = 270; |
| private final static int ORIENTATION_RIGHT = 90; |
| private final static int MAX_IMAGE_DIMENSION = 1024; |
| |
| /** |
| * Copy assets from a folder recursively ( files and subfolder) |
| * @param assetManager Asset Manager ( you can get it from Context.getAssets() ) |
| * @param fromAssetPath path to the assets folder we want to copy |
| * @param toPath a directory in internal storage |
| * @return true if success |
| */ |
| public static boolean copyAssetFolder( @NonNull AssetManager assetManager, String fromAssetPath, File toPath) { |
| try { |
| boolean res = true; |
| |
| // mkdirs checks if the folder exists and if not creates it |
| toPath.mkdirs(); |
| |
| // List the files of this asset directory |
| String[] files = assetManager.list(fromAssetPath); |
| |
| if (files != null) { |
| for (String file : files) { |
| String subAsset = fromAssetPath + File.separator + file; |
| if (isAssetDirectory(assetManager, subAsset)) { |
| String destination = toPath.getAbsolutePath() + File.separator + file; |
| File newDir = new File(destination); |
| copyAssetFolder(assetManager, subAsset, newDir); |
| Log.d(TAG, "Copied folder: " + subAsset + " to " + newDir); |
| } else { |
| File newFile = new File(toPath, file); |
| res &= copyAsset(assetManager, fromAssetPath + File.separator + file, newFile); |
| Log.d(TAG, "Copied file: " + subAsset + " to " + newFile); |
| } |
| } |
| } |
| |
| return res; |
| } catch (IOException e) { |
| Log.e(TAG, "Error while copying asset folder", e); |
| return false; |
| } |
| } |
| |
| /** |
| * Checks whether an asset is a file or a directory |
| * @param assetManager Asset Manager ( you can get it from Context.getAssets() ) |
| * @param fromAssetPath asset path, if just a file in assets root folder then it should be |
| * the file name, otherwise, folder/filename |
| * @return boolean directory or not |
| */ |
| public static boolean isAssetDirectory( @NonNull AssetManager assetManager, String fromAssetPath) { |
| try { |
| String[] files = assetManager.list(fromAssetPath); |
| if (files != null && files.length > 0) { |
| return true; |
| } |
| } catch (IOException e) { |
| Log.e(TAG, "Error while reading an asset ", e); |
| } |
| return false; |
| } |
| |
| /** |
| * Prints assets tree |
| * @param assetManager Asset Manager ( you can get it from Context.getAssets() ) |
| * @param rootPath default empty, sub folder otherwise |
| * @param fileName the name of the file or folder |
| * @param level default 0, should be 0 |
| */ |
| public static void assetTree( @NonNull AssetManager assetManager, String rootPath, String fileName, int level) { |
| try { |
| String fromAssetPath; |
| if(TextUtils.isEmpty(rootPath)) { |
| fromAssetPath = fileName; |
| } else { |
| fromAssetPath = rootPath + File.separator+ fileName; |
| } |
| |
| String repeated = new String(new char[level]).replace("\0", "\t|"); |
| |
| String[] files = assetManager.list(fromAssetPath); |
| |
| if (files != null) { |
| Log.d(TAG, "|"+ repeated + "-- " + fileName); |
| for(String file : files) { |
| assetTree(assetManager,fromAssetPath,file,level+1); |
| } |
| } |
| } catch (IOException e) { |
| Log.e(TAG, "Error while reading asset ", e); |
| } |
| } |
| |
| public static boolean copyAsset(AssetManager assetManager, String fromAssetPath, File toPath) { |
| try (InputStream in = assetManager.open(fromAssetPath); |
| OutputStream out = new FileOutputStream(toPath)) { |
| FileUtils.copyFile(in, out); |
| out.flush(); |
| return true; |
| } catch (IOException e) { |
| Log.e(TAG, "Error while copying asset", e); |
| return false; |
| } |
| } |
| |
| public static String getRealPathFromURI(Context context, Uri uri) { |
| String path = null; |
| if (DocumentsContract.isDocumentUri(context, uri)) { |
| if (isExternalStorageDocument(uri)) { |
| final String docId = DocumentsContract.getDocumentId(uri); |
| final String[] split = docId.split(":"); |
| final String type = split[0]; |
| if ("primary".equalsIgnoreCase(type)) { |
| path = Environment.getExternalStorageDirectory() + "/" + split[1]; |
| } |
| } else if (isDownloadsDocument(uri)) { |
| final String id = DocumentsContract.getDocumentId(uri); |
| final Uri contentUri = ContentUris.withAppendedId( |
| Uri.parse("content://downloads/public_downloads"), Long.parseLong(id)); |
| path = getDataColumn(context, contentUri, null, null); |
| } 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 = "_id=?"; |
| final String[] selectionArgs = new String[]{split[1]}; |
| path = getDataColumn(context, contentUri, selection, selectionArgs); |
| } |
| } else if ("content".equalsIgnoreCase(uri.getScheme())) { |
| path = getDataColumn(context, uri, null, null); |
| } else if ("file".equalsIgnoreCase(uri.getScheme())) { |
| path = uri.getPath(); |
| } |
| return path; |
| } |
| |
| private static String getFilename(ContentResolver cr, Uri uri) { |
| String result = null; |
| if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { |
| try (Cursor cursor = cr.query(uri, null, null, null, null)) { |
| if (cursor != null && cursor.moveToFirst()) { |
| result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); |
| } |
| } |
| } |
| if (result == null) { |
| result = uri.getPath(); |
| int cut = result.lastIndexOf('/'); |
| if (cut != -1) { |
| result = result.substring(cut + 1); |
| } |
| } |
| if (result.lastIndexOf('.') == -1) { |
| String mimeType = getMimeType(cr, uri); |
| String extensionFromMimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); |
| if (extensionFromMimeType != null) { |
| result += '.' + extensionFromMimeType; |
| } |
| } |
| return result; |
| } |
| |
| private static String getMimeType(ContentResolver cr, Uri uri) { |
| String mimeType; |
| if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { |
| mimeType = cr.getType(uri); |
| } else { |
| mimeType = getMimeType(uri.toString()); |
| } |
| return mimeType; |
| } |
| |
| public static String getMimeType(String filename) { |
| int pos = filename.lastIndexOf("."); |
| String fileExtension = null; |
| if (pos >= 0) { |
| fileExtension = MimeTypeMap.getFileExtensionFromUrl(filename.substring(pos)); |
| } |
| return getMimeTypeFromExtension(fileExtension); |
| } |
| |
| public static String getMimeTypeFromExtension(String ext) { |
| if (!TextUtils.isEmpty(ext)) { |
| String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.toLowerCase()); |
| if (!TextUtils.isEmpty(mimeType)) |
| return mimeType; |
| if (ext.contentEquals("gz")) { |
| return "application/gzip"; |
| } |
| } |
| return "application/octet-stream"; |
| } |
| |
| public static File getTempShareDir(@NonNull Context context) { |
| File tmp = new File(context.getCacheDir(), "tmp"); |
| tmp.mkdir(); |
| return tmp; |
| } |
| |
| public static File createImageFile(@NonNull Context context) throws IOException { |
| // Create an image file name |
| String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); |
| String imageFileName = "img_" + timeStamp + "_"; |
| |
| // Save a file: path for use with ACTION_VIEW intents |
| return File.createTempFile(imageFileName, ".jpg", getTempShareDir(context)); |
| } |
| |
| public static File createAudioFile(@NonNull Context context) throws IOException { |
| // Create an image file name |
| String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); |
| String imageFileName = "audio_" + timeStamp + "_"; |
| |
| // Save a file: path for use with ACTION_VIEW intents |
| return File.createTempFile(imageFileName, ".mp3", getTempShareDir(context)); |
| } |
| public static File createVideoFile(@NonNull Context context) throws IOException { |
| // Create an image file name |
| String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); |
| String imageFileName = "video_" + timeStamp + "_"; |
| |
| // Save a file: path for use with ACTION_VIEW intents |
| return File.createTempFile(imageFileName, ".webm", getTempShareDir(context)); |
| } |
| |
| /** |
| * Copies a file from a uri whether locally on a remote location to the local cache |
| * @param context Context to get access to cache directory |
| * @param uri uri of the |
| * @return Single<File> which points to the newly created copy in the cache |
| */ |
| public static @NonNull Single<File> getCacheFile(@NonNull Context context, @NonNull Uri uri) { |
| ContentResolver contentResolver = context.getContentResolver(); |
| File cacheDir = context.getCacheDir(); |
| return Single.fromCallable(() -> { |
| File file = new File(cacheDir, getFilename(contentResolver, uri)); |
| FileOutputStream output = new FileOutputStream(file); |
| InputStream inputStream = contentResolver.openInputStream(uri); |
| if (inputStream == null) |
| throw new FileNotFoundException(); |
| FileUtils.copyFile(inputStream, output); |
| inputStream.close(); |
| output.flush(); |
| output.close(); |
| return file; |
| }).subscribeOn(Schedulers.io()); |
| } |
| |
| public static Completable moveToUri(@NonNull ContentResolver cr, @NonNull File input, @NonNull Uri outUri) { |
| return Completable.fromAction(() -> { |
| InputStream inputStream = null; |
| OutputStream output = null; |
| try { |
| inputStream = new FileInputStream(input); |
| output = cr.openOutputStream(outUri); |
| FileUtils.copyFile(inputStream, output); |
| input.delete(); |
| } finally { |
| try { |
| if (inputStream != null) |
| inputStream.close(); |
| if (output != null) |
| output.close(); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| } |
| } |
| }).subscribeOn(Schedulers.io()); |
| } |
| |
| /** |
| * Copies a file to a predefined Uri destination |
| * Uses the underlying copyFile(InputStream,OutputStream) |
| * @param cr content resolver |
| * @param input the file we want to copy |
| * @param outUri the uri destination |
| * @return success value |
| */ |
| public static Completable copyFileToUri(ContentResolver cr, File input, Uri outUri){ |
| return Completable.fromAction(() -> { |
| try (InputStream inputStream = new FileInputStream(input); OutputStream outputStream = cr.openOutputStream(outUri)) { |
| FileUtils.copyFile(inputStream, outputStream); |
| } |
| }).subscribeOn(Schedulers.io()); |
| } |
| |
| public static File getConversationFile(Context context, Uri uri, String conversationId, String name) throws IOException { |
| File file = getConversationPath(context, conversationId, name); |
| FileOutputStream output = new FileOutputStream(file); |
| InputStream inputStream = context.getContentResolver().openInputStream(uri); |
| FileUtils.copyFile(inputStream, output); |
| return file; |
| } |
| |
| public static File getCachePath(Context context, String filename) { |
| return new File(context.getCacheDir(), filename); |
| } |
| |
| public static File getFilePath(Context context, String filename) { |
| return context.getFileStreamPath(filename); |
| } |
| |
| public static File getConversationPath(Context context, String conversationId, String name) { |
| File conversationsDir = getFilePath(context, "conversation_data"); |
| |
| if (!conversationsDir.exists()) |
| conversationsDir.mkdir(); |
| |
| File conversationDir = new File(conversationsDir, conversationId); |
| if (!conversationDir.exists()) |
| conversationDir.mkdir(); |
| |
| return new File(conversationDir, name); |
| } |
| |
| public static File getTempPath(Context context, String conversationId, String name) { |
| File conversationsDir = getCachePath(context, "conversation_data"); |
| |
| if (!conversationsDir.exists()) |
| conversationsDir.mkdir(); |
| |
| File conversationDir = new File(conversationsDir, conversationId); |
| if (!conversationDir.exists()) |
| conversationDir.mkdir(); |
| |
| return new File(conversationDir, name); |
| } |
| |
| public static String writeCacheFileToExtStorage(Context context, Uri cacheFile, String targetFilename) throws IOException { |
| File downloadsDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); |
| |
| int fileCount = 0; |
| File finalFile = new File(downloadsDirectory, targetFilename); |
| int lastDotIndex = targetFilename.lastIndexOf('.'); |
| String filename = targetFilename.substring(0, lastDotIndex); |
| String extension = targetFilename.substring(lastDotIndex + 1); |
| while (finalFile.exists()) { |
| finalFile = new File(downloadsDirectory, filename + "_" + fileCount + '.' + extension); |
| fileCount++; |
| } |
| |
| Log.d(TAG, "writeCacheFileToExtStorage: finalFile=" + finalFile + ",exists=" + finalFile.exists()); |
| InputStream inputStream = context.getContentResolver().openInputStream(cacheFile); |
| FileOutputStream output = new FileOutputStream(finalFile); |
| FileUtils.copyFile(inputStream, output); |
| return finalFile.toString(); |
| } |
| |
| public static boolean isExternalStorageWritable() { |
| String state = Environment.getExternalStorageState(); |
| return Environment.MEDIA_MOUNTED.equals(state); |
| } |
| |
| private static boolean isExternalStorageDocument(Uri uri) { |
| return "com.android.externalstorage.documents".equals(uri.getAuthority()); |
| } |
| |
| private static boolean isDownloadsDocument(Uri uri) { |
| return "com.android.providers.downloads.documents".equals(uri.getAuthority()); |
| } |
| |
| private static boolean isMediaDocument(Uri uri) { |
| return "com.android.providers.media.documents".equals(uri.getAuthority()); |
| } |
| |
| private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { |
| String path = null; |
| final String column = "_data"; |
| final String[] projection = {column}; |
| try (Cursor cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null)) { |
| if (cursor != null && cursor.moveToFirst()) { |
| final int column_index = cursor.getColumnIndexOrThrow(column); |
| path = cursor.getString(column_index); |
| } |
| } catch (Exception e) { |
| Log.e(TAG, "Error while saving the ringtone", e); |
| } |
| return path; |
| } |
| |
| public static File ringtonesPath(Context context) { |
| return new File(context.getFilesDir(), "ringtones"); |
| } |
| |
| /** |
| * Get space left in a specific path |
| * |
| * @return -1L if an error occurred, size otherwise |
| */ |
| public static long getSpaceLeft(String path) { |
| try { |
| StatFs statfs = new StatFs(path); |
| return statfs.getAvailableBytes(); |
| } catch (IllegalArgumentException e) { |
| Log.e(TAG, "getSpaceLeft: not able to access path on " + path); |
| return -1L; |
| } |
| } |
| |
| public static Single<Bitmap> loadBitmap(Context context, Uri uriImage) { |
| return Single.fromCallable(() -> { |
| BitmapFactory.Options dbo = new BitmapFactory.Options(); |
| dbo.inJustDecodeBounds = true; |
| |
| try (InputStream is = context.getContentResolver().openInputStream(uriImage)) { |
| BitmapFactory.decodeStream(is, null, dbo); |
| } |
| |
| int rotatedWidth, rotatedHeight; |
| int orientation = getOrientation(context, uriImage); |
| |
| if (orientation == ORIENTATION_LEFT || orientation == ORIENTATION_RIGHT) { |
| rotatedWidth = dbo.outHeight; |
| rotatedHeight = dbo.outWidth; |
| } else { |
| rotatedWidth = dbo.outWidth; |
| rotatedHeight = dbo.outHeight; |
| } |
| |
| Bitmap srcBitmap; |
| try (InputStream is = context.getContentResolver().openInputStream(uriImage)) { |
| if (rotatedWidth > MAX_IMAGE_DIMENSION || rotatedHeight > MAX_IMAGE_DIMENSION) { |
| float widthRatio = ((float) rotatedWidth) / ((float) MAX_IMAGE_DIMENSION); |
| float heightRatio = ((float) rotatedHeight) / ((float) MAX_IMAGE_DIMENSION); |
| float maxRatio = Math.max(widthRatio, heightRatio); |
| |
| // Create the bitmap from file |
| BitmapFactory.Options options = new BitmapFactory.Options(); |
| options.inSampleSize = (int) maxRatio; |
| srcBitmap = BitmapFactory.decodeStream(is, null, options); |
| } else { |
| srcBitmap = BitmapFactory.decodeStream(is); |
| } |
| } |
| |
| if (orientation > 0) { |
| Matrix matrix = new Matrix(); |
| matrix.postRotate(orientation); |
| |
| srcBitmap = Bitmap.createBitmap(srcBitmap, 0, 0, srcBitmap.getWidth(), |
| srcBitmap.getHeight(), matrix, true); |
| } |
| return srcBitmap; |
| }).subscribeOn(Schedulers.io()); |
| } |
| |
| private static int getOrientation(@NonNull Context context, @NonNull Uri photoUri) { |
| ContentResolver resolver = context.getContentResolver(); |
| if (resolver == null) |
| return 0; |
| try (Cursor cursor = resolver.query(photoUri, new String[]{MediaStore.Images.ImageColumns.ORIENTATION}, null, null, null)) { |
| cursor.moveToFirst(); |
| return cursor.getInt(0); |
| } catch (Exception e) { |
| switch (getExifOrientation(resolver, photoUri)) { |
| case ExifInterface.ORIENTATION_ROTATE_90: |
| return 90; |
| case ExifInterface.ORIENTATION_ROTATE_180: |
| return 180; |
| case ExifInterface.ORIENTATION_ROTATE_270: |
| return 270; |
| default: |
| return 0; |
| } |
| } |
| } |
| |
| private static int getExifOrientation(@NonNull ContentResolver resolver, @NonNull Uri photoUri) { |
| if (Build.VERSION.SDK_INT > 23) { |
| try (InputStream input = resolver.openInputStream(photoUri)) { |
| return new ExifInterface(input) |
| .getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); |
| } catch (Exception e) { |
| return 0; |
| } |
| } else { |
| try { |
| return new ExifInterface(photoUri.getPath()) |
| .getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); |
| } catch (Exception e) { |
| return 0; |
| } |
| } |
| } |
| |
| public static boolean isImage(String s) { |
| return getMimeType(s).startsWith("image"); |
| } |
| |
| public static String getFileName(String s) { |
| String[] parts = s.split("\\/"); |
| return parts[parts.length - 1]; |
| } |
| } |