mirror of
https://github.com/lifegpc/eh_downloader_flutter.git
synced 2026-07-08 01:30:28 +08:00
feat: Android get storage permission via SAF
This commit is contained in:
@@ -69,4 +69,6 @@ flutter {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
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
|
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.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 {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven { url 'https://www.jitpack.io' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ final Logger _log = Logger("platformPath");
|
|||||||
|
|
||||||
class Path {
|
class Path {
|
||||||
static const platform = MethodChannel("lifegpc.eh_downloader_flutter/path");
|
static const platform = MethodChannel("lifegpc.eh_downloader_flutter/path");
|
||||||
|
static const _safChannel=MethodChannel("lifegpc.eh_downloader_flutter/saf");
|
||||||
String? _currentExe;
|
String? _currentExe;
|
||||||
bool _currentExeLoaded = false;
|
bool _currentExeLoaded = false;
|
||||||
|
|
||||||
@@ -25,4 +26,9 @@ class Path {
|
|||||||
_currentExeLoaded = true;
|
_currentExeLoaded = true;
|
||||||
return _currentExe;
|
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