/*
|
* 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.mozillaonline.providers.downloads;
|
|
import java.io.File;
|
import java.io.FileNotFoundException;
|
import java.util.ArrayList;
|
import java.util.HashSet;
|
import java.util.Iterator;
|
import java.util.List;
|
import java.util.Map;
|
|
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.PackageManager;
|
import android.database.CrossProcessCursor;
|
import android.database.Cursor;
|
import android.database.CursorWindow;
|
import android.database.CursorWrapper;
|
import android.database.SQLException;
|
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteOpenHelper;
|
import android.net.Uri;
|
import android.os.Binder;
|
import android.os.ParcelFileDescriptor;
|
import android.os.Process;
|
import android.util.Log;
|
|
/**
|
* Allows application to interact with the download manager.
|
*/
|
public final class DownloadProvider extends ContentProvider {
|
private static final String TAG = "DownloadProvider";
|
/** Database filename */
|
private static final String DB_NAME = "downloads.db";
|
/** Current database version */
|
private static final int DB_VERSION = 106;
|
/** Name of table in the database */
|
private static final String DB_TABLE = "downloads";
|
|
/** 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";
|
|
/** URI matcher used to recognize URIs sent by applications */
|
private static final UriMatcher sURIMatcher = 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
|
* URI匹配常数下载的请求头的URI
|
*/
|
private static final int REQUEST_HEADERS_URI = 5;
|
static {
|
sURIMatcher.addURI(Downloads.AUTHORITY, "my_downloads", MY_DOWNLOADS);
|
sURIMatcher.addURI(Downloads.AUTHORITY, "my_downloads/#",
|
MY_DOWNLOADS_ID);
|
sURIMatcher.addURI(Downloads.AUTHORITY, "all_downloads", ALL_DOWNLOADS);
|
sURIMatcher.addURI(Downloads.AUTHORITY, "all_downloads/#",
|
ALL_DOWNLOADS_ID);
|
sURIMatcher.addURI(Downloads.AUTHORITY, "my_downloads/#/"
|
+ Downloads.RequestHeaders.URI_SEGMENT, REQUEST_HEADERS_URI);
|
sURIMatcher.addURI(Downloads.AUTHORITY, "all_downloads/#/"
|
+ Downloads.RequestHeaders.URI_SEGMENT, REQUEST_HEADERS_URI);
|
}
|
|
/**
|
* Different base URIs that could be used to access an individual download
|
* 用来访问单个下载的不同标准的Uri
|
*/
|
private static final Uri[] BASE_URIS = new Uri[] { Downloads.CONTENT_URI,
|
Downloads.ALL_DOWNLOADS_CONTENT_URI, };
|
|
private static final String[] sAppReadableColumnsArray = new String[] {
|
Downloads._ID, Downloads.COLUMN_APP_DATA, Downloads._DATA,
|
Downloads.COLUMN_MIME_TYPE, Downloads.COLUMN_VISIBILITY,
|
Downloads.COLUMN_DESTINATION, Downloads.COLUMN_CONTROL,
|
Downloads.COLUMN_STATUS, Downloads.COLUMN_LAST_MODIFICATION,
|
Downloads.COLUMN_NOTIFICATION_PACKAGE,
|
Downloads.COLUMN_NOTIFICATION_CLASS, Downloads.COLUMN_TOTAL_BYTES,
|
Downloads.COLUMN_CURRENT_BYTES, Downloads.COLUMN_TITLE,
|
Downloads.COLUMN_DESCRIPTION, Downloads.COLUMN_URI,
|
Downloads.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI,
|
Downloads.COLUMN_FILE_NAME_HINT, Downloads.COLUMN_DELETED };
|
|
private static HashSet<String> sAppReadableColumnsSet;
|
static {
|
sAppReadableColumnsSet = new HashSet<String>();
|
for (int i = 0; i < sAppReadableColumnsArray.length; ++i) {
|
sAppReadableColumnsSet.add(sAppReadableColumnsArray[i]);
|
}
|
}
|
|
/**
|
* The database that lies underneath this content provider 摆在这 content
|
* provider 下的数据库
|
*/
|
private SQLiteOpenHelper mOpenHelper = null;
|
|
SystemFacade mSystemFacade;
|
|
/**
|
* 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.
|
* 此类封装了一个SQL语句和他的参数。它使得有可能共享的方法(如
|
* {@link DownloadProvider#getWhereClause(Uri, String, String[], int)}
|
* )返回两条信息,并提供了一些实用的逻辑来缓解 piece-by-piece 结构的选择
|
*/
|
private static class SqlSelection {
|
public StringBuilder mWhereClause = new StringBuilder();
|
public List<String> mParameters = new ArrayList<String>();
|
|
public <T> void appendClause(String newClause, final T... parameters) {
|
if (newClause == null || newClause.length() == 0) {
|
return;
|
}
|
if (mWhereClause.length() != 0) {
|
mWhereClause.append(" AND ");
|
}
|
mWhereClause.append("(");
|
mWhereClause.append(newClause);
|
mWhereClause.append(")");
|
if (parameters != null) {
|
for (Object parameter : parameters) {
|
mParameters.add(parameter.toString());
|
}
|
}
|
}
|
|
public String getSelection() {
|
return mWhereClause.toString();
|
}
|
|
public String[] getParameters() {
|
String[] array = new String[mParameters.size()];
|
return mParameters.toArray(array);
|
}
|
}
|
|
/**
|
* Creates and updated database on demand when opening it. Helper class to
|
* create database the first time the provider is initialized and upgrade it
|
* when a new version of the provider needs an updated version of the
|
* database.创建和打开数据库的时候按需求更新数据库。Helper类创建数据库,当一个新版本数据库更新的时候第一时间需要初始化和升级
|
*/
|
private final class DatabaseHelper extends SQLiteOpenHelper {
|
|
public DatabaseHelper(final Context context) {
|
super(context, DB_NAME, null, DB_VERSION);
|
}
|
|
/**
|
* Creates database the first time we try to open it. 创建数据库的第一时间尝试打开它
|
* 调用时机是用户首次安装应用后启动,或是清除App数据库文件后启动。这时可以在这个函数中完成初始的数据表的创建。
|
*/
|
@Override
|
public void onCreate(final SQLiteDatabase db) {
|
if (Constants.LOGVV) {
|
Log.v(Constants.TAG, "populating new database");
|
}
|
onUpgrade(db, 0, DB_VERSION);
|
}
|
|
/**
|
* Updates the database format when a content provider is used with a
|
* database that was created with a different format.当content
|
* provider被一个数据库调用的时候更新数据库格式与被创建的数据库的格式不同时
|
* 调用时机是用户在做应用更新,覆盖安装后启动,如果新版本中数据库版本号要比旧版本中的数据库版本号高则会调用
|
* 。这时可以在这个函数完成数据库版本升级带来的旧版本的兼容问题,以及数据迁移问题。
|
* Note: to support downgrades,
|
* creating a table should always drop it first if it already
|
* exists.为了支持降级,创建一个表时如果此表已经存在,则应该先删除它
|
*/
|
@Override
|
public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) {
|
if (oldV == 31) {
|
// 31 and 100 are identical, just in different codelines.
|
// Upgrading from 31 is the
|
// same as upgrading from 100.
|
// 31和100是相同的,只是在不同的代码线。
|
// 从31升级和100升级相同
|
oldV = 100;
|
} else if (oldV < 100) {
|
// no logic to upgrade from these older version, just recreate
|
// the DB
|
// 如果从旧版本升级没有根据,则重新创建数据库
|
Log.i(Constants.TAG,
|
"Upgrading downloads database from version " + oldV
|
+ " to version " + newV
|
+ ", which will destroy all old data");
|
oldV = 99;
|
} else if (oldV > newV) {
|
// user must have downgraded software; we have no way to know
|
// how to downgrade the
|
// DB, so just recreate it
|
// 用户必须降级软件,我们不知道怎么去降级数据可,因此必须创建他
|
Log.i(Constants.TAG,
|
"Downgrading downloads database from version " + oldV
|
+ " (current version is " + newV
|
+ "), destroying all old data");
|
oldV = 99;
|
}
|
|
for (int version = oldV + 1; version <= newV; version++) {
|
upgradeTo(db, version);
|
}
|
}
|
|
/**
|
* Upgrade database from (version - 1) to version.升级数据库版本
|
*/
|
private void upgradeTo(SQLiteDatabase db, int version) {
|
switch (version) {
|
case 100:
|
createDownloadsTable(db);// 创建下载表格
|
break;
|
|
case 101:
|
createHeadersTable(db);
|
break;
|
|
case 102:
|
addColumn(db, DB_TABLE, Downloads.COLUMN_IS_PUBLIC_API,
|
"INTEGER NOT NULL DEFAULT 0");// 列指示下载是否被要求通过公共API的名称。它控制一些行为差异。
|
addColumn(db, DB_TABLE, Downloads.COLUMN_ALLOW_ROAMING,
|
"INTEGER NOT NULL DEFAULT 0");// 可以使用的列表示是否漫游连接的名称。这仅用于公共API的下载。
|
addColumn(db, DB_TABLE, Downloads.COLUMN_ALLOWED_NETWORK_TYPES,
|
"INTEGER NOT NULL DEFAULT 0");// 列保持允许的网络类型的位掩码的名称。这仅用于公共API的下载。
|
break;
|
|
case 103:
|
addColumn(db, DB_TABLE,
|
Downloads.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI,
|
"INTEGER NOT NULL DEFAULT 1");// 下载是否应显示在系统的下载界面。默认为真。
|
makeCacheDownloadsInvisible(db);
|
break;
|
|
case 104:
|
addColumn(db, DB_TABLE,
|
Downloads.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT,
|
"INTEGER NOT NULL DEFAULT 0");// 如果为true,用户已经确认此下载可以继续在移动网络上,即使它超过了推荐的最大尺寸。
|
break;
|
|
case 105:
|
fillNullValues(db);
|
break;
|
|
case 106:
|
addColumn(db, DB_TABLE, Downloads.COLUMN_DELETED,
|
"BOOLEAN NOT NULL DEFAULT 0");// 如果此下载被删除则设置为true。
|
break;
|
|
default:
|
throw new IllegalStateException("Don't know how to upgrade to "
|
+ version);
|
}
|
}
|
|
/**
|
* insert() now ensures these four columns are never null for new
|
* downloads, so this method makes that true for existing columns, so
|
* that code can rely on this
|
* assumption.插入时确认此四列在新下载中不为空,然后此方法是正确的,如此可以依赖这一假设
|
*/
|
private void fillNullValues(SQLiteDatabase db) {
|
ContentValues values = new ContentValues();
|
values.put(Downloads.COLUMN_CURRENT_BYTES, 0);
|
fillNullValuesForColumn(db, values);
|
values.put(Downloads.COLUMN_TOTAL_BYTES, -1);
|
fillNullValuesForColumn(db, values);
|
values.put(Downloads.COLUMN_TITLE, "");
|
fillNullValuesForColumn(db, values);
|
values.put(Downloads.COLUMN_DESCRIPTION, "");
|
fillNullValuesForColumn(db, values);
|
}
|
|
private void fillNullValuesForColumn(SQLiteDatabase db,
|
ContentValues values) {
|
String column = values.valueSet().iterator().next().getKey();
|
db.update(DB_TABLE, values, column + " is null", null);
|
values.clear();
|
}
|
|
/**
|
* Set all existing downloads to the cache partition to be invisible in
|
* the downloads UI.设置所有的下载到缓存区是不可见的
|
*/
|
private void makeCacheDownloadsInvisible(SQLiteDatabase db) {
|
ContentValues values = new ContentValues();
|
values.put(Downloads.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, false);
|
String cacheSelection = Downloads.COLUMN_DESTINATION + " != "
|
+ Downloads.DESTINATION_EXTERNAL;
|
db.update(DB_TABLE, values, cacheSelection, null);
|
}
|
|
/**
|
* Add a column to a table using ALTER TABLE. 添加一列到表格中使用alter Table
|
*
|
* @param dbTable
|
* name of the table表格名
|
* @param columnName
|
* name of the column to add添加的列名
|
* @param columnDefinition
|
* SQL for the column definition定义列
|
*/
|
private void addColumn(SQLiteDatabase db, String dbTable,
|
String columnName, String columnDefinition) {
|
db.execSQL("ALTER TABLE " + dbTable + " ADD COLUMN " + columnName
|
+ " " + columnDefinition);
|
}
|
|
/**
|
* Creates the table that'll hold the download information.
|
* 在数据库中创建一个存放下载信息的表格
|
*/
|
private void createDownloadsTable(SQLiteDatabase db) {
|
try {
|
db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
|
db.execSQL("CREATE TABLE " + DB_TABLE + "(" + Downloads._ID
|
+ " INTEGER PRIMARY KEY AUTOINCREMENT,"
|
+ Downloads.COLUMN_URI + " TEXT, "
|
+ Constants.RETRY_AFTER_X_REDIRECT_COUNT + " INTEGER, "
|
+ Downloads.COLUMN_APP_DATA + " TEXT, "
|
+ Downloads.COLUMN_NO_INTEGRITY + " BOOLEAN, "
|
+ Downloads.COLUMN_FILE_NAME_HINT + " TEXT, "
|
+ Constants.OTA_UPDATE + " BOOLEAN, " + Downloads._DATA
|
+ " TEXT, " + Downloads.COLUMN_MIME_TYPE + " TEXT, "
|
+ Downloads.COLUMN_DESTINATION + " INTEGER, "
|
+ Constants.NO_SYSTEM_FILES + " BOOLEAN, "
|
+ Downloads.COLUMN_VISIBILITY + " INTEGER, "
|
+ Downloads.COLUMN_CONTROL + " INTEGER, "
|
+ Downloads.COLUMN_STATUS + " INTEGER, "
|
+ Constants.FAILED_CONNECTIONS + " INTEGER, "
|
+ Downloads.COLUMN_LAST_MODIFICATION + " BIGINT, "
|
+ Downloads.COLUMN_NOTIFICATION_PACKAGE + " TEXT, "
|
+ Downloads.COLUMN_NOTIFICATION_CLASS + " TEXT, "
|
+ Downloads.COLUMN_NOTIFICATION_EXTRAS + " TEXT, "
|
+ Downloads.COLUMN_COOKIE_DATA + " TEXT, "
|
+ Downloads.COLUMN_USER_AGENT + " TEXT, "
|
+ Downloads.COLUMN_REFERER + " TEXT, "
|
+ Downloads.COLUMN_TOTAL_BYTES + " INTEGER, "
|
+ Downloads.COLUMN_CURRENT_BYTES + " INTEGER, "
|
+ Constants.ETAG + " TEXT, " + Constants.UID
|
+ " INTEGER, " + Downloads.COLUMN_OTHER_UID
|
+ " INTEGER, " + Downloads.COLUMN_TITLE + " TEXT, "
|
+ Downloads.COLUMN_DESCRIPTION + " TEXT); ");
|
} catch (SQLException ex) {
|
Log.e(Constants.TAG,
|
"couldn't create table in downloads database");
|
throw ex;
|
}
|
}
|
|
/**
|
* 创建表头
|
*
|
* @param db
|
*/
|
private void createHeadersTable(SQLiteDatabase db) {
|
db.execSQL("DROP TABLE IF EXISTS "
|
+ Downloads.RequestHeaders.HEADERS_DB_TABLE);
|
db.execSQL("CREATE TABLE "
|
+ Downloads.RequestHeaders.HEADERS_DB_TABLE + "("
|
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
+ Downloads.RequestHeaders.COLUMN_DOWNLOAD_ID
|
+ " INTEGER NOT NULL,"
|
+ Downloads.RequestHeaders.COLUMN_HEADER
|
+ " TEXT NOT NULL," + Downloads.RequestHeaders.COLUMN_VALUE
|
+ " TEXT NOT NULL" + ");");
|
}
|
|
}
|
|
/**
|
* Initializes the content provider when it is created.在类被创建时初始化 content
|
* provider
|
*/
|
@Override
|
public boolean onCreate() {
|
if (mSystemFacade == null) {
|
mSystemFacade = new RealSystemFacade(getContext());
|
}
|
|
mOpenHelper = new DatabaseHelper(getContext());
|
return true;
|
}
|
|
/**
|
* Returns the content-provider-style MIME types of the various types
|
* accessible through this content provider.通过content provider
|
* 返回content-provider-style提供的各种类型中的MIME类型
|
*/
|
@Override
|
public String getType(final Uri uri) {
|
int match = sURIMatcher.match(uri);
|
switch (match) {
|
case MY_DOWNLOADS: {
|
return DOWNLOAD_LIST_TYPE;
|
}
|
case MY_DOWNLOADS_ID: {
|
return DOWNLOAD_TYPE;
|
}
|
default: {
|
if (Constants.LOGV) {
|
Log.v(Constants.TAG, "calling getType on an unknown URI: "
|
+ uri);
|
}
|
throw new IllegalArgumentException("Unknown URI: " + uri);
|
}
|
}
|
}
|
|
/**
|
* Inserts a row in the database 加入一行数据到数据库中
|
*/
|
@Override
|
public Uri insert(final Uri uri, final ContentValues values) {
|
checkInsertPermissions(values);
|
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
|
// note we disallow inserting into ALL_DOWNLOADS提示:不允许插入ALL_DOWNLOADS
|
int match = sURIMatcher.match(uri);
|
if (match != MY_DOWNLOADS) {
|
Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: "
|
+ uri);
|
throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
|
}
|
|
ContentValues filteredValues = new ContentValues();
|
|
copyString(Downloads.COLUMN_URI, values, filteredValues);
|
copyString(Downloads.COLUMN_APP_DATA, values, filteredValues);
|
copyBoolean(Downloads.COLUMN_NO_INTEGRITY, values, filteredValues);
|
copyString(Downloads.COLUMN_FILE_NAME_HINT, values, filteredValues);
|
copyString(Downloads.COLUMN_MIME_TYPE, values, filteredValues);
|
|
copyBoolean(Downloads.COLUMN_IS_PUBLIC_API, values, filteredValues);
|
boolean isPublicApi = values
|
.getAsBoolean(Downloads.COLUMN_IS_PUBLIC_API) == Boolean.TRUE;
|
|
Integer dest = values.getAsInteger(Downloads.COLUMN_DESTINATION);
|
if (dest != null) {
|
if (getContext().checkCallingPermission(
|
Downloads.PERMISSION_ACCESS_ADVANCED) != PackageManager.PERMISSION_GRANTED
|
&& dest != Downloads.DESTINATION_EXTERNAL
|
&& dest != Downloads.DESTINATION_FILE_URI) {
|
throw new SecurityException("unauthorized destination code");
|
}
|
if (dest == Downloads.DESTINATION_FILE_URI) {
|
getContext()
|
.enforcePermission(
|
android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
Binder.getCallingPid(), Binder.getCallingUid(),
|
"need WRITE_EXTERNAL_STORAGE permission to use DESTINATION_FILE_URI");
|
checkFileUriDestination(values);
|
}
|
filteredValues.put(Downloads.COLUMN_DESTINATION, dest);
|
}
|
Integer vis = values.getAsInteger(Downloads.COLUMN_VISIBILITY);
|
if (vis == null) {
|
if (dest == Downloads.DESTINATION_EXTERNAL) {
|
filteredValues.put(Downloads.COLUMN_VISIBILITY,
|
Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
|
} else {
|
filteredValues.put(Downloads.COLUMN_VISIBILITY,
|
Downloads.VISIBILITY_HIDDEN);
|
}
|
} else {
|
filteredValues.put(Downloads.COLUMN_VISIBILITY, vis);
|
}
|
copyInteger(Downloads.COLUMN_CONTROL, values, filteredValues);
|
filteredValues.put(Downloads.COLUMN_STATUS, Downloads.STATUS_PENDING);
|
filteredValues.put(Downloads.COLUMN_LAST_MODIFICATION,
|
mSystemFacade.currentTimeMillis());
|
|
String pckg = values.getAsString(Downloads.COLUMN_NOTIFICATION_PACKAGE);
|
String clazz = values.getAsString(Downloads.COLUMN_NOTIFICATION_CLASS);
|
if (pckg != null && (clazz != null || isPublicApi)) {
|
int uid = Binder.getCallingUid();
|
try {
|
if (uid == 0 || mSystemFacade.userOwnsPackage(uid, pckg)) {
|
filteredValues.put(Downloads.COLUMN_NOTIFICATION_PACKAGE,
|
pckg);
|
if (clazz != null) {
|
filteredValues.put(Downloads.COLUMN_NOTIFICATION_CLASS,
|
clazz);
|
}
|
}
|
} catch (PackageManager.NameNotFoundException ex) {
|
/* ignored for now */
|
}
|
}
|
copyString(Downloads.COLUMN_NOTIFICATION_EXTRAS, values, filteredValues);
|
copyString(Downloads.COLUMN_COOKIE_DATA, values, filteredValues);
|
copyString(Downloads.COLUMN_USER_AGENT, values, filteredValues);
|
copyString(Downloads.COLUMN_REFERER, values, filteredValues);
|
if (getContext().checkCallingPermission(
|
Downloads.PERMISSION_ACCESS_ADVANCED) == PackageManager.PERMISSION_GRANTED) {
|
copyInteger(Downloads.COLUMN_OTHER_UID, values, filteredValues);
|
}
|
filteredValues.put(Constants.UID, Binder.getCallingUid());
|
if (Binder.getCallingUid() == 0) {
|
copyInteger(Constants.UID, values, filteredValues);
|
}
|
copyStringWithDefault(Downloads.COLUMN_TITLE, values, filteredValues,
|
"");
|
copyStringWithDefault(Downloads.COLUMN_DESCRIPTION, values,
|
filteredValues, "");
|
filteredValues.put(Downloads.COLUMN_TOTAL_BYTES, -1);
|
filteredValues.put(Downloads.COLUMN_CURRENT_BYTES, 0);
|
|
if (values.containsKey(Downloads.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)) {
|
copyBoolean(Downloads.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, values,
|
filteredValues);
|
} else {
|
// by default, make external downloads visible in the
|
// UI在默认情况下,使得下载咋外部可见
|
boolean isExternal = (dest == null || dest == Downloads.DESTINATION_EXTERNAL);
|
filteredValues.put(Downloads.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI,
|
isExternal);
|
}
|
|
if (isPublicApi) {
|
copyInteger(Downloads.COLUMN_ALLOWED_NETWORK_TYPES, values,
|
filteredValues);
|
copyBoolean(Downloads.COLUMN_ALLOW_ROAMING, values, filteredValues);
|
}
|
|
if (Constants.LOGVV) {
|
Log.v(Constants.TAG, "initiating download with UID "
|
+ filteredValues.getAsInteger(Constants.UID));
|
if (filteredValues.containsKey(Downloads.COLUMN_OTHER_UID)) {
|
Log.v(Constants.TAG,
|
"other UID "
|
+ filteredValues
|
.getAsInteger(Downloads.COLUMN_OTHER_UID));
|
}
|
}
|
|
// 启动下载服务
|
Context context = getContext();
|
context.startService(new Intent(context, DownloadService.class));
|
|
// 插入数据库
|
long rowID = db.insert(DB_TABLE, null, filteredValues);
|
if (rowID == -1) {
|
Log.d(Constants.TAG, "couldn't insert into downloads database");
|
return null;
|
}
|
|
insertRequestHeaders(db, rowID, values);
|
|
// 启动下载服务
|
context.startService(new Intent(context, DownloadService.class));
|
notifyContentChanged(uri, match);
|
return ContentUris.withAppendedId(Downloads.CONTENT_URI, rowID);
|
}
|
|
/**
|
* Check that the file URI provided for DESTINATION_FILE_URI is
|
* valid.检查uri提供的DESTINATION_FILE_URI的有效性
|
*/
|
private void checkFileUriDestination(ContentValues values) {
|
String fileUri = values.getAsString(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);
|
}
|
String path = uri.getPath();
|
Log.i(TAG, "涓嬭浇璺緞:" + path);
|
if (path == null) {
|
throw new IllegalArgumentException("Invalid file URI: " + uri);
|
}
|
// 鍏佽浣跨敤澶栭儴sd鍗�
|
/*
|
* String externalPath = Environment.getExternalStorageDirectory()
|
* .getAbsolutePath(); if (!path.startsWith(externalPath)) { throw new
|
* SecurityException( "Destination must be on external storage: " +
|
* uri); }
|
*/
|
}
|
|
/**
|
* Apps with the ACCESS_DOWNLOAD_MANAGER permission can access this provider
|
* freely, subject to constraints in the rest of the code. Apps without that
|
* may still access this provider through the public API, but additional
|
* restrictions are imposed. We check those restrictions here.
|
* APP使用ACCESS_DOWNLOAD_MANAGER提供访问权限
|
* ,但是受到代码部分的控制,APP不必要通过公共API仍然可以访问,但是受到附加条件限制
|
*
|
* @param values
|
* ContentValues provided to insert()提供插入数据库的值
|
* @throws SecurityException
|
* if the caller has insufficient permissions假如调用方法权限不足的时候调用
|
*/
|
private void checkInsertPermissions(ContentValues values) {// 检查插入值权限
|
if (getContext().checkCallingOrSelfPermission(
|
Downloads.PERMISSION_ACCESS) == PackageManager.PERMISSION_GRANTED) {
|
return;
|
}
|
|
getContext().enforceCallingOrSelfPermission(
|
android.Manifest.permission.INTERNET,
|
"INTERNET permission is required to use the download manager");
|
|
// ensure the request fits within the bounds of a public API request
|
// first copy so we can remove values首先确保公共API请求符合范围,然后我们可以移除其值
|
values = new ContentValues(values);
|
|
// check columns whose values are restricted检查值被限制的列
|
enforceAllowedValues(values, Downloads.COLUMN_IS_PUBLIC_API,
|
Boolean.TRUE);
|
enforceAllowedValues(values, Downloads.COLUMN_DESTINATION,
|
Downloads.DESTINATION_FILE_URI);
|
|
if (getContext().checkCallingOrSelfPermission(
|
Downloads.PERMISSION_NO_NOTIFICATION) == PackageManager.PERMISSION_GRANTED) {
|
enforceAllowedValues(values, Downloads.COLUMN_VISIBILITY,
|
Downloads.VISIBILITY_HIDDEN, Downloads.VISIBILITY_VISIBLE);
|
} else {
|
enforceAllowedValues(values, Downloads.COLUMN_VISIBILITY,
|
Downloads.VISIBILITY_VISIBLE);
|
}
|
|
// remove the rest of the columns that are allowed (with any
|
// value)在允许的情况下删除其余的列(任意值)
|
values.remove(Downloads.COLUMN_URI);
|
values.remove(Downloads.COLUMN_TITLE);
|
values.remove(Downloads.COLUMN_DESCRIPTION);
|
values.remove(Downloads.COLUMN_MIME_TYPE);
|
values.remove(Downloads.COLUMN_FILE_NAME_HINT); // checked later in
|
// insert()
|
values.remove(Downloads.COLUMN_NOTIFICATION_PACKAGE); // checked
|
// later in
|
// insert()
|
values.remove(Downloads.COLUMN_ALLOWED_NETWORK_TYPES);
|
values.remove(Downloads.COLUMN_ALLOW_ROAMING);
|
values.remove(Downloads.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI);
|
Iterator<Map.Entry<String, Object>> iterator = values.valueSet()
|
.iterator();
|
while (iterator.hasNext()) {
|
String key = iterator.next().getKey();
|
if (key.startsWith(Downloads.RequestHeaders.INSERT_KEY_PREFIX)) {
|
iterator.remove();
|
}
|
}
|
|
// any extra columns are extraneous and disallowed不允许添加多余的额外列
|
if (values.size() > 0) {
|
StringBuilder error = new StringBuilder(
|
"Invalid columns in request: ");
|
boolean first = true;
|
for (Map.Entry<String, Object> entry : values.valueSet()) {
|
if (!first) {
|
error.append(", ");
|
}
|
error.append(entry.getKey());
|
}
|
throw new SecurityException(error.toString());
|
}
|
}
|
|
/**
|
* Remove column from values, and throw a SecurityException if the value
|
* isn't within the specified allowedValues. 通过值移除列数据,假如值不在指定范围内则抛出安全异常
|
*/
|
private void enforceAllowedValues(ContentValues values, String column,
|
Object... allowedValues) {
|
Object value = values.get(column);
|
values.remove(column);
|
for (Object allowedValue : allowedValues) {
|
if (value == null && allowedValue == null) {
|
return;
|
}
|
if (value != null && value.equals(allowedValue)) {
|
return;
|
}
|
}
|
throw new SecurityException("Invalid value for " + column + ": "
|
+ value);
|
}
|
|
/**
|
* Starts a database query 开始一个数据库查询
|
*/
|
@Override
|
public Cursor query(final Uri uri, String[] projection,
|
final String selection, final String[] selectionArgs,
|
final String sort) {
|
|
Helpers.validateSelection(selection, sAppReadableColumnsSet);
|
|
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
|
|
int match = sURIMatcher.match(uri);
|
if (match == -1) {
|
if (Constants.LOGV) {
|
Log.v(Constants.TAG, "querying unknown URI: " + uri);
|
}
|
throw new IllegalArgumentException("Unknown URI: " + uri);
|
}
|
|
if (match == 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);
|
}
|
|
SqlSelection fullSelection = getWhereClause(uri, selection,
|
selectionArgs, match);
|
|
if (Constants.LOGVV) {
|
logVerboseQueryInfo(projection, selection, selectionArgs, sort, db);
|
}
|
|
Cursor ret = db.query(DB_TABLE, projection,
|
fullSelection.getSelection(), fullSelection.getParameters(),
|
null, null, sort);
|
|
if (ret != null) {
|
ret = new ReadOnlyCursorWrapper(ret);
|
}
|
|
if (ret != null) {
|
ret.setNotificationUri(getContext().getContentResolver(), uri);
|
if (Constants.LOGVV) {
|
Log.v(Constants.TAG, "created cursor " + ret + " on behalf of "
|
+ Binder.getCallingPid());
|
}
|
} else {
|
if (Constants.LOGV) {
|
Log.v(Constants.TAG, "query failed in downloads database");
|
}
|
}
|
|
return ret;
|
}
|
|
/**
|
* 查询信息详细日志
|
*
|
* @param projection
|
* @param selection
|
* @param selectionArgs
|
* @param sort
|
* @param db
|
*/
|
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(".");
|
Log.v(Constants.TAG, sb.toString());
|
}
|
|
/**
|
* 通过uri获取下载项ID
|
*
|
* @param uri
|
* @return
|
*/
|
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(Downloads.RequestHeaders.COLUMN_DOWNLOAD_ID, downloadId);
|
for (Map.Entry<String, Object> entry : values.valueSet()) {
|
String key = entry.getKey();
|
if (key.startsWith(Downloads.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(Downloads.RequestHeaders.COLUMN_HEADER,
|
parts[0].trim());
|
rowValues.put(Downloads.RequestHeaders.COLUMN_VALUE,
|
parts[1].trim());
|
db.insert(Downloads.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 = Downloads.RequestHeaders.COLUMN_DOWNLOAD_ID + "="
|
+ getDownloadIdFromUri(uri);
|
String[] projection = new String[] {
|
Downloads.RequestHeaders.COLUMN_HEADER,
|
Downloads.RequestHeaders.COLUMN_VALUE };
|
Cursor cursor = db.query(Downloads.RequestHeaders.HEADERS_DB_TABLE,
|
projection, where, null, null, null, null);
|
return new ReadOnlyCursorWrapper(cursor);
|
}
|
|
/**
|
* Delete request headers for downloads matching the given query.
|
*/
|
private void deleteRequestHeaders(SQLiteDatabase db, String where,
|
String[] whereArgs) {
|
String[] projection = new String[] { Downloads._ID };
|
Cursor cursor = db.query(DB_TABLE, projection, where, whereArgs, null,
|
null, null, null);
|
try {
|
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor
|
.moveToNext()) {
|
long id = cursor.getLong(0);
|
String idWhere = Downloads.RequestHeaders.COLUMN_DOWNLOAD_ID
|
+ "=" + id;
|
db.delete(Downloads.RequestHeaders.HEADERS_DB_TABLE, idWhere,
|
null);
|
}
|
} finally {
|
cursor.close();
|
}
|
}
|
|
/**
|
* 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, sAppReadableColumnsSet);
|
|
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
|
int count;
|
boolean startService = false;
|
|
if (values.containsKey(Downloads.COLUMN_DELETED)) {
|
if (values.getAsInteger(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(Downloads.COLUMN_APP_DATA, values, filteredValues);
|
copyInteger(Downloads.COLUMN_VISIBILITY, values, filteredValues);
|
Integer i = values.getAsInteger(Downloads.COLUMN_CONTROL);
|
if (i != null) {
|
filteredValues.put(Downloads.COLUMN_CONTROL, i);
|
startService = true;
|
}
|
|
copyInteger(Downloads.COLUMN_CONTROL, values, filteredValues);
|
copyString(Downloads.COLUMN_TITLE, values, filteredValues);
|
copyString(Downloads.COLUMN_DESCRIPTION, values, filteredValues);
|
copyInteger(Downloads.COLUMN_DELETED, values, filteredValues);
|
} else {
|
filteredValues = values;
|
String filename = values.getAsString(Downloads._DATA);
|
if (filename != null) {
|
Cursor c = query(uri, new String[] { Downloads.COLUMN_TITLE },
|
null, null, null);
|
if (!c.moveToFirst() || c.getString(0).length() == 0) {
|
values.put(Downloads.COLUMN_TITLE,
|
new File(filename).getName());
|
}
|
c.close();
|
}
|
|
Integer status = values.getAsInteger(Downloads.COLUMN_STATUS);
|
boolean isRestart = status != null
|
&& status == Downloads.STATUS_PENDING;
|
boolean isUserBypassingSizeLimit = values
|
.containsKey(Downloads.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
|
if (isRestart || isUserBypassingSizeLimit) {
|
startService = true;
|
}
|
}
|
|
int match = sURIMatcher.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(DB_TABLE, filteredValues,
|
selection.getSelection(), selection.getParameters());
|
} else {
|
count = 0;
|
}
|
break;
|
|
default:
|
Log.d(Constants.TAG, "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;
|
}
|
|
/**
|
* Notify of a change through both URIs (/my_downloads and /all_downloads)
|
* 通过uri通知下载发生变化
|
*
|
* @param uri
|
* either URI for the changed download(s)
|
* @param uriMatch
|
* the match ID from {@link #sURIMatcher}
|
*/
|
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 : BASE_URIS) {
|
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) {
|
selection.appendClause(Downloads._ID + " = ?",
|
getDownloadIdFromUri(uri));
|
}
|
if ((uriMatch == MY_DOWNLOADS || uriMatch == MY_DOWNLOADS_ID)
|
&& getContext().checkCallingPermission(
|
Downloads.PERMISSION_ACCESS_ALL) != PackageManager.PERMISSION_GRANTED) {
|
selection.appendClause(Constants.UID + "= ? OR "
|
+ Downloads.COLUMN_OTHER_UID + "= ?",
|
Binder.getCallingUid(), Binder.getCallingPid());
|
}
|
return selection;
|
}
|
|
/**
|
* Deletes a row in the database 删除数据库中的一行
|
*/
|
@Override
|
public int delete(final Uri uri, final String where,
|
final String[] whereArgs) {
|
|
Helpers.validateSelection(where, sAppReadableColumnsSet);
|
|
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
int count;
|
int match = sURIMatcher.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(DB_TABLE, selection.getSelection(),
|
selection.getParameters());
|
break;
|
|
default:
|
Log.d(Constants.TAG, "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(Uri uri, String mode)
|
throws FileNotFoundException {
|
if (Constants.LOGVV) {
|
logVerboseOpenFileInfo(uri, mode);
|
}
|
|
Cursor cursor = query(uri, new String[] { "_data" }, null, null, null);
|
String path;
|
try {
|
int count = (cursor != null) ? cursor.getCount() : 0;
|
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 {
|
if (cursor != null) {
|
cursor.close();
|
}
|
}
|
|
if (path == null) {
|
throw new FileNotFoundException("No filename found.");
|
}
|
if (!Helpers.isFilenameValid(path)) {
|
throw new FileNotFoundException("Invalid filename.");
|
}
|
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) {
|
if (Constants.LOGV) {
|
Log.v(Constants.TAG, "couldn't open file");
|
}
|
throw new FileNotFoundException("couldn't open file");
|
}
|
return ret;
|
}
|
|
/**
|
* 打开文件详细日志
|
*
|
* @param uri
|
* @param mode
|
*/
|
private void logVerboseOpenFileInfo(Uri uri, String mode) {
|
Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode
|
+ ", uid: " + Binder.getCallingUid());
|
Cursor cursor = query(Downloads.CONTENT_URI, new String[] { "_id" },
|
null, null, "_id");
|
if (cursor == null) {
|
Log.v(Constants.TAG, "null cursor in openFile");
|
} else {
|
if (!cursor.moveToFirst()) {
|
Log.v(Constants.TAG, "empty cursor in openFile");
|
} else {
|
do {
|
Log.v(Constants.TAG, "row " + cursor.getInt(0)
|
+ " available");
|
} while (cursor.moveToNext());
|
}
|
cursor.close();
|
}
|
cursor = query(uri, new String[] { "_data" }, null, null, null);
|
if (cursor == null) {
|
Log.v(Constants.TAG, "null cursor in openFile");
|
} else {
|
if (!cursor.moveToFirst()) {
|
Log.v(Constants.TAG, "empty cursor in openFile");
|
} else {
|
String filename = cursor.getString(0);
|
Log.v(Constants.TAG, "filename in openFile: " + filename);
|
if (new java.io.File(filename).isFile()) {
|
Log.v(Constants.TAG, "file exists in openFile");
|
}
|
}
|
cursor.close();
|
}
|
}
|
|
/**
|
* 整数复制
|
*
|
* @param key
|
* @param from
|
* @param to
|
*/
|
private static final void copyInteger(String key, ContentValues from,
|
ContentValues to) {
|
Integer i = from.getAsInteger(key);
|
if (i != null) {
|
to.put(key, i);
|
}
|
}
|
|
/**
|
* Boolean复制
|
*
|
* @param key
|
* @param from
|
* @param to
|
*/
|
private static final void copyBoolean(String key, ContentValues from,
|
ContentValues to) {
|
Boolean b = from.getAsBoolean(key);
|
if (b != null) {
|
to.put(key, b);
|
}
|
}
|
|
/**
|
* 字符串复制
|
*
|
* @param key
|
* @param from
|
* @param to
|
*/
|
private static final void copyString(String key, ContentValues from,
|
ContentValues to) {
|
String s = from.getAsString(key);
|
if (s != null) {
|
to.put(key, s);
|
}
|
}
|
|
/**
|
* 复制默认字符串
|
*
|
* @param key
|
* @param from
|
* @param to
|
* @param defaultValue
|
*/
|
private static final void copyStringWithDefault(String key,
|
ContentValues from, ContentValues to, String defaultValue) {
|
copyString(key, from, to);
|
if (!to.containsKey(key)) {
|
to.put(key, defaultValue);
|
}
|
}
|
|
/**
|
* 只读取外部cursor
|
*
|
* @author weikou2015
|
*
|
*/
|
private class ReadOnlyCursorWrapper extends CursorWrapper implements
|
CrossProcessCursor {
|
public ReadOnlyCursorWrapper(Cursor cursor) {
|
super(cursor);
|
mCursor = (CrossProcessCursor) cursor;
|
}
|
|
@SuppressWarnings("unused")
|
public boolean deleteRow() {
|
throw new SecurityException(
|
"Download manager cursors are read-only");
|
}
|
|
@SuppressWarnings("unused")
|
public boolean commitUpdates() {
|
throw new SecurityException(
|
"Download manager cursors are read-only");
|
}
|
|
public void fillWindow(int pos, CursorWindow window) {
|
mCursor.fillWindow(pos, window);
|
}
|
|
public CursorWindow getWindow() {
|
return mCursor.getWindow();
|
}
|
|
public boolean onMove(int oldPosition, int newPosition) {
|
return mCursor.onMove(oldPosition, newPosition);
|
}
|
|
private CrossProcessCursor mCursor;
|
}
|
|
}
|