fix(deps): refresh and document native dependencies

This commit is contained in:
edde746
2026-07-12 17:31:16 +02:00
parent b676ef6c56
commit 858952929b
49 changed files with 2444 additions and 494 deletions
+24 -6
View File
@@ -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
View File
@@ -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
+9
View File
@@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx
+8 -7
View File
@@ -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"
@@ -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()
@@ -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)
}
}
}
+58 -29
View File
@@ -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,
);
}
}
+109 -102
View File
@@ -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.',
);
}
}
+11 -3
View File
@@ -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"
+4 -4
View File
@@ -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
+29
View File
@@ -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>
@@ -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")
@@ -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)
}
}
}
}
}
@@ -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()
}
}
+40
View File
@@ -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")
]
)
]
)
@@ -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>
@@ -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
@@ -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
@@ -0,0 +1,4 @@
#import <Flutter/Flutter.h>
@interface WakelockPlusPlugin : NSObject<FlutterPlugin>
@end
@@ -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"),
]
)
]
)
@@ -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>
@@ -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
}
}
}
+108
View File
@@ -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