Android问题分享:DownloadManager基本用法及发生java.lang.SecurityException异常的解决办法

DownloadManager

这里我就简单的介绍吧,DownloadManager是一个自API 9开始加入的,用来处理http长连接的一个系统服务,我们可以使用它请求一个URI来下载到一个目标文件。DownloadManager会把下载放在后台执行,并且会在http连接失败后一旦网络恢复或者系统重启时自动尝试重新下载。我们可以通过contex.getSystemService(DOWNLOAD_SERVICE)强转获得一个DownloadManager实例。

DownloadManager的基本用法

1、
Uri uri = Uri.parse(addressString);//addressString是下载链接地址
2、
DownloadManager.Request request = new DownloadManager.Request(uri);
3、
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename);//将它下载到公共目录的Download目录,filename根据下载链接得来,别忘记检查外部存储是否可读写
//与上面类似的方法还有
request.setDestinationUri(uri);//将文件转为Uri传入,比如Uri.parse(storage/external/sdb/downloadFilename);
request.setDestinationInExternalFilesDir(context, dirType, subPath);//第二个参数表示文件类型的描述字符串,第三个参数是包括文件名在内的内部存储路径,这个方法把文件下载到外部存储的指定目录下
4、
request.allowScanningByMediaScanner();//设置允许被扫描到
5、
request.setDescription(description);//设置文件描述,可有可无
6、
request.setNotificationVisibility(
                DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);//设置在通知栏显示下载通知
7、
final DownloadManager manager
                    = (DownloadManager) activity.getSystemService(Context.DOWNLOAD_SERVICE);
            new Thread("Browser download") {
                public void run() {
                    manager.enqueue(request);
                }
            }.start();
下载的步骤大致如此,还有完成后如何查询下载的文件稍后会提到

我碰到的需求是,在TVBox上如果有U盘,就得把文件下载到U盘中去,而不是硬盘中,因为我是基于Brower源码进行开发,而Brower是用的DownloadManager下载文件,所以我还必须得在源码上进行修改,而不能自己开线程去下载。
我起初是做判断,如果有U盘,就调用request.setDestinationUri();将U盘目录及文件名拼凑起来转成uri穿进去可是:

产生的异常

E/DatabaseUtils( 1127):         java.lang.SecurityException: Destination must be on external storage: file:///storage/external_storage/sdb1/Download/com.baidu.netdisk.apk
E/DatabaseUtils( 1127):         at com.android.providers.downloads.DownloadProvider.checkFileUriDestination(DownloadProvider.java:717)
E/DatabaseUtils( 1127):         at com.android.providers.downloads.DownloadProvider.insert(DownloadProvider.java:566)
E/DatabaseUtils( 1127):         at android.content.ContentProvider$Transport.insert(ContentProvider.java:220)
E/DatabaseUtils( 1127):         at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:156)
E/DatabaseUtils( 1127):         at android.os.Binder.execTransact(Binder.java:404)
根据第二行错误提示,于是我查看了一下DownloadProvider的(4.4.2-API 19)源码,在DownloadProvider.java的第717行,系统会调用checkFileUriDestination方法对我们传入的存储位置会进行检查,具体代码如下:
    private void checkFileUriDestination(ContentValues values) {
        String fileUri = values.getAsString(Downloads.Impl.COLUMN_FILE_NAME_HINT);//查询我们下载的文件
        if (fileUri == null) {//假如要下载的文件不存在,抛出如下非法参数异常
            throw new IllegalArgumentException(
                    "DESTINATION_FILE_URI must include a file URI under COLUMN_FILE_NAME_HINT");
        }
        Uri uri = Uri.parse(fileUri);
        String scheme = uri.getScheme();
        if (scheme == null || !scheme.equals("file")) {//假如<span style="font-family: Arial, Helvetica, sans-serif;">下载地址不是绝对路径</span><span style="font-family: Arial, Helvetica, sans-serif;">或者</span>下载的不是文件,抛出如下异常
            throw new IllegalArgumentException("Not a file URI: " + uri);
        }
        final String path = uri.getPath();
        if (path == null) {//假如解码得到的路径无效,还是抛异常
            throw new IllegalArgumentException("Invalid file URI: " + uri);
        }
        try {
            final String canonicalPath = new File(path).getCanonicalPath();//下载目标存储路径
            <span style="font-family: Arial, Helvetica, sans-serif;">final String externalPath = Environment.getExternalStorageDirectory().getAbsolutePath();</span>
            if (!canonicalPath.startsWith(<span style="font-family: Arial, Helvetica, sans-serif;">externalPath </span><span style="font-family: Arial, Helvetica, sans-serif;">)) {</span>
                throw new SecurityException("Destination must be on external storage: " + uri);
            }
        } catch (IOException e) {
            throw new SecurityException("Problem resolving path: " + uri);
        }
    }

重点这一句:
            if (!canonicalPath.startsWith(externalPath )) {
                throw new SecurityException("Destination must be on external storage: " + uri);
            }
假如要文件要存放的位置不是外部存储的路径,那么抛出安全异常,也就是我碰到的问题所在。
可是我传入的地址明明是外部存储位置,怎么不对呢?再看这一句的上一句:
final String externalPath = Environment.getExternalStorageDirectory().getAbsolutePath();//外部存储位置
Environment.getExternalStorageDirectory()的官方api解释是:

Return the primary external storage directory. This directory may not  currently be accessible if it has been mounted by the user on their  computer, has been removed from the device, or some other problem has happened.

译:这个方法会得到(primary external storage directory)主外部储存目录,这个目录可能因为用户把它挂载到了电脑而被Android设备移除(笔者注:也就咱们用数据线连电脑时常用的USB大容量存储)或者什么其他原因而暂时不能够访问。

说直白点,就是系统会做一个判断,如果,我们要保存的位置不是系统指定的主分区目录,它就要给你搞鬼,让你下不成 搞了这么久,原来是这么个原因,当我使用 setDestinationUri(uri)设置文件保存位置时,即使我传入的地址是插入的U盘地址(外部存储),但它却是所谓的secondry external storage,所以它是下载不了的,导致了异常崩溃。那照这么说,我就不能完成不了老大分配的任务了吗?No,No,NO 来年我还想在这公司继续干呢,虽然 谷歌考虑到安全问题不让下载文件到secondry external storage,可是我发现可以通过文件IO流正常读写primary external storage和secondry external storage 我采取了“曲线救国”方针。

解决方案

转载时请注明原作者和链接:http://blog.csdn.net/xiong_it/article/details/42247219  谢谢!!

于是我想到了在下载完成后接收下载完成广播,在广播接收者中将文件剪切到U盘中去。
上正菜:
先定义一个下载完成的广播接收者,接收下载完成的广播DownloadCompleteReceiver继承BroadcastReceiver,定义它的意图过滤器为:DownloadManager.ACTION_DOWNLOAD_COMPLETE
再去扫描挂载的外部存储有哪些,如果有U盘存在的话就,拷贝已下载文件到U盘去,最后将下载在primary external storage的文件删除,完成整个流程。ps:当然,你也可以写一个U盘拔插广播接收者,判断是否有U盘插入,我没有试过。
我用的代码是下面这段:
		try {
			// 遍历外部挂载设备并找到u盘
			String rootExStorage = Environment.getRootDirectory().getParent()+ "storage" + File.separator + "external_storage";// 外挂的存储设备根目录路径
			File rootExStorageDir = new File(rootExStorage);
			Log.e(LOGTAG, "rootExStorage=" + rootExStorage + "\n rootExStorageDir="+ rootExStorageDir);
			if (rootExStorageDir != null) {
				File[] dirs = rootExStorageDir.listFiles();
				File[] usbRootDirs = new File[3];//有三个usb口
				for (int i = 0; i < dirs.length; i++) {
					long usableSpace = dirs[i].getUsableSpace();
					long totalSpace = dirs[i].getTotalSpace();
					// 如果有一个外挂设备总空间小于128G并且可用空间大于1M,那么记录这个外挂U盘或移动存储设备
					if ((usableSpace / (1024 * 1024)) > 1 && (totalSpace / (1024 * 14024 * 1024)) < 128) {
						usbRootDirs[i] = dirs[i];
					}
					Log.e(LOGTAG, "dirs[" + i + "]=" + dirs[i]);
					Log.e(LOGTAG, "usbRootDirs[" + i + "]=" + usbRootDirs[i]);
				}
				if (usbRootDirs.length > 0 && usbRootDirs[0] != null) {
					String customDownloadPath = usbRootDirs[0].getAbsolutePath()+ File.separator + "Download";//定义文件下载目录路径
					Log.e(LOGTAG, "外置路径=" + customDownloadPath);
					File browerDownloadDir = new File(customDownloadPath);
					if (!browerDownloadDir.exists()) {
						browerDownloadDir.mkdirs();
					}
					usbDownload=browerDownloadDir;
					usbRoot = usbDownload.getParentFile();
					String downloadFilePath = queryFileFromeInternalStorage(reference);//查询下载文件的路径
					if (downloadFilePath!=null && !"".equals(downloadFilePath)) {//如果已下载文件路径不为空
						if (queryFileSpace(downloadFilePath)) {//并且查询到目标目标足够装下已下载文件
							copyFileToExternalStorage(downloadFilePath);//开始复制文件
						}else {
							Toast.makeText(mContext, mContext.getString(R.string.have_no_enough_space), Toast.LENGTH_SHORT).show();//否则提示空间不足
						}
					}else {
						Toast.makeText(mContext, mContext.getString(R.string.not_find_file), Toast.LENGTH_SHORT).show();//否则提示下载文件不存在
					}
				}
			}
		} catch (Exception e) {
			Log.e(LOGTAG, "检测U盘失败");
			e.printStackTrace();
		}

上面提到的queryFileFromeInternalStorage(reference)方法是用来找到已下载文件的路径的,其中reference是代表文件的ID,long类型,它会在广播接收者的onReceive(Intent intent)的intent中:
long reference = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID,-1);
具体查询方法如下:
/**
 * 查询已下载文件的路径
 * @param reference 下载文件id
 * @return 下载文件的绝对路径
 */
	private String queryFileFromeInternalStorage(long reference) {
		String filePath = null;
		Query downloadQuery = new Query();  
	    downloadQuery.setFilterById(reference);  
	    DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);    
	    Cursor downloadCursor = downloadManager.query(downloadQuery);  
	    if (downloadCursor.moveToFirst()) {  
	    	int fileNameIdx = downloadCursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME);  
	    	filePath = downloadCursor.getString(fileNameIdx);  

	    	Log.e(LOGTAG, "fileName="+filePath);
	    }
	    downloadCursor.close();
	    return filePath;
	}

上面提到的queryFileSpace(downloadFilePath)方法如下,主要作用是查询U盘空间够不够用
		public boolean queryFileSpace(String path){
			Log.e(LOGTAG, "queryFileSpace...");
			File FileSrc =new File(path);
			long destUsableSpace = usbRoot.getUsableSpace();
			long fileLength = FileSrc.length();
			if (fileLength<destUsableSpace) {//空间够了
				Log.e(LOGTAG, "继续拷贝");
				return true;
			}else {//空间不够
				Log.e(LOGTAG, "取消拷贝");
				return false;
			}
		}

下面是copyFileToExternalStorage(downloadFilePath)的具体方法,具体作用是拷贝下载的文件到U盘
	/**
	 * 拷贝文件到外部存储U盘
	 * 考虑到可能要同时拷贝多个文件,顾采用开启工作线程拷贝
	 * 
	 * @param filePath 文件路径
	 */
	private void copyFileToExternalStorage(final String filePath) {

		new Thread(){

			public void run() {
				Log.e(LOGTAG, "copy file start");

				File srcFile=new File(filePath);
				File destFile = new File(usbDownload,fileName);
				FileOutputStream toExternalStorage = null;
				FileInputStream fromFile = null;
				try {
					Message msg_start = handler.obtainMessage();
					msg_start.what=0x00;
					handler.sendMessage(msg_start);//发送广播更新UI,提示即将完成
					fromFile = new FileInputStream(srcFile);
					toExternalStorage = new FileOutputStream(destFile);
					byte [] buffer = new byte[2*1024];
					int len = 0;
					while ((len = fromFile.read(buffer))!=-1) {
						toExternalStorage.write(buffer, 0, len);
						toExternalStorage.flush();
					}
				} catch (FileNotFoundException e) {
					Log.e(LOGTAG, e.getMessage());
					e.printStackTrace();
				} catch (IOException e) {
					Log.e(LOGTAG, e.getMessage());
					e.printStackTrace();
				}finally{
					try {
						fromFile.close();
						toExternalStorage.close();
					} catch (IOException e) {
						e.printStackTrace();
						Log.e(LOGTAG, e.getMessage());
					}
					//删除硬盘中的源文件
					srcFile.delete();
					mContext.unregisterReceiver(DownloadCompleteReceiver.this);
					
					//避免文件小的时候弹吐司太快,睡眠2s再发消息
					try {
						Thread.sleep(2000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					Message msg_finish = handler.obtainMessage();
					msg_finish.what = 0x01;
					handler.sendMessage(msg_finish);//发消息提示文件下载完成,实际是拷贝完成,我太奸诈了,呜哈哈哈
				}
			};
		}.start();

	}
别忘记注册广播接收者,以及给应用添加联网权限。

如果你觉着在这里看的思路不够清晰,请下载:DownloadCompleteReceiver.java 很抱歉,这里设置了下载要1分,因为楼主手头紧,如果想要看完整版但又没分的朋友可以在评论留下你的邮箱。同时希望各位朋友能提出疑问,共同讨论,进步!

参考链接:

转载时请注明原作者和链接:http://blog.csdn.net/xiong_it/article/details/42247219  谢谢!!
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值