Snap for 8426163 from 965cc56320b780ef6c30475e809886bb0cd44505 to mainline-tzdata2-release
Change-Id: I3cb19386c0c12321145b78aa47894598b938769c
diff --git a/Android.bp b/Android.bp
index 106ed1e..78c8072 100644
--- a/Android.bp
+++ b/Android.bp
@@ -13,28 +13,8 @@
// limitations under the License.
//
-package {
- default_applicable_licenses: [
- "packages_providers_DownloadProvider_license",
- ],
-}
-
-// Added automatically by a large-scale-change
-// See: http://go/android-license-faq
-license {
- name: "packages_providers_DownloadProvider_license",
- visibility: [":__subpackages__"],
- license_kinds: [
- "SPDX-license-identifier-Apache-2.0",
- ],
- license_text: [
- "NOTICE",
- ],
-}
-
android_app {
name: "DownloadProvider",
- defaults: ["platform_app_defaults"],
manifest: "AndroidManifest.xml",
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 43cf258..6ef1b54 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -60,7 +60,6 @@
<uses-permission android:name="android.permission.START_ACTIVITIES_FROM_BACKGROUND"/>
<uses-permission android:name="android.permission.WRITE_MEDIA_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
- <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS"/>
<application android:process="android.process.media"
android:label="@string/app_label"
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/OWNERS b/OWNERS
index 79add9e..8134437 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,3 +1,2 @@
-# Bug component: 95221
-
-include platform/frameworks/base:/core/java/android/os/storage/OWNERS
+jsharkey@android.com
+sudheersai@google.com
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 2355624..bd9021e 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -16,7 +16,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="5264040740662487684">"Менеджер скачивания"</string>
+ <string name="app_label" msgid="5264040740662487684">"Диспетчер загрузки"</string>
<string name="storage_description" msgid="169690279447532621">"Скачанные"</string>
<string name="permlab_downloadManager" msgid="4241473724446132797">"Доступ к менеджеру загрузки."</string>
<string name="permdesc_downloadManager" msgid="5562734314998369030">"Приложение получит доступ к диспетчеру загрузок и сможет загружать файлы через него. Вредоносные программы смогут таким образом прерывать загрузки и просматривать личную информацию."</string>
diff --git a/src/com/android/providers/downloads/DownloadNotifier.java b/src/com/android/providers/downloads/DownloadNotifier.java
index 224aee4..d38aa75 100644
--- a/src/com/android/providers/downloads/DownloadNotifier.java
+++ b/src/com/android/providers/downloads/DownloadNotifier.java
@@ -233,8 +233,7 @@
intent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS,
downloadIds);
builder.setContentIntent(PendingIntent.getBroadcast(mContext,
- 0, intent,
- PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE));
+ 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
if (type == TYPE_ACTIVE) {
builder.setOngoing(true);
}
@@ -251,8 +250,7 @@
android.R.drawable.ic_menu_close_clear_cancel,
res.getString(R.string.button_cancel_download),
PendingIntent.getBroadcast(mContext,
- 0, cancelIntent,
- PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE));
+ 0, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT));
} else if (type == TYPE_COMPLETE) {
cursor.moveToPosition(cluster.get(0));
@@ -276,14 +274,12 @@
intent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS,
getDownloadIds(cursor, cluster));
builder.setContentIntent(PendingIntent.getBroadcast(mContext,
- 0, intent,
- PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE));
+ 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
final Intent hideIntent = new Intent(Constants.ACTION_HIDE,
uri, mContext, DownloadReceiver.class);
hideIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
- builder.setDeleteIntent(PendingIntent.getBroadcast(mContext, 0, hideIntent,
- PendingIntent.FLAG_IMMUTABLE));
+ builder.setDeleteIntent(PendingIntent.getBroadcast(mContext, 0, hideIntent, 0));
}
// Calculate and show progress
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index a9e0237..b868113 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -257,6 +257,7 @@
private int mSystemUid = -1;
private StorageManager mStorageManager;
+ private AppOpsManager mAppOpsManager;
/**
* Creates and updated database on demand when opening it.
@@ -587,6 +588,7 @@
mSystemUid = Process.SYSTEM_UID;
mStorageManager = getContext().getSystemService(StorageManager.class);
+ mAppOpsManager = getContext().getSystemService(AppOpsManager.class);
// Grant access permissions for all known downloads to the owning apps.
final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
@@ -735,10 +737,9 @@
android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
"No permission to write");
- final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
- if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, getCallingPackage(),
- Binder.getCallingUid(), getCallingAttributionTag(), null)
- != AppOpsManager.MODE_ALLOWED) {
+ if (mAppOpsManager.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
+ getCallingPackage(), Binder.getCallingUid(), getCallingAttributionTag(),
+ null) != AppOpsManager.MODE_ALLOWED) {
throw new SecurityException("No permission to write");
}
}
@@ -942,13 +943,6 @@
}
return mediaStoreUri;
}
- } catch (IllegalArgumentException ignored) {
- // Insert or update MediaStore failed. At this point we can't do
- // much here. If the file belongs to MediaStore collection, it will
- // get added to MediaStore collection during next scan, and we will
- // obtain the uri to the file in the next MediaStore#scanFile
- // initiated by us
- Log.w(Constants.TAG, "Couldn't update MediaStore for " + filePath, ignored);
} catch (RemoteException e) {
// Should not happen
}
@@ -1067,41 +1061,11 @@
throw new SecurityException(e);
}
- final int targetSdkVersion = getCallingPackageTargetSdkVersion();
- final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
- final boolean runningLegacyMode = appOpsManager.checkOp(AppOpsManager.OP_LEGACY_STORAGE,
+ final boolean isLegacyMode = mAppOpsManager.checkOp(AppOpsManager.OP_LEGACY_STORAGE,
Binder.getCallingUid(), getCallingPackage()) == AppOpsManager.MODE_ALLOWED;
-
- if (Helpers.isFilenameValidInExternalPackage(getContext(), file, getCallingPackage())
- || Helpers.isFilenameValidInKnownPublicDir(file.getAbsolutePath())) {
- // No permissions required for paths belonging to calling package or
- // public downloads dir.
- return;
- } else if (runningLegacyMode && Helpers.isFilenameValidInExternal(getContext(), file)) {
- // Otherwise we require write permission
- getContext().enforceCallingOrSelfPermission(
- android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
- "No permission to write to " + file);
-
- final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
- if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, getCallingPackage(),
- Binder.getCallingUid(), getCallingAttributionTag(), null)
- != AppOpsManager.MODE_ALLOWED) {
- throw new SecurityException("No permission to write to " + file);
- }
- } else if (Helpers.isFilenameValidInExternalObbDir(file) &&
- ((appOpsManager.noteOp(
- AppOpsManager.OP_REQUEST_INSTALL_PACKAGES,
- Binder.getCallingUid(), getCallingPackage(), null, "obb_download")
- == AppOpsManager.MODE_ALLOWED)
- || (getContext().checkCallingOrSelfPermission(
- android.Manifest.permission.REQUEST_INSTALL_PACKAGES)
- == PackageManager.PERMISSION_GRANTED))) {
- // Installers are allowed to download in OBB dirs, even outside their own package
- return;
- } else {
- throw new SecurityException("Unsupported path " + file);
- }
+ Helpers.checkDestinationFilePathRestrictions(file, getCallingPackage(), getContext(),
+ mAppOpsManager, getCallingAttributionTag(), isLegacyMode,
+ /* allowDownloadsDirOnly */ false);
}
private void checkDownloadedFilePath(ContentValues values) {
@@ -1123,49 +1087,15 @@
throw new IllegalArgumentException("File doesn't exist: " + file);
}
- final int targetSdkVersion = getCallingPackageTargetSdkVersion();
- final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
- final boolean runningLegacyMode = appOpsManager.checkOp(AppOpsManager.OP_LEGACY_STORAGE,
- Binder.getCallingUid(), getCallingPackage()) == AppOpsManager.MODE_ALLOWED;
-
if (Binder.getCallingPid() == Process.myPid()) {
return;
- } else if (Helpers.isFilenameValidInExternalPackage(getContext(), file, getCallingPackage())
- || Helpers.isFilenameValidInPublicDownloadsDir(file)) {
- // No permissions required for paths belonging to calling package or
- // public downloads dir.
- return;
- } else if (runningLegacyMode && Helpers.isFilenameValidInExternal(getContext(), file)) {
- // Otherwise we require write permission
- getContext().enforceCallingOrSelfPermission(
- android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
- "No permission to write to " + file);
-
- final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
- if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, getCallingPackage(),
- Binder.getCallingUid(), getCallingAttributionTag(), null)
- != AppOpsManager.MODE_ALLOWED) {
- throw new SecurityException("No permission to write to " + file);
- }
- } else {
- throw new SecurityException("Unsupported path " + file);
}
- }
- private int getCallingPackageTargetSdkVersion() {
- final String callingPackage = getCallingPackage();
- if (callingPackage != null) {
- ApplicationInfo ai = null;
- try {
- ai = getContext().getPackageManager()
- .getApplicationInfo(callingPackage, 0);
- } catch (PackageManager.NameNotFoundException ignored) {
- }
- if (ai != null) {
- return ai.targetSdkVersion;
- }
- }
- return Build.VERSION_CODES.CUR_DEVELOPMENT;
+ final boolean isLegacyMode = mAppOpsManager.checkOp(AppOpsManager.OP_LEGACY_STORAGE,
+ Binder.getCallingUid(), getCallingPackage()) == AppOpsManager.MODE_ALLOWED;
+ Helpers.checkDestinationFilePathRestrictions(file, getCallingPackage(), getContext(),
+ mAppOpsManager, getCallingAttributionTag(), isLegacyMode,
+ /* allowDownloadsDirOnly */ true);
}
/**
@@ -1253,7 +1183,6 @@
error.append(", ");
}
error.append(entry.getKey());
- first = false;
}
throw new SecurityException(error.toString());
}
@@ -1817,7 +1746,7 @@
final ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, file.length());
values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION,
- mSystemFacade.currentTimeMillis());
+ System.currentTimeMillis());
update(uri, values, null, null);
if (shouldScan) {
diff --git a/src/com/android/providers/downloads/DownloadStorageProvider.java b/src/com/android/providers/downloads/DownloadStorageProvider.java
index 421c261..36304aa 100644
--- a/src/com/android/providers/downloads/DownloadStorageProvider.java
+++ b/src/com/android/providers/downloads/DownloadStorageProvider.java
@@ -18,7 +18,7 @@
import static com.android.providers.downloads.MediaStoreDownloadsHelper.getDocIdForMediaStoreDownload;
import static com.android.providers.downloads.MediaStoreDownloadsHelper.getMediaStoreIdString;
-import static com.android.providers.downloads.MediaStoreDownloadsHelper.getMediaStoreUriForQuery;
+import static com.android.providers.downloads.MediaStoreDownloadsHelper.getMediaStoreUri;
import static com.android.providers.downloads.MediaStoreDownloadsHelper.isMediaStoreDownload;
import static com.android.providers.downloads.MediaStoreDownloadsHelper.isMediaStoreDownloadDir;
@@ -28,6 +28,7 @@
import android.app.DownloadManager.Query;
import android.content.ContentResolver;
import android.content.ContentUris;
+import android.content.ContentValues;
import android.content.Context;
import android.content.UriPermission;
import android.database.Cursor;
@@ -66,7 +67,6 @@
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
-import java.util.Locale;
import java.util.Set;
/**
@@ -251,7 +251,7 @@
displayName = FileUtils.buildValidFatFilename(displayName);
if (isMediaStoreDownload(docId)) {
- return renameMediaStoreDownload(docId, displayName);
+ renameMediaStoreDownload(docId, displayName);
} else {
final long id = Long.parseLong(docId);
if (!mDm.rename(getContext(), id, displayName)) {
@@ -281,7 +281,7 @@
if (DOC_ID_ROOT.equals(docId)) {
includeDefaultDocument(result);
} else if (isMediaStoreDownload(docId)) {
- cursor = getContext().getContentResolver().query(getMediaStoreUriForQuery(docId),
+ cursor = getContext().getContentResolver().query(getMediaStoreUri(docId),
null, null, null);
copyNotificationUri(result, cursor);
if (cursor.moveToFirst()) {
@@ -513,7 +513,7 @@
final ContentResolver resolver = getContext().getContentResolver();
final Uri contentUri;
if (isMediaStoreDownload(docId)) {
- contentUri = getMediaStoreUriForQuery(docId);
+ contentUri = getMediaStoreUri(docId);
} else {
final long id = Long.parseLong(docId);
contentUri = mDm.getDownloadUri(id);
@@ -537,7 +537,7 @@
final ContentResolver resolver = getContext().getContentResolver();
final Uri contentUri;
if (isMediaStoreDownload(docId)) {
- contentUri = getMediaStoreUriForQuery(docId);
+ contentUri = getMediaStoreUri(docId);
} else {
final long id = Long.parseLong(docId);
contentUri = mDm.getDownloadUri(id);
@@ -769,7 +769,7 @@
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
}
- private String renameMediaStoreDownload(String docId, String displayName) {
+ private void renameMediaStoreDownload(String docId, String displayName) {
final File before = getFileForMediaStoreDownload(docId);
final File after = new File(before.getParentFile(), displayName);
@@ -780,34 +780,25 @@
throw new IllegalStateException("Failed to rename from " + before + " to " + after);
}
- final String noMedia = ".nomedia";
- // Scan the file to update the database
- // For file, check whether the file is renamed to .nomedia. If yes, to scan the parent
- // directory to update all files in the directory. We don't consider the case of renaming
- // .nomedia file. We don't show .nomedia file.
- if (!after.isDirectory() && displayName.toLowerCase(Locale.ROOT).endsWith(noMedia)) {
- final Uri newUri = MediaStore.scanFile(getContext().getContentResolver(),
- after.getParentFile());
- // the file will not show in the list, return the parent docId to avoid not finding
- // the detail for the file.
- return getDocIdForMediaStoreDownloadUri(newUri, true /* isDir */);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ final Uri mediaStoreUri = getMediaStoreUri(docId);
+ final ContentValues values = new ContentValues();
+ values.put(DownloadColumns.DATA, after.getAbsolutePath());
+ values.put(DownloadColumns.DISPLAY_NAME, displayName);
+ final int count = getContext().getContentResolver().update(mediaStoreUri, values,
+ null, null);
+ if (count != 1) {
+ throw new IllegalStateException("Failed to update " + mediaStoreUri
+ + ", values=" + values);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
- // update the database for the old file
- MediaStore.scanFile(getContext().getContentResolver(), before);
- // Update tne database for the new file and get the new uri
- final Uri newUri = MediaStore.scanFile(getContext().getContentResolver(), after);
- return getDocIdForMediaStoreDownloadUri(newUri, after.isDirectory());
- }
-
- private static String getDocIdForMediaStoreDownloadUri(Uri uri, boolean isDir) {
- if (uri != null) {
- return getDocIdForMediaStoreDownload(Long.parseLong(uri.getLastPathSegment()), isDir);
- }
- return null;
}
private File getFileForMediaStoreDownload(String docId) {
- final Uri mediaStoreUri = getMediaStoreUriForQuery(docId);
+ final Uri mediaStoreUri = getMediaStoreUri(docId);
final long token = Binder.clearCallingIdentity();
try (Cursor cursor = queryForSingleItem(mediaStoreUri,
new String[] { DownloadColumns.DATA }, null, null, null)) {
diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java
index 00fa30a..752dbfc 100644
--- a/src/com/android/providers/downloads/DownloadThread.java
+++ b/src/com/android/providers/downloads/DownloadThread.java
@@ -90,7 +90,6 @@
import java.net.URL;
import java.net.URLConnection;
import java.security.GeneralSecurityException;
-import java.util.Arrays;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
@@ -234,6 +233,8 @@
private boolean mIgnoreBlocked;
private Network mNetwork;
+ private int mNetworkType = ConnectivityManager.TYPE_NONE;
+
/** Historical bytes/second speed of this download. */
private long mSpeed;
/** Time when current sample started. */
@@ -293,6 +294,14 @@
"No network associated with requesting UID");
}
+ // Remember which network this download started on; used to
+ // determine if errors were due to network changes.
+ final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork, mInfo.mUid,
+ mIgnoreBlocked);
+ if (info != null) {
+ mNetworkType = info.getType();
+ }
+
// Network traffic on this thread should be counted against the
// requesting UID, and is tagged with well-known value.
TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD);
@@ -331,11 +340,13 @@
}
if (mInfoDelta.mNumFailed < Constants.MAX_RETRIES) {
- if (null != mSystemFacade.getNetworkCapabilities(mNetwork)) {
+ final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork, mInfo.mUid,
+ mIgnoreBlocked);
+ if (info != null && info.getType() == mNetworkType && info.isConnected()) {
// Underlying network is still intact, use normal backoff
mInfoDelta.mStatus = STATUS_WAITING_TO_RETRY;
} else {
- // Network unavailable, retry on any next available
+ // Network changed, retry on any next available
mInfoDelta.mStatus = STATUS_WAITING_FOR_NETWORK;
}
@@ -713,8 +724,9 @@
// checking connectivity will apply current policy
mPolicyDirty = false;
+ final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork, mInfo.mUid, mIgnoreBlocked);
final NetworkCapabilities caps = mSystemFacade.getNetworkCapabilities(mNetwork);
- if (caps == null) {
+ if (info == null || !info.isConnected()) {
throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is disconnected");
}
if (!caps.hasCapability(NET_CAPABILITY_NOT_ROAMING)
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index 772c0b9..574b222 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -34,6 +34,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.AppOpsManager;
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.ComponentName;
@@ -41,8 +42,10 @@
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
+import android.os.Binder;
import android.os.Environment;
import android.os.FileUtils;
import android.os.Handler;
@@ -84,6 +87,9 @@
private static final Pattern PATTERN_ANDROID_DIRS =
Pattern.compile("(?i)^/storage/[^/]+(?:/[0-9]+)?/Android/(?:data|obb|media)/.+");
+ private static final Pattern PATTERN_ANDROID_PRIVATE_DIRS =
+ Pattern.compile("(?i)^/storage/[^/]+(?:/[0-9]+)?/Android/(data|obb)/.+");
+
private static final Pattern PATTERN_PUBLIC_DIRS =
Pattern.compile("(?i)^/storage/[^/]+(?:/[0-9]+)?/([^/]+)/.+");
@@ -530,8 +536,7 @@
* directories that are always writable to apps, regardless of storage
* permission.
*/
- static boolean isFilenameValidInExternalPackage(Context context, File file,
- String packageName) {
+ static boolean isFilenameValidInExternalPackage(File file, String packageName) {
try {
if (containsCanonical(buildExternalStorageAppDataDirs(packageName), file) ||
containsCanonical(buildExternalStorageAppObbDirs(packageName), file) ||
@@ -539,7 +544,7 @@
return true;
}
} catch (IOException e) {
- Log.w(TAG, "Failed to resolve canonical path: " + e);
+ Log.w(TAG, "Failed to resolve canonical path: " + file.getAbsolutePath(), e);
return false;
}
@@ -552,13 +557,77 @@
return true;
}
} catch (IOException e) {
- Log.w(TAG, "Failed to resolve canonical path: " + e);
+ Log.w(TAG, "Failed to resolve canonical path: " + file.getAbsolutePath(), e);
return false;
}
return false;
}
+ /**
+ * Check if given file exists in one of the private package-specific external storage
+ * directories.
+ */
+ static boolean isFileInPrivateExternalAndroidDirs(File file) {
+ try {
+ return PATTERN_ANDROID_PRIVATE_DIRS.matcher(file.getCanonicalPath()).matches();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to resolve canonical path: " + file.getAbsolutePath(), e);
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks destination file path restrictions adhering to App privacy restrictions
+ *
+ * Note: This method is extracted to a static method for better test coverage.
+ */
+ @VisibleForTesting
+ static void checkDestinationFilePathRestrictions(File file, String callingPackage,
+ Context context, AppOpsManager appOpsManager, String callingAttributionTag,
+ boolean isLegacyMode, boolean allowDownloadsDirOnly) {
+ boolean isFileNameValid = allowDownloadsDirOnly ? isFilenameValidInPublicDownloadsDir(file)
+ : isFilenameValidInKnownPublicDir(file.getAbsolutePath());
+ if (isFilenameValidInExternalPackage(file, callingPackage) || isFileNameValid) {
+ // No permissions required for paths belonging to calling package or
+ // public downloads dir.
+ return;
+ } else if (isFilenameValidInExternalObbDir(file) &&
+ isCallingAppInstaller(context, appOpsManager, callingPackage)) {
+ // Installers are allowed to download in OBB dirs, even outside their own package
+ return;
+ } else if (isFileInPrivateExternalAndroidDirs(file)) {
+ // Positive cases of writing to external Android dirs is covered in the if blocks above.
+ // If the caller made it this far, then it cannot write to this path as it is restricted
+ // from writing to other app's external Android dirs.
+ throw new SecurityException("Unsupported path " + file);
+ } else if (isLegacyMode && isFilenameValidInExternal(context, file)) {
+ // Otherwise we require write permission
+ context.enforceCallingOrSelfPermission(
+ android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ "No permission to write to " + file);
+
+ if (appOpsManager.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
+ callingPackage, Binder.getCallingUid(), callingAttributionTag, null)
+ != AppOpsManager.MODE_ALLOWED) {
+ throw new SecurityException("No permission to write to " + file);
+ }
+ } else {
+ throw new SecurityException("Unsupported path " + file);
+ }
+ }
+
+ private static boolean isCallingAppInstaller(Context context, AppOpsManager appOpsManager,
+ String callingPackage) {
+ return (appOpsManager.noteOp(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES,
+ Binder.getCallingUid(), callingPackage, null, "obb_download")
+ == AppOpsManager.MODE_ALLOWED)
+ || (context.checkCallingOrSelfPermission(
+ android.Manifest.permission.REQUEST_INSTALL_PACKAGES)
+ == PackageManager.PERMISSION_GRANTED);
+ }
+
static boolean isFilenameValidInPublicDownloadsDir(File file) {
try {
if (containsCanonical(buildExternalStoragePublicDirs(
@@ -566,7 +635,7 @@
return true;
}
} catch (IOException e) {
- Log.w(TAG, "Failed to resolve canonical path: " + e);
+ Log.w(TAG, "Failed to resolve canonical path: " + file.getAbsolutePath(), e);
return false;
}
@@ -608,7 +677,7 @@
}
}
} catch (IOException e) {
- Log.w(TAG, "Failed to resolve canonical path: " + e);
+ Log.w(TAG, "Failed to resolve canonical path: " + file.getAbsolutePath(), e);
return false;
}
@@ -786,13 +855,13 @@
final ContentValues values = new ContentValues();
values.putNull(Constants.UID);
downloadProvider.update(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, values,
- Helpers.buildQueryWithIds(idsToOrphan), null);
+ buildQueryWithIds(idsToOrphan), null);
}
if (idsToDelete.size() > 0) {
Log.i(Constants.TAG, "Deleting downloads with ids "
+ Arrays.toString(idsToDelete.toArray()) + " as owner package is removed");
downloadProvider.delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
- Helpers.buildQueryWithIds(idsToDelete), null);
+ buildQueryWithIds(idsToDelete), null);
}
}
diff --git a/src/com/android/providers/downloads/MediaStoreDownloadsHelper.java b/src/com/android/providers/downloads/MediaStoreDownloadsHelper.java
index 9c86d53..c4f347c 100644
--- a/src/com/android/providers/downloads/MediaStoreDownloadsHelper.java
+++ b/src/com/android/providers/downloads/MediaStoreDownloadsHelper.java
@@ -47,18 +47,8 @@
return docId != null && docId.startsWith(MEDIASTORE_DOWNLOAD_DIR_PREFIX);
}
- /**
- * The returned uri always appends external volume {@link MediaStore#VOLUME_EXTERNAL}.
- * It doesn't consider the item is located on second volume. It can't be used to update
- * or insert.
- * @param docId the doc id
- * @return external uri for query
- */
- public static Uri getMediaStoreUriForQuery(String docId) {
- return getMediaStoreUri(MediaStore.VOLUME_EXTERNAL, docId);
- }
-
- public static Uri getMediaStoreUri(String volume, String docId) {
- return MediaStore.Downloads.getContentUri(volume, getMediaStoreId(docId));
+ public static Uri getMediaStoreUri(String docId) {
+ return ContentUris.withAppendedId(MediaStore.Downloads.EXTERNAL_CONTENT_URI,
+ getMediaStoreId(docId));
}
}
diff --git a/src/com/android/providers/downloads/RealSystemFacade.java b/src/com/android/providers/downloads/RealSystemFacade.java
index ba4068e..94461a6 100644
--- a/src/com/android/providers/downloads/RealSystemFacade.java
+++ b/src/com/android/providers/downloads/RealSystemFacade.java
@@ -57,6 +57,12 @@
}
@Override
+ public NetworkInfo getNetworkInfo(Network network, int uid, boolean ignoreBlocked) {
+ return mContext.getSystemService(ConnectivityManager.class)
+ .getNetworkInfoForUid(network, uid, ignoreBlocked);
+ }
+
+ @Override
public NetworkCapabilities getNetworkCapabilities(Network network) {
return mContext.getSystemService(ConnectivityManager.class)
.getNetworkCapabilities(network);
diff --git a/src/com/android/providers/downloads/SystemFacade.java b/src/com/android/providers/downloads/SystemFacade.java
index 1fda858..d73fe11 100644
--- a/src/com/android/providers/downloads/SystemFacade.java
+++ b/src/com/android/providers/downloads/SystemFacade.java
@@ -37,6 +37,7 @@
public Network getNetwork(JobParameters params);
+ public NetworkInfo getNetworkInfo(Network network, int uid, boolean ignoreBlocked);
public NetworkCapabilities getNetworkCapabilities(Network network);
/**
diff --git a/tests/Android.bp b/tests/Android.bp
index d0ba0a5..3c03c82 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -13,17 +13,6 @@
// limitations under the License.
//
-package {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "packages_providers_DownloadProvider_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: [
- "packages_providers_DownloadProvider_license",
- ],
-}
-
android_test {
name: "DownloadProviderTests",
@@ -38,8 +27,6 @@
],
static_libs: [
- "androidx.core_core",
- "androidx.test.rules",
"mockito-target",
"mockwebserver",
],
diff --git a/tests/permission/Android.bp b/tests/permission/Android.bp
index a96621f..274106f 100644
--- a/tests/permission/Android.bp
+++ b/tests/permission/Android.bp
@@ -13,17 +13,6 @@
// limitations under the License.
//
-package {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "packages_providers_DownloadProvider_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: [
- "packages_providers_DownloadProvider_license",
- ],
-}
-
android_test {
name: "DownloadProviderPermissionTests",
test_suites: ["device-tests"],
diff --git a/tests/public_api_access/Android.bp b/tests/public_api_access/Android.bp
index 3d2cd98..1f982b3 100644
--- a/tests/public_api_access/Android.bp
+++ b/tests/public_api_access/Android.bp
@@ -13,17 +13,6 @@
// limitations under the License.
//
-package {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "packages_providers_DownloadProvider_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: [
- "packages_providers_DownloadProvider_license",
- ],
-}
-
android_test {
name: "DownloadPublicApiAccessTests",
test_suites: ["device-tests"],
diff --git a/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java b/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java
index 4afe3b7..a505632 100644
--- a/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java
@@ -127,6 +127,11 @@
public Bundle call(String method, String request, Bundle args) {
return new Bundle();
}
+
+ @Override
+ public IBinder getIContentProviderBinder() {
+ return new Binder();
+ }
}
/**
@@ -208,7 +213,6 @@
setupService();
Helpers.setSystemFacade(mSystemFacade);
- cleanUpDownloads();
mSystemFacade.setUp();
assertDatabaseEmpty(); // ensure we're not messing with real data
assertDatabaseSecureAgainstBadSelection();
diff --git a/tests/src/com/android/providers/downloads/FakeInputStream.java b/tests/src/com/android/providers/downloads/FakeInputStream.java
index 65724ac..3f68e31 100644
--- a/tests/src/com/android/providers/downloads/FakeInputStream.java
+++ b/tests/src/com/android/providers/downloads/FakeInputStream.java
@@ -16,6 +16,8 @@
package com.android.providers.downloads;
+import libcore.util.ArrayUtils;
+
import java.io.InputStream;
/**
@@ -41,7 +43,7 @@
@Override
public int read(byte[] buffer, int offset, int length) {
- throwsIfOutOfBounds(buffer.length, offset, length);
+ ArrayUtils.throwsIfOutOfBounds(buffer.length, offset, length);
if (length > mRemaining) {
length = (int) mRemaining;
@@ -54,15 +56,4 @@
return length;
}
}
-
- private static void throwsIfOutOfBounds(int len, int offset, int count) {
- if (len < 0) {
- throw new ArrayIndexOutOfBoundsException("Negative length: " + len);
- }
-
- if ((offset | count) < 0 || offset > len - count) {
- throw new ArrayIndexOutOfBoundsException(
- "length=" + len + "; regionStart=" + offset + "; regionLength=" + count);
- }
- }
}
diff --git a/tests/src/com/android/providers/downloads/FakeSystemFacade.java b/tests/src/com/android/providers/downloads/FakeSystemFacade.java
index fadcd36..d11e0ac 100644
--- a/tests/src/com/android/providers/downloads/FakeSystemFacade.java
+++ b/tests/src/com/android/providers/downloads/FakeSystemFacade.java
@@ -93,14 +93,25 @@
}
@Override
+ public NetworkInfo getNetworkInfo(Network network, int uid, boolean ignoreBlocked) {
+ if (mActiveNetworkType == null) {
+ return null;
+ } else {
+ final NetworkInfo info = new NetworkInfo(mActiveNetworkType, 0, null, null);
+ info.setDetailedState(DetailedState.CONNECTED, null, null);
+ return info;
+ }
+ }
+
+ @Override
public NetworkCapabilities getNetworkCapabilities(Network network) {
if (mActiveNetworkType == null) {
return null;
} else {
- final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder();
- if (!mIsMetered) builder.addCapability(NET_CAPABILITY_NOT_METERED);
- if (!mIsRoaming) builder.addCapability(NET_CAPABILITY_NOT_ROAMING);
- return builder.build();
+ final NetworkCapabilities caps = new NetworkCapabilities();
+ caps.setCapability(NET_CAPABILITY_NOT_METERED, !mIsMetered);
+ caps.setCapability(NET_CAPABILITY_NOT_ROAMING, !mIsRoaming);
+ return caps;
}
}
diff --git a/tests/src/com/android/providers/downloads/FsHelper.java b/tests/src/com/android/providers/downloads/FsHelper.java
deleted file mode 100644
index d83827c..0000000
--- a/tests/src/com/android/providers/downloads/FsHelper.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.android.providers.downloads;
-
-import java.io.File;
-
-public class FsHelper {
-
- /**
- * Deletes all files under a given directory. Deliberately ignores errors, on the assumption
- * that test cleanup is only supposed to be best-effort.
- *
- * @param directory directory to clear its contents
- */
- public static void deleteContents(File directory) {
- File[] files = directory.listFiles();
- if (files != null) {
- for (File file : files) {
- if (file.isDirectory()) {
- deleteContents(file);
- }
- file.delete();
- }
- }
- }
-}
diff --git a/tests/src/com/android/providers/downloads/HelpersTest.java b/tests/src/com/android/providers/downloads/HelpersTest.java
index 08c0b13..793a157 100644
--- a/tests/src/com/android/providers/downloads/HelpersTest.java
+++ b/tests/src/com/android/providers/downloads/HelpersTest.java
@@ -35,11 +35,13 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.AppOpsManager;
import android.content.ContentProvider;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.MatrixCursor;
import android.net.Uri;
+import android.os.Binder;
import android.os.Environment;
import android.os.Process;
import android.provider.Downloads;
@@ -53,7 +55,6 @@
import java.io.File;
import java.util.Arrays;
-import java.util.function.BiConsumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -62,6 +63,7 @@
*/
@SmallTest
public class HelpersTest extends AndroidTestCase {
+ private static final String TAG = "DownloadManagerHelpersTest";
private final static int TEST_UID1 = 11111;
private final static int TEST_UID2 = 11112;
@@ -72,15 +74,16 @@
@Override
protected void setUp() throws Exception {
super.setUp();
-
+ // This is necessary for mockito to work
+ System.setProperty("dexmaker.dexcache", mContext.getCacheDir().toString());
mMockitoHelper.setUp(getClass());
}
@Override
protected void tearDown() throws Exception {
mMockitoHelper.tearDown();
- FsHelper.deleteContents(getContext().getFilesDir());
- FsHelper.deleteContents(getContext().getCacheDir());
+ IoUtils.deleteContents(getContext().getFilesDir());
+ IoUtils.deleteContents(getContext().getCacheDir());
super.tearDown();
}
@@ -149,6 +152,363 @@
"/storage/AAAA-FFFF/Download/dir/bar.html"));
}
+ public void testCheckDestinationFilePathRestrictions_noPermission() throws Exception {
+ // Downloading to our own private app directory should always be allowed, even for
+ // permission-less app
+ checkDestinationFilePathRestrictions_noPermission(
+ "/storage/emulated/0/Android/data/DownloadManagerHelpersTest/test",
+ /* isLegacyMode */ false);
+ checkDestinationFilePathRestrictions_noPermission(
+ "/storage/emulated/0/Android/data/DownloadManagerHelpersTest/test",
+ /* isLegacyMode */ true);
+ checkDestinationFilePathRestrictions_noPermission(
+ "/storage/emulated/0/Android/obb/DownloadManagerHelpersTest/test",
+ /* isLegacyMode */ false);
+ checkDestinationFilePathRestrictions_noPermission(
+ "/storage/emulated/0/Android/obb/DownloadManagerHelpersTest/test",
+ /* isLegacyMode */ true);
+ checkDestinationFilePathRestrictions_noPermission(
+ "/storage/emulated/0/Android/media/DownloadManagerHelpersTest/test",
+ /* isLegacyMode */ false);
+ checkDestinationFilePathRestrictions_noPermission(
+ "/storage/emulated/0/Android/media/DownloadManagerHelpersTest/test",
+ /* isLegacyMode */ true);
+
+ // All apps can write to Environment.STANDARD_DIRECTORIES
+ checkDestinationFilePathRestrictions_noPermission("/storage/emulated/0/Pictures/test",
+ /* isLegacyMode */ false);
+ checkDestinationFilePathRestrictions_noPermission("/storage/emulated/0/Download/test",
+ /* isLegacyMode */ false);
+ checkDestinationFilePathRestrictions_noPermission("/storage/emulated/0/Pictures/test",
+ /* isLegacyMode */ true);
+ checkDestinationFilePathRestrictions_noPermission("/storage/emulated/0/Download/test",
+ /* isLegacyMode */ true);
+
+ // Apps can never access other app's private directories (Android/data, Android/obb) paths
+ // (unless they are installers in which case they can access Android/obb paths)
+ try {
+ checkDestinationFilePathRestrictions_noPermission(
+ "/storage/emulated/0/Android/data/foo/test", /* isLegacyMode */ false);
+ fail("Expected SecurityException as caller cannot access other app's private packages");
+ } catch (SecurityException expected) {
+ }
+
+ try {
+ checkDestinationFilePathRestrictions_noPermission(
+ "/storage/emulated/0/Android/data/foo/test", /* isLegacyMode */ true);
+ fail("Expected SecurityException as caller cannot access other app's private packages"
+ + " even in legacy mode");
+ } catch (SecurityException expected) {
+ }
+
+ try {
+ checkDestinationFilePathRestrictions_noPermission(
+ "/storage/emulated/0/Android/obb/foo/test", /* isLegacyMode */ false);
+ fail("Expected SecurityException as caller cannot access other app's private packages");
+ } catch (SecurityException expected) {
+ }
+
+ try {
+ checkDestinationFilePathRestrictions_noPermission(
+ "/storage/emulated/0/Android/obb/foo/test", /* isLegacyMode */ true);
+ fail("Expected SecurityException as caller cannot access other app's private packages"
+ + " even in legacy mode");
+ } catch (SecurityException expected) {
+ }
+
+ // Non-legacy apps can never access Android/ or Android/media dirs for other packages.
+ try {
+ checkDestinationFilePathRestrictions_noPermission("/storage/emulated/0/Android/",
+ /* isLegacyMode */ false);
+ fail("Expected SecurityException as caller cannot write to Android dir");
+ } catch (SecurityException expected) {
+ }
+
+ try {
+ checkDestinationFilePathRestrictions_noPermission(
+ "/storage/emulated/0/Android/media/", /* isLegacyMode */ false);
+ fail("Expected SecurityException as caller cannot write to Android dir");
+ } catch (SecurityException expected) {
+ }
+
+ try {
+ checkDestinationFilePathRestrictions_noPermission(
+ "/storage/emulated/0/Android/media/foo", /* isLegacyMode */ false);
+ fail("Expected SecurityException as caller cannot write to Android dir");
+ } catch (SecurityException expected) {
+ }
+
+ // Legacy apps require WRITE_EXTERNAL_STORAGE permission to access Android/ or Android/media
+ // dirs.
+ try {
+ checkDestinationFilePathRestrictions_noPermission("/storage/emulated/0/Android/",
+ /* isLegacyMode */ true);
+ fail("Expected SecurityException as caller cannot write to Android/ as it does not"
+ + " have WRITE_EXTERNAL_STORAGE permission");
+ } catch (SecurityException expected) {
+ }
+
+ try {
+ checkDestinationFilePathRestrictions_noPermission(
+ "/storage/emulated/0/Android/media/", /* isLegacyMode */ true);
+ fail("Expected SecurityException as caller cannot write to Android/ as it does not"
+ + " have WRITE_EXTERNAL_STORAGE permission");
+ } catch (SecurityException expected) {
+ }
+
+ try {
+ checkDestinationFilePathRestrictions_noPermission(
+ "/storage/emulated/0/Android/media/foo", /* isLegacyMode */ true);
+ fail("Expected SecurityException as caller cannot write to Android/media as it does not"
+ + " have WRITE_EXTERNAL_STORAGE permission");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ public void testCheckDestinationFilePathRestrictions_installer() throws Exception {
+ // Downloading to other obb dirs should be allowed as installer
+ checkDestinationFilePathRestrictions_installer("/storage/emulated/0/Android/obb/foo/test",
+ /* isLegacyMode */ false);
+ checkDestinationFilePathRestrictions_installer("/storage/emulated/0/Android/obb/foo/test",
+ /* isLegacyMode */ true);
+
+ // Installer apps can not access other app's Android/data private dirs
+ try {
+ checkDestinationFilePathRestrictions_installer(
+ "/storage/emulated/0/Android/data/foo/test", /* isLegacyMode */ false);
+ fail("Expected SecurityException as caller cannot access other app's private packages");
+ } catch (SecurityException expected) {
+ }
+
+ try {
+ checkDestinationFilePathRestrictions_installer(
+ "/storage/emulated/0/Android/data/foo/test", /* isLegacyMode */ true);
+ fail("Expected SecurityException as caller cannot access other app's private packages"
+ + " even in legacy mode");
+ } catch (SecurityException expected) {
+ }
+
+ // Non-legacy apps can never access Android/ or Android/media dirs for other packages.
+ try {
+ checkDestinationFilePathRestrictions_installer("/storage/emulated/0/Android/",
+ /* isLegacyMode */ false);
+ fail("Expected SecurityException as caller cannot write to Android dir");
+ } catch (SecurityException expected) {
+ }
+
+ try {
+ checkDestinationFilePathRestrictions_installer("/storage/emulated/0/Android/media/",
+ /* isLegacyMode */ false);
+ fail("Expected SecurityException as caller cannot write to Android dir");
+ } catch (SecurityException expected) {
+ }
+
+ try {
+ checkDestinationFilePathRestrictions_installer("/storage/emulated/0/Android/media/foo",
+ /* isLegacyMode */ false);
+ fail("Expected SecurityException as caller cannot write to Android dir");
+ } catch (SecurityException expected) {
+ }
+
+ // Legacy apps require WRITE_EXTERNAL_STORAGE permission to access Android/ or Android/media
+ // dirs.
+ try {
+ checkDestinationFilePathRestrictions_installer("/storage/emulated/0/Android/",
+ /* isLegacyMode */ true);
+ fail("Expected SecurityException as caller cannot write to Android/ as it does not"
+ + " have WRITE_EXTERNAL_STORAGE permission");
+ } catch (SecurityException expected) {
+ }
+
+ try {
+ checkDestinationFilePathRestrictions_installer("/storage/emulated/0/Android/media/",
+ /* isLegacyMode */ true);
+ fail("Expected SecurityException as caller cannot write to Android/ as it does not"
+ + " have WRITE_EXTERNAL_STORAGE permission");
+ } catch (SecurityException expected) {
+ }
+
+ try {
+ checkDestinationFilePathRestrictions_installer("/storage/emulated/0/Android/media/foo",
+ /* isLegacyMode */ true);
+ fail("Expected SecurityException as caller cannot write to Android/media as it does not"
+ + " have WRITE_EXTERNAL_STORAGE permission");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ public void testCheckDestinationFilePathRestrictions_WES() throws Exception {
+ // Apps with WRITE_EXTERNAL_STORAGE can not access other app's private dirs
+ // (Android/data and Android/obb paths)
+ try {
+ checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/data/foo/test",
+ /* isLegacyMode */ false);
+ fail("Expected SecurityException as caller cannot access other app's private packages");
+ } catch (SecurityException expected) {
+ }
+
+ try {
+ checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/data/foo/test",
+ /* isLegacyMode */ true);
+ fail("Expected SecurityException as caller cannot access other app's private packages"
+ + " even in legacy mode");
+ } catch (SecurityException expected) {
+ }
+
+ try {
+ checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/obb/foo/test",
+ /* isLegacyMode */ false);
+ fail("Expected SecurityException as caller cannot access other app's private packages");
+ } catch (SecurityException expected) {
+ }
+
+ try {
+ checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/obb/foo/test",
+ /* isLegacyMode */ true);
+ fail("Expected SecurityException as caller cannot access other app's private packages"
+ + " even in legacy mode");
+ } catch (SecurityException expected) {
+ }
+
+ // Non-legacy apps can never access Android/ or Android/media dirs for other packages.
+ try {
+ checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/",
+ /* isLegacyMode */ false);
+ fail("Expected SecurityException as caller cannot write to Android dir");
+ } catch (SecurityException expected) {
+ }
+
+ try {
+ checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/media/",
+ /* isLegacyMode */ false);
+ fail("Expected SecurityException as caller cannot write to Android dir");
+ } catch (SecurityException expected) {
+ }
+
+ try {
+ checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/media/foo",
+ /* isLegacyMode */ false);
+ fail("Expected SecurityException as caller cannot write to Android dir");
+ } catch (SecurityException expected) {
+ }
+
+ // Legacy apps with WRITE_EXTERNAL_STORAGE can access shared storage file path including
+ // Android/ and Android/media dirs
+ checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Pictures/test",
+ /* isLegacyMode */ true);
+ checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Download/test",
+ /* isLegacyMode */ true);
+ checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/",
+ /* isLegacyMode */ true);
+ checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/media/",
+ /* isLegacyMode */ true);
+ checkDestinationFilePathRestrictions_WES("/storage/emulated/0/Android/media/foo",
+ /* isLegacyMode */ true);
+ }
+
+ private void checkDestinationFilePathRestrictions_noPermission(String filePath,
+ boolean isLegacyMode) {
+ final Context mockContext = mock(Context.class);
+ when(mockContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.REQUEST_INSTALL_PACKAGES))
+ .thenReturn(PackageManager.PERMISSION_DENIED);
+ when(mockContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
+ .thenReturn(PackageManager.PERMISSION_DENIED);
+ final String callingAttributionTag = "test";
+ final AppOpsManager mockAppOpsManager = mock(AppOpsManager.class);
+ final String callingPackage = TAG;
+ when(mockAppOpsManager.noteOp(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES,
+ Binder.getCallingUid(), callingPackage, null, "obb_download"))
+ .thenReturn(AppOpsManager.MODE_ERRORED);
+ when(mockAppOpsManager.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
+ callingPackage, Binder.getCallingUid(), callingAttributionTag, null))
+ .thenReturn(AppOpsManager.MODE_ERRORED);
+ File file = new File(filePath);
+
+ Helpers.checkDestinationFilePathRestrictions(file, callingPackage, mockContext,
+ mockAppOpsManager, callingAttributionTag, isLegacyMode,
+ /* allowDownloadsDirOnly */ false);
+ }
+
+ private void checkDestinationFilePathRestrictions_installer(String filePath,
+ boolean isLegacyMode) throws Exception {
+ final Context mockContext = mock(Context.class);
+ when(mockContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.REQUEST_INSTALL_PACKAGES))
+ .thenReturn(PackageManager.PERMISSION_GRANTED);
+ when(mockContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
+ .thenReturn(PackageManager.PERMISSION_DENIED);
+
+ final String callingAttributionTag = "test";
+ final AppOpsManager mockAppOpsManager = mock(AppOpsManager.class);
+ final String callingPackage = TAG;
+ when(mockAppOpsManager.noteOp(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES,
+ Binder.getCallingUid(), callingPackage, null, "obb_download"))
+ .thenReturn(AppOpsManager.MODE_ALLOWED);
+ when(mockAppOpsManager.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
+ callingPackage, Binder.getCallingUid(), callingAttributionTag, null))
+ .thenReturn(AppOpsManager.MODE_ERRORED);
+ File file = new File(filePath);
+
+ Helpers.checkDestinationFilePathRestrictions(file, callingPackage, mockContext,
+ mockAppOpsManager, callingAttributionTag, isLegacyMode,
+ /* allowDownloadsDirOnly */ false);
+ }
+
+ private void checkDestinationFilePathRestrictions_WES(String filePath, boolean isLegacyMode)
+ throws Exception {
+ final Context mockContext = mock(Context.class);
+ when(mockContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
+ .thenReturn(PackageManager.PERMISSION_GRANTED);
+ when(mockContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.REQUEST_INSTALL_PACKAGES))
+ .thenReturn(PackageManager.PERMISSION_DENIED);
+
+ final AppOpsManager mockAppOpsManager = mock(AppOpsManager.class);
+ final String callingAttributionTag = "test";
+ final String callingPackage = TAG;
+ when(mockAppOpsManager.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
+ callingPackage, Binder.getCallingUid(), callingAttributionTag, null))
+ .thenReturn(AppOpsManager.MODE_ALLOWED);
+ when(mockAppOpsManager.noteOp(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES,
+ Binder.getCallingUid(), callingPackage, null, "obb_download"))
+ .thenReturn(AppOpsManager.MODE_ERRORED);
+ File file = new File(filePath);
+
+ Helpers.checkDestinationFilePathRestrictions(file, callingPackage, mockContext,
+ mockAppOpsManager, callingAttributionTag, isLegacyMode,
+ /* allowDownloadsDirOnly */ false);
+ }
+
+ public void testIsFileInPrivateExternalAndroidDirs() throws Exception {
+ assertTrue(isFileInPrivateExternalAndroidDirs(
+ "/storage/emulated/0/Android/data/com.example"));
+ assertTrue(isFileInPrivateExternalAndroidDirs(
+ "/storage/emulated/0/Android/data/com.example/colors.txt"));
+ assertTrue(isFileInPrivateExternalAndroidDirs(
+ "/storage/emulated/0/Android/obb/com.example/file.mp4"));
+ assertTrue(isFileInPrivateExternalAndroidDirs(
+ "/storage/AAAA-FFFF/Android/obb/com.example/file.mp4"));
+
+ assertFalse(isFileInPrivateExternalAndroidDirs("/storage/emulated/0/Android/"));
+ assertFalse(isFileInPrivateExternalAndroidDirs("/storage/AAAA-FFFF/Android/"));
+ assertFalse(isFileInPrivateExternalAndroidDirs(
+ "/storage/emulated/0/Android/media/com.example/file.mp4"));
+ assertFalse(isFileInPrivateExternalAndroidDirs(
+ "/storage/AAAA-FFFF/Android/media/com.example/file.mp4"));
+ assertFalse(isFileInPrivateExternalAndroidDirs("/storage/emulated/0/Download/foo.pdf"));
+ assertFalse(isFileInPrivateExternalAndroidDirs(
+ "/storage/emulated/0/Download/dir/bar.html"));
+ assertFalse(isFileInPrivateExternalAndroidDirs("/storage/AAAA-FFFF/Download/dir/bar.html"));
+ }
+
+ private static boolean isFileInPrivateExternalAndroidDirs(String filePath) {
+ return Helpers.isFileInPrivateExternalAndroidDirs(new File(filePath));
+ }
+
public void testIsFilenameValidinKnownPublicDir() throws Exception {
assertTrue(Helpers.isFilenameValidInKnownPublicDir(
"/storage/emulated/0/Download/dir/file.txt"));
diff --git a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
index f45e102..0652f24 100644
--- a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
@@ -52,7 +52,7 @@
import android.test.suitebuilder.annotation.Suppress;
import android.text.format.DateUtils;
-import androidx.test.filters.FlakyTest;
+import libcore.io.IoUtils;
import com.google.mockwebserver.MockResponse;
import com.google.mockwebserver.RecordedRequest;
@@ -91,7 +91,7 @@
Environment.DIRECTORY_DOWNLOADS)
+ File.separator + "download_manager_functional_test");
if (mTestDirectory.exists()) {
- FsHelper.deleteContents(mTestDirectory);
+ IoUtils.deleteContents(mTestDirectory);
} else {
mTestDirectory.mkdir();
}
@@ -100,7 +100,7 @@
@Override
protected void tearDown() throws Exception {
if (mTestDirectory != null && mTestDirectory.exists()) {
- FsHelper.deleteContents(mTestDirectory);
+ IoUtils.deleteContents(mTestDirectory);
mTestDirectory.delete();
}
super.tearDown();
@@ -691,7 +691,6 @@
download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
}
- @FlakyTest(bugId = 177499952)
public void testManyInterruptions() throws Exception {
final int length = FILE_CONTENT.length();
for (int i = 0; i < length; i++) {
diff --git a/ui/Android.bp b/ui/Android.bp
index 04606c2..5192ead 100644
--- a/ui/Android.bp
+++ b/ui/Android.bp
@@ -13,17 +13,6 @@
// limitations under the License.
//
-package {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "packages_providers_DownloadProvider_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: [
- "packages_providers_DownloadProvider_license",
- ],
-}
-
android_app {
name: "DownloadProviderUi",
diff --git a/ui/AndroidManifest.xml b/ui/AndroidManifest.xml
index 8eae933..d08ec19 100644
--- a/ui/AndroidManifest.xml
+++ b/ui/AndroidManifest.xml
@@ -1,30 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
-
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.providers.downloads.ui"
- android:sharedUserId="android.media">
+ package="com.android.providers.downloads.ui"
+ android:sharedUserId="android.media">
- <uses-permission android:name="android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS"/>
- <uses-permission android:name="android.permission.ACCESS_ALL_DOWNLOADS"/>
+ <uses-permission android:name="android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS" />
+ <uses-permission android:name="android.permission.ACCESS_ALL_DOWNLOADS" />
<application android:process="android.process.media"
- android:label="@string/app_label"
- android:icon="@mipmap/ic_launcher_download"
- android:hardwareAccelerated="true"
- android:supportsRtl="true"
- android:requiredForAllUsers="true"
- android:usesCleartextTraffic="true">
+ android:label="@string/app_label"
+ android:icon="@mipmap/ic_launcher_download"
+ android:hardwareAccelerated="true"
+ android:supportsRtl="true"
+ android:requiredForAllUsers="true"
+ android:usesCleartextTraffic="true">
- <activity android:name=".TrampolineActivity"
- android:theme="@android:style/Theme.Translucent.NoTitleBar"
- android:permission="android.permission.MANAGE_DOCUMENTS"
- android:exported="true">
+ <activity
+ android:name=".TrampolineActivity"
+ android:theme="@android:style/Theme.Translucent.NoTitleBar"
+ android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
- <action android:name="android.provider.action.MANAGE_DOCUMENT"/>
- <category android:name="android.intent.category.DEFAULT"/>
- <data android:scheme="content"
- android:host="com.android.providers.downloads.documents"
- android:mimeType="*/*"/>
+ <action android:name="android.provider.action.MANAGE_DOCUMENT" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data
+ android:scheme="content"
+ android:host="com.android.providers.downloads.documents"
+ android:mimeType="*/*" />
</intent-filter>
</activity>
</application>