feat: 实现了 Android 端写入文件和打开文件

This commit is contained in:
13574
2023-09-17 12:33:00 +08:00
parent 24f9baa7dc
commit 274ad4978a
8 changed files with 237 additions and 110 deletions

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
package com.lifegpc.ehf.eventbus
import android.content.Intent
data class SAFAuthEvent(
val success: Boolean = true,
val data: Intent?,
)

View File

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

View File

@@ -1,4 +1,4 @@
package com.lifegpc.ehf.util
package com.lifegpc.ehf.platform
import android.util.Log
import com.lifegpc.ehf.annotation.ChannelMethod

View File

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

View File

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

View File

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