From d3a839aea78989b9719db4151637f33a95dfa9da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=92=E3=83=A5=E3=83=BC=E3=83=9E=E3=83=B3=E3=83=AF?= =?UTF-8?q?=E3=83=BC=E3=82=AD=E3=83=B3?= Date: Thu, 27 Apr 2023 17:51:49 +0800 Subject: [PATCH] fix save qrcode above android 10 --- .../top/mrxiaom/mirai/aoki/util/ShareUtil.kt | 156 +++++++++++++----- 1 file changed, 114 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/top/mrxiaom/mirai/aoki/util/ShareUtil.kt b/app/src/main/java/top/mrxiaom/mirai/aoki/util/ShareUtil.kt index 3554a38..d1e50b1 100644 --- a/app/src/main/java/top/mrxiaom/mirai/aoki/util/ShareUtil.kt +++ b/app/src/main/java/top/mrxiaom/mirai/aoki/util/ShareUtil.kt @@ -2,9 +2,7 @@ package top.mrxiaom.mirai.aoki.util import android.Manifest import android.app.Activity -import android.content.ContentValues -import android.content.Context -import android.content.Intent +import android.content.* import android.content.pm.PackageManager import android.net.Uri import android.os.Build @@ -14,6 +12,9 @@ import android.text.format.DateUtils import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import java.io.File +import java.io.FileNotFoundException +import java.io.OutputStream +import java.lang.IllegalStateException private const val REQUEST_CODE_SAVE_IMG = 6 @@ -23,11 +24,9 @@ private const val REQUEST_CODE_SAVE_IMG = 6 * 请求读取sd卡的权限 */ fun T.requireExternalPermission(block: T.() -> Unit) { - if (hasPermissions(*EXTERNAL_PERMISSION)) { - block(); - } else { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && !hasPermissions(*EXTERNAL_PERMISSION)) { ActivityCompat.requestPermissions(this, EXTERNAL_PERMISSION, REQUEST_CODE_SAVE_IMG) - } + } else block() } fun Context.hasPermissions(vararg perms: String): Boolean { @@ -39,16 +38,117 @@ private const val REQUEST_CODE_SAVE_IMG = 6 } } - fun Context.saveQRCode(name: String, byteArray: ByteArray): Boolean { - return kotlin.runCatching { - if (Build.VERSION.SDK_INT < 29) { - saveImageBelowAndroid10(name, byteArray) - } else { - saveImageAboveAndroid10(name, byteArray) +fun Context.saveQRCode(name: String, byteArray: ByteArray): Boolean { + return kotlin.runCatching { + val resolver = contentResolver + val relativePath = "Aoki" + val fileName = "Aoki_qrlogin_${name}_${System.currentTimeMillis()}.png" + var imageFile: File? = null + val imageValues = ContentValues().apply { + put(MediaStore.Images.Media.MIME_TYPE, "image/png") + val date = System.currentTimeMillis() / 1000 + put(MediaStore.Images.Media.DATE_ADDED, date) + put(MediaStore.Images.Media.DATE_MODIFIED, date) + } + val collection: Uri + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val path = "${Environment.DIRECTORY_PICTURES}/${relativePath}" + imageValues.apply { + put(MediaStore.Images.Media.DISPLAY_NAME, fileName) + put(MediaStore.Images.Media.RELATIVE_PATH, path) + put(MediaStore.Images.Media.IS_PENDING, 1) + } + collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + } else { + val pictures = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + val saveDir = File(pictures, relativePath) + + if (!saveDir.exists() && !saveDir.mkdirs()) { + throw FileNotFoundException(saveDir.absolutePath) + } + + imageFile = File(saveDir, fileName) + val fileNameWithoutExtension = imageFile.nameWithoutExtension + val fileExtension = imageFile.extension + + var queryUri = resolver.queryMediaImage28(imageFile.absolutePath) + var suffix = 1 + while (queryUri != null) { + val newName = fileNameWithoutExtension + "(${suffix++})." + fileExtension + imageFile = File(saveDir, newName) + queryUri = resolver.queryMediaImage28(imageFile.absolutePath) + } + + imageValues.apply { + put(MediaStore.Images.Media.DISPLAY_NAME, imageFile?.name) + // 保存路径 + val imagePath = imageFile?.absolutePath + put(MediaStore.Images.Media.DATA, imagePath) } - }.getOrNull() != null + collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } + val imageUri = resolver.insert(collection, imageValues) + (imageUri?.outputStream(resolver) ?: throw IllegalStateException()).use { output -> + output.write(byteArray) + imageUri.finishPending(this, resolver, imageFile) + } + }.getOrNull() != null +} +private fun Uri.outputStream(resolver: ContentResolver): OutputStream? { + return try { + resolver.openOutputStream(this) + } catch (e: FileNotFoundException) { + e.printStackTrace() + null + } +} +private fun Uri.finishPending( + context: Context, + resolver: ContentResolver, + outputFile: File?, +) { + val imageValues = ContentValues() + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (outputFile != null) { + imageValues.put(MediaStore.Images.Media.SIZE, outputFile.length()) + } + resolver.update(this, imageValues, null, null) + // 通知媒体库更新 + val intent = Intent(@Suppress("DEPRECATION") Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, this) + context.sendBroadcast(intent) + } else { + // Android Q添加了IS_PENDING状态,为0时其他应用才可见 + imageValues.put(MediaStore.Images.Media.IS_PENDING, 0) + resolver.update(this, imageValues, null, null) + } +} +private fun ContentResolver.queryMediaImage28(imagePath: String): Uri? { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return null + + val imageFile = File(imagePath) + if (imageFile.canRead() && imageFile.exists()) { + return Uri.fromFile(imageFile) } + // 保存的位置 + val collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + // 查询是否已经存在相同图片 + val query = this.query( + collection, + arrayOf(MediaStore.Images.Media._ID, @Suppress("DEPRECATION") MediaStore.Images.Media.DATA), + "${@Suppress("DEPRECATION") MediaStore.Images.Media.DATA} == ?", + arrayOf(imagePath), null + ) + query?.use { + while (it.moveToNext()) { + val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID) + val id = it.getLong(idColumn) + val existsUri = ContentUris.withAppendedId(collection, id) + return existsUri + } + } + return null +} private fun Context.saveImageBelowAndroid10(name: String, byteArray: ByteArray) { val appDir = File(Environment.getExternalStorageDirectory(), "Aoki") if (!appDir.exists()) { @@ -61,31 +161,3 @@ private const val REQUEST_CODE_SAVE_IMG = 6 val uri = Uri.fromFile(file) sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri)) } - - private fun Context.saveImageAboveAndroid10(name: String, byteArray: ByteArray) { - val now = System.currentTimeMillis() - val path = Environment.DIRECTORY_PICTURES + File.separator + "Aoki" - val fileName = "Aoki_qrlogin_${name}_$now.png" - val values = ContentValues().apply { - put(MediaStore.MediaColumns.RELATIVE_PATH, path) - put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) - put(MediaStore.MediaColumns.MIME_TYPE, "image/png") - put(MediaStore.MediaColumns.DATE_ADDED, now / 1000) - put(MediaStore.MediaColumns.DATE_MODIFIED, now / 1000) - put(MediaStore.MediaColumns.DATE_EXPIRES, (now + DateUtils.WEEK_IN_MILLIS) / 1000) - put(MediaStore.MediaColumns.IS_PENDING, 1) - } - val resolver = contentResolver - val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) ?: return - kotlin.runCatching { - resolver.openOutputStream(uri)?.use { it.write(byteArray) } - resolver.update(uri, values.apply { - clear() - put(MediaStore.MediaColumns.IS_PENDING, 0) - putNull(MediaStore.MediaColumns.DATE_EXPIRES) - }, null, null) - }.onFailure { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) - resolver.delete(uri, null) - }.getOrThrow() - } \ No newline at end of file