/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.novoda.downloadmanager.lib;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.UriMatcher;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.os.Binder;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.provider.OpenableColumns;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.novoda.downloadmanager.lib.logger.LLog;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Allows application to interact with the download manager.
*/
public final class DownloadProvider extends ContentProvider {
/**
* Added so we can use our own ContentProvider
*/
public static final String AUTHORITY = Reflector.reflectAuthority();
/**
* Database filename
*/
private static final String DB_NAME = "downloads.db";
/**
* MIME type for the entire download list
*/
private static final String DOWNLOAD_LIST_TYPE = "vnd.android.cursor.dir/download";
/**
* MIME type for an individual download
*/
private static final String DOWNLOAD_TYPE = "vnd.android.cursor.item/download";
/**
* MIME type for the entire batch list
*/
private static final String BATCH_LIST_TYPE = "vnd.android.cursor.dir/batch";
/**
* MIME type for an individual batch
*/
private static final String BATCH_TYPE = "vnd.android.cursor.item/batch";
/**
* MIME type for the list of download by batch
*/
private static final String DOWNLOADS_BY_BATCH_TYPE = "vnd.android.cursor.dir/download_by_batch";
/**
* URI matcher used to recognize URIs sent by applications
*/
private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
/**
* URI matcher constant for the URI of all downloads belonging to the calling UID
*/
private static final int MY_DOWNLOADS = 1;
/**
* URI matcher constant for the URI of an individual download belonging to the calling UID
*/
private static final int MY_DOWNLOADS_ID = 2;
/**
* URI matcher constant for the URI of all downloads in the system
*/
private static final int ALL_DOWNLOADS = 3;
/**
* URI matcher constant for the URI of an individual download
*/
private static final int ALL_DOWNLOADS_ID = 4;
/**
* URI matcher constant for the URI of a download's request headers
*/
private static final int REQUEST_HEADERS_URI = 5;
/**
* URI matcher constant for the public URI returned by
* {@link DownloadManager#getUriForDownloadedFile(long)} if the given downloaded file
* is publicly accessible.
*/
private static final int PUBLIC_DOWNLOAD_ID = 6;
/**
* URI matcher constant for the URI of a download's request headers
*/
private static final int BATCHES = 7;
/**
* URI matcher constant for the URI of a download's request headers
*/
private static final int BATCHES_ID = 8;
/**
* URI matcher constant for the URI of downloads with their batch data
*/
private static final int DOWNLOADS_BY_BATCH = 9;
private static final String[] APP_READABLE_COLUMNS_ARRAY = new String[]{
DownloadContract.Downloads._ID,
DownloadContract.Downloads.COLUMN_APP_DATA,
DownloadContract.Downloads.COLUMN_DATA,
DownloadContract.Downloads.COLUMN_MIME_TYPE,
DownloadContract.Downloads.COLUMN_DESTINATION,
DownloadContract.Downloads.COLUMN_CONTROL,
DownloadContract.Downloads.COLUMN_STATUS,
DownloadContract.Downloads.COLUMN_LAST_MODIFICATION,
DownloadContract.Downloads.COLUMN_NOTIFICATION_CLASS,
DownloadContract.Downloads.COLUMN_TOTAL_BYTES,
DownloadContract.Downloads.COLUMN_CURRENT_BYTES,
DownloadContract.Downloads.COLUMN_URI,
DownloadContract.Downloads.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI,
DownloadContract.Downloads.COLUMN_FILE_NAME_HINT,
DownloadContract.Downloads.COLUMN_MEDIAPROVIDER_URI,
DownloadContract.Downloads.COLUMN_DELETED,
DownloadContract.Downloads.COLUMN_NOTIFICATION_EXTRAS,
DownloadContract.Downloads.COLUMN_BATCH_ID,
DownloadContract.Downloads.COLUMN_ALWAYS_RESUME,
DownloadContract.Downloads.COLUMN_ALLOW_TAR_UPDATES,
DownloadContract.Batches._ID,
DownloadContract.Batches.COLUMN_STATUS,
DownloadContract.Batches.COLUMN_TITLE,
DownloadContract.Batches.COLUMN_DESCRIPTION,
DownloadContract.Batches.COLUMN_BIG_PICTURE,
DownloadContract.Batches.COLUMN_VISIBILITY,
DownloadContract.BatchesWithSizes.COLUMN_TOTAL_BYTES,
DownloadContract.BatchesWithSizes.COLUMN_CURRENT_BYTES,
OpenableColumns.DISPLAY_NAME,
OpenableColumns.SIZE,
};
private static final Set<String> APP_READABLE_COLUMNS_SET;
private static final Map<String, String> COLUMNS_MAP;
private static final List<String> DOWNLOAD_MANAGER_COLUMNS_LIST = Arrays.asList(DownloadManager.UNDERLYING_COLUMNS);
private final DownloadsUriProvider downloadsUriProvider;
/**
* Different base URIs that could be used to access an individual download
*/
private final Uri[] baseUris;
/**
* The database that lies underneath this content provider
*/
private SQLiteOpenHelper openHelper;
/**
* List of uids that can access the downloads
*/
private int systemUid = -1;
private int defcontaineruid = -1;
private File downloadsDataDir;
// @VisibleForTesting
SystemFacade systemFacade;
static {
URI_MATCHER.addURI(AUTHORITY, "my_downloads", MY_DOWNLOADS);
URI_MATCHER.addURI(AUTHORITY, "my_downloads/#", MY_DOWNLOADS_ID);
URI_MATCHER.addURI(AUTHORITY, "all_downloads", ALL_DOWNLOADS);
URI_MATCHER.addURI(AUTHORITY, "all_downloads/#", ALL_DOWNLOADS_ID);
URI_MATCHER.addURI(AUTHORITY, "batches", BATCHES);
URI_MATCHER.addURI(AUTHORITY, "batches/#", BATCHES_ID);
URI_MATCHER.addURI(AUTHORITY, "downloads_by_batch", DOWNLOADS_BY_BATCH);
URI_MATCHER.addURI(AUTHORITY, "my_downloads/#/" + DownloadContract.RequestHeaders.URI_SEGMENT, REQUEST_HEADERS_URI);
URI_MATCHER.addURI(AUTHORITY, "all_downloads/#/" + DownloadContract.RequestHeaders.URI_SEGMENT, REQUEST_HEADERS_URI);
// temporary, for backwards compatibility
URI_MATCHER.addURI(AUTHORITY, "download", MY_DOWNLOADS);
URI_MATCHER.addURI(AUTHORITY, "download/#", MY_DOWNLOADS_ID);
URI_MATCHER.addURI(AUTHORITY, "download/#/" + DownloadContract.RequestHeaders.URI_SEGMENT, REQUEST_HEADERS_URI);
URI_MATCHER.addURI(AUTHORITY, DownloadsDestination.PUBLICLY_ACCESSIBLE_DOWNLOADS_URI_SEGMENT + "/#", PUBLIC_DOWNLOAD_ID);
APP_READABLE_COLUMNS_SET = new HashSet<>();
Collections.addAll(APP_READABLE_COLUMNS_SET, APP_READABLE_COLUMNS_ARRAY);
COLUMNS_MAP = new HashMap<>();
COLUMNS_MAP.put(OpenableColumns.DISPLAY_NAME, DownloadContract.Batches.COLUMN_TITLE + " AS " + OpenableColumns.DISPLAY_NAME);
COLUMNS_MAP.put(OpenableColumns.SIZE, DownloadContract.Downloads.COLUMN_TOTAL_BYTES + " AS " + OpenableColumns.SIZE);
}
public DownloadProvider() {
downloadsUriProvider = DownloadsUriProvider.getInstance();
baseUris = new Uri[]{
downloadsUriProvider.getContentUri(),
downloadsUriProvider.getAllDownloadsUri(),
downloadsUriProvider.getBatchesUri()
};
}
/**
* This class encapsulates a SQL where clause and its parameters. It makes it possible for
* shared methods (like {@link DownloadProvider#getWhereClause(Uri, String, String[], int)})
* to return both pieces of information, and provides some utility logic to ease piece-by-piece
* construction of selections.
*/
private static class SqlSelection {
public final StringBuilder whereClause = new StringBuilder();
public final List<String> parameters = new ArrayList<>();
public void appendClause(String newClause, final String... parameters) {
if (newClause == null || newClause.isEmpty()) {
return;
}
if (whereClause.length() != 0) {
whereClause.append(" AND ");
}
whereClause.append("(");
whereClause.append(newClause);
whereClause.append(")");
if (parameters != null) {
for (String parameter : parameters) {
this.parameters.add(parameter);
}
}
}
public String getSelection() {
return whereClause.toString();
}
public String[] getParameters() {
String[] array = new String[parameters.size()];
return parameters.toArray(array);
}
}
/**
* Initializes the content provider when it is created.
*/
@Override
public boolean onCreate() {
if (systemFacade == null) {
systemFacade = new RealSystemFacade(getContext(), new Clock());
}
Context context = getContext();
PackageManager packageManager = context.getPackageManager();
String packageName = context.getApplicationContext().getPackageName();
DatabaseFilenameProvider databaseFilenameProvider = new DatabaseFilenameProvider(packageManager, packageName, DB_NAME);
String databaseFilename = databaseFilenameProvider.getDatabaseFilename();
openHelper = new DatabaseHelper(context, databaseFilename);
// Initialize the system uid
systemUid = Process.SYSTEM_UID;
// Initialize the default container uid. Package name hardcoded
// for now.
ApplicationInfo appInfo = null;
try {
appInfo = getContext().getPackageManager().
getApplicationInfo("com.android.defcontainer", 0);
} catch (NameNotFoundException e) {
LLog.wtf(e, "Could not get ApplicationInfo for com.android.defconatiner");
}
if (appInfo != null) {
defcontaineruid = appInfo.uid;
}
// start the DownloadService class. don't wait for the 1st download to be issued.
// saves us by getting some initialization code in DownloadService out of the way.
context.startService(new Intent(context, DownloadService.class));
// downloadsDataDir = StorageManager.getDownloadDataDirectory(getContext());
downloadsDataDir = context.getCacheDir();
// try {
// android.os.SELinux.restorecon(downloadsDataDir.getCanonicalPath());
// } catch (IOException e) {
// LLog.wtf("Could not get canonical path for download directory", e);
// }
return true;
}
/**
* Returns the content-provider-style MIME types of the various
* types accessible through this content provider.
*/
@NonNull
@Override
public String getType(@NonNull Uri uri) {
int match = URI_MATCHER.match(uri);
switch (match) {
case MY_DOWNLOADS:
case ALL_DOWNLOADS:
return DOWNLOAD_LIST_TYPE;
case MY_DOWNLOADS_ID:
case ALL_DOWNLOADS_ID:
case PUBLIC_DOWNLOAD_ID: {
// return the mimetype of this id from the database
final String id = getDownloadIdFromUri(uri);
final SQLiteDatabase db = openHelper.getReadableDatabase();
final String mimeType = DatabaseUtils.stringForQuery(
db,
"SELECT " + DownloadContract.Downloads.COLUMN_MIME_TYPE + " FROM " + DownloadContract.Downloads.DOWNLOADS_TABLE_NAME +
" WHERE " + DownloadContract.Downloads._ID + " = ?",
new String[]{id});
if (TextUtils.isEmpty(mimeType)) {
return DOWNLOAD_TYPE;
} else {
return mimeType;
}
}
case BATCHES:
return BATCH_LIST_TYPE;
case BATCHES_ID:
return BATCH_TYPE;
case DOWNLOADS_BY_BATCH:
return DOWNLOADS_BY_BATCH_TYPE;
default:
LLog.v("calling getType on an unknown URI: " + uri);
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
/**
* Inserts a row in the database
*/
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
SQLiteDatabase db = openHelper.getWritableDatabase();
// note we disallow inserting into ALL_DOWNLOADS
int match = URI_MATCHER.match(uri);
if (match == MY_DOWNLOADS) {
return insertDownload(uri, values, db, match);
}
if (match == BATCHES) {
long rowId = db.insert(DownloadContract.Batches.BATCHES_TABLE_NAME, null, values);
notifyBatchesChanged();
return ContentUris.withAppendedId(downloadsUriProvider.getBatchesUri(), rowId);
}
LLog.d("calling insert on an unknown/invalid URI: " + uri);
throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
}
@Nullable
private Uri insertDownload(Uri uri, ContentValues values, SQLiteDatabase db, int match) {
// copy some of the input values as it
ContentValues filteredValues = new ContentValues();
copyString(DownloadContract.Downloads.COLUMN_URI, values, filteredValues);
copyString(DownloadContract.Downloads.COLUMN_APP_DATA, values, filteredValues);
copyBoolean(DownloadContract.Downloads.COLUMN_NO_INTEGRITY, values, filteredValues);
copyString(DownloadContract.Downloads.COLUMN_FILE_NAME_HINT, values, filteredValues);
copyString(DownloadContract.Downloads.COLUMN_MIME_TYPE, values, filteredValues);
// validate the destination column
Integer dest = values.getAsInteger(DownloadContract.Downloads.COLUMN_DESTINATION);
if (dest != null) {
if (getContext().checkCallingPermission(DownloadsPermission.PERMISSION_ACCESS_ADVANCED)
!= PackageManager.PERMISSION_GRANTED
&& (dest == DownloadsDestination.DESTINATION_CACHE_PARTITION
|| dest == DownloadsDestination.DESTINATION_CACHE_PARTITION_NOROAMING
|| dest == DownloadsDestination.DESTINATION_SYSTEMCACHE_PARTITION)) {
throw new SecurityException(
"setting destination to : " + dest +
" not allowed, unless PERMISSION_ACCESS_ADVANCED is granted");
}
// for public API behavior, if an app has CACHE_NON_PURGEABLE permission, automatically
// switch to non-purgeable download
boolean hasNonPurgeablePermission =
getContext().checkCallingPermission(DownloadsPermission.PERMISSION_CACHE_NON_PURGEABLE) == PackageManager.PERMISSION_GRANTED;
if (dest == DownloadsDestination.DESTINATION_CACHE_PARTITION_PURGEABLE && hasNonPurgeablePermission) {
dest = DownloadsDestination.DESTINATION_CACHE_PARTITION;
}
if (dest == DownloadsDestination.DESTINATION_FILE_URI) {
if (!getFileUriDestinationPath(values).startsWith(Environment.getDataDirectory() + "/")) {
// external, not internal storage
getContext().enforcePermission(
android.Manifest.permission.WRITE_EXTERNAL_STORAGE, Binder.getCallingPid(), Binder.getCallingUid(),
"need WRITE_EXTERNAL_STORAGE permission to use DESTINATION_FILE_URI");
}
} else if (dest == DownloadsDestination.DESTINATION_SYSTEMCACHE_PARTITION) {
getContext().enforcePermission(
"android.permission.ACCESS_CACHE_FILESYSTEM", Binder.getCallingPid(), Binder.getCallingUid(),
"need ACCESS_CACHE_FILESYSTEM permission to use system cache");
}
filteredValues.put(DownloadContract.Downloads.COLUMN_DESTINATION, dest);
}
// copy the control column as is
copyInteger(DownloadContract.Downloads.COLUMN_CONTROL, values, filteredValues);
/*
* requests coming from
* DownloadManager.addCompletedDownload(String, String, String,
* boolean, String, String, long) need special treatment
*/
if (values.getAsInteger(DownloadContract.Downloads.COLUMN_DESTINATION) == DownloadsDestination.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
// these requests always are marked as 'completed'
filteredValues.put(DownloadContract.Downloads.COLUMN_STATUS, DownloadStatus.SUCCESS);
filteredValues.put(DownloadContract.Downloads.COLUMN_TOTAL_BYTES, values.getAsLong(DownloadContract.Downloads.COLUMN_TOTAL_BYTES));
filteredValues.put(DownloadContract.Downloads.COLUMN_CURRENT_BYTES, values.getAsLong(DownloadContract.Downloads.COLUMN_CURRENT_BYTES));
copyInteger(DownloadContract.Downloads.COLUMN_MEDIA_SCANNED, values, filteredValues);
copyString(DownloadContract.Downloads.COLUMN_DATA, values, filteredValues);
} else {
filteredValues.put(DownloadContract.Downloads.COLUMN_STATUS, DownloadStatus.PENDING);
filteredValues.put(DownloadContract.Downloads.COLUMN_TOTAL_BYTES, -1);
filteredValues.put(DownloadContract.Downloads.COLUMN_CURRENT_BYTES, 0);
}
// set lastupdate to current time
long lastMod = systemFacade.currentTimeMillis();
filteredValues.put(DownloadContract.Downloads.COLUMN_LAST_MODIFICATION, lastMod);
// use packagename of the caller to set the notification columns
String clazz = values.getAsString(DownloadContract.Downloads.COLUMN_NOTIFICATION_CLASS);
if (clazz != null) {
int uid = Binder.getCallingUid();
try {
if ((uid == 0) || systemFacade.userOwnsPackage(uid, getContext().getPackageName())) {
filteredValues.put(DownloadContract.Downloads.COLUMN_NOTIFICATION_CLASS, clazz);
}
} catch (NameNotFoundException ex) {
/* ignored for now */
}
}
// copy some more columns as is
copyString(DownloadContract.Downloads.COLUMN_NOTIFICATION_EXTRAS, values, filteredValues);
copyString(DownloadContract.Downloads.COLUMN_EXTRA_DATA, values, filteredValues);
copyString(DownloadContract.Downloads.COLUMN_COOKIE_DATA, values, filteredValues);
copyString(DownloadContract.Downloads.COLUMN_USER_AGENT, values, filteredValues);
copyString(DownloadContract.Downloads.COLUMN_REFERER, values, filteredValues);
// UID, PID columns
if (getContext().checkCallingPermission(DownloadsPermission.PERMISSION_ACCESS_ADVANCED) == PackageManager.PERMISSION_GRANTED) {
copyInteger(DownloadContract.Downloads.COLUMN_OTHER_UID, values, filteredValues);
}
filteredValues.put(Constants.UID, Binder.getCallingUid());
if (Binder.getCallingUid() == 0) {
copyInteger(Constants.UID, values, filteredValues);
}
// is_visible_in_downloads_ui column
if (values.containsKey(DownloadContract.Downloads.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)) {
copyBoolean(DownloadContract.Downloads.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, values, filteredValues);
} else {
// by default, make external downloads visible in the UI
boolean isExternal = (dest == null || dest == DownloadsDestination.DESTINATION_EXTERNAL);
filteredValues.put(DownloadContract.Downloads.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, isExternal);
}
// public api requests and networktypes/roaming columns
copyInteger(DownloadContract.Downloads.COLUMN_ALLOWED_NETWORK_TYPES, values, filteredValues);
copyBoolean(DownloadContract.Downloads.COLUMN_ALLOW_ROAMING, values, filteredValues);
copyBoolean(DownloadContract.Downloads.COLUMN_ALLOW_METERED, values, filteredValues);
copyBoolean(DownloadContract.Downloads.COLUMN_ALWAYS_RESUME, values, filteredValues);
copyBoolean(DownloadContract.Downloads.COLUMN_ALLOW_TAR_UPDATES, values, filteredValues);
copyInteger(DownloadContract.Downloads.COLUMN_BATCH_ID, values, filteredValues);
LLog.v("initiating download with UID " + filteredValues.getAsInteger(Constants.UID));
if (filteredValues.containsKey(DownloadContract.Downloads.COLUMN_OTHER_UID)) {
LLog.v("other UID " + filteredValues.getAsInteger(DownloadContract.Downloads.COLUMN_OTHER_UID));
}
long rowID = db.insert(DownloadContract.Downloads.DOWNLOADS_TABLE_NAME, null, filteredValues);
if (rowID == -1) {
LLog.d("couldn't insert into downloads database");
return null;
}
insertRequestHeaders(db, rowID, values);
/*
* requests coming from
* DownloadManager.addCompletedDownload(String, String, String,
* boolean, String, String, long) need special treatment
*/
Context context = getContext();
context.startService(new Intent(context, DownloadService.class));
notifyContentChanged(uri, match);
notifyDownloadStatusChanged();
return ContentUris.withAppendedId(downloadsUriProvider.getContentUri(), rowID);
}
private void notifyDownloadStatusChanged() {
getContext().getContentResolver().notifyChange(downloadsUriProvider.getDownloadsWithoutProgressUri(), null);
}
private void notifyBatchesChanged() {
getContext().getContentResolver().notifyChange(downloadsUriProvider.getBatchesWithoutProgressUri(), null);
getContext().getContentResolver().notifyChange(downloadsUriProvider.getBatchesUri(), null);
}
/**
* Retrieve the file path for DESTINATION_FILE_URI if the URI is valid
*/
private String getFileUriDestinationPath(ContentValues values) {
String fileUri = values.getAsString(DownloadContract.Downloads.COLUMN_FILE_NAME_HINT);
if (fileUri == null) {
throw new IllegalArgumentException(
"DESTINATION_FILE_URI must include a file URI under COLUMN_FILE_NAME_HINT");
}
Uri uri = Uri.parse(fileUri);
String scheme = uri.getScheme();
if (scheme == null || !scheme.equals("file")) {
throw new IllegalArgumentException("Not a file URI: " + uri);
}
final String path = uri.getPath();
if (path == null) {
throw new IllegalArgumentException("Invalid file URI: " + uri);
}
return path;
}
/**
* Starts a database query
*/
@NonNull
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sort) {
Helpers.validateSelection(selection, APP_READABLE_COLUMNS_SET);
SQLiteDatabase db = openHelper.getReadableDatabase();
int match = URI_MATCHER.match(uri);
switch (match) {
case ALL_DOWNLOADS:
case ALL_DOWNLOADS_ID:
case MY_DOWNLOADS:
case MY_DOWNLOADS_ID:
return queryDownloads(uri, projection, selection, selectionArgs, sort, db, match);
case BATCHES:
case BATCHES_ID:
SqlSelection batchSelection = getWhereClause(uri, selection, selectionArgs, match);
return db.query(
DownloadContract.BatchesWithSizes.VIEW_NAME_BATCHES_WITH_SIZES, projection, batchSelection.getSelection(),
batchSelection.getParameters(), null, null, sort);
case DOWNLOADS_BY_BATCH:
return db.query(DownloadContract.DownloadsByBatch.VIEW_NAME_DOWNLOADS_BY_BATCH, projection, selection, selectionArgs, null, null, sort);
case REQUEST_HEADERS_URI:
if (projection != null || selection != null || sort != null) {
throw new UnsupportedOperationException(
"Request header queries do not support "
+ "projections, selections or sorting");
}
return queryRequestHeaders(db, uri);
default:
LLog.v("querying unknown URI: " + uri);
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
@Nullable
private Cursor queryDownloads(Uri uri, String[] projection, String selection, String[] selectionArgs, String sort, SQLiteDatabase db, int match) {
SqlSelection fullSelection = getWhereClause(uri, selection, selectionArgs, match);
if (shouldRestrictVisibility()) {
if (projection == null) {
projection = APP_READABLE_COLUMNS_ARRAY.clone();
} else {
// check the validity of the columns in projection
for (int i = 0; i < projection.length; ++i) {
if (!APP_READABLE_COLUMNS_SET.contains(projection[i]) &&
!DOWNLOAD_MANAGER_COLUMNS_LIST.contains(projection[i])) {
throw new IllegalArgumentException(
"column " + projection[i] + " is not allowed in queries");
}
}
}
for (int i = 0; i < projection.length; i++) {
final String newColumn = COLUMNS_MAP.get(projection[i]);
if (newColumn != null) {
projection[i] = newColumn;
}
}
}
if (GlobalState.hasVerboseLogging()) {
logVerboseQueryInfo(projection, selection, selectionArgs, sort, db);
}
Cursor ret = db.query(
DownloadContract.Downloads.DOWNLOADS_TABLE_NAME, projection, fullSelection.getSelection(),
fullSelection.getParameters(), null, null, sort);
if (ret == null) {
LLog.v("query failed in downloads database");
} else {
ret.setNotificationUri(getContext().getContentResolver(), uri);
LLog.v("created cursor " + ret + " on behalf of " + Binder.getCallingPid());
}
return ret;
}
private void logVerboseQueryInfo(String[] projection,
final String selection,
final String[] selectionArgs,
final String sort,
SQLiteDatabase db) {
java.lang.StringBuilder sb = new java.lang.StringBuilder();
sb.append("starting query, database is ");
if (db != null) {
sb.append("not ");
}
sb.append("null; ");
if (projection == null) {
sb.append("projection is null; ");
} else if (projection.length == 0) {
sb.append("projection is empty; ");
} else {
for (int i = 0; i < projection.length; ++i) {
sb.append("projection[");
sb.append(i);
sb.append("] is ");
sb.append(projection[i]);
sb.append("; ");
}
}
sb.append("selection is ");
sb.append(selection);
sb.append("; ");
if (selectionArgs == null) {
sb.append("selectionArgs is null; ");
} else if (selectionArgs.length == 0) {
sb.append("selectionArgs is empty; ");
} else {
for (int i = 0; i < selectionArgs.length; ++i) {
sb.append("selectionArgs[");
sb.append(i);
sb.append("] is ");
sb.append(selectionArgs[i]);
sb.append("; ");
}
}
sb.append("sort is ");
sb.append(sort);
sb.append(".");
LLog.v(sb.toString());
}
private String getDownloadIdFromUri(final Uri uri) {
return uri.getPathSegments().get(1);
}
/**
* Insert request headers for a download into the DB.
*/
private void insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values) {
ContentValues rowValues = new ContentValues();
rowValues.put(DownloadContract.RequestHeaders.COLUMN_DOWNLOAD_ID, downloadId);
for (Map.Entry<String, Object> entry : values.valueSet()) {
String key = entry.getKey();
if (key.startsWith(DownloadContract.RequestHeaders.INSERT_KEY_PREFIX)) {
String headerLine = entry.getValue().toString();
if (!headerLine.contains(":")) {
throw new IllegalArgumentException("Invalid HTTP header line: " + headerLine);
}
String[] parts = headerLine.split(":", 2);
rowValues.put(DownloadContract.RequestHeaders.COLUMN_HEADER, parts[0].trim());
rowValues.put(DownloadContract.RequestHeaders.COLUMN_VALUE, parts[1].trim());
db.insert(DownloadContract.RequestHeaders.HEADERS_DB_TABLE, null, rowValues);
}
}
}
/**
* Handle a query for the custom request headers registered for a download.
*/
private Cursor queryRequestHeaders(SQLiteDatabase db, Uri uri) {
String where = DownloadContract.RequestHeaders.COLUMN_DOWNLOAD_ID + "="
+ getDownloadIdFromUri(uri);
String[] projection = new String[]{DownloadContract.RequestHeaders.COLUMN_HEADER,
DownloadContract.RequestHeaders.COLUMN_VALUE};
return db.query(
DownloadContract.RequestHeaders.HEADERS_DB_TABLE, projection, where,
null, null, null, null);
}
/**
* Delete request headers for downloads matching the given query.
*/
private void deleteRequestHeaders(SQLiteDatabase db, String where, String[] whereArgs) {
String[] projection = new String[]{DownloadContract.Downloads._ID};
Cursor cursor = db.query(DownloadContract.Downloads.DOWNLOADS_TABLE_NAME, projection, where, whereArgs, null, null, null, null);
try {
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
long id = cursor.getLong(0);
String idWhere = DownloadContract.RequestHeaders.COLUMN_DOWNLOAD_ID + "=" + id;
db.delete(DownloadContract.RequestHeaders.HEADERS_DB_TABLE, idWhere, null);
}
} finally {
cursor.close();
}
}
/**
* @return true if we should restrict the columns readable by this caller
*/
private boolean shouldRestrictVisibility() {
int callingUid = Binder.getCallingUid();
return Binder.getCallingPid() != Process.myPid() &&
callingUid != systemUid &&
callingUid != defcontaineruid;
}
/**
* Updates a row in the database
*/
@Override
public int update(final Uri uri, final ContentValues values, final String where, final String[] whereArgs) {
Helpers.validateSelection(where, APP_READABLE_COLUMNS_SET);
SQLiteDatabase db = openHelper.getWritableDatabase();
int count;
boolean startService = false;
if (values.containsKey(DownloadContract.Downloads.COLUMN_DELETED)) {
if (values.getAsInteger(DownloadContract.Downloads.COLUMN_DELETED) == 1) {
// some rows are to be 'deleted'. need to start DownloadService.
startService = true;
}
}
ContentValues filteredValues;
if (Binder.getCallingPid() != Process.myPid()) {
filteredValues = new ContentValues();
copyString(DownloadContract.Downloads.COLUMN_APP_DATA, values, filteredValues);
Integer i = values.getAsInteger(DownloadContract.Downloads.COLUMN_CONTROL);
if (i != null) {
filteredValues.put(DownloadContract.Downloads.COLUMN_CONTROL, i);
startService = true;
}
copyInteger(DownloadContract.Downloads.COLUMN_CONTROL, values, filteredValues);
copyString(DownloadContract.Downloads.COLUMN_MEDIAPROVIDER_URI, values, filteredValues);
copyInteger(DownloadContract.Downloads.COLUMN_DELETED, values, filteredValues);
} else {
filteredValues = values;
Integer status = values.getAsInteger(DownloadContract.Downloads.COLUMN_STATUS);
boolean isRestart = status != null && status == DownloadStatus.PENDING;
boolean isUserBypassingSizeLimit =
values.containsKey(DownloadContract.Downloads.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
if (isRestart || isUserBypassingSizeLimit) {
startService = true;
}
}
int match = URI_MATCHER.match(uri);
switch (match) {
case MY_DOWNLOADS:
case MY_DOWNLOADS_ID:
case ALL_DOWNLOADS:
case ALL_DOWNLOADS_ID:
SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
if (filteredValues.size() > 0) {
count = db.update(DownloadContract.Downloads.DOWNLOADS_TABLE_NAME, filteredValues, selection.getSelection(), selection.getParameters());
} else {
count = 0;
}
notifyStatusIfDownloadStatusChanged(values);
break;
case BATCHES:
case BATCHES_ID:
SqlSelection batchSelection = getWhereClause(uri, where, whereArgs, match);
count = db.update(DownloadContract.Batches.BATCHES_TABLE_NAME, values, batchSelection.getSelection(), batchSelection.getParameters());
if (values.containsKey(DownloadContract.Batches.COLUMN_STATUS) || values.containsKey(DownloadContract.Batches.COLUMN_DELETED)){
notifyBatchesChanged();
}
break;
default:
LLog.d("updating unknown/invalid URI: " + uri);
throw new UnsupportedOperationException("Cannot update URI: " + uri);
}
notifyContentChanged(uri, match);
if (startService) {
Context context = getContext();
context.startService(new Intent(context, DownloadService.class));
}
return count;
}
private void notifyStatusIfDownloadStatusChanged(ContentValues values) {
if (values.containsKey(DownloadContract.Downloads.COLUMN_STATUS)) {
notifyDownloadStatusChanged();
}
}
/**
* Notify of a change through both URIs (/my_downloads and /all_downloads)
*
* @param uri either URI for the changed download(s)
* @param uriMatch the match ID from {@link #URI_MATCHER}
*/
private void notifyContentChanged(final Uri uri, int uriMatch) {
Long downloadId = null;
if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID) {
downloadId = Long.parseLong(getDownloadIdFromUri(uri));
}
for (Uri uriToNotify : baseUris) {
if (downloadId != null) {
uriToNotify = ContentUris.withAppendedId(uriToNotify, downloadId);
}
getContext().getContentResolver().notifyChange(uriToNotify, null);
}
}
private SqlSelection getWhereClause(final Uri uri, final String where, final String[] whereArgs,
int uriMatch) {
SqlSelection selection = new SqlSelection();
selection.appendClause(where, whereArgs);
if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID ||
uriMatch == PUBLIC_DOWNLOAD_ID) {
selection.appendClause(DownloadContract.Downloads._ID + " = ?", getDownloadIdFromUri(uri));
}
if (uriMatch == BATCHES_ID) {
selection.appendClause(DownloadContract.Batches._ID + " = ?", uri.getLastPathSegment());
}
if ((uriMatch == MY_DOWNLOADS || uriMatch == MY_DOWNLOADS_ID)
&& getContext().checkCallingPermission(DownloadsPermission.PERMISSION_ACCESS_ALL)
!= PackageManager.PERMISSION_GRANTED) {
String callingUid = String.valueOf(Binder.getCallingUid());
selection.appendClause(
Constants.UID + "= ? OR " + DownloadContract.Downloads.COLUMN_OTHER_UID + "= ?",
callingUid, callingUid);
}
return selection;
}
/**
* Deletes a row in the database
*/
@Override
public int delete(@NonNull Uri uri, String where, String[] whereArgs) {
Helpers.validateSelection(where, APP_READABLE_COLUMNS_SET);
SQLiteDatabase db = openHelper.getWritableDatabase();
int count;
int match = URI_MATCHER.match(uri);
switch (match) {
case MY_DOWNLOADS:
case MY_DOWNLOADS_ID:
case ALL_DOWNLOADS:
case ALL_DOWNLOADS_ID:
SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
deleteRequestHeaders(db, selection.getSelection(), selection.getParameters());
count = db.delete(DownloadContract.Downloads.DOWNLOADS_TABLE_NAME, selection.getSelection(), selection.getParameters());
notifyDownloadStatusChanged();
break;
case BATCHES:
case BATCHES_ID:
SqlSelection batchSelection = getWhereClause(uri, where, whereArgs, match);
count = db.delete(DownloadContract.Batches.BATCHES_TABLE_NAME, batchSelection.getSelection(), batchSelection.getParameters());
notifyBatchesChanged();
break;
default:
LLog.d("deleting unknown/invalid URI: " + uri);
throw new UnsupportedOperationException("Cannot delete URI: " + uri);
}
notifyContentChanged(uri, match);
return count;
}
/**
* Remotely opens a file
*/
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, String mode) throws FileNotFoundException {
if (GlobalState.hasVerboseLogging()) {
logVerboseOpenFileInfo(uri, mode);
}
String path;
Cursor cursor = query(uri, new String[]{"_data"}, null, null, null);
try {
int count = cursor.getCount();
if (count != 1) {
// If there is not exactly one result, throw an appropriate exception.
if (count == 0) {
throw new FileNotFoundException("No entry for " + uri);
}
throw new FileNotFoundException("Multiple items at " + uri);
}
cursor.moveToFirst();
path = cursor.getString(0);
} finally {
cursor.close();
}
if (path == null) {
throw new FileNotFoundException("No filename found.");
}
if (!Helpers.isFilenameValid(path, downloadsDataDir)) {
LLog.d("INTERNAL FILE DOWNLOAD LOL COMMENTED EXCEPTION");
// throw new FileNotFoundException("Invalid filename: " + path);
}
if (!"r".equals(mode)) {
throw new FileNotFoundException("Bad mode for " + uri + ": " + mode);
}
ParcelFileDescriptor ret = ParcelFileDescriptor.open(
new File(path),
ParcelFileDescriptor.MODE_READ_ONLY);
if (ret == null) {
LLog.v("couldn't open file");
throw new FileNotFoundException("couldn't open file");
}
return ret;
}
@Override
public void dump(FileDescriptor fd, @NonNull PrintWriter writer, String[] args) {
LLog.e("I want dump, but nothing to dump into");
}
private void logVerboseOpenFileInfo(Uri uri, String mode) {
LLog.v(
"openFile uri: " + uri + ", mode: " + mode
+ ", uid: " + Binder.getCallingUid()
);
Cursor cursor = query(downloadsUriProvider.getContentUri(), new String[]{"_id"}, null, null, "_id");
if (cursor == null) {
LLog.v("null cursor in openFile");
} else {
if (!cursor.moveToFirst()) {
LLog.v("empty cursor in openFile");
} else {
do {
LLog.v("row " + cursor.getInt(0) + " available");
} while (cursor.moveToNext());
}
cursor.close();
}
cursor = query(uri, new String[]{"_data"}, null, null, null);
if (cursor == null) {
LLog.v("null cursor in openFile");
} else {
if (!cursor.moveToFirst()) {
LLog.v("empty cursor in openFile");
} else {
String filename = cursor.getString(0);
LLog.v("filename in openFile: " + filename);
if (new java.io.File(filename).isFile()) {
LLog.v("file exists in openFile");
}
}
cursor.close();
}
}
private static void copyInteger(String key, ContentValues from, ContentValues to) {
Integer i = from.getAsInteger(key);
if (i != null) {
to.put(key, i);
}
}
private static void copyBoolean(String key, ContentValues from, ContentValues to) {
Boolean b = from.getAsBoolean(key);
if (b != null) {
to.put(key, b);
}
}
private static void copyString(String key, ContentValues from, ContentValues to) {
String s = from.getAsString(key);
if (s != null) {
to.put(key, s);
}
}
}