1 // Copyright 2015 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.base; 6 7 import android.content.Context; 8 import android.graphics.Bitmap; 9 import android.graphics.BitmapFactory; 10 import android.net.Uri; 11 import android.os.ParcelFileDescriptor; 12 13 import androidx.annotation.NonNull; 14 import androidx.annotation.Nullable; 15 16 import org.jni_zero.JNINamespace; 17 import org.jni_zero.NativeMethods; 18 19 import java.io.ByteArrayOutputStream; 20 import java.io.File; 21 import java.io.FileDescriptor; 22 import java.io.FileOutputStream; 23 import java.io.IOException; 24 import java.io.InputStream; 25 import java.io.OutputStream; 26 import java.util.List; 27 import java.util.Locale; 28 import java.util.function.Function; 29 30 /** Helper methods for dealing with Files. */ 31 @JNINamespace("base::android") 32 public class FileUtils { 33 private static final String TAG = "FileUtils"; 34 35 public static Function<String, Boolean> DELETE_ALL = filepath -> true; 36 37 /** 38 * Delete the given File and (if it's a directory) everything within it. 39 * @param currentFile The file or directory to delete. Does not need to exist. 40 * @param canDelete the {@link Function} function used to check if the file can be deleted. 41 * @return True if the files are deleted, or files reserved by |canDelete|, false if failed to 42 * delete files. 43 * @note Caveat: Return values from recursive deletes are ignored. 44 * @note Caveat: |canDelete| is not robust; see https://crbug.com/1066733. 45 */ recursivelyDeleteFile( File currentFile, Function<String, Boolean> canDelete)46 public static boolean recursivelyDeleteFile( 47 File currentFile, Function<String, Boolean> canDelete) { 48 if (!currentFile.exists()) { 49 // This file could be a broken symlink, so try to delete. If we don't delete a broken 50 // symlink, the directory containing it cannot be deleted. 51 currentFile.delete(); 52 return true; 53 } 54 if (canDelete != null && !canDelete.apply(currentFile.getPath())) { 55 return true; 56 } 57 58 if (currentFile.isDirectory()) { 59 File[] files = currentFile.listFiles(); 60 if (files != null) { 61 for (var file : files) { 62 recursivelyDeleteFile(file, canDelete); 63 } 64 } 65 } 66 67 boolean ret = currentFile.delete(); 68 if (!ret) { 69 Log.e(TAG, "Failed to delete: %s", currentFile); 70 } 71 return ret; 72 } 73 74 /** 75 * Delete the given files or directories by calling {@link #recursivelyDeleteFile(File)}. This 76 * supports deletion of content URIs. 77 * @param filePaths The file paths or content URIs to delete. 78 * @param canDelete the {@link Function} function used to check if the file can be deleted. 79 */ batchDeleteFiles( List<String> filePaths, Function<String, Boolean> canDelete)80 public static void batchDeleteFiles( 81 List<String> filePaths, Function<String, Boolean> canDelete) { 82 for (String filePath : filePaths) { 83 if (canDelete != null && !canDelete.apply(filePath)) continue; 84 if (ContentUriUtils.isContentUri(filePath)) { 85 ContentUriUtils.delete(filePath); 86 } else { 87 File file = new File(filePath); 88 if (file.exists()) recursivelyDeleteFile(file, canDelete); 89 } 90 } 91 } 92 93 /** 94 * Get file size. If it is a directory, recursively get the size of all files within it. 95 * @param file The file or directory. 96 * @return The size in bytes. 97 */ getFileSizeBytes(File file)98 public static long getFileSizeBytes(File file) { 99 if (file == null) return 0L; 100 if (file.isDirectory()) { 101 long size = 0L; 102 final File[] files = file.listFiles(); 103 if (files == null) { 104 return size; 105 } 106 for (File f : files) { 107 size += getFileSizeBytes(f); 108 } 109 return size; 110 } else { 111 return file.length(); 112 } 113 } 114 115 /** Performs a simple copy of inputStream to outputStream. */ copyStream(InputStream inputStream, OutputStream outputStream)116 public static void copyStream(InputStream inputStream, OutputStream outputStream) 117 throws IOException { 118 byte[] buffer = new byte[8192]; 119 int amountRead; 120 while ((amountRead = inputStream.read(buffer)) != -1) { 121 outputStream.write(buffer, 0, amountRead); 122 } 123 } 124 125 /** 126 * Atomically copies the data from an input stream into an output file. 127 * @param is Input file stream to read data from. 128 * @param outFile Output file path. 129 * @throws IOException in case of I/O error. 130 */ copyStreamToFile(InputStream is, File outFile)131 public static void copyStreamToFile(InputStream is, File outFile) throws IOException { 132 File tmpOutputFile = new File(outFile.getPath() + ".tmp"); 133 try (OutputStream os = new FileOutputStream(tmpOutputFile)) { 134 Log.i(TAG, "Writing to %s", outFile); 135 copyStream(is, os); 136 } 137 if (!tmpOutputFile.renameTo(outFile)) { 138 throw new IOException(); 139 } 140 } 141 142 /** Reads inputStream into a byte array. */ 143 @NonNull readStream(InputStream inputStream)144 public static byte[] readStream(InputStream inputStream) throws IOException { 145 ByteArrayOutputStream data = new ByteArrayOutputStream(); 146 FileUtils.copyStream(inputStream, data); 147 return data.toByteArray(); 148 } 149 150 /** 151 * Returns a URI that points at the file. 152 * @param file File to get a URI for. 153 * @return URI that points at that file, either as a content:// URI or a file:// URI. 154 */ getUriForFile(File file)155 public static Uri getUriForFile(File file) { 156 // TODO(crbug/709584): Uncomment this when http://crbug.com/709584 has been fixed. 157 // assert !ThreadUtils.runningOnUiThread(); 158 Uri uri = null; 159 160 try { 161 // Try to obtain a content:// URI, which is preferred to a file:// URI so that 162 // receiving apps don't attempt to determine the file's mime type (which often fails). 163 uri = ContentUriUtils.getContentUriFromFile(file); 164 } catch (IllegalArgumentException e) { 165 Log.e(TAG, "Could not create content uri: " + e); 166 } 167 168 if (uri == null) uri = Uri.fromFile(file); 169 170 return uri; 171 } 172 173 /** 174 * Returns the file extension, or an empty string if none. 175 * @param file Name of the file, with or without the full path (Unix style). 176 * @return empty string if no extension, extension otherwise. 177 */ getExtension(String file)178 public static String getExtension(String file) { 179 int lastSep = file.lastIndexOf('/'); 180 int lastDot = file.lastIndexOf('.'); 181 if (lastSep >= lastDot) return ""; // Subsumes |lastDot == -1|. 182 return file.substring(lastDot + 1).toLowerCase(Locale.US); 183 } 184 185 /** Queries and decodes bitmap from content provider. */ 186 @Nullable queryBitmapFromContentProvider(Context context, Uri uri)187 public static Bitmap queryBitmapFromContentProvider(Context context, Uri uri) { 188 try (ParcelFileDescriptor parcelFileDescriptor = 189 context.getContentResolver().openFileDescriptor(uri, "r")) { 190 if (parcelFileDescriptor == null) { 191 Log.w(TAG, "Null ParcelFileDescriptor from uri " + uri); 192 return null; 193 } 194 FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); 195 if (fileDescriptor == null) { 196 Log.w(TAG, "Null FileDescriptor from uri " + uri); 197 return null; 198 } 199 Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor); 200 if (bitmap == null) { 201 Log.w(TAG, "Failed to decode image from uri " + uri); 202 return null; 203 } 204 return bitmap; 205 } catch (IOException e) { 206 Log.w(TAG, "IO exception when reading uri " + uri); 207 } 208 return null; 209 } 210 211 /** 212 * Gets the canonicalised absolute pathname for |filePath|. Returns empty string if the path is 213 * invalid. This function can result in I/O so it can be slow. 214 * @param filePath Path of the file, has to be a file path instead of a content URI. 215 * @return canonicalised absolute pathname for |filePath|. 216 */ getAbsoluteFilePath(String filePath)217 public static String getAbsoluteFilePath(String filePath) { 218 return FileUtilsJni.get().getAbsoluteFilePath(filePath); 219 } 220 221 @NativeMethods 222 public interface Natives { 223 /** Returns the canonicalised absolute pathname for |filePath|. */ getAbsoluteFilePath(String filePath)224 String getAbsoluteFilePath(String filePath); 225 } 226 } 227