1200字范文,内容丰富有趣,写作的好帮手!
1200字范文 > Android 不申请权限储存 删除相册图片

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

时间:2020-02-18 21:21:02

相关推荐

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

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()press(pressFormat.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_PATHif (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: Intdo {//从输入流里读取数据len = inputStream.read(byteArray)if (len != -1) {outputStream.write(byteArray, 0, len)outputStream.flush()}} while (len != -1)}}

这里的代码也只是示例,关于Exception的部分没做,但是关键在于我们没有申请权限就把图片保存到相册去了,现在打开你的相册就能发现你保存的图片,点开详细信息就能看到它的实际位置:

删除相册图片

刚开始我也觉得这里可能需要申请系统的储存权限,不然怎么可以去删除外部相册的图片呢?实际上,这里也是有限制的,你可以删除你自己创建的图片,这样一说是不是又很合理了。下面试试:

private fun clearAppPictures() {val selection: Stringval selectionArgs: Stringif (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.Manifestimport android.app.Activity.RESULT_OKimport android.content.ContentValuesimport android.content.Contextimport android.content.Intentimport android.graphics.Bitmapimport android.graphics.BitmapFactoryimport .Uriimport android.os.Buildimport android.os.Environmentimport android.provider.MediaStoreimport android.util.Logimport android.view.LayoutInflaterimport android.view.Viewimport android.view.ViewGroupimport androidx.core.content.FileProviderimport androidx.core.graphics.drawable.toBitmapimport androidx.core.util.Consumerimport com.silencefly96.module_base.base.BaseFragmentimport com.silencefly96.module_base.base.IPermissionHelperimport com.silencefly96.module_hardware.databinding.FragmentTakePhotoBindingimport java.io.*class TakePhotoFragment : BaseFragment() {companion object{const val REQUEST_CAMERA_CODE = 1const val REQUEST_ALBUM_CODE = 2const val REQUEST_CROP_CODE = 3const val MAX_WIDTH = 480const val MAX_HEIGHT = 720}private var _binding: FragmentTakePhotoBinding? = nullprivate 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-Picturesval 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 {// 转换为urireturn if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {//适配Android 7.0文件权限,通过FileProvider创建一个content类型的UriFileProvider.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 = trueif (uri == null) {BitmapFactory.decodeFile(path, options)}else {val input = requireContext().contentResolver.openInputStream(uri)BitmapFactory.decodeStream(input, null, options)}// 预获取信息,大图压缩后加载val width = options.outWidthval height = options.outHeightLog.d("TAG", "before compress: width = " +options.outWidth + ", height = " + options.outHeight)// 尺寸压缩var size = 1while (width / size >= MAX_WIDTH || height / size >= MAX_HEIGHT) {size *= 2}options.inSampleSize = sizeoptions.inJustDecodeBounds = falsebitmap = 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(pressFormat.JPEG, 80, baos)val bais = ByteArrayInputStream(baos.toByteArray())options.inSampleSize = 1bitmap = 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", pressFormat.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()press(pressFormat.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_PATHif (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: Intdo {//从输入流里读取数据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: Stringval selectionArgs: Stringif (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="/apk/res/android"xmlns:app="/apk/res-auto"xmlns:tools="/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".camera.TakePhotoFragment"><Buttonandroid: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"/><Buttonandroid: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"/><Buttonandroid: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"/><Buttonandroid: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"/><Buttonandroid: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"/><TextViewandroid: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.SwitchCompatandroid: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"/><ImageViewandroid: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>

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。