今天来说说DownloadManager的功能,看类名就知道是一个下载管理器,处理下载操作的,那么这个怎么用呢?
一般来说搭配BroadcastReceiver 来使用,因为下载管理器在下载完成的时候是会发送广播的,而且在通知栏也可以设置是否显示下载的通知,也可以有点击事件,这在DownloadManager里面有对应的常量;下载管理器那肯定是下载的,那么我想知道我下载的文件大小呢?下载的进度呢?别急,首先来说说源码,以下基本都包括了 也标有注释
/**
* 下载管理器是一个处理长时间HTTP下载的系统服务。客户端可以请求将URI下载到特定的目标文件。下载管理器将在
* 后台进行下载,处理HTTP交互,并在失败后或连接更改和系统重新启动时重试下载。
* 通过此API请求下载的应用程序应为ACTION_NOTIFICATION_LICKED注册广播接收器,以便在用户在通知中或从下
* 载UI中单击正在运行的下载时进行适当处理。
* 必须具有android.Manifest.permission.INTERNET权限才能使用此类。
*/
@SystemService(Context.DOWNLOAD_SERVICE)
public class DownloadManager {
/**
* 特定下载的标识符,在整个系统中是唯一的。客户端使用此ID进行与下载相关的后续操作
*/
public final static String COLUMN_ID = Downloads.Impl._ID;
/**
* 显示在系统通知中的标题
*/
public final static String COLUMN_TITLE = Downloads.Impl.COLUMN_TITLE;
/**
* 下载的URI
*/
public final static String COLUMN_URI = Downloads.Impl.COLUMN_URI;
/**
* 下载文件的internet媒体类型
*/
public final static String COLUMN_MEDIA_TYPE = "media_type";
/**
* 下载的总大小(字节)。最初是-1
*/
public final static String COLUMN_TOTAL_SIZE_BYTES = "total_size";
/**
* 存储下载文件的Uri。如果提供了目的地,使用该URI。否则默认为空,下载开始
* 之后使用生成的URI
*/
public final static String COLUMN_LOCAL_URI = "local_uri";
/**
* 下载的当前状态
*/
public final static String COLUMN_STATUS = Downloads.Impl.COLUMN_STATUS;
/**
* 提供下载状态的详细信息。取决于COLUMN_STATUS的值。当COLUMN_STATUS 为
* STATUS_FAILED时,表示发生的错误类型。如果发生HTTP错误,这将保存RFC 2616中定义的HTTP状态代
* 码。否则,它将保存ERROR_*常数之一。当COLUMN_STATUS为STATUS_PAUSED时,表示下载暂停的原
* 因。保存PAUSED_*常数之一。如果COLUMN_STATUS既不是STATUS_FAILED也不是STATUS_PAUSED,则
* 表示此列的值未定。
* RFC 2616 查看 https://www.rfc-editor.org/rfc/rfc9110.html
*/
public final static String COLUMN_REASON = "reason";
/**
* @link COLUMN_STATUS 下载状态 开始时
*/
public final static int STATUS_PENDING = 1 << 0;
/**
* @link COLUMN_STATUS 下载正在运行时
*/
public final static int STATUS_RUNNING = 1 << 1;
/**
* @link COLUMN_STATUS} 下载等待重试或者恢复时
*/
public final static int STATUS_PAUSED = 1 << 2;
/**
* @link COLUMN_STATUS 下载成功完成
*/
public final static int STATUS_SUCCESSFUL = 1 << 3;
/**
* @link COLUMN_STATUS 下载失败 (不会重试)
*/
public final static int STATUS_FAILED = 1 << 4;
/**
* 下载错误 不符合其他错误的时候
*/
public final static int ERROR_UNKNOWN = 1000;
// 下面是一些下载错误的状态
...
/**
* @link COLUMN_REASON} 由网络错误而暂停下载时 COLUMN_REASON的值 重试下载之前 正在等待开始
*/
public final static int PAUSED_WAITING_TO_RETRY = 1;
/**
* @link COLUMN_REASON 下载正在等待网络连接进行时
*/
public final static int PAUSED_WAITING_FOR_NETWORK = 2;
/**
* @link COLUMN_REASON 当下载超过移动网络下载的大小限制 并且下载正在等待Wi-Fi连接继续时
*/
public final static int PAUSED_QUEUED_FOR_WIFI = 3;
/**
* @link COLUMN_REASON 下载由于某些其他原因而暂停时
*/
public final static int PAUSED_UNKNOWN = 4;
/**
* 下载完成时,发送广播事件
*/
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
public final static String ACTION_DOWNLOAD_COMPLETE = "android.intent.action.DOWNLOAD_COMPLETE";
/**
* 当用户点击从下拉系统通知UI时,发送广播事件。
*/
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
public final static String ACTION_NOTIFICATION_CLICKED =
"android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED";
// 省略代码
...
/**
* 此类包含请求新下载所需的所有信息。URI是必传的参数。默认下载目标是可以共享的,如果系统需要回收空间
* 的话,系统会在其中删除文件。如果觉得这个有问题,请用外部存储。看这个 @link setDestinationUri(Uri)
*/
public static class Request {
/**
* 移动
*/
public static final int NETWORK_MOBILE = 1 << 0;
/**
* WIFI
*/
public static final int NETWORK_WIFI = 1 << 1;
@UnsupportedAppUsage
private Uri mUri;
private Uri mDestinationUri;
private List<Pair<String, String>> mRequestHeaders = new ArrayList<Pair<String, String>>();
private CharSequence mTitle;
private CharSequence mDescription;
private String mMimeType;
// 默认为允许的所有网络类型
private int mAllowedNetworkTypes = ~0;
private boolean mRoamingAllowed = true;
private boolean mMeteredAllowed = true;
private int mFlags = 0;
private boolean mIsVisibleInDownloadsUi = true;
private boolean mScannable = false;
/** if a file is designated as a MediaScanner scannable file, the following value is
* stored in the database column {@link Downloads.Impl#COLUMN_MEDIA_SCANNED}.
*/
private static final int SCANNABLE_VALUE_YES = Downloads.Impl.MEDIA_NOT_SCANNED;
// value of 1 is stored in the above column by DownloadProvider after it is scanned by
// MediaScanner
/** if a file is designated as a file that should not be scanned by MediaScanner,
* the following value is stored in the database column
* {@link Downloads.Impl#COLUMN_MEDIA_SCANNED}.
*/
private static final int SCANNABLE_VALUE_NO = Downloads.Impl.MEDIA_NOT_SCANNABLE;
/**
* 下载可见
*/
public static final int VISIBILITY_VISIBLE = 0;
/**
* 下载可见,进行中和完成后都显示
*/
public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1;
/**
* 下载隐藏 不可见
*/
public static final int VISIBILITY_HIDDEN = 2;
/**
* 下载只在完成后的通知中显示
* {@link DownloadManager#addCompletedDownload(String, String,
* boolean, String, String, long, boolean)}.
*/
public static final int VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION = 3;
/**
* 通知默认是可见的
*/
private int mNotificationVisibility = VISIBILITY_VISIBLE;
/**
* @param uri 下载的uri
*/
public Request(Uri uri) {
if (uri == null) {
throw new NullPointerException();
}
String scheme = uri.getScheme();
if (scheme == null || (!scheme.equals("http") && !scheme.equals("https"))) {
throw new IllegalArgumentException("Can only download HTTP/HTTPS URIs: " + uri);
}
mUri = uri;
}
Request(String uriString) {
mUri = Uri.parse(uriString);
}
/**
* 设置下载文件的本地目标。必须得是指向外部存储上路径的文件URI,并且调用应用程序必须具有
* WRITE_EXTERNAL_STORAGE权限。
* 默认下载保存到共享下载缓存中生成的文件名中,系统可以随时删除下载回收空间。
* 对于Q或更高版本,不需要WRITE EXTERNAL_STORAGE权限,并且uri必须引用应用程序拥有的目录内的路径(比
* 如Context.getExternalFilesDir() )或顶级下载目录内的路径(Environment.getExternalStoragePublicDirectory()
* 和Environment.directory_Downloads返回)。
* 参数:uri–下载文件目标的文件uri
*/
public Request setDestinationUri(Uri uri) {
mDestinationUri = uri;
return this;
}
/**
* 下载文件的本地目标设置为应用程序外部文件目录 @link Context#getExternalFilesDir(String)
*
* @param context
* @param dirType 传递给Context.getExternalFilesDir(String))的目录类型
* @param subPath 外部目录中的路径,包括文件名
*/
public Request setDestinationInExternalFilesDir(Context context, String dirType,
String subPath) {
final File file = context.getExternalFilesDir(dirType);
if (file == null) {
throw new IllegalStateException("Failed to get external storage files directory");
} else if (file.exists()) {
if (!file.isDirectory()) {
throw new IllegalStateException(file.getAbsolutePath() +
" already exists and is not a directory");
}
} else {
if (!file.mkdirs()) {
throw new IllegalStateException("Unable to create directory: "+
file.getAbsolutePath());
}
}
setDestinationFromBase(file, subPath);
return this;
}
/**
* 下载文件的本地目标设置为应用程序外部文件目录 @link Context#getExternalStoragePublicDirectory(String)
* @param dirType 传递给Context.getExternalStoragePublicDirectory(String))的目录类型
* @param subPath 外部目录中的路径,包括文件名
*/
public Request setDestinationInExternalPublicDir(String dirType, String subPath) {
File file = Environment.getExternalStoragePublicDirectory(dirType);
if (file == null) {
throw new IllegalStateException("Failed to get external storage public directory");
}
final Context context = AppGlobals.getInitialApplication();
if (context.getApplicationInfo().targetSdkVersion
>= Build.VERSION_CODES.Q || !Environment.isExternalStorageLegacy()) {
try (ContentProviderClient client = context.getContentResolver()
.acquireContentProviderClient(Downloads.Impl.AUTHORITY)) {
final Bundle extras = new Bundle();
extras.putString(Downloads.DIR_TYPE, dirType);
client.call(Downloads.CALL_CREATE_EXTERNAL_PUBLIC_DIR, null, extras);
} catch (RemoteException e) {
throw new IllegalStateException("Unable to create directory: "
+ file.getAbsolutePath());
}
} else {
if (file.exists()) {
if (!file.isDirectory()) {
throw new IllegalStateException(file.getAbsolutePath()
+ " already exists and is not a directory");
}
} else if (!file.mkdirs()) {
throw new IllegalStateException("Unable to create directory: "
+ file.getAbsolutePath());
}
}
setDestinationFromBase(file, subPath);
return this;
}
private void setDestinationFromBase(File base, String subPath) {
if (subPath == null) {
throw new NullPointerException("subPath cannot be null");
}
// 通过已编码的路径段 创建新的Uri。
mDestinationUri = Uri.withAppendedPath(Uri.fromFile(base), subPath);
}
/**
* 添加请求头
*/
public Request addRequestHeader(String header, String value) {
if (header == null) {
throw new NullPointerException("header cannot be null");
}
if (header.contains(":")) {
throw new IllegalArgumentException("header may not contain ':'");
}
if (value == null) {
value = "";
}
mRequestHeaders.add(Pair.create(header, value));
return this;
}
/**
* 设置标题
*/
public Request setTitle(CharSequence title) {
mTitle = title;
return this;
}
/**
* 描述
*/
public Request setDescription(CharSequence description) {
mDescription = description;
return this;
}
/**
* 类型
*/
public Request setMimeType(String mimeType) {
mMimeType = mimeType;
return this;
}
/**
* 设置通知是否显示
*/
public Request setNotificationVisibility(int visibility) {
mNotificationVisibility = visibility;
return this;
}
/**
* 设置下载的网络类型
* 一般把两种 都设置上 第三种过时了 无所谓
*/
public Request setAllowedNetworkTypes(int flags) {
mAllowedNetworkTypes = flags;
return this;
}
...
}
/**
* 查询
*/
public static class Query {
/**
* 查询排序 升序
*/
public static final int ORDER_ASCENDING = 1;
/**
* 查询排序 倒序
*/
public static final int ORDER_DESCENDING = 2;
private long[] mIds = null;
private Integer mStatusFlags = null;
private String mFilterString = null;
private String mOrderByColumn = Downloads.Impl.COLUMN_LAST_MODIFICATION;
private int mOrderDirection = ORDER_DESCENDING;
private boolean mOnlyIncludeVisibleInDownloadsUi = false;
/**
* 设置条件id
*/
public Query setFilterById(long... ids) {
mIds = ids;
return this;
}
/**
* 设置条件 字符串
*/
public Query setFilterByString(@Nullable String filter) {
mFilterString = filter;
return this;
}
/**
* 设置条件 状态
* @link STATUS_PENDING STATUS_RUNNING STATUS_PAUSED STATUS_SUCCESSFUL STATUS_FAILED
*/
public Query setFilterByStatus(int flags) {
mStatusFlags = flags;
return this;
}
/**
* 查询是否包含在系统的下载UI中不可见的下载 默认是false 全部查询
* 如果为true,则查询将仅包括应在系统的下载UI中显示的下载
*/
@UnsupportedAppUsage
public Query setOnlyIncludeVisibleInDownloadsUi(boolean value) {
mOnlyIncludeVisibleInDownloadsUi = value;
return this;
}
/**
* 返回游标的排序顺序
* @param column @link COLUMN_LAST_MODIFIED_TIMESTAMP @link COLUMN_TOTAL_SIZE_BYTES
* @param direction @link ORDER_ASCENDING or @link ORDER_DESCENDING
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public Query orderBy(String column, int direction) {
if (direction != ORDER_ASCENDING && direction != ORDER_DESCENDING) {
throw new IllegalArgumentException("Invalid direction: " + direction);
}
if (column.equals(COLUMN_LAST_MODIFIED_TIMESTAMP)) {
mOrderByColumn = Downloads.Impl.COLUMN_LAST_MODIFICATION;
} else if (column.equals(COLUMN_TOTAL_SIZE_BYTES)) {
mOrderByColumn = Downloads.Impl.COLUMN_TOTAL_BYTES;
} else {
throw new IllegalArgumentException("Cannot order by " + column);
}
mOrderDirection = direction;
return this;
}
private final ContentResolver mResolver;
private final String mPackageName;
private Uri mBaseUri = Downloads.Impl.CONTENT_URI;
private boolean mAccessFilename;
public DownloadManager(Context context) {
mResolver = context.getContentResolver();
mPackageName = context.getPackageName();
// Callers can access filename columns when targeting old platform
// versions; otherwise we throw telling them it's deprecated.
mAccessFilename = context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N;
}
/**
* Makes this object access the download provider through /all_downloads URIs rather than
* /my_downloads URIs, for clients that have permission to do so.
* @hide
*/
@UnsupportedAppUsage
public void setAccessAllDownloads(boolean accessAllDownloads) {
if (accessAllDownloads) {
mBaseUri = Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI;
} else {
mBaseUri = Downloads.Impl.CONTENT_URI;
}
}
/** {@hide} */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public void setAccessFilename(boolean accessFilename) {
mAccessFilename = accessFilename;
}
/**
* Notify {@link DownloadManager} that the given {@link MediaStore} items
* were just deleted so that {@link DownloadManager} internal data
* structures can be cleaned up.
*
* @param idToMime map from {@link BaseColumns#_ID} to
* {@link ContentResolver#getType(Uri)}.
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE)
public void onMediaStoreDownloadsDeleted(@NonNull LongSparseArray<String> idToMime) {
try (ContentProviderClient client = mResolver
.acquireUnstableContentProviderClient(mBaseUri)) {
final Bundle callExtras = new Bundle();
final long[] ids = new long[idToMime.size()];
final String[] mimeTypes = new String[idToMime.size()];
for (int i = idToMime.size() - 1; i >= 0; --i) {
ids[i] = idToMime.keyAt(i);
mimeTypes[i] = idToMime.valueAt(i);
}
callExtras.putLongArray(android.provider.Downloads.EXTRA_IDS, ids);
callExtras.putStringArray(android.provider.Downloads.EXTRA_MIME_TYPES,
mimeTypes);
client.call(android.provider.Downloads.CALL_MEDIASTORE_DOWNLOADS_DELETED,
null, callExtras);
} catch (RemoteException e) {
// Should not happen
}
}
/**
* 加入下载队列,一旦下载管理器准备好执行下载并且连接可用,下载将自动开始
*
* @param request 下载的请求
* @return 返回下载的id 唯一的id
*/
public long enqueue(Request request) {
ContentValues values = request.toContentValues(mPackageName);
Uri downloadUri = mResolver.insert(Downloads.Impl.CONTENT_URI, values);
long id = Long.parseLong(downloadUri.getLastPathSegment());
return id;
}
/**
* Marks the specified download as 'to be deleted'. This is done when a completed download
* is to be removed but the row was stored without enough info to delete the corresponding
* metadata from Mediaprovider database. Actual cleanup of this row is done in DownloadService.
*
* @param ids the IDs of the downloads to be marked 'deleted'
* @return the number of downloads actually updated
* @hide
*/
public int markRowDeleted(long... ids) {
if (ids == null || ids.length == 0) {
// called with nothing to remove!
throw new IllegalArgumentException("input param 'ids' can't be null");
}
return mResolver.delete(mBaseUri, getWhereClauseForIds(ids), getWhereArgsForIds(ids));
}
/**
* 取消下载并将从下载管理器中删除。如果每次下载都在运行,将停止,并且不再可以通过下载管理器访问。如果有下载的部分
* 或完整文件,则会将其删除
* @param ids 要删除下载的ID
* @return 返回实际删除的下载数
*/
public int remove(long... ids) {
return markRowDeleted(ids);
}
/**
* 向下载管理器中 查询已请求的下载。
*/
public Cursor query(Query query) {
return query(query, UNDERLYING_COLUMNS);
}
/** @hide */
public Cursor query(Query query, String[] projection) {
Cursor underlyingCursor = query.runQuery(mResolver, projection, mBaseUri);
if (underlyingCursor == null) {
return null;
}
return new CursorTranslator(underlyingCursor, mBaseUri, mAccessFilename);
}
/**
* 打开下载的文件进行阅读。下载必须已完成
* @param id 下载的id
*/
public ParcelFileDescriptor openDownloadedFile(long id) throws FileNotFoundException {
return mResolver.openFileDescriptor(getDownloadUri(id), "r");
}
/**
* 如果文件下载成功,返回下载文件id的Uri。否则返回null。
* @param id–下载文件的id。
*/
public Uri getUriForDownloadedFile(long id) {
// to check if the file is in cache, get its destination from the database
Query query = new Query().setFilterById(id);
Cursor cursor = null;
try {
cursor = query(query);
if (cursor == null) {
return null;
}
if (cursor.moveToFirst()) {
int status = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_STATUS));
if (DownloadManager.STATUS_SUCCESSFUL == status) {
return ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
// downloaded file not found or its status is not 'successfully completed'
return null;
}
/**
* 如果文件下载成功,返回下载文件id的媒体类型。否则返回null。
*/
public String getMimeTypeForDownloadedFile(long id) {
Query query = new Query().setFilterById(id);
Cursor cursor = null;
try {
cursor = query(query);
if (cursor == null) {
return null;
}
while (cursor.moveToFirst()) {
return cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_MEDIA_TYPE));
}
} finally {
if (cursor != null) {
cursor.close();
}
}
// downloaded file not found or its status is not 'successfully completed'
return null;
}
/**
* 重新启动下载
*/
@UnsupportedAppUsage
public void restartDownload(long... ids) {
Cursor cursor = query(new Query().setFilterById(ids));
try {
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
int status = cursor.getInt(cursor.getColumnIndex(COLUMN_STATUS));
if (status != STATUS_SUCCESSFUL && status != STATUS_FAILED) {
throw new IllegalArgumentException("Cannot restart incomplete download: "
+ cursor.getLong(cursor.getColumnIndex(COLUMN_ID)));
}
}
} finally {
cursor.close();
}
ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1);
values.putNull(Downloads.Impl._DATA);
values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, 0);
mResolver.update(mBaseUri, values, getWhereClauseForIds(ids), getWhereArgsForIds(ids));
}
/**
* 即使下载的大小 大于getMaxBytesOverMobile,也强制下载继续。
*/
public void forceDownload(long... ids) {
ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
values.put(Downloads.Impl.COLUMN_CONTROL, Downloads.Impl.CONTROL_RUN);
values.put(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT, 1);
mResolver.update(mBaseUri, values, getWhereClauseForIds(ids), getWhereArgsForIds(ids));
}
/**
* 如果下载已完成,则重命名
*/
public boolean rename(Context context, long id, String displayName) {
if (!FileUtils.isValidFatFilename(displayName)) {
throw new SecurityException(displayName + " is not a valid filename");
}
final String filePath;
final Query query = new Query().setFilterById(id);
try (Cursor cursor = query(query)) {
if (cursor == null) {
throw new IllegalStateException("Missing cursor for download id=" + id);
}
if (cursor.moveToFirst()) {
final int status = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_STATUS));
if (status != DownloadManager.STATUS_SUCCESSFUL) {
throw new IllegalStateException("Download is not completed yet: "
+ DatabaseUtils.dumpCurrentRowToString(cursor));
}
filePath = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_LOCAL_FILENAME));
if (filePath == null) {
throw new IllegalStateException("Download doesn't have a valid file path: "
+ DatabaseUtils.dumpCurrentRowToString(cursor));
} else if (!new File(filePath).exists()) {
throw new IllegalStateException("Downloaded file doesn't exist anymore: "
+ DatabaseUtils.dumpCurrentRowToString(cursor));
}
} else {
throw new IllegalStateException("Missing download id=" + id);
}
}
final File before = new File(filePath);
final File after = new File(before.getParentFile(), displayName);
if (after.exists()) {
throw new IllegalStateException("File already exists: " + after);
}
if (!before.renameTo(after)) {
throw new IllegalStateException(
"Failed to rename file from " + before + " to " + after);
}
// TODO: DownloadProvider.update() should take care of updating corresponding
// MediaProvider entries.
MediaStore.scanFile(mResolver, before);
MediaStore.scanFile(mResolver, after);
final ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_TITLE, displayName);
values.put(Downloads.Impl._DATA, after.toString());
values.putNull(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI);
final long[] ids = { id };
return mResolver.update(
mBaseUri, values, getWhereClauseForIds(ids), getWhereArgsForIds(ids)) == 1;
}
...
/**
* 添加文件到下载数据库系统中
* Q或更高版本,路径必须位于所拥有的目录{例如Context.getExternalFilesDir(String)},或者如果应用程序在旧存储模型下
* 运行(请参见android:requestLegacyExternalStorage),路径也可以位于顶级下载目录中(由
* Environment.getExternalStoragePublicDirectory(String)和 Environment.directory_Downloads返回)
* {@link Environment#getExternalStoragePublicDirectory(String)} with
* {@link Environment#DIRECTORY_DOWNLOADS}).
* 返回id
*/
@Deprecated
public long addCompletedDownload(String title, String description,
boolean isMediaScannerScannable, String mimeType, String path, long length,
boolean showNotification) {
return addCompletedDownload(title, description, isMediaScannerScannable, mimeType, path,
length, showNotification, false, null, null);
}
@Deprecated
public long addCompletedDownload(String title, String description,
boolean isMediaScannerScannable, String mimeType, String path, long length,
boolean showNotification, Uri uri, Uri referer) {
return addCompletedDownload(title, description, isMediaScannerScannable, mimeType, path,
length, showNotification, false, uri, referer);
}
@Deprecated
public long addCompletedDownload(String title, String description,
boolean isMediaScannerScannable, String mimeType, String path, long length,
boolean showNotification, boolean allowWrite) {
return addCompletedDownload(title, description, isMediaScannerScannable, mimeType, path,
length, showNotification, allowWrite, null, null);
}
@Deprecated
public long addCompletedDownload(String title, String description,
boolean isMediaScannerScannable, String mimeType, String path, long length,
boolean showNotification, boolean allowWrite, Uri uri, Uri referer) {
// make sure the input args are non-null/non-zero
validateArgumentIsNonEmpty("title", title);
validateArgumentIsNonEmpty("description", description);
validateArgumentIsNonEmpty("path", path);
validateArgumentIsNonEmpty("mimeType", mimeType);
if (length < 0) {
throw new IllegalArgumentException(" invalid value for param: totalBytes");
}
// if there is already an entry with the given path name in downloads.db, return its id
Request request;
if (uri != null) {
request = new Request(uri);
} else {
request = new Request(NON_DOWNLOADMANAGER_DOWNLOAD);
}
request.setTitle(title)
.setDescription(description)
.setMimeType(mimeType);
if (referer != null) {
request.addRequestHeader("Referer", referer.toString());
}
ContentValues values = request.toContentValues(null);
values.put(Downloads.Impl.COLUMN_DESTINATION,
Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD);
values.put(Downloads.Impl._DATA, path);
values.put(Downloads.Impl.COLUMN_MIME_TYPE, resolveMimeType(new File(path)));
values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_SUCCESS);
values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, length);
values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED,
(isMediaScannerScannable) ? Request.SCANNABLE_VALUE_YES :
Request.SCANNABLE_VALUE_NO);
values.put(Downloads.Impl.COLUMN_VISIBILITY, (showNotification) ?
Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION : Request.VISIBILITY_HIDDEN);
values.put(Downloads.Impl.COLUMN_ALLOW_WRITE, allowWrite ? 1 : 0);
Uri downloadUri = mResolver.insert(Downloads.Impl.CONTENT_URI, values);
if (downloadUri == null) {
return -1;
}
return Long.parseLong(downloadUri.getLastPathSegment());
}
...
/**
* DownloadProvider返回的游标,并显示一组不同的列,这些列是在DownloadManager.COLUMN_*常
* 量中定义。
* 主要是封装了一下 返回的各种状态,以及成功失败的状态码
*/
private static class CursorTranslator extends CursorWrapper {
private final Uri mBaseUri;
private final boolean mAccessFilename;
public CursorTranslator(Cursor cursor, Uri baseUri, boolean accessFilename) {
super(cursor);
mBaseUri = baseUri;
mAccessFilename = accessFilename;
}
@Override
public int getInt(int columnIndex) {
return (int) getLong(columnIndex);
}
@Override
public long getLong(int columnIndex) {
if (getColumnName(columnIndex).equals(COLUMN_REASON)) {
return getReason(super.getInt(getColumnIndex(Downloads.Impl.COLUMN_STATUS)));
} else if (getColumnName(columnIndex).equals(COLUMN_STATUS)) {
return translateStatus(super.getInt(getColumnIndex(Downloads.Impl.COLUMN_STATUS)));
} else {
return super.getLong(columnIndex);
}
}
@Override
public String getString(int columnIndex) {
final String columnName = getColumnName(columnIndex);
switch (columnName) {
case COLUMN_LOCAL_URI:
return getLocalUri();
case COLUMN_LOCAL_FILENAME:
if (!mAccessFilename) {
throw new SecurityException(
"COLUMN_LOCAL_FILENAME is deprecated;"
+ " use ContentResolver.openFileDescriptor() instead");
}
default:
return super.getString(columnIndex);
}
}
private String getLocalUri() {
long destinationType = getLong(getColumnIndex(Downloads.Impl.COLUMN_DESTINATION));
if (destinationType == Downloads.Impl.DESTINATION_FILE_URI ||
destinationType == Downloads.Impl.DESTINATION_EXTERNAL ||
destinationType == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
String localPath = super.getString(getColumnIndex(COLUMN_LOCAL_FILENAME));
if (localPath == null) {
return null;
}
return Uri.fromFile(new File(localPath)).toString();
}
// return content URI for cache download
long downloadId = getLong(getColumnIndex(Downloads.Impl._ID));
return ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, downloadId).toString();
}
private long getReason(int status) {
switch (translateStatus(status)) {
case STATUS_FAILED:
return getErrorCode(status);
case STATUS_PAUSED:
return getPausedReason(status);
default:
return 0; // arbitrary value when status is not an error
}
}
private long getPausedReason(int status) {
switch (status) {
case Downloads.Impl.STATUS_WAITING_TO_RETRY:
return PAUSED_WAITING_TO_RETRY;
case Downloads.Impl.STATUS_WAITING_FOR_NETWORK:
return PAUSED_WAITING_FOR_NETWORK;
case Downloads.Impl.STATUS_QUEUED_FOR_WIFI:
return PAUSED_QUEUED_FOR_WIFI;
default:
return PAUSED_UNKNOWN;
}
}
private long getErrorCode(int status) {
if ((400 <= status && status < Downloads.Impl.MIN_ARTIFICIAL_ERROR_STATUS)
|| (500 <= status && status < 600)) {
// HTTP status code
return status;
}
switch (status) {
case Downloads.Impl.STATUS_FILE_ERROR:
return ERROR_FILE_ERROR;
case Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE:
case Downloads.Impl.STATUS_UNHANDLED_REDIRECT:
return ERROR_UNHANDLED_HTTP_CODE;
case Downloads.Impl.STATUS_HTTP_DATA_ERROR:
return ERROR_HTTP_DATA_ERROR;
case Downloads.Impl.STATUS_TOO_MANY_REDIRECTS:
return ERROR_TOO_MANY_REDIRECTS;
case Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR:
return ERROR_INSUFFICIENT_SPACE;
case Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR:
return ERROR_DEVICE_NOT_FOUND;
case Downloads.Impl.STATUS_CANNOT_RESUME:
return ERROR_CANNOT_RESUME;
case Downloads.Impl.STATUS_FILE_ALREADY_EXISTS_ERROR:
return ERROR_FILE_ALREADY_EXISTS;
default:
return ERROR_UNKNOWN;
}
}
private int translateStatus(int status) {
switch (status) {
case Downloads.Impl.STATUS_PENDING:
return STATUS_PENDING;
case Downloads.Impl.STATUS_RUNNING:
return STATUS_RUNNING;
case Downloads.Impl.STATUS_PAUSED_BY_APP:
case Downloads.Impl.STATUS_WAITING_TO_RETRY:
case Downloads.Impl.STATUS_WAITING_FOR_NETWORK:
case Downloads.Impl.STATUS_QUEUED_FOR_WIFI:
return STATUS_PAUSED;
case Downloads.Impl.STATUS_SUCCESS:
return STATUS_SUCCESSFUL;
default:
assert Downloads.Impl.isStatusError(status);
return STATUS_FAILED;
}
}
}
}
以上基本就是DownloadManager的源码了,其实也没有太多,首先构建一个请求,把要下载的url传进去,然后设置一些下载请求的参数,比如通知栏的标题、描述、网络类型、是否显示在通知栏等等;然后通过ContentValues传递参数,加入下载队列,然后开始下载;
...
val request = DownloadManager.Request(Uri.parse(fileMsg.url)) //添加下载文件的网络路径
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileMsg.name) //添加保存文件路径与名称
request.setTitle(fileMsg.name) //添加在通知栏里显示的标题
request.setDescription(StringUtils.getString(R.string.down_loading)) //添加在通知栏里显示的描述
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE or DownloadManager.Request.NETWORK_WIFI) //设置下载的网络类型
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) //下载中与下载完成后都会在通知中显示| 另外可以选 DownloadManager.Request.VISIBILITY_VISIBLE 仅在下载中时显示在通知中,完成后会自动隐藏
val downloadId = downloadManager.enqueue(request) //加入队列,会返回一个唯一下载id
// 线程池 更好的利用资源
executorServiceSingle.submit(UpdateProcessTask(downloadManager, downloadId, item))
但是在下载之前,咱们还需要有其他的逻辑操作,咱们不能来一个就直接下载吧?是不是得判断当前文件的地址是不是下载成功?或者正在下载?所以咱们得有一段逻辑判断,下面就开始写代码
/**
* 广播接受器, 下载完成监听器
*/
val receiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
val action = intent.action
// 下载完成
if (action == DownloadManager.ACTION_DOWNLOAD_COMPLETE) {
//获取当前完成任务的ID
val reference = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
// 自行根据需求写逻辑
}
if (action == DownloadManager.ACTION_NOTIFICATION_CLICKED) {
//广播被点击了
}
}
}
fun downloadFile(activity: Activity, executorServiceSingle: ExecutorService, url: String) {
try {
val downloadManager = activity.getSystemService(AppCompatActivity.DOWNLOAD_SERVICE) as DownloadManager
// 查询是否存在
val (isExit, id) = queryExist(activity, url)
// 存在即是下载过了
if (isExit) {
id?.let {
// 根据下载返回的id或者下载文件的uri 然后打开这个文件
val uri = downloadManager.getUriForDownloadedFile(it)
openFileInBrowser(activity, uri, url)
}
} else {
val request = DownloadManager.Request(Uri.parse(fileMsg.url)) //添加下载文件的网络路径
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileMsg.name) //添加保存文件路径与名称
request.setTitle(name) //添加在通知栏里显示的标题
request.setDescription(StringUtils.getString(R.string.down_loading)) //添加在通知栏里显示的描述
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE or DownloadManager.Request.NETWORK_WIFI) //设置下载的网络类型
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) //下载中与下载完成后都会在通知中显示| 另外可以选 DownloadManager.Request.VISIBILITY_VISIBLE 仅在下载中时显示在通知中,完成后会自动隐藏
val downloadId = downloadManager.enqueue(request) //加入队列,会返回一个唯一下载id
executorServiceSingle.submit(UpdateProcessTask(downloadManager, downloadId))
}
} catch (e: Exception) {
StringUtils.getString(R.string.prompt_msg_15).toast()
}
}
// 查询是否下载过
fun queryExist(context: Context, url: String): Pair<Boolean, Long?> {
//获取下载管理器
val manager = context.getSystemService(AppCompatActivity.DOWNLOAD_SERVICE) as DownloadManager
//获取下载器任务队列
val query = DownloadManager.Query()
// 过滤条件 查询下载成功的
query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL)
manager.query(query).use {
while (it.moveToNext()) {
if (it.getString(it.getColumnIndex(DownloadManager.COLUMN_URI)).contains(url)) {
val id = it.getLong(it.getColumnIndex(DownloadManager.COLUMN_ID))
// 进行替换路径 把file:// 去掉
val path = it.getString(it.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)).replace("file://","")
// 有一些文件的路径需要解码,不然下面的判断不存在
val file = File(Uri.decode(path))
if (file.exists()) {
return Pair(true, id)
}
}
}
}
return Pair(false, null)
}
// 打开文件
private fun openFileInBrowser(context: Context, uri: Uri, url: String) {
try {
val intent = Intent(Intent.ACTION_VIEW)
intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.packageName)
intent.setDataAndType(uri, getMimeType(url))
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
context.startActivity(Intent.createChooser(intent, "Chooser"))
} catch (e: ActivityNotFoundException) {
StringUtils.getString(R.string.prompt_msg_17).toast()
}
}
// 获取类型 先是获取文件的扩展名 然后根据扩展名给定MIME类型
fun getMimeType(url: String?): String? {
var type: String? = ""
val extension = MimeTypeMap.getFileExtensionFromUrl(url)
if (extension != null) {
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
}
return type
}
class UpdateProcessTask(private val downloadManager: DownloadManager, private val downloadId: Long) : Runnable {
override fun run() {
do {
val sizeArray = getBytesAndStatus(downloadManager, downloadId)
if (sizeArray[0] <= 0) {
sizeArray[0] = 0
}
if (sizeArray[1] <= 0) {
sizeArray[1] = 1
}
val downloadProcess = sizeArray[0].toFloat() / sizeArray[1]
//获取到文件下载的状态
val downloadState = sizeArray[2]
// 如果需要更新下载进度 那么在这里发送 下载的进度和状态
LiveEventBus.get(EventKeys.FILE_DOWNLOAD_PROCESS, ChatMessageBean::class.java).post(downloadProcess)
SystemClock.sleep(200)
} while (sizeArray[0] < sizeArray[1])
}
}
/**
* 通过query查询下载状态,包括已下载数据大小,总大小,下载状态
* 根据DownloadManager 的常量获取对应的数据
* @param downloadId
*/
fun getBytesAndStatus(downloadManager: DownloadManager, downloadId: Long): IntArray {
val bytesAndStatus = intArrayOf(0, 1, 0)
val query = DownloadManager.Query().setFilterById(downloadId)
var cursor: Cursor? = null
try {
cursor = downloadManager.query(query)
if (cursor != null && cursor.moveToFirst()) {
//已经下载文件大小
bytesAndStatus[0] = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
//下载文件的总大小
bytesAndStatus[1] = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
//下载状态
bytesAndStatus[2] = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
}
} finally {
cursor?.close()
}
return bytesAndStatus
}
至此 基本就已经完成了简单的需求下载显示进度,打开文件等这只是一部分模板代码,自行可根据实际需求修改,比如UI部分,比如更新的逻辑,还有通知的点击事件等等,这里只是写出用法以及源码的方法。
源码中包括了几个部分:
Request请求:设置基本信息,标题、描述、类型等等;下载文件的本地目标设置为外部文件目录中的路径;通知显示等等;
Query查询:根据下载的id或下载中的状态条件查询下载的进度,大小等;
DownloadManager:添加下载到队列,根据id移除下载,根据id获取uri等;
CursorTranslator:封装了DownloadProvider返回的游标,显示了一组不同的列,这些列是在DownloadManager.COLUMN_*常量中定义的,某些列直接对应于基础值,而其他列则根据基础数据计算。
当然还有很多方法,这里就不一一提了,如果感兴趣的话,可以去看源码。
如果有遗漏或者不对的地方,请指正@我
网友评论