blob: a07274f9fbd724c711141e7fbdede7cc6b190b02 [file] [log] [blame]
// 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);
}
}