diff options
Diffstat (limited to 'src/com/android/providers/downloads/Helpers.java')
-rw-r--r-- | src/com/android/providers/downloads/Helpers.java | 347 |
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]; } } |