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