blob: 7b99dd4bebc91bdb3ba6b80c161b67618be3d82a [file] [log] [blame]
// Copyright 2018 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.chrome.browser.download;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Environment;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.PathUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.AsyncTask;
import org.chromium.base.task.PostTask;
import org.chromium.chrome.browser.download.DirectoryOption.DownloadLocationDirectoryType;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import java.io.File;
import java.util.ArrayList;
/**
* Class to provide download related directory options including the default download directory on
* the primary storage, or a private directory on external SD card.
*
* This class uses an asynchronous task to retrieve the directories, and guarantee only one task
* can execute at any time. Multiple tasks may cause certain device fail to retrieve download
* directories. Should be used on main thread.
*
* Also, this class listens to SD card insertion and removal events to update the directory
* options accordingly.
*/
public class DownloadDirectoryProvider {
/**
* Asynchronous task to retrieve all download directories on a background thread. Only one task
* can exist at the same time.
*
* The logic to retrieve directories should match
* {@link PathUtils#getAllPrivateDownloadsDirectories}.
*/
private class AllDirectoriesTask extends AsyncTask<ArrayList<DirectoryOption>> {
@Override
protected ArrayList<DirectoryOption> doInBackground() {
ArrayList<DirectoryOption> dirs = new ArrayList<>();
// Retrieve default directory.
File defaultDirectory = DownloadUtils.getPrimaryDownloadDirectory();
// If no default directory, return an error option.
if (defaultDirectory == null) {
dirs.add(new DirectoryOption(
null, 0, 0, DirectoryOption.DownloadLocationDirectoryType.ERROR));
return dirs;
}
DirectoryOption defaultOption = toDirectoryOption(
defaultDirectory, DirectoryOption.DownloadLocationDirectoryType.DEFAULT);
dirs.add(defaultOption);
recordDirectoryType(DirectoryOption.DownloadLocationDirectoryType.DEFAULT);
// Retrieve additional directories, i.e. the external SD card directory.
mExternalStorageDirectory = Environment.getExternalStorageDirectory().getAbsolutePath();
File[] files;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
files = ContextUtils.getApplicationContext().getExternalFilesDirs(
Environment.DIRECTORY_DOWNLOADS);
} else {
files = new File[] {Environment.getExternalStorageDirectory()};
}
if (files.length <= 1) return dirs;
boolean hasAddtionalDirectory = false;
for (int i = 0; i < files.length; ++i) {
if (files[i] == null) continue;
// Skip primary storage directory.
if (files[i].getAbsolutePath().contains(mExternalStorageDirectory)) continue;
dirs.add(toDirectoryOption(
files[i], DirectoryOption.DownloadLocationDirectoryType.ADDITIONAL));
hasAddtionalDirectory = true;
}
if (hasAddtionalDirectory)
recordDirectoryType(DirectoryOption.DownloadLocationDirectoryType.ADDITIONAL);
return dirs;
}
@Override
protected void onPostExecute(ArrayList<DirectoryOption> dirs) {
mDirectoryOptions = dirs;
mDirectoriesReady = true;
mNeedsUpdate = false;
for (Callback<ArrayList<DirectoryOption>> callback : mCallbacks) {
callback.onResult(mDirectoryOptions);
}
mCallbacks.clear();
mAllDirectoriesTask = null;
}
private DirectoryOption toDirectoryOption(
File dir, @DownloadLocationDirectoryType int type) {
if (dir == null) return null;
return new DirectoryOption(
dir.getAbsolutePath(), dir.getUsableSpace(), dir.getTotalSpace(), type);
}
}
// Singleton instance.
private static class LazyHolder {
private static DownloadDirectoryProvider sInstance = new DownloadDirectoryProvider();
}
/**
* Get the instance of directory provider.
* @return The singleton directory provider instance.
*/
public static DownloadDirectoryProvider getInstance() {
return LazyHolder.sInstance;
}
/**
* Sets the directory provider for testing.
* @param provider The directory provider used in tests.
*/
public void setDirectoryProviderForTesting(DownloadDirectoryProvider provider) {
LazyHolder.sInstance = provider;
}
/**
* BroadcastReceiver to listen to external SD card insertion and removal events.
*/
private final class ExternalSDCardReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(Intent.ACTION_MEDIA_REMOVED)
|| intent.getAction().equals(Intent.ACTION_MEDIA_MOUNTED)
|| intent.getAction().equals(Intent.ACTION_MEDIA_EJECT)) {
// When receiving SD card events, immediately retrieve download directory may not
// yield correct result, mark needs update to force to fire another
// AllDirectoriesTask on next getAllDirectoriesOptions call.
mNeedsUpdate = true;
}
}
}
private ExternalSDCardReceiver mExternalSDCardReceiver;
private boolean mDirectoriesReady;
private boolean mNeedsUpdate;
private AllDirectoriesTask mAllDirectoriesTask;
private ArrayList<DirectoryOption> mDirectoryOptions;
private String mExternalStorageDirectory;
private ArrayList < Callback < ArrayList<DirectoryOption>>> mCallbacks = new ArrayList<>();
protected DownloadDirectoryProvider() {
registerSDCardReceiver();
}
/**
* Get all available download directories.
* @param callback The callback that carries the result of all download directories.
*/
public void getAllDirectoriesOptions(Callback<ArrayList<DirectoryOption>> callback) {
// Use cache value.
if (!mNeedsUpdate && mDirectoriesReady) {
PostTask.postTask(
UiThreadTaskTraits.DEFAULT, () -> callback.onResult(mDirectoryOptions));
return;
}
mCallbacks.add(callback);
updateDirectories();
}
/**
* Retrieves the external storage directory from in-memory cache. On Android M,
* {@link Environment#getExternalStorageDirectory} may access disk, so this operation can't be
* done on main thread.
* @return The external storage path or null if the or null if the asynchronous task to query
* the directories is not finished.
*/
public String getExternalStorageDirectory() {
if (mDirectoriesReady) return mExternalStorageDirectory;
return null;
}
private void updateDirectories() {
// If asynchronous task is pending, wait for its result.
if (mAllDirectoriesTask != null) return;
mAllDirectoriesTask = new AllDirectoriesTask();
mAllDirectoriesTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void registerSDCardReceiver() {
IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_REMOVED);
filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
filter.addAction(Intent.ACTION_MEDIA_EJECT);
filter.addDataScheme("file");
mExternalSDCardReceiver = new ExternalSDCardReceiver();
ContextUtils.getApplicationContext().registerReceiver(mExternalSDCardReceiver, filter);
}
private void recordDirectoryType(@DirectoryOption.DownloadLocationDirectoryType int type) {
RecordHistogram.recordEnumeratedHistogram("MobileDownload.Location.DirectoryType", type,
DirectoryOption.DownloadLocationDirectoryType.NUM_ENTRIES);
}
}