• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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