blob: d09d6bf78de4878ff1123e73022b0a558fddcc40 [file] [log] [blame]
// Copyright 2012 The Chromium Authors. All rights reserved.
// 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.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.os.Environment;
import android.os.SystemClock;
import android.system.Os;
import android.text.TextUtils;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.MainDex;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.AsyncTask;
import java.io.File;
import java.util.ArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* This class provides the path related methods for the native library.
*/
@MainDex
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 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 String sDataDirectorySuffix;
private static String sCacheSubDirectory;
// Prevent instantiation.
private PathUtils() {}
/**
* Initialization-on-demand holder. This exists for thread-safe lazy initialization. It will
* cause getOrComputeDirectoryPaths() to be called (safely) the first time DIRECTORY_PATHS is
* accessed.
*
* <p>See https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom.
*/
private static class Holder {
private static final String[] DIRECTORY_PATHS = getOrComputeDirectoryPaths();
}
/**
* 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() {
try {
// We need to call sDirPathFetchTask.cancel() here to prevent races. If it returns
// true, that means that the task got canceled successfully (and thus, it did not
// finish running its task). Otherwise, it failed to cancel, meaning that it was
// already finished.
if (sDirPathFetchTask.cancel(false)) {
// Allow disk access here because we have no other choice.
try (StrictModeContext unused = StrictModeContext.allowDiskWrites()) {
// sDirPathFetchTask did not complete. We have to run the code it was supposed
// to be responsible for synchronously on the UI thread.
return PathUtils.setPrivateDataDirectorySuffixInternal();
}
} else {
// sDirPathFetchTask succeeded, and the values we need should be ready to access
// synchronously in its internal future.
return sDirPathFetchTask.get();
}
} catch (InterruptedException e) {
} catch (ExecutionException e) {
}
return null;
}
@SuppressLint("NewApi")
private static void chmod(String path, int mode) {
// Both Os.chmod and ErrnoException require SDK >= 21. But while Dalvik on < 21 tolerates
// Os.chmod, it throws VerifyError for ErrnoException, so catch Exception instead.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
try {
Os.chmod(path, mode);
} catch (Exception e) {
Log.e(TAG, "Failed to set permissions for path \"" + path + "\"");
}
}
/**
* 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)
*/
private static String[] setPrivateDataDirectorySuffixInternal() {
String[] paths = new String[NUM_DIRECTORIES];
Context appContext = ContextUtils.getApplicationContext();
paths[DATA_DIRECTORY] = appContext.getDir(
sDataDirectorySuffix, Context.MODE_PRIVATE).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] = appContext.getDir(
THUMBNAIL_DIRECTORY_NAME, Context.MODE_PRIVATE).getPath();
if (appContext.getCacheDir() != null) {
if (sCacheSubDirectory == null) {
paths[CACHE_DIRECTORY] = appContext.getCacheDir().getPath();
} else {
paths[CACHE_DIRECTORY] =
new File(appContext.getCacheDir(), sCacheSubDirectory).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 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, 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;
sDataDirectorySuffix = suffix;
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::setPrivateDataDirectorySuffixInternal);
AsyncTask.THREAD_POOL_EXECUTOR.execute(sDirPathFetchTask);
}
}
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) {
return Holder.DIRECTORY_PATHS[index];
}
/**
* @return the private directory that is used to store application data.
*/
@CalledByNative
public static String getDataDirectory() {
assert sDirPathFetchTask != null : "setDataDirectorySuffix must be called first.";
return getDirectoryPath(DATA_DIRECTORY);
}
/**
* @return the cache directory.
*/
@CalledByNative
public static String getCacheDirectory() {
assert sDirPathFetchTask != null : "setDataDirectorySuffix must be called first.";
return getDirectoryPath(CACHE_DIRECTORY);
}
@CalledByNative
public static String getThumbnailCacheDirectory() {
assert sDirPathFetchTask != null : "setDataDirectorySuffix must be called first.";
return getDirectoryPath(THUMBNAIL_DIRECTORY);
}
/**
* @return the public downloads directory.
*/
@SuppressWarnings("unused")
@CalledByNative
private static String getDownloadsDirectory() {
// Temporarily allowing disk access while fixing. TODO: http://crbug.com/508615
try (StrictModeContext unused = StrictModeContext.allowDiskReads()) {
long time = SystemClock.elapsedRealtime();
String downloadsPath =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
.getPath();
RecordHistogram.recordTimesHistogram("Android.StrictMode.DownloadsDir",
SystemClock.elapsedRealtime() - time, TimeUnit.MILLISECONDS);
return downloadsPath;
}
}
/**
* @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() {
File[] files;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try (StrictModeContext unused = StrictModeContext.allowDiskWrites()) {
files = ContextUtils.getApplicationContext().getExternalFilesDirs(
Environment.DIRECTORY_DOWNLOADS);
}
} else {
files = new File[] {Environment.getExternalStorageDirectory()};
}
ArrayList<String> absolutePaths = new ArrayList<String>();
for (int i = 0; i < files.length; ++i) {
if (files[i] == null || TextUtils.isEmpty(files[i].getAbsolutePath())) continue;
absolutePaths.add(files[i].getAbsolutePath());
}
return absolutePaths.toArray(new String[absolutePaths.size()]);
}
/**
* @return the path to native libraries.
*/
@SuppressWarnings("unused")
@CalledByNative
private static 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 String getExternalStorageDirectory() {
return Environment.getExternalStorageDirectory().getAbsolutePath();
}
/**
* @ return the path to the base apk.
*/
@SuppressWarnings("unused")
@CalledByNative
public static String getPathToBaseApk() {
return ContextUtils.getApplicationContext().getApplicationInfo().sourceDir;
}
}