// Copyright 2019 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.chrome.browser.download;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.content.FileProvider;
import org.chromium.base.ContentUriUtils;
import org.chromium.base.ContextUtils;
import org.chromium.chrome.browser.download.DownloadDirectoryProvider.SecondaryStorageInfo;
import org.chromium.components.embedder_support.util.UrlConstants;
import java.io.File;
import java.io.FileNotFoundException;
/**
* Provides data access and generate content URI for downloaded files open or shared to other
* application. By default, {@link FileProvider} doesn't support any paths on external SD card. This
* provider can generate content URI for arbitrary path.
*
* File on primary storage: /storage/emulated/0/Download/demo.apk
* generates URI: content://[package name].DownloadFileProvide/download?file=demo.apk
* Currently the primary storage downloads still use {@link FileProvider} instead of this.
*
* File on external SD card pre R: /storage/724E-59EE/Android/data/[package
* name]/files/Download/demo.apk" generates URI: content://[package
* name].DownloadFileProvide/download_external?file=demo.apk
*
* File on external SD card from R: /storage/724E-59EE/Download/demo.apk"
* * generates URI: content://[package name].DownloadFileProvide/external_volume?file=demo.apk
*/
public class DownloadFileProvider extends FileProvider {
private static final String[] COLUMNS = new String[] {"_display_name", "_size"};
private static final String URI_AUTHORITY_SUFFIX = ".DownloadFileProvider";
/** The URI path for downloads on primary storage. */
private static final String URI_PATH = "download";
/** The URI path for downloads on external storage before Android R. */
private static final String URI_EXTERNAL_PATH_LEGACY = "download_external";
/** The URI path for downloads on external storage from Android R. */
private static final String URI_EXTERNAL_PATH = "external_volume";
private static final String URI_QUERY_FILE = "file";
/**
* Create content URI on user device.
* See {@link #createContentUri(String, DownloadDirectoryProvider.Delegate)}
*/
public static Uri createContentUri(String filePath) {
return createContentUri(
filePath, new DownloadDirectoryProvider.DownloadDirectoryProviderDelegate());
}
/**
* Create content uri for a downloaded file, mainly for files on external sd card.
* @param filePath The file path of the file.
* @param delegate Delegate that queries download directories.
* @return The content URI that points to the downloaded file.
*/
@VisibleForTesting
public static Uri createContentUri(
String filePath, DownloadDirectoryProvider.Delegate delegate) {
// From Android Q, we may already have a content URI generated by the media store. Then
// just let the media store to handle this content URI.
if (ContentUriUtils.isContentUri(filePath)) return Uri.parse(filePath);
if (TextUtils.isEmpty(filePath)) return Uri.EMPTY;
// Check whether the download is on primary storage.
File primaryDir = delegate.getPrimaryDownloadDirectory();
int index = filePath.indexOf(primaryDir.getAbsolutePath());
if (index == 0 && filePath.length() > primaryDir.getAbsolutePath().length()) {
return buildUri(
URI_PATH, filePath.substring(primaryDir.getAbsolutePath().length() + 1));
}
// Check whether the download is in Android R's SD card directory.
SecondaryStorageInfo info = delegate.getSecondaryStorageDownloadDirectories();
if (info.directories != null) {
for (File file : info.directories) {
if (file == null) continue;
if (filePath.startsWith(file.getAbsolutePath())) {
return buildUri(
URI_EXTERNAL_PATH,
filePath.substring(file.getAbsolutePath().length() + 1));
}
}
}
// Check whether the download is in legacy SD card directory pre R.
if (info.directoriesPreR == null) return Uri.EMPTY;
for (File file : info.directoriesPreR) {
if (file == null) continue;
if (filePath.startsWith(file.getAbsolutePath())) {
return buildUri(
URI_EXTERNAL_PATH_LEGACY,
filePath.substring(file.getAbsolutePath().length() + 1));
}
}
return Uri.EMPTY;
}
private static Uri buildUri(String path, String query) {
Uri uri =
new Uri.Builder()
.scheme(UrlConstants.CONTENT_SCHEME)
.authority(
ContextUtils.getApplicationContext().getPackageName()
+ URI_AUTHORITY_SUFFIX)
.path(path)
.appendQueryParameter(URI_QUERY_FILE, query)
.build();
return uri;
}
@Override
public boolean onCreate() {
return true;
}
@Override
public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
super.attachInfo(context, info);
if (info.exported) {
throw new SecurityException("Provider must not be exported");
} else if (!info.grantUriPermissions) {
throw new SecurityException("Provider must grant uri permissions");
}
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
String filePath =
getFilePathFromUri(
uri, new DownloadDirectoryProvider.DownloadDirectoryProviderDelegate());
if (filePath == null) throw new FileNotFoundException();
int fileMode = modeToMode(mode);
File file = new File(filePath);
return ParcelFileDescriptor.open(file, fileMode);
}
@Override
public Cursor query(
@NonNull Uri uri,
@Nullable String[] projection,
@Nullable String selection,
@Nullable String[] selectionArgs,
@Nullable String sortOrder) {
if (projection == null) {
projection = COLUMNS;
}
String[] cols = new String[projection.length];
Object[] values = new Object[projection.length];
String filePath =
getFilePathFromUri(
uri, new DownloadDirectoryProvider.DownloadDirectoryProviderDelegate());
if (TextUtils.isEmpty(filePath)) return new MatrixCursor(cols, 1);
File file = new File(filePath);
if (!file.exists() || !file.isFile()) return new MatrixCursor(cols, 1);
int i = 0;
String[] projectionCopy = projection;
int projectionLen = projection.length;
for (int j = 0; j < projectionLen; ++j) {
String col = projectionCopy[j];
if ("_display_name".equals(col)) {
cols[i] = "_display_name";
values[i++] = file.getName();
} else if ("_size".equals(col)) {
cols[i] = "_size";
values[i++] = Long.valueOf(file.length());
}
}
cols = copyOf(cols, i);
values = copyOf(values, i);
MatrixCursor cursor = new MatrixCursor(cols, 1);
cursor.addRow(values);
return cursor;
}
@Override
public String getType(Uri uri) {
if (uri == null) return null;
String filePath = uri.getQueryParameter(URI_QUERY_FILE);
if (TextUtils.isEmpty(filePath)) return null;
return getMimeTypeFromUri(Uri.parse(filePath));
}
@Override
public Uri insert(Uri uri, ContentValues contentValues) {
throw new UnsupportedOperationException("No external inserts");
}
@Override
public int delete(Uri uri, String s, String[] strings) {
throw new UnsupportedOperationException("No external deletes");
}
@Override
public int update(Uri uri, ContentValues contentValues, String s, String[] strings) {
throw new UnsupportedOperationException("No external updates");
}
private static String getMimeTypeFromUri(Uri fileUri) {
String extension = MimeTypeMap.getFileExtensionFromUrl(fileUri.toString());
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
/** Copy from {@link FileProvider}. */
private static String[] copyOf(String[] original, int newLength) {
String[] result = new String[newLength];
System.arraycopy(original, 0, result, 0, newLength);
return result;
}
/** Copy from {@link FileProvider}. */
private static Object[] copyOf(Object[] original, int newLength) {
Object[] result = new Object[newLength];
System.arraycopy(original, 0, result, 0, newLength);
return result;
}
/** Copy from {@link FileProvider}. */
private static int modeToMode(String mode) {
int modeBits;
if ("r".equals(mode)) {
modeBits = 268435456;
} else if (!"w".equals(mode) && !"wt".equals(mode)) {
if ("wa".equals(mode)) {
modeBits = 704643072;
} else if ("rw".equals(mode)) {
modeBits = 939524096;
} else {
if (!"rwt".equals(mode)) {
throw new IllegalArgumentException("Invalid mode: " + mode);
}
modeBits = 1006632960;
}
} else {
modeBits = 738197504;
}
return modeBits;
}
/**
* Get file path based on URI. The file must live in default download directory or external
* removable storage.
* @param uri The content URI to parse.
* @return The absolute path of the file or null if the file is not in known download directory
* or null if the file doesn't exist.
*/
@VisibleForTesting
public static String getFilePathFromUri(Uri uri, DownloadDirectoryProvider.Delegate delegate) {
if (uri == null) return null;
String path = uri.getPath();
if (TextUtils.isEmpty(path)) return null;
if (path.charAt(0) == File.separatorChar && path.length() > 1) path = path.substring(1);
// Path traverse to parent is not allowed.
String query = uri.getQueryParameter(URI_QUERY_FILE);
if (query.contains(".." + File.separator)) return null;
// Parse download on primary storage.
if (path.equals(URI_PATH)) {
File primaryDir = delegate.getPrimaryDownloadDirectory();
return primaryDir + File.separator + query;
}
// Parse download on external SD card on new Android R directory.
SecondaryStorageInfo info = delegate.getSecondaryStorageDownloadDirectories();
if (path.equals(URI_EXTERNAL_PATH) && !info.directories.isEmpty()) {
// Only supports one directory.
return info.directories.get(0).getAbsolutePath() + File.separator + query;
}
// Parse download on legacy SD card directory. On R, we also parse this to support existing
// downloads before R.
if (path.equals(URI_EXTERNAL_PATH_LEGACY) && !info.directoriesPreR.isEmpty()) {
// Only supports one directory.
return info.directoriesPreR.get(0).getAbsolutePath() + File.separator + query;
}
return null;
}
}