mirror of
https://github.com/lifegpc/eh_downloader_flutter.git
synced 2026-06-06 05:49:03 +08:00
feat: Android get storage permission via SAF
This commit is contained in:
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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("")
|
||||
}
|
||||
@@ -15,6 +15,7 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user