本文主要介绍如何修改Android系统下载管理,以支持更多的功能及部分bug修改和如何编译生效。目前内容包括暂停下载、继续下载、通知设置NotiExtra和NotiClass、wifi切换到3g自动暂停、Bug修改。
更多下载相关开源项目可见 Android 下载。
PS: 很多童鞋不是自己做rom,所以就算修改了系统源码编译出来的包在其他系统上也不通用
这里推荐DownloadProvider@Github(并不是我的开源项目,我的项目为TrineaAndroidCommon@Github,包含图片缓存,下拉刷新,静默安装等,欢迎关注),系统下载管理的独立版,可整合进自己的应用,感谢@DONG童鞋提供地址。
下面需要修改的DownloadManager.java所在目录为frameworks/base/core/java/android/app
DownloadInfo.java, DownloadProvider.java,DownloadThread.java文件所在目录为packages/providers/DownloadProvider/src/com/android/providers/downloads
1、暂停、继续下载功能
(1) DownloadProvider类修改
1 |
public int update(final Uri uri, final ContentValues values, final String where, final String[] whereArgs) |
函数,修改后代码如下(只增加了一行有效代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
if (Binder.getCallingPid() != Process.myPid()) { filteredValues = new ContentValues(); copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues); copyInteger(Downloads.Impl.COLUMN_VISIBILITY, values, filteredValues); Integer i = values.getAsInteger(Downloads.Impl.COLUMN_CONTROL); if (i != null) { filteredValues.put(Downloads.Impl.COLUMN_CONTROL, i); startService = true; } // trinea BEGIN, added by trinea@trinea.cn 2013/03/01 copyInteger(Downloads.Impl.COLUMN_STATUS, values, filteredValues); // trinea END copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues); copyString(Downloads.Impl.COLUMN_TITLE, values, filteredValues); copyString(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, values, filteredValues); copyString(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues); copyInteger(Downloads.Impl.COLUMN_DELETED, values, filteredValues); } else { |
其中以// trinea BEGIN开头,// trinea END结尾为修改部分,下面代码示例同样如此。因为DownloadProvider安全策略对非该进程id的修改会过滤掉COLUMN_STATUS状态,所以我们需要添加该行。
(2) DownloadThread类修改
1 |
private void setupDestinationFile(State state, InnerState innerState) |
函数中这个注释掉一个else if,如下:
1 2 3 4 5 6 7 8 9 10 11 |
// trinea BEGIN, noted by trinea@trinea.cn 2013/03/01 //} else if (mInfo.mETag == null && !mInfo.mNoIntegrity) { // // This should've been caught upon failure // if (Constants.LOGVV) { // Log.d(TAG, "setupDestinationFile() unable to resume download, deleting " // + state.mFilename); // } // f.delete(); // throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME, // "Trying to resume a download that can't be resumed"); // trinea END |
上面一段代码表示一个验证过程,可以去掉。
mETag为数据库中的etag字段值,代码中没有解释,感觉是一个验证值,类似hashcode。
mNoIntegrity为数据中no_integrity字段值,表示启动下载的应用程序能否验证下载的文件的完整性。不过坑爹的是对于etag和no_integrity都没有提供设置的接口
(3) DownloadManager类中添加对外接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
/** * pause download, added by trinea@trinea.cn 2013/03/01 * * @param ids the IDs of the downloads to be paused * @return the number of downloads actually paused */ public int pauseDownload(long... ids) { if (ids == null || ids.length == 0) { // called with nothing to remove! throw new IllegalArgumentException("input param 'ids' can't be null"); } ContentValues values = new ContentValues(); values.put(Downloads.Impl.COLUMN_CONTROL, Downloads.Impl.CONTROL_PAUSED); values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PAUSED_BY_APP); if (ids.length == 1) { return mResolver.update(ContentUris.withAppendedId(mBaseUri, ids[0]), values, null, null); } return mResolver.update(mBaseUri, values, getWhereClauseForIds(ids), getWhereArgsForIds(ids)); } /** * resume download, added by trinea@trinea.cn 2013/03/01 * * @param ids the IDs of the downloads to be resumed * @return the number of downloads actually resumed */ public int resumeDownload(long... ids) { if (ids == null || ids.length == 0) { // called with nothing to remove! throw new IllegalArgumentException("input param 'ids' can't be null"); } ContentValues values = new ContentValues(); values.put(Downloads.Impl.COLUMN_CONTROL, Downloads.Impl.CONTROL_RUN); values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_RUNNING); if (ids.length == 1) { return mResolver.update(ContentUris.withAppendedId(mBaseUri, ids[0]), values, null, null); } return mResolver.update(mBaseUri, values, getWhereClauseForIds(ids), getWhereArgsForIds(ids)); } |
无论是暂停还是继续我们都是同时把Downloads.Impl.COLUMN_CONTROL和Downloads.Impl.COLUMN_STATUS字段进行修改,因为在DownloadInfo的private boolean isReadyToStart(long now)函数中,会对COLUMN_CONTROL字段进行判断,如果是用户手动暂停的话,是不会自动继续的,部分代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
private boolean isReadyToStart(long now) { if (DownloadHandler.getInstance().hasDownloadInQueue(mId)) { // already running return false; } if (mControl == Downloads.Impl.CONTROL_PAUSED) { // the download is paused, so it's not going to start Xlog.i(Constants.DL_ENHANCE, "Download is paused " + "then no need to start"); return false; } …… } |
之后我们直接调用DownloadManager的pauseDownload和resumeDownload接口即可
PS:也可以试试不做第二步的修改,而将第一步DownloadProvider的update函数修改变为
1 2 3 4 |
// trinea BEGIN, added by trinea@trinea.cn 2013/03/01 copyInteger(Downloads.Impl.COLUMN_STATUS, values, filteredValues); copyInteger(Downloads.Impl.COLUMN_NO_INTEGRITY, values, filteredValues); // trinea END |
第二步修改变为在public int resumeDownload(long… ids)加入
1 2 3 |
values.put(Downloads.Impl.COLUMN_CONTROL, Downloads.Impl.CONTROL_RUN); values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_RUNNING); values.put(Downloads.Impl.COLUMN_NO_INTEGRITY, true); |
没有亲自试,不过按照逻辑应该也可以。
2、通知栏可以设置NotiExtra和NotiClass
(1) DownloadProvider类中修改private void checkInsertPermissions(ContentValues values)函数
1 2 3 4 5 6 |
values.remove(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI); values.remove(Downloads.Impl.COLUMN_MEDIA_SCANNED); // BEGIN, added by trinea@trinea.cn 2013/03/01 values.remove(Downloads.Impl.COLUMN_NOTIFICATION_CLASS); values.remove(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS); // trinea END |
在DownloadProvider insert之前会调用checkInsertPermissions检查不能被插入的字段插入,这里我们需要允许这两个字段存在。
(2) DownloadManager.Request添加对外接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// BEGIN, added by trinea@trinea.cn 2013/03/01 private CharSequence mNotiClass; private CharSequence mNotiExtras; // trinea END /** * Set notiClass, to be used as destination when click downloading in download manager ui * * @return this object */ public Request setNotiClass(CharSequence notiClass) { mNotiClass = notiClass; return this; } /** * Set notiExtras, to be sended to notiClass when click downloading in download manager ui * * @return this object */ public Request setNotiExtras(CharSequence notiExtras) { mNotiExtras = notiExtras; return this; } ContentValues toContentValues(String packageName)中 putIfNonNull(values, Downloads.Impl.COLUMN_TITLE, mTitle); putIfNonNull(values, Downloads.Impl.COLUMN_DESCRIPTION, mDescription); putIfNonNull(values, Downloads.Impl.COLUMN_MIME_TYPE, mMimeType); // trinea BEGIN putIfNonNull(values, Downloads.Impl.COLUMN_NOTIFICATION_CLASS, mNotiClass); putIfNonNull(values, Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, mNotiExtras); // trinea END |
在Request中添加接口以及允许字段修改。通过允许设置NotiExtra和NotiClass,我们可以给系统传递更丰富的参数,在通知栏点击相应或是DownloadUi中通过broadcast将这些参数传递出来方便应用调用。
3、wifi切换到3g自动暂停
(1) 修改DownloadInfo.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
private int checkIsNetworkTypeAllowed(int networkType) { if (mIsPublicApi) { final int flag = translateNetworkTypeToApiFlag(networkType); final boolean allowAllNetworkTypes = mAllowedNetworkTypes == ~0; if (!allowAllNetworkTypes && (flag & mAllowedNetworkTypes) == 0) { return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR; } // trinea BEGIN if (mStatus == Downloads.Impl.STATUS_WAITING_FOR_NETWORK && flag != DownloadManager.Request.NETWORK_WIFI) { return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR; } // trinea END } return checkSizeAllowedForNetwork(networkType); } |
表示等待网络时始终只等待wifi
(2) 修改DownloadReceiver.java
1 2 3 4 5 6 7 |
} else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) { NetworkInfo info = (NetworkInfo) intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO); if (info != null && info.isConnected()) { startService(context); } } else if (action.equals(Constants.ACTION_RETRY)) { |
修改为:
1 2 3 4 5 6 7 8 9 10 11 12 |
} else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) { NetworkInfo info = (NetworkInfo) intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO); // trinea BEGIN /** * modified by trinea@trinea.cn @2013/04/01, resume download only when network type is wifi */ if (info != null && info.isConnected() && info.getType() == ConnectivityManager.TYPE_WIFI) { // trinea END startService(context); } } else if (action.equals(Constants.ACTION_RETRY)) { |
表示只有连接wifi时才唤醒service去检查是否下载
(3) 修改DownloadThread.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
/** * Check if the download has been paused or canceled, stopping the request appropriately if it * has been. */ private void checkPausedOrCanceled(State state) throws StopRequestException { synchronized (mInfo) { if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) { Xlog.i(Constants.DL_ENHANCE, "DownloadThread: checkPausedOrCanceled: user pause download"); throw new StopRequestException( Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner"); } if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) { throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled"); } } // if policy has been changed, trigger connectivity check if (mPolicyDirty) { // trinea BEGIN /** * add by trinea@trinea.cn @2013/04/01, when switched from wifi to 3g, pause all download */ NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid); if (info != null && !info.isConnected()) { mInfo.mStatus = Downloads.Impl.STATUS_WAITING_FOR_NETWORK; } // trinea END checkConnectivity(); } } |
表示如果网络变化并且表示网络断开时,下载状态变为等待网络。
4、Bug修改
(1) 当存储空间不足时,利用DownloadManager下载,只显示通知栏提示,在下载管理UI中不显示
DownloadManager的Cursor runQuery(ContentResolver resolver, String[] projection, Uri baseUri)函数修改如下:
1 2 3 4 5 6 |
if ((mStatusFlags & STATUS_RUNNING) != 0) { parts.add(statusClause("=", Downloads.Impl.STATUS_RUNNING)); // trinea BEGIN parts.add(statusClause("=", Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR)); // trinea END } |
DownloadManager的CursorTranslator类的private int translateStatus(int status) 函数修改如下:
1 2 3 4 5 |
// trinea BEGIN case Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR: // trinea END case Downloads.Impl.STATUS_RUNNING: return STATUS_RUNNING; |
5、编译安装
修改后是需要重新编译的,需同时编译framweork和DownloadProvider。
framework编译命令为:./makeMtk model mm frameworks/base/core/
编译后apk所在路径为out\target\product\model\system\framework\secondary_framework.jar,之后push到system/framework重启即可。编译命令中model为机型,非mtk平台命令有所不同
DownloadProvider编译命令为./makeMtk model mm packages/providers/DownloadProvider/
编译后apk所在路径为out\target\product\model\system\app\DownloadProvider.apk,之后push到system/app即可(可能需要先删除/system/app/目录下的DownloadProvider.odex)
楼主你好,为什么只能下载apk文件啊 其他类型的文件下载失败啊???
支持所有类型文件下载,你可以通过数据库看看失败的具体原因
存储下载文件信息的数据库在哪啊 找不到……
/data/data/com.android.providers.downloads/databases/downloads.db
楼主,三星s3手机上找不到数据库文件。还有请问如何修改下载目录到外置内存卡上啊,代码改动比较大了
需要root权限,否则无法查看。下载目录可通过DownloadManager.Request对象的setDestinationInExternalPublicDir函数设置,使用可见android系统下载管理downloadmanager功能介绍及使用示例
Pingback: Android系统下载管理DownloadManager功能介绍及使用示例 - 移动端开发 - 无忧网
楼主你好,在调用manager.enqueue(request)时,一些手机会报java lang illegalargumentexception unknown url content://downloads/my_downl的错误,网上查了很多都查不到,还望麻烦帮忙解答下,谢谢
1到2台手机设备出现这样的问题,将手机恢复出厂设置,又可以下载了
contentprovider没注册上,看看downloadprovider工程的manifest文件是不是有问题,可对比原生的DownloadProvider Manifest
再次麻烦楼主,拿自己工程的manifest和楼主的博文以及提供的参考网页对比下,并无发现不对的,还望再次帮忙解答下,manifest中download广播添加的声明
这段时间都在休假,怕是没时间看你那个
现在报这个错误:
Cannot cast value for no_integrity to a Boolean: 1
java.lang.ClassCastException: java.lang.Integer
at android.content.ContentValues.getAsBoolean(ContentValues.java:413)
at com.mozillaonline.providers.downloads.DownloadProvider.copyBoolean(DownloadProvider.java:1108)
at com.mozillaonline.providers.downloads.DownloadProvider.insert(DownloadProvider.java:436)
at android.content.ContentProvider$Transport.insert(ContentProvider.java:199)
at android.content.ContentResolver.insert(ContentResolver.java:618)
at com.mozillaonline.providers.DownloadManager.enqueue(DownloadManager.java:816)
at com.mozillaonline.downloadprovider.DownloadProviderActivity.startDownload(DownloadProviderActivity.java:109)
at com.mozillaonline.downloadprovider.DownloadProviderActivity.onClick(DownloadProviderActivity.java:82)
at android.view.View.performClick(View.java:2485)
at android.view.View$PerformClick.run(View.java:9080)
at android.os.Handler.handleCallback(Handler.java:587)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:130)
at android.app.ActivityThread.main(ActivityThread.java:3694)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:507)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:907)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:665)
at dalvik.system.NativeStart.main(Native Method)
这里推荐DownloadProvider@Github,系统下载管理的独立版,可整合进自己的应用,感谢@DONG童鞋提供地址。
我从你提供的地址下载整个工程下来,不能运行,请教lz什么情况?
得看你具体什么错误了,也可以联系开源作者yxl或之前已经使用过的@Dong
能运行就是下载一直没下载下来。。。就是说我在开始界面上点击start,然后再点击show download list查看下载列表一直没看见下载在进行,然后我点击下载列表选择继续也没下载,这是什么情况?
而且后台还有这两行的提示:
08-23 09:53:44.026: I/DownloadManager(980): Initiating request for download 1
08-23 09:53:44.026: W/DownloadManager(980): Aborting request for download 1: download was requested to not use the current network type
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI|DownloadManager.Request.NETWORK_MOBILE);
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI|DownloadManager.Request.NETWORK_MOBILE);
有写网络设置:
String url = mUrlInputEditText.getText().toString();
Uri srcUri = Uri.parse(url);
DownloadManager.Request request = new Request(srcUri);
////下载文件保存的位置
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, “/”);
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI);
request.setDescription(“Just for test”);
mDownloadManager.enqueue(request);
还是不行..
request.setDestinationInExternalPublicDir(“foldername”, “aa.apk”);
第二个参数是文件名,需要保证文件夹存在
lz,干嘛不考虑把你改好的代码打成一个jar包呢,这样就可以造福更多不会底层的人
因为有一些系统依赖,之前有计划但却没时间拿出来单独使用,现在github上有朋友开源了,大家可以直接下载,文章开头已提供具体地址。后面如果时间充裕的话会考虑整合到TrineaAndroidCommon中
DownloadManager.Request添加对外接口,我尝试了一下,增加字段没出问题。但是查询的时候出错,就是query拿到cursor后,拿cursor里面的字段时出错。查错后发现要改的地方好多好多啊,完全晕掉了..第一次改这么复杂的源码。。
DownloadManager.UNDERLYING_COLUMNS做了字段过滤
我才意识到,增加字段之后,需要到DownloadProvider中的DatabaseHelper给数据库增加字段是吧,我看到预留有addColumn方法好像可以用。还需要改些什么呢?还是对DownloadManager的结构不够熟悉,改起来好费劲,不过顺便了解了下构成
基本就在DatabaseHelper和provider里面。加什么字段,原来的不满足需求吗,改数据库结构维护成本比较大不建议
做电子市场,需要增加appId,与appIcon两个字段,用于唯一识别一个app,和显示app的图标。比如在下载管理界面,需要显示app的图标,用原有的Manager取出的Cursor,是拿不到图标的。
之前想单独维护一个数据库,但是想来更加麻烦,而且容易出错
我能说这种改法很sb吗。。你要知道系统下载管理应该跟任何一个应用的逻辑是分离的。你那种改法会坑死自己的。。
数据库里面已经有packageName了,有了packageName想拿应用的什么信息拿不到,icon和id都是小case啊,用PackageManager,类似如下:
Drawable icon = null;
if (iconCache.containsKey(packageName)) {
icon = iconCache.get(packageName);
} else {
icon = mPackageManager.getApplicationIcon(packageName);
}
if (icon != null) {
iconView.setImageDrawable(icon);
iconCache.put(packageName, icon);
iconView.setVisibility(View.VISIBLE);
return;
}
多谢指点!
其实本来就是将系统下载管理单独封装出来作为应用自用的,所以感觉逻辑上有关联还算合理。。
忽略了能直接取得apk文件的图标与信息了,之前一直以为只能拿到已安装的应用信息,这样确实好很多。才想起来文件管理器是可以取得apk文件图标的。。
不过正在下载还未完成的文件,是无法取得这些信息的吧。
还有个笨问题啊,如何防止重复下载同一个URL呢?
好像没提供相关方法呢,我暂时的想法是,query得到下载列表,遍历匹配URL?这样做可以吗
不是完整的apk是取不到信息的
url防止重复的问题,可以在下载前根据url查询数据库中是否已存在相关记录,给url加上唯一索引吧,既能防止重复又能加快查询速度,见:性能优化之数据库优化
另外如果你想把下载管理拿出来的话,github上有开源项目,可以直接用https://github.com/yxl/DownloadProvider
加索引可见性能优化之数据库优化
功能差不多了,优化确实要提上议程了。能完成功能对于我而言真的是个进步,近些日子没少学东西啊。准备好好研究下你的优化系列文章。回头需要问题可能还请多多指教呢。
之前大概看了下,看来正好和你文章中的项目差不多呢,我就是ViewPager,3个Page,每个Page是网络取值的ListView,左右切换卡的不行。
下载我是用的那个开源项目,话说我就是微博上的“DONG的叹息”,前几天发给你链接那个,嘿嘿~
soga,哈哈
不过优化系列的核心还没时间开始写呢
android原生的downloadmanger独立出来能运行吗
不能,现在整个代码都在你手上,自己学会debug
还有核心呢?你那些优化方法,我要是能学来三招两式,应该就很受用了
经过不懈的努力,终于走通了从插入数据到取出的流程,可以顺利拿到自己的自定义数据了!顺便了解了下ContentProvider、CursorWrapper,看了Google的源码,感觉对封装又有了一定的认识。
看来看代码需要充足的精力啊,昨天下午困的时候开始看,直接就晕了。今天是从早晨开始啃,感觉无比刘畅,哈哈
哈哈,看源码就是这样,一累就犯困。不过这也是快速成长的方式啊
优化的话因为我以前做java,这方面就有了一些经验,现在已经完成的数据库和布局优化在android里面很少成为性能瓶颈点,接下来准备写的Java代码优化才是经常出性能问题的地方。原理都很简单的你看看就会了,甚至已经用了不少,我只是做个总结而已。
这边文章开头加入了对你的感谢,哈哈
Pingback: Android系统下载管理DownloadManager功能介绍及使用示例 - 移动端开发 - 开发者
我对Android还不是很熟悉,请问修改系统源码再编译,是制作自己的SDK吗?。请问作为一个应用开发的话,应该如何做?可以把相关源码全部提取出来,然后修改之后作为自己的类使用吗?
我是什么都不懂啊,假如博主可以推荐下相关的资料,我自己去补习就好了
修改源码再编译也是不行的,结果只是对该系统试用的apk而已。其他系统会报缺少该接口
理论上把整个下载管理部分提取出来放到自己的应用中是可行的,不过依赖代码较多,可能需要花费不少时间提取,这个本类是准备做的,优先级不该排在了后面
关于系统下载管理网上资料比较少,这块的架构设计本来是计划写篇博客介绍的,最近抽不出时间。
我的建议是这篇文章介绍的扩展功能最重要的是暂停继续,你的应用也不一定需要这两个功能,系统中原来对启动下载、取消下载都是支持的,所以你直接使用原生就好,暂停、继续功能系统下载管理Ui提供了该功能,用户可以在里面操作
万分感谢!我这小菜鸟要学的东西真是太多了..十分期待博主关于下载管理的设计
最近是抽不出时间来完成这个了。
Android的DownloadManager是系统级,所以设计考虑的比较多,几乎是遵循http协议规范的。你自己设计的话不用这么复杂,用RandAccessFile加多线程就行了,给你个参考地址:用Java 实现断点续传
谢谢,已经是帮了大忙了,我再继续看看。
Pingback: Android系统下载管理DownloadManager功能介绍及使用示例 | 劳博