package org.horaapps.leafpic.util; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; import android.media.MediaScannerConnection; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.provider.BaseColumns; import android.provider.DocumentsContract; import android.provider.MediaStore; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.FileProvider; import android.support.v4.provider.DocumentFile; import android.util.Log; import org.horaapps.leafpic.R; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.channels.FileChannel; import java.util.HashSet; /** * Created by dnld on 26/05/16. */ public class ContentHelper { private static final String TAG = "ContentHelper"; private static final String PRIMARY_VOLUME_NAME = "primary"; /** * Check is a file is writable. Detects write issues on external SD card. * * @param file The file * @return true if the file is writable. */ private static boolean isWritable(@NonNull final File file) { boolean isExisting = file.exists(); try { FileOutputStream output = new FileOutputStream(file, true); try { output.close(); } catch (IOException e) { // do nothing. } } catch (java.io.FileNotFoundException e) { return false; } boolean result = file.canWrite(); // Ensure that file is not created during this process. if (!isExisting) { //noinspection ResultOfMethodCallIgnored file.delete(); } return result; } private static void scanFile(Context context, String[] paths) { MediaScannerConnection.scanFile(context, paths, null, null); } /** * Create a folder. The folder may even be on external SD card for Kitkat. * * @param dir The folder to be created. * @return True if creation was successful. */ public static boolean mkdir(Context context, @NonNull final File dir) { boolean success = dir.exists(); // Try the normal way if (!success) success = dir.mkdir(); // Try with Storage Access Framework. if (!success && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { DocumentFile document = getDocumentFile(context, dir, true, true); // getDocumentFile implicitly creates the directory. success = document != null && document.exists(); } //let MediaStore know that a dir was created if (success) scanFile(context, new String[] { dir.getPath() }); return success; } private static File getTargetFile(File source, File targetDir) { File file = new File(targetDir, source.getName()); if (!source.getParentFile().equals(targetDir) && !file.exists()) return file; return new File(targetDir, StringUtils.incrementFileNameSuffix(source.getName())); } public static Uri getUriForFile(Context context, File file) { return FileProvider.getUriForFile(context, context.getPackageName() + ".provider", file); } public static boolean copyFile(Context context, @NonNull final File source, @NonNull final File targetDir) { InputStream inStream = null; OutputStream outStream = null; boolean success = false; File target = getTargetFile(source, targetDir); try { inStream = new FileInputStream(source); // First try the normal way if (isWritable(target)) { // standard way FileChannel inChannel = new FileInputStream(source).getChannel(); FileChannel outChannel = new FileOutputStream(target).getChannel(); inChannel.transferTo(0, inChannel.size(), outChannel); success = true; try { inChannel.close(); } catch (Exception ignored) { } try { outChannel.close(); } catch (Exception ignored) { } } else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { //inStream = context.getContentResolver().openInputStream(Uri.fromFile(source)); //outStream = context.getContentResolver().openOutputStream(Uri.fromFile(target)); if (isFileOnSdCard(context, source)) { DocumentFile sourceDocument = getDocumentFile(context, source, false, false); if (sourceDocument != null) { inStream = context.getContentResolver().openInputStream(sourceDocument.getUri()); } } // Storage Access Framework DocumentFile targetDocument = getDocumentFile(context, target, false, false); if (targetDocument != null) { outStream = context.getContentResolver().openOutputStream(targetDocument.getUri()); } } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { // TODO: 13/08/16 test this // Workaround for Kitkat ext SD card Uri uri = getUriFromFile(context,target.getAbsolutePath()); if (uri != null) { outStream = context.getContentResolver().openOutputStream(uri); } } if (outStream != null) { // Both for SAF and for Kitkat, write to output stream. byte[] buffer = new byte[4096]; // MAGIC_NUMBER int bytesRead; while ((bytesRead = inStream.read(buffer)) != -1) outStream.write(buffer, 0, bytesRead); success = true; } } } catch (Exception e) { Log.e(TAG, "Error when copying file from " + source.getAbsolutePath() + " to " + target.getAbsolutePath(), e); return false; } finally { try { inStream.close(); } catch (Exception ignored) { } try { outStream.close(); } catch (Exception ignored) { } } if (success) scanFile(context, new String[] { target.getPath() }); return success; } public static boolean isFileOnSdCard(Context context, File file) { String sdcardPath = getSdcardPath(context); if (sdcardPath != null) return file.getPath().startsWith(sdcardPath); return false; } /** * Move a file. The target file may even be on external SD card. * * @param source The source file * @param targetDir The target Directory * @return true if the copying was successful. */ public static boolean moveFile(Context context, @NonNull final File source, @NonNull final File targetDir) { // First try the normal rename. File target = new File(targetDir, source.getName()); boolean success = source.renameTo(target); if (!success) { success = copyFile(context, source, targetDir); if (success) { success = deleteFile(context, source); } } //if (success) scanFile(context, new String[]{ source.getPath(), target.getPath() }); return success; } /** * Get an Uri from an file path. * * @param path The file path. * @return The Uri. */ private static Uri getUriFromFile(Context context, final String path) { ContentResolver resolver = context.getContentResolver(); Cursor filecursor = resolver.query(MediaStore.Files.getContentUri("external"), new String[] {BaseColumns._ID}, MediaStore.MediaColumns.DATA + " = ?", new String[] {path}, MediaStore.MediaColumns.DATE_ADDED + " desc"); if (filecursor == null) { return null; } filecursor.moveToFirst(); if (filecursor.isAfterLast()) { filecursor.close(); ContentValues values = new ContentValues(); values.put(MediaStore.MediaColumns.DATA, path); return resolver.insert(MediaStore.Files.getContentUri("external"), values); } else { int imageId = filecursor.getInt(filecursor.getColumnIndex(BaseColumns._ID)); Uri uri = MediaStore.Files.getContentUri("external").buildUpon().appendPath( Integer.toString(imageId)).build(); filecursor.close(); return uri; } } /** * Delete all files in a folder. * * @param folder the folder * @return true if successful. */ public static boolean deleteFilesInFolder(Context context, @NonNull final File folder) { boolean totalSuccess = true; String[] children = folder.list(); if (children != null) { for (String child : children) { File file = new File(folder, child); if (!file.isDirectory()) { boolean success = deleteFile(context, file); if (!success) { Log.w(TAG, "Failed to delete file" + child); totalSuccess = false; } } } } return totalSuccess; } /** * Delete a file. May be even on external SD card. * * @param file the file to be deleted. * @return True if successfully deleted. */ public static boolean deleteFile(Context context, @NonNull final File file) { //W/DocumentFile: Failed query: java.lang.IllegalArgumentException: Failed to determine if A613-F0E1:.android_secure is child of A613-F0E1:: java.io.FileNotFoundException: Missing file for A613-F0E1:.android_secure at /storage/sdcard1/.android_secure // First try the normal deletion. boolean success = file.delete(); // Try with Storage Access Framework. if (!success && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { DocumentFile document = getDocumentFile(context, file, false, false); success = document != null && document.delete(); } // Try the Kitkat workaround. if (!success && Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { ContentResolver resolver = context.getContentResolver(); try { Uri uri = null;//MediaStoreUtil.getUriFromFile(file.getAbsolutePath()); if (uri != null) { resolver.delete(uri, null, null); } success = !file.exists(); } catch (Exception e) { Log.e(TAG, "Error when deleting file " + file.getAbsolutePath(), e); return false; } } if(success) scanFile(context, new String[]{ file.getPath() }); return success; } public static HashSet<File> getStorageRoots(Context context) { HashSet<File> paths = new HashSet<File>(); for (File file : context.getExternalFilesDirs("external")) { if (file != null) { int index = file.getAbsolutePath().lastIndexOf("/Android/data"); if (index < 0) Log.w("asd", "Unexpected external file dir: " + file.getAbsolutePath()); else paths.add(new File(file.getAbsolutePath().substring(0, index))); } } return paths; } public static String getSdcardPath(Context context) { for(File file : context.getExternalFilesDirs("external")) { if (file != null && !file.equals(context.getExternalFilesDir("external"))) { int index = file.getAbsolutePath().lastIndexOf("/Android/data"); if (index < 0) Log.w("asd", "Unexpected external file dir: " + file.getAbsolutePath()); else return new File(file.getAbsolutePath().substring(0, index)).getPath(); } } return null; } /** * Delete a folder. * * @param file The folder name. * @return true if successful. */ public static boolean rmdir(Context context, @NonNull final File file) { if(!file.exists() && !file.isDirectory()) return false; String[] fileList = file.list(); if(fileList != null && fileList.length > 0) // Delete only empty folder. return false; // Try the normal way if (file.delete()) return true; // Try with Storage Access Framework. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { DocumentFile document = getDocumentFile(context, file, true, true); return document != null && document.delete(); } // Try the Kitkat workaround. if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { ContentResolver resolver = context.getContentResolver(); ContentValues values = new ContentValues(); values.put(MediaStore.MediaColumns.DATA, file.getAbsolutePath()); resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); // Delete the created entry, such that content provider will delete the file. resolver.delete(MediaStore.Files.getContentUri("external"), MediaStore.MediaColumns.DATA + "=?", new String[] {file.getAbsolutePath()}); } return !file.exists(); } /** * Get a DocumentFile corresponding to the given file (for writing on ExtSdCard on Android 5). If the file is not * existing, it is created. * * @param file The file. * @param isDirectory flag indicating if the file should be a directory. * @param createDirectories flag indicating if intermediate path directories should be created if not existing. * @return The DocumentFile */ private static DocumentFile getDocumentFile(Context context, @NonNull final File file, final boolean isDirectory, final boolean createDirectories) { Uri treeUri = getTreeUri(context); if (treeUri == null) return null; DocumentFile document = DocumentFile.fromTreeUri(context, treeUri); String sdcardPath = getSavedSdcardPath(context); String suffixPathPart = null; if (sdcardPath != null) { if((file.getPath().indexOf(sdcardPath)) != -1) suffixPathPart = file.getAbsolutePath().substring(sdcardPath.length()); } else { HashSet<File> storageRoots = ContentHelper.getStorageRoots(context); for(File root : storageRoots) { if (root != null) { if ((file.getPath().indexOf(root.getPath())) != -1) suffixPathPart = file.getAbsolutePath().substring(file.getPath().length()); } } } if (suffixPathPart == null) { Log.d(TAG, "unable to find the document file, filePath:"+ file.getPath()+ " root: " + ""+sdcardPath); return null; } if (suffixPathPart.startsWith(File.separator)) suffixPathPart = suffixPathPart.substring(1); String[] parts = suffixPathPart.split("/"); for (int i = 0; i < parts.length; i++) { // 3 is the DocumentFile tmp = document.findFile(parts[i]); if (tmp != null) document = document.findFile(parts[i]); else { if (i < parts.length - 1) { if (createDirectories) document = document.createDirectory(parts[i]); else return null; } else if (isDirectory) document = document.createDirectory(parts[i]); else return document.createFile("image", parts[i]); } } return document; } /** * Get the stored tree URIs. * * @return The tree URIs. * @param context context */ private static Uri getTreeUri(Context context) { String uriString = PreferenceUtil.getInstance(context).getString(context.getString(R.string.preference_internal_uri_extsdcard_photos), null); if (uriString == null) return null; return Uri.parse(uriString); } /** * Set a shared preference for an Uri. * * @param context context * @param uri the target value of the preference. */ public static void saveSdCardInfo(Context context, @Nullable final Uri uri) { SharedPreferences.Editor editor = PreferenceUtil.getInstance(context).getEditor(); editor.putString(context.getString(R.string.preference_internal_uri_extsdcard_photos), uri == null ? null : uri.toString()); editor.putString("sd_card_path", ContentHelper.getSdcardPath(context)); editor.commit(); } private static String getSavedSdcardPath(Context context) { return PreferenceUtil.getInstance(context).getString("sd_card_path", null); } public static String getMediaPath(final Context context, final Uri uri) { // DocumentProvider if (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 = "_id=?"; final String[] selectionArgs = new String[] { split[1] }; return getDataColumn(context, contentUri, selection, selectionArgs); } } else if ("downloads".equals(uri.getAuthority())) { //download for chrome-dev workaround String[] seg = uri.toString().split("/"); final String id = seg[seg.length - 1]; final Uri contentUri = ContentUris.withAppendedId( Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); return getDataColumn(context, contentUri, null, null); } // MediaStore (and general) else if ("content".equalsIgnoreCase(uri.getScheme())) { try { return getDataColumn(context, uri, null, null); } catch (Exception ignored){ } } // File else if ("file".equalsIgnoreCase(uri.getScheme())) { return uri.getPath(); } 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. */ private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { Cursor cursor = null; final String column = "_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.getColumnIndexOrThrow(column); 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. */ private 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. */ private 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. */ private static boolean isMediaDocument(Uri uri) { return "com.android.providers.media.documents".equals(uri.getAuthority()); } /** unused methods **/ //region old stuff /** * Determine the main folder of the external SD card containing the given file. * @param file the file. * @return The main folder of the external SD card containing this file, if the file is on an SD card. Otherwise, * null is returned. */ /*@TargetApi(Build.VERSION_CODES.KITKAT) private static String getExtSdCardFolder(Context context, @NonNull final File file) { String[] extSdPaths = getStorageRoots(context); try { for (String extSdPath : extSdPaths) if (file.getCanonicalPath().startsWith(extSdPath)) return extSdPath; } catch (IOException e) { return null; } return null; } *//** * Get the full path of a document from its tree URI. * * @param treeUri The tree RI. * @return The path (without trailing file separator). *//* @Nullable private static String getFullPathFromTreeUri(Context context, @Nullable final Uri treeUri) { if (treeUri == null) return null; String volumePath = getVolumePath(context, getVolumeIdFromTreeUri(treeUri)); if (volumePath == null) return File.separator; if (volumePath.endsWith(File.separator)) volumePath = volumePath.substring(0, volumePath.length() - 1); String documentPath = getDocumentPathFromTreeUri(treeUri); if (documentPath.endsWith(File.separator)) documentPath = documentPath.substring(0, documentPath.length() - 1); if (documentPath.length() > 0) { if (documentPath.startsWith(File.separator)) return volumePath + documentPath; else return volumePath + File.separator + documentPath; } else return volumePath; } *//** * Get the volume ID from the tree URI. * * @param treeUri The tree URI. * @return The volume ID. *//* @TargetApi(Build.VERSION_CODES.LOLLIPOP) private static String getVolumeIdFromTreeUri(final Uri treeUri) { final String docId = DocumentsContract.getTreeDocumentId(treeUri); final String[] split = docId.split(":"); if (split.length > 0) return split[0]; else return null; }*/ /* *//** * Get the document path (relative to volume name) for a tree URI (LOLLIPOP). * * @param treeUri The tree URI. * @return the document path. *//* @TargetApi(Build.VERSION_CODES.LOLLIPOP) private static String getDocumentPathFromTreeUri(final Uri treeUri) { final String docId = DocumentsContract.getTreeDocumentId(treeUri); final String[] split = docId.split(":"); if ((split.length >= 2) && (split[1] != null)) return split[1]; else return File.separator; } *//** * Get the path of a certain volume. * *//* private static String getVolumePath(Context context, final String volumeId) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return null; try { StorageManager mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); Class<?> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume"); Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList"); Method getUuid = storageVolumeClazz.getMethod("getUuid"); Method getPath = storageVolumeClazz.getMethod("getMediaPath"); Method isPrimary = storageVolumeClazz.getMethod("isPrimary"); Object result = getVolumeList.invoke(mStorageManager); final int length = java.lang.reflect.Array.getLength(result); for (int i = 0; i < length; i++) { Object storageVolumeElement = Array.getValue(result, i); String uuid = (String) getUuid.invoke(storageVolumeElement); Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement); // primary volume? if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) return (String) getPath.invoke(storageVolumeElement); // other volumes? if (uuid != null && uuid.equals(volumeId)) return (String) getPath.invoke(storageVolumeElement); } return null; } catch (Exception ex) { return null; } }*/ //endregion }