fix(android): vendor saf_util with a sound pendingResult lifecycle
saf_util 2.0.0 leaves an already-answered MethodChannel.Result behind in pendingResult: unrelated activity request codes consume (and answer) the pending picker, and a failed picker launch replies without clearing the pending state. The next delivery for request 1001 then replies a second time — IllegalStateException "Reply already submitted" in MainActivity.onActivityResult, crashing the process. Vendored at packages/saf_util (BSD-3) with take-and-clear reply ownership, request codes scoped to the plugin's own pickers, and a guarded reply on the teardown race.
This commit is contained in:
@@ -0,0 +1,77 @@
|
|||||||
|
## 2.0.0 (plezy vendored patch)
|
||||||
|
|
||||||
|
Vendored from pub.dev saf_util 2.0.0 with a Result-lifecycle fix:
|
||||||
|
pending picker replies are take-and-clear (no reply to an already-answered
|
||||||
|
Result → no "Reply already submitted" crash), unrelated activity request
|
||||||
|
codes no longer consume the pending picker, and failed picker launches
|
||||||
|
clear the stale pending state.
|
||||||
|
|
||||||
|
## 2.0.0
|
||||||
|
|
||||||
|
- Update to Kotlin 2.2.0
|
||||||
|
|
||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
- No changes. This is just a version bump to mark the stable release.
|
||||||
|
|
||||||
|
## 0.12.0
|
||||||
|
|
||||||
|
- Add `releasePersistedPermission` to release the persisted permission of a URI.
|
||||||
|
|
||||||
|
## 0.11.0
|
||||||
|
|
||||||
|
- Allow empty names in `.child`, which returns a [DocumentFile] from the first path param (like `stat`).
|
||||||
|
|
||||||
|
## 0.10.0
|
||||||
|
|
||||||
|
- Add `stat`. Unlike [documentFileFromUri], `stat` returns null if uri does not exist.
|
||||||
|
|
||||||
|
## 0.9.0
|
||||||
|
|
||||||
|
- Auto detect path type when `documentFileFromUri.isDir` is null.
|
||||||
|
|
||||||
|
## 0.8.0
|
||||||
|
|
||||||
|
- Add `hasPersistedPermission`.
|
||||||
|
|
||||||
|
## 0.7.0
|
||||||
|
|
||||||
|
- Add file descriptor support.
|
||||||
|
|
||||||
|
## 0.6.2
|
||||||
|
|
||||||
|
- Fix length overflow.
|
||||||
|
|
||||||
|
## 0.6.1
|
||||||
|
|
||||||
|
- Update docs.
|
||||||
|
|
||||||
|
## 0.6.0
|
||||||
|
|
||||||
|
- Add `pickFiles` and `pickDirectory` to replace `openFile` and `openDirectory`. The new methods return [DocumentFile] instead of Uri string.
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
- Add `openFile` and `openFiles` to open file picker dialog.
|
||||||
|
- Make sure read permission is always set when write permission is on.
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
- Relax minimum Dart and Flutter versions (Fixes #2).
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
- `saveThumbnailToFile` width and height params should be int.
|
||||||
|
|
||||||
|
## 0.2.0
|
||||||
|
|
||||||
|
- Add `saveThumbnailToFile`, which supports extracting image and video thumbnails from an SAF [DocumentFile].
|
||||||
|
|
||||||
|
## 0.1.0
|
||||||
|
|
||||||
|
- Add `writePermission` and `persistablePermission` to openDirectory
|
||||||
|
- Support renaming on file `DocumentFile`
|
||||||
|
|
||||||
|
## 0.0.1
|
||||||
|
|
||||||
|
- Initial release.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2023, Mgenware (Liu YuanYuan)
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# saf_util
|
||||||
|
|
||||||
|
[](https://pub.dev/packages/saf_util)
|
||||||
|
|
||||||
|
Util functions for SAF (Storage Access Framework). Note that this package doesn't provide any read / write functions (go to [saf_stream](https://github.com/flutter-cavalry/saf_stream) for that).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
[Documentation](https://pub.dev/documentation/saf_util/latest/saf_util/)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
group = "com.fluttercavalry.saf_util"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
val kotlinVersion = "2.2.20"
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle:8.11.1")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
id("kotlin-android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.fluttercavalry.saf_util"
|
||||||
|
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
getByName("main") {
|
||||||
|
java.srcDirs("src/main/kotlin")
|
||||||
|
}
|
||||||
|
getByName("test") {
|
||||||
|
java.srcDirs("src/test/kotlin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
all {
|
||||||
|
it.useJUnitPlatform()
|
||||||
|
|
||||||
|
it.outputs.upToDateWhen { false }
|
||||||
|
|
||||||
|
it.testLogging {
|
||||||
|
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||||
|
showStandardStreams = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("androidx.documentfile:documentfile:1.1.0")
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||||
|
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
rootProject.name = 'saf_util'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
rootProject.name = 'saf_util'
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.fluttercavalry.saf_util">
|
||||||
|
</manifest>
|
||||||
+862
@@ -0,0 +1,862 @@
|
|||||||
|
package com.fluttercavalry.saf_util
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Point
|
||||||
|
import android.media.MediaMetadataRetriever
|
||||||
|
import android.media.MediaMetadataRetriever.OPTION_CLOSEST_SYNC
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.provider.DocumentsContract
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
|
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||||
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||||
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||||
|
import io.flutter.plugin.common.MethodChannel.Result
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
|
||||||
|
|
||||||
|
/** SafUtilPlugin */
|
||||||
|
class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||||
|
/// The MethodChannel that will the communication between Flutter and native Android
|
||||||
|
///
|
||||||
|
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
|
||||||
|
/// when the Flutter Engine is detached from the Activity
|
||||||
|
private lateinit var channel : MethodChannel
|
||||||
|
|
||||||
|
private lateinit var context: Context
|
||||||
|
private var activity: Activity? = null
|
||||||
|
|
||||||
|
private var pendingResult: Result? = null
|
||||||
|
private var pendingArguments: PendingArguments? = null
|
||||||
|
private val requestCodeOpenDocumentTree = 1001
|
||||||
|
private val requestCodeOpenFiles = 1002
|
||||||
|
|
||||||
|
/// Atomically takes ownership of the pending picker state. Every reply to a
|
||||||
|
/// pending Result must go through this so no already-answered Result is ever
|
||||||
|
/// left behind to be answered again ("Reply already submitted" crashes).
|
||||||
|
private fun takePendingResult(): Result? {
|
||||||
|
val result = pendingResult
|
||||||
|
pendingResult = null
|
||||||
|
pendingArguments = null
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
private val fdMap = mutableMapOf<Int, ParcelFileDescriptor>()
|
||||||
|
|
||||||
|
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "saf_util")
|
||||||
|
channel.setMethodCallHandler(this)
|
||||||
|
context = flutterPluginBinding.applicationContext
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromActivity() {
|
||||||
|
activity = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||||
|
activity = binding.activity
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||||
|
activity = binding.activity
|
||||||
|
binding.addActivityResultListener { requestCode, resultCode, data ->
|
||||||
|
onActivityResult(requestCode, resultCode, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromActivityForConfigChanges() {
|
||||||
|
activity = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||||
|
when (call.method) {
|
||||||
|
"list" -> {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
var cursor: Cursor? = null
|
||||||
|
try {
|
||||||
|
val uri = call.argument<String>("uri") as String
|
||||||
|
|
||||||
|
val dir = documentFileFromUri(uri, true) ?: throw Exception("Failed to get DocumentFile from $uri")
|
||||||
|
val resolver = context.contentResolver
|
||||||
|
val mUri = dir.uri
|
||||||
|
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(
|
||||||
|
mUri,
|
||||||
|
DocumentsContract.getDocumentId(mUri)
|
||||||
|
)
|
||||||
|
val results = mutableListOf<Map<String, Any?>>()
|
||||||
|
|
||||||
|
cursor = resolver.query(
|
||||||
|
childrenUri,
|
||||||
|
arrayOf(
|
||||||
|
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||||
|
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||||
|
DocumentsContract.Document.COLUMN_SIZE,
|
||||||
|
DocumentsContract.Document.COLUMN_MIME_TYPE,
|
||||||
|
DocumentsContract.Document.COLUMN_LAST_MODIFIED
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
while (cursor?.moveToNext() == true) {
|
||||||
|
val documentId = cursor.getString(0)
|
||||||
|
val fileName = cursor.getString(1)
|
||||||
|
val fileSize = cursor.getLong(2)
|
||||||
|
val mimeType = cursor.getString(3)
|
||||||
|
val lastModified = cursor.getLong(4)
|
||||||
|
val documentUri = DocumentsContract.buildDocumentUriUsingTree(mUri, documentId)
|
||||||
|
|
||||||
|
// Determine if the file is a directory based on the MIME type
|
||||||
|
val isDirectory = DocumentsContract.Document.MIME_TYPE_DIR == mimeType
|
||||||
|
|
||||||
|
// Create a dictionary (map) for each file with its details
|
||||||
|
val fileInfo = fileObjMap(
|
||||||
|
documentUri,
|
||||||
|
isDirectory,
|
||||||
|
fileName,
|
||||||
|
fileSize,
|
||||||
|
lastModified,
|
||||||
|
)
|
||||||
|
results.add(fileInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
launch(Dispatchers.Main) { result.success(results) }
|
||||||
|
} catch (err: Exception) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.error("PluginError", err.message, null)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
cursor?.close()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"documentFileFromUri" -> {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val uri = call.argument<String>("uri") as String
|
||||||
|
val isDir = call.argument<Boolean>("isDir")
|
||||||
|
|
||||||
|
val df = documentFileFromUri(uri, isDir)
|
||||||
|
if (df == null) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(documentFileToMap(df))
|
||||||
|
}
|
||||||
|
} catch (err: Exception) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.error("PluginError", err.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"stat" -> {
|
||||||
|
// Unlike `documentFileFromUri` this returns null if `uri` doesn't exist.
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val uri = call.argument<String>("uri") as String
|
||||||
|
val isDir = call.argument<Boolean>("isDir")
|
||||||
|
|
||||||
|
val df = documentFileFromUri(uri, isDir)
|
||||||
|
if (df == null || !df.exists()) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(documentFileToMap(df))
|
||||||
|
}
|
||||||
|
} catch (err: Exception) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.error("PluginError", err.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"exists" -> {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val uri = call.argument<String>("uri") as String
|
||||||
|
val isDir = call.argument<Boolean>("isDir") as Boolean
|
||||||
|
|
||||||
|
val df = documentFileFromUri(uri, isDir)
|
||||||
|
if (df == null) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(false)
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(df.exists())
|
||||||
|
}
|
||||||
|
} catch (err: Exception) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.error("PluginError", err.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"delete" -> {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val uri = call.argument<String>("uri") as String
|
||||||
|
val isDir = call.argument<Boolean>("isDir") as Boolean
|
||||||
|
|
||||||
|
val df = documentFileFromUri(uri, isDir) ?: throw Exception("Failed to get DocumentFile from $uri")
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(df.delete())
|
||||||
|
}
|
||||||
|
} catch (err: Exception) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.error("PluginError", err.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"getFileDescriptor" -> {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val uri = call.argument<String>("uri") as String
|
||||||
|
|
||||||
|
val df = documentFileFromUri(uri, false) ?: throw Exception("Failed to get DocumentFile from $uri")
|
||||||
|
val fd = context.contentResolver.openFileDescriptor(df.uri, "r") ?: throw Exception("Failed to open file descriptor")
|
||||||
|
val fdInt = fd.fd
|
||||||
|
fdMap[fdInt] = fd
|
||||||
|
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(fdInt)
|
||||||
|
}
|
||||||
|
} catch (err: Exception) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.error("PluginError", err.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"closeFileDescriptor" -> {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val fdInt = call.argument<Int>("fd") as Int
|
||||||
|
val fd = fdMap.remove(fdInt)
|
||||||
|
fd?.close()
|
||||||
|
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
} catch (err: Exception) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.error("PluginError", err.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"mkdirp" -> {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val uri = call.argument<String?>("uri") as String
|
||||||
|
val names =
|
||||||
|
call.argument<ArrayList<String>>("names") as ArrayList<String>
|
||||||
|
|
||||||
|
var curDocument = documentFileFromUri(uri, true) ?: throw Exception("Failed to get DocumentFile from $uri")
|
||||||
|
for (curName in names) {
|
||||||
|
val findRes = findDirectChild(curDocument.uri, curName)
|
||||||
|
val nextDocument: DocumentFile?
|
||||||
|
if (findRes == null) {
|
||||||
|
val createRes = curDocument.createDirectory(curName)
|
||||||
|
if (createRes != null && createRes.name != curName) {
|
||||||
|
// There are cases where the created directory has a different name due to concurrent operations.
|
||||||
|
// In this case, we need to find the directory with the correct name again.
|
||||||
|
val findRes2 = findDirectChild(curDocument.uri, curName)
|
||||||
|
nextDocument = if (findRes2 != null) {
|
||||||
|
documentFileFromUriObj(findRes2.uri, findRes2.isDir)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nextDocument = createRes
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nextDocument = documentFileFromUriObj(findRes.uri, findRes.isDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextDocument == null) {
|
||||||
|
throw Exception("Failed to create directory at $curName")
|
||||||
|
}
|
||||||
|
if (nextDocument.isFile) {
|
||||||
|
throw Exception("File found at $curName while creating directory")
|
||||||
|
}
|
||||||
|
curDocument = nextDocument
|
||||||
|
}
|
||||||
|
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(documentFileToMap(curDocument))
|
||||||
|
}
|
||||||
|
} catch (err: Exception) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.error("PluginError", err.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"child" -> {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val uri = call.argument<String>("uri") as String
|
||||||
|
val names = call.argument<ArrayList<String>>("names") as ArrayList<String>
|
||||||
|
|
||||||
|
var curDocument = documentFileFromUri(uri, true) ?: throw Exception("Failed to get DocumentFile from $uri")
|
||||||
|
for (curName in names) {
|
||||||
|
val findRes = findDirectChild(curDocument.uri, curName)
|
||||||
|
if (findRes == null) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
curDocument = documentFileFromUriObj(findRes.uri, findRes.isDir) ?: throw Exception("Failed to get DocumentFile at $curName")
|
||||||
|
}
|
||||||
|
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(documentFileToMap(curDocument))
|
||||||
|
}
|
||||||
|
} catch (err: Exception) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.error("PluginError", err.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"rename" -> {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val uri = call.argument<String>("uri") as String
|
||||||
|
val isDir = call.argument<Boolean>("isDir") as Boolean
|
||||||
|
val newName = call.argument<String>("newName") as String
|
||||||
|
|
||||||
|
val df = documentFileFromUri(uri, isDir) ?: throw Exception("Failed to get DocumentFile from $uri")
|
||||||
|
if (isDir) {
|
||||||
|
val success = df.renameTo(newName)
|
||||||
|
if (!success) {
|
||||||
|
throw Exception("Failed to rename to $newName")
|
||||||
|
}
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(documentFileToMap(df))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val newUri = renameFileDocumentFile(df, newName)
|
||||||
|
?: throw Exception("Failed to rename to $newName")
|
||||||
|
val newDF = documentFileFromUriObj(newUri, false)
|
||||||
|
?: throw Exception("Failed to get DocumentFile from $newUri")
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(documentFileToMap(newDF))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: Exception) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.error("PluginError", err.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"moveTo" -> {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val uri = call.argument<String>("uri") as String
|
||||||
|
val isDir = call.argument<Boolean>("isDir") as Boolean
|
||||||
|
val parentUri = call.argument<String>("parentUri") as String
|
||||||
|
val newParentUri = call.argument<String>("newParentUri") as String
|
||||||
|
|
||||||
|
val uriObj = uri.toUri()
|
||||||
|
val parentUriObj = parentUri.toUri()
|
||||||
|
val newParentUriObj = newParentUri.toUri()
|
||||||
|
|
||||||
|
val resUri = DocumentsContract.moveDocument(
|
||||||
|
context.contentResolver,
|
||||||
|
uriObj,
|
||||||
|
parentUriObj,
|
||||||
|
newParentUriObj
|
||||||
|
) ?: throw Exception("Failed to move document")
|
||||||
|
|
||||||
|
val resultDF = documentFileFromUriObj(resUri, isDir) ?: throw Exception("Failed to get DocumentFile from $resUri")
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(documentFileToMap(resultDF))
|
||||||
|
}
|
||||||
|
} catch (err: Exception) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.error("PluginError", err.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"copyTo" -> {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val uri = call.argument<String>("uri") as String
|
||||||
|
val isDir = call.argument<Boolean>("isDir") as Boolean
|
||||||
|
val newParentUri = call.argument<String>("newParentUri") as String
|
||||||
|
|
||||||
|
val uriObj = uri.toUri()
|
||||||
|
val newParentUriObj = newParentUri.toUri()
|
||||||
|
|
||||||
|
val resUri = DocumentsContract.copyDocument(
|
||||||
|
context.contentResolver,
|
||||||
|
uriObj,
|
||||||
|
newParentUriObj
|
||||||
|
) ?: throw Exception("Failed to move document")
|
||||||
|
|
||||||
|
val resultDF = documentFileFromUriObj(resUri, isDir) ?: throw Exception("Failed to get DocumentFile from $resUri")
|
||||||
|
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(documentFileToMap(resultDF))
|
||||||
|
}
|
||||||
|
} catch (err: Exception) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.error("PluginError", err.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"pickDirectory" -> {
|
||||||
|
try {
|
||||||
|
val initialUri = call.argument<String>("initialUri")
|
||||||
|
val writePermission = call.argument<Boolean>("writePermission") ?: false
|
||||||
|
val persistablePermission = call.argument<Boolean>("persistablePermission") ?: false
|
||||||
|
|
||||||
|
if (activity == null) {
|
||||||
|
result.error("NO_ACTIVITY", "Activity is null", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (pendingResult != null) {
|
||||||
|
result.error("ALREADY_PICKING", "Another picker process is already in progress", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the result to return the URI later
|
||||||
|
pendingResult = result
|
||||||
|
pendingArguments = PendingDirArguments(writePermission, persistablePermission)
|
||||||
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||||
|
if (initialUri != null) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri.toUri())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (persistablePermission) {
|
||||||
|
intent.addFlags(
|
||||||
|
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
intent.addFlags(
|
||||||
|
if (writePermission) Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
else Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
)
|
||||||
|
|
||||||
|
activity?.startActivityForResult(intent, requestCodeOpenDocumentTree)
|
||||||
|
} catch (err: Exception) {
|
||||||
|
// Launch failed after pendingResult was set: drop the pending state
|
||||||
|
// so a later activity result can't reply to this Result again.
|
||||||
|
takePendingResult()
|
||||||
|
result.error("PluginError", err.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"pickFiles" -> {
|
||||||
|
try {
|
||||||
|
val initialUri = call.argument<String>("initialUri")
|
||||||
|
val multiple = call.argument<Boolean>("multiple") ?: false
|
||||||
|
val mimeTypes = call.argument<ArrayList<String>>("mimeTypes") ?: arrayListOf()
|
||||||
|
|
||||||
|
if (activity == null) {
|
||||||
|
result.error("NO_ACTIVITY", "Activity is null", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (pendingResult != null) {
|
||||||
|
result.error("ALREADY_PICKING", "Another picker process is already in progress", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the result to return the URI later
|
||||||
|
pendingResult = result
|
||||||
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
if (initialUri != null) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri.toUri())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (multiple) {
|
||||||
|
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mimeTypes.isEmpty()) {
|
||||||
|
intent.type = "*/*"
|
||||||
|
} else if (mimeTypes.size == 1) {
|
||||||
|
intent.type = mimeTypes[0]
|
||||||
|
} else {
|
||||||
|
intent.type = "*/*"
|
||||||
|
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
activity?.startActivityForResult(intent, requestCodeOpenFiles)
|
||||||
|
} catch (err: Exception) {
|
||||||
|
// Launch failed after pendingResult was set: drop the pending state
|
||||||
|
// so a later activity result can't reply to this Result again.
|
||||||
|
takePendingResult()
|
||||||
|
result.error("PluginError", err.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"hasPersistedPermission" -> {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val uri = call.argument<String>("uri") as String
|
||||||
|
val checkRead = call.argument<Boolean>("checkRead") ?: true
|
||||||
|
val checkWrite = call.argument<Boolean>("checkWrite") ?: false
|
||||||
|
|
||||||
|
val persisted = hasPersistedUriPermission(
|
||||||
|
context,
|
||||||
|
uri.toUri(),
|
||||||
|
checkRead,
|
||||||
|
checkWrite
|
||||||
|
)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(persisted)
|
||||||
|
}
|
||||||
|
} catch (err: Exception) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.error("PluginError", err.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"releasePersistedPermission" -> {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val uri = call.argument<String>("uri") as String
|
||||||
|
val read = call.argument<Boolean>("read") ?: true
|
||||||
|
val write = call.argument<Boolean>("write") ?: false
|
||||||
|
|
||||||
|
context.contentResolver.releasePersistableUriPermission(
|
||||||
|
uri.toUri(),
|
||||||
|
if (read && write) Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
else if (read) Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
else if (write) Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
} catch (err: Exception) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.error("PluginError", err.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"saveThumbnailToFile" -> {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val uri = (call.argument<String>("uri") as String).toUri()
|
||||||
|
val dest = call.argument<String>("destPath")!!
|
||||||
|
val format = call.argument<String>("format") ?: "jpeg"
|
||||||
|
val isPng = format == "png"
|
||||||
|
val quality = call.argument("quality") ?: if (isPng) 100 else 80
|
||||||
|
|
||||||
|
val width = call.argument<Int>("width")!!
|
||||||
|
val height = call.argument<Int>("height")!!
|
||||||
|
|
||||||
|
val df = DocumentFile.fromSingleUri(context, uri)
|
||||||
|
val mime = df?.type
|
||||||
|
if (mime == null) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(false)
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val bitmap: Bitmap?
|
||||||
|
// Use MediaMetadataRetriever for video files.
|
||||||
|
if (mime.startsWith("video/")) {
|
||||||
|
val mmr = MediaMetadataRetriever()
|
||||||
|
mmr.setDataSource(context, uri)
|
||||||
|
bitmap = if (Build.VERSION.SDK_INT >= 27) {
|
||||||
|
mmr.getScaledFrameAtTime(-1, OPTION_CLOSEST_SYNC, width, height)
|
||||||
|
} else {
|
||||||
|
mmr.frameAtTime
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use DocumentsContract for other files.
|
||||||
|
bitmap = DocumentsContract.getDocumentThumbnail(
|
||||||
|
context.contentResolver,
|
||||||
|
uri,
|
||||||
|
Point(width, height),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bitmap != null) {
|
||||||
|
File(dest).writeBitmap(
|
||||||
|
bitmap,
|
||||||
|
if (isPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG,
|
||||||
|
quality,
|
||||||
|
)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.success(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: Exception) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
result.error("PluginError", err.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the result of the folder/file pickers. Returns whether the
|
||||||
|
// request code was ours — unrelated request codes must not touch (let
|
||||||
|
// alone answer) the pending picker state.
|
||||||
|
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||||
|
if (requestCode != requestCodeOpenDocumentTree && requestCode != requestCodeOpenFiles) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Take ownership before replying: a duplicate delivery finds no pending
|
||||||
|
// state instead of a second reply on an already-answered Result.
|
||||||
|
val args = pendingArguments
|
||||||
|
val result = takePendingResult() ?: return true
|
||||||
|
try {
|
||||||
|
if (requestCode == requestCodeOpenDocumentTree) {
|
||||||
|
// Handle the result of the folder picker.
|
||||||
|
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||||
|
val uri: Uri? = data.data
|
||||||
|
if (uri != null && args is PendingDirArguments) {
|
||||||
|
if (args.persistablePermission) {
|
||||||
|
context.contentResolver.takePersistableUriPermission(
|
||||||
|
uri,
|
||||||
|
if (args.writePermission) Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
else Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val df = documentFileFromUri(uri.toString(), true)
|
||||||
|
result.success(
|
||||||
|
if (df != null) documentFileToMap(df)
|
||||||
|
else null
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle the result of file picker.
|
||||||
|
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||||
|
val uris: List<Uri> = if (data.clipData != null) {
|
||||||
|
val clipData = data.clipData
|
||||||
|
val uris = mutableListOf<Uri>()
|
||||||
|
for (i in 0 until clipData!!.itemCount) {
|
||||||
|
uris.add(clipData.getItemAt(i).uri)
|
||||||
|
}
|
||||||
|
uris
|
||||||
|
} else {
|
||||||
|
listOf(data.data!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
val documentFileMaps: MutableList<Map<String, Any?>> = mutableListOf()
|
||||||
|
for (uri in uris) {
|
||||||
|
val df = documentFileFromUri(uri.toString(), false)
|
||||||
|
if (df != null) {
|
||||||
|
documentFileMaps.add(documentFileToMap(df))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success(documentFileMaps) // Return the URIs to Flutter
|
||||||
|
} else {
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: Exception) {
|
||||||
|
try {
|
||||||
|
result.error("PluginError", err.message, null)
|
||||||
|
} catch (_: IllegalStateException) {
|
||||||
|
// Reply already submitted — never crash the host activity over a
|
||||||
|
// picker teardown race.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
channel.setMethodCallHandler(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun documentFileFromUri(uri: String, isDir: Boolean?): DocumentFile? {
|
||||||
|
val uriObj = uri.toUri()
|
||||||
|
val isDirRes =
|
||||||
|
isDir ?: DocumentsContract.isTreeUri(uriObj)
|
||||||
|
return documentFileFromUriObj(uriObj, isDirRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun documentFileFromUriObj(uriObj: Uri, isDir: Boolean): DocumentFile? {
|
||||||
|
val res = if (isDir) {
|
||||||
|
DocumentFile.fromTreeUri(context, uriObj)
|
||||||
|
} else {
|
||||||
|
DocumentFile.fromSingleUri(context, uriObj)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasPersistedUriPermission(
|
||||||
|
context: Context,
|
||||||
|
uri: Uri,
|
||||||
|
checkRead: Boolean,
|
||||||
|
checkWrite: Boolean
|
||||||
|
): Boolean {
|
||||||
|
val permissions = context.contentResolver.persistedUriPermissions
|
||||||
|
for (permission in permissions) {
|
||||||
|
if (areSameDocumentLocation(permission.uri, uri)) {
|
||||||
|
val hasRead = !checkRead || permission.isReadPermission
|
||||||
|
val hasWrite = !checkWrite || permission.isWritePermission
|
||||||
|
return hasRead && hasWrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun areSameDocumentLocation(treeUri: Uri, docUri: Uri): Boolean {
|
||||||
|
val treeDocId = DocumentsContract.getTreeDocumentId(treeUri)
|
||||||
|
val docId = DocumentsContract.getDocumentId(docUri)
|
||||||
|
return treeDocId == docId
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findDirectChild(parentUri: Uri, name: String): UriInfo? {
|
||||||
|
var cursor: Cursor? = null
|
||||||
|
try {
|
||||||
|
val resolver = context.contentResolver
|
||||||
|
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(
|
||||||
|
parentUri,
|
||||||
|
DocumentsContract.getDocumentId(parentUri)
|
||||||
|
)
|
||||||
|
cursor = resolver.query(
|
||||||
|
childrenUri,
|
||||||
|
arrayOf(
|
||||||
|
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||||
|
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||||
|
DocumentsContract.Document.COLUMN_MIME_TYPE,
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
while (cursor?.moveToNext() == true) {
|
||||||
|
val documentId = cursor.getString(0)
|
||||||
|
val fileName = cursor.getString(1)
|
||||||
|
val mimeType = cursor.getString(2)
|
||||||
|
val isDirectory = DocumentsContract.Document.MIME_TYPE_DIR == mimeType
|
||||||
|
|
||||||
|
if (fileName == name) {
|
||||||
|
val documentUri = DocumentsContract.buildDocumentUriUsingTree(parentUri, documentId)
|
||||||
|
return UriInfo(documentUri, fileName, isDirectory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
cursor?.close()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renameFileDocumentFile(df: DocumentFile, newName: String): Uri? {
|
||||||
|
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:documentfile/documentfile/src/main/java/androidx/documentfile/provider/TreeDocumentFile.java
|
||||||
|
try {
|
||||||
|
val result = DocumentsContract.renameDocument(
|
||||||
|
context.contentResolver, df.uri, newName
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
} catch (err: Exception) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun documentFileToMap(file: DocumentFile): Map<String, Any?> {
|
||||||
|
return fileObjMap(
|
||||||
|
file.uri,
|
||||||
|
file.isDirectory,
|
||||||
|
file.name ?: "",
|
||||||
|
file.length(),
|
||||||
|
file.lastModified(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fileObjMap(
|
||||||
|
uri: Uri,
|
||||||
|
isDir: Boolean,
|
||||||
|
name: String,
|
||||||
|
length: Long,
|
||||||
|
lastMod: Long,
|
||||||
|
): Map<String, Any?> {
|
||||||
|
return mapOf(
|
||||||
|
"uri" to uri.toString(),
|
||||||
|
"isDir" to isDir,
|
||||||
|
"name" to name,
|
||||||
|
"length" to length,
|
||||||
|
"lastModified" to lastMod,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) {
|
||||||
|
outputStream().use { out ->
|
||||||
|
bitmap.compress(format, quality, out)
|
||||||
|
out.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class UriInfo(val uri: Uri, val name: String, val isDir: Boolean)
|
||||||
|
|
||||||
|
internal open class PendingArguments
|
||||||
|
|
||||||
|
internal class PendingDirArguments(
|
||||||
|
val writePermission: Boolean,
|
||||||
|
val persistablePermission: Boolean,
|
||||||
|
): PendingArguments()
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
package com.fluttercavalry.saf_util
|
||||||
|
|
||||||
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation.
|
||||||
|
*
|
||||||
|
* Once you have built the plugin's example app, you can run these tests from the command
|
||||||
|
* line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
|
||||||
|
* you can run them directly from IDEs that support JUnit such as Android Studio.
|
||||||
|
*/
|
||||||
|
|
||||||
|
internal class SafUtilPluginTest {
|
||||||
|
@Test
|
||||||
|
fun onMethodCall_getPlatformVersion_returnsExpectedValue() {
|
||||||
|
val plugin = SafUtilPlugin()
|
||||||
|
|
||||||
|
val call = MethodCall("getPlatformVersion", null)
|
||||||
|
val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java)
|
||||||
|
plugin.onMethodCall(call, mockResult)
|
||||||
|
|
||||||
|
Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
import 'saf_util_platform_interface.dart';
|
||||||
|
|
||||||
|
class SafUtil {
|
||||||
|
/// Shows a folder picker dialog and returns the selected folder as [SafDocumentFile].
|
||||||
|
///
|
||||||
|
/// [initialUri] is the initial URI to show in the dialog.
|
||||||
|
/// [writePermission] is true if the folder should have write permission.
|
||||||
|
/// [persistablePermission] is true if the permission should be persistable.
|
||||||
|
Future<SafDocumentFile?> pickDirectory(
|
||||||
|
{String? initialUri,
|
||||||
|
bool? writePermission,
|
||||||
|
bool? persistablePermission}) {
|
||||||
|
return SafUtilPlatform.instance.pickDirectory(
|
||||||
|
initialUri: initialUri,
|
||||||
|
writePermission: writePermission,
|
||||||
|
persistablePermission: persistablePermission);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows a file picker dialog and returns the selected file [SafDocumentFile].
|
||||||
|
/// This calls [pickFiles] with [multiple] set to false.
|
||||||
|
///
|
||||||
|
/// [initialUri] is the initial URI to show in the dialog.
|
||||||
|
/// [mimeTypes] is a list of MIME types to filter the files.
|
||||||
|
Future<SafDocumentFile?> pickFile({
|
||||||
|
String? initialUri,
|
||||||
|
List<String>? mimeTypes,
|
||||||
|
}) {
|
||||||
|
return SafUtilPlatform.instance.pickFile(
|
||||||
|
initialUri: initialUri,
|
||||||
|
mimeTypes: mimeTypes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows a file picker dialog and returns a list of selected file [SafDocumentFile].
|
||||||
|
///
|
||||||
|
/// [initialUri] is the initial URI to show in the dialog.
|
||||||
|
/// [mimeTypes] is a list of MIME types to filter the files.
|
||||||
|
/// [multiple] is true if multiple files can be selected.
|
||||||
|
Future<List<SafDocumentFile>?> pickFiles({
|
||||||
|
String? initialUri,
|
||||||
|
List<String>? mimeTypes,
|
||||||
|
multiple = true,
|
||||||
|
}) {
|
||||||
|
return SafUtilPlatform.instance.pickFiles(
|
||||||
|
initialUri: initialUri,
|
||||||
|
mimeTypes: mimeTypes,
|
||||||
|
multiple: multiple,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lists the contents of the specified directory URI.
|
||||||
|
/// Returns a list of [SafDocumentFile] objects.
|
||||||
|
///
|
||||||
|
/// [uri] is the URI of the directory.
|
||||||
|
Future<List<SafDocumentFile>> list(String uri) {
|
||||||
|
return SafUtilPlatform.instance.list(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a [SafDocumentFile] object from the specified URI.
|
||||||
|
///
|
||||||
|
/// [uri] is the URI of the file or directory.
|
||||||
|
/// [isDir] is true if the URI is a directory. [null] means
|
||||||
|
/// auto-detect.
|
||||||
|
Future<SafDocumentFile?> documentFileFromUri(String uri, bool? isDir) {
|
||||||
|
return SafUtilPlatform.instance.documentFileFromUri(uri, isDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a [SafDocumentFile] object from the specified URI.
|
||||||
|
/// Unlike [documentFileFromUri], this method returns null
|
||||||
|
/// if the URI does not exist.
|
||||||
|
///
|
||||||
|
/// [uri] is the URI of the file or directory.
|
||||||
|
/// [isDir] is true if the URI is a directory. [null] means
|
||||||
|
/// auto-detect.
|
||||||
|
Future<SafDocumentFile?> stat(String uri, bool? isDir) {
|
||||||
|
return SafUtilPlatform.instance.stat(uri, isDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the specified file or directory exists.
|
||||||
|
///
|
||||||
|
/// [uri] is the URI of the file or directory.
|
||||||
|
/// [isDir] is true if the URI is a directory.
|
||||||
|
Future<bool> exists(String uri, bool isDir) {
|
||||||
|
return SafUtilPlatform.instance.exists(uri, isDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes the specified file or directory.
|
||||||
|
///
|
||||||
|
/// [uri] is the URI of the file or directory.
|
||||||
|
/// [isDir] is true if the URI is a directory.
|
||||||
|
Future<void> delete(String uri, bool isDir) {
|
||||||
|
return SafUtilPlatform.instance.delete(uri, isDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a directory and all its parent directories.
|
||||||
|
/// Returns the created directory as a [SafDocumentFile] object.
|
||||||
|
///
|
||||||
|
/// [uri] is the URI of the directory to create.
|
||||||
|
/// [names] is a list of directory names to create.
|
||||||
|
Future<SafDocumentFile> mkdirp(String uri, List<String> names) {
|
||||||
|
return SafUtilPlatform.instance.mkdirp(uri, names);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the child file or directory with the specified name.
|
||||||
|
///
|
||||||
|
/// [uri] is the URI of the parent directory.
|
||||||
|
/// [names] is a list of directory names to traverse.
|
||||||
|
Future<SafDocumentFile?> child(String uri, List<String> names) {
|
||||||
|
return SafUtilPlatform.instance.child(uri, names);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renames the specified file or directory.
|
||||||
|
/// Returns the renamed file or directory as a [SafDocumentFile] object.
|
||||||
|
///
|
||||||
|
/// [uri] is the URI of the file or directory.
|
||||||
|
/// [isDir] is true if the URI is a directory.
|
||||||
|
/// [newName] is the new name of the file or directory.
|
||||||
|
Future<SafDocumentFile> rename(String uri, bool isDir, String newName) {
|
||||||
|
return SafUtilPlatform.instance.rename(uri, isDir, newName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the specified file or directory to a new parent directory.
|
||||||
|
/// Returns the moved file or directory as a [SafDocumentFile] object.
|
||||||
|
///
|
||||||
|
/// [uri] is the URI of the file or directory.
|
||||||
|
/// [parentUri] is the URI of the current parent directory.
|
||||||
|
/// [newParentUri] is the URI of the new parent directory.
|
||||||
|
Future<SafDocumentFile> moveTo(
|
||||||
|
String uri, bool isDir, String parentUri, String newParentUri) {
|
||||||
|
return SafUtilPlatform.instance.moveTo(uri, isDir, parentUri, newParentUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies the specified file or directory to a new parent directory.
|
||||||
|
/// Returns the copied file or directory as a [SafDocumentFile] object.
|
||||||
|
///
|
||||||
|
/// [uri] is the URI of the file or directory.
|
||||||
|
/// [newParentUri] is the URI of the new parent directory.
|
||||||
|
Future<SafDocumentFile> copyTo(String uri, bool isDir, String newParentUri) {
|
||||||
|
return SafUtilPlatform.instance.copyTo(uri, isDir, newParentUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves the thumbnail of the specified document [uri] to a local file [destPath].
|
||||||
|
/// Image and video thumbnails are supported.
|
||||||
|
/// Returns true if the thumbnail is saved successfully, or false if no
|
||||||
|
/// thumbnail found in the document.
|
||||||
|
///
|
||||||
|
/// [uri] is the URI of the document.
|
||||||
|
/// [destPath] is the destination file path to save the thumbnail.
|
||||||
|
/// [width] is the max width of the thumbnail.
|
||||||
|
/// [height] is the max height of the thumbnail.
|
||||||
|
/// [format] is the format of the thumbnail file, default is 'jpeg'.
|
||||||
|
/// Supported formats are 'jpeg' and 'png'.
|
||||||
|
/// [quality] is the quality of the thumbnail file,
|
||||||
|
/// defaults to 80 for 'jpeg' and 100 for 'png'.
|
||||||
|
Future<bool> saveThumbnailToFile({
|
||||||
|
required String uri,
|
||||||
|
required int width,
|
||||||
|
required int height,
|
||||||
|
required String destPath,
|
||||||
|
String? format,
|
||||||
|
int? quality,
|
||||||
|
}) {
|
||||||
|
return SafUtilPlatform.instance.saveThumbnailToFile(
|
||||||
|
uri: uri,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
destPath: destPath,
|
||||||
|
format: format,
|
||||||
|
quality: quality,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deprecated functions. */
|
||||||
|
|
||||||
|
/// Shows a folder picker dialog and returns the selected folder URI.
|
||||||
|
///
|
||||||
|
/// [initialUri] is the initial URI to show in the dialog.
|
||||||
|
/// [writePermission] is true if the folder should have write permission.
|
||||||
|
/// [persistablePermission] is true if the permission should be persistable.
|
||||||
|
@Deprecated('Use [pickDirectory] instead, which returns a [SafDocumentFile].')
|
||||||
|
Future<String?> openDirectory(
|
||||||
|
{String? initialUri,
|
||||||
|
bool? writePermission,
|
||||||
|
bool? persistablePermission}) {
|
||||||
|
return SafUtilPlatform.instance.openDirectory(
|
||||||
|
initialUri: initialUri,
|
||||||
|
writePermission: writePermission,
|
||||||
|
persistablePermission: persistablePermission);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows a file picker dialog and returns the selected file URI.
|
||||||
|
/// This calls [openFiles] with [multiple] set to false.
|
||||||
|
///
|
||||||
|
/// [initialUri] is the initial URI to show in the dialog.
|
||||||
|
/// [mimeTypes] is a list of MIME types to filter the files.
|
||||||
|
@Deprecated('Use [pickFile] instead, which returns a [SafDocumentFile].')
|
||||||
|
Future<String?> openFile({
|
||||||
|
String? initialUri,
|
||||||
|
List<String>? mimeTypes,
|
||||||
|
}) {
|
||||||
|
return SafUtilPlatform.instance.openFile(
|
||||||
|
initialUri: initialUri,
|
||||||
|
mimeTypes: mimeTypes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows a file picker dialog and returns a list of selected file URIs.
|
||||||
|
///
|
||||||
|
/// [initialUri] is the initial URI to show in the dialog.
|
||||||
|
/// [mimeTypes] is a list of MIME types to filter the files.
|
||||||
|
@Deprecated('Use [pickFiles] instead, which returns a [SafDocumentFile].')
|
||||||
|
Future<List<String>?> openFiles({
|
||||||
|
String? initialUri,
|
||||||
|
List<String>? mimeTypes,
|
||||||
|
multiple = true,
|
||||||
|
}) {
|
||||||
|
return SafUtilPlatform.instance.openFiles(
|
||||||
|
initialUri: initialUri,
|
||||||
|
mimeTypes: mimeTypes,
|
||||||
|
multiple: multiple,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the file descriptor of the specified URI.
|
||||||
|
Future<int> getFileDescriptor(String uri) {
|
||||||
|
return SafUtilPlatform.instance.getFileDescriptor(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closes the specified file descriptor.
|
||||||
|
Future<void> closeFileDescriptor(int fd) {
|
||||||
|
return SafUtilPlatform.instance.closeFileDescriptor(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the specified URI has persisted permission.
|
||||||
|
/// Use [checkRead] and [checkWrite] to specify the type of permission to check.
|
||||||
|
/// [checkRead] defaults to true.
|
||||||
|
/// [checkWrite] defaults to false.
|
||||||
|
Future<bool> hasPersistedPermission(
|
||||||
|
String uri, {
|
||||||
|
bool checkRead = true,
|
||||||
|
bool checkWrite = false,
|
||||||
|
}) {
|
||||||
|
return SafUtilPlatform.instance.hasPersistedPermission(uri,
|
||||||
|
checkRead: checkRead, checkWrite: checkWrite);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Releases the persisted permission of the specified URI.
|
||||||
|
/// Use [read] and [write] to specify the type of permission to release.
|
||||||
|
/// [read] defaults to true.
|
||||||
|
/// [write] defaults to false.
|
||||||
|
Future<void> releasePersistedPermission(String uri,
|
||||||
|
{bool read = true, bool write = false}) async {
|
||||||
|
return SafUtilPlatform.instance
|
||||||
|
.releasePersistedPermission(uri, read: read, write: write);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'saf_util_platform_interface.dart';
|
||||||
|
|
||||||
|
/// An implementation of [SafUtilPlatform] that uses method channels.
|
||||||
|
class MethodChannelSafUtil extends SafUtilPlatform {
|
||||||
|
/// The method channel used to interact with the native platform.
|
||||||
|
@visibleForTesting
|
||||||
|
final methodChannel = const MethodChannel('saf_util');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SafDocumentFile?> pickDirectory(
|
||||||
|
{String? initialUri,
|
||||||
|
bool? writePermission,
|
||||||
|
bool? persistablePermission}) async {
|
||||||
|
final map =
|
||||||
|
await methodChannel.invokeMapMethod<String, dynamic>('pickDirectory', {
|
||||||
|
'initialUri': initialUri,
|
||||||
|
'writePermission': writePermission,
|
||||||
|
'persistablePermission': persistablePermission,
|
||||||
|
});
|
||||||
|
if (map == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return SafDocumentFile.fromMap(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> openDirectory(
|
||||||
|
{String? initialUri,
|
||||||
|
bool? writePermission,
|
||||||
|
bool? persistablePermission}) async {
|
||||||
|
final res = await pickDirectory(
|
||||||
|
initialUri: initialUri,
|
||||||
|
writePermission: writePermission,
|
||||||
|
persistablePermission: persistablePermission,
|
||||||
|
);
|
||||||
|
return res?.uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SafDocumentFile?> pickFile({
|
||||||
|
String? initialUri,
|
||||||
|
List<String>? mimeTypes,
|
||||||
|
}) async {
|
||||||
|
final res = await pickFiles(
|
||||||
|
initialUri: initialUri,
|
||||||
|
mimeTypes: mimeTypes,
|
||||||
|
multiple: false,
|
||||||
|
);
|
||||||
|
return res?.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> openFile({
|
||||||
|
String? initialUri,
|
||||||
|
List<String>? mimeTypes,
|
||||||
|
}) async {
|
||||||
|
final res = await pickFile(
|
||||||
|
initialUri: initialUri,
|
||||||
|
mimeTypes: mimeTypes,
|
||||||
|
);
|
||||||
|
return res?.uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<SafDocumentFile>?> pickFiles({
|
||||||
|
String? initialUri,
|
||||||
|
List<String>? mimeTypes,
|
||||||
|
multiple = true,
|
||||||
|
}) async {
|
||||||
|
final maps = await methodChannel
|
||||||
|
.invokeListMethod<Map<dynamic, dynamic>>('pickFiles', {
|
||||||
|
'initialUri': initialUri,
|
||||||
|
'mimeTypes': mimeTypes,
|
||||||
|
'multiple': multiple,
|
||||||
|
});
|
||||||
|
return maps?.map((map) => SafDocumentFile.fromMap(map)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<String>?> openFiles({
|
||||||
|
String? initialUri,
|
||||||
|
List<String>? mimeTypes,
|
||||||
|
multiple = true,
|
||||||
|
}) async {
|
||||||
|
final res = await pickFiles(
|
||||||
|
initialUri: initialUri,
|
||||||
|
mimeTypes: mimeTypes,
|
||||||
|
multiple: multiple,
|
||||||
|
);
|
||||||
|
return res?.map((file) => file.uri).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<SafDocumentFile>> list(String uri) async {
|
||||||
|
final maps = await methodChannel
|
||||||
|
.invokeListMethod<Map<dynamic, dynamic>>('list', {'uri': uri});
|
||||||
|
return (maps ?? []).map((map) => SafDocumentFile.fromMap(map)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SafDocumentFile?> documentFileFromUri(String uri, bool? isDir) async {
|
||||||
|
final map = await methodChannel.invokeMapMethod<String, dynamic>(
|
||||||
|
'documentFileFromUri',
|
||||||
|
{'uri': uri, 'isDir': isDir},
|
||||||
|
);
|
||||||
|
if (map == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return SafDocumentFile.fromMap(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SafDocumentFile?> stat(String uri, bool? isDir) async {
|
||||||
|
final map = await methodChannel.invokeMapMethod<String, dynamic>(
|
||||||
|
'stat',
|
||||||
|
{'uri': uri, 'isDir': isDir},
|
||||||
|
);
|
||||||
|
if (map == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return SafDocumentFile.fromMap(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> exists(String uri, bool isDir) async {
|
||||||
|
final res = await methodChannel.invokeMethod<bool>(
|
||||||
|
'exists',
|
||||||
|
{'uri': uri, 'isDir': isDir},
|
||||||
|
);
|
||||||
|
if (res == null) {
|
||||||
|
throw Exception('Failed to check if file exists: $uri');
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete(String uri, bool isDir) async {
|
||||||
|
final res = await methodChannel.invokeMethod<bool>(
|
||||||
|
'delete',
|
||||||
|
{'uri': uri, 'isDir': isDir},
|
||||||
|
);
|
||||||
|
if (res != true) {
|
||||||
|
throw Exception('Failed to delete file: $uri');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SafDocumentFile> mkdirp(String uri, List<String> names) async {
|
||||||
|
final map = await methodChannel.invokeMapMethod<String, dynamic>(
|
||||||
|
'mkdirp',
|
||||||
|
{'uri': uri, 'names': names},
|
||||||
|
);
|
||||||
|
if (map == null) {
|
||||||
|
throw Exception('Failed to create directory: $uri');
|
||||||
|
}
|
||||||
|
return SafDocumentFile.fromMap(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SafDocumentFile?> child(String uri, List<String> names) async {
|
||||||
|
final map = await methodChannel.invokeMapMethod<String, dynamic>(
|
||||||
|
'child',
|
||||||
|
{'uri': uri, 'names': names},
|
||||||
|
);
|
||||||
|
if (map == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return SafDocumentFile.fromMap(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SafDocumentFile> rename(String uri, bool isDir, String newName) async {
|
||||||
|
final map = await methodChannel.invokeMapMethod<String, dynamic>(
|
||||||
|
'rename',
|
||||||
|
{'uri': uri, 'isDir': isDir, 'newName': newName},
|
||||||
|
);
|
||||||
|
if (map == null) {
|
||||||
|
throw Exception('Failed to rename: $uri');
|
||||||
|
}
|
||||||
|
return SafDocumentFile.fromMap(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SafDocumentFile> moveTo(
|
||||||
|
String uri, bool isDir, String parentUri, String newParentUri) async {
|
||||||
|
final map = await methodChannel.invokeMapMethod<String, dynamic>(
|
||||||
|
'moveTo',
|
||||||
|
{
|
||||||
|
'uri': uri,
|
||||||
|
'isDir': isDir,
|
||||||
|
'parentUri': parentUri,
|
||||||
|
'newParentUri': newParentUri
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (map == null) {
|
||||||
|
throw Exception('Failed to move: $uri');
|
||||||
|
}
|
||||||
|
return SafDocumentFile.fromMap(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SafDocumentFile> copyTo(
|
||||||
|
String uri, bool isDir, String newParentUri) async {
|
||||||
|
final map = await methodChannel.invokeMapMethod<String, dynamic>(
|
||||||
|
'copyTo',
|
||||||
|
{'uri': uri, 'isDir': isDir, 'newParentUri': newParentUri},
|
||||||
|
);
|
||||||
|
if (map == null) {
|
||||||
|
throw Exception('Failed to copy: $uri');
|
||||||
|
}
|
||||||
|
return SafDocumentFile.fromMap(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> saveThumbnailToFile({
|
||||||
|
required String uri,
|
||||||
|
required int width,
|
||||||
|
required int height,
|
||||||
|
required String destPath,
|
||||||
|
String? format,
|
||||||
|
int? quality,
|
||||||
|
}) async {
|
||||||
|
final res = await methodChannel.invokeMethod<bool>(
|
||||||
|
'saveThumbnailToFile',
|
||||||
|
{
|
||||||
|
'uri': uri.toString(),
|
||||||
|
'width': width,
|
||||||
|
'height': height,
|
||||||
|
'destPath': destPath,
|
||||||
|
'format': format,
|
||||||
|
'quality': quality,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return res ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> getFileDescriptor(String uri) async {
|
||||||
|
final res = await methodChannel.invokeMethod<int>(
|
||||||
|
'getFileDescriptor',
|
||||||
|
{'uri': uri},
|
||||||
|
);
|
||||||
|
if (res == null) {
|
||||||
|
throw Exception('Failed to get file descriptor: $uri');
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> closeFileDescriptor(int fd) {
|
||||||
|
return methodChannel.invokeMethod<void>(
|
||||||
|
'closeFileDescriptor',
|
||||||
|
{'fd': fd},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> hasPersistedPermission(
|
||||||
|
String uri, {
|
||||||
|
bool checkRead = true,
|
||||||
|
bool checkWrite = false,
|
||||||
|
}) async {
|
||||||
|
final res = await methodChannel.invokeMethod<bool>(
|
||||||
|
'hasPersistedPermission',
|
||||||
|
{
|
||||||
|
'uri': uri,
|
||||||
|
'checkRead': checkRead,
|
||||||
|
'checkWrite': checkWrite,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res == null) {
|
||||||
|
throw Exception('Failed to check persisted permission: $uri');
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> releasePersistedPermission(String uri,
|
||||||
|
{bool read = true, bool write = false}) async {
|
||||||
|
await methodChannel.invokeMethod<void>(
|
||||||
|
'releasePersistedPermission',
|
||||||
|
{
|
||||||
|
'uri': uri,
|
||||||
|
'read': read,
|
||||||
|
'write': write,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
||||||
|
|
||||||
|
import 'saf_util_method_channel.dart';
|
||||||
|
|
||||||
|
class SafDocumentFile {
|
||||||
|
final String uri;
|
||||||
|
final String name;
|
||||||
|
final bool isDir;
|
||||||
|
final int length;
|
||||||
|
final int lastModified;
|
||||||
|
|
||||||
|
SafDocumentFile({
|
||||||
|
required this.uri,
|
||||||
|
required this.name,
|
||||||
|
required this.isDir,
|
||||||
|
required this.length,
|
||||||
|
required this.lastModified,
|
||||||
|
});
|
||||||
|
|
||||||
|
static SafDocumentFile fromMap(Map<dynamic, dynamic> map) {
|
||||||
|
return SafDocumentFile(
|
||||||
|
uri: map['uri'],
|
||||||
|
name: map['name'],
|
||||||
|
isDir: map['isDir'] ?? false,
|
||||||
|
length: map['length'] ?? 0,
|
||||||
|
lastModified: map['lastModified'] ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SafDocumentFile{uri: $uri, name: $name, isDir: $isDir, length: $length, lastModified: $lastModified}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class SafUtilPlatform extends PlatformInterface {
|
||||||
|
/// Constructs a SafUtilPlatform.
|
||||||
|
SafUtilPlatform() : super(token: _token);
|
||||||
|
|
||||||
|
static final Object _token = Object();
|
||||||
|
|
||||||
|
static SafUtilPlatform _instance = MethodChannelSafUtil();
|
||||||
|
|
||||||
|
/// The default instance of [SafUtilPlatform] to use.
|
||||||
|
///
|
||||||
|
/// Defaults to [MethodChannelSafUtil].
|
||||||
|
static SafUtilPlatform get instance => _instance;
|
||||||
|
|
||||||
|
/// Platform-specific implementations should set this with their own
|
||||||
|
/// platform-specific class that extends [SafUtilPlatform] when
|
||||||
|
/// they register themselves.
|
||||||
|
static set instance(SafUtilPlatform instance) {
|
||||||
|
PlatformInterface.verifyToken(instance, _token);
|
||||||
|
_instance = instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SafDocumentFile?> pickDirectory(
|
||||||
|
{String? initialUri,
|
||||||
|
bool? writePermission,
|
||||||
|
bool? persistablePermission}) {
|
||||||
|
throw UnimplementedError('pickDirectory() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> openDirectory(
|
||||||
|
{String? initialUri,
|
||||||
|
bool? writePermission,
|
||||||
|
bool? persistablePermission}) {
|
||||||
|
throw UnimplementedError('openDirectory() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SafDocumentFile?> pickFile({
|
||||||
|
String? initialUri,
|
||||||
|
List<String>? mimeTypes,
|
||||||
|
}) {
|
||||||
|
throw UnimplementedError('pickFile() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> openFile({
|
||||||
|
String? initialUri,
|
||||||
|
List<String>? mimeTypes,
|
||||||
|
}) {
|
||||||
|
throw UnimplementedError('openFile() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SafDocumentFile>?> pickFiles({
|
||||||
|
String? initialUri,
|
||||||
|
List<String>? mimeTypes,
|
||||||
|
multiple = true,
|
||||||
|
}) {
|
||||||
|
throw UnimplementedError('pickFiles() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>?> openFiles({
|
||||||
|
String? initialUri,
|
||||||
|
List<String>? mimeTypes,
|
||||||
|
multiple = true,
|
||||||
|
}) {
|
||||||
|
throw UnimplementedError('openFiles() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SafDocumentFile>> list(String uri) {
|
||||||
|
throw UnimplementedError('list() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SafDocumentFile?> documentFileFromUri(String uri, bool? isDir) {
|
||||||
|
throw UnimplementedError('documentFileFromUri() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SafDocumentFile?> stat(String uri, bool? isDir) {
|
||||||
|
throw UnimplementedError('stat() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> exists(String uri, bool isDir) {
|
||||||
|
throw UnimplementedError('exists() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> delete(String uri, bool isDir) {
|
||||||
|
throw UnimplementedError('delete() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SafDocumentFile> mkdirp(String uri, List<String> names) {
|
||||||
|
throw UnimplementedError('mkdirp() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SafDocumentFile?> child(String uri, List<String> names) {
|
||||||
|
throw UnimplementedError('child() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SafDocumentFile> rename(String uri, bool isDir, String newName) {
|
||||||
|
throw UnimplementedError('rename() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SafDocumentFile> moveTo(
|
||||||
|
String uri, bool isDir, String parentUri, String newParentUri) {
|
||||||
|
throw UnimplementedError('moveTo() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SafDocumentFile> copyTo(String uri, bool isDir, String newParentUri) {
|
||||||
|
throw UnimplementedError('copyTo() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> saveThumbnailToFile({
|
||||||
|
required String uri,
|
||||||
|
required int width,
|
||||||
|
required int height,
|
||||||
|
required String destPath,
|
||||||
|
String? format,
|
||||||
|
int? quality,
|
||||||
|
}) {
|
||||||
|
throw UnimplementedError('saveThumbnailToFile() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getFileDescriptor(String uri) {
|
||||||
|
throw UnimplementedError('getFileDescriptor() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> closeFileDescriptor(int fd) {
|
||||||
|
throw UnimplementedError('closeFileDescriptor() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> hasPersistedPermission(
|
||||||
|
String uri, {
|
||||||
|
bool checkRead = true,
|
||||||
|
bool checkWrite = false,
|
||||||
|
}) {
|
||||||
|
throw UnimplementedError(
|
||||||
|
'hasPersistedPermission() has not been implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> releasePersistedPermission(String uri,
|
||||||
|
{bool read = true, bool write = false}) {
|
||||||
|
throw UnimplementedError(
|
||||||
|
'releasePersistedPermission() has not been implemented.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.13.1"
|
||||||
|
boolean_selector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: boolean_selector
|
||||||
|
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
characters:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: characters
|
||||||
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
clock:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: clock
|
||||||
|
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.19.1"
|
||||||
|
fake_async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fake_async
|
||||||
|
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.3"
|
||||||
|
flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_lints:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: flutter_lints
|
||||||
|
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.0"
|
||||||
|
flutter_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
leak_tracker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker
|
||||||
|
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.0.2"
|
||||||
|
leak_tracker_flutter_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_flutter_testing
|
||||||
|
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.10"
|
||||||
|
leak_tracker_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_testing
|
||||||
|
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
|
lints:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: lints
|
||||||
|
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.0"
|
||||||
|
matcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: matcher
|
||||||
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.19"
|
||||||
|
material_color_utilities:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: material_color_utilities
|
||||||
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.13.0"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.18.0"
|
||||||
|
path:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.1"
|
||||||
|
plugin_platform_interface:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: plugin_platform_interface
|
||||||
|
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.8"
|
||||||
|
sky_engine:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
source_span:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.2"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.12.1"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.2"
|
||||||
|
test_api:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_api
|
||||||
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.11"
|
||||||
|
vector_math:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_math
|
||||||
|
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
vm_service:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vm_service
|
||||||
|
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "15.2.0"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.10.0-0 <4.0.0"
|
||||||
|
flutter: ">=3.18.0-18.0.pre.54"
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
name: saf_util
|
||||||
|
description: "Util functions for SAF (Storage Access Framework)."
|
||||||
|
version: 2.0.0
|
||||||
|
homepage: https://github.com/flutter-cavalry/saf_util
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=2.18.0 <4.0.0"
|
||||||
|
flutter: ">=2.5.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
plugin_platform_interface: ^2.0.2
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^6.0.0
|
||||||
|
|
||||||
|
# For information on the generic Dart part of this file, see the
|
||||||
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
|
# The following section is specific to Flutter packages.
|
||||||
|
flutter:
|
||||||
|
# This section identifies this Flutter project as a plugin project.
|
||||||
|
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
|
||||||
|
# which should be registered in the plugin registry. This is required for
|
||||||
|
# using method channels.
|
||||||
|
# The Android 'package' specifies package in which the registered class is.
|
||||||
|
# This is required for using method channels on Android.
|
||||||
|
# The 'ffiPlugin' specifies that native code should be built and bundled.
|
||||||
|
# This is required for using `dart:ffi`.
|
||||||
|
# All these are used by the tooling to maintain consistency when
|
||||||
|
# adding or updating assets for this project.
|
||||||
|
plugin:
|
||||||
|
platforms:
|
||||||
|
android:
|
||||||
|
package: com.fluttercavalry.saf_util
|
||||||
|
pluginClass: SafUtilPlugin
|
||||||
|
|
||||||
|
# To add assets to your plugin package, add an assets section, like this:
|
||||||
|
# assets:
|
||||||
|
# - images/a_dot_burr.jpeg
|
||||||
|
# - images/a_dot_ham.jpeg
|
||||||
|
#
|
||||||
|
# For details regarding assets in packages, see
|
||||||
|
# https://flutter.dev/to/asset-from-package
|
||||||
|
#
|
||||||
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
# https://flutter.dev/to/resolution-aware-images
|
||||||
|
|
||||||
|
# To add custom fonts to your plugin package, add a fonts section here,
|
||||||
|
# in this "flutter" section. Each entry in this list should have a
|
||||||
|
# "family" key with the font family name, and a "fonts" key with a
|
||||||
|
# list giving the asset and other descriptors for the font. For
|
||||||
|
# example:
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
||||||
|
# - family: Trajan Pro
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/TrajanPro.ttf
|
||||||
|
# - asset: fonts/TrajanPro_Bold.ttf
|
||||||
|
# weight: 700
|
||||||
|
#
|
||||||
|
# For details regarding fonts in packages, see
|
||||||
|
# https://flutter.dev/to/font-from-package
|
||||||
+3
-4
@@ -984,10 +984,9 @@ packages:
|
|||||||
saf_util:
|
saf_util:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: saf_util
|
path: "packages/saf_util"
|
||||||
sha256: "0cf5fc462fc7e466e1d5608dd5a331d88d6dd6c7b01f62a305aaf469bf3309ea"
|
relative: true
|
||||||
url: "https://pub.dev"
|
source: path
|
||||||
source: hosted
|
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
screen_retriever:
|
screen_retriever:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
|
|||||||
+4
-1
@@ -50,7 +50,10 @@ dependencies:
|
|||||||
crypto: ^3.0.7
|
crypto: ^3.0.7
|
||||||
cryptography: ^2.9.0
|
cryptography: ^2.9.0
|
||||||
file_picker: ^10.3.10
|
file_picker: ^10.3.10
|
||||||
saf_util: ^2.0.0
|
saf_util:
|
||||||
|
# Vendored 2.0.0 with a pendingResult lifecycle fix (see its CHANGELOG) —
|
||||||
|
# upstream double-replies MethodChannel results and crashes the activity.
|
||||||
|
path: packages/saf_util
|
||||||
saf_stream: ^2.0.0
|
saf_stream: ^2.0.0
|
||||||
material_symbols_icons: ^4.2906.0
|
material_symbols_icons: ^4.2906.0
|
||||||
web_socket_channel: ^3.0.1
|
web_socket_channel: ^3.0.1
|
||||||
|
|||||||
Reference in New Issue
Block a user