安卓7.0以后苫拍,google修改了文件權(quán)限睦袖,不再允許app透露file://Uri
來給其他app珊肃,轉(zhuǎn)而使用FileProvider通過content://Uri
來取代file://Uri
1.使用FileProvider
必須在manifest
文件中注冊provider
:
<application>
... ...
<provider
android:authorities="${applicationId}.provider"
android:name="android.support.v4.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepath"/>
</provider>
</application>
-
android:authorities
:FileProvider
唯一標識 -
android:exported
: 必須設(shè)置為falseFileProvider
不能公開 -
android:grantUriPermissions
: 控制文件權(quán)限 -
android:resource
: xml路徑設(shè)置的filepath.xml
2.res/xml/新增 filepath.xml
<?xml version="1.0" encoding="utf-8"?>
<paths>
<!--拍照存儲路徑-->
<external-path
name="pocket"
path="pocket/picture/" />
<!--訪問相冊路徑-->
<external-path
name="external"
path="." />
</paths>
-
path
: 子目錄名稱 -
name
: 取代path的別名
即原來路徑名為file://xxxx/pocket/picture/x.jpg
-> content://xxxx/pocket/picture/x.jpg
3.由于項目結(jié)果使用的是單activity多fragment,項目中的每個fragment都間接繼承了PermissionCheckerDelegate
,因此伦乔,項目所有的權(quán)限都放在此類中進行申請(先貼代碼厉亏,后續(xù)再講適配中出現(xiàn)的一些Bug解決方案):
abstract class PermissionCheckerDelegate : BaseDelegate() {
//給子類用于顯示的相片地址
private lateinit var photoUri: Uri
private lateinit var imagePath: String
//供裁剪使用
private lateinit var oriUri: Uri
private val cropFile = File(Environment.getExternalStorageDirectory().absolutePath,
"/pocket/picture/" + "crop_photo.jpg")
companion object {
const val WRITE_EXTERNAL_STORAGE = 1
const val OPEN_CAMERA = 2
const val OPEN_ALBUM = 3
const val CROP_IMAGE = 4
}
/**
* 相機讀寫權(quán)限申請
*/
fun applyCameraPermission() {
applyWritePermission(OPEN_CAMERA) {
openCamera()
}
}
/**
* 相冊讀寫權(quán)限申請
*/
fun applyOpenAlbumPermission() {
applyWritePermission(OPEN_ALBUM) {
openAlbum()
}
}
/**
* 獲取相機拍下的uri并轉(zhuǎn)為bitmap
*/
fun getBitmapByCamera() = BitmapFactory
.decodeStream(context!!.contentResolver.openInputStream(photoUri))!!
/**
* 獲取相冊的圖片轉(zhuǎn)為bitmap
*/
fun getBitmapByAlbum(data: Intent): Bitmap {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
handleImageAfterKitKat(data)
} else {
handleImageBeforeKitKat(data)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//7.0適配
oriUri = FileProvider.getUriForFile(context!!, "com.dididi.pocket.provider", File(imagePath))
}
return MediaStore.Images.Media.getBitmap(context!!.contentResolver, oriUri)
}
/**
* 打開相機
*/
private fun openCamera() {
//創(chuàng)建file于sdcard/pocketPicture/ 以當前時間命名的jpg圖像
File(Environment.getExternalStorageDirectory().absolutePath,
"/pocket/picture/" + System.currentTimeMillis() + ".jpg").apply {
parentFile.mkdirs()
photoUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//android7.0之后,不再允許app透露file://Uri給其他app
//轉(zhuǎn)而使用FileProvider來生成content://Uri取代file://Uri
FileProvider
.getUriForFile(context!!, "com.dididi.pocket.provider", this)
} else {
//7.0之前 直接獲取Uri
Uri.fromFile(this)
}
}
Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
//將uri存進intent烈和,供相機回調(diào)使用 data.getData中獲取
putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
startActivityForResult(this, OPEN_CAMERA)
}
}
/**
* 打開相冊
*/
private fun openAlbum() {
Intent(Intent.ACTION_GET_CONTENT).apply {
type = "image/*"
startActivityForResult(this, OPEN_ALBUM)
}
}
/**
* 裁剪Uri
* @param oriUri 原始Uri
* @param desUri 目標Uri
*/
fun cropImageUri(oriUri: Uri, desUri: Uri, aspectX: Int, aspectY: Int, width: Int, height: Int) {
Intent("com.android.camera.action.CROP").apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
setDataAndType(oriUri, "image/*")
putExtra("crop", "true")
putExtra("aspectX", aspectX)
putExtra("aspectY", aspectY)
putExtra("outputX", width)
putExtra("outputY", height)
putExtra("scale", true)
//將剪切的圖片保存到目標Uri中
putExtra(MediaStore.EXTRA_OUTPUT, desUri)
putExtra("return-data", false)
putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString())
putExtra("noFaceDetection", true)
this@PermissionCheckerDelegate.startActivityForResult(this, CROP_IMAGE)
}
}
override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
//權(quán)限請求結(jié)果
when (requestCode) {
WRITE_EXTERNAL_STORAGE -> {
permissionHint(grantResults, "沒有讀寫權(quán)限") {}
}
OPEN_CAMERA -> {
permissionHint(grantResults, "沒有讀寫權(quán)限") {
openCamera()
}
}
OPEN_ALBUM -> {
permissionHint(grantResults, "沒有讀寫權(quán)限") {
openAlbum()
}
}
else -> {
Toast.makeText(context, "沒有權(quán)限", Toast.LENGTH_SHORT).show()
}
}
}
/**
* 權(quán)限結(jié)果處理lambda函數(shù)
* @param grantResults 請求結(jié)果
* @param msg toast內(nèi)容
* @param target 權(quán)限拿到要做什么
*/
private fun permissionHint(grantResults: IntArray, msg: String, target: () -> Unit) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
target()
} else {
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
}
}
/**
* 請求讀寫權(quán)限
* @param requestCode 請求碼
* @param target 要做什么
*/
private fun applyWritePermission(requestCode: Int, target: () -> Unit) {
val permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
//android6.0之后爱只,需要動態(tài)申請讀寫權(quán)限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//讀寫是否已經(jīng)授權(quán)
val check = ContextCompat.checkSelfPermission(context!!, permissions[0])
if (check == PackageManager.PERMISSION_GRANTED) {
target()
} else {
//如果未發(fā)現(xiàn)授權(quán),則請求權(quán)限
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
requestCode)
}
} else {
target()
}
}
/**
* android4.4之后招刹,需要解析獲取圖片真實路徑
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
private fun handleImageAfterKitKat(data: Intent) {
val uri = data.data
//document類型的Uri
when {
DocumentsContract.isDocumentUri(context, uri) -> {
//通過documentId處理
val docId = DocumentsContract.getDocumentId(uri)
when (uri?.authority) {
"com.android.externalstorage.documents" -> {
val type = docId.split(":")[0]
if ("primary".equals(type, ignoreCase = true)) {
imagePath = Environment.getExternalStorageDirectory()
.toString() + "/" + docId.split(":")[1]
}
}
//media類型解析
"com.android.providers.media.documents" -> {
val id = docId.split(":")[1]
val type = docId.split(":")[0]
val contentUri: Uri? = when (type) {
"image" -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
"video" -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
"audio" -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
else -> null
}
val selection = "_id=?"
val selectionArgs: Array<String> = arrayOf(id)
imagePath = getImagePath(contentUri!!, selection, selectionArgs)!!
}
//downloads文件解析
"com.android.providers.downloads.documents" -> {
ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), docId.toLong()
).apply {
imagePath = getImagePath(this, null, null)!!
}
}
else -> {
}
}
}
"content".equals(uri?.scheme, ignoreCase = true) ->
//content類型數(shù)據(jù)不需要解析恬试,直接傳入生成即可
imagePath = getImagePath(uri!!, null, null)!!
"file".equals(uri?.scheme, ignoreCase = true) ->
//file類型的uri直接獲取圖片路徑即可
imagePath = uri!!.path!!
}
}
/**
* android4.4之前可直接獲取圖片真實uri
*/
private fun handleImageBeforeKitKat(data: Intent) {
val uri = data.data
imagePath = getImagePath(uri!!, null, null)!!
}
/**
* 解析uri及selection
* 獲取圖片真實路徑
*/
private fun getImagePath(uri: Uri, selection: String?, selectionArgs: Array<String>?): String? {
var cursor: Cursor? = null
try {
cursor = context!!.contentResolver.query(uri, null, selection, selectionArgs, null)
if (cursor?.moveToFirst()!!) {
return cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA))
}
} finally {
cursor?.close()
}
return null
}
}
-
子類
ChatDelegate
間接繼承PermissionCheckerDelegate
,因此疯暑,子類只需要復(fù)寫onActivityResult()
方法即可训柴,具體代碼省略如下:... ... //上拉頁面的按鈕 //打開相機 val moreCamera = morePagerView?.findViewById<MoreButtonItem>(R.id.item_msg_chat_more_camera) moreCamera?.setOnClickListener { applyCameraPermission() } //打開相冊 val moreOpenAlbum = morePagerView?.findViewById<MoreButtonItem>(R.id.item_msg_chat_more_album) moreOpenAlbum?.setOnClickListener { applyOpenAlbumPermission() } ... ... override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) when (requestCode) { OPEN_CAMERA -> { if (resultCode == ISupportFragment.RESULT_OK) { insertBitmapToList(getBitmapByCamera()) } } OPEN_ALBUM -> { if (resultCode == ISupportFragment.RESULT_OK) { insertBitmapToList(getBitmapByAlbum(data!!)) } } } }
碰到了以下幾個Error:
-
java.lang.IllegalStateException: Couldn't read row 0, col -1 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it.
注意檢查android4.4之后Uri的解析是否正確是否有遺漏項沒解析,參考如handleImageAfterKitKat()
方法妇拯。
2.java.io.FileNotFoundException: No content provider: /storage/emulated/0/.../xxx.jpg
幻馁,這個問題出現(xiàn)在訪問相冊獲取相冊照片時,android7.0以上設(shè)備需要通過FileProvider
來獲得訪問權(quán)限越锈,檢查一下filepath.xml
文件仗嗦,如果是使用getExternalStorageDirectory()
需要加上:<!--訪問相冊路徑--> <external-path name="external" path="." />
然后拿到處理后的imagePath
之后,需要通過FileProvider
來獲取圖片Uri甘凭,具體如方法getBitmapByAlbum()
:
/**
* 獲取相冊的圖片轉(zhuǎn)為bitmap
*/
fun getBitmapByAlbum(data: Intent): Bitmap {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
handleImageAfterKitKat(data)
} else {
handleImageBeforeKitKat(data)
}
oriUri = Uri.parse(imagePath)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//7.0適配(此處的authority為${applicationId}.provider)
oriUri = FileProvider.getUriForFile(context!!, "com.dididi.pocket.provider", File(imagePath))
}
return MediaStore.Images.Media.getBitmap(context!!.contentResolver, oriUri)
}
至此稀拐。放張效果圖吧。丹弱。钩蚊。