Commit 06b152e9 authored by Xing Liu's avatar Xing Liu Committed by Commit Bot

Download: Add a method to get secondary storage Download directory on R.

Android R starts to restrict other apps to access app's private
directory on R. Based on suggestion from Android engineer, we can use
a few new APIs added in Q and R to move the storage path a non private
directory.

Old path:
/storage/$volume_id/$app_private_directory/Download
New path:
/storage/&volume_id/Download

Bug: 1116905
Change-Id: Iecb34a08648678dbd1ff986c7ee60f3a4647c725
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2515262Reviewed-by: default avatarTommy Nyquist <nyquist@chromium.org>
Reviewed-by: default avatarMatt Menke <mmenke@chromium.org>
Reviewed-by: default avatarMin Qin <qinmin@chromium.org>
Reviewed-by: default avatarDavid Trainor <dtrainor@chromium.org>
Commit-Queue: Xing Liu <xingliu@chromium.org>
Cr-Commit-Position: refs/heads/master@{#826685}
parent 938e32d0
...@@ -9,10 +9,13 @@ import android.content.Context; ...@@ -9,10 +9,13 @@ import android.content.Context;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.os.Build; import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.os.storage.StorageManager;
import android.provider.MediaStore;
import android.system.Os; import android.system.Os;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import org.chromium.base.annotations.CalledByNative; import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.MainDex; import org.chromium.base.annotations.MainDex;
...@@ -20,6 +23,9 @@ import org.chromium.base.task.AsyncTask; ...@@ -20,6 +23,9 @@ import org.chromium.base.task.AsyncTask;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.FutureTask; import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
...@@ -202,7 +208,7 @@ public abstract class PathUtils { ...@@ -202,7 +208,7 @@ public abstract class PathUtils {
try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) { try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
if (BuildInfo.isAtLeastQ()) { if (BuildInfo.isAtLeastQ()) {
// https://developer.android.com/preview/privacy/scoped-storage // https://developer.android.com/preview/privacy/scoped-storage
// In Q+, Android has bugun sandboxing external storage. Chrome may not have // In Q+, Android has begun sandboxing external storage. Chrome may not have
// permission to write to Environment.getExternalStoragePublicDirectory(). Instead // permission to write to Environment.getExternalStoragePublicDirectory(). Instead
// using Context.getExternalFilesDir() will return a path to sandboxed external // using Context.getExternalFilesDir() will return a path to sandboxed external
// storage for which no additional permissions are required. // storage for which no additional permissions are required.
...@@ -221,20 +227,56 @@ public abstract class PathUtils { ...@@ -221,20 +227,56 @@ public abstract class PathUtils {
@SuppressWarnings("unused") @SuppressWarnings("unused")
@CalledByNative @CalledByNative
public static String[] getAllPrivateDownloadsDirectories() { public static String[] getAllPrivateDownloadsDirectories() {
File[] files; List<File> files = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try (StrictModeContext ignored = StrictModeContext.allowDiskWrites()) { try (StrictModeContext ignored = StrictModeContext.allowDiskWrites()) {
files = ContextUtils.getApplicationContext().getExternalFilesDirs( files = Arrays.asList(ContextUtils.getApplicationContext().getExternalFilesDirs(
Environment.DIRECTORY_DOWNLOADS); Environment.DIRECTORY_DOWNLOADS));
} }
} else { } else {
files = new File[] {Environment.getExternalStorageDirectory()}; files.add(Environment.getExternalStorageDirectory());
} }
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() {
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)) {
File volumeDir = ContextUtils.getApplicationContext()
.getSystemService(StorageManager.class)
.getStorageVolume(MediaStore.Files.getContentUri(vol))
.getDirectory();
assert volumeDir.isDirectory();
assert volumeDir.exists();
File volumeDownloadDir =
new File(volumeDir.getAbsolutePath(), Environment.DIRECTORY_DOWNLOADS);
assert volumeDownloadDir.isDirectory();
assert volumeDownloadDir.exists();
files.add(volumeDownloadDir);
}
}
return toAbsolutePathStrings(files);
}
private static String[] toAbsolutePathStrings(List<File> files) {
ArrayList<String> absolutePaths = new ArrayList<String>(); ArrayList<String> absolutePaths = new ArrayList<String>();
for (int i = 0; i < files.length; ++i) { for (File file : files) {
if (files[i] == null || TextUtils.isEmpty(files[i].getAbsolutePath())) continue; if (file == null || TextUtils.isEmpty(file.getAbsolutePath())) continue;
absolutePaths.add(files[i].getAbsolutePath()); absolutePaths.add(file.getAbsolutePath());
} }
return absolutePaths.toArray(new String[absolutePaths.size()]); return absolutePaths.toArray(new String[absolutePaths.size()]);
......
...@@ -60,6 +60,18 @@ std::vector<FilePath> GetAllPrivateDownloadsDirectories() { ...@@ -60,6 +60,18 @@ std::vector<FilePath> GetAllPrivateDownloadsDirectories() {
return file_paths; return file_paths;
} }
std::vector<FilePath> GetSecondaryStorageDownloadDirectories() {
std::vector<std::string> dirs;
JNIEnv* env = AttachCurrentThread();
auto jarray = Java_PathUtils_getExternalDownloadVolumesNames(env);
base::android::AppendJavaStringArrayToStringVector(env, jarray, &dirs);
std::vector<base::FilePath> file_paths;
for (const auto& dir : dirs)
file_paths.emplace_back(dir);
return file_paths;
}
bool GetNativeLibraryDirectory(FilePath* result) { bool GetNativeLibraryDirectory(FilePath* result) {
JNIEnv* env = AttachCurrentThread(); JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jstring> path = ScopedJavaLocalRef<jstring> path =
......
...@@ -40,6 +40,10 @@ BASE_EXPORT bool GetDownloadsDirectory(FilePath* result); ...@@ -40,6 +40,10 @@ BASE_EXPORT bool GetDownloadsDirectory(FilePath* result);
// directory, and a private directory on external SD card. // directory, and a private directory on external SD card.
BASE_EXPORT std::vector<FilePath> GetAllPrivateDownloadsDirectories(); BASE_EXPORT std::vector<FilePath> GetAllPrivateDownloadsDirectories();
// Retrieves the paths to all secondary storage download directories. e.g.
// /storage/1AEF-1A1E/Download/.
BASE_EXPORT std::vector<FilePath> GetSecondaryStorageDownloadDirectories();
// Retrieves the path to the native JNI libraries via // Retrieves the path to the native JNI libraries via
// ApplicationInfo.nativeLibraryDir on the Java side. The result is placed in // ApplicationInfo.nativeLibraryDir on the Java side. The result is placed in
// the FilePath pointed to by 'result'. // the FilePath pointed to by 'result'.
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
#endif #endif
#if defined(OS_ANDROID) #if defined(OS_ANDROID)
#include "base/android/build_info.h"
#include "base/android/path_utils.h" #include "base/android/path_utils.h"
#endif #endif
...@@ -76,9 +77,19 @@ bool IsAccessAllowedInternal(const base::FilePath& path, ...@@ -76,9 +77,19 @@ bool IsAccessAllowedInternal(const base::FilePath& path,
if (external_storage_path.IsParent(path)) if (external_storage_path.IsParent(path))
return true; return true;
auto all_download_dirs = base::android::GetAllPrivateDownloadsDirectories(); std::vector<base::FilePath> all_download_dirs =
for (const auto& dir : all_download_dirs) base::android::GetAllPrivateDownloadsDirectories();
allowlist.push_back(dir); allowlist.insert(allowlist.end(), all_download_dirs.begin(),
all_download_dirs.end());
base::android::BuildInfo* build_info =
base::android::BuildInfo::GetInstance();
if (build_info->sdk_int() > base::android::SDK_VERSION_Q) {
std::vector<base::FilePath> all_external_download_volumes =
base::android::GetSecondaryStorageDownloadDirectories();
allowlist.insert(allowlist.end(), all_external_download_volumes.begin(),
all_external_download_volumes.end());
}
// allowlist of other allowed directories. // allowlist of other allowed directories.
static const base::FilePath::CharType* const kLocalAccessAllowList[] = { static const base::FilePath::CharType* const kLocalAccessAllowList[] = {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment