Android 不请求权限贮存、删去相册图片

前言

最近重新看了下安卓的贮存适配,并结合之前做的拍照、裁切demo,小小实验了一下。Android 6.0增加了动态文件权限请求; Android 7.0需求运用FileProvider来获取Uri,不能直接运用file获得; Android 10.0引入了分区贮存,并在Android 11.0中强制运用。

Java的File变得越来越难运用,那安卓供给的 SAF(存储拜访结构)和 Uri(ContentProvider)是不是得学起来?SAF通过intent的GET_CONTENT action运用体系界面挑选文件,Uri则是ContentProvider供给的途径。

开发的时分不知道读者有没有很迷惑究竟什么地方应该敞开贮存权限,不敞开贮存权限能否贮存、删去相册图片呢?下面咱们结合代码试试。

知识储备

前面我现已发了一篇关于拍照、裁切的博文,里边能够方便地获得bitmap,下面内容里就不具体叙述了,博文链接如下:

安卓拍照、裁切、选取图片实践

关于文件权限适配的能够看下面几篇文章:

Android 存储基础

Android 10、11 存储彻底适配(上)

Android 10、11 存储彻底适配(下)

导出图片到相册

上篇文章咱们通过拍照获得了新图片,可是也仅仅是保存在外部贮存的私有目录里,如果删去了运用,图片也随之被删去了,那这样是不可的,应该考虑怎么把图片保存到体系相册去。要是以前,我就直接请求权限,直接Environment拿到根目录,运用File直接保存曩昔就行了,可是这样好吗?想想咱们手机里边一大堆的目录,这个文件的随便运用,是不是有问题?是不是违反了Android的标准?那应该怎么做呢?请看下面代码:

    // 保存到外部贮存-公有目录-Picture内,而且无需贮存权限
    private fun insert2Pictures() {
        binding.image.drawable?.let {
            val bitmap = it.toBitmap()
            val baos = ByteArrayOutputStream()
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
            val bais = ByteArrayInputStream(baos.toByteArray())
            insert2Album(bais, "Media")
            showToast("导出到相册成功")
        }
    }
    // 运用MediaStore方式将流写入相册
    @Suppress("SameParameterValue")
    private fun insert2Album(inputStream: InputStream, type: String) {
        val fileName = "${type}_${System.currentTimeMillis()}.jpg"
        val contentValues = ContentValues()
        contentValues.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, fileName)
        // Android 10,途径保存在RELATIVE_PATH
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            //RELATIVE_PATH 字段表明相对途径,Fundark为相册下专有目录
            contentValues.put(
                MediaStore.Images.ImageColumns.RELATIVE_PATH,
                Environment.DIRECTORY_PICTURES + File.separator + "Fundark"
            )
        } else {
            val dstPath = StringBuilder().let { sb->
                sb.append(requireContext()
                    .getExternalFilesDir(Environment.DIRECTORY_PICTURES)!!.path)
                sb.append(File.separator)
                sb.append("Fundark")
                sb.append(File.separator)
                sb.append(fileName)
                sb.toString()
            }
            //DATA字段在Android 10.0 之后现已抛弃(Android 11又启用了,可是只读)
            contentValues.put(MediaStore.Images.ImageColumns.DATA, dstPath)
        }
        // 刺进相册
        val uri =  requireContext().contentResolver
            .insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
        // 写入文件
        uri?.let {
            write2File(it, inputStream)
        }
    }
    private fun write2File(uri: Uri, inputStream: InputStream) {
        // 从Uri结构输出流
        requireContext().contentResolver.openOutputStream(uri)?.use { outputStream->
            val byteArray = ByteArray(1024)
            var len: Int
            do {
                //从输入流里读取数据
                len = inputStream.read(byteArray)
                if (len != -1) {
                    outputStream.write(byteArray, 0, len)
                    outputStream.flush()
                }
            } while (len != -1)
        }
    }

这儿的代码也仅仅示例,关于Exception的部分没做,可是关键在于咱们没有请求权限就把图片保存到相册去了,现在打开你的相册就能发现你保存的图片,点开具体信息就能看到它的实践方位:

Android 不申请权限储存、删除相册图片

删去相册图 片

刚开始我也觉得这儿或许需求请求体系的贮存权限,不然怎么能够去删去外部相册的图片呢?实践上,这儿也是有约束的,你能够删去你自己创立的图片,这样一说是不是又很合理了。下面试试:

    private fun clearAppPictures() {
        val selection: String
        val selectionArgs: String
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            selection = "${MediaStore.Images.ImageColumns.RELATIVE_PATH} like ?"
            selectionArgs = "%" + Environment.DIRECTORY_PICTURES + File.separator + "Fundark" + "%"
        } else {
            val dstPath = StringBuilder().let { sb->
                sb.append(requireContext()
                    .getExternalFilesDir(Environment.DIRECTORY_PICTURES)!!.path)
                sb.append(File.separator)
                sb.append("Fundark")
                sb.toString()
            }
            selection = "${MediaStore.Images.ImageColumns.DATA} like ?"
            selectionArgs = "%$dstPath%"
        }
        val num = requireContext().contentResolver.delete(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            selection,
            arrayOf(selectionArgs)
        )
        showToast("删去本运用相册图片${num}张")
    }

仍是调用ContentProvider处理的,稍微处理下条件,就能完成删去。不过有意思的是,这儿删去图片会被体系阻拦,荣耀10提示删去了相册图片,并会将图片放到体系回收站去,实践这样也还行。

完好代码

上篇博客的代码和这篇博客的代堆放一同,希望对读者有用:

import android.Manifest
import android.app.Activity.RESULT_OK
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.FileProvider
import androidx.core.graphics.drawable.toBitmap
import androidx.core.util.Consumer
import com.silencefly96.module_base.base.BaseFragment
import com.silencefly96.module_base.base.IPermissionHelper
import com.silencefly96.module_hardware.databinding.FragmentTakePhotoBinding
import java.io.*
class TakePhotoFragment : BaseFragment() {
    companion object{
        const val REQUEST_CAMERA_CODE = 1
        const val REQUEST_ALBUM_CODE = 2
        const val REQUEST_CROP_CODE = 3
        const val MAX_WIDTH = 480
        const val MAX_HEIGHT = 720
    }
    private var _binding: FragmentTakePhotoBinding? = null
    private val binding get() = _binding!!
    // 文件途径
    private var picturePath: String = ""
    // 裁切途径
    private var cropPicPath: String = ""
    // 启用裁切
    private var enableCrop: Boolean = true
    // 绑定布局
    override fun bindView(inflater: LayoutInflater, container: ViewGroup?): View {
        _binding = FragmentTakePhotoBinding.inflate(inflater, container, false)
        return binding.root
    }
    override fun doBusiness(context: Context?) {
        binding.takePhoto.setOnClickListener {
            requestPermission { openCamera() }
        }
        binding.pickPhoto.setOnClickListener {
            openAlbum()
        }
        binding.insertPictures.setOnClickListener {
            insert2Pictures()
        }
        binding.clearCache.setOnClickListener {
            clearCachePictures()
        }
        binding.clearPictures.setOnClickListener {
            clearAppPictures()
        }
        binding.cropSwitch.setOnCheckedChangeListener { _, isChecked -> enableCrop = isChecked}
    }
    private fun requestPermission(consumer: Consumer<Boolean>) {
        // 动态请求权限,运用的外部私有目录无需请求权限
        requestRunTimePermission(requireActivity(), arrayOf(
            Manifest.permission.CAMERA,
//            Manifest.permission.WRITE_EXTERNAL_STORAGE
        ),
            object : IPermissionHelper.PermissionListener {
                override fun onGranted() {
                    consumer.accept(true)
                }
                override fun onGranted(grantedPermission: List<String>?) {
                    consumer.accept(false)
                }
                override fun onDenied(deniedPermission: List<String>?) {
                    consumer.accept(false)
                }
            })
    }
    private fun openCamera() {
        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        // 运用外部私有目录:files-Pictures
        val picFile = createFile("Camera")
        val photoUri = getUriForFile(picFile)
        // 保存途径,不要uri,读取bitmap时麻烦
        picturePath = picFile.absolutePath
        // 给方针运用一个暂时授权
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        //android11以后强制分区存储,外部资源无法拜访,所以增加一个输出保存方位,然后取值操作
        intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
        startActivityForResult(intent, REQUEST_CAMERA_CODE)
    }
    private fun createFile(type: String): File {
        // 在相册创立一个暂时文件
        val picFile = File(requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES),
                "${type}_${System.currentTimeMillis()}.jpg")
        try {
            if (picFile.exists()) {
                picFile.delete()
            }
            picFile.createNewFile()
        } catch (e: IOException) {
            e.printStackTrace()
        }
        // 暂时文件,后边会加long型随机数
//        return File.createTempFile(
//            type,
//            ".jpg",
//            requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES)
//        )
        return picFile
    }
    private fun getUriForFile(file: File): Uri {
        // 转换为uri
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            //适配Android 7.0文件权限,通过FileProvider创立一个content类型的Uri
            FileProvider.getUriForFile(
                requireActivity(),
                "com.silencefly96.module_hardware.fileProvider", file
            )
        } else {
            Uri.fromFile(file)
        }
    }
    private fun openAlbum() {
        val intent = Intent()
        intent.type = "image/*"
        intent.action = "android.intent.action.GET_CONTENT"
        intent.addCategory("android.intent.category.OPENABLE")
        startActivityForResult(intent, REQUEST_ALBUM_CODE)
    }
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == RESULT_OK) {
            when(requestCode) {
                REQUEST_CAMERA_CODE -> {
                    // 告诉体系文件更新
//                    requireContext().sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
//                        Uri.fromFile(File(picturePath))))
                    if (!enableCrop) {
                        val bitmap = getBitmap(picturePath)
                        bitmap?.let {
                            // 显现图片
                            binding.image.setImageBitmap(it)
                        }
                    }else {
                        cropImage(picturePath)
                    }
                }
                REQUEST_ALBUM_CODE -> {
                    data?.data?.let { uri ->
                        if (!enableCrop) {
                            val bitmap = getBitmap("", uri)
                            bitmap?.let {
                                // 显现图片
                                binding.image.setImageBitmap(it)
                            }
                        }else {
                            cropImage(uri)
                        }
                    }
                }
                REQUEST_CROP_CODE -> {
                    val bitmap = getBitmap(cropPicPath)
                    bitmap?.let {
                        // 显现图片
                        binding.image.setImageBitmap(it)
                    }
                }
            }
        }
    }
    private fun getBitmap(path: String, uri: Uri? = null): Bitmap? {
        var bitmap: Bitmap?
        val options = BitmapFactory.Options()
        // 先不读取,仅获取信息
        options.inJustDecodeBounds = true
        if (uri == null) {
            BitmapFactory.decodeFile(path, options)
        }else {
            val input = requireContext().contentResolver.openInputStream(uri)
            BitmapFactory.decodeStream(input, null, options)
        }
        // 预获取信息,大图紧缩后加载
        val width = options.outWidth
        val height = options.outHeight
        Log.d("TAG", "before compress: width = " +
                options.outWidth + ", height = " + options.outHeight)
        // 尺度紧缩
        var size = 1
        while (width / size >= MAX_WIDTH || height / size >= MAX_HEIGHT) {
            size *= 2
        }
        options.inSampleSize = size
        options.inJustDecodeBounds = false
        bitmap = if (uri == null) {
            BitmapFactory.decodeFile(path, options)
        }else {
            val input = requireContext().contentResolver.openInputStream(uri)
            BitmapFactory.decodeStream(input, null, options)
        }
        Log.d("TAG", "after compress: width = " +
                options.outWidth + ", height = " + options.outHeight)
        // 质量紧缩
        val baos = ByteArrayOutputStream()
        bitmap!!.compress(Bitmap.CompressFormat.JPEG, 80, baos)
        val bais = ByteArrayInputStream(baos.toByteArray())
        options.inSampleSize = 1
        bitmap = BitmapFactory.decodeStream(bais, null, options)
        return bitmap
    }
    private fun cropImage(path: String) {
        cropImage(getUriForFile(File(path)))
    }
    private fun cropImage(uri: Uri) {
        val intent = Intent("com.android.camera.action.CROP")
        // Android 7.0需求暂时增加读取Url的权限
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
//        intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
        intent.setDataAndType(uri, "image/*")
        // 使图片处于可裁剪状况
        intent.putExtra("crop", "true")
        // 裁剪框的份额(根据需求显现的图片份额进行设置)
//        if (Build.MANUFACTURER.contains("HUAWEI")) {
//            //硬件厂商为华为的,默许是圆形裁剪框,这儿让它无法成圆形
//            intent.putExtra("aspectX", 9999)
//            intent.putExtra("aspectY", 9998)
//        } else {
//            //其他手机一般默以为方形
//            intent.putExtra("aspectX", 1)
//            intent.putExtra("aspectY", 1)
//        }
        // 设置裁剪区域的形状,默以为矩形,也可设置为圆形,或许无效
        // intent.putExtra("circleCrop", true);
        // 让裁剪框支持缩放
        intent.putExtra("scale", true)
        // 属性控制裁剪结束,保存的图片的巨细格局。太大会OOM(return-data)
//        intent.putExtra("outputX", 400)
//        intent.putExtra("outputY", 400)
        // 生成暂时文件
        val cropFile = createFile("Crop")
        // 裁切图片时不能运用provider的uri,不然无法保存
//        val cropUri = getUriForFile(cropFile)
        val cropUri = Uri.fromFile(cropFile)
        intent.putExtra(MediaStore.EXTRA_OUTPUT, cropUri)
        // 记载暂时方位
        cropPicPath = cropFile.absolutePath
        // 设置图片的输出格局
        intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString())
        // return-data=true传递的为缩略图,小米手机默许传递大图, Android 11以上设置为true会闪退
        intent.putExtra("return-data", false)
        startActivityForResult(intent, REQUEST_CROP_CODE)
    }
    // 保存到外部贮存-公有目录-Picture内,而且无需贮存权限
    private fun insert2Pictures() {
        binding.image.drawable?.let {
            val bitmap = it.toBitmap()
            val baos = ByteArrayOutputStream()
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
            val bais = ByteArrayInputStream(baos.toByteArray())
            insert2Album(bais, "Media")
            showToast("导出到相册成功")
        }
    }
    // 运用MediaStore方式将流写入相册
    @Suppress("SameParameterValue")
    private fun insert2Album(inputStream: InputStream, type: String) {
        val fileName = "${type}_${System.currentTimeMillis()}.jpg"
        val contentValues = ContentValues()
        contentValues.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, fileName)
        // Android 10,途径保存在RELATIVE_PATH
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            //RELATIVE_PATH 字段表明相对途径,Fundark为相册下专有目录
            contentValues.put(
                MediaStore.Images.ImageColumns.RELATIVE_PATH,
                Environment.DIRECTORY_PICTURES + File.separator + "Fundark"
            )
        } else {
            val dstPath = StringBuilder().let { sb->
                sb.append(requireContext()
                    .getExternalFilesDir(Environment.DIRECTORY_PICTURES)!!.path)
                sb.append(File.separator)
                sb.append("Fundark")
                sb.append(File.separator)
                sb.append(fileName)
                sb.toString()
            }
            //DATA字段在Android 10.0 之后现已抛弃(Android 11又启用了,可是只读)
            contentValues.put(MediaStore.Images.ImageColumns.DATA, dstPath)
        }
        // 刺进相册
        val uri =  requireContext().contentResolver
            .insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
        // 写入文件
        uri?.let {
            write2File(it, inputStream)
        }
    }
    private fun write2File(uri: Uri, inputStream: InputStream) {
        // 从Uri结构输出流
        requireContext().contentResolver.openOutputStream(uri)?.use { outputStream->
            val byteArray = ByteArray(1024)
            var len: Int
            do {
                //从输入流里读取数据
                len = inputStream.read(byteArray)
                if (len != -1) {
                    outputStream.write(byteArray, 0, len)
                    outputStream.flush()
                }
            } while (len != -1)
        }
    }
    private fun clearCachePictures() {
        // 外部贮存-私有目录-files-Pictures目录
        requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES)?.let { dir->
            // 删去其中的图片
            try {
                val pics = dir.listFiles()
                pics?.forEach { pic ->
                    pic.delete()
                }
                showToast("铲除缓存成功")
            }catch (e: Exception) {
                e.printStackTrace()
                showToast("铲除缓存失利")
            }
        }
    }
    private fun clearAppPictures() {
        val selection: String
        val selectionArgs: String
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            selection = "${MediaStore.Images.ImageColumns.RELATIVE_PATH} like ?"
            selectionArgs = "%" + Environment.DIRECTORY_PICTURES + File.separator + "Fundark" + "%"
        } else {
            val dstPath = StringBuilder().let { sb->
                sb.append(requireContext()
                    .getExternalFilesDir(Environment.DIRECTORY_PICTURES)!!.path)
                sb.append(File.separator)
                sb.append("Fundark")
                sb.toString()
            }
            selection = "${MediaStore.Images.ImageColumns.DATA} like ?"
            selectionArgs = "%$dstPath%"
        }
        val num = requireContext().contentResolver.delete(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            selection,
            arrayOf(selectionArgs)
        )
        showToast("删去本运用相册图片${num}张")
    }
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

里边的BaseActivity读者自己改下。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".camera.TakePhotoFragment">
    <Button
        android:id="@+id/takePhoto"
        android:text="@string/take_photo_by_system"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginTop="10dp"
        />
    <Button
        android:id="@+id/pickPhoto"
        android:text="@string/pick_photo_by_system"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/takePhoto"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginTop="10dp"
        />
    <Button
        android:id="@+id/insertPictures"
        android:text="@string/insert_photo_to_pictures"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/pickPhoto"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginTop="10dp"
        />
    <Button
        android:id="@+id/clearCache"
        android:text="@string/clear_Cache"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/insertPictures"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/clearPictures"
        android:layout_marginTop="10dp"
        />
    <Button
        android:id="@+id/clearPictures"
        android:text="@string/clear_this_pictures"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/insertPictures"
        app:layout_constraintLeft_toRightOf="@+id/clearCache"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginTop="10dp"
        />
    <TextView
        android:id="@+id/enable_crop_text"
        android:textSize="18sp"
        android:text="@string/enable_crop"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="@id/cropSwitch"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@id/cropSwitch"
        app:layout_constraintBottom_toBottomOf="@id/cropSwitch"
        />
    <androidx.appcompat.widget.SwitchCompat
        android:id="@+id/cropSwitch"
        android:checked="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        app:layout_constraintLeft_toRightOf="@id/enable_crop_text"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/clearPictures"
        />
    <ImageView
        android:id="@+id/image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/cropSwitch"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginTop="10dp"
        tools:ignore="ContentDescription"
        />
</androidx.constraintlayout.widget.ConstraintLayout>