05/23
2013

Android下载管理DownloadManager功能扩展和bug修改

本文主要介绍如何修改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类修改

public int update(final Uri uri, final ContentValues values, final String where, final String[] whereArgs)

函数,修改后代码如下(只增加了一行有效代码):

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类修改

private void setupDestinationFile(State state, InnerState innerState)

函数中这个注释掉一个else if,如下:

// 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类中添加对外接口

/**
 * 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字段进行判断,如果是用户手动暂停的话,是不会自动继续的,部分代码如下:

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函数修改变为

// 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)加入

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)函数

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添加对外接口

// 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

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

} 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)) {

修改为:

} 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

/**
 * 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)函数修改如下:

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) 函数修改如下:

// 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)

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

145 thoughts on “Android下载管理DownloadManager功能扩展和bug修改

  1. Pingback: Android系统下载管理DownloadManager功能介绍及使用示例 - 移动端开发 - 无忧网

  2. 楼主你好,在调用manager.enqueue(request)时,一些手机会报java lang illegalargumentexception unknown url content://downloads/my_downl的错误,网上查了很多都查不到,还望麻烦帮忙解答下,谢谢

  3. 现在报这个错误:
    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)

  4. 这里推荐DownloadProvider@Github,系统下载管理的独立版,可整合进自己的应用,感谢@DONG童鞋提供地址。

    我从你提供的地址下载整个工程下来,不能运行,请教lz什么情况?

      • 能运行就是下载一直没下载下来。。。就是说我在开始界面上点击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

      • 有写网络设置:
        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);

        还是不行..

  5. DownloadManager.Request添加对外接口,我尝试了一下,增加字段没出问题。但是查询的时候出错,就是query拿到cursor后,拿cursor里面的字段时出错。查错后发现要改的地方好多好多啊,完全晕掉了..第一次改这么复杂的源码。。

      • 我才意识到,增加字段之后,需要到DownloadProvider中的DatabaseHelper给数据库增加字段是吧,我看到预留有addColumn方法好像可以用。还需要改些什么呢?还是对DownloadManager的结构不够熟悉,改起来好费劲,不过顺便了解了下构成

      • 做电子市场,需要增加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?这样做可以吗

      • 功能差不多了,优化确实要提上议程了。能完成功能对于我而言真的是个进步,近些日子没少学东西啊。准备好好研究下你的优化系列文章。回头需要问题可能还请多多指教呢。
        之前大概看了下,看来正好和你文章中的项目差不多呢,我就是ViewPager,3个Page,每个Page是网络取值的ListView,左右切换卡的不行。
        下载我是用的那个开源项目,话说我就是微博上的“DONG的叹息”,前几天发给你链接那个,嘿嘿~

      • 还有核心呢?你那些优化方法,我要是能学来三招两式,应该就很受用了
        经过不懈的努力,终于走通了从插入数据到取出的流程,可以顺利拿到自己的自定义数据了!顺便了解了下ContentProvider、CursorWrapper,看了Google的源码,感觉对封装又有了一定的认识。
        看来看代码需要充足的精力啊,昨天下午困的时候开始看,直接就晕了。今天是从早晨开始啃,感觉无比刘畅,哈哈

        • 哈哈,看源码就是这样,一累就犯困。不过这也是快速成长的方式啊
          优化的话因为我以前做java,这方面就有了一些经验,现在已经完成的数据库和布局优化在android里面很少成为性能瓶颈点,接下来准备写的Java代码优化才是经常出性能问题的地方。原理都很简单的你看看就会了,甚至已经用了不少,我只是做个总结而已。

  6. Pingback: Android系统下载管理DownloadManager功能介绍及使用示例 - 移动端开发 - 开发者

  7. 我对Android还不是很熟悉,请问修改系统源码再编译,是制作自己的SDK吗?。请问作为一个应用开发的话,应该如何做?可以把相关源码全部提取出来,然后修改之后作为自己的类使用吗?
    我是什么都不懂啊,假如博主可以推荐下相关的资料,我自己去补习就好了

    • 修改源码再编译也是不行的,结果只是对该系统试用的apk而已。其他系统会报缺少该接口
      理论上把整个下载管理部分提取出来放到自己的应用中是可行的,不过依赖代码较多,可能需要花费不少时间提取,这个本类是准备做的,优先级不该排在了后面
      关于系统下载管理网上资料比较少,这块的架构设计本来是计划写篇博客介绍的,最近抽不出时间。
      我的建议是这篇文章介绍的扩展功能最重要的是暂停继续,你的应用也不一定需要这两个功能,系统中原来对启动下载、取消下载都是支持的,所以你直接使用原生就好,暂停、继续功能系统下载管理Ui提供了该功能,用户可以在里面操作

  8. Pingback: Android系统下载管理DownloadManager功能介绍及使用示例 | 劳博