适配Android10分区存储(Scoped storage)文件上传

658 阅读2分钟

背景:

Android 10 中外部存储访问权限范围限定为应用文件和媒体

默认情况下,对于以 Android 10 及更高版本为目标平台的应用,其访问权限范围限定为外部存储,即分区存储**。此类应用可以查看外部存储设备内以下类型的文件,无需请求任何与存储相关的用户权限:

  • 特定于应用的目录中的文件(使用 getExternalFilesDir() 访问)。
  • 应用创建的照片、视频和音频片段(通过媒体库访问)。

要详细了解分区存储以及如何共享、访问和修改在外部存储设备上保存的文件,请参阅有关如何管理外部存储设备中的文件以及如何访问和修改媒体文件的指南。

  1. 扩展


import android.content.ContentResolver
import android.content.res.AssetFileDescriptor
import android.net.Uri
import android.os.ParcelFileDescriptor
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody
import okio.BufferedSink
import okio.source
import java.io.FileNotFoundException
import java.io.IOException

fun Uri.getContentType(contentResolver: ContentResolver): MediaType? =
    contentResolver.getType(this)?.toMediaTypeOrNull()

/** It supports file/content/mediaStore/asset URIs. asset not tested */
fun Uri.createAssetFileDescriptor(contentResolver: ContentResolver) = try {
    contentResolver.openAssetFileDescriptor(this, "r")
} catch (e: FileNotFoundException) {
    null
}

/** It supports file/content/mediaStore URIs. Will not work with providers that return sub-sections of files */
fun Uri.createParcelFileDescriptor(contentResolver: ContentResolver) = try {
    contentResolver.openFileDescriptor(this, "r")
} catch (e: FileNotFoundException) {
    null
}

/** - It supports file/content/mediaStore/asset URIs. asset not tested
 * - When file URI is used, may get contentLength error (expected x but got y) error when uploading if contentLength header is filled from assetFileDescriptor.length */
fun Uri.createInputStreamFromContentResolver(contentResolver: ContentResolver) = try {
    contentResolver.openInputStream(this)
} catch (e: FileNotFoundException) {
    null
}

fun Uri.asRequestBody(
    contentResolver: ContentResolver,
    contentLength: Long = -1L,
): RequestBody {

    return object : RequestBody() {
        /** If null is given, it is binary for Streams */
        override fun contentType() = getContentType(contentResolver)

        /** 'chunked' transfer encoding will be used for big files when length not specified */
        override fun contentLength() = contentLength

        /** This may get called twice if HttpLoggingInterceptor is used */
        override fun writeTo(sink: BufferedSink) {
            val assetFileDescriptor = createAssetFileDescriptor(contentResolver)
            if (assetFileDescriptor != null) {
                // when InputStream is closed, it auto closes AssetFileDescriptor
                AssetFileDescriptor.AutoCloseInputStream(assetFileDescriptor)
                    .source()
                    .use { source -> sink.writeAll(source) }
            } else {
                val inputStream = createInputStreamFromContentResolver(contentResolver)
                if (inputStream != null) {
                    inputStream
                        .source()
                        .use { source -> sink.writeAll(source) }
                } else {
                    val parcelFileDescriptor = createParcelFileDescriptor(contentResolver)
                    if (parcelFileDescriptor != null) {
                        // when InputStream is closed, it auto closes ParcelFileDescriptor
                        ParcelFileDescriptor.AutoCloseInputStream(parcelFileDescriptor)
                            .source()
                            .use { source -> sink.writeAll(source) }
                    } else {
                        throw IOException()
                    }
                }
            }
        }
    }
}
  1. 上传文件接口


import okhttp3.MultipartBody
import retrofit2.http.*

@Multipart
@POST("/api/uploadImage")
suspend fun uploadReturnCertificateImage(@Part data: List<MultipartBody.Part>): ApiResponse<String>
  1. 使用


import android.content.Context
import android.net.Uri
import okhttp3.MultipartBody.Part.Companion.createFormData

val uri: Uri = parameters.imageUri
val fileName = context.getFileName(uri)
val requestBody = uri.asRequestBody(context.contentResolver)

uploadReturnCertificateImage(listOf(
            createFormData("other_param1", "xxxx"),
            createFormData("other_param2", "xxxx"),
            createFormData("image", fileName, requestBody)
        ))
  1. 参考:

[Feature Request] - RequestBody supports InputStream · Issue #3585 · square/okhttp · GitHub Android - how to upload video in chunks using OkHTTP? - Stack Overflow Android 10 中的隐私权变更  |  Android 开发者  |  Android Developers