package me.devsaki.hentoid.util; import android.content.Context; import android.net.Uri; import android.os.Build; import android.support.annotation.NonNull; import android.support.v4.provider.DocumentFile; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.channels.FileChannel; import me.devsaki.hentoid.HentoidApp; /** * Created by avluis on 08/25/2016. * Methods for use by FileHelper */ class FileUtil { private static final String TAG = LogHelper.makeLogTag(FileUtil.class); private static final int LOLLIPOP = Build.VERSION_CODES.LOLLIPOP; /** * Method ensures file creation from stream. * * @param stream - FileOutputStream. * @return true if all OK. */ static boolean sync(@NonNull final FileOutputStream stream) { try { stream.getFD().sync(); return true; } catch (IOException e) { LogHelper.e(TAG, e, "IO Error"); } return false; } /** * Get a DocumentFile corresponding to the given file. * If the file does not exist, it is created. * * @param file The file. * @param isDirectory flag indicating if the file should be a directory. * @return The DocumentFile. */ private static DocumentFile getDocumentFile(final File file, final boolean isDirectory) { String baseFolder = FileHelper.getExtSdCardFolder(file); boolean originalDirectory = false; if (baseFolder == null) { return null; } String relativePath = null; try { String fullPath = file.getCanonicalPath(); if (!baseFolder.equals(fullPath)) { relativePath = fullPath.substring(baseFolder.length() + 1); } else { originalDirectory = true; } } catch (IOException e) { return null; } catch (Exception f) { originalDirectory = true; //continue } String as = FileHelper.getStringUri(); Uri treeUri = null; if (as != null) { treeUri = Uri.parse(as); } if (treeUri == null) { return null; } return documentFileHelper(treeUri, originalDirectory, relativePath, isDirectory); } private static DocumentFile documentFileHelper(Uri treeUri, boolean originalDirectory, String relativePath, boolean isDirectory) { // start with root of SD card and then parse through document tree. Context cxt = HentoidApp.getAppContext(); DocumentFile document = DocumentFile.fromTreeUri(cxt, treeUri); if (originalDirectory) { return document; } String[] parts = relativePath.split("/"); for (int i = 0; i < parts.length; i++) { DocumentFile nextDocument = document.findFile(parts[i]); if (nextDocument == null) { if ((i < parts.length - 1) || isDirectory) { nextDocument = document.createDirectory(parts[i]); } else { nextDocument = document.createFile("image", parts[i]); } } document = nextDocument; } return document; } /** * Get OutputStream from file. * * @param target The file. * @return FileOutputStream. */ static OutputStream getOutputStream(@NonNull final File target) { OutputStream outStream = null; try { // First try the normal way if (FileHelper.isWritable(target)) { // standard way outStream = new FileOutputStream(target); } else { if (Helper.isAtLeastAPI(LOLLIPOP)) { // Storage Access Framework DocumentFile targetDocument = getDocumentFile(target, false); if (targetDocument != null) { Context cxt = HentoidApp.getAppContext(); outStream = cxt.getContentResolver().openOutputStream( targetDocument.getUri()); } } } } catch (Exception e) { LogHelper.e(TAG, e, "Error while attempting to get file: " + target.getAbsolutePath()); } return outStream; } /** * Create a file. * * @param file The file to be created. * @return true if creation was successful. */ @SuppressWarnings("RedundantThrows") static boolean makeFile(@NonNull final File file) throws IOException { if (file.exists()) { // nothing to create. return !file.isDirectory(); } // Try the normal way try { if (file.createNewFile()) { return true; } } catch (IOException e) { // Fail silently } // Try with Storage Access Framework. if (Helper.isAtLeastAPI(LOLLIPOP)) { DocumentFile document = getDocumentFile(file.getParentFile(), true); // getDocumentFile implicitly creates the directory. try { if (document != null) { return document.createFile( MimeTypes.getMimeType(file), file.getName()) != null; } } catch (Exception e) { return false; } } return false; } /** * Create a folder. * * @param file The folder to be created. * @return true if creation was successful. */ static boolean makeDir(@NonNull final File file) { if (file.exists()) { // nothing to create. return file.isDirectory(); } // Try the normal way if (file.mkdirs()) { return true; } // Try with Storage Access Framework. if (Helper.isAtLeastAPI(LOLLIPOP)) { DocumentFile document = getDocumentFile(file, true); // getDocumentFile implicitly creates the directory. if (document != null) { return document.exists(); } } return false; } /** * Delete a file. * * @param file The file to be deleted. * @return true if successfully deleted. */ static boolean deleteFile(@NonNull final File file) { // First try the normal deletion boolean fileDelete = rmFile(file); if (file.delete() || fileDelete) { return true; } // Try with Storage Access Framework if (Helper.isAtLeastAPI(LOLLIPOP)) { DocumentFile document = getDocumentFile(file, false); if (document != null) { return document.delete(); } } return !file.exists(); } private static boolean rmFile(@NonNull final File folder) { boolean totalSuccess = true; if (folder.isDirectory()) { for (File child : folder.listFiles()) { rmFile(child); } if (!folder.delete()) { totalSuccess = false; } } else { if (!folder.delete()) { totalSuccess = false; } } return totalSuccess; } /** * Delete a folder. * * @param folder The folder. * @return true if successful. */ static boolean deleteDir(@NonNull final File folder) { if (!folder.exists()) { return true; } if (!folder.isDirectory()) { return false; } String[] fileList = folder.list(); if (fileList != null && fileList.length > 0) { // empty the folder. rmDir(folder); } String[] fileList1 = folder.list(); if (fileList1 != null && fileList1.length > 0) { // Delete only empty folder. return false; } // Try the normal way if (folder.delete()) { return true; } // Try with Storage Access Framework. if (Helper.isAtLeastAPI(LOLLIPOP)) { DocumentFile document = getDocumentFile(folder, true); if (document != null) { return document.delete(); } } return !folder.exists(); } private static boolean rmDir(@NonNull final File folder) { for (File dir : folder.listFiles()) { if (dir.isDirectory()) { if (!rmDir(dir)) { return false; } } else { if (!deleteFile(dir)) { return false; } } } return true; } /** * Copy a file. * * @param source The source file. * @param target The target file. * @return true if copying was successful. */ static boolean copyFile(final File source, final File target) { final int BUFFER = 10 * 1024; FileInputStream inStream = null; OutputStream outStream = null; FileChannel inChannel = null; FileChannel outChannel = null; try { inStream = new FileInputStream(source); // First try the normal way if (FileHelper.isWritable(target)) { // standard way outStream = new FileOutputStream(target); inChannel = inStream.getChannel(); outChannel = ((FileOutputStream) outStream).getChannel(); inChannel.transferTo(0, inChannel.size(), outChannel); } else { if (Helper.isAtLeastAPI(LOLLIPOP)) { // Storage Access Framework DocumentFile targetDocument = getDocumentFile(target, false); if (targetDocument != null) { Context cxt = HentoidApp.getAppContext(); outStream = cxt.getContentResolver().openOutputStream( targetDocument.getUri()); } } else { return false; } if (outStream != null) { // write to output stream byte[] buffer = new byte[BUFFER]; int bytesRead; while ((bytesRead = inStream.read(buffer)) != -1) { outStream.write(buffer, 0, bytesRead); } } } } catch (Exception e) { LogHelper.e(TAG, e, "Error while copying file from " + source.getAbsolutePath() + " to " + target.getAbsolutePath()); return false; } finally { try { if (inStream != null) { inStream.close(); } if (outStream != null) { outStream.close(); } if (inChannel != null) { inChannel.close(); } if (outChannel != null) { outChannel.close(); } } catch (Exception e) { // ignore exception } } return true; } /** * Rename a folder. * * @param source The source folder. * @param target The target folder. * @return true if the renaming was successful. */ static boolean renameDir(@NonNull final File source, @NonNull final File target) { // First try the normal rename. if (rename(source, target.getName())) { return true; } if (target.exists()) { return false; } // Try the Storage Access Framework if it is just a rename within the same parent folder. if (Helper.isAtLeastAPI(LOLLIPOP) && source.getParent().equals(target.getParent())) { DocumentFile document = getDocumentFile(source, true); if (document != null && document.renameTo(target.getName())) { return true; } } // Try the manual way, moving files individually. if (!makeDir(target)) { return false; } File[] sourceFiles = source.listFiles(); if (sourceFiles == null) { return true; } for (File sourceFile : sourceFiles) { String fileName = sourceFile.getName(); File targetFile = new File(target, fileName); if (!copyFile(sourceFile, targetFile)) { // stop on first error return false; } } // Only after successfully copying all files, delete files on source folder. for (File sourceFile : sourceFiles) { if (!deleteFile(sourceFile)) { // stop on first error return false; } } return true; } private static boolean rename(File file, String name) { String newName = file.getParent() + "/" + name; return !file.getParentFile().canWrite() || file.renameTo(new File(newName)); } /** * Move a file. * * @param source The source file. * @param target The target file. * @return true if the copying was successful. */ static boolean moveFile(@NonNull final File source, @NonNull final File target) { // First try the normal rename if (source.renameTo(target)) { return true; } boolean success = copyFile(source, target); if (success) { success = deleteFile(source); } return success; } }