| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.base; |
| |
| import android.content.Context; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.net.Uri; |
| import android.os.ParcelFileDescriptor; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| import org.jni_zero.JNINamespace; |
| import org.jni_zero.NativeMethods; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileDescriptor; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.function.Function; |
| |
| /** Helper methods for dealing with Files. */ |
| @JNINamespace("base::android") |
| public class FileUtils { |
| private static final String TAG = "FileUtils"; |
| |
| public static Function<String, Boolean> DELETE_ALL = filepath -> true; |
| |
| /** |
| * Delete the given File and (if it's a directory) everything within it. |
| * @param currentFile The file or directory to delete. Does not need to exist. |
| * @param canDelete the {@link Function} function used to check if the file can be deleted. |
| * @return True if the files are deleted, or files reserved by |canDelete|, false if failed to |
| * delete files. |
| * @note Caveat: Return values from recursive deletes are ignored. |
| * @note Caveat: |canDelete| is not robust; see https://crbug.com/1066733. |
| */ |
| public static boolean recursivelyDeleteFile( |
| File currentFile, Function<String, Boolean> canDelete) { |
| if (!currentFile.exists()) { |
| // This file could be a broken symlink, so try to delete. If we don't delete a broken |
| // symlink, the directory containing it cannot be deleted. |
| currentFile.delete(); |
| return true; |
| } |
| if (canDelete != null && !canDelete.apply(currentFile.getPath())) { |
| return true; |
| } |
| |
| if (currentFile.isDirectory()) { |
| File[] files = currentFile.listFiles(); |
| if (files != null) { |
| for (var file : files) { |
| recursivelyDeleteFile(file, canDelete); |
| } |
| } |
| } |
| |
| boolean ret = currentFile.delete(); |
| if (!ret) { |
| Log.e(TAG, "Failed to delete: %s", currentFile); |
| } |
| return ret; |
| } |
| |
| /** |
| * Delete the given files or directories by calling {@link #recursivelyDeleteFile(File)}. This |
| * supports deletion of content URIs. |
| * @param filePaths The file paths or content URIs to delete. |
| * @param canDelete the {@link Function} function used to check if the file can be deleted. |
| */ |
| public static void batchDeleteFiles( |
| List<String> filePaths, Function<String, Boolean> canDelete) { |
| for (String filePath : filePaths) { |
| if (canDelete != null && !canDelete.apply(filePath)) continue; |
| if (ContentUriUtils.isContentUri(filePath)) { |
| ContentUriUtils.delete(filePath); |
| } else { |
| File file = new File(filePath); |
| if (file.exists()) recursivelyDeleteFile(file, canDelete); |
| } |
| } |
| } |
| |
| /** |
| * Get file size. If it is a directory, recursively get the size of all files within it. |
| * @param file The file or directory. |
| * @return The size in bytes. |
| */ |
| public static long getFileSizeBytes(File file) { |
| if (file == null) return 0L; |
| if (file.isDirectory()) { |
| long size = 0L; |
| final File[] files = file.listFiles(); |
| if (files == null) { |
| return size; |
| } |
| for (File f : files) { |
| size += getFileSizeBytes(f); |
| } |
| return size; |
| } else { |
| return file.length(); |
| } |
| } |
| |
| /** Performs a simple copy of inputStream to outputStream. */ |
| public static void copyStream(InputStream inputStream, OutputStream outputStream) |
| throws IOException { |
| byte[] buffer = new byte[8192]; |
| int amountRead; |
| while ((amountRead = inputStream.read(buffer)) != -1) { |
| outputStream.write(buffer, 0, amountRead); |
| } |
| } |
| |
| /** |
| * Atomically copies the data from an input stream into an output file. |
| * @param is Input file stream to read data from. |
| * @param outFile Output file path. |
| * @throws IOException in case of I/O error. |
| */ |
| public static void copyStreamToFile(InputStream is, File outFile) throws IOException { |
| File tmpOutputFile = new File(outFile.getPath() + ".tmp"); |
| try (OutputStream os = new FileOutputStream(tmpOutputFile)) { |
| Log.i(TAG, "Writing to %s", outFile); |
| copyStream(is, os); |
| } |
| if (!tmpOutputFile.renameTo(outFile)) { |
| throw new IOException(); |
| } |
| } |
| |
| /** Reads inputStream into a byte array. */ |
| @NonNull |
| public static byte[] readStream(InputStream inputStream) throws IOException { |
| ByteArrayOutputStream data = new ByteArrayOutputStream(); |
| FileUtils.copyStream(inputStream, data); |
| return data.toByteArray(); |
| } |
| |
| /** |
| * Returns a URI that points at the file. |
| * @param file File to get a URI for. |
| * @return URI that points at that file, either as a content:// URI or a file:// URI. |
| */ |
| public static Uri getUriForFile(File file) { |
| // TODO(crbug/709584): Uncomment this when http://crbug.com/709584 has been fixed. |
| // assert !ThreadUtils.runningOnUiThread(); |
| Uri uri = null; |
| |
| try { |
| // Try to obtain a content:// URI, which is preferred to a file:// URI so that |
| // receiving apps don't attempt to determine the file's mime type (which often fails). |
| uri = ContentUriUtils.getContentUriFromFile(file); |
| } catch (IllegalArgumentException e) { |
| Log.e(TAG, "Could not create content uri: " + e); |
| } |
| |
| if (uri == null) uri = Uri.fromFile(file); |
| |
| return uri; |
| } |
| |
| /** |
| * Returns the file extension, or an empty string if none. |
| * @param file Name of the file, with or without the full path (Unix style). |
| * @return empty string if no extension, extension otherwise. |
| */ |
| public static String getExtension(String file) { |
| int lastSep = file.lastIndexOf('/'); |
| int lastDot = file.lastIndexOf('.'); |
| if (lastSep >= lastDot) return ""; // Subsumes |lastDot == -1|. |
| return file.substring(lastDot + 1).toLowerCase(Locale.US); |
| } |
| |
| /** Queries and decodes bitmap from content provider. */ |
| @Nullable |
| public static Bitmap queryBitmapFromContentProvider(Context context, Uri uri) { |
| try (ParcelFileDescriptor parcelFileDescriptor = |
| context.getContentResolver().openFileDescriptor(uri, "r")) { |
| if (parcelFileDescriptor == null) { |
| Log.w(TAG, "Null ParcelFileDescriptor from uri " + uri); |
| return null; |
| } |
| FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); |
| if (fileDescriptor == null) { |
| Log.w(TAG, "Null FileDescriptor from uri " + uri); |
| return null; |
| } |
| Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor); |
| if (bitmap == null) { |
| Log.w(TAG, "Failed to decode image from uri " + uri); |
| return null; |
| } |
| return bitmap; |
| } catch (IOException e) { |
| Log.w(TAG, "IO exception when reading uri " + uri); |
| } |
| return null; |
| } |
| |
| /** |
| * Gets the canonicalised absolute pathname for |filePath|. Returns empty string if the path is |
| * invalid. This function can result in I/O so it can be slow. |
| * @param filePath Path of the file, has to be a file path instead of a content URI. |
| * @return canonicalised absolute pathname for |filePath|. |
| */ |
| public static String getAbsoluteFilePath(String filePath) { |
| return FileUtilsJni.get().getAbsoluteFilePath(filePath); |
| } |
| |
| @NativeMethods |
| public interface Natives { |
| /** Returns the canonicalised absolute pathname for |filePath|. */ |
| String getAbsoluteFilePath(String filePath); |
| } |
| } |