fix(deps): refresh and document native dependencies
This commit is contained in:
@@ -1,10 +1,28 @@
|
||||
## 2.0.0 (plezy vendored patch)
|
||||
## 3.1.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.
|
||||
Vendored from `flutter-cavalry/saf_util` at
|
||||
`e300a03ea34b49414b42f309e02531ece57cd0d1`. The local Result-lifecycle patch
|
||||
take-and-clears picker replies, ignores unrelated request codes, reattaches the
|
||||
listener across configuration changes, and clears state after launch failures.
|
||||
When updating, reapply those behaviors to every picker request code and run the
|
||||
Android plugin tests.
|
||||
|
||||
## 3.1.0
|
||||
|
||||
- Set `length` to -1 when the length is unknown, matching Android `DocumentFile.length()`.
|
||||
|
||||
## 3.0.0
|
||||
|
||||
- **Breaking**: Migrate to AGP 9.
|
||||
- **Breaking**: Minimum Flutter version is 3.44.0.
|
||||
|
||||
## 2.2.0
|
||||
|
||||
- Add `pickMedia`.
|
||||
|
||||
## 2.1.0
|
||||
|
||||
- Add the `throws` parameter to `stat`.
|
||||
|
||||
## 2.0.0
|
||||
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
include: package:mgenware_dart_lints/flutter_lints.yaml
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.cxx
|
||||
@@ -2,14 +2,14 @@ group = "com.fluttercavalry.saf_util"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
val kotlinVersion = "2.2.20"
|
||||
val kotlinVersion = "2.3.20"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
classpath("com.android.tools.build:gradle:9.0.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ allprojects {
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -36,10 +35,6 @@ android {
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") {
|
||||
java.srcDirs("src/main/kotlin")
|
||||
@@ -71,6 +66,12 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.documentfile:documentfile:1.1.0")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
|
||||
@@ -1 +1 @@
|
||||
rootProject.name = 'saf_util'
|
||||
rootProject.name = "saf_util"
|
||||
|
||||
+293
-179
@@ -12,8 +12,9 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.net.toUri
|
||||
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
|
||||
@@ -22,19 +23,21 @@ import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
import io.flutter.plugin.common.PluginRegistry
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
|
||||
/** 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
|
||||
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
|
||||
@@ -44,20 +47,19 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
private var pendingArguments: PendingArguments? = null
|
||||
private val requestCodeOpenDocumentTree = 1001
|
||||
private val requestCodeOpenFiles = 1002
|
||||
private val requestCodePickMedia = 1003
|
||||
private val activityResultListener = PluginRegistry.ActivityResultListener { requestCode, resultCode, data ->
|
||||
onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
private val fdMap = mutableMapOf<Int, ParcelFileDescriptor>()
|
||||
|
||||
/// 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).
|
||||
/** Takes ownership before replying so a Result can never be answered twice. */
|
||||
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")
|
||||
@@ -94,7 +96,10 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
activity = null
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||
override fun onMethodCall(
|
||||
call: MethodCall,
|
||||
result: Result
|
||||
) {
|
||||
when (call.method) {
|
||||
"list" -> {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
@@ -105,25 +110,27 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
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 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
|
||||
)
|
||||
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)
|
||||
@@ -137,13 +144,14 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
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,
|
||||
)
|
||||
val fileInfo =
|
||||
fileObjMap(
|
||||
documentUri,
|
||||
isDirectory,
|
||||
fileName,
|
||||
fileSize,
|
||||
lastModified
|
||||
)
|
||||
results.add(fileInfo)
|
||||
}
|
||||
|
||||
@@ -191,9 +199,22 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
try {
|
||||
val uri = call.argument<String>("uri") as String
|
||||
val isDir = call.argument<Boolean>("isDir")
|
||||
val throws = call.argument<Boolean>("throws") ?: false
|
||||
|
||||
val df = documentFileFromUri(uri, isDir)
|
||||
if (df == null || !df.exists()) {
|
||||
if (df == null) {
|
||||
if (throws) {
|
||||
throw Exception("Failed to get DocumentFile from $uri")
|
||||
}
|
||||
launch(Dispatchers.Main) {
|
||||
result.success(null)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
if (!df.exists()) {
|
||||
if (throws) {
|
||||
throw Exception("DocumentFile at $uri does not exist")
|
||||
}
|
||||
launch(Dispatchers.Main) {
|
||||
result.success(null)
|
||||
}
|
||||
@@ -258,7 +279,8 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
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 fd =
|
||||
context.contentResolver.openFileDescriptor(df.uri, "r") ?: throw Exception("Failed to open file descriptor")
|
||||
val fdInt = fd.fd
|
||||
fdMap[fdInt] = fd
|
||||
|
||||
@@ -308,11 +330,12 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
// 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
|
||||
}
|
||||
nextDocument =
|
||||
if (findRes2 != null) {
|
||||
documentFileFromUriObj(findRes2.uri, findRes2.isDir)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
nextDocument = createRes
|
||||
}
|
||||
@@ -355,7 +378,9 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
curDocument = documentFileFromUriObj(findRes.uri, findRes.isDir) ?: throw Exception("Failed to get DocumentFile at $curName")
|
||||
curDocument =
|
||||
documentFileFromUriObj(findRes.uri, findRes.isDir)
|
||||
?: throw Exception("Failed to get DocumentFile at $curName")
|
||||
}
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
@@ -386,10 +411,12 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
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")
|
||||
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))
|
||||
}
|
||||
@@ -414,12 +441,13 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
val parentUriObj = parentUri.toUri()
|
||||
val newParentUriObj = newParentUri.toUri()
|
||||
|
||||
val resUri = DocumentsContract.moveDocument(
|
||||
context.contentResolver,
|
||||
uriObj,
|
||||
parentUriObj,
|
||||
newParentUriObj
|
||||
) ?: throw Exception("Failed to move document")
|
||||
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) {
|
||||
@@ -443,11 +471,12 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
val uriObj = uri.toUri()
|
||||
val newParentUriObj = newParentUri.toUri()
|
||||
|
||||
val resUri = DocumentsContract.copyDocument(
|
||||
context.contentResolver,
|
||||
uriObj,
|
||||
newParentUriObj
|
||||
) ?: throw Exception("Failed to move document")
|
||||
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")
|
||||
|
||||
@@ -492,14 +521,15 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
)
|
||||
}
|
||||
intent.addFlags(
|
||||
if (writePermission) Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
else Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
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)
|
||||
}
|
||||
@@ -508,7 +538,7 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
"pickFiles" -> {
|
||||
try {
|
||||
val initialUri = call.argument<String>("initialUri")
|
||||
val multiple = call.argument<Boolean>("multiple") ?: false
|
||||
val multiple = call.argument<Boolean>("multiple") ?: true
|
||||
val mimeTypes = call.argument<ArrayList<String>>("mimeTypes") ?: arrayListOf()
|
||||
|
||||
if (activity == null) {
|
||||
@@ -544,8 +574,53 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
"pickMedia" -> {
|
||||
try {
|
||||
val multiple = call.argument<Boolean>("multiple") ?: false
|
||||
val mode = call.argument<String>("mode") ?: "all"
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
result.error("NOT_SUPPORTED", "Photo Picker is only supported on Android 13 (API 33) and above", null)
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
val normalizedMode = mode.lowercase()
|
||||
if (normalizedMode != "photo" && normalizedMode != "video" && normalizedMode != "all") {
|
||||
result.error("INVALID_ARGUMENT", "mode must be one of: photo, video, all", null)
|
||||
return
|
||||
}
|
||||
|
||||
pendingResult = result
|
||||
pendingArguments = PendingMediaArguments(multiple)
|
||||
val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
|
||||
intent.type =
|
||||
when (normalizedMode) {
|
||||
"photo" -> "image/*"
|
||||
"video" -> "video/*"
|
||||
else -> "*/*"
|
||||
}
|
||||
if (multiple) {
|
||||
intent.putExtra(
|
||||
MediaStore.EXTRA_PICK_IMAGES_MAX,
|
||||
MediaStore.getPickImagesMaxLimit()
|
||||
)
|
||||
}
|
||||
|
||||
activity?.startActivityForResult(intent, requestCodePickMedia)
|
||||
} catch (err: Exception) {
|
||||
takePendingResult()
|
||||
result.error("PluginError", err.message, null)
|
||||
}
|
||||
@@ -558,12 +633,13 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
val checkRead = call.argument<Boolean>("checkRead") ?: true
|
||||
val checkWrite = call.argument<Boolean>("checkWrite") ?: false
|
||||
|
||||
val persisted = hasPersistedUriPermission(
|
||||
context,
|
||||
uri.toUri(),
|
||||
checkRead,
|
||||
checkWrite
|
||||
)
|
||||
val persisted =
|
||||
hasPersistedUriPermission(
|
||||
context,
|
||||
uri.toUri(),
|
||||
checkRead,
|
||||
checkWrite
|
||||
)
|
||||
launch(Dispatchers.Main) {
|
||||
result.success(persisted)
|
||||
}
|
||||
@@ -584,10 +660,15 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
|
||||
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
|
||||
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)
|
||||
@@ -626,26 +707,28 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
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
|
||||
}
|
||||
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
|
||||
)
|
||||
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,
|
||||
quality
|
||||
)
|
||||
launch(Dispatchers.Main) {
|
||||
result.success(true)
|
||||
@@ -663,66 +746,69 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
}
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
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) {
|
||||
// Handle folder/file/media picker results; unrelated request codes are not ours.
|
||||
private fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
data: Intent?
|
||||
): Boolean {
|
||||
if (
|
||||
requestCode != requestCodeOpenDocumentTree &&
|
||||
requestCode != requestCodeOpenFiles &&
|
||||
requestCode != requestCodePickMedia
|
||||
) {
|
||||
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
|
||||
)
|
||||
}
|
||||
if (uri != null && args is PendingDirArguments && 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
|
||||
)
|
||||
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)
|
||||
val allowMultiple =
|
||||
if (requestCode == requestCodePickMedia) {
|
||||
(args as? PendingMediaArguments)?.multiple ?: false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
val uris: List<Uri> =
|
||||
if (allowMultiple && data.clipData != null) {
|
||||
val clipData = data.clipData!!
|
||||
List(clipData.itemCount) { index -> clipData.getItemAt(index).uri }
|
||||
} else {
|
||||
listOf(data.data!!)
|
||||
}
|
||||
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))
|
||||
}
|
||||
if (df != null) documentFileMaps.add(documentFileToMap(df))
|
||||
}
|
||||
|
||||
result.success(documentFileMaps) // Return the URIs to Flutter
|
||||
result.success(documentFileMaps)
|
||||
} else {
|
||||
result.success(null)
|
||||
}
|
||||
@@ -731,8 +817,7 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
try {
|
||||
result.error("PluginError", err.message, null)
|
||||
} catch (_: IllegalStateException) {
|
||||
// Reply already submitted — never crash the host activity over a
|
||||
// picker teardown race.
|
||||
// A duplicate native delivery must not crash the host Activity.
|
||||
}
|
||||
}
|
||||
return true
|
||||
@@ -742,19 +827,26 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
channel.setMethodCallHandler(null)
|
||||
}
|
||||
|
||||
private fun documentFileFromUri(uri: String, isDir: Boolean?): DocumentFile? {
|
||||
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)
|
||||
}
|
||||
private fun documentFileFromUriObj(
|
||||
uriObj: Uri,
|
||||
isDir: Boolean
|
||||
): DocumentFile? {
|
||||
val res =
|
||||
if (isDir) {
|
||||
DocumentFile.fromTreeUri(context, uriObj)
|
||||
} else {
|
||||
DocumentFile.fromSingleUri(context, uriObj)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -775,31 +867,39 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
return false
|
||||
}
|
||||
|
||||
private fun areSameDocumentLocation(treeUri: Uri, docUri: Uri): Boolean {
|
||||
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? {
|
||||
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
|
||||
)
|
||||
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)
|
||||
@@ -821,45 +921,51 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
}
|
||||
}
|
||||
|
||||
private fun renameFileDocumentFile(df: DocumentFile, newName: String): Uri? {
|
||||
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
|
||||
)
|
||||
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 documentFileToMap(file: DocumentFile): Map<String, Any?> = 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,
|
||||
)
|
||||
}
|
||||
lastMod: Long
|
||||
): Map<String, Any?> = 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) {
|
||||
private fun File.writeBitmap(
|
||||
bitmap: Bitmap,
|
||||
format: Bitmap.CompressFormat,
|
||||
quality: Int
|
||||
) {
|
||||
outputStream().use { out ->
|
||||
bitmap.compress(format, quality, out)
|
||||
out.flush()
|
||||
@@ -867,11 +973,19 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
}
|
||||
}
|
||||
|
||||
internal data class UriInfo(val uri: Uri, val name: String, val isDir: Boolean)
|
||||
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()
|
||||
val persistablePermission: Boolean
|
||||
) : PendingArguments()
|
||||
|
||||
internal class PendingMediaArguments(
|
||||
val multiple: Boolean
|
||||
) : PendingArguments()
|
||||
|
||||
+96
-70
@@ -6,84 +6,110 @@ 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.PluginRegistry
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.verifyNoInteractions
|
||||
import org.mockito.Mockito.verifyNoMoreInteractions
|
||||
import org.mockito.Mockito.`when`
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.Test
|
||||
|
||||
internal class SafUtilPluginTest {
|
||||
@Test
|
||||
fun onMethodCall_unknownMethod_returnsNotImplemented() {
|
||||
val plugin = SafUtilPlugin()
|
||||
val result = mock(MethodChannel.Result::class.java)
|
||||
@Test
|
||||
fun onMethodCall_unknownMethod_returnsNotImplemented() {
|
||||
val plugin = SafUtilPlugin()
|
||||
val result = mock(MethodChannel.Result::class.java)
|
||||
|
||||
plugin.onMethodCall(MethodCall("unknown", null), result)
|
||||
plugin.onMethodCall(MethodCall("unknown", null), result)
|
||||
|
||||
verify(result).notImplemented()
|
||||
verifyNoMoreInteractions(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pickDirectory_withoutActivity_returnsNoActivityError() {
|
||||
val plugin = SafUtilPlugin()
|
||||
val result = mock(MethodChannel.Result::class.java)
|
||||
|
||||
plugin.onMethodCall(MethodCall("pickDirectory", null), result)
|
||||
|
||||
verify(result).error("NO_ACTIVITY", "Activity is null", null)
|
||||
verifyNoMoreInteractions(result)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Test
|
||||
fun pickDirectory_afterConfigChange_reattachesListenerAndClearsPendingResult() {
|
||||
val plugin = SafUtilPlugin()
|
||||
val firstActivity = RecordingActivity()
|
||||
val firstBinding = mock(ActivityPluginBinding::class.java)
|
||||
`when`(firstBinding.activity).thenReturn(firstActivity)
|
||||
|
||||
plugin.onAttachedToActivity(firstBinding)
|
||||
|
||||
val firstListenerCaptor = ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener::class.java)
|
||||
verify(firstBinding).addActivityResultListener(firstListenerCaptor.capture())
|
||||
|
||||
val firstResult = mock(MethodChannel.Result::class.java)
|
||||
plugin.onMethodCall(MethodCall("pickDirectory", null), firstResult)
|
||||
assertEquals(listOf(1001), firstActivity.startedRequestCodes)
|
||||
|
||||
val secondResult = mock(MethodChannel.Result::class.java)
|
||||
plugin.onMethodCall(MethodCall("pickDirectory", null), secondResult)
|
||||
verify(secondResult).error("ALREADY_PICKING", "Another picker process is already in progress", null)
|
||||
|
||||
plugin.onDetachedFromActivityForConfigChanges()
|
||||
verify(firstBinding).removeActivityResultListener(firstListenerCaptor.value)
|
||||
|
||||
val secondActivity = RecordingActivity()
|
||||
val secondBinding = mock(ActivityPluginBinding::class.java)
|
||||
`when`(secondBinding.activity).thenReturn(secondActivity)
|
||||
|
||||
plugin.onReattachedToActivityForConfigChanges(secondBinding)
|
||||
|
||||
val secondListenerCaptor = ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener::class.java)
|
||||
verify(secondBinding).addActivityResultListener(secondListenerCaptor.capture())
|
||||
|
||||
secondListenerCaptor.value.onActivityResult(1001, Activity.RESULT_CANCELED, null)
|
||||
verify(firstResult).success(null)
|
||||
|
||||
val thirdResult = mock(MethodChannel.Result::class.java)
|
||||
plugin.onMethodCall(MethodCall("pickDirectory", null), thirdResult)
|
||||
assertEquals(listOf(1001), secondActivity.startedRequestCodes)
|
||||
}
|
||||
|
||||
private class RecordingActivity : Activity() {
|
||||
val startedRequestCodes = mutableListOf<Int>()
|
||||
|
||||
@Deprecated("Deprecated in Android")
|
||||
override fun startActivityForResult(intent: Intent?, requestCode: Int) {
|
||||
startedRequestCodes.add(requestCode)
|
||||
}
|
||||
verify(result).notImplemented()
|
||||
verifyNoMoreInteractions(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pickDirectory_withoutActivity_returnsNoActivityError() {
|
||||
val plugin = SafUtilPlugin()
|
||||
val result = mock(MethodChannel.Result::class.java)
|
||||
|
||||
plugin.onMethodCall(MethodCall("pickDirectory", null), result)
|
||||
|
||||
verify(result).error("NO_ACTIVITY", "Activity is null", null)
|
||||
verifyNoMoreInteractions(result)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Test
|
||||
fun unrelatedActivityResultDoesNotConsumeOrAnswerPendingPicker() {
|
||||
val plugin = SafUtilPlugin()
|
||||
val activity = RecordingActivity()
|
||||
val binding = mock(ActivityPluginBinding::class.java)
|
||||
`when`(binding.activity).thenReturn(activity)
|
||||
plugin.onAttachedToActivity(binding)
|
||||
|
||||
val listenerCaptor = ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener::class.java)
|
||||
verify(binding).addActivityResultListener(listenerCaptor.capture())
|
||||
val result = mock(MethodChannel.Result::class.java)
|
||||
plugin.onMethodCall(MethodCall("pickDirectory", null), result)
|
||||
|
||||
assertFalse(listenerCaptor.value.onActivityResult(9999, Activity.RESULT_CANCELED, null))
|
||||
verifyNoInteractions(result)
|
||||
assertTrue(listenerCaptor.value.onActivityResult(1001, Activity.RESULT_CANCELED, null))
|
||||
verify(result).success(null)
|
||||
|
||||
assertTrue(listenerCaptor.value.onActivityResult(1001, Activity.RESULT_CANCELED, null))
|
||||
verifyNoMoreInteractions(result)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Test
|
||||
fun pickDirectory_afterConfigChange_reattachesListenerAndClearsPendingResult() {
|
||||
val plugin = SafUtilPlugin()
|
||||
val firstActivity = RecordingActivity()
|
||||
val firstBinding = mock(ActivityPluginBinding::class.java)
|
||||
`when`(firstBinding.activity).thenReturn(firstActivity)
|
||||
|
||||
plugin.onAttachedToActivity(firstBinding)
|
||||
|
||||
val firstListenerCaptor = ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener::class.java)
|
||||
verify(firstBinding).addActivityResultListener(firstListenerCaptor.capture())
|
||||
|
||||
val firstResult = mock(MethodChannel.Result::class.java)
|
||||
plugin.onMethodCall(MethodCall("pickDirectory", null), firstResult)
|
||||
assertEquals(listOf(1001), firstActivity.startedRequestCodes)
|
||||
|
||||
val secondResult = mock(MethodChannel.Result::class.java)
|
||||
plugin.onMethodCall(MethodCall("pickDirectory", null), secondResult)
|
||||
verify(secondResult).error("ALREADY_PICKING", "Another picker process is already in progress", null)
|
||||
|
||||
plugin.onDetachedFromActivityForConfigChanges()
|
||||
verify(firstBinding).removeActivityResultListener(firstListenerCaptor.value)
|
||||
|
||||
val secondActivity = RecordingActivity()
|
||||
val secondBinding = mock(ActivityPluginBinding::class.java)
|
||||
`when`(secondBinding.activity).thenReturn(secondActivity)
|
||||
|
||||
plugin.onReattachedToActivityForConfigChanges(secondBinding)
|
||||
|
||||
val secondListenerCaptor = ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener::class.java)
|
||||
verify(secondBinding).addActivityResultListener(secondListenerCaptor.capture())
|
||||
|
||||
secondListenerCaptor.value.onActivityResult(1001, Activity.RESULT_CANCELED, null)
|
||||
verify(firstResult).success(null)
|
||||
|
||||
val thirdResult = mock(MethodChannel.Result::class.java)
|
||||
plugin.onMethodCall(MethodCall("pickDirectory", null), thirdResult)
|
||||
assertEquals(listOf(1001), secondActivity.startedRequestCodes)
|
||||
}
|
||||
|
||||
private class RecordingActivity : Activity() {
|
||||
val startedRequestCodes = mutableListOf<Int>()
|
||||
|
||||
@Deprecated("Deprecated in Android")
|
||||
override fun startActivityForResult(intent: Intent?, requestCode: Int) {
|
||||
startedRequestCodes.add(requestCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,16 @@ class SafUtil {
|
||||
/// [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}) {
|
||||
Future<SafDocumentFile?> pickDirectory({
|
||||
String? initialUri,
|
||||
bool? writePermission,
|
||||
bool? persistablePermission,
|
||||
}) {
|
||||
return SafUtilPlatform.instance.pickDirectory(
|
||||
initialUri: initialUri,
|
||||
writePermission: writePermission,
|
||||
persistablePermission: persistablePermission);
|
||||
initialUri: initialUri,
|
||||
writePermission: writePermission,
|
||||
persistablePermission: persistablePermission,
|
||||
);
|
||||
}
|
||||
|
||||
/// Shows a file picker dialog and returns the selected file [SafDocumentFile].
|
||||
@@ -39,7 +41,7 @@ class SafUtil {
|
||||
Future<List<SafDocumentFile>?> pickFiles({
|
||||
String? initialUri,
|
||||
List<String>? mimeTypes,
|
||||
multiple = true,
|
||||
bool multiple = true,
|
||||
}) {
|
||||
return SafUtilPlatform.instance.pickFiles(
|
||||
initialUri: initialUri,
|
||||
@@ -48,6 +50,20 @@ class SafUtil {
|
||||
);
|
||||
}
|
||||
|
||||
/// Shows a media picker dialog and returns selected media as [SafDocumentFile].
|
||||
///
|
||||
/// [multiple] is true if multiple media files can be selected.
|
||||
/// [mode] controls what can be picked and must be one of:
|
||||
/// - 'photo'
|
||||
/// - 'video'
|
||||
/// - 'all'
|
||||
Future<List<SafDocumentFile>?> pickMedia({
|
||||
bool multiple = true,
|
||||
String mode = 'all',
|
||||
}) {
|
||||
return SafUtilPlatform.instance.pickMedia(multiple: multiple, mode: mode);
|
||||
}
|
||||
|
||||
/// Lists the contents of the specified directory URI.
|
||||
/// Returns a list of [SafDocumentFile] objects.
|
||||
///
|
||||
@@ -72,8 +88,9 @@ class SafUtil {
|
||||
/// [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);
|
||||
/// [throws] when true, throws an exception if the URI does not exist or is inaccessible.
|
||||
Future<SafDocumentFile?> stat(String uri, bool? isDir, {bool? throws}) {
|
||||
return SafUtilPlatform.instance.stat(uri, isDir, throws: throws);
|
||||
}
|
||||
|
||||
/// Checks if the specified file or directory exists.
|
||||
@@ -126,7 +143,11 @@ class SafUtil {
|
||||
/// [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) {
|
||||
String uri,
|
||||
bool isDir,
|
||||
String parentUri,
|
||||
String newParentUri,
|
||||
) {
|
||||
return SafUtilPlatform.instance.moveTo(uri, isDir, parentUri, newParentUri);
|
||||
}
|
||||
|
||||
@@ -178,14 +199,16 @@ class SafUtil {
|
||||
/// [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}) {
|
||||
Future<String?> openDirectory({
|
||||
String? initialUri,
|
||||
bool? writePermission,
|
||||
bool? persistablePermission,
|
||||
}) {
|
||||
return SafUtilPlatform.instance.openDirectory(
|
||||
initialUri: initialUri,
|
||||
writePermission: writePermission,
|
||||
persistablePermission: persistablePermission);
|
||||
initialUri: initialUri,
|
||||
writePermission: writePermission,
|
||||
persistablePermission: persistablePermission,
|
||||
);
|
||||
}
|
||||
|
||||
/// Shows a file picker dialog and returns the selected file URI.
|
||||
@@ -194,10 +217,7 @@ class SafUtil {
|
||||
/// [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,
|
||||
}) {
|
||||
Future<String?> openFile({String? initialUri, List<String>? mimeTypes}) {
|
||||
return SafUtilPlatform.instance.openFile(
|
||||
initialUri: initialUri,
|
||||
mimeTypes: mimeTypes,
|
||||
@@ -212,7 +232,7 @@ class SafUtil {
|
||||
Future<List<String>?> openFiles({
|
||||
String? initialUri,
|
||||
List<String>? mimeTypes,
|
||||
multiple = true,
|
||||
bool multiple = true,
|
||||
}) {
|
||||
return SafUtilPlatform.instance.openFiles(
|
||||
initialUri: initialUri,
|
||||
@@ -240,17 +260,26 @@ class SafUtil {
|
||||
bool checkRead = true,
|
||||
bool checkWrite = false,
|
||||
}) {
|
||||
return SafUtilPlatform.instance.hasPersistedPermission(uri,
|
||||
checkRead: checkRead, checkWrite: checkWrite);
|
||||
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);
|
||||
Future<void> releasePersistedPermission(
|
||||
String uri, {
|
||||
bool read = true,
|
||||
bool write = false,
|
||||
}) async {
|
||||
return SafUtilPlatform.instance.releasePersistedPermission(
|
||||
uri,
|
||||
read: read,
|
||||
write: write,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,16 +10,17 @@ class MethodChannelSafUtil extends SafUtilPlatform {
|
||||
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,
|
||||
});
|
||||
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;
|
||||
}
|
||||
@@ -27,10 +28,11 @@ class MethodChannelSafUtil extends SafUtilPlatform {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> openDirectory(
|
||||
{String? initialUri,
|
||||
bool? writePermission,
|
||||
bool? persistablePermission}) async {
|
||||
Future<String?> openDirectory({
|
||||
String? initialUri,
|
||||
bool? writePermission,
|
||||
bool? persistablePermission,
|
||||
}) async {
|
||||
final res = await pickDirectory(
|
||||
initialUri: initialUri,
|
||||
writePermission: writePermission,
|
||||
@@ -57,10 +59,7 @@ class MethodChannelSafUtil extends SafUtilPlatform {
|
||||
String? initialUri,
|
||||
List<String>? mimeTypes,
|
||||
}) async {
|
||||
final res = await pickFile(
|
||||
initialUri: initialUri,
|
||||
mimeTypes: mimeTypes,
|
||||
);
|
||||
final res = await pickFile(initialUri: initialUri, mimeTypes: mimeTypes);
|
||||
return res?.uri;
|
||||
}
|
||||
|
||||
@@ -68,14 +67,24 @@ class MethodChannelSafUtil extends SafUtilPlatform {
|
||||
Future<List<SafDocumentFile>?> pickFiles({
|
||||
String? initialUri,
|
||||
List<String>? mimeTypes,
|
||||
multiple = true,
|
||||
bool multiple = true,
|
||||
}) async {
|
||||
final maps = await methodChannel
|
||||
.invokeListMethod<Map<dynamic, dynamic>>('pickFiles', {
|
||||
'initialUri': initialUri,
|
||||
'mimeTypes': mimeTypes,
|
||||
'multiple': multiple,
|
||||
});
|
||||
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<SafDocumentFile>?> pickMedia({
|
||||
bool multiple = true,
|
||||
String mode = 'all',
|
||||
}) async {
|
||||
final maps = await methodChannel.invokeListMethod<Map<dynamic, dynamic>>(
|
||||
'pickMedia',
|
||||
{'multiple': multiple, 'mode': mode},
|
||||
);
|
||||
return maps?.map((map) => SafDocumentFile.fromMap(map)).toList();
|
||||
}
|
||||
|
||||
@@ -83,7 +92,7 @@ class MethodChannelSafUtil extends SafUtilPlatform {
|
||||
Future<List<String>?> openFiles({
|
||||
String? initialUri,
|
||||
List<String>? mimeTypes,
|
||||
multiple = true,
|
||||
bool multiple = true,
|
||||
}) async {
|
||||
final res = await pickFiles(
|
||||
initialUri: initialUri,
|
||||
@@ -95,8 +104,10 @@ class MethodChannelSafUtil extends SafUtilPlatform {
|
||||
|
||||
@override
|
||||
Future<List<SafDocumentFile>> list(String uri) async {
|
||||
final maps = await methodChannel
|
||||
.invokeListMethod<Map<dynamic, dynamic>>('list', {'uri': uri});
|
||||
final maps = await methodChannel.invokeListMethod<Map<dynamic, dynamic>>(
|
||||
'list',
|
||||
{'uri': uri},
|
||||
);
|
||||
return (maps ?? []).map((map) => SafDocumentFile.fromMap(map)).toList();
|
||||
}
|
||||
|
||||
@@ -113,11 +124,12 @@ class MethodChannelSafUtil extends SafUtilPlatform {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SafDocumentFile?> stat(String uri, bool? isDir) async {
|
||||
final map = await methodChannel.invokeMapMethod<String, dynamic>(
|
||||
'stat',
|
||||
{'uri': uri, 'isDir': isDir},
|
||||
);
|
||||
Future<SafDocumentFile?> stat(String uri, bool? isDir, {bool? throws}) async {
|
||||
final map = await methodChannel.invokeMapMethod<String, dynamic>('stat', {
|
||||
'uri': uri,
|
||||
'isDir': isDir,
|
||||
'throws': throws,
|
||||
});
|
||||
if (map == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -126,10 +138,10 @@ class MethodChannelSafUtil extends SafUtilPlatform {
|
||||
|
||||
@override
|
||||
Future<bool> exists(String uri, bool isDir) async {
|
||||
final res = await methodChannel.invokeMethod<bool>(
|
||||
'exists',
|
||||
{'uri': uri, 'isDir': isDir},
|
||||
);
|
||||
final res = await methodChannel.invokeMethod<bool>('exists', {
|
||||
'uri': uri,
|
||||
'isDir': isDir,
|
||||
});
|
||||
if (res == null) {
|
||||
throw Exception('Failed to check if file exists: $uri');
|
||||
}
|
||||
@@ -138,10 +150,10 @@ class MethodChannelSafUtil extends SafUtilPlatform {
|
||||
|
||||
@override
|
||||
Future<void> delete(String uri, bool isDir) async {
|
||||
final res = await methodChannel.invokeMethod<bool>(
|
||||
'delete',
|
||||
{'uri': uri, 'isDir': isDir},
|
||||
);
|
||||
final res = await methodChannel.invokeMethod<bool>('delete', {
|
||||
'uri': uri,
|
||||
'isDir': isDir,
|
||||
});
|
||||
if (res != true) {
|
||||
throw Exception('Failed to delete file: $uri');
|
||||
}
|
||||
@@ -149,10 +161,10 @@ class MethodChannelSafUtil extends SafUtilPlatform {
|
||||
|
||||
@override
|
||||
Future<SafDocumentFile> mkdirp(String uri, List<String> names) async {
|
||||
final map = await methodChannel.invokeMapMethod<String, dynamic>(
|
||||
'mkdirp',
|
||||
{'uri': uri, 'names': names},
|
||||
);
|
||||
final map = await methodChannel.invokeMapMethod<String, dynamic>('mkdirp', {
|
||||
'uri': uri,
|
||||
'names': names,
|
||||
});
|
||||
if (map == null) {
|
||||
throw Exception('Failed to create directory: $uri');
|
||||
}
|
||||
@@ -161,10 +173,10 @@ class MethodChannelSafUtil extends SafUtilPlatform {
|
||||
|
||||
@override
|
||||
Future<SafDocumentFile?> child(String uri, List<String> names) async {
|
||||
final map = await methodChannel.invokeMapMethod<String, dynamic>(
|
||||
'child',
|
||||
{'uri': uri, 'names': names},
|
||||
);
|
||||
final map = await methodChannel.invokeMapMethod<String, dynamic>('child', {
|
||||
'uri': uri,
|
||||
'names': names,
|
||||
});
|
||||
if (map == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -173,10 +185,11 @@ class MethodChannelSafUtil extends SafUtilPlatform {
|
||||
|
||||
@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},
|
||||
);
|
||||
final map = await methodChannel.invokeMapMethod<String, dynamic>('rename', {
|
||||
'uri': uri,
|
||||
'isDir': isDir,
|
||||
'newName': newName,
|
||||
});
|
||||
if (map == null) {
|
||||
throw Exception('Failed to rename: $uri');
|
||||
}
|
||||
@@ -185,16 +198,17 @@ class MethodChannelSafUtil extends SafUtilPlatform {
|
||||
|
||||
@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
|
||||
},
|
||||
);
|
||||
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');
|
||||
}
|
||||
@@ -203,11 +217,15 @@ class MethodChannelSafUtil extends SafUtilPlatform {
|
||||
|
||||
@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},
|
||||
);
|
||||
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');
|
||||
}
|
||||
@@ -223,26 +241,22 @@ class MethodChannelSafUtil extends SafUtilPlatform {
|
||||
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,
|
||||
},
|
||||
);
|
||||
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},
|
||||
);
|
||||
final res = await methodChannel.invokeMethod<int>('getFileDescriptor', {
|
||||
'uri': uri,
|
||||
});
|
||||
if (res == null) {
|
||||
throw Exception('Failed to get file descriptor: $uri');
|
||||
}
|
||||
@@ -251,10 +265,7 @@ class MethodChannelSafUtil extends SafUtilPlatform {
|
||||
|
||||
@override
|
||||
Future<void> closeFileDescriptor(int fd) {
|
||||
return methodChannel.invokeMethod<void>(
|
||||
'closeFileDescriptor',
|
||||
{'fd': fd},
|
||||
);
|
||||
return methodChannel.invokeMethod<void>('closeFileDescriptor', {'fd': fd});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -265,11 +276,7 @@ class MethodChannelSafUtil extends SafUtilPlatform {
|
||||
}) async {
|
||||
final res = await methodChannel.invokeMethod<bool>(
|
||||
'hasPersistedPermission',
|
||||
{
|
||||
'uri': uri,
|
||||
'checkRead': checkRead,
|
||||
'checkWrite': checkWrite,
|
||||
},
|
||||
{'uri': uri, 'checkRead': checkRead, 'checkWrite': checkWrite},
|
||||
);
|
||||
if (res == null) {
|
||||
throw Exception('Failed to check persisted permission: $uri');
|
||||
@@ -278,15 +285,15 @@ class MethodChannelSafUtil extends SafUtilPlatform {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> releasePersistedPermission(String uri,
|
||||
{bool read = true, bool write = false}) async {
|
||||
await methodChannel.invokeMethod<void>(
|
||||
'releasePersistedPermission',
|
||||
{
|
||||
'uri': uri,
|
||||
'read': read,
|
||||
'write': write,
|
||||
},
|
||||
);
|
||||
Future<void> releasePersistedPermission(
|
||||
String uri, {
|
||||
bool read = true,
|
||||
bool write = false,
|
||||
}) async {
|
||||
await methodChannel.invokeMethod<void>('releasePersistedPermission', {
|
||||
'uri': uri,
|
||||
'read': read,
|
||||
'write': write,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ class SafDocumentFile {
|
||||
|
||||
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,
|
||||
uri: map['uri'] as String,
|
||||
name: map['name'] as String,
|
||||
isDir: map['isDir'] as bool? ?? false,
|
||||
length: map['length'] as int? ?? -1,
|
||||
lastModified: map['lastModified'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,17 +54,19 @@ abstract class SafUtilPlatform extends PlatformInterface {
|
||||
_instance = instance;
|
||||
}
|
||||
|
||||
Future<SafDocumentFile?> pickDirectory(
|
||||
{String? initialUri,
|
||||
bool? writePermission,
|
||||
bool? persistablePermission}) {
|
||||
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}) {
|
||||
Future<String?> openDirectory({
|
||||
String? initialUri,
|
||||
bool? writePermission,
|
||||
bool? persistablePermission,
|
||||
}) {
|
||||
throw UnimplementedError('openDirectory() has not been implemented.');
|
||||
}
|
||||
|
||||
@@ -75,25 +77,29 @@ abstract class SafUtilPlatform extends PlatformInterface {
|
||||
throw UnimplementedError('pickFile() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<String?> openFile({
|
||||
String? initialUri,
|
||||
List<String>? mimeTypes,
|
||||
}) {
|
||||
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,
|
||||
bool multiple = true,
|
||||
}) {
|
||||
throw UnimplementedError('pickFiles() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<List<SafDocumentFile>?> pickMedia({
|
||||
bool multiple = true,
|
||||
String mode = 'all',
|
||||
}) {
|
||||
throw UnimplementedError('pickMedia() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<List<String>?> openFiles({
|
||||
String? initialUri,
|
||||
List<String>? mimeTypes,
|
||||
multiple = true,
|
||||
bool multiple = true,
|
||||
}) {
|
||||
throw UnimplementedError('openFiles() has not been implemented.');
|
||||
}
|
||||
@@ -106,7 +112,7 @@ abstract class SafUtilPlatform extends PlatformInterface {
|
||||
throw UnimplementedError('documentFileFromUri() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<SafDocumentFile?> stat(String uri, bool? isDir) {
|
||||
Future<SafDocumentFile?> stat(String uri, bool? isDir, {bool? throws}) {
|
||||
throw UnimplementedError('stat() has not been implemented.');
|
||||
}
|
||||
|
||||
@@ -131,7 +137,11 @@ abstract class SafUtilPlatform extends PlatformInterface {
|
||||
}
|
||||
|
||||
Future<SafDocumentFile> moveTo(
|
||||
String uri, bool isDir, String parentUri, String newParentUri) {
|
||||
String uri,
|
||||
bool isDir,
|
||||
String parentUri,
|
||||
String newParentUri,
|
||||
) {
|
||||
throw UnimplementedError('moveTo() has not been implemented.');
|
||||
}
|
||||
|
||||
@@ -164,12 +174,17 @@ abstract class SafUtilPlatform extends PlatformInterface {
|
||||
bool checkWrite = false,
|
||||
}) {
|
||||
throw UnimplementedError(
|
||||
'hasPersistedPermission() has not been implemented.');
|
||||
'hasPersistedPermission() has not been implemented.',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> releasePersistedPermission(String uri,
|
||||
{bool read = true, bool write = false}) {
|
||||
Future<void> releasePersistedPermission(
|
||||
String uri, {
|
||||
bool read = true,
|
||||
bool write = false,
|
||||
}) {
|
||||
throw UnimplementedError(
|
||||
'releasePersistedPermission() has not been implemented.');
|
||||
'releasePersistedPermission() has not been implemented.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ packages:
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||
@@ -123,6 +123,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
mgenware_dart_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: mgenware_dart_lints
|
||||
sha256: "14f47c0ba0073c1980298ee10f602c19fa244151c68ec75fde602642c3de3a4e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -209,5 +217,5 @@ packages:
|
||||
source: hosted
|
||||
version: "15.2.0"
|
||||
sdks:
|
||||
dart: ">=3.10.0-0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
dart: ">=3.12.0 <4.0.0"
|
||||
flutter: ">=3.44.0"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
name: saf_util
|
||||
description: "Util functions for SAF (Storage Access Framework)."
|
||||
version: 2.0.0
|
||||
version: 3.1.0
|
||||
homepage: https://github.com/flutter-cavalry/saf_util
|
||||
|
||||
environment:
|
||||
sdk: ">=2.18.0 <4.0.0"
|
||||
flutter: ">=2.5.0"
|
||||
sdk: ^3.12.0
|
||||
flutter: ">=3.44.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
@@ -15,7 +15,7 @@ dependencies:
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^6.0.0
|
||||
mgenware_dart_lints: ^8.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2020-2023, creativecreatorormaybenot
|
||||
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 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.cxx
|
||||
@@ -0,0 +1,71 @@
|
||||
group 'dev.fluttercommunity.plus.wakelock'
|
||||
version '1.0-SNAPSHOT'
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '2.2.0'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.12.1'
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
namespace 'dev.fluttercommunity.plus.wakelock'
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
test.java.srcDirs += 'src/test/kotlin'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// Use flutter.minSdkVersion once the minimum supported Flutter version is 3.35 or higher.
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
testImplementation 'org.jetbrains.kotlin:kotlin-test'
|
||||
testImplementation 'org.mockito:mockito-core:5.0.0'
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.includeAndroidResources = true
|
||||
unitTests.all {
|
||||
useJUnitPlatform()
|
||||
|
||||
testLogging {
|
||||
events 'passed', 'skipped', 'failed', 'standardOut', 'standardError'
|
||||
outputs.upToDateWhen { false }
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
rootProject.name = 'wakelock_plus'
|
||||
@@ -0,0 +1,3 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="dev.fluttercommunity.plus.wakelock">
|
||||
</manifest>
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package dev.fluttercommunity.plus.wakelock
|
||||
|
||||
import IsEnabledMessage
|
||||
import ToggleMessage
|
||||
import android.app.Activity
|
||||
import android.view.WindowManager
|
||||
|
||||
internal class Wakelock {
|
||||
var activity: Activity? = null
|
||||
|
||||
private val enabled
|
||||
get() = activity!!.window.attributes.flags and
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON != 0
|
||||
|
||||
fun toggle(message: ToggleMessage) {
|
||||
if (activity == null) {
|
||||
throw NoActivityException()
|
||||
}
|
||||
|
||||
val activity = this.activity!!
|
||||
val enabled = this.enabled
|
||||
|
||||
if (message.enable!!) {
|
||||
if (!enabled) activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
} else if (enabled) {
|
||||
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
}
|
||||
|
||||
fun isEnabled(): IsEnabledMessage {
|
||||
if (activity == null) {
|
||||
throw NoActivityException()
|
||||
}
|
||||
|
||||
return IsEnabledMessage(enabled = enabled)
|
||||
}
|
||||
}
|
||||
|
||||
class NoActivityException : Exception("wakelock requires a foreground activity")
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
// Autogenerated from Pigeon (v26.2.3), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
|
||||
import android.util.Log
|
||||
import io.flutter.plugin.common.BasicMessageChannel
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MessageCodec
|
||||
import io.flutter.plugin.common.StandardMethodCodec
|
||||
import io.flutter.plugin.common.StandardMessageCodec
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
private object WakelockPlusMessagesPigeonUtils {
|
||||
|
||||
fun wrapResult(result: Any?): List<Any?> {
|
||||
return listOf(result)
|
||||
}
|
||||
|
||||
fun wrapError(exception: Throwable): List<Any?> {
|
||||
return if (exception is WakelockPlusFlutterError) {
|
||||
listOf(
|
||||
exception.code,
|
||||
exception.message,
|
||||
exception.details
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
exception.javaClass.simpleName,
|
||||
exception.toString(),
|
||||
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
||||
)
|
||||
}
|
||||
}
|
||||
fun deepEquals(a: Any?, b: Any?): Boolean {
|
||||
if (a is ByteArray && b is ByteArray) {
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is IntArray && b is IntArray) {
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is LongArray && b is LongArray) {
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is DoubleArray && b is DoubleArray) {
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is Array<*> && b is Array<*>) {
|
||||
return a.size == b.size &&
|
||||
a.indices.all{ deepEquals(a[it], b[it]) }
|
||||
}
|
||||
if (a is List<*> && b is List<*>) {
|
||||
return a.size == b.size &&
|
||||
a.indices.all{ deepEquals(a[it], b[it]) }
|
||||
}
|
||||
if (a is Map<*, *> && b is Map<*, *>) {
|
||||
return a.size == b.size && a.all {
|
||||
(b as Map<Any?, Any?>).contains(it.key) &&
|
||||
deepEquals(it.value, b[it.key])
|
||||
}
|
||||
}
|
||||
return a == b
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Error class for passing custom error details to Flutter via a thrown PlatformException.
|
||||
* @property code The error code.
|
||||
* @property message The error message.
|
||||
* @property details The error details. Must be a datatype supported by the api codec.
|
||||
*/
|
||||
class WakelockPlusFlutterError (
|
||||
val code: String,
|
||||
override val message: String? = null,
|
||||
val details: Any? = null
|
||||
) : Throwable()
|
||||
|
||||
/**
|
||||
* Message for toggling the wakelock on the platform side.
|
||||
*
|
||||
* Generated class from Pigeon that represents data sent in messages.
|
||||
*/
|
||||
data class ToggleMessage (
|
||||
val enable: Boolean? = null
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): ToggleMessage {
|
||||
val enable = pigeonVar_list[0] as Boolean?
|
||||
return ToggleMessage(enable)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
enable,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is ToggleMessage) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
return WakelockPlusMessagesPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
}
|
||||
|
||||
/**
|
||||
* Message for reporting the wakelock state from the platform side.
|
||||
*
|
||||
* Generated class from Pigeon that represents data sent in messages.
|
||||
*/
|
||||
data class IsEnabledMessage (
|
||||
val enabled: Boolean? = null
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): IsEnabledMessage {
|
||||
val enabled = pigeonVar_list[0] as Boolean?
|
||||
return IsEnabledMessage(enabled)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
enabled,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is IsEnabledMessage) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
return WakelockPlusMessagesPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
}
|
||||
private open class WakelockPlusMessagesPigeonCodec : StandardMessageCodec() {
|
||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||
return when (type) {
|
||||
129.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
ToggleMessage.fromList(it)
|
||||
}
|
||||
}
|
||||
130.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
IsEnabledMessage.fromList(it)
|
||||
}
|
||||
}
|
||||
else -> super.readValueOfType(type, buffer)
|
||||
}
|
||||
}
|
||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||
when (value) {
|
||||
is ToggleMessage -> {
|
||||
stream.write(129)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is IsEnabledMessage -> {
|
||||
stream.write(130)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
else -> super.writeValue(stream, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
interface WakelockPlusApi {
|
||||
fun toggle(msg: ToggleMessage)
|
||||
fun isEnabled(): IsEnabledMessage
|
||||
|
||||
companion object {
|
||||
/** The codec used by WakelockPlusApi. */
|
||||
val codec: MessageCodec<Any?> by lazy {
|
||||
WakelockPlusMessagesPigeonCodec()
|
||||
}
|
||||
/** Sets up an instance of `WakelockPlusApi` to handle messages through the `binaryMessenger`. */
|
||||
@JvmOverloads
|
||||
fun setUp(binaryMessenger: BinaryMessenger, api: WakelockPlusApi?, messageChannelSuffix: String = "") {
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.wakelock_plus_platform_interface.WakelockPlusApi.toggle$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val msgArg = args[0] as ToggleMessage
|
||||
val wrapped: List<Any?> = try {
|
||||
api.toggle(msgArg)
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
WakelockPlusMessagesPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.wakelock_plus_platform_interface.WakelockPlusApi.isEnabled$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
val wrapped: List<Any?> = try {
|
||||
listOf(api.isEnabled())
|
||||
} catch (exception: Throwable) {
|
||||
WakelockPlusMessagesPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package dev.fluttercommunity.plus.wakelock
|
||||
|
||||
import IsEnabledMessage
|
||||
import ToggleMessage
|
||||
import WakelockPlusApi
|
||||
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
|
||||
/** WakelockPlusPlugin */
|
||||
class WakelockPlusPlugin: FlutterPlugin, WakelockPlusApi, ActivityAware {
|
||||
private var wakelock: Wakelock? = null
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
WakelockPlusApi.setUp(flutterPluginBinding.binaryMessenger, this)
|
||||
wakelock = Wakelock()
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
WakelockPlusApi.setUp(binding.binaryMessenger, null)
|
||||
wakelock = null
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
wakelock?.activity = binding.activity
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
wakelock?.activity = null
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
onAttachedToActivity(binding)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
onDetachedFromActivity()
|
||||
}
|
||||
|
||||
override fun toggle(msg: ToggleMessage) {
|
||||
wakelock!!.toggle(msg)
|
||||
}
|
||||
|
||||
override fun isEnabled(): IsEnabledMessage {
|
||||
return wakelock!!.isEnabled()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
.idea/
|
||||
.vagrant/
|
||||
.sconsign.dblite
|
||||
.svn/
|
||||
|
||||
.DS_Store
|
||||
*.swp
|
||||
profile
|
||||
|
||||
DerivedData/
|
||||
build/
|
||||
.build/
|
||||
.index-build/
|
||||
GeneratedPluginRegistrant.h
|
||||
GeneratedPluginRegistrant.m
|
||||
|
||||
.generated/
|
||||
|
||||
*.pbxuser
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.perspectivev3
|
||||
|
||||
!default.pbxuser
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.perspectivev3
|
||||
|
||||
xcuserdata
|
||||
|
||||
*.moved-aside
|
||||
|
||||
*.pyc
|
||||
*sync/
|
||||
Icon?
|
||||
.tags*
|
||||
|
||||
/Flutter/Generated.xcconfig
|
||||
/Flutter/ephemeral/
|
||||
/Flutter/flutter_export_environment.sh
|
||||
@@ -0,0 +1,25 @@
|
||||
#
|
||||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
|
||||
# Run `pod lib lint wakelock_plus.podspec` to validate before publishing.
|
||||
#
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'wakelock_plus'
|
||||
s.version = '0.0.1'
|
||||
s.summary = 'Plugin that allows you to keep the device screen awake, i.e. prevent the screen from sleeping on Android, iOS, macOS, Windows, Linux, and web.'
|
||||
s.description = <<-DESC
|
||||
Plugin that allows you to keep the device screen awake, i.e. prevent the screen from sleeping on Android, iOS, macOS, Windows, Linux, and web.
|
||||
DESC
|
||||
s.homepage = 'https://github.com/fluttercommunity/wakelock_plus'
|
||||
s.license = { :file => '../LICENSE' }
|
||||
s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' }
|
||||
s.source = { :path => '.' }
|
||||
s.source_files = 'wakelock_plus/Sources/wakelock_plus/**/*.{h,m}'
|
||||
s.public_header_files = 'wakelock_plus/Sources/wakelock_plus/include/**/*.h'
|
||||
s.dependency 'Flutter'
|
||||
s.ios.deployment_target = '12.0'
|
||||
s.tvos.deployment_target = '12.0'
|
||||
|
||||
# Flutter.framework does not contain a i386 slice.
|
||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||
s.resource_bundles = {'wakelock_plus_privacy' => ['wakelock_plus/Sources/wakelock_plus/Resources/PrivacyInfo.xcprivacy']}
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
// swift-tools-version: 5.9
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "wakelock_plus",
|
||||
platforms: [
|
||||
.iOS("11.0")
|
||||
],
|
||||
products: [
|
||||
.library(name: "wakelock-plus", targets: ["wakelock_plus"])
|
||||
],
|
||||
dependencies: [],
|
||||
targets: [
|
||||
.target(
|
||||
name: "wakelock_plus",
|
||||
dependencies: [],
|
||||
resources: [
|
||||
.process("Resources")
|
||||
],
|
||||
cSettings: [
|
||||
.headerSearchPath("include/wakelock_plus")
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// UIApplication+idleTimerLock.m
|
||||
// wakelock
|
||||
//
|
||||
// Created by suyao on 2021/12/17.
|
||||
//
|
||||
|
||||
#import "./include/wakelock_plus/UIApplication+idleTimerLock.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
static NSString *idleTimerLockKey = @"idleTimerLockKey";
|
||||
|
||||
|
||||
@implementation UIApplication (idleTimerLock)
|
||||
|
||||
+ (void)load {
|
||||
Method setIdleTimerDisabled = class_getInstanceMethod(self, @selector(setIdleTimerDisabled:));
|
||||
|
||||
Method lock_setIdleTimerDisabled = class_getInstanceMethod(self, @selector(lock_setIdleTimerDisabled:));
|
||||
|
||||
method_exchangeImplementations(setIdleTimerDisabled, lock_setIdleTimerDisabled);
|
||||
}
|
||||
|
||||
- (void)lock_setIdleTimerDisabled:(BOOL)enable {
|
||||
if ([self lock_idleTimerlockEnable]) {
|
||||
return;
|
||||
}
|
||||
[self lock_setIdleTimerDisabled:enable];
|
||||
}
|
||||
|
||||
- (void)lock_idleTimerlockEnable:(BOOL)enable {
|
||||
objc_setAssociatedObject(self, &idleTimerLockKey, @(enable), OBJC_ASSOCIATION_COPY);
|
||||
}
|
||||
|
||||
- (BOOL)lock_idleTimerlockEnable
|
||||
{
|
||||
return [objc_getAssociatedObject(self, &idleTimerLockKey) boolValue];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,48 @@
|
||||
#import "./include/wakelock_plus/WakelockPlusPlugin.h"
|
||||
#import "./include/wakelock_plus/messages.g.h"
|
||||
#import "./include/wakelock_plus/UIApplication+idleTimerLock.h"
|
||||
|
||||
@interface WakelockPlusPlugin () <WAKELOCKPLUSWakelockPlusApi>
|
||||
|
||||
@property (nonatomic, assign) BOOL enable;
|
||||
|
||||
@end
|
||||
|
||||
@implementation WakelockPlusPlugin
|
||||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
|
||||
WakelockPlusPlugin* instance = [[WakelockPlusPlugin alloc] init];
|
||||
SetUpWAKELOCKPLUSWakelockPlusApi(registrar.messenger, instance);
|
||||
}
|
||||
|
||||
- (void)toggleMsg:(WAKELOCKPLUSToggleMessage*)input error:(FlutterError**)error {
|
||||
BOOL enable = [input.enable boolValue];
|
||||
if (!enable) {
|
||||
[[UIApplication sharedApplication] lock_idleTimerlockEnable:enable];//should disable first
|
||||
[self setIdleTimerDisabled:enable];
|
||||
} else {
|
||||
[self setIdleTimerDisabled:enable];
|
||||
[[UIApplication sharedApplication] lock_idleTimerlockEnable:enable];
|
||||
}
|
||||
self.enable = enable;
|
||||
}
|
||||
|
||||
- (void)setIdleTimerDisabled:(BOOL)enable {
|
||||
BOOL enabled = [[UIApplication sharedApplication] isIdleTimerDisabled];
|
||||
if (enable!= enabled) {
|
||||
[[UIApplication sharedApplication] setIdleTimerDisabled:enable];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (WAKELOCKPLUSIsEnabledMessage*)isEnabledWithError:(FlutterError* __autoreleasing *)error {
|
||||
NSNumber *enabled = [NSNumber numberWithBool:[[UIApplication sharedApplication] isIdleTimerDisabled]];
|
||||
WAKELOCKPLUSIsEnabledMessage* result = [[WAKELOCKPLUSIsEnabledMessage alloc] init];
|
||||
result.enabled = enabled;
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)setEnable:(BOOL)enable {
|
||||
_enable = enable;
|
||||
}
|
||||
|
||||
@end
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// UIApplication+idleTimerLock.h
|
||||
// wakelock
|
||||
//
|
||||
// Created by suyao on 2021/12/17.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface UIApplication (idleTimerLock)
|
||||
|
||||
- (void)lock_idleTimerlockEnable:(BOOL)enable;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
#import <Flutter/Flutter.h>
|
||||
|
||||
@interface WakelockPlusPlugin : NSObject<FlutterPlugin>
|
||||
@end
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
// Autogenerated from Pigeon (v26.2.3), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
@import Foundation;
|
||||
|
||||
@protocol FlutterBinaryMessenger;
|
||||
@protocol FlutterMessageCodec;
|
||||
@class FlutterError;
|
||||
@class FlutterStandardTypedData;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class WAKELOCKPLUSToggleMessage;
|
||||
@class WAKELOCKPLUSIsEnabledMessage;
|
||||
|
||||
/// Message for toggling the wakelock on the platform side.
|
||||
@interface WAKELOCKPLUSToggleMessage : NSObject
|
||||
+ (instancetype)makeWithEnable:(nullable NSNumber *)enable;
|
||||
@property(nonatomic, strong, nullable) NSNumber * enable;
|
||||
@end
|
||||
|
||||
/// Message for reporting the wakelock state from the platform side.
|
||||
@interface WAKELOCKPLUSIsEnabledMessage : NSObject
|
||||
+ (instancetype)makeWithEnabled:(nullable NSNumber *)enabled;
|
||||
@property(nonatomic, strong, nullable) NSNumber * enabled;
|
||||
@end
|
||||
|
||||
/// The codec used by all APIs.
|
||||
NSObject<FlutterMessageCodec> *WAKELOCKPLUSGetMessagesCodec(void);
|
||||
|
||||
@protocol WAKELOCKPLUSWakelockPlusApi
|
||||
- (void)toggleMsg:(WAKELOCKPLUSToggleMessage *)msg error:(FlutterError *_Nullable *_Nonnull)error;
|
||||
/// @return `nil` only when `error != nil`.
|
||||
- (nullable WAKELOCKPLUSIsEnabledMessage *)isEnabledWithError:(FlutterError *_Nullable *_Nonnull)error;
|
||||
@end
|
||||
|
||||
extern void SetUpWAKELOCKPLUSWakelockPlusApi(id<FlutterBinaryMessenger> binaryMessenger, NSObject<WAKELOCKPLUSWakelockPlusApi> *_Nullable api);
|
||||
|
||||
extern void SetUpWAKELOCKPLUSWakelockPlusApiWithSuffix(id<FlutterBinaryMessenger> binaryMessenger, NSObject<WAKELOCKPLUSWakelockPlusApi> *_Nullable api, NSString *messageChannelSuffix);
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,173 @@
|
||||
// Autogenerated from Pigeon (v26.2.3), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
#import "./include/wakelock_plus/messages.g.h"
|
||||
|
||||
#if TARGET_OS_OSX
|
||||
@import FlutterMacOS;
|
||||
#else
|
||||
@import Flutter;
|
||||
#endif
|
||||
|
||||
static NSArray<id> *wrapResult(id result, FlutterError *error) {
|
||||
if (error) {
|
||||
return @[
|
||||
error.code ?: [NSNull null], error.message ?: [NSNull null], error.details ?: [NSNull null]
|
||||
];
|
||||
}
|
||||
return @[ result ?: [NSNull null] ];
|
||||
}
|
||||
|
||||
static id GetNullableObjectAtIndex(NSArray<id> *array, NSInteger key) {
|
||||
id result = array[key];
|
||||
return (result == [NSNull null]) ? nil : result;
|
||||
}
|
||||
|
||||
@interface WAKELOCKPLUSToggleMessage ()
|
||||
+ (WAKELOCKPLUSToggleMessage *)fromList:(NSArray<id> *)list;
|
||||
+ (nullable WAKELOCKPLUSToggleMessage *)nullableFromList:(NSArray<id> *)list;
|
||||
- (NSArray<id> *)toList;
|
||||
@end
|
||||
|
||||
@interface WAKELOCKPLUSIsEnabledMessage ()
|
||||
+ (WAKELOCKPLUSIsEnabledMessage *)fromList:(NSArray<id> *)list;
|
||||
+ (nullable WAKELOCKPLUSIsEnabledMessage *)nullableFromList:(NSArray<id> *)list;
|
||||
- (NSArray<id> *)toList;
|
||||
@end
|
||||
|
||||
@implementation WAKELOCKPLUSToggleMessage
|
||||
+ (instancetype)makeWithEnable:(nullable NSNumber *)enable {
|
||||
WAKELOCKPLUSToggleMessage* pigeonResult = [[WAKELOCKPLUSToggleMessage alloc] init];
|
||||
pigeonResult.enable = enable;
|
||||
return pigeonResult;
|
||||
}
|
||||
+ (WAKELOCKPLUSToggleMessage *)fromList:(NSArray<id> *)list {
|
||||
WAKELOCKPLUSToggleMessage *pigeonResult = [[WAKELOCKPLUSToggleMessage alloc] init];
|
||||
pigeonResult.enable = GetNullableObjectAtIndex(list, 0);
|
||||
return pigeonResult;
|
||||
}
|
||||
+ (nullable WAKELOCKPLUSToggleMessage *)nullableFromList:(NSArray<id> *)list {
|
||||
return (list) ? [WAKELOCKPLUSToggleMessage fromList:list] : nil;
|
||||
}
|
||||
- (NSArray<id> *)toList {
|
||||
return @[
|
||||
self.enable ?: [NSNull null],
|
||||
];
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation WAKELOCKPLUSIsEnabledMessage
|
||||
+ (instancetype)makeWithEnabled:(nullable NSNumber *)enabled {
|
||||
WAKELOCKPLUSIsEnabledMessage* pigeonResult = [[WAKELOCKPLUSIsEnabledMessage alloc] init];
|
||||
pigeonResult.enabled = enabled;
|
||||
return pigeonResult;
|
||||
}
|
||||
+ (WAKELOCKPLUSIsEnabledMessage *)fromList:(NSArray<id> *)list {
|
||||
WAKELOCKPLUSIsEnabledMessage *pigeonResult = [[WAKELOCKPLUSIsEnabledMessage alloc] init];
|
||||
pigeonResult.enabled = GetNullableObjectAtIndex(list, 0);
|
||||
return pigeonResult;
|
||||
}
|
||||
+ (nullable WAKELOCKPLUSIsEnabledMessage *)nullableFromList:(NSArray<id> *)list {
|
||||
return (list) ? [WAKELOCKPLUSIsEnabledMessage fromList:list] : nil;
|
||||
}
|
||||
- (NSArray<id> *)toList {
|
||||
return @[
|
||||
self.enabled ?: [NSNull null],
|
||||
];
|
||||
}
|
||||
@end
|
||||
|
||||
@interface WAKELOCKPLUSMessagesPigeonCodecReader : FlutterStandardReader
|
||||
@end
|
||||
@implementation WAKELOCKPLUSMessagesPigeonCodecReader
|
||||
- (nullable id)readValueOfType:(UInt8)type {
|
||||
switch (type) {
|
||||
case 129:
|
||||
return [WAKELOCKPLUSToggleMessage fromList:[self readValue]];
|
||||
case 130:
|
||||
return [WAKELOCKPLUSIsEnabledMessage fromList:[self readValue]];
|
||||
default:
|
||||
return [super readValueOfType:type];
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
@interface WAKELOCKPLUSMessagesPigeonCodecWriter : FlutterStandardWriter
|
||||
@end
|
||||
@implementation WAKELOCKPLUSMessagesPigeonCodecWriter
|
||||
- (void)writeValue:(id)value {
|
||||
if ([value isKindOfClass:[WAKELOCKPLUSToggleMessage class]]) {
|
||||
[self writeByte:129];
|
||||
[self writeValue:[value toList]];
|
||||
} else if ([value isKindOfClass:[WAKELOCKPLUSIsEnabledMessage class]]) {
|
||||
[self writeByte:130];
|
||||
[self writeValue:[value toList]];
|
||||
} else {
|
||||
[super writeValue:value];
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
@interface WAKELOCKPLUSMessagesPigeonCodecReaderWriter : FlutterStandardReaderWriter
|
||||
@end
|
||||
@implementation WAKELOCKPLUSMessagesPigeonCodecReaderWriter
|
||||
- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data {
|
||||
return [[WAKELOCKPLUSMessagesPigeonCodecWriter alloc] initWithData:data];
|
||||
}
|
||||
- (FlutterStandardReader *)readerWithData:(NSData *)data {
|
||||
return [[WAKELOCKPLUSMessagesPigeonCodecReader alloc] initWithData:data];
|
||||
}
|
||||
@end
|
||||
|
||||
NSObject<FlutterMessageCodec> *WAKELOCKPLUSGetMessagesCodec(void) {
|
||||
static FlutterStandardMessageCodec *sSharedObject = nil;
|
||||
static dispatch_once_t sPred = 0;
|
||||
dispatch_once(&sPred, ^{
|
||||
WAKELOCKPLUSMessagesPigeonCodecReaderWriter *readerWriter = [[WAKELOCKPLUSMessagesPigeonCodecReaderWriter alloc] init];
|
||||
sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter];
|
||||
});
|
||||
return sSharedObject;
|
||||
}
|
||||
void SetUpWAKELOCKPLUSWakelockPlusApi(id<FlutterBinaryMessenger> binaryMessenger, NSObject<WAKELOCKPLUSWakelockPlusApi> *api) {
|
||||
SetUpWAKELOCKPLUSWakelockPlusApiWithSuffix(binaryMessenger, api, @"");
|
||||
}
|
||||
|
||||
void SetUpWAKELOCKPLUSWakelockPlusApiWithSuffix(id<FlutterBinaryMessenger> binaryMessenger, NSObject<WAKELOCKPLUSWakelockPlusApi> *api, NSString *messageChannelSuffix) {
|
||||
messageChannelSuffix = messageChannelSuffix.length > 0 ? [NSString stringWithFormat: @".%@", messageChannelSuffix] : @"";
|
||||
{
|
||||
FlutterBasicMessageChannel *channel =
|
||||
[[FlutterBasicMessageChannel alloc]
|
||||
initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.wakelock_plus_platform_interface.WakelockPlusApi.toggle", messageChannelSuffix]
|
||||
binaryMessenger:binaryMessenger
|
||||
codec:WAKELOCKPLUSGetMessagesCodec()];
|
||||
if (api) {
|
||||
NSCAssert([api respondsToSelector:@selector(toggleMsg:error:)], @"WAKELOCKPLUSWakelockPlusApi api (%@) doesn't respond to @selector(toggleMsg:error:)", api);
|
||||
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
|
||||
NSArray<id> *args = message;
|
||||
WAKELOCKPLUSToggleMessage *arg_msg = GetNullableObjectAtIndex(args, 0);
|
||||
FlutterError *error;
|
||||
[api toggleMsg:arg_msg error:&error];
|
||||
callback(wrapResult(nil, error));
|
||||
}];
|
||||
} else {
|
||||
[channel setMessageHandler:nil];
|
||||
}
|
||||
}
|
||||
{
|
||||
FlutterBasicMessageChannel *channel =
|
||||
[[FlutterBasicMessageChannel alloc]
|
||||
initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.wakelock_plus_platform_interface.WakelockPlusApi.isEnabled", messageChannelSuffix]
|
||||
binaryMessenger:binaryMessenger
|
||||
codec:WAKELOCKPLUSGetMessagesCodec()];
|
||||
if (api) {
|
||||
NSCAssert([api respondsToSelector:@selector(isEnabledWithError:)], @"WAKELOCKPLUSWakelockPlusApi api (%@) doesn't respond to @selector(isEnabledWithError:)", api);
|
||||
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
|
||||
FlutterError *error;
|
||||
WAKELOCKPLUSIsEnabledMessage *output = [api isEnabledWithError:&error];
|
||||
callback(wrapResult(output, error));
|
||||
}];
|
||||
} else {
|
||||
[channel setMessageHandler:nil];
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
export 'wakelock_plus_linux_plugin.dart';
|
||||
export 'wakelock_plus_macos_plugin.dart';
|
||||
export 'wakelock_plus_windows_plugin.dart';
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dbus/dbus.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:wakelock_plus_platform_interface/wakelock_plus_platform_interface.dart';
|
||||
|
||||
/// The Linux implementation of the [WakelockPlusPlatformInterface].
|
||||
///
|
||||
/// This class implements the `wakelock_plus` plugin functionality for Linux
|
||||
/// using the `org.freedesktop.portal.Inhibit` D-Bus API
|
||||
/// (see https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Inhibit).
|
||||
class WakelockPlusLinuxPlugin extends WakelockPlusPlatformInterface {
|
||||
/// Registers this class as the default instance of [WakelockPlatformInterface].
|
||||
static void registerWith() {
|
||||
WakelockPlusPlatformInterface.instance = WakelockPlusLinuxPlugin();
|
||||
}
|
||||
|
||||
/// Constructs an instance of [WakelockPlusLinuxPlugin].
|
||||
factory WakelockPlusLinuxPlugin({
|
||||
@visibleForTesting DBusClient? client,
|
||||
@visibleForTesting DBusRemoteObject? object,
|
||||
@visibleForTesting Future<String> Function()? appNameGetter,
|
||||
}) {
|
||||
final dbusClient = client ?? DBusClient.session();
|
||||
final remoteObject =
|
||||
object ??
|
||||
DBusRemoteObject(
|
||||
dbusClient,
|
||||
name: 'org.freedesktop.portal.Desktop',
|
||||
path: DBusObjectPath('/org/freedesktop/portal/desktop'),
|
||||
);
|
||||
return WakelockPlusLinuxPlugin._internal(
|
||||
dbusClient,
|
||||
remoteObject,
|
||||
appNameGetter,
|
||||
);
|
||||
}
|
||||
|
||||
WakelockPlusLinuxPlugin._internal(
|
||||
this._client,
|
||||
this._object,
|
||||
this._appNameGetter,
|
||||
);
|
||||
|
||||
final DBusClient _client;
|
||||
final DBusRemoteObject _object;
|
||||
final Future<String> Function()? _appNameGetter;
|
||||
DBusObjectPath? _requestHandle;
|
||||
|
||||
Future<String> get _appName =>
|
||||
_appNameGetter?.call() ??
|
||||
PackageInfo.fromPlatform().then((info) => info.appName);
|
||||
|
||||
@override
|
||||
Future<void> toggle({required bool enable}) async {
|
||||
if (enable) {
|
||||
final appName = await _appName;
|
||||
_requestHandle = await _object
|
||||
.callMethod(
|
||||
'org.freedesktop.portal.Inhibit',
|
||||
'Inhibit',
|
||||
[
|
||||
const DBusString(''),
|
||||
const DBusUint32(8),
|
||||
DBusDict.stringVariant({
|
||||
'reason': DBusString('$appName: wakelock active'),
|
||||
}),
|
||||
],
|
||||
replySignature: DBusSignature('o'),
|
||||
)
|
||||
.then((response) => response.returnValues.single.asObjectPath());
|
||||
} else if (_requestHandle != null) {
|
||||
final requestObject = DBusRemoteObject(
|
||||
_client,
|
||||
name: 'org.freedesktop.portal.Desktop',
|
||||
path: _requestHandle!,
|
||||
);
|
||||
await requestObject.callMethod(
|
||||
'org.freedesktop.portal.Request',
|
||||
'Close',
|
||||
[],
|
||||
replySignature: DBusSignature.empty,
|
||||
);
|
||||
_requestHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> get enabled async => _requestHandle != null;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:wakelock_plus_platform_interface/wakelock_plus_platform_interface.dart';
|
||||
|
||||
/// The macOS implementation of the [WakelockPlusPlatformInterface].
|
||||
///
|
||||
/// This class implements the `wakelock_plus` plugin functionality for macOS.
|
||||
///
|
||||
/// Note that this is *also* a method channel implementation (like the default
|
||||
/// instance). We use manual method channel calls instead of `pigeon` for the
|
||||
/// moment because macOS support for `pigeon` is not clear yet.
|
||||
/// See https://github.com/flutter/flutter/issues/73738.
|
||||
class WakelockPlusMacOSPlugin extends WakelockPlusPlatformInterface {
|
||||
static const MethodChannel _channel = MethodChannel('wakelock_plus_macos');
|
||||
|
||||
/// Registers this class as the default instance of [WakelockPlatformInterface].
|
||||
static void registerWith() {
|
||||
WakelockPlusPlatformInterface.instance = WakelockPlusMacOSPlugin();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> toggle({required bool enable}) async {
|
||||
await _channel.invokeMethod('toggle', <String, dynamic>{'enable': enable});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> get enabled async =>
|
||||
await _channel.invokeMethod('enabled') as bool;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
||||
import 'package:wakelock_plus/src/web_impl/import_js_library.dart';
|
||||
import 'package:wakelock_plus/src/web_impl/js_wakelock.dart'
|
||||
as wakelock_plus_web;
|
||||
import 'package:wakelock_plus_platform_interface/wakelock_plus_platform_interface.dart';
|
||||
|
||||
/// The web implementation of the [WakelockPlatformInterface].
|
||||
///
|
||||
/// This class implements the `wakelock_plus` plugin functionality for web.
|
||||
class WakelockPlusWebPlugin extends WakelockPlusPlatformInterface {
|
||||
/// Registers [WakelockPlusWebPlugin] as the default instance of the
|
||||
/// [WakelockPlatformInterface].
|
||||
static void registerWith(Registrar registrar) {
|
||||
WakelockPlusPlatformInterface.instance = WakelockPlusWebPlugin();
|
||||
}
|
||||
|
||||
// The future that signals when the JS is loaded.
|
||||
// This needs to be `await`ed before accessing any methods of the
|
||||
// JS-interop layer.
|
||||
Future<void>? _jsLoaded;
|
||||
|
||||
//
|
||||
// Lazily imports the JS library once, then awaits to ensure that
|
||||
// it's loaded into the DOM.
|
||||
//
|
||||
Future<void> _ensureJsLoaded() async {
|
||||
_jsLoaded ??= importJsLibrary(
|
||||
url: 'assets/no_sleep.js',
|
||||
flutterPluginName: 'wakelock_plus',
|
||||
);
|
||||
return _jsLoaded;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> toggle({required bool enable}) async {
|
||||
// Make sure the JS library is loaded before calling it.
|
||||
await _ensureJsLoaded();
|
||||
await wakelock_plus_web.toggle(enable);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> get enabled async {
|
||||
// Make sure the JS library is loaded before calling it.
|
||||
await _ensureJsLoaded();
|
||||
return wakelock_plus_web.enabled();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:wakelock_plus_platform_interface/wakelock_plus_platform_interface.dart';
|
||||
import 'package:win32/win32.dart';
|
||||
|
||||
/// The Windows implementation of the [WakelockPlusPlatformInterface].
|
||||
///
|
||||
/// This class implements the `wakelock_plus` plugin functionality for Windows
|
||||
/// using the `SetThreadExecutionState` win32 API
|
||||
/// (see https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setthreadexecutionstate).
|
||||
class WakelockPlusWindowsPlugin extends WakelockPlusPlatformInterface {
|
||||
/// Registers this class as the default instance of [WakelockPlatformInterface].
|
||||
static void registerWith() {
|
||||
WakelockPlusPlatformInterface.instance = WakelockPlusWindowsPlugin();
|
||||
}
|
||||
|
||||
var _enabled = false;
|
||||
|
||||
@override
|
||||
Future<void> toggle({required bool enable}) async {
|
||||
final int response;
|
||||
if (enable) {
|
||||
response = SetThreadExecutionState(ES_CONTINUOUS | ES_DISPLAY_REQUIRED);
|
||||
} else {
|
||||
response = SetThreadExecutionState(ES_CONTINUOUS);
|
||||
}
|
||||
|
||||
// SetThreadExecutionState returns 0 if the operation failed.
|
||||
if (response != 0) {
|
||||
_enabled = enable;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> get enabled async => _enabled;
|
||||
|
||||
@override
|
||||
bool get isMock => false;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui_web' as ui_web;
|
||||
|
||||
import 'package:web/web.dart' as web;
|
||||
|
||||
/// This is an implementation of the `import_js_library` plugin that is used
|
||||
/// until that plugin is migrated to null safety.
|
||||
/// See https://github.com/florent37/flutter_web_import_js_library/pull/6#issuecomment-735349208.
|
||||
|
||||
/// Imports a JS script file from the given [url] given the relative
|
||||
/// [flutterPluginName].
|
||||
Future<void> importJsLibrary({required String url, String? flutterPluginName}) async {
|
||||
if (flutterPluginName == null) {
|
||||
return _importJSLibraries([url]);
|
||||
} else {
|
||||
return _importJSLibraries([_libraryUrl(url, flutterPluginName)]);
|
||||
}
|
||||
}
|
||||
|
||||
String _libraryUrl(String url, String pluginName) {
|
||||
// Added suggested changes as per
|
||||
// https://github.com/fluttercommunity/wakelock_plus/issues/19#issuecomment-2301963609
|
||||
if (url.startsWith('./')) {
|
||||
url = url.replaceFirst('./', '');
|
||||
}
|
||||
|
||||
if (url.startsWith('assets/')) {
|
||||
if (const bool.fromEnvironment('WEB_PLUGIN_TESTS', defaultValue: false)) {
|
||||
// Flutter tests running on Chrome just need to use the library path
|
||||
// without pre-pending "assets/".
|
||||
//
|
||||
// In other words, don't use the asset manager since it's not currently
|
||||
// supported for Chrome-based Flutter tests.
|
||||
//
|
||||
// See https://github.com/flutter/flutter/issues/159879 for more details.
|
||||
// TODO: Remove the workaround once test asset support is added
|
||||
// for tests running in Chrome.
|
||||
return 'packages/$pluginName/$url';
|
||||
}
|
||||
return ui_web.assetManager.getAssetUrl('packages/$pluginName/$url');
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
Future? _importRunning;
|
||||
Map<String, String> _loadedLibraries = {};
|
||||
int _nextLibraryId = 0;
|
||||
|
||||
web.HTMLScriptElement _createScriptTag(String library) {
|
||||
final scriptId = 'imported-js-library-${_nextLibraryId++}';
|
||||
final script = web.document.createElement('script') as web.HTMLScriptElement
|
||||
..type = 'text/javascript'
|
||||
..charset = 'utf-8'
|
||||
..async = true
|
||||
..src = library
|
||||
..id = scriptId;
|
||||
return script;
|
||||
}
|
||||
|
||||
/// Injects a bunch of libraries in the `<head>` and returns a
|
||||
/// Future that resolves when all load.
|
||||
Future<void> _importJSLibraries(List<String> libraries) async {
|
||||
// we add the library to _loadedLibraries asynchronously, so we need locking.
|
||||
// Dart uses voluntary preemption, so everything between two `await`s can be
|
||||
// considered locked
|
||||
while (_importRunning != null) {
|
||||
await _importRunning;
|
||||
}
|
||||
final importLockCompleter = Completer();
|
||||
_importRunning = importLockCompleter.future;
|
||||
final loading = <Future<void>>[];
|
||||
final head = web.document.head;
|
||||
|
||||
for (final library in libraries) {
|
||||
if (!_isImported(library)) {
|
||||
final scriptTag = _createScriptTag(library);
|
||||
head!.appendChild(scriptTag);
|
||||
final completer = Completer();
|
||||
loading.add(completer.future);
|
||||
|
||||
unawaited(
|
||||
scriptTag.onLoad.first.then((_) {
|
||||
_loadedLibraries[library] = scriptTag.id;
|
||||
completer.complete();
|
||||
}),
|
||||
);
|
||||
unawaited(scriptTag.onError.first.then((event) => completer.completeError(Exception('Error loading: $library'))));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await Future.wait(loading, eagerError: true);
|
||||
} finally {
|
||||
// first "unlock" future, then complete the completer for anyone already waiting.
|
||||
// I'm not sure if `.complete()` is yielding execution, so this is the safe order
|
||||
_importRunning = null;
|
||||
importLockCompleter.complete();
|
||||
}
|
||||
}
|
||||
|
||||
bool _isImported(String url) {
|
||||
final head = web.document.head!;
|
||||
return _isLoaded(head, url);
|
||||
}
|
||||
|
||||
bool _isLoaded(web.HTMLHeadElement head, String url) {
|
||||
final scriptId = _loadedLibraries[url];
|
||||
if (scriptId == null) {
|
||||
return false;
|
||||
}
|
||||
return head.querySelector('#$scriptId') != null;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
@JS('Wakelock')
|
||||
library;
|
||||
|
||||
import 'dart:js_interop';
|
||||
|
||||
@JS('toggle')
|
||||
external JSPromise<JSAny?> _toggle(JSBoolean enable);
|
||||
|
||||
/// Toggles the JS wakelock.
|
||||
Future<void> toggle(bool enable) {
|
||||
return _toggle(enable.toJS).toDart.then((_) => null);
|
||||
}
|
||||
|
||||
@JS('enabled')
|
||||
external JSPromise<JSBoolean> _enabled();
|
||||
|
||||
/// Returns a JS promise of whether the wakelock is enabled or not.
|
||||
Future<bool> enabled() {
|
||||
return _enabled().toDart.then((enabled) => enabled.toDart);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:wakelock_plus_platform_interface/wakelock_plus_platform_interface.dart';
|
||||
|
||||
export 'src/wakelock_plus_io_plugin.dart'
|
||||
if (dart.library.js_interop) 'src/wakelock_plus_web_plugin.dart';
|
||||
|
||||
/// The [WakelockPlusPlatformInterface] that is used by [WakelockPlus].
|
||||
///
|
||||
/// This needs to be exposed for testing as unit tests might run on macOS.
|
||||
/// In that case, the "hacky" instance override that we use here would be
|
||||
/// triggered for the unit tests, even though the unit tests should actually
|
||||
/// test the `pigeon` method channel implementation. Therefore, we want to
|
||||
/// override this in tests that run on macOS (where there is no actual device).
|
||||
@visibleForTesting
|
||||
var wakelockPlusPlatformInstance = WakelockPlusPlatformInterface.instance;
|
||||
|
||||
/// Class providing all wakelock functionality using static members.
|
||||
///
|
||||
/// To enable the wakelock, you can use [WakelockPlus.enable] and to disable it,
|
||||
/// you can call [WakelockPlus.disable].
|
||||
/// You do not need to worry about making redundant calls, e.g. calling
|
||||
/// [WakelockPlus.enable] when the wakelock is already enabled as the plugin handles
|
||||
/// this for you, i.e. it checks the status to determine if the wakelock is
|
||||
/// already enabled or disabled.
|
||||
/// If you want the flexibility to pass a [bool] to control whether the wakelock
|
||||
/// should be enabled or disabled, you can use [WakelockPlus.toggle].
|
||||
///
|
||||
/// The [WakelockPlus.enabled] getter allows you to retrieve the current wakelock
|
||||
/// status of the device..
|
||||
class WakelockPlus {
|
||||
/// Enables the wakelock.
|
||||
///
|
||||
/// This can simply be called using `WakelockPlus.enable()` and does not return
|
||||
/// anything.
|
||||
/// You can await the [Future] to wait for the operation to complete.
|
||||
///
|
||||
/// See also:
|
||||
/// * [toggle], which allows to enable or disable using a [bool] parameter.
|
||||
static Future<void> enable() => toggle(enable: true);
|
||||
|
||||
/// Disables the wakelock.
|
||||
///
|
||||
/// This can simply be called using `WakelockPlus.disable()` and does not return
|
||||
/// anything.
|
||||
/// You can await the [Future] to wait for the operation to complete.
|
||||
///
|
||||
/// See also:
|
||||
/// * [toggle], which allows to enable or disable using a [bool] parameter.
|
||||
static Future<void> disable() => toggle(enable: false);
|
||||
|
||||
/// Toggles the wakelock on or off.
|
||||
///
|
||||
/// You can simply use this function to toggle the wakelock using a [bool]
|
||||
/// value (for the [enable] parameter).
|
||||
///
|
||||
/// ```dart
|
||||
/// // This line keeps the screen on.
|
||||
/// WakelockPlus.toggle(enable: true);
|
||||
///
|
||||
/// bool enableWakelock = false;
|
||||
/// // The following line disables the WakelockPlus.
|
||||
/// WakelockPlus.toggle(enable: enableWakelock);
|
||||
/// ```
|
||||
///
|
||||
/// You can await the [Future] to wait for the operation to complete.
|
||||
static Future<void> toggle({required bool enable}) {
|
||||
return wakelockPlusPlatformInstance.toggle(enable: enable);
|
||||
}
|
||||
|
||||
/// Returns whether the wakelock is currently enabled or not.
|
||||
///
|
||||
/// If you want to retrieve the current wakelock status, you will have to call
|
||||
/// [WakelockPlus.enabled] and await its result:
|
||||
///
|
||||
/// ```dart
|
||||
/// bool wakelockEnabled = await WakelockPlus.enabled;
|
||||
/// ```
|
||||
static Future<bool> get enabled => wakelockPlusPlatformInstance.enabled;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#
|
||||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
|
||||
#
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'wakelock_plus'
|
||||
s.version = '0.0.1'
|
||||
s.summary = 'Plugin that allows you to keep the device screen awake, i.e. prevent the screen from sleeping on Android, iOS, macOS, Windows, and web.'
|
||||
s.description = <<-DESC
|
||||
Plugin that allows you to keep the device screen awake, i.e. prevent the screen from sleeping on Android, iOS, macOS, Windows, and web.
|
||||
DESC
|
||||
s.homepage = 'https://github.com/fluttercommunity/wakelock_plus'
|
||||
s.license = { :type => 'BSD', :file => '../LICENSE' }
|
||||
s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' }
|
||||
s.source = { :http => 'https://github.com/fluttercommunity/wakelock_plus/tree/main/packages/wakelock_plus_macos' }
|
||||
s.source_files = 'wakelock_plus/Sources/wakelock_plus/**/*.swift'
|
||||
s.dependency 'FlutterMacOS'
|
||||
s.osx.deployment_target = '10.15'
|
||||
# Flutter.framework does not contain a i386 slice.
|
||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||
s.swift_version = '5.0'
|
||||
s.resource_bundles = {'wakelock_plus' => ['wakelock_plus/Sources/wakelock_plus/Resources/PrivacyInfo.xcprivacy']}
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
// swift-tools-version: 5.9
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "wakelock_plus",
|
||||
platforms: [
|
||||
.macOS("10.15")
|
||||
],
|
||||
products: [
|
||||
.library(name: "wakelock-plus", targets: ["wakelock_plus"])
|
||||
],
|
||||
dependencies: [],
|
||||
targets: [
|
||||
.target(
|
||||
name: "wakelock_plus",
|
||||
dependencies: [],
|
||||
resources: [
|
||||
.process("Resources"),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import Cocoa
|
||||
import FlutterMacOS
|
||||
import IOKit.pwr_mgt
|
||||
|
||||
public class WakelockPlusMacosPlugin: NSObject, FlutterPlugin {
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let channel = FlutterMethodChannel(name: "wakelock_plus_macos", binaryMessenger: registrar.messenger)
|
||||
let instance = WakelockPlusMacosPlugin()
|
||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||
}
|
||||
|
||||
var assertionID: IOPMAssertionID = 0
|
||||
var wakelockEnabled = false
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
case "toggle":
|
||||
let args = call.arguments as? Dictionary<String, Any>
|
||||
let enable = args!["enable"] as! Bool
|
||||
if enable {
|
||||
enableWakelock()
|
||||
} else {
|
||||
disableWakelock();
|
||||
}
|
||||
result(true)
|
||||
case "enabled":
|
||||
result(wakelockEnabled)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
func enableWakelock(reason: String = "Disabling display sleep") {
|
||||
if !wakelockEnabled {
|
||||
wakelockEnabled = IOPMAssertionCreateWithName( kIOPMAssertionTypeNoDisplaySleep as CFString,
|
||||
IOPMAssertionLevel(kIOPMAssertionLevelOn),
|
||||
reason as CFString,
|
||||
&assertionID) == kIOReturnSuccess
|
||||
}
|
||||
}
|
||||
|
||||
func disableWakelock() {
|
||||
if wakelockEnabled {
|
||||
IOPMAssertionRelease(assertionID)
|
||||
wakelockEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
# Vendored from fluttercommunity/wakelock_plus at
|
||||
# 4f4be85aafe1f8216c2fdd3376263d6f40529684. Local changes are the tvOS
|
||||
# deployment target and explicit unawaited wrappers required by the root lint
|
||||
# policy. Refresh from the newest upstream release compatible with win32 5.x,
|
||||
# reapply both changes, then run pub get and the tvOS pod build.
|
||||
name: wakelock_plus
|
||||
description: >-2
|
||||
Plugin that allows you to keep the device screen awake, i.e. prevent the screen from sleeping on
|
||||
Android, iOS, macOS, Windows, Linux, and web.
|
||||
version: 1.5.2
|
||||
repository: https://github.com/fluttercommunity/wakelock_plus/tree/main/wakelock_plus
|
||||
|
||||
environment:
|
||||
sdk: '>=3.10.0 <4.0.0'
|
||||
flutter: ">=3.38.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_web_plugins:
|
||||
sdk: flutter
|
||||
meta: ^1.17.0
|
||||
wakelock_plus_platform_interface: ^1.4.0
|
||||
|
||||
# Windows dependencies
|
||||
# win32 is compatible across v5 for Win32 only (not COM)
|
||||
win32: ">=5.6.1 <6.0.0"
|
||||
|
||||
# Linux dependencies
|
||||
dbus: ^0.7.12
|
||||
package_info_plus: ^9.0.0
|
||||
|
||||
# Web dependencies
|
||||
web: ">=0.5.1 <2.0.0"
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^6.0.0
|
||||
pigeon: ^26.2.3 # dart run pigeon --input "pigeons/messages.dart"
|
||||
mocktail: ^1.0.4
|
||||
|
||||
# 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: dev.fluttercommunity.plus.wakelock
|
||||
pluginClass: WakelockPlusPlugin
|
||||
ios:
|
||||
pluginClass: WakelockPlusPlugin
|
||||
windows:
|
||||
dartPluginClass: WakelockPlusWindowsPlugin
|
||||
macos:
|
||||
pluginClass: WakelockPlusMacosPlugin
|
||||
dartPluginClass: WakelockPlusMacOSPlugin
|
||||
linux:
|
||||
dartPluginClass: WakelockPlusLinuxPlugin
|
||||
web:
|
||||
pluginClass: WakelockPlusWebPlugin
|
||||
fileName: src/wakelock_plus_web_plugin.dart
|
||||
|
||||
# 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/assets-and-images/#from-packages
|
||||
#
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||
|
||||
# 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/custom-fonts/#from-packages
|
||||
|
||||
assets:
|
||||
- packages/wakelock_plus/assets/no_sleep.js
|
||||
Reference in New Issue
Block a user