package com.npes87184.s2tdroid.donate.model; import android.annotation.TargetApi; import android.content.Context; import android.net.Uri; import android.os.Build; import android.os.storage.StorageManager; import android.provider.DocumentsContract; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.provider.DocumentFile; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.lang.reflect.Array; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; /** * Utility class for helping parsing file systems. * From jeisfeld/Augendiagnose */ public final class FileUtil { /** * The name of the primary volume (LOLLIPOP). */ private static final String PRIMARY_VOLUME_NAME = "primary"; private Context context; /** * Hide default constructor. */ public FileUtil(Context context) { this.context = context; } /** * Check is a file is writable. Detects write issues on external SD card. * * @param file The file * @return true if the file is writable. */ public 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 (FileNotFoundException e) { // in android, if file not found, it will create it auto. } boolean result = file.canWrite(); // Ensure that file is not created during this process. if (!isExisting) { //noinspection ResultOfMethodCallIgnored file.delete(); } return result; } /** * Check for a directory if it is possible to create files within this directory via normal writing * * @param folder The directory * @return true if it is possible to write in this directory. */ public static boolean isWritableNormal(@Nullable final File folder) { int i = 0; File file; do { String fileName = "AugendiagnoseDummyFile" + (++i); file = new File(folder, fileName); } while (file.exists()); return isWritable(file); } // Utility methods for Android 5 /** * Check for a directory if it is possible to create files within this directory, either via normal writing or via * Storage Access Framework. * * @param folder The directory * @return true if it is possible to write in this directory. */ public boolean isWritableNormalOrSaf(@Nullable final File folder, Uri uri) { // Find a non-existing file in this directory. int i = 0; File file; do { String fileName = "AugendiagnoseDummyFile" + (++i); file = new File(folder, fileName); } while (file.exists()); // First check regular writability if (isWritable(file)) { return true; } // Next check SAF writability. DocumentFile document = null; try { document = getDocumentFile(file, uri); } catch (Exception e) { return false; } if (document == null) { return false; } // This should have created the file - otherwise something is wrong with access URL. boolean result = document.canWrite() && file.exists(); // Ensure that the dummy file is not remaining. document.delete(); return result; } /** * Determine if a file is on external sd card. (Kitkat or higher.) * * @param file The file. * @return true if on external sd card. */ @TargetApi(Build.VERSION_CODES.KITKAT) public boolean isOnExtSdCard(@NonNull final File file) { return getExtSdCardFolder(file) != null; } /** * 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. * @return The DocumentFile */ public DocumentFile getDocumentFile(@NonNull final File file, Uri uri) { Uri treeUri = null; String fullPath; try { fullPath = file.getCanonicalPath(); } catch (IOException e) { return null; } String baseFolder = null; // First try to get the base folder via unofficial StorageVolume API from the URIs. String treeBase = getFullPathFromTreeUri(uri); if (treeBase != null && fullPath.startsWith(treeBase)) { treeUri = uri; baseFolder = treeBase; } if (baseFolder == null) { // Alternatively, take root folder from device and assume that base URI works. treeUri = uri; baseFolder = getExtSdCardFolder(file); } if (baseFolder == null) { return null; } String relativePath = fullPath.substring(baseFolder.length() + 1); // start with root of SD card and then parse through document tree. DocumentFile document = DocumentFile.fromTreeUri(context, treeUri); String[] parts = relativePath.split("\\/"); for (int i = 0; i < parts.length; i++) { DocumentFile nextDocument = document.findFile(parts[i]); if (nextDocument == null) { nextDocument = document.createFile("image", parts[i]); } document = nextDocument; } return document; } /** * 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 String getFullPathFromTreeUri(@Nullable final Uri treeUri) { if (treeUri == null) { return null; } String volumePath = getVolumePath(FileUtil.getVolumeIdFromTreeUri(treeUri)); if (volumePath == null) { return File.separator; } if (volumePath.endsWith(File.separator)) { volumePath = volumePath.substring(0, volumePath.length() - 1); } String documentPath = FileUtil.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. * * @param volumeId The volume id. * @return The path. */ private String getVolumePath(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("getPath"); Method isPrimary = storageVolumeClazz.getMethod("isPrimary"); Object result = getVolumeList.invoke(mStorageManager); final int length = Array.getLength(result); for (int i = 0; i < length; i++) { Object storageVolumeElement = Array.get(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) { if (uuid.equals(volumeId)) { return (String) getPath.invoke(storageVolumeElement); } } } // not found. return null; } catch (Exception ex) { return null; } } /** * 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) public String getExtSdCardFolder(@NonNull final File file) { String[] extSdPaths = getExtSdCardPaths(); try { for (String extSdPath : extSdPaths) { if (file.getCanonicalPath().startsWith(extSdPath)) { return extSdPath; } } } catch (IOException e) { return null; } return null; } /** * Get a list of external SD card paths. (Kitkat or higher.) * * @return A list of external SD card paths. */ @TargetApi(Build.VERSION_CODES.KITKAT) private String[] getExtSdCardPaths() { List<String> paths = new ArrayList<>(); for (File file : context.getExternalFilesDirs("external")) { if (file != null && !file.equals(context.getExternalFilesDir("external"))) { int index = file.getAbsolutePath().lastIndexOf("/Android/data"); if (index < 0) { // Unexpected external file dir } else { String path = file.getAbsolutePath().substring(0, index); try { path = new File(path).getCanonicalPath(); } catch (IOException e) { // Keep non-canonical path. } paths.add(path); } } } return paths.toArray(new String[paths.size()]); } }