随着Android 11的正式发布,适配Android 10/11 分区存储就更加的迫切了,由于Android 11开始,将强制开启分区存储,咱们就没法再以绝对路径的方式去读写非沙盒目录下的文件,为此,RxHttp 在2.4.0
版本中就正式适配了分区存储,而且,能够很是优雅的实现文件上传/下载/进度监听,三步便可搞懂任意请求。java
老规矩,先看看请求三部曲
若是你想了解RxHttp更过功能,请查看如下系列文章react
RxHttp 2000+star,协程请求,仅需三步android
gradle依赖github
//使用kapt依赖rxhttp-compiler时必须 apply plugin: 'kotlin-kapt' android { //必须,java 8或更高 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation 'com.ljx.rxhttp:rxhttp:2.5.2' implementation 'com.squareup.okhttp3:okhttp:4.9.0' //rxhttp v2.2.2版本起,须要手动依赖okhttp kapt 'com.ljx.rxhttp:rxhttp-compiler:2.5.2' //生成RxHttp类,纯Java项目,请使用annotationProcessor代替kapt }
android { defaultConfig { javaCompileOptions { annotationProcessorOptions { arguments = [ //使用asXxx方法时必须,告知RxHttp你依赖的rxjava版本,可传入rxjava二、rxjava3 rxhttp_rxjava: 'rxjava3', rxhttp_package: 'rxhttp' //非必须,指定RxHttp类包名 ] } } } } dependencies { implementation 'com.ljx.rxlife:rxlife-coroutine:2.0.1' //管理协程生命周期,页面销毁,关闭请求 //rxjava2 (RxJava2/Rxjava3二选一,使用asXxx方法时必须) implementation 'io.reactivex.rxjava2:rxjava:2.2.8' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation 'com.ljx.rxlife2:rxlife-rxjava:2.0.0' //管理RxJava2生命周期,页面销毁,关闭请求 //rxjava3 implementation 'io.reactivex.rxjava3:rxjava:3.0.6' implementation 'io.reactivex.rxjava3:rxandroid:3.0.0' implementation 'com.ljx.rxlife3:rxlife-rxjava:3.0.0' //管理RxJava3生命周期,页面销毁,关闭请求 //非必须,根据本身需求选择 RxHttp默认内置了GsonConverter implementation 'com.ljx.rxhttp:converter-fastjson:2.5.2' implementation 'com.ljx.rxhttp:converter-jackson:2.5.2' implementation 'com.ljx.rxhttp:converter-moshi:2.5.2' implementation 'com.ljx.rxhttp:converter-protobuf:2.5.2' implementation 'com.ljx.rxhttp:converter-simplexml:2.5.2' }
当咱们App的targetSdkVersion
更改成28以上,而且运行在Android 10以上设备时,咱们没法再以绝对路径的方式,去读写非沙盒目录下的文件,固然,若是App是覆盖安装(如:targetSdkVersion 28 覆盖安装为 29),则会保持原来的访问方式。json
requestLegacyExternalStorage属性app
若是咱们的app将targetSdkVersion更改成28以上,且想保持原来的访问方式,则须要在清单文件中将 requestLegacyExternalStorage
的值设置为 true
,以下:框架
<manifest ...> <!-- This attribute is "false" by default on apps targeting Android 10 or higher. --> <application android:requestLegacyExternalStorage="true" ... > ... </application> </manifest>
此时,即可继续以原来的方式去读写文件,然而,在Android 11上,Google又给了它新的含义,来看看官网的原话ide
也就是说,在Android 11设备上,targetSdkVersion
为29以上的app,将强制开启分区存储,requestLegacyExternalStorage
属性失效post
注意,只要同时知足以上两个条件,不论是覆盖安装仍是requestLegacyExternalStorage = true
,都会强制开启分区存储
分区存储优点
对用户来讲,解决了文件乱放的现象
对于开发者来讲,咱们无需写权限,就能够在分区目录下建立文件,而且访问本身建立的文件,不须要读权限(访问其它应用建立的文件,仍是须要读权限)
新的文件访问方式
此图来源于做者[连续三届村草]分享的Android 10(Q)/11(R) 分区存储适配一文,感谢做者的总结
在介绍Android 10文件上传前,咱们先来看看Android 10以前是如何上传文件的,以下:
//kotlin 协程 val result = RxHttp.postForm("/service/...") .add("key", "value") .addFile("file", File("xxx/1.jpg")) .awaitString() //awaitXxx系列方法是挂断方法 //RxJava RxHttp.postForm("/service/...") .add("key", "value") .addFile("file", File("xxx/1.jpg")) .asString() .subscribe({ //成功回调 }, { //异常回调 })
以上,咱们仅需调用 addFile
方法添加文件对象便可,RxHttp提供了一系列addFile
方法,列出几个经常使用的,以下:
//添加单个文件 addFile(String, File) //添加多个文件,每一个文件对应相同的key addFile(String, List<? extends File> fileList) //添加多个文件,每一个文件对应不一样的key addFile(Map<String, ? extends File> fileMap) //等等其它addFile方法
在Android 10,咱们须要经过Uri对象去上传文件,在RxHttp中,经过addPart
方法添加Uri
对象,以下:
val context = getContext(); //获取上下文对象 //获取Uri对象,这里为了方便,随便写了一个Downlaod目录下的Uri地址 val uri = Uri.parse("content://media/external/downloads/13417") //kotlin 协程 val result = RxHttp.postForm("/service/...") .add("key", "value") .addPart(context, "file", uri) .awaitString() //awaitXxx系列方法是挂断方法 //RxJava RxHttp.postForm("/service/...") .add("key", "value") .addPart(context, "file", uri) .asString() .subscribe({ //成功回调 }, { //异常回调 })
一样的,RxHttp内部提供了一系列addPart
方法供你们选择,列出几个经常使用的,以下:
//添加单个Uri对象 addPart(Context, String, Uri) //添加多个Uri对象,每一个Uri对应相同的key addParts(Context,String, List<? extends Uri> uris) //添加多个Uri对象,每一个Uri对应不一样的key addParts(Context context, Map<String, ? extends Uri> uriMap) //等等其它addPart方法
老规矩,看看Android 10以前是如何监听上传进度的,以下:
//kotlin 协程 val result = RxHttp.postForm("/service/...") .add("key", "value") .addFile("file", File("xxx/1.jpg")) .upload(this) {//this为当前协程CoroutineScope对象,用于控制回调线程 //上传进度回调,0-100,仅在进度有更新时才会回调 val currentProgress = it.progress //当前进度 0-100 val currentSize = it.currentSize //当前已上传的字节大小 val totalSize = it.totalSize //要上传的总字节大小 } .awaitString() //awaitXxx系列方法是挂断方法 //RxJava RxHttp.postForm("/service/...") .add("key", "value") .addFile("file", File("xxx/1.jpg")) .upload(AndroidSchedulers.mainThread()) { //上传进度回调,0-100,仅在进度有更新时才会回调 val currentProgress = it.progress //当前进度 0-100 val currentSize = it.currentSize //当前已上传的字节大小 val totalSize = it.totalSize //要上传的总字节大小 } .asString() .subscribe({ //成功回调 }, { //异常回调 })
相比于单纯的上传文件,咱们仅需额外调用upload
操做符,传入线程调度器及进度回调便可。
一样的,对于Andorid 10,咱们仅须要将File对象换成Uri对象便可,以下:
val context = getContext(); //获取上下文对象 //获取Uri对象,这里为了方便,随便写了一个Downlaod目录下的Uri地址 val uri = Uri.parse("content://media/external/downloads/13417") //kotlin 协程 val result = RxHttp.postForm("/service/...") .add("key", "value") .addPart(context, "file", uri) .upload(this) {//this为当前协程CoroutineScope对象,用于控制回调线程 //上传进度回调,0-100,仅在进度有更新时才会回调 val currentProgress = it.progress //当前进度 0-100 val currentSize = it.currentSize //当前已上传的字节大小 val totalSize = it.totalSize //要上传的总字节大小 } .awaitString() //awaitXxx系列方法是挂断方法 //RxJava RxHttp.postForm("/service/...") .add("key", "value") .addPart(context, "file", uri) .upload(AndroidSchedulers.mainThread()) { //上传进度回调,0-100,仅在进度有更新时才会回调 val currentProgress = it.progress //当前进度 0-100 val currentSize = it.currentSize //当前已上传的字节大小 val totalSize = it.totalSize //要上传的总字节大小 } .asString() .subscribe({ //成功回调 }, { //异常回调 })
怎么样?是否是so easy!!
下载较于上传,要丰富不少,RxHttp
内部提供类一系列下载方法来知足不一样的需求,以下:
//kotlin fun IRxHttp.toDownload( destPath: String, context: CoroutineContext? = null, progress: (suspend (ProgressT<String>) -> Unit)? = null ) fun IRxHttp.toDownload( context: Context, uri: Uri, coroutineContext: CoroutineContext? = null, progress: (suspend (ProgressT<Uri>) -> Unit)? = null ) fun <T> IRxHttp.toDownload( osFactory: OutputStreamFactory<T>, context: CoroutineContext? = null, progress: (suspend (ProgressT<T>) -> Unit)? = null )
在Android 10以前,咱们仅需传入一个本地文件路径便可,以下:
val localPath = "/sdcard/.../xxx.apk" //kotlin 协程 val result = RxHttp.get("/service/.../xxx.apk") .toDownload(localPath) .await() //这里返回sd卡存储路径 //RxJava RxHttp.get("/service/.../xxx.apk") .asDownload(localPath) .subscribe({ //成功回调,这里返回sd卡存储路径 }, { //异常回调 })
而到了Android 10,咱们须要自定义一个Android10DownloadFactory
类,继承UriFactory
类,以下:
class Android10DownloadFactory @JvmOverloads constructor( context: Context, fileName: String, queryUri: Uri? = null ) : UriFactory(context, queryUri, fileName) { override fun getUri(response: Response): Uri { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { ContentValues().run { put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) //文件名 //取contentType响应头做为文件类型 put(MediaStore.MediaColumns.MIME_TYPE, response.body?.contentType().toString()) //下载到Download目录 put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) val uri = queryUri ?: MediaStore.Downloads.EXTERNAL_CONTENT_URI context.contentResolver.insert(uri, this) } ?: throw NullPointerException("Uri insert fail, Please change the file name") } else { Uri.fromFile(File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), displayName)) } } }
这里简单介绍下上面的代码,本文后续会详细介绍为啥定义这样一个类,以及如何构建一个Uri
对象。
UriFactory
抽象类,实现getUri(Response)
方法contentResolver
构建Uri对象,不然就根据File
对象构建Uri对象,这样就能够兼容到全部系统版本Uri
对象便可注:以上代码,基本能够知足大部分人的需求,如你有特殊需求,构建Uri的过程的做出简单的修改便可
有了Android10DownloadFactory
类,执行Android 10
下载就会及其方便,以下:
val factory = Android10DownloadFactory(context, "test.apk") //kotlin 协程 val uri = RxHttp.get("/service/.../xxx.apk") .toDownload(factory) .await() //这里返回工厂类构建的Uri对象 //RxJava RxHttp.get("/service/.../xxx.apk") .asDownload(factory) .subscribe({ //成功回调,这里返回工厂类构建的Uri对象 }, { //异常失败 })
以上asDownload
、toDownload
方法都接收一个UriFactory
类型参数,故咱们能够直接传入Android10DownloadFactory
对象。
对于带进度下载,咱们只须要调用asDownload
、toDownload
方法时,传入线程调度器及进度回调便可,以下:
Android 10以前
val localPath = "/sdcard/.../xxx.apk" //kotlin 协程 val result = RxHttp.get("/service/.../xxx.apk") .toDownload(localPath, Dispatchers.Main) { //下载进度回调,0-100,仅在进度有更新时才会回调 val currentProgress = it.progress //当前进度 0-100 val currentSize = it.currentSize //当前已上传的字节大小 val totalSize = it.totalSize //要上传的总字节大小 } .await() //这里返回sd卡存储路径 //RxJava RxHttp.get("/service/.../xxx.apk") .asDownload(localPath, AndroidSchedulers.mainThread()) { //下载进度回调,0-100,仅在进度有更新时才会回调 val currentProgress = it.progress //当前进度 0-100 val currentSize = it.currentSize //当前已上传的字节大小 val totalSize = it.totalSize //要上传的总字节大小 } .subscribe({ //成功回调,这里返回sd卡存储路径 }, { //异常失败 })
Android 10以上,传入咱们定义的Android10DownloadFactory
对象的同时,再传入传入线程调度器及进度监听便可,以下:
val factory = Android10DownloadFactory(context, "test.apk") //kotlin 协程 val uri = RxHttp.get("/service/.../xxx.apk") .toDownload(factory, Dispatchers.Main) { //下载进度回调,0-100,仅在进度有更新时才会回调 val currentProgress = it.progress //当前进度 0-100 val currentSize = it.currentSize //当前已上传的字节大小 val totalSize = it.totalSize //要上传的总字节大小 } .await() //这里返回工厂类构建的Uri对象 //RxJava RxHttp.get("/service/.../xxx.apk") .asDownload(factory, AndroidSchedulers.mainThread()) { //下载进度回调,0-100,仅在进度有更新时才会回调 val currentProgress = it.progress //当前进度 0-100 val currentSize = it.currentSize //当前已上传的字节大小 val totalSize = it.totalSize //要上传的总字节大小 } .subscribe({ //成功回调,这里返回工厂类构建的Uri对象 }, { //异常失败 })
对于断点下载,咱们须要调用一系列asAppendDownload、toAppendDownload
方法,能够把它们理解为追加下载,实现以下:
Android 10以前
val localPath = "/sdcard/.../xxx.apk" //kotlin 协程 val result = RxHttp.get("/service/.../xxx.apk") .toAppendDownload(localPath, Dispatchers.Main) { //下载进度回调,0-100,仅在进度有更新时才会回调 val currentProgress = it.progress //当前进度 0-100 val currentSize = it.currentSize //当前已上传的字节大小 val totalSize = it.totalSize //要上传的总字节大小 } .await() //这里返回sd卡存储路径 //RxJava RxHttp.get("/service/.../xxx.apk") .asAppendDownload(localPath, AndroidSchedulers.mainThread()) { //下载进度回调,0-100,仅在进度有更新时才会回调 val currentProgress = it.progress //当前进度 0-100 val currentSize = it.currentSize //当前已上传的字节大小 val totalSize = it.totalSize //要上传的总字节大小 } .subscribe({ //成功回调,这里返回sd卡存储路径 }, { //异常失败 })
在Android 10上,有一点须要注意的是,咱们在构建Android10DownloadFactory
对象时,须要传入第三个参数queryUri
,能够把它理解为要查询的文件夹,断点下载,RxHttp内部会根据文件名在指定的文件夹下查找对应的文件,获得当前文件的长度,也就是断点位置,从而告诉服务端从哪里开始下载,以下:
val queryUri = MediaStore.Downloads.EXTERNAL_CONTENT_URI //在Download目录下查找test.apk文件 val factory = Android10DownloadFactory(context, "test.apk", queryUri) //kotlin 协程 val uri = RxHttp.get("/service/.../xxx.apk") .toAppendDownload(factory, Dispatchers.Main) { //下载进度回调,0-100,仅在进度有更新时才会回调 val currentProgress = it.progress //当前进度 0-100 val currentSize = it.currentSize //当前已上传的字节大小 val totalSize = it.totalSize //要上传的总字节大小 } .await() //这里返回工厂类构建的Uri对象 //RxJava RxHttp.get("/service/.../xxx.apk") .asAppendDownload(factory, AndroidSchedulers.mainThread()) { //下载进度回调,0-100,仅在进度有更新时才会回调 val currentProgress = it.progress //当前进度 0-100 val currentSize = it.currentSize //当前已上传的字节大小 val totalSize = it.totalSize //要上传的总字节大小 } .subscribe({ //成功回调,这里返回工厂类构建的Uri对象 }, { //异常失败 })
在上面代码中,咱们自定义了Android10DownloadFactory
类,其中最为关键的代码就是如何构建一个Uri
对象,接下来,就教你们如何去构建一个Uri
,立刻开始,以下:
public Uri getUri(Context context) { ContentValues values = new ContentValues(); //一、配置文件名 values.put(MediaStore.MediaColumns.DISPLAY_NAME, "1.jpg"); //二、配置文件类型 values.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") //三、配置存储目录 values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM); //四、将配置好的对象插入到某张表中,最终获得Uri对象 return context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); }
第一步,配置文件名称,这个就没啥好说的了
第二步,配置文件类型,每一个文件都应该有一个类型描述,这样,后续查找时,就能够根据这个类型去查找出同一类型的文件,如:查找相册,此属性是可选的,若是不配置,后续就没法根据类型查找到这个文件
第三步,配置存储目录,这个是相对路径,总共有10个目录可选,以下:
Environment.DIRECTORY_DOCUMENTS
对应路径:/storage/emulated/0/Documents/
Environment.DIRECTORY_DOWNLOADS
对应路径:/storage/emulated/0/Download/
Environment.DIRECTORY_DCIM
对应路径:/storage/emulated/0/DCIM/
Environment.DIRECTORY_PICTURES
对应路径:/storage/emulated/0/Pictures/
Environment.DIRECTORY_MOVIES
对应路径:/storage/emulated/0/Movies/
Environment.DIRECTORY_ALARMS
对应路径:/storage/emulated/0/Alrams/
Environment.DIRECTORY_MUSIC
对应路径:/storage/emulated/0/Music/
Environment.DIRECTORY_NOTIFICATIONS
对应路径:/storage/emulated/0/Notifications/
Environment.DIRECTORY_PODCASTS
对应路径:/storage/emulated/0/Podcasts/
Environment.DIRECTORY_RINGTONES
对应路径:/storage/emulated/0/Ringtones/
若是须要在以上目录下,建立子目录,则传入的时候,直接带上便可,以下
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM + "/RxHttp");
第四步,插入到对应的表中,总共有5张表可选,以下:
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
MediaStore.Downloads.EXTERNAL_CONTENT_URI
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
须要特殊说明下,以上5张表中,只能存入对应文件类型的信息,如咱们不能将音频文件信息,插入到MediaStore.Images.Media.EXTERNAL_CONTENT_URI
图片表中,插入时,系统会直接抛出异常
注意事项
以上5张表中,除了对插入的文件类型有限制外,还对要插入的相对路径有限制,如,咱们将一个apk文件下载/storage/emulated/0/Download/RxHttp/
目录下,并插入到图片表中,以下:
public Uri getUri(Context context) { ContentValues values = new ContentValues(); values.put(MediaStore.MediaColumns.DISPLAY_NAME, "1.apk"); values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/RxHttp"); return context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); }
当执行到insert
操做时,系统将会直接报错,报错信息以下:
Primary directory Download not allowed for content://media/external/images/media; allowed directories are [DCIM, Pictures]
大体意思就是,Download
目录不容许插入到MediaStore.Images.Media.EXTERNAL_CONTENT_URI
表中,该表只容许插入DCIM
和Pictures
目录
开源不易,写文章更不易,喜欢的话,还需劳烦你们给本文点个赞,能够的话,再给个star,我将感激涕零,🙏🙏🙏🙏🙏🙏🙏🙏🙏🙏🙏🙏