You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用DrJacky ImagePicker与Retrofit2从相册传图遇权限异常

Android相册上传图片SecurityException权限问题解决

问题场景

使用DrJacky ImagePicker和Retrofit 2实现相册图片上传时,触发SecurityException,提示需要通过ACTION_OPEN_DOCUMENT或相关API获取访问权限,异常出现在调用contentResolver.openFileDescriptor读取图片URI的代码行。

异常详情

java.lang.SecurityException: Permission Denial: opening provider com.android.providers.media.MediaDocumentsProvider from ProcessRecord{efed650 15275:app.fadlyproject.com/u0a158} (pid=15275, uid=10158) requires that you obtain access using ACTION_OPEN_DOCUMENT or related APIs

出错代码行:

val parcelFileDescriptor =
        contentResolver.openFileDescriptor(selectedImageUri!!, "r", null) ?: return

依赖配置

implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.github.Drjacky:ImagePicker:2.3.19'

核心代码

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    image_view = findViewById(R.id.image_view)
    button_upload = findViewById(R.id.button_upload)

    image_view!!.setOnClickListener {
        openImageChooser()
    }

    button_upload!!.setOnClickListener {
        uploadImage()
    }

}                                                                               
private val profileLauncher =
    registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
        if (it.resultCode == Activity.RESULT_OK) {
            val uri = it.data?.data!!
            selectedImageUri = uri
            image_view!!.setImageURI(selectedImageUri)
        } else parseError(it)
    }

private fun openImageChooser() {
    ImagePicker.with(this)
        .provider(ImageProvider.BOTH)
        .setDismissListener {
            Log.d("ImagePicker", "onDismiss");
        }
        .createIntentFromDialog { profileLauncher.launch(it) }
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (resultCode == Activity.RESULT_OK) {
        when (requestCode) {
            REQUEST_CODE_PICK_IMAGE -> {
                selectedImageUri = data?.data
                image_view!!.setImageURI(selectedImageUri)
            }
        }
    }
}

private fun uploadImage() {
    if (selectedImageUri == null) {
        layout_root!!.snackbar("Select an Image First")
        return
    }

    val parcelFileDescriptor =
        contentResolver.openFileDescriptor(selectedImageUri!!, "r", null) ?: return

    val inputStream = FileInputStream(parcelFileDescriptor.fileDescriptor)
    val file = File(cacheDir, contentResolver.getFileName(selectedImageUri!!))
    val outputStream = FileOutputStream(file)
    inputStream.copyTo(outputStream)

    progress_bar!!.progress = 0
    val body = UploadRequestBody(file, "image", this)
    MyAPI().uploadImage(
        MultipartBody.Part.createFormData(
            "file",
            file.name,
            body
        ),
        RequestBody.create(MediaType.parse("multipart/form-data"), "json")
    ).enqueue(object : Callback<UploadResponse> {
        override fun onFailure(call: Call<UploadResponse>, t: Throwable) {
            layout_root!!.snackbar(t.message!!)
            progress_bar!!.progress = 0
        }

        override fun onResponse(
            call: Call<UploadResponse>,
            response: Response<UploadResponse>
        ) {
            response.body()?.let {
                layout_root!!.snackbar(it.message)
                progress_bar!!.progress = 100
                classes!!.text = it.data.classes
                layout!!.visibility = View.VISIBLE
                Glide.with(this@MainActivity).load("http://167.172.72.26:1337/"+ it.data.image_after_preprocessing).into(
                    image_view!!
                )
            }
        }
    })

}

override fun onProgressUpdate(percentage: Int) {
    progress_bar!!.progress = percentage
}

companion object {
    const val REQUEST_CODE_PICK_IMAGE = 101
}

解决方案

方法1:为URI添加持久化权限

在获取到图片URI的回调中,调用takePersistableUriPermission固化访问权限,修改profileLauncher代码:

private val profileLauncher =
    registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
        if (it.resultCode == Activity.RESULT_OK) {
            val uri = it.data?.data!!
            // 申请持久化读写权限
            val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            contentResolver.takePersistableUriPermission(uri, takeFlags)
            selectedImageUri = uri
            image_view!!.setImageURI(selectedImageUri)
        } else parseError(it)
    }

方法2:改用系统OpenDocument API选择图片

直接使用ActivityResultContracts.OpenDocument获取拥有持久权限的URI,替换原有的选择逻辑:

// 替换原有的profileLauncher
private val pickImageLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
    uri?.let {
        selectedImageUri = it
        image_view!!.setImageURI(selectedImageUri)
        // 可选:再次确认持久化权限
        val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION
        contentResolver.takePersistableUriPermission(it, takeFlags)
    }
}

// 替换原有的openImageChooser
private fun openImageChooser() {
    pickImageLauncher.launch(arrayOf("image/*"))
}

方法3:配置ImagePicker启用持久权限

在ImagePicker初始化时添加权限Flag,确保返回的URI具备临时权限,再配合持久化权限申请:

private fun openImageChooser() {
    ImagePicker.with(this)
        .provider(ImageProvider.BOTH)
        // 添加权限Flag
        .setPermissionFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
        .setDismissListener {
            Log.d("ImagePicker", "onDismiss");
        }
        .createIntentFromDialog { profileLauncher.launch(it) }
}

同时仍需在回调中调用takePersistableUriPermission固化权限。

问题原因

Android 10及以上版本中,相册返回的content://类型URI默认仅提供临时访问权限,当后续操作(如上传时读取文件)触发权限校验时,临时权限可能已失效。必须通过takePersistableUriPermission获取持久化权限,或使用ACTION_OPEN_DOCUMENT这类API直接获取具备持久权限的URI,才能避免SecurityException。

内容的提问来源于stack exchange,提问作者dfjick

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 08:05:54