解析DownloadManager源码

今天来说说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_*常量中定义的,某些列直接对应于基础值,而其他列则根据基础数据计算。
当然还有很多方法,这里就不一一提了,如果感兴趣的话,可以去看源码。
如果有遗漏或者不对的地方,请指正@我

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容