mirror of
https://github.com/lifegpc/eh_downloader_flutter.git
synced 2026-06-06 13:59:19 +08:00
feat: 实现了 Android 端写入文件和打开文件
This commit is contained in:
@@ -87,4 +87,6 @@ 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'
|
||||
implementation "org.greenrobot:eventbus:3.3.1"
|
||||
implementation "com.anggrayudi:storage:1.5.5"
|
||||
}
|
||||
|
||||
@@ -2,124 +2,41 @@ package com.lifegpc.ehf
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.lifegpc.ehf.annotation.ChannelMethod
|
||||
import com.lifegpc.ehf.data.mmkv.SAFSettings
|
||||
import com.lifegpc.ehf.util.ClipboardUtils
|
||||
import com.lifegpc.ehf.util.MethodChannelUtils
|
||||
import com.lifegpc.ehf.eventbus.SAFAuthEvent
|
||||
import com.lifegpc.ehf.platform.ClipboardPlugin
|
||||
import com.lifegpc.ehf.platform.MethodChannelUtils
|
||||
import com.lifegpc.ehf.platform.SAFPlugin
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
private val safAuthorizationCode = 0x10086
|
||||
private var safAuthorizationResult: MethodChannel.Result? = null
|
||||
private var afterAuthSuccess: (() -> Unit)? = null
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
MethodChannelUtils.registerMethodChannel(
|
||||
"lifegpc.eh_downloader_flutter/saf",
|
||||
flutterEngine,
|
||||
this
|
||||
SAFPlugin(this)
|
||||
)
|
||||
MethodChannelUtils.registerMethodChannel(
|
||||
"lifegpc.eh_downloader_flutter/clipboard",
|
||||
flutterEngine,
|
||||
ClipboardUtils
|
||||
ClipboardPlugin
|
||||
)
|
||||
}
|
||||
|
||||
@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 -> {
|
||||
SAFPlugin.safAuthorizationCode -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
// 授权成功
|
||||
onSafAuthSuccess(data!!)
|
||||
afterAuthSuccess?.invoke()
|
||||
EventBus.getDefault().post(SAFAuthEvent(true, data!!))
|
||||
} else {
|
||||
// 授权失败
|
||||
safAuthorizationResult?.error("Permission denied", null, null)
|
||||
safAuthorizationResult = null
|
||||
EventBus.getDefault().post(SAFAuthEvent(false, null))
|
||||
}
|
||||
afterAuthSuccess = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.lifegpc.ehf.eventbus
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
data class SAFAuthEvent(
|
||||
val success: Boolean = true,
|
||||
val data: Intent?,
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.lifegpc.ehf.util
|
||||
package com.lifegpc.ehf.platform
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
@@ -12,7 +12,7 @@ import com.lifegpc.ehf.annotation.ChannelMethod
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
object ClipboardUtils {
|
||||
object ClipboardPlugin {
|
||||
private const val AUTHORITY = "com.lifegpc.ehf.ClipboardImageProvider"
|
||||
|
||||
@ChannelMethod
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.lifegpc.ehf.util
|
||||
package com.lifegpc.ehf.platform
|
||||
|
||||
import android.util.Log
|
||||
import com.lifegpc.ehf.annotation.ChannelMethod
|
||||
@@ -0,0 +1,190 @@
|
||||
package com.lifegpc.ehf.platform
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.anggrayudi.storage.file.makeFile
|
||||
import com.anggrayudi.storage.file.openOutputStream
|
||||
import com.lifegpc.ehf.MainActivity
|
||||
import com.lifegpc.ehf.annotation.ChannelMethod
|
||||
import com.lifegpc.ehf.data.mmkv.SAFSettings
|
||||
import com.lifegpc.ehf.eventbus.SAFAuthEvent
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
|
||||
class SAFPlugin(private val activity: MainActivity) {
|
||||
private var safAuthorizationResult: MethodChannel.Result? = null
|
||||
private val fdMap = mutableMapOf<Int, DocumentFile>()
|
||||
private var onSAFAuthSuccess: (() -> Unit)? = null
|
||||
private var onSAFAuthFailed: (() -> Unit)? = null
|
||||
|
||||
companion object {
|
||||
const val safAuthorizationCode = 0x10086
|
||||
}
|
||||
|
||||
init {
|
||||
EventBus.getDefault().register(this)
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@Suppress("unused")
|
||||
fun onSAFAuthResult(event: SAFAuthEvent) {
|
||||
if (!event.success) {
|
||||
onSAFAuthFailed?.invoke()
|
||||
onSAFAuthFailed = null
|
||||
return
|
||||
}
|
||||
|
||||
val data = event.data!!
|
||||
val authUri = data.data!!
|
||||
saveSAFPermission(authUri)
|
||||
onSAFAuthSuccess?.invoke()
|
||||
onSAFAuthSuccess = null
|
||||
}
|
||||
|
||||
@ChannelMethod(responseManually = true)
|
||||
@Suppress("unused")
|
||||
private fun saveFile(
|
||||
channelResult: MethodChannel.Result,
|
||||
filename: String,
|
||||
dir: String,
|
||||
mimeType: String,
|
||||
content: ByteArray
|
||||
) {
|
||||
this.safAuthorizationResult = channelResult
|
||||
if (!checkSafPermission()) {
|
||||
onSAFAuthSuccess = {
|
||||
doWriteFile(filename, dir, mimeType, content)
|
||||
channelResult.success(null)
|
||||
}
|
||||
onSAFAuthFailed = {
|
||||
channelResult.error("Permission denied", null, null)
|
||||
}
|
||||
authSAF()
|
||||
} else {
|
||||
doWriteFile(filename, dir, mimeType, content)
|
||||
channelResult.success(null)
|
||||
}
|
||||
}
|
||||
|
||||
@ChannelMethod(responseManually = true)
|
||||
@Suppress("unused")
|
||||
private fun openFile(
|
||||
channelResult: MethodChannel.Result,
|
||||
filenameWithoutExtension: String,
|
||||
dir: String,
|
||||
mimeType: String,
|
||||
) {
|
||||
if (!checkSafPermission()) {
|
||||
onSAFAuthSuccess = {
|
||||
channelResult.success(doOpenFile(filenameWithoutExtension, mimeType, dir))
|
||||
}
|
||||
onSAFAuthFailed = {
|
||||
channelResult.error("Permission denied", null, null)
|
||||
}
|
||||
authSAF()
|
||||
} else {
|
||||
channelResult.success(doOpenFile(filenameWithoutExtension, mimeType, dir))
|
||||
}
|
||||
}
|
||||
|
||||
private fun doOpenFile(filenameWithoutExtension: String, mimeType: String, dir: String): Int {
|
||||
var documentFile =
|
||||
DocumentFile.fromTreeUri(activity, Uri.parse(SAFSettings.authorizedUri))!!
|
||||
documentFile = createFileRecursively(documentFile, dir)
|
||||
|
||||
val f = documentFile.makeFile(activity, filenameWithoutExtension, mimeType)!!
|
||||
|
||||
val id = f.hashCode()
|
||||
fdMap[id] = f
|
||||
return id
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入文件
|
||||
* @param id Int
|
||||
* @param bytes ByteArray
|
||||
* @return Int 写入的字节数
|
||||
*/
|
||||
@ChannelMethod
|
||||
@Suppress("unused")
|
||||
private fun writeFile(id: Int, bytes: ByteArray, append: Boolean): Int {
|
||||
val f = fdMap[id]!!
|
||||
f.openOutputStream(activity, append)!!.use {
|
||||
it.write(bytes)
|
||||
}
|
||||
return bytes.size
|
||||
}
|
||||
|
||||
@ChannelMethod
|
||||
@Suppress("unused")
|
||||
private fun closeFile(id: Int) {
|
||||
fdMap.remove(id)
|
||||
}
|
||||
|
||||
private fun createFileRecursively(base: DocumentFile, dir: String): DocumentFile {
|
||||
var file = base
|
||||
val pathPart = dir.split('/', '\\')
|
||||
pathPart.forEach {
|
||||
if (it.isNotEmpty()) {
|
||||
file = file.createDirectory(it)!!
|
||||
}
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入文件
|
||||
* @param filename String
|
||||
* @param dir String
|
||||
* @param mimeType String
|
||||
* @param content ByteArray
|
||||
*/
|
||||
private fun doWriteFile(filename: String, dir: String, mimeType: String, content: ByteArray) {
|
||||
var documentDir = DocumentFile.fromTreeUri(activity, Uri.parse(SAFSettings.authorizedUri))!!
|
||||
documentDir = createFileRecursively(documentDir, dir)
|
||||
|
||||
val filenameWithoutExtension = if (filename.indexOf('.') != -1) {
|
||||
filename.substring(0, filename.lastIndexOf('.'))
|
||||
} else {
|
||||
filename
|
||||
}
|
||||
val file = documentDir.createFile(mimeType, filenameWithoutExtension)!!
|
||||
val uri = file.uri
|
||||
activity.contentResolver.openOutputStream(uri)!!.use {
|
||||
it.write(content)
|
||||
}
|
||||
}
|
||||
|
||||
private fun authSAF() {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
activity.startActivityForResult(intent, safAuthorizationCode)
|
||||
}
|
||||
|
||||
// 保存权限
|
||||
private fun saveSAFPermission(uri: Uri) {
|
||||
val takeFlags =
|
||||
(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
activity.contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||
SAFSettings.authorizedUri = uri.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
|
||||
activity.contentResolver.takePersistableUriPermission(uri, flags)
|
||||
DocumentFile.fromTreeUri(activity, uri)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../utils.dart';
|
||||
import 'save_file.dart';
|
||||
|
||||
@@ -50,11 +52,19 @@ class Path {
|
||||
|
||||
class SAFFile {
|
||||
SAFFile(this._fd);
|
||||
|
||||
final int _fd;
|
||||
bool _disposed = false;
|
||||
Future<int> write(Uint8List data) async {
|
||||
|
||||
/// 写入文件,返回此次写入的字节数
|
||||
///
|
||||
/// [data] 要写入文件的数据
|
||||
///
|
||||
/// [append] 如果为 true 则追加写入,如果为 false 则清空原始文件内容,重新写入,默认为 true
|
||||
Future<int> write(Uint8List data, {bool append = true}) async {
|
||||
if (_disposed) throw Exception("File already closed");
|
||||
return await Path._safChannel.invokeMethod("writeFile", [_fd, data]);
|
||||
return await Path._safChannel
|
||||
.invokeMethod("writeFile", [_fd, data, append]);
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
|
||||
24
pubspec.lock
24
pubspec.lock
@@ -21,10 +21,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "49b1fad315e57ab0bbc15bcbb874e83116a1d78f77ebd500a4af6c9407d6b28e"
|
||||
sha256: e0902a06f0e00414e4e3438a084580161279f137aeb862274710f29ec10cf01e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.8"
|
||||
version: "3.3.9"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -77,10 +77,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: a7417cc44d9edb3f2c8760000270c99dba8c72ff66d0146772b8326565780745
|
||||
sha256: d912852cce27c9e80a93603db721c267716894462e7033165178b91138587972
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
version: "2.3.2"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -190,26 +190,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cryptography
|
||||
sha256: df156c5109286340817d21fa7b62f9140f17915077127dd70f8bd7a2a0997a35
|
||||
sha256: "11d083541666d80bba21190d35ff70b2497c81e064e82d1b8a07d801f7c7e282"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
version: "2.6.0"
|
||||
cryptography_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cryptography_flutter
|
||||
sha256: a66ce021e0b600688c2d51b0594cb156ee677ce9bfbc981b62219ad577dd302e
|
||||
sha256: "423ac095bbc4650fe6c07c64632ddc5dda1d4851241367efa0f26ca24b0ce480"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.1"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55"
|
||||
sha256: abd7625e16f51f554ea244d090292945ec4d4be7bfbaf2ec8cccea568919d334
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.3.3"
|
||||
device_info_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -980,5 +980,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.1.0-185.0.dev <4.0.0"
|
||||
flutter: ">=3.7.0"
|
||||
dart: ">=3.1.0 <4.0.0"
|
||||
flutter: ">=3.13.0"
|
||||
|
||||
Reference in New Issue
Block a user