| // Copyright 2012 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.content.pm.ApplicationInfo; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Environment; |
| import android.os.storage.StorageManager; |
| import android.provider.MediaStore; |
| import android.system.Os; |
| import android.text.TextUtils; |
| |
| import androidx.annotation.RequiresApi; |
| |
| import org.jni_zero.CalledByNative; |
| import org.jni_zero.JniType; |
| |
| import org.chromium.base.task.AsyncTask; |
| import org.chromium.build.annotations.NullMarked; |
| import org.chromium.build.annotations.Nullable; |
| import org.chromium.build.annotations.RequiresNonNull; |
| |
| import java.io.File; |
| import java.nio.file.Path; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.UUID; |
| import java.util.concurrent.FutureTask; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| |
| /** This class provides the path related methods for the native library. */ |
| @SuppressWarnings("NullAway") // Too hard to annotate this class. |
| @NullMarked |
| public abstract class PathUtils { |
| private static final String TAG = "PathUtils"; |
| private static final String THUMBNAIL_DIRECTORY_NAME = "textures"; |
| |
| private static final int DATA_DIRECTORY = 0; |
| private static final int THUMBNAIL_DIRECTORY = 1; |
| private static final int CACHE_DIRECTORY = 2; |
| private static final int NUM_DIRECTORIES = 3; |
| private static final AtomicBoolean sInitializationStarted = new AtomicBoolean(); |
| private static @Nullable FutureTask<String[]> sDirPathFetchTask; |
| |
| // If the FutureTask started in setPrivateDataDirectorySuffix() fails to complete by the time we |
| // need the values, we will need the suffix so that we can restart the task synchronously on |
| // the UI thread. |
| private static @Nullable String sDataDirectorySuffix; |
| private static @Nullable String sCacheSubDirectory; |
| private static @Nullable String sDataDirectoryBasePath; |
| private static @Nullable String sCacheDirectoryBasePath; |
| |
| // Prevent instantiation. |
| private PathUtils() {} |
| |
| // Resetting is useful in Robolectric tests, where each test is run with a different |
| // data directory. |
| public static void resetForTesting() { |
| sInitializationStarted.set(false); |
| sDirPathFetchTask = null; |
| sDataDirectorySuffix = null; |
| sCacheSubDirectory = null; |
| sDataDirectoryBasePath = null; |
| sCacheDirectoryBasePath = null; |
| } |
| |
| /** |
| * Get the directory paths from sDirPathFetchTask if available, or compute it synchronously |
| * on the UI thread otherwise. This should only be called as part of Holder's initialization |
| * above to guarantee thread-safety as part of the initialization-on-demand holder idiom. |
| */ |
| private static String[] getOrComputeDirectoryPaths() { |
| assert sDirPathFetchTask != null : "setDataDirectorySuffix must be called first."; |
| if (!sDirPathFetchTask.isDone()) { |
| try (StrictModeContext ignored = StrictModeContext.allowDiskWrites()) { |
| // No-op if already ran. |
| sDirPathFetchTask.run(); |
| } |
| } |
| try { |
| return sDirPathFetchTask.get(); |
| } catch (Exception e) { |
| throw JavaUtils.throwUnchecked(e); |
| } |
| } |
| |
| private static void chmod(String path, int mode) { |
| try { |
| Os.chmod(path, mode); |
| } catch (Exception e) { |
| Log.e(TAG, "Failed to set permissions for path \"" + path + "\""); |
| } |
| } |
| |
| // TODO(crbug.com/41484704): Merge the Chrome and WebView implementations |
| // of isPathUnderAppDir into one. |
| public static boolean isPathUnderAppDir(String path, Context context) { |
| File file = new File(path); |
| File dataDir = context.getDataDir(); |
| File externalDir = ContextUtils.getApplicationContext().getExternalFilesDir(null); |
| try { |
| Path fileRealPath = file.toPath().toRealPath(); |
| return (fileRealPath.startsWith(dataDir.toPath().toRealPath()) |
| || (externalDir != null |
| && fileRealPath.startsWith(externalDir.toPath().toRealPath()))); |
| } catch (Exception e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Fetch the path of the directory where private data is to be stored by the application. This |
| * is meant to be called in an FutureTask in setPrivateDataDirectorySuffix(), but if we need the |
| * result before the FutureTask has had a chance to finish, then it's best to cancel the task |
| * and run it on the UI thread instead, inside getOrComputeDirectoryPaths(). |
| * |
| * @see Context#getDir(String, int) |
| */ |
| @RequiresNonNull("sDataDirectorySuffix") |
| private static String[] setPrivateDirectoryPathInternal() { |
| String[] paths = new String[NUM_DIRECTORIES]; |
| File dataDir = null; |
| File thumbnailDir = null; |
| Context appContext = ContextUtils.getApplicationContext(); |
| if (sDataDirectoryBasePath == null) { |
| dataDir = appContext.getDir(sDataDirectorySuffix, Context.MODE_PRIVATE); |
| thumbnailDir = appContext.getDir(THUMBNAIL_DIRECTORY_NAME, Context.MODE_PRIVATE); |
| } else { |
| dataDir = new File(sDataDirectoryBasePath, sDataDirectorySuffix); |
| dataDir.mkdirs(); |
| thumbnailDir = new File(sDataDirectoryBasePath, THUMBNAIL_DIRECTORY_NAME); |
| thumbnailDir.mkdirs(); |
| } |
| |
| File cacheDir = null; |
| if (sCacheDirectoryBasePath != null) { |
| cacheDir = new File(sCacheDirectoryBasePath); |
| } else { |
| cacheDir = appContext.getCacheDir(); |
| } |
| if (cacheDir != null) { |
| if (sCacheSubDirectory != null) { |
| cacheDir = new File(cacheDir, sCacheSubDirectory); |
| } |
| if (sCacheDirectoryBasePath != null || sCacheSubDirectory != null) { |
| cacheDir.mkdirs(); |
| // Set to rwx--S--- as the Android cache dir has a distinct gid and is setgid. |
| chmod(cacheDir.getPath(), 02700); |
| } |
| paths[CACHE_DIRECTORY] = cacheDir.getPath(); |
| } |
| paths[DATA_DIRECTORY] = dataDir.getPath(); |
| // MODE_PRIVATE results in rwxrwx--x, but we want rwx------, as a defence-in-depth measure. |
| chmod(paths[DATA_DIRECTORY], 0700); |
| paths[THUMBNAIL_DIRECTORY] = thumbnailDir.getPath(); |
| return paths; |
| } |
| |
| /** |
| * Starts an asynchronous task to fetch the path of the directory where private data is to be |
| * stored by the application. |
| * |
| * <p>This task can run long (or more likely be delayed in a large task queue), in which case we |
| * want to cancel it and run on the UI thread instead. Unfortunately, this means keeping a bit |
| * of extra static state - we need to store the suffix and the application context in case we |
| * need to try to re-execute later. |
| * |
| * @param dataBasePath The base path for the data directory. If null, defaults to using Android |
| * Platform specific app data directory. |
| * @param cacheBasePath The base path for the cache directory. If null, defaults to using |
| * Android Platform specific app cache directory. |
| * @param dataDirSuffix The private data directory suffix. |
| * @param cacheSubDir The subdirectory in the cache directory to use, if non-null. |
| * @see Context#getDir(String, int) |
| */ |
| public static void setPrivateDirectoryPath( |
| @Nullable String dataBasePath, |
| @Nullable String cacheBasePath, |
| String dataDirSuffix, |
| @Nullable String cacheSubDir) { |
| // This method should only be called once, but many tests end up calling it multiple times, |
| // so adding a guard here. |
| if (!sInitializationStarted.getAndSet(true)) { |
| assert ContextUtils.getApplicationContext() != null; |
| sDataDirectoryBasePath = dataBasePath; |
| sCacheDirectoryBasePath = cacheBasePath; |
| sDataDirectorySuffix = dataDirSuffix; |
| sCacheSubDirectory = cacheSubDir; |
| |
| // We don't use an AsyncTask because this function is called in early Webview startup |
| // and it won't always have a UI thread available. Thus, we can't use AsyncTask which |
| // inherently posts to the UI thread for onPostExecute(). |
| sDirPathFetchTask = new FutureTask<>(PathUtils::setPrivateDirectoryPathInternal); |
| AsyncTask.THREAD_POOL_EXECUTOR.execute(sDirPathFetchTask); |
| } else { |
| assert TextUtils.equals(sDataDirectoryBasePath, dataBasePath) |
| : String.format("%s != %s", dataBasePath, sDataDirectoryBasePath); |
| assert TextUtils.equals(sCacheDirectoryBasePath, cacheBasePath) |
| : String.format("%s != %s", cacheBasePath, sCacheDirectoryBasePath); |
| assert TextUtils.equals(sDataDirectorySuffix, dataDirSuffix) |
| : String.format("%s != %s", dataDirSuffix, sDataDirectorySuffix); |
| assert TextUtils.equals(sCacheSubDirectory, cacheSubDir) |
| : String.format("%s != %s", cacheSubDir, sCacheSubDirectory); |
| } |
| } |
| |
| /** |
| * Starts an asynchronous task to fetch the path of the directory where private data is to be |
| * stored by the application. |
| * |
| * <p>This task can run long (or more likely be delayed in a large task queue), in which case we |
| * want to cancel it and run on the UI thread instead. Unfortunately, this means keeping a bit |
| * of extra static state - we need to store the suffix and the application context in case we |
| * need to try to re-execute later. |
| * |
| * @param suffix The private data directory suffix. |
| * @param cacheSubDir The subdirectory in the cache directory to use, if non-null. |
| * @see Context#getDir(String, int) |
| */ |
| public static void setPrivateDataDirectorySuffix(String suffix, @Nullable String cacheSubDir) { |
| setPrivateDirectoryPath(null, null, suffix, cacheSubDir); |
| } |
| |
| public static void setPrivateDataDirectorySuffix(String suffix) { |
| setPrivateDataDirectorySuffix(suffix, null); |
| } |
| |
| /** |
| * @param index The index of the cached directory path. |
| * @return The directory path requested. |
| */ |
| private static String getDirectoryPath(int index) { |
| String[] paths = getOrComputeDirectoryPaths(); |
| return paths[index]; |
| } |
| |
| /** |
| * @return the private directory that is used to store application data. |
| */ |
| @CalledByNative |
| public static @JniType("std::string") String getDataDirectory() { |
| return getDirectoryPath(DATA_DIRECTORY); |
| } |
| |
| /** |
| * @return the cache directory. |
| */ |
| @CalledByNative |
| public static @JniType("std::string") String getCacheDirectory() { |
| return getDirectoryPath(CACHE_DIRECTORY); |
| } |
| |
| // Should not be called from WebView, since it does not support being used in a multiprocess |
| // environment. |
| @CalledByNative |
| public static @JniType("std::string") String getThumbnailCacheDirectory() { |
| return getDirectoryPath(THUMBNAIL_DIRECTORY); |
| } |
| |
| private static String sDownloadsDirectoryForTesting; |
| private static String[] sAllPrivateDownloadsDirectoriesForTesting; |
| private static String[] sExternalDownloadVolumesNamesForTesting; |
| |
| public static void setDownloadsDirectoryForTesting(String downloadsDirectory) { |
| sDownloadsDirectoryForTesting = downloadsDirectory; |
| ResettersForTesting.register(() -> sDownloadsDirectoryForTesting = null); |
| } |
| |
| public static void setAllPrivateDownloadsDirectoriesForTesting( |
| String[] allPrivateDownloadsDirectories) { |
| sAllPrivateDownloadsDirectoriesForTesting = allPrivateDownloadsDirectories; |
| ResettersForTesting.register(() -> sAllPrivateDownloadsDirectoriesForTesting = null); |
| } |
| |
| public static void setExternalDownloadVolumesNamesForTesting(String[] externalDownloadVolumes) { |
| sExternalDownloadVolumesNamesForTesting = externalDownloadVolumes; |
| ResettersForTesting.register(() -> sExternalDownloadVolumesNamesForTesting = null); |
| } |
| |
| /** |
| * Returns the downloads directory. Before Android Q, this returns the public download directory |
| * for Chrome app. On Q+, this returns the first private download directory for the app, since Q |
| * will block public directory access. May return empty string when there are no external |
| * storage volumes mounted. |
| */ |
| @SuppressWarnings("unused") |
| @CalledByNative |
| public static @JniType("std::string") String getDownloadsDirectory() { |
| if (sDownloadsDirectoryForTesting != null) return sDownloadsDirectoryForTesting; |
| // TODO(crbug.com/41187555): Move calls to getDownloadsDirectory() to background thread. |
| try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) { |
| // https://developer.android.com/preview/privacy/scoped-storage |
| String[] dirs = getAllPrivateDownloadsDirectories(); |
| assert dirs != null; |
| return dirs.length == 0 ? "" : dirs[0]; |
| } |
| } |
| |
| /** |
| * @return Download directories including the default storage directory on SD card, and a |
| * private directory on external SD card. |
| */ |
| @SuppressWarnings("unused") |
| @CalledByNative |
| public static String[] getAllPrivateDownloadsDirectories() { |
| if (sAllPrivateDownloadsDirectoriesForTesting != null) { |
| return sAllPrivateDownloadsDirectoriesForTesting; |
| } |
| List<File> files = new ArrayList<>(); |
| try (StrictModeContext ignored = StrictModeContext.allowDiskWrites()) { |
| File[] externalDirs = |
| ContextUtils.getApplicationContext() |
| .getExternalFilesDirs(Environment.DIRECTORY_DOWNLOADS); |
| files = (externalDirs == null) ? files : Arrays.asList(externalDirs); |
| } |
| return toAbsolutePathStrings(files); |
| } |
| |
| /** |
| * @return The download directory for secondary storage on Q+, returned by {@link |
| * MediaStore#getExternalVolumeNames(Context)}. Notices on Android R, apps can no longer |
| * expose app's private directory for secondary storage. Apps should put files to |
| * /storage/$volume_id/Download/ directory instead. |
| */ |
| @RequiresApi(Build.VERSION_CODES.R) |
| @CalledByNative |
| public static String[] getExternalDownloadVolumesNames() { |
| if (sExternalDownloadVolumesNamesForTesting != null) { |
| return sExternalDownloadVolumesNamesForTesting; |
| } |
| ArrayList<File> files = new ArrayList<>(); |
| Set<String> volumes = |
| MediaStore.getExternalVolumeNames(ContextUtils.getApplicationContext()); |
| for (String vol : volumes) { |
| if (!TextUtils.isEmpty(vol) && !vol.contains(MediaStore.VOLUME_EXTERNAL_PRIMARY)) { |
| StorageManager manager = |
| ContextUtils.getApplicationContext().getSystemService(StorageManager.class); |
| Uri uri = MediaStore.Files.getContentUri(vol); |
| try { |
| File volumeDir = manager.getStorageVolume(uri).getDirectory(); |
| File volumeDownloadDir = new File(volumeDir, Environment.DIRECTORY_DOWNLOADS); |
| // Happens in rare case when Android doesn't create the download directory for |
| // this volume. |
| if (!volumeDownloadDir.isDirectory()) { |
| Log.w( |
| TAG, |
| "Download dir missing: %s, parent dir:%s, isDirectory:%s", |
| volumeDownloadDir.getAbsolutePath(), |
| volumeDir.getAbsolutePath(), |
| volumeDir.isDirectory()); |
| } |
| files.add(volumeDownloadDir); |
| } catch (Exception e) { |
| Log.e(TAG, "Failed to get storage volume for uri: " + uri, e); |
| } |
| } |
| } |
| |
| return toAbsolutePathStrings(files); |
| } |
| |
| /** |
| * @return The cache quota allocated to the app by the Android framework in bytes. If the app |
| * uses more cache than its quota, the app is at a higher risk of having its cache entries |
| * evicted. Return value of -1 depicts an error. |
| */ |
| @CalledByNative |
| public static long getCacheQuotaBytes() { |
| try { |
| StorageManager storageManager = |
| ContextUtils.getApplicationContext().getSystemService(StorageManager.class); |
| UUID storageUuid = storageManager.getUuidForPath(new File(getCacheDirectory())); |
| // This can throw `SecurityException` if the app doesn't have sufficient privileges. |
| // See crbug.com/422174715 |
| return storageManager.getCacheQuotaBytes(storageUuid); |
| } catch (Exception e) { |
| return -1; |
| } |
| } |
| |
| private static String[] toAbsolutePathStrings(List<File> files) { |
| ArrayList<String> absolutePaths = new ArrayList<String>(); |
| for (File file : files) { |
| if (file == null || TextUtils.isEmpty(file.getAbsolutePath())) continue; |
| absolutePaths.add(file.getAbsolutePath()); |
| } |
| |
| return absolutePaths.toArray(new String[absolutePaths.size()]); |
| } |
| |
| /** |
| * @return the path to native libraries. |
| */ |
| @SuppressWarnings("unused") |
| @CalledByNative |
| private static @JniType("std::string") String getNativeLibraryDirectory() { |
| ApplicationInfo ai = ContextUtils.getApplicationContext().getApplicationInfo(); |
| if ((ai.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0 |
| || (ai.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { |
| return ai.nativeLibraryDir; |
| } |
| |
| return "/system/lib/"; |
| } |
| |
| /** |
| * @return the external storage directory. |
| */ |
| @SuppressWarnings("unused") |
| @CalledByNative |
| public static @JniType("std::string") String getExternalStorageDirectory() { |
| return Environment.getExternalStorageDirectory().getAbsolutePath(); |
| } |
| } |