summaryrefslogtreecommitdiffstats
path: root/src/com/android/providers/downloads/Helpers.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/providers/downloads/Helpers.java')
-rw-r--r--src/com/android/providers/downloads/Helpers.java347
1 files changed, 93 insertions, 254 deletions
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index 963ca9da..226fb481 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -16,15 +16,20 @@
package com.android.providers.downloads;
-import static android.os.Environment.buildExternalStorageAppCacheDirs;
-import static android.os.Environment.buildExternalStorageAppFilesDirs;
+import static android.os.Environment.buildExternalStorageAppDataDirs;
import static android.os.Environment.buildExternalStorageAppMediaDirs;
import static android.os.Environment.buildExternalStorageAppObbDirs;
+import static android.os.Environment.buildExternalStoragePublicDirs;
+import static android.provider.Downloads.Impl.DESTINATION_EXTERNAL;
+import static android.provider.Downloads.Impl.DESTINATION_FILE_URI;
+import static android.provider.Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD;
import static android.provider.Downloads.Impl.FLAG_REQUIRES_CHARGING;
import static android.provider.Downloads.Impl.FLAG_REQUIRES_DEVICE_IDLE;
import static com.android.providers.downloads.Constants.TAG;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.ComponentName;
@@ -43,14 +48,21 @@ import android.os.storage.StorageVolume;
import android.provider.Downloads;
import android.text.TextUtils;
import android.util.Log;
+import android.util.LongSparseArray;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import android.webkit.MimeTypeMap;
+import com.android.internal.util.ArrayUtils;
+
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Random;
import java.util.Set;
+import java.util.function.BiConsumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -64,6 +76,12 @@ public class Helpers {
private static final Pattern CONTENT_DISPOSITION_PATTERN =
Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
+ private static final Pattern PATTERN_ANDROID_DIRS =
+ Pattern.compile("(?i)^/storage/[^/]+(?:/[0-9]+)?/Android/(?:data|obb|media)/.+");
+
+ private static final Pattern PATTERN_PUBLIC_DIRS =
+ Pattern.compile("(?i)^/storage/[^/]+(?:/[0-9]+)?/([^/]+)/.+");
+
private static final Object sUniqueLock = new Object();
private static HandlerThread sAsyncHandlerThread;
@@ -144,7 +162,7 @@ public class Helpers {
// When this download will show a notification, run with a higher
// priority, since it's effectively a foreground service
if (info.isVisible()) {
- builder.setPriority(JobInfo.PRIORITY_FOREGROUND_APP);
+ builder.setPriority(JobInfo.PRIORITY_FOREGROUND_SERVICE);
builder.setFlags(JobInfo.FLAG_WILL_BE_FOREGROUND);
}
@@ -472,6 +490,10 @@ public class Helpers {
throw new IOException("Failed to generate an available filename");
}
+ public static boolean isFileInExternalAndroidDirs(String filePath) {
+ return PATTERN_ANDROID_DIRS.matcher(filePath).matches();
+ }
+
static boolean isFilenameValid(Context context, File file) {
return isFilenameValid(context, file, true);
}
@@ -488,9 +510,8 @@ public class Helpers {
static boolean isFilenameValidInExternalPackage(Context context, File file,
String packageName) {
try {
- if (containsCanonical(buildExternalStorageAppFilesDirs(packageName), file) ||
+ if (containsCanonical(buildExternalStorageAppDataDirs(packageName), file) ||
containsCanonical(buildExternalStorageAppObbDirs(packageName), file) ||
- containsCanonical(buildExternalStorageAppCacheDirs(packageName), file) ||
containsCanonical(buildExternalStorageAppMediaDirs(packageName), file)) {
return true;
}
@@ -499,7 +520,33 @@ public class Helpers {
return false;
}
- Log.w(TAG, "Path appears to be invalid: " + file);
+ return false;
+ }
+
+ static boolean isFilenameValidInPublicDownloadsDir(File file) {
+ try {
+ if (containsCanonical(buildExternalStoragePublicDirs(
+ Environment.DIRECTORY_DOWNLOADS), file)) {
+ return true;
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to resolve canonical path: " + e);
+ return false;
+ }
+
+ return false;
+ }
+
+ @com.android.internal.annotations.VisibleForTesting
+ public static boolean isFilenameValidInKnownPublicDir(@Nullable String filePath) {
+ if (filePath == null) {
+ return false;
+ }
+ final Matcher matcher = PATTERN_PUBLIC_DIRS.matcher(filePath);
+ if (matcher.matches()) {
+ final String publicDir = matcher.group(1);
+ return ArrayUtils.contains(Environment.STANDARD_DIRECTORIES, publicDir);
+ }
return false;
}
@@ -529,7 +576,6 @@ public class Helpers {
return false;
}
- Log.w(TAG, "Path appears to be invalid: " + file);
return false;
}
@@ -581,264 +627,57 @@ public class Helpers {
}
}
- /**
- * Checks whether this looks like a legitimate selection parameter
- */
- public static void validateSelection(String selection, Set<String> allowedColumns) {
- try {
- if (selection == null || selection.isEmpty()) {
- return;
- }
- Lexer lexer = new Lexer(selection, allowedColumns);
- parseExpression(lexer);
- if (lexer.currentToken() != Lexer.TOKEN_END) {
- throw new IllegalArgumentException("syntax error");
- }
- } catch (RuntimeException ex) {
- if (Constants.LOGV) {
- Log.d(Constants.TAG, "invalid selection [" + selection + "] triggered " + ex);
- } else if (false) {
- Log.d(Constants.TAG, "invalid selection triggered " + ex);
+ public static void handleRemovedUidEntries(@NonNull Context context, @NonNull Cursor cursor,
+ @NonNull ArrayList<Long> idsToDelete, @NonNull ArrayList<Long> idsToOrphan,
+ @Nullable LongSparseArray<String> idsToGrantPermission) {
+ final SparseArray<String> knownUids = new SparseArray<>();
+ while (cursor.moveToNext()) {
+ final long downloadId = cursor.getLong(0);
+ final int uid = cursor.getInt(1);
+
+ final String ownerPackageName;
+ final int index = knownUids.indexOfKey(uid);
+ if (index >= 0) {
+ ownerPackageName = knownUids.valueAt(index);
+ } else {
+ ownerPackageName = getPackageForUid(context, uid);
+ knownUids.put(uid, ownerPackageName);
}
- throw ex;
- }
- }
+ if (ownerPackageName == null) {
+ final int destination = cursor.getInt(2);
+ final String filePath = cursor.getString(3);
- // expression <- ( expression ) | statement [AND_OR ( expression ) | statement] *
- // | statement [AND_OR expression]*
- private static void parseExpression(Lexer lexer) {
- for (;;) {
- // ( expression )
- if (lexer.currentToken() == Lexer.TOKEN_OPEN_PAREN) {
- lexer.advance();
- parseExpression(lexer);
- if (lexer.currentToken() != Lexer.TOKEN_CLOSE_PAREN) {
- throw new IllegalArgumentException("syntax error, unmatched parenthese");
+ if ((destination == DESTINATION_EXTERNAL
+ || destination == DESTINATION_FILE_URI
+ || destination == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
+ && isFilenameValidInKnownPublicDir(filePath)) {
+ idsToOrphan.add(downloadId);
+ } else {
+ idsToDelete.add(downloadId);
}
- lexer.advance();
- } else {
- // statement
- parseStatement(lexer);
- }
- if (lexer.currentToken() != Lexer.TOKEN_AND_OR) {
- break;
+ } else if (idsToGrantPermission != null) {
+ idsToGrantPermission.put(downloadId, ownerPackageName);
}
- lexer.advance();
}
}
- // statement <- COLUMN COMPARE VALUE
- // | COLUMN IS NULL
- private static void parseStatement(Lexer lexer) {
- // both possibilities start with COLUMN
- if (lexer.currentToken() != Lexer.TOKEN_COLUMN) {
- throw new IllegalArgumentException("syntax error, expected column name");
- }
- lexer.advance();
-
- // statement <- COLUMN COMPARE VALUE
- if (lexer.currentToken() == Lexer.TOKEN_COMPARE) {
- lexer.advance();
- if (lexer.currentToken() != Lexer.TOKEN_VALUE) {
- throw new IllegalArgumentException("syntax error, expected quoted string");
- }
- lexer.advance();
- return;
- }
-
- // statement <- COLUMN IS NULL
- if (lexer.currentToken() == Lexer.TOKEN_IS) {
- lexer.advance();
- if (lexer.currentToken() != Lexer.TOKEN_NULL) {
- throw new IllegalArgumentException("syntax error, expected NULL");
- }
- lexer.advance();
- return;
+ public static String buildQueryWithIds(ArrayList<Long> downloadIds) {
+ final StringBuilder queryBuilder = new StringBuilder(Downloads.Impl._ID + " in (");
+ final int size = downloadIds.size();
+ for (int i = 0; i < size; i++) {
+ queryBuilder.append(downloadIds.get(i));
+ queryBuilder.append((i == size - 1) ? ")" : ",");
}
-
- // didn't get anything good after COLUMN
- throw new IllegalArgumentException("syntax error after column name");
+ return queryBuilder.toString();
}
- /**
- * A simple lexer that recognizes the words of our restricted subset of SQL where clauses
- */
- private static class Lexer {
- public static final int TOKEN_START = 0;
- public static final int TOKEN_OPEN_PAREN = 1;
- public static final int TOKEN_CLOSE_PAREN = 2;
- public static final int TOKEN_AND_OR = 3;
- public static final int TOKEN_COLUMN = 4;
- public static final int TOKEN_COMPARE = 5;
- public static final int TOKEN_VALUE = 6;
- public static final int TOKEN_IS = 7;
- public static final int TOKEN_NULL = 8;
- public static final int TOKEN_END = 9;
-
- private final String mSelection;
- private final Set<String> mAllowedColumns;
- private int mOffset = 0;
- private int mCurrentToken = TOKEN_START;
- private final char[] mChars;
-
- public Lexer(String selection, Set<String> allowedColumns) {
- mSelection = selection;
- mAllowedColumns = allowedColumns;
- mChars = new char[mSelection.length()];
- mSelection.getChars(0, mChars.length, mChars, 0);
- advance();
- }
-
- public int currentToken() {
- return mCurrentToken;
- }
-
- public void advance() {
- char[] chars = mChars;
-
- // consume whitespace
- while (mOffset < chars.length && chars[mOffset] == ' ') {
- ++mOffset;
- }
-
- // end of input
- if (mOffset == chars.length) {
- mCurrentToken = TOKEN_END;
- return;
- }
-
- // "("
- if (chars[mOffset] == '(') {
- ++mOffset;
- mCurrentToken = TOKEN_OPEN_PAREN;
- return;
- }
-
- // ")"
- if (chars[mOffset] == ')') {
- ++mOffset;
- mCurrentToken = TOKEN_CLOSE_PAREN;
- return;
- }
-
- // "?"
- if (chars[mOffset] == '?') {
- ++mOffset;
- mCurrentToken = TOKEN_VALUE;
- return;
- }
-
- // "=" and "=="
- if (chars[mOffset] == '=') {
- ++mOffset;
- mCurrentToken = TOKEN_COMPARE;
- if (mOffset < chars.length && chars[mOffset] == '=') {
- ++mOffset;
- }
- return;
- }
-
- // ">" and ">="
- if (chars[mOffset] == '>') {
- ++mOffset;
- mCurrentToken = TOKEN_COMPARE;
- if (mOffset < chars.length && chars[mOffset] == '=') {
- ++mOffset;
- }
- return;
- }
-
- // "<", "<=" and "<>"
- if (chars[mOffset] == '<') {
- ++mOffset;
- mCurrentToken = TOKEN_COMPARE;
- if (mOffset < chars.length && (chars[mOffset] == '=' || chars[mOffset] == '>')) {
- ++mOffset;
- }
- return;
- }
-
- // "!="
- if (chars[mOffset] == '!') {
- ++mOffset;
- mCurrentToken = TOKEN_COMPARE;
- if (mOffset < chars.length && chars[mOffset] == '=') {
- ++mOffset;
- return;
- }
- throw new IllegalArgumentException("Unexpected character after !");
- }
-
- // columns and keywords
- // first look for anything that looks like an identifier or a keyword
- // and then recognize the individual words.
- // no attempt is made at discarding sequences of underscores with no alphanumeric
- // characters, even though it's not clear that they'd be legal column names.
- if (isIdentifierStart(chars[mOffset])) {
- int startOffset = mOffset;
- ++mOffset;
- while (mOffset < chars.length && isIdentifierChar(chars[mOffset])) {
- ++mOffset;
- }
- String word = mSelection.substring(startOffset, mOffset);
- if (mOffset - startOffset <= 4) {
- if (word.equals("IS")) {
- mCurrentToken = TOKEN_IS;
- return;
- }
- if (word.equals("OR") || word.equals("AND")) {
- mCurrentToken = TOKEN_AND_OR;
- return;
- }
- if (word.equals("NULL")) {
- mCurrentToken = TOKEN_NULL;
- return;
- }
- }
- if (mAllowedColumns.contains(word)) {
- mCurrentToken = TOKEN_COLUMN;
- return;
- }
- throw new IllegalArgumentException("unrecognized column or keyword: " + word);
- }
-
- // quoted strings
- if (chars[mOffset] == '\'') {
- ++mOffset;
- while (mOffset < chars.length) {
- if (chars[mOffset] == '\'') {
- if (mOffset + 1 < chars.length && chars[mOffset + 1] == '\'') {
- ++mOffset;
- } else {
- break;
- }
- }
- ++mOffset;
- }
- if (mOffset == chars.length) {
- throw new IllegalArgumentException("unterminated string");
- }
- ++mOffset;
- mCurrentToken = TOKEN_VALUE;
- return;
- }
-
- // anything we don't recognize
- throw new IllegalArgumentException("illegal character: " + chars[mOffset]);
- }
-
- private static final boolean isIdentifierStart(char c) {
- return c == '_' ||
- (c >= 'A' && c <= 'Z') ||
- (c >= 'a' && c <= 'z');
- }
-
- private static final boolean isIdentifierChar(char c) {
- return c == '_' ||
- (c >= 'A' && c <= 'Z') ||
- (c >= 'a' && c <= 'z') ||
- (c >= '0' && c <= '9');
+ public static String getPackageForUid(Context context, int uid) {
+ String[] packages = context.getPackageManager().getPackagesForUid(uid);
+ if (packages == null || packages.length == 0) {
+ return null;
}
+ // For permission related purposes, any package belonging to the given uid should work.
+ return packages[0];
}
}