feat: Android get storage permission via SAF

This commit is contained in:
13574
2023-09-09 01:18:59 +08:00
parent b23d1a193f
commit d0e5178dc8
7 changed files with 220 additions and 1 deletions

View File

@@ -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'
}

View File

@@ -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
}
}
}
}

View File

@@ -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<String, Method>()
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)
}
}
}
}
}

View File

@@ -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
)

View File

@@ -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("")
}

View File

@@ -15,6 +15,7 @@ allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://www.jitpack.io' }
}
}

View File

@@ -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<void> saveFile(String filenameWithoutExtension,String mimeType,Uint8List bytes,{String dir=""}) async{
return _safChannel.invokeMethod("saveFile",[filenameWithoutExtension,dir,mimeType,bytes]);
}
}