使用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
相关产品推荐
相关产品推荐

