From d0e5178dc8f74a27a6cb2f2b1bbe77f142c802e5 Mon Sep 17 00:00:00 2001 From: 13574 <1357496606@qq.com> Date: Sat, 9 Sep 2023 01:18:59 +0800 Subject: [PATCH] feat: Android get storage permission via SAF --- android/app/build.gradle | 2 + .../kotlin/com/lifegpc/ehf/MainActivity.kt | 116 +++++++++++++++++- .../com/lifegpc/ehf/MethodChannelUtils.kt | 74 +++++++++++ .../lifegpc/ehf/annotation/ChannelMethod.kt | 14 +++ .../com/lifegpc/ehf/mmkv/SAFSettings.kt | 8 ++ android/build.gradle | 1 + lib/platform/path.dart | 6 + 7 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 android/app/src/main/kotlin/com/lifegpc/ehf/MethodChannelUtils.kt create mode 100644 android/app/src/main/kotlin/com/lifegpc/ehf/annotation/ChannelMethod.kt create mode 100644 android/app/src/main/kotlin/com/lifegpc/ehf/mmkv/SAFSettings.kt diff --git a/android/app/build.gradle b/android/app/build.gradle index 227d9de..1066fd2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -69,4 +69,6 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.github.DylanCaiCoding:MMKV-KTX:1.2.16' + implementation 'androidx.documentfile:documentfile:1.0.1' } diff --git a/android/app/src/main/kotlin/com/lifegpc/ehf/MainActivity.kt b/android/app/src/main/kotlin/com/lifegpc/ehf/MainActivity.kt index 3ab9654..e977572 100644 --- a/android/app/src/main/kotlin/com/lifegpc/ehf/MainActivity.kt +++ b/android/app/src/main/kotlin/com/lifegpc/ehf/MainActivity.kt @@ -1,6 +1,120 @@ package com.lifegpc.ehf +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.documentfile.provider.DocumentFile +import com.lifegpc.ehf.annotation.ChannelMethod +import com.lifegpc.ehf.mmkv.SAFSettings import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel +import java.lang.Exception -class MainActivity: FlutterActivity() { +class MainActivity : FlutterActivity() { + private val safAuthorizationCode = 0x10086 + private var safAuthorizationResult: MethodChannel.Result? = null + private var afterAuthSuccess:(()->Unit)?=null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + MethodChannelUtils.registerMethodChannel("lifegpc.eh_downloader_flutter/saf", flutterEngine, this) + } + + @ChannelMethod(responseManually = true) + private fun saveFile( + channelResult: MethodChannel.Result, + filename: String, + dir: String, + mimeType: String, + content: ByteArray + ) { + this.safAuthorizationResult = channelResult + if (!checkSafPermission()) { + authSAF(channelResult) { + doWriteFile(filename, dir, mimeType, content) + channelResult.success(null) + } + } else { + doWriteFile(filename, dir, mimeType,content) + channelResult.success(null) + } + } + + private fun doWriteFile(filename: String, dir: String, mimeType: String, content: ByteArray) { + var documentDir = DocumentFile.fromTreeUri(this, Uri.parse(SAFSettings.authorizedUri))!! + val pathPart=dir.split('/','\\') + pathPart.forEach { + if (it.isNotEmpty()){ + documentDir = documentDir.createDirectory(it)!! + } + } + + val filenameWithoutExtension=if (filename.indexOf('.')!=-1){ + filename.substring(0,filename.lastIndexOf('.')) + }else{ + filename + } + val file=documentDir.createFile(mimeType,filenameWithoutExtension)!! + val uri=file.uri + contentResolver.openOutputStream(uri)!!.use { + it.write(content) + } + } + + private fun authSAF(result: MethodChannel.Result,onSuccess:(()->Unit)?=null) { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + startActivityForResult(intent, safAuthorizationCode) + safAuthorizationResult = result + this.afterAuthSuccess=onSuccess + } + + private fun onSafAuthSuccess(data: Intent) { + // 保存权限 + val resultData = data.data!! + val takeFlags = + (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + contentResolver.takePersistableUriPermission(resultData, takeFlags) + SAFSettings.authorizedUri = resultData.toString() + } + + private fun checkSafPermission(): Boolean { + val dir = SAFSettings.authorizedUri + if (dir.isBlank()) return false + + val uri = Uri.parse(dir) + + return try { + val flags = + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + contentResolver.takePersistableUriPermission(uri, flags) + DocumentFile.fromTreeUri(this, uri) + true + } catch (e: Exception) { + e.printStackTrace() + false + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + safAuthorizationCode -> { + if (resultCode == Activity.RESULT_OK) { + // 授权成功 + onSafAuthSuccess(data!!) + afterAuthSuccess?.invoke() + } else { + // 授权失败 + safAuthorizationResult?.error("Permission denied", null, null) + safAuthorizationResult = null + } + afterAuthSuccess=null + } + } + } } diff --git a/android/app/src/main/kotlin/com/lifegpc/ehf/MethodChannelUtils.kt b/android/app/src/main/kotlin/com/lifegpc/ehf/MethodChannelUtils.kt new file mode 100644 index 0000000..84a2af0 --- /dev/null +++ b/android/app/src/main/kotlin/com/lifegpc/ehf/MethodChannelUtils.kt @@ -0,0 +1,74 @@ +package com.lifegpc.ehf + +import android.util.Log +import com.lifegpc.ehf.annotation.ChannelMethod +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel +import java.lang.Exception +import java.lang.reflect.Method +import java.lang.reflect.Modifier + +object MethodChannelUtils { + @JvmStatic + fun registerMethodChannel(channelName: String, flutterEngine: FlutterEngine, obj: Any,ignoreNotImplemented:Boolean=true) { + val methodsMapping = mutableMapOf() + val methods = obj::class.java.declaredMethods + methods.forEach { + val annotation = it.getAnnotation(ChannelMethod::class.java) ?: return@forEach + val methodName = annotation.methodName.takeIf(String::isNotBlank) ?: it.name + methodsMapping[methodName]=it + } + + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName).setMethodCallHandler { call, result -> + val invokeMethodName=call.method + val targetMethod=methodsMapping[invokeMethodName] + if (targetMethod==null){ + Log.w("MethodChannel","$channelName/$invokeMethodName not implemented.") + if (!ignoreNotImplemented){ + result.notImplemented() + }else{ + result.error("Not implemented",null,null) + } + return@setMethodCallHandler + }else{ + val argv=call.arguments?.takeIf { it is List<*> } as List<*>? ?:return@setMethodCallHandler result.error("Argument must be List",null,null) + val argTypes=targetMethod.parameterTypes + val targetArgc=argTypes.size + + val invokeTargetObject=if (Modifier.isStatic(targetMethod.modifiers)) null else obj + targetMethod.isAccessible=true + + if (targetArgc==argv.size){ + try { + val res=targetMethod.invoke(invokeTargetObject,*(argv.toTypedArray())) + if (res is Unit){ + result.success(null) + }else{ + result.success(res) + } + }catch (e:Exception){ + e.printStackTrace() + result.error("Error",e.toString(),e.stackTraceToString()) + } + }else if (targetArgc==argv.size+1&&argTypes[0]==MethodChannel.Result::class.java){ + try { + val responseManually=targetMethod.getAnnotation(ChannelMethod::class.java)!!.responseManually + val res=targetMethod.invoke(invokeTargetObject,result,*(argv.toTypedArray())) + if (!responseManually){ + if (res is Unit){ + result.success(null) + }else{ + result.success(res) + } + } + }catch (e:Exception){ + e.printStackTrace() + result.error("Error",e.toString(),e.stackTraceToString()) + } + }else{ + result.error("Error","Parameter count error, required: $targetArgc, found: $argv",null) + } + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/lifegpc/ehf/annotation/ChannelMethod.kt b/android/app/src/main/kotlin/com/lifegpc/ehf/annotation/ChannelMethod.kt new file mode 100644 index 0000000..55f299e --- /dev/null +++ b/android/app/src/main/kotlin/com/lifegpc/ehf/annotation/ChannelMethod.kt @@ -0,0 +1,14 @@ +package com.lifegpc.ehf.annotation + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +/** + * @param methodName + * @param responseManually 是否需要手动返回 + * 若为 true 则实现方法中需要手动调用 [io.flutter.plugin.common.MethodChannel.Result.success] 等方法 + * 否则,则会将实现方法的返回值返回给 [io.flutter.plugin.common.MethodChannel.Result] + */ +annotation class ChannelMethod( + val methodName:String="", + val responseManually:Boolean=false +) diff --git a/android/app/src/main/kotlin/com/lifegpc/ehf/mmkv/SAFSettings.kt b/android/app/src/main/kotlin/com/lifegpc/ehf/mmkv/SAFSettings.kt new file mode 100644 index 0000000..1a4c21a --- /dev/null +++ b/android/app/src/main/kotlin/com/lifegpc/ehf/mmkv/SAFSettings.kt @@ -0,0 +1,8 @@ +package com.lifegpc.ehf.mmkv + +import com.dylanc.mmkv.MMKVOwner +import com.dylanc.mmkv.mmkvString + +object SAFSettings:MMKVOwner(mmapID = "SAFSettings") { + var authorizedUri by mmkvString("") +} \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index f7eb7f6..688039d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -15,6 +15,7 @@ allprojects { repositories { google() mavenCentral() + maven { url 'https://www.jitpack.io' } } } diff --git a/lib/platform/path.dart b/lib/platform/path.dart index 279fd3e..d38a00c 100644 --- a/lib/platform/path.dart +++ b/lib/platform/path.dart @@ -7,6 +7,7 @@ final Logger _log = Logger("platformPath"); class Path { static const platform = MethodChannel("lifegpc.eh_downloader_flutter/path"); + static const _safChannel=MethodChannel("lifegpc.eh_downloader_flutter/saf"); String? _currentExe; bool _currentExeLoaded = false; @@ -25,4 +26,9 @@ class Path { _currentExeLoaded = true; return _currentExe; } + + /// 保存文件 + static Future saveFile(String filenameWithoutExtension,String mimeType,Uint8List bytes,{String dir=""}) async{ + return _safChannel.invokeMethod("saveFile",[filenameWithoutExtension,dir,mimeType,bytes]); + } }