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
@@ -58,8 +58,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/getsentry/sentry-cocoa", "location" : "https://github.com/getsentry/sentry-cocoa",
"state" : { "state" : {
"revision" : "16cd512711375fa73f25ae5e373f596bdf4251ae", "revision" : "dad229c665bfd043c5d80ac7aa77717cbd19a1c3",
"version" : "8.58.0" "version" : "8.58.3"
} }
}, },
{ {
+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: Vendored from `flutter-cavalry/saf_util` at
pending picker replies are take-and-clear (no reply to an already-answered `e300a03ea34b49414b42f309e02531ece57cd0d1`. The local Result-lifecycle patch
Result → no "Reply already submitted" crash), unrelated activity request take-and-clears picker replies, ignores unrelated request codes, reattaches the
codes no longer consume the pending picker, and failed picker launches listener across configuration changes, and clears state after launch failures.
clear the stale pending state. 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 ## 2.0.0
+1 -4
View File
@@ -1,4 +1 @@
include: package:flutter_lints/flutter.yaml include: package:mgenware_dart_lints/flutter_lints.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+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" version = "1.0-SNAPSHOT"
buildscript { buildscript {
val kotlinVersion = "2.2.20" val kotlinVersion = "2.3.20"
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
} }
dependencies { 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") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
} }
} }
@@ -23,7 +23,6 @@ allprojects {
plugins { plugins {
id("com.android.library") id("com.android.library")
id("kotlin-android")
} }
android { android {
@@ -36,10 +35,6 @@ android {
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
} }
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
sourceSets { sourceSets {
getByName("main") { getByName("main") {
java.srcDirs("src/main/kotlin") java.srcDirs("src/main/kotlin")
@@ -71,6 +66,12 @@ android {
} }
} }
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
dependencies { dependencies {
implementation("androidx.documentfile:documentfile:1.1.0") implementation("androidx.documentfile:documentfile:1.1.0")
testImplementation("org.jetbrains.kotlin:kotlin-test") 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.Build
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract import android.provider.DocumentsContract
import androidx.documentfile.provider.DocumentFile import android.provider.MediaStore
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding 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.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry import io.flutter.plugin.common.PluginRegistry
import java.io.File
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
/** SafUtilPlugin */ /** SafUtilPlugin */
class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { class SafUtilPlugin :
/// The MethodChannel that will the communication between Flutter and native Android FlutterPlugin,
/// MethodCallHandler,
/// This local reference serves to register the plugin with the Flutter Engine and unregister it ActivityAware {
/// when the Flutter Engine is detached from the Activity // / The MethodChannel that will the communication between Flutter and native Android
private lateinit var channel : MethodChannel // /
// / 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 lateinit var context: Context
private var activity: Activity? = null private var activity: Activity? = null
@@ -44,20 +47,19 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
private var pendingArguments: PendingArguments? = null private var pendingArguments: PendingArguments? = null
private val requestCodeOpenDocumentTree = 1001 private val requestCodeOpenDocumentTree = 1001
private val requestCodeOpenFiles = 1002 private val requestCodeOpenFiles = 1002
private val requestCodePickMedia = 1003
private val activityResultListener = PluginRegistry.ActivityResultListener { requestCode, resultCode, data -> private val activityResultListener = PluginRegistry.ActivityResultListener { requestCode, resultCode, data ->
onActivityResult(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 /** Takes ownership before replying so a Result can never be answered twice. */
/// pending Result must go through this so no already-answered Result is ever
/// left behind to be answered again ("Reply already submitted" crashes).
private fun takePendingResult(): Result? { private fun takePendingResult(): Result? {
val result = pendingResult val result = pendingResult
pendingResult = null pendingResult = null
pendingArguments = null pendingArguments = null
return result return result
} }
private val fdMap = mutableMapOf<Int, ParcelFileDescriptor>()
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "saf_util") channel = MethodChannel(flutterPluginBinding.binaryMessenger, "saf_util")
@@ -94,7 +96,10 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
activity = null activity = null
} }
override fun onMethodCall(call: MethodCall, result: Result) { override fun onMethodCall(
call: MethodCall,
result: Result
) {
when (call.method) { when (call.method) {
"list" -> { "list" -> {
CoroutineScope(Dispatchers.IO).launch { 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 dir = documentFileFromUri(uri, true) ?: throw Exception("Failed to get DocumentFile from $uri")
val resolver = context.contentResolver val resolver = context.contentResolver
val mUri = dir.uri val mUri = dir.uri
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( val childrenUri =
mUri, DocumentsContract.buildChildDocumentsUriUsingTree(
DocumentsContract.getDocumentId(mUri) mUri,
) DocumentsContract.getDocumentId(mUri)
)
val results = mutableListOf<Map<String, Any?>>() val results = mutableListOf<Map<String, Any?>>()
cursor = resolver.query( cursor =
childrenUri, resolver.query(
arrayOf( childrenUri,
DocumentsContract.Document.COLUMN_DOCUMENT_ID, arrayOf(
DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_SIZE, DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_SIZE,
DocumentsContract.Document.COLUMN_LAST_MODIFIED DocumentsContract.Document.COLUMN_MIME_TYPE,
), DocumentsContract.Document.COLUMN_LAST_MODIFIED
null, ),
null, null,
null null,
) null
)
while (cursor?.moveToNext() == true) { while (cursor?.moveToNext() == true) {
val documentId = cursor.getString(0) val documentId = cursor.getString(0)
@@ -137,13 +144,14 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
val isDirectory = DocumentsContract.Document.MIME_TYPE_DIR == mimeType val isDirectory = DocumentsContract.Document.MIME_TYPE_DIR == mimeType
// Create a dictionary (map) for each file with its details // Create a dictionary (map) for each file with its details
val fileInfo = fileObjMap( val fileInfo =
documentUri, fileObjMap(
isDirectory, documentUri,
fileName, isDirectory,
fileSize, fileName,
lastModified, fileSize,
) lastModified
)
results.add(fileInfo) results.add(fileInfo)
} }
@@ -191,9 +199,22 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
try { try {
val uri = call.argument<String>("uri") as String val uri = call.argument<String>("uri") as String
val isDir = call.argument<Boolean>("isDir") val isDir = call.argument<Boolean>("isDir")
val throws = call.argument<Boolean>("throws") ?: false
val df = documentFileFromUri(uri, isDir) 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) { launch(Dispatchers.Main) {
result.success(null) result.success(null)
} }
@@ -258,7 +279,8 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
val uri = call.argument<String>("uri") as String val uri = call.argument<String>("uri") as String
val df = documentFileFromUri(uri, false) ?: throw Exception("Failed to get DocumentFile from $uri") 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 val fdInt = fd.fd
fdMap[fdInt] = 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. // 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. // In this case, we need to find the directory with the correct name again.
val findRes2 = findDirectChild(curDocument.uri, curName) val findRes2 = findDirectChild(curDocument.uri, curName)
nextDocument = if (findRes2 != null) { nextDocument =
documentFileFromUriObj(findRes2.uri, findRes2.isDir) if (findRes2 != null) {
} else { documentFileFromUriObj(findRes2.uri, findRes2.isDir)
null } else {
} null
}
} else { } else {
nextDocument = createRes nextDocument = createRes
} }
@@ -355,7 +378,9 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
} }
return@launch 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) { launch(Dispatchers.Main) {
@@ -386,10 +411,12 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
result.success(documentFileToMap(df)) result.success(documentFileToMap(df))
} }
} else { } else {
val newUri = renameFileDocumentFile(df, newName) val newUri =
?: throw Exception("Failed to rename to $newName") renameFileDocumentFile(df, newName)
val newDF = documentFileFromUriObj(newUri, false) ?: throw Exception("Failed to rename to $newName")
?: throw Exception("Failed to get DocumentFile from $newUri") val newDF =
documentFileFromUriObj(newUri, false)
?: throw Exception("Failed to get DocumentFile from $newUri")
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
result.success(documentFileToMap(newDF)) result.success(documentFileToMap(newDF))
} }
@@ -414,12 +441,13 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
val parentUriObj = parentUri.toUri() val parentUriObj = parentUri.toUri()
val newParentUriObj = newParentUri.toUri() val newParentUriObj = newParentUri.toUri()
val resUri = DocumentsContract.moveDocument( val resUri =
context.contentResolver, DocumentsContract.moveDocument(
uriObj, context.contentResolver,
parentUriObj, uriObj,
newParentUriObj parentUriObj,
) ?: throw Exception("Failed to move document") newParentUriObj
) ?: throw Exception("Failed to move document")
val resultDF = documentFileFromUriObj(resUri, isDir) ?: throw Exception("Failed to get DocumentFile from $resUri") val resultDF = documentFileFromUriObj(resUri, isDir) ?: throw Exception("Failed to get DocumentFile from $resUri")
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
@@ -443,11 +471,12 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
val uriObj = uri.toUri() val uriObj = uri.toUri()
val newParentUriObj = newParentUri.toUri() val newParentUriObj = newParentUri.toUri()
val resUri = DocumentsContract.copyDocument( val resUri =
context.contentResolver, DocumentsContract.copyDocument(
uriObj, context.contentResolver,
newParentUriObj uriObj,
) ?: throw Exception("Failed to move document") newParentUriObj
) ?: throw Exception("Failed to move document")
val resultDF = documentFileFromUriObj(resUri, isDir) ?: throw Exception("Failed to get DocumentFile from $resUri") val resultDF = documentFileFromUriObj(resUri, isDir) ?: throw Exception("Failed to get DocumentFile from $resUri")
@@ -492,14 +521,15 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
) )
} }
intent.addFlags( intent.addFlags(
if (writePermission) Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION if (writePermission) {
else Intent.FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
} else {
Intent.FLAG_GRANT_READ_URI_PERMISSION
}
) )
activity?.startActivityForResult(intent, requestCodeOpenDocumentTree) activity?.startActivityForResult(intent, requestCodeOpenDocumentTree)
} catch (err: Exception) { } 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() takePendingResult()
result.error("PluginError", err.message, null) result.error("PluginError", err.message, null)
} }
@@ -508,7 +538,7 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
"pickFiles" -> { "pickFiles" -> {
try { try {
val initialUri = call.argument<String>("initialUri") 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() val mimeTypes = call.argument<ArrayList<String>>("mimeTypes") ?: arrayListOf()
if (activity == null) { if (activity == null) {
@@ -544,8 +574,53 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
activity?.startActivityForResult(intent, requestCodeOpenFiles) activity?.startActivityForResult(intent, requestCodeOpenFiles)
} catch (err: Exception) { } catch (err: Exception) {
// Launch failed after pendingResult was set: drop the pending state takePendingResult()
// so a later activity result can't reply to this Result again. 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() takePendingResult()
result.error("PluginError", err.message, null) result.error("PluginError", err.message, null)
} }
@@ -558,12 +633,13 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
val checkRead = call.argument<Boolean>("checkRead") ?: true val checkRead = call.argument<Boolean>("checkRead") ?: true
val checkWrite = call.argument<Boolean>("checkWrite") ?: false val checkWrite = call.argument<Boolean>("checkWrite") ?: false
val persisted = hasPersistedUriPermission( val persisted =
context, hasPersistedUriPermission(
uri.toUri(), context,
checkRead, uri.toUri(),
checkWrite checkRead,
) checkWrite
)
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
result.success(persisted) result.success(persisted)
} }
@@ -584,10 +660,15 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
context.contentResolver.releasePersistableUriPermission( context.contentResolver.releasePersistableUriPermission(
uri.toUri(), uri.toUri(),
if (read && write) Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION if (read && write) {
else if (read) Intent.FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
else if (write) Intent.FLAG_GRANT_WRITE_URI_PERMISSION } else if (read) {
else 0 Intent.FLAG_GRANT_READ_URI_PERMISSION
} else if (write) {
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
} else {
0
}
) )
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
result.success(null) result.success(null)
@@ -626,26 +707,28 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
if (mime.startsWith("video/")) { if (mime.startsWith("video/")) {
val mmr = MediaMetadataRetriever() val mmr = MediaMetadataRetriever()
mmr.setDataSource(context, uri) mmr.setDataSource(context, uri)
bitmap = if (Build.VERSION.SDK_INT >= 27) { bitmap =
mmr.getScaledFrameAtTime(-1, OPTION_CLOSEST_SYNC, width, height) if (Build.VERSION.SDK_INT >= 27) {
} else { mmr.getScaledFrameAtTime(-1, OPTION_CLOSEST_SYNC, width, height)
mmr.frameAtTime } else {
} mmr.frameAtTime
}
} else { } else {
// Use DocumentsContract for other files. // Use DocumentsContract for other files.
bitmap = DocumentsContract.getDocumentThumbnail( bitmap =
context.contentResolver, DocumentsContract.getDocumentThumbnail(
uri, context.contentResolver,
Point(width, height), uri,
null Point(width, height),
) null
)
} }
if (bitmap != null) { if (bitmap != null) {
File(dest).writeBitmap( File(dest).writeBitmap(
bitmap, bitmap,
if (isPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG, if (isPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG,
quality, quality
) )
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
result.success(true) 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 // Handle folder/file/media picker results; unrelated request codes are not ours.
// request code was ours — unrelated request codes must not touch (let private fun onActivityResult(
// alone answer) the pending picker state. requestCode: Int,
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { resultCode: Int,
if (requestCode != requestCodeOpenDocumentTree && requestCode != requestCodeOpenFiles) { data: Intent?
): Boolean {
if (
requestCode != requestCodeOpenDocumentTree &&
requestCode != requestCodeOpenFiles &&
requestCode != requestCodePickMedia
) {
return false 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 args = pendingArguments
val result = takePendingResult() ?: return true val result = takePendingResult() ?: return true
try { try {
if (requestCode == requestCodeOpenDocumentTree) { if (requestCode == requestCodeOpenDocumentTree) {
// Handle the result of the folder picker.
if (resultCode == Activity.RESULT_OK && data != null) { if (resultCode == Activity.RESULT_OK && data != null) {
val uri: Uri? = data.data val uri: Uri? = data.data
if (uri != null && args is PendingDirArguments) { if (uri != null && args is PendingDirArguments && args.persistablePermission) {
if (args.persistablePermission) { context.contentResolver.takePersistableUriPermission(
context.contentResolver.takePersistableUriPermission( uri,
uri, if (args.writePermission) {
if (args.writePermission) Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
else Intent.FLAG_GRANT_READ_URI_PERMISSION } else {
) Intent.FLAG_GRANT_READ_URI_PERMISSION
} }
)
} }
val df = documentFileFromUri(uri.toString(), true) val df = documentFileFromUri(uri.toString(), true)
result.success( result.success(if (df != null) documentFileToMap(df) else null)
if (df != null) documentFileToMap(df)
else null
)
} else { } else {
result.success(null) result.success(null)
} }
} else { } else {
// Handle the result of file picker.
if (resultCode == Activity.RESULT_OK && data != null) { if (resultCode == Activity.RESULT_OK && data != null) {
val uris: List<Uri> = if (data.clipData != null) { val allowMultiple =
val clipData = data.clipData if (requestCode == requestCodePickMedia) {
val uris = mutableListOf<Uri>() (args as? PendingMediaArguments)?.multiple ?: false
for (i in 0 until clipData!!.itemCount) { } else {
uris.add(clipData.getItemAt(i).uri) 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() val documentFileMaps: MutableList<Map<String, Any?>> = mutableListOf()
for (uri in uris) { for (uri in uris) {
val df = documentFileFromUri(uri.toString(), false) val df = documentFileFromUri(uri.toString(), false)
if (df != null) { if (df != null) documentFileMaps.add(documentFileToMap(df))
documentFileMaps.add(documentFileToMap(df))
}
} }
result.success(documentFileMaps)
result.success(documentFileMaps) // Return the URIs to Flutter
} else { } else {
result.success(null) result.success(null)
} }
@@ -731,8 +817,7 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
try { try {
result.error("PluginError", err.message, null) result.error("PluginError", err.message, null)
} catch (_: IllegalStateException) { } catch (_: IllegalStateException) {
// Reply already submitted — never crash the host activity over a // A duplicate native delivery must not crash the host Activity.
// picker teardown race.
} }
} }
return true return true
@@ -742,19 +827,26 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
channel.setMethodCallHandler(null) channel.setMethodCallHandler(null)
} }
private fun documentFileFromUri(uri: String, isDir: Boolean?): DocumentFile? { private fun documentFileFromUri(
uri: String,
isDir: Boolean?
): DocumentFile? {
val uriObj = uri.toUri() val uriObj = uri.toUri()
val isDirRes = val isDirRes =
isDir ?: DocumentsContract.isTreeUri(uriObj) isDir ?: DocumentsContract.isTreeUri(uriObj)
return documentFileFromUriObj(uriObj, isDirRes) return documentFileFromUriObj(uriObj, isDirRes)
} }
private fun documentFileFromUriObj(uriObj: Uri, isDir: Boolean): DocumentFile? { private fun documentFileFromUriObj(
val res = if (isDir) { uriObj: Uri,
DocumentFile.fromTreeUri(context, uriObj) isDir: Boolean
} else { ): DocumentFile? {
DocumentFile.fromSingleUri(context, uriObj) val res =
} if (isDir) {
DocumentFile.fromTreeUri(context, uriObj)
} else {
DocumentFile.fromSingleUri(context, uriObj)
}
return res return res
} }
@@ -775,31 +867,39 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
return false return false
} }
private fun areSameDocumentLocation(treeUri: Uri, docUri: Uri): Boolean { private fun areSameDocumentLocation(
treeUri: Uri,
docUri: Uri
): Boolean {
val treeDocId = DocumentsContract.getTreeDocumentId(treeUri) val treeDocId = DocumentsContract.getTreeDocumentId(treeUri)
val docId = DocumentsContract.getDocumentId(docUri) val docId = DocumentsContract.getDocumentId(docUri)
return treeDocId == docId return treeDocId == docId
} }
private fun findDirectChild(parentUri: Uri, name: String): UriInfo? { private fun findDirectChild(
parentUri: Uri,
name: String
): UriInfo? {
var cursor: Cursor? = null var cursor: Cursor? = null
try { try {
val resolver = context.contentResolver val resolver = context.contentResolver
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( val childrenUri =
parentUri, DocumentsContract.buildChildDocumentsUriUsingTree(
DocumentsContract.getDocumentId(parentUri) parentUri,
) DocumentsContract.getDocumentId(parentUri)
cursor = resolver.query( )
childrenUri, cursor =
arrayOf( resolver.query(
DocumentsContract.Document.COLUMN_DOCUMENT_ID, childrenUri,
DocumentsContract.Document.COLUMN_DISPLAY_NAME, arrayOf(
DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_DOCUMENT_ID,
), DocumentsContract.Document.COLUMN_DISPLAY_NAME,
null, DocumentsContract.Document.COLUMN_MIME_TYPE
null, ),
null null,
) null,
null
)
while (cursor?.moveToNext() == true) { while (cursor?.moveToNext() == true) {
val documentId = cursor.getString(0) 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 // https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:documentfile/documentfile/src/main/java/androidx/documentfile/provider/TreeDocumentFile.java
try { try {
val result = DocumentsContract.renameDocument( val result =
context.contentResolver, df.uri, newName DocumentsContract.renameDocument(
) context.contentResolver,
df.uri,
newName
)
return result return result
} catch (err: Exception) { } catch (err: Exception) {
return null return null
} }
} }
private fun documentFileToMap(file: DocumentFile): Map<String, Any?> { private fun documentFileToMap(file: DocumentFile): Map<String, Any?> = fileObjMap(
return fileObjMap( file.uri,
file.uri, file.isDirectory,
file.isDirectory, file.name ?: "",
file.name ?: "", file.length(),
file.length(), file.lastModified()
file.lastModified(), )
)
}
private fun fileObjMap( private fun fileObjMap(
uri: Uri, uri: Uri,
isDir: Boolean, isDir: Boolean,
name: String, name: String,
length: Long, length: Long,
lastMod: Long, lastMod: Long
): Map<String, Any?> { ): Map<String, Any?> = mapOf(
return mapOf( "uri" to uri.toString(),
"uri" to uri.toString(), "isDir" to isDir,
"isDir" to isDir, "name" to name,
"name" to name, "length" to length,
"length" to length, "lastModified" to lastMod
"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 -> outputStream().use { out ->
bitmap.compress(format, quality, out) bitmap.compress(format, quality, out)
out.flush() 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 open class PendingArguments
internal class PendingDirArguments( internal class PendingDirArguments(
val writePermission: Boolean, val writePermission: Boolean,
val persistablePermission: Boolean, val persistablePermission: Boolean
): PendingArguments() ) : 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.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.PluginRegistry 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.ArgumentCaptor
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoInteractions
import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when` import org.mockito.Mockito.`when`
import kotlin.test.assertEquals
import kotlin.test.Test
internal class SafUtilPluginTest { internal class SafUtilPluginTest {
@Test @Test
fun onMethodCall_unknownMethod_returnsNotImplemented() { fun onMethodCall_unknownMethod_returnsNotImplemented() {
val plugin = SafUtilPlugin() val plugin = SafUtilPlugin()
val result = mock(MethodChannel.Result::class.java) val result = mock(MethodChannel.Result::class.java)
plugin.onMethodCall(MethodCall("unknown", null), result) plugin.onMethodCall(MethodCall("unknown", null), result)
verify(result).notImplemented() verify(result).notImplemented()
verifyNoMoreInteractions(result) verifyNoMoreInteractions(result)
} }
@Test @Test
fun pickDirectory_withoutActivity_returnsNoActivityError() { fun pickDirectory_withoutActivity_returnsNoActivityError() {
val plugin = SafUtilPlugin() val plugin = SafUtilPlugin()
val result = mock(MethodChannel.Result::class.java) val result = mock(MethodChannel.Result::class.java)
plugin.onMethodCall(MethodCall("pickDirectory", null), result) plugin.onMethodCall(MethodCall("pickDirectory", null), result)
verify(result).error("NO_ACTIVITY", "Activity is null", null) verify(result).error("NO_ACTIVITY", "Activity is null", null)
verifyNoMoreInteractions(result) verifyNoMoreInteractions(result)
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@Test @Test
fun pickDirectory_afterConfigChange_reattachesListenerAndClearsPendingResult() { fun unrelatedActivityResultDoesNotConsumeOrAnswerPendingPicker() {
val plugin = SafUtilPlugin() val plugin = SafUtilPlugin()
val firstActivity = RecordingActivity() val activity = RecordingActivity()
val firstBinding = mock(ActivityPluginBinding::class.java) val binding = mock(ActivityPluginBinding::class.java)
`when`(firstBinding.activity).thenReturn(firstActivity) `when`(binding.activity).thenReturn(activity)
plugin.onAttachedToActivity(binding)
plugin.onAttachedToActivity(firstBinding)
val listenerCaptor = ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener::class.java)
val firstListenerCaptor = ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener::class.java) verify(binding).addActivityResultListener(listenerCaptor.capture())
verify(firstBinding).addActivityResultListener(firstListenerCaptor.capture()) val result = mock(MethodChannel.Result::class.java)
plugin.onMethodCall(MethodCall("pickDirectory", null), result)
val firstResult = mock(MethodChannel.Result::class.java)
plugin.onMethodCall(MethodCall("pickDirectory", null), firstResult) assertFalse(listenerCaptor.value.onActivityResult(9999, Activity.RESULT_CANCELED, null))
assertEquals(listOf(1001), firstActivity.startedRequestCodes) verifyNoInteractions(result)
assertTrue(listenerCaptor.value.onActivityResult(1001, Activity.RESULT_CANCELED, null))
val secondResult = mock(MethodChannel.Result::class.java) verify(result).success(null)
plugin.onMethodCall(MethodCall("pickDirectory", null), secondResult)
verify(secondResult).error("ALREADY_PICKING", "Another picker process is already in progress", null) assertTrue(listenerCaptor.value.onActivityResult(1001, Activity.RESULT_CANCELED, null))
verifyNoMoreInteractions(result)
plugin.onDetachedFromActivityForConfigChanges() }
verify(firstBinding).removeActivityResultListener(firstListenerCaptor.value)
@Suppress("DEPRECATION")
val secondActivity = RecordingActivity() @Test
val secondBinding = mock(ActivityPluginBinding::class.java) fun pickDirectory_afterConfigChange_reattachesListenerAndClearsPendingResult() {
`when`(secondBinding.activity).thenReturn(secondActivity) val plugin = SafUtilPlugin()
val firstActivity = RecordingActivity()
plugin.onReattachedToActivityForConfigChanges(secondBinding) val firstBinding = mock(ActivityPluginBinding::class.java)
`when`(firstBinding.activity).thenReturn(firstActivity)
val secondListenerCaptor = ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener::class.java)
verify(secondBinding).addActivityResultListener(secondListenerCaptor.capture()) plugin.onAttachedToActivity(firstBinding)
secondListenerCaptor.value.onActivityResult(1001, Activity.RESULT_CANCELED, null) val firstListenerCaptor = ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener::class.java)
verify(firstResult).success(null) verify(firstBinding).addActivityResultListener(firstListenerCaptor.capture())
val thirdResult = mock(MethodChannel.Result::class.java) val firstResult = mock(MethodChannel.Result::class.java)
plugin.onMethodCall(MethodCall("pickDirectory", null), thirdResult) plugin.onMethodCall(MethodCall("pickDirectory", null), firstResult)
assertEquals(listOf(1001), secondActivity.startedRequestCodes) assertEquals(listOf(1001), firstActivity.startedRequestCodes)
}
val secondResult = mock(MethodChannel.Result::class.java)
private class RecordingActivity : Activity() { plugin.onMethodCall(MethodCall("pickDirectory", null), secondResult)
val startedRequestCodes = mutableListOf<Int>() verify(secondResult).error("ALREADY_PICKING", "Another picker process is already in progress", null)
@Deprecated("Deprecated in Android") plugin.onDetachedFromActivityForConfigChanges()
override fun startActivityForResult(intent: Intent?, requestCode: Int) { verify(firstBinding).removeActivityResultListener(firstListenerCaptor.value)
startedRequestCodes.add(requestCode)
} 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. /// [initialUri] is the initial URI to show in the dialog.
/// [writePermission] is true if the folder should have write permission. /// [writePermission] is true if the folder should have write permission.
/// [persistablePermission] is true if the permission should be persistable. /// [persistablePermission] is true if the permission should be persistable.
Future<SafDocumentFile?> pickDirectory( Future<SafDocumentFile?> pickDirectory({
{String? initialUri, String? initialUri,
bool? writePermission, bool? writePermission,
bool? persistablePermission}) { bool? persistablePermission,
}) {
return SafUtilPlatform.instance.pickDirectory( return SafUtilPlatform.instance.pickDirectory(
initialUri: initialUri, initialUri: initialUri,
writePermission: writePermission, writePermission: writePermission,
persistablePermission: persistablePermission); persistablePermission: persistablePermission,
);
} }
/// Shows a file picker dialog and returns the selected file [SafDocumentFile]. /// Shows a file picker dialog and returns the selected file [SafDocumentFile].
@@ -39,7 +41,7 @@ class SafUtil {
Future<List<SafDocumentFile>?> pickFiles({ Future<List<SafDocumentFile>?> pickFiles({
String? initialUri, String? initialUri,
List<String>? mimeTypes, List<String>? mimeTypes,
multiple = true, bool multiple = true,
}) { }) {
return SafUtilPlatform.instance.pickFiles( return SafUtilPlatform.instance.pickFiles(
initialUri: initialUri, 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. /// Lists the contents of the specified directory URI.
/// Returns a list of [SafDocumentFile] objects. /// Returns a list of [SafDocumentFile] objects.
/// ///
@@ -72,8 +88,9 @@ class SafUtil {
/// [uri] is the URI of the file or directory. /// [uri] is the URI of the file or directory.
/// [isDir] is true if the URI is a directory. [null] means /// [isDir] is true if the URI is a directory. [null] means
/// auto-detect. /// auto-detect.
Future<SafDocumentFile?> stat(String uri, bool? isDir) { /// [throws] when true, throws an exception if the URI does not exist or is inaccessible.
return SafUtilPlatform.instance.stat(uri, isDir); 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. /// Checks if the specified file or directory exists.
@@ -126,7 +143,11 @@ class SafUtil {
/// [parentUri] is the URI of the current parent directory. /// [parentUri] is the URI of the current parent directory.
/// [newParentUri] is the URI of the new parent directory. /// [newParentUri] is the URI of the new parent directory.
Future<SafDocumentFile> moveTo( 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); return SafUtilPlatform.instance.moveTo(uri, isDir, parentUri, newParentUri);
} }
@@ -178,14 +199,16 @@ class SafUtil {
/// [writePermission] is true if the folder should have write permission. /// [writePermission] is true if the folder should have write permission.
/// [persistablePermission] is true if the permission should be persistable. /// [persistablePermission] is true if the permission should be persistable.
@Deprecated('Use [pickDirectory] instead, which returns a [SafDocumentFile].') @Deprecated('Use [pickDirectory] instead, which returns a [SafDocumentFile].')
Future<String?> openDirectory( Future<String?> openDirectory({
{String? initialUri, String? initialUri,
bool? writePermission, bool? writePermission,
bool? persistablePermission}) { bool? persistablePermission,
}) {
return SafUtilPlatform.instance.openDirectory( return SafUtilPlatform.instance.openDirectory(
initialUri: initialUri, initialUri: initialUri,
writePermission: writePermission, writePermission: writePermission,
persistablePermission: persistablePermission); persistablePermission: persistablePermission,
);
} }
/// Shows a file picker dialog and returns the selected file URI. /// 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. /// [initialUri] is the initial URI to show in the dialog.
/// [mimeTypes] is a list of MIME types to filter the files. /// [mimeTypes] is a list of MIME types to filter the files.
@Deprecated('Use [pickFile] instead, which returns a [SafDocumentFile].') @Deprecated('Use [pickFile] instead, which returns a [SafDocumentFile].')
Future<String?> openFile({ Future<String?> openFile({String? initialUri, List<String>? mimeTypes}) {
String? initialUri,
List<String>? mimeTypes,
}) {
return SafUtilPlatform.instance.openFile( return SafUtilPlatform.instance.openFile(
initialUri: initialUri, initialUri: initialUri,
mimeTypes: mimeTypes, mimeTypes: mimeTypes,
@@ -212,7 +232,7 @@ class SafUtil {
Future<List<String>?> openFiles({ Future<List<String>?> openFiles({
String? initialUri, String? initialUri,
List<String>? mimeTypes, List<String>? mimeTypes,
multiple = true, bool multiple = true,
}) { }) {
return SafUtilPlatform.instance.openFiles( return SafUtilPlatform.instance.openFiles(
initialUri: initialUri, initialUri: initialUri,
@@ -240,17 +260,26 @@ class SafUtil {
bool checkRead = true, bool checkRead = true,
bool checkWrite = false, bool checkWrite = false,
}) { }) {
return SafUtilPlatform.instance.hasPersistedPermission(uri, return SafUtilPlatform.instance.hasPersistedPermission(
checkRead: checkRead, checkWrite: checkWrite); uri,
checkRead: checkRead,
checkWrite: checkWrite,
);
} }
/// Releases the persisted permission of the specified URI. /// Releases the persisted permission of the specified URI.
/// Use [read] and [write] to specify the type of permission to release. /// Use [read] and [write] to specify the type of permission to release.
/// [read] defaults to true. /// [read] defaults to true.
/// [write] defaults to false. /// [write] defaults to false.
Future<void> releasePersistedPermission(String uri, Future<void> releasePersistedPermission(
{bool read = true, bool write = false}) async { String uri, {
return SafUtilPlatform.instance bool read = true,
.releasePersistedPermission(uri, read: read, write: write); 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'); final methodChannel = const MethodChannel('saf_util');
@override @override
Future<SafDocumentFile?> pickDirectory( Future<SafDocumentFile?> pickDirectory({
{String? initialUri, String? initialUri,
bool? writePermission, bool? writePermission,
bool? persistablePermission}) async { bool? persistablePermission,
final map = }) async {
await methodChannel.invokeMapMethod<String, dynamic>('pickDirectory', { final map = await methodChannel
'initialUri': initialUri, .invokeMapMethod<String, dynamic>('pickDirectory', {
'writePermission': writePermission, 'initialUri': initialUri,
'persistablePermission': persistablePermission, 'writePermission': writePermission,
}); 'persistablePermission': persistablePermission,
});
if (map == null) { if (map == null) {
return null; return null;
} }
@@ -27,10 +28,11 @@ class MethodChannelSafUtil extends SafUtilPlatform {
} }
@override @override
Future<String?> openDirectory( Future<String?> openDirectory({
{String? initialUri, String? initialUri,
bool? writePermission, bool? writePermission,
bool? persistablePermission}) async { bool? persistablePermission,
}) async {
final res = await pickDirectory( final res = await pickDirectory(
initialUri: initialUri, initialUri: initialUri,
writePermission: writePermission, writePermission: writePermission,
@@ -57,10 +59,7 @@ class MethodChannelSafUtil extends SafUtilPlatform {
String? initialUri, String? initialUri,
List<String>? mimeTypes, List<String>? mimeTypes,
}) async { }) async {
final res = await pickFile( final res = await pickFile(initialUri: initialUri, mimeTypes: mimeTypes);
initialUri: initialUri,
mimeTypes: mimeTypes,
);
return res?.uri; return res?.uri;
} }
@@ -68,14 +67,24 @@ class MethodChannelSafUtil extends SafUtilPlatform {
Future<List<SafDocumentFile>?> pickFiles({ Future<List<SafDocumentFile>?> pickFiles({
String? initialUri, String? initialUri,
List<String>? mimeTypes, List<String>? mimeTypes,
multiple = true, bool multiple = true,
}) async { }) async {
final maps = await methodChannel final maps = await methodChannel.invokeListMethod<Map<dynamic, dynamic>>(
.invokeListMethod<Map<dynamic, dynamic>>('pickFiles', { 'pickFiles',
'initialUri': initialUri, {'initialUri': initialUri, 'mimeTypes': mimeTypes, 'multiple': multiple},
'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(); return maps?.map((map) => SafDocumentFile.fromMap(map)).toList();
} }
@@ -83,7 +92,7 @@ class MethodChannelSafUtil extends SafUtilPlatform {
Future<List<String>?> openFiles({ Future<List<String>?> openFiles({
String? initialUri, String? initialUri,
List<String>? mimeTypes, List<String>? mimeTypes,
multiple = true, bool multiple = true,
}) async { }) async {
final res = await pickFiles( final res = await pickFiles(
initialUri: initialUri, initialUri: initialUri,
@@ -95,8 +104,10 @@ class MethodChannelSafUtil extends SafUtilPlatform {
@override @override
Future<List<SafDocumentFile>> list(String uri) async { Future<List<SafDocumentFile>> list(String uri) async {
final maps = await methodChannel final maps = await methodChannel.invokeListMethod<Map<dynamic, dynamic>>(
.invokeListMethod<Map<dynamic, dynamic>>('list', {'uri': uri}); 'list',
{'uri': uri},
);
return (maps ?? []).map((map) => SafDocumentFile.fromMap(map)).toList(); return (maps ?? []).map((map) => SafDocumentFile.fromMap(map)).toList();
} }
@@ -113,11 +124,12 @@ class MethodChannelSafUtil extends SafUtilPlatform {
} }
@override @override
Future<SafDocumentFile?> stat(String uri, bool? isDir) async { Future<SafDocumentFile?> stat(String uri, bool? isDir, {bool? throws}) async {
final map = await methodChannel.invokeMapMethod<String, dynamic>( final map = await methodChannel.invokeMapMethod<String, dynamic>('stat', {
'stat', 'uri': uri,
{'uri': uri, 'isDir': isDir}, 'isDir': isDir,
); 'throws': throws,
});
if (map == null) { if (map == null) {
return null; return null;
} }
@@ -126,10 +138,10 @@ class MethodChannelSafUtil extends SafUtilPlatform {
@override @override
Future<bool> exists(String uri, bool isDir) async { Future<bool> exists(String uri, bool isDir) async {
final res = await methodChannel.invokeMethod<bool>( final res = await methodChannel.invokeMethod<bool>('exists', {
'exists', 'uri': uri,
{'uri': uri, 'isDir': isDir}, 'isDir': isDir,
); });
if (res == null) { if (res == null) {
throw Exception('Failed to check if file exists: $uri'); throw Exception('Failed to check if file exists: $uri');
} }
@@ -138,10 +150,10 @@ class MethodChannelSafUtil extends SafUtilPlatform {
@override @override
Future<void> delete(String uri, bool isDir) async { Future<void> delete(String uri, bool isDir) async {
final res = await methodChannel.invokeMethod<bool>( final res = await methodChannel.invokeMethod<bool>('delete', {
'delete', 'uri': uri,
{'uri': uri, 'isDir': isDir}, 'isDir': isDir,
); });
if (res != true) { if (res != true) {
throw Exception('Failed to delete file: $uri'); throw Exception('Failed to delete file: $uri');
} }
@@ -149,10 +161,10 @@ class MethodChannelSafUtil extends SafUtilPlatform {
@override @override
Future<SafDocumentFile> mkdirp(String uri, List<String> names) async { Future<SafDocumentFile> mkdirp(String uri, List<String> names) async {
final map = await methodChannel.invokeMapMethod<String, dynamic>( final map = await methodChannel.invokeMapMethod<String, dynamic>('mkdirp', {
'mkdirp', 'uri': uri,
{'uri': uri, 'names': names}, 'names': names,
); });
if (map == null) { if (map == null) {
throw Exception('Failed to create directory: $uri'); throw Exception('Failed to create directory: $uri');
} }
@@ -161,10 +173,10 @@ class MethodChannelSafUtil extends SafUtilPlatform {
@override @override
Future<SafDocumentFile?> child(String uri, List<String> names) async { Future<SafDocumentFile?> child(String uri, List<String> names) async {
final map = await methodChannel.invokeMapMethod<String, dynamic>( final map = await methodChannel.invokeMapMethod<String, dynamic>('child', {
'child', 'uri': uri,
{'uri': uri, 'names': names}, 'names': names,
); });
if (map == null) { if (map == null) {
return null; return null;
} }
@@ -173,10 +185,11 @@ class MethodChannelSafUtil extends SafUtilPlatform {
@override @override
Future<SafDocumentFile> rename(String uri, bool isDir, String newName) async { Future<SafDocumentFile> rename(String uri, bool isDir, String newName) async {
final map = await methodChannel.invokeMapMethod<String, dynamic>( final map = await methodChannel.invokeMapMethod<String, dynamic>('rename', {
'rename', 'uri': uri,
{'uri': uri, 'isDir': isDir, 'newName': newName}, 'isDir': isDir,
); 'newName': newName,
});
if (map == null) { if (map == null) {
throw Exception('Failed to rename: $uri'); throw Exception('Failed to rename: $uri');
} }
@@ -185,16 +198,17 @@ class MethodChannelSafUtil extends SafUtilPlatform {
@override @override
Future<SafDocumentFile> moveTo( Future<SafDocumentFile> moveTo(
String uri, bool isDir, String parentUri, String newParentUri) async { String uri,
final map = await methodChannel.invokeMapMethod<String, dynamic>( bool isDir,
'moveTo', String parentUri,
{ String newParentUri,
'uri': uri, ) async {
'isDir': isDir, final map = await methodChannel.invokeMapMethod<String, dynamic>('moveTo', {
'parentUri': parentUri, 'uri': uri,
'newParentUri': newParentUri 'isDir': isDir,
}, 'parentUri': parentUri,
); 'newParentUri': newParentUri,
});
if (map == null) { if (map == null) {
throw Exception('Failed to move: $uri'); throw Exception('Failed to move: $uri');
} }
@@ -203,11 +217,15 @@ class MethodChannelSafUtil extends SafUtilPlatform {
@override @override
Future<SafDocumentFile> copyTo( Future<SafDocumentFile> copyTo(
String uri, bool isDir, String newParentUri) async { String uri,
final map = await methodChannel.invokeMapMethod<String, dynamic>( bool isDir,
'copyTo', String newParentUri,
{'uri': uri, 'isDir': isDir, 'newParentUri': newParentUri}, ) async {
); final map = await methodChannel.invokeMapMethod<String, dynamic>('copyTo', {
'uri': uri,
'isDir': isDir,
'newParentUri': newParentUri,
});
if (map == null) { if (map == null) {
throw Exception('Failed to copy: $uri'); throw Exception('Failed to copy: $uri');
} }
@@ -223,26 +241,22 @@ class MethodChannelSafUtil extends SafUtilPlatform {
String? format, String? format,
int? quality, int? quality,
}) async { }) async {
final res = await methodChannel.invokeMethod<bool>( final res = await methodChannel.invokeMethod<bool>('saveThumbnailToFile', {
'saveThumbnailToFile', 'uri': uri.toString(),
{ 'width': width,
'uri': uri.toString(), 'height': height,
'width': width, 'destPath': destPath,
'height': height, 'format': format,
'destPath': destPath, 'quality': quality,
'format': format, });
'quality': quality,
},
);
return res ?? false; return res ?? false;
} }
@override @override
Future<int> getFileDescriptor(String uri) async { Future<int> getFileDescriptor(String uri) async {
final res = await methodChannel.invokeMethod<int>( final res = await methodChannel.invokeMethod<int>('getFileDescriptor', {
'getFileDescriptor', 'uri': uri,
{'uri': uri}, });
);
if (res == null) { if (res == null) {
throw Exception('Failed to get file descriptor: $uri'); throw Exception('Failed to get file descriptor: $uri');
} }
@@ -251,10 +265,7 @@ class MethodChannelSafUtil extends SafUtilPlatform {
@override @override
Future<void> closeFileDescriptor(int fd) { Future<void> closeFileDescriptor(int fd) {
return methodChannel.invokeMethod<void>( return methodChannel.invokeMethod<void>('closeFileDescriptor', {'fd': fd});
'closeFileDescriptor',
{'fd': fd},
);
} }
@override @override
@@ -265,11 +276,7 @@ class MethodChannelSafUtil extends SafUtilPlatform {
}) async { }) async {
final res = await methodChannel.invokeMethod<bool>( final res = await methodChannel.invokeMethod<bool>(
'hasPersistedPermission', 'hasPersistedPermission',
{ {'uri': uri, 'checkRead': checkRead, 'checkWrite': checkWrite},
'uri': uri,
'checkRead': checkRead,
'checkWrite': checkWrite,
},
); );
if (res == null) { if (res == null) {
throw Exception('Failed to check persisted permission: $uri'); throw Exception('Failed to check persisted permission: $uri');
@@ -278,15 +285,15 @@ class MethodChannelSafUtil extends SafUtilPlatform {
} }
@override @override
Future<void> releasePersistedPermission(String uri, Future<void> releasePersistedPermission(
{bool read = true, bool write = false}) async { String uri, {
await methodChannel.invokeMethod<void>( bool read = true,
'releasePersistedPermission', bool write = false,
{ }) async {
'uri': uri, await methodChannel.invokeMethod<void>('releasePersistedPermission', {
'read': read, 'uri': uri,
'write': write, 'read': read,
}, 'write': write,
); });
} }
} }
@@ -19,11 +19,11 @@ class SafDocumentFile {
static SafDocumentFile fromMap(Map<dynamic, dynamic> map) { static SafDocumentFile fromMap(Map<dynamic, dynamic> map) {
return SafDocumentFile( return SafDocumentFile(
uri: map['uri'], uri: map['uri'] as String,
name: map['name'], name: map['name'] as String,
isDir: map['isDir'] ?? false, isDir: map['isDir'] as bool? ?? false,
length: map['length'] ?? 0, length: map['length'] as int? ?? -1,
lastModified: map['lastModified'] ?? 0, lastModified: map['lastModified'] as int? ?? 0,
); );
} }
@@ -54,17 +54,19 @@ abstract class SafUtilPlatform extends PlatformInterface {
_instance = instance; _instance = instance;
} }
Future<SafDocumentFile?> pickDirectory( Future<SafDocumentFile?> pickDirectory({
{String? initialUri, String? initialUri,
bool? writePermission, bool? writePermission,
bool? persistablePermission}) { bool? persistablePermission,
}) {
throw UnimplementedError('pickDirectory() has not been implemented.'); throw UnimplementedError('pickDirectory() has not been implemented.');
} }
Future<String?> openDirectory( Future<String?> openDirectory({
{String? initialUri, String? initialUri,
bool? writePermission, bool? writePermission,
bool? persistablePermission}) { bool? persistablePermission,
}) {
throw UnimplementedError('openDirectory() has not been implemented.'); throw UnimplementedError('openDirectory() has not been implemented.');
} }
@@ -75,25 +77,29 @@ abstract class SafUtilPlatform extends PlatformInterface {
throw UnimplementedError('pickFile() has not been implemented.'); throw UnimplementedError('pickFile() has not been implemented.');
} }
Future<String?> openFile({ Future<String?> openFile({String? initialUri, List<String>? mimeTypes}) {
String? initialUri,
List<String>? mimeTypes,
}) {
throw UnimplementedError('openFile() has not been implemented.'); throw UnimplementedError('openFile() has not been implemented.');
} }
Future<List<SafDocumentFile>?> pickFiles({ Future<List<SafDocumentFile>?> pickFiles({
String? initialUri, String? initialUri,
List<String>? mimeTypes, List<String>? mimeTypes,
multiple = true, bool multiple = true,
}) { }) {
throw UnimplementedError('pickFiles() has not been implemented.'); 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({ Future<List<String>?> openFiles({
String? initialUri, String? initialUri,
List<String>? mimeTypes, List<String>? mimeTypes,
multiple = true, bool multiple = true,
}) { }) {
throw UnimplementedError('openFiles() has not been implemented.'); throw UnimplementedError('openFiles() has not been implemented.');
} }
@@ -106,7 +112,7 @@ abstract class SafUtilPlatform extends PlatformInterface {
throw UnimplementedError('documentFileFromUri() has not been implemented.'); 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.'); throw UnimplementedError('stat() has not been implemented.');
} }
@@ -131,7 +137,11 @@ abstract class SafUtilPlatform extends PlatformInterface {
} }
Future<SafDocumentFile> moveTo( 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.'); throw UnimplementedError('moveTo() has not been implemented.');
} }
@@ -164,12 +174,17 @@ abstract class SafUtilPlatform extends PlatformInterface {
bool checkWrite = false, bool checkWrite = false,
}) { }) {
throw UnimplementedError( throw UnimplementedError(
'hasPersistedPermission() has not been implemented.'); 'hasPersistedPermission() has not been implemented.',
);
} }
Future<void> releasePersistedPermission(String uri, Future<void> releasePersistedPermission(
{bool read = true, bool write = false}) { String uri, {
bool read = true,
bool write = false,
}) {
throw UnimplementedError( throw UnimplementedError(
'releasePersistedPermission() has not been implemented.'); 'releasePersistedPermission() has not been implemented.',
);
} }
} }
+11 -3
View File
@@ -55,7 +55,7 @@ packages:
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: transitive
description: description:
name: flutter_lints name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
@@ -123,6 +123,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.18.0" 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: path:
dependency: transitive dependency: transitive
description: description:
@@ -209,5 +217,5 @@ packages:
source: hosted source: hosted
version: "15.2.0" version: "15.2.0"
sdks: sdks:
dart: ">=3.10.0-0 <4.0.0" dart: ">=3.12.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54" flutter: ">=3.44.0"
+4 -4
View File
@@ -1,11 +1,11 @@
name: saf_util name: saf_util
description: "Util functions for SAF (Storage Access Framework)." description: "Util functions for SAF (Storage Access Framework)."
version: 2.0.0 version: 3.1.0
homepage: https://github.com/flutter-cavalry/saf_util homepage: https://github.com/flutter-cavalry/saf_util
environment: environment:
sdk: ">=2.18.0 <4.0.0" sdk: ^3.12.0
flutter: ">=2.5.0" flutter: ">=3.44.0"
dependencies: dependencies:
flutter: flutter:
@@ -15,7 +15,7 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter 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 # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # 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
+25 -30
View File
@@ -242,11 +242,11 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "packages/connectivity_plus/connectivity_plus" path: "packages/connectivity_plus/connectivity_plus"
ref: "4c0d4071150481f76d753c6ce1ff325bb959b9b5" ref: bf04cdf66598dc3fca274b8b1db2b92b0bf6b73e
resolved-ref: "4c0d4071150481f76d753c6ce1ff325bb959b9b5" resolved-ref: bf04cdf66598dc3fca274b8b1db2b92b0bf6b73e
url: "https://github.com/edde746/plus_plugins" url: "https://github.com/fluttercommunity/plus_plugins"
source: git source: git
version: "7.1.1" version: "7.2.0"
connectivity_plus_platform_interface: connectivity_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -702,12 +702,11 @@ packages:
material_symbols_icons: material_symbols_icons:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." name: material_symbols_icons
ref: "1d8cd83" sha256: "49c532dd0b74544e9d8d93ec0f821d52ec532e7c9263c889ebe71b1be4f34ba7"
resolved-ref: "1d8cd8325db29691a01738a25f220dd3515bd1e2" url: "https://pub.dev"
url: "https://github.com/edde746/material_symbols_icons" source: hosted
source: git version: "4.2951.0"
version: "4.2906.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@@ -987,7 +986,7 @@ packages:
path: "packages/saf_util" path: "packages/saf_util"
relative: true relative: true
source: path source: path
version: "2.0.0" version: "3.1.0"
screen_retriever: screen_retriever:
dependency: transitive dependency: transitive
description: description:
@@ -1029,14 +1028,13 @@ packages:
source: hosted source: hosted
version: "0.2.0" version: "0.2.0"
sentry: sentry:
dependency: "direct overridden" dependency: transitive
description: description:
path: "packages/dart" name: sentry
ref: "build/fetch-native-zip" sha256: "09c573f98ff6c5e98d527f351f037eb8ea9ff684a6e9e84b2e91357144016254"
resolved-ref: "711f54f34d6cda9cccfb57519b9e3b72dd116b69" url: "https://pub.dev"
url: "https://github.com/edde746/sentry-dart" source: hosted
source: git version: "9.24.0"
version: "9.16.0"
sentry_dart_plugin: sentry_dart_plugin:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -1048,12 +1046,11 @@ packages:
sentry_flutter: sentry_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
path: "packages/flutter" name: sentry_flutter
ref: "build/fetch-native-zip" sha256: "4a04b7e1901b8128df783b6d36d48706b07b0cd055e621517c998de87f5dafb9"
resolved-ref: "711f54f34d6cda9cccfb57519b9e3b72dd116b69" url: "https://pub.dev"
url: "https://github.com/edde746/sentry-dart" source: hosted
source: git version: "9.24.0"
version: "9.16.0"
serial_csv: serial_csv:
dependency: transitive dependency: transitive
description: description:
@@ -1406,12 +1403,10 @@ packages:
wakelock_plus: wakelock_plus:
dependency: "direct main" dependency: "direct main"
description: description:
path: wakelock_plus path: "packages/wakelock_plus"
ref: "8595fee595c952b73d430f2eab05f2e0d858290e" relative: true
resolved-ref: "8595fee595c952b73d430f2eab05f2e0d858290e" source: path
url: "https://github.com/edde746/wakelock_plus" version: "1.5.2"
source: git
version: "1.4.0"
wakelock_plus_platform_interface: wakelock_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
+15 -28
View File
@@ -29,8 +29,8 @@ dependencies:
duration: ^4.0.3 duration: ^4.0.3
connectivity_plus: connectivity_plus:
git: git:
url: https://github.com/edde746/plus_plugins url: https://github.com/fluttercommunity/plus_plugins
ref: 4c0d4071150481f76d753c6ce1ff325bb959b9b5 ref: bf04cdf66598dc3fca274b8b1db2b92b0bf6b73e
path: packages/connectivity_plus/connectivity_plus path: packages/connectivity_plus/connectivity_plus
os_media_controls: os_media_controls:
git: git:
@@ -38,10 +38,7 @@ dependencies:
ref: 75556a968a4a1ebc42fbb8c31942c2418d4326e1 ref: 75556a968a4a1ebc42fbb8c31942c2418d4326e1
rate_limiter: ^1.0.0 rate_limiter: ^1.0.0
wakelock_plus: wakelock_plus:
git: path: packages/wakelock_plus
url: https://github.com/edde746/wakelock_plus
ref: 8595fee595c952b73d430f2eab05f2e0d858290e
path: wakelock_plus
path_provider: ^2.1.0 path_provider: ^2.1.0
path: ^1.9.0 path: ^1.9.0
universal_gamepad: ^1.5.7 universal_gamepad: ^1.5.7
@@ -51,23 +48,27 @@ dependencies:
cryptography: ^2.9.0 cryptography: ^2.9.0
file_picker: ^10.3.10 file_picker: ^10.3.10
saf_util: saf_util:
# Vendored 2.0.0 with a pendingResult lifecycle fix (see its CHANGELOG) — # Vendored upstream 3.1.0 with a pending Result lifecycle fix (CHANGELOG) —
# upstream double-replies MethodChannel results and crashes the activity. # upstream still double-replies MethodChannel results and crashes the Activity.
path: packages/saf_util path: packages/saf_util
saf_stream: ^2.0.0 saf_stream: ^2.0.0
material_symbols_icons: ^4.2906.0 material_symbols_icons: ^4.2951.0
web_socket_channel: ^3.0.1 web_socket_channel: ^3.0.1
dart_discord_presence: ^1.2.0 dart_discord_presence: ^1.2.0
flutter_svg: ^2.2.3 flutter_svg: ^2.2.3
# Fork note: 41c16be creates the desktop temp parent before opening the
# partial file, independently fixing upstream #649's missing-directory case.
# Upstream's later target-directory fallback is unnecessary while this fork
# retains its same-filesystem move and resume-data cleanup implementation.
# No per-host trust bypass is carried: Plezy has no app-wide certificate
# exception policy, and weakening only background downloads would make API
# and playback behavior inconsistent. Revisit only with an explicit,
# host-scoped trust setting shared by every media-server HTTP client.
background_downloader: background_downloader:
git: git:
url: https://github.com/edde746/background_downloader url: https://github.com/edde746/background_downloader
ref: 021e2075260e324b7440a5c81b57bf0242d35020 ref: 021e2075260e324b7440a5c81b57bf0242d35020
sentry_flutter: sentry_flutter: ^9.24.0
git:
url: https://github.com/edde746/sentry-dart
path: packages/flutter
ref: build/fetch-native-zip
auto_updater: auto_updater:
git: git:
url: https://github.com/edde746/auto_updater url: https://github.com/edde746/auto_updater
@@ -113,20 +114,6 @@ dependency_overrides:
url: https://github.com/edde746/auto_updater url: https://github.com/edde746/auto_updater
ref: 9e150f7 ref: 9e150f7
path: packages/auto_updater_windows path: packages/auto_updater_windows
sentry:
git:
url: https://github.com/edde746/sentry-dart
path: packages/dart
ref: build/fetch-native-zip
sentry_flutter:
git:
url: https://github.com/edde746/sentry-dart
path: packages/flutter
ref: build/fetch-native-zip
material_symbols_icons:
git:
url: https://github.com/edde746/material_symbols_icons
ref: 1d8cd83
sentry: sentry:
org: plezy org: plezy
project: plezy project: plezy
@@ -1,6 +1,11 @@
// Copyright 2013 The Flutter Authors // Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// Plezy tvOS copy: shared_preferences_foundation 2.5.6 from flutter/packages
// commit 18b9cc5f2b0820cb6164f0150958ba76db75c61a. Local delta: add
// `os(tvOS)` to the three iOS import/messenger guards. To update, copy this
// file and messages.g.swift from the newly locked package version, reapply only
// those guards, then build Runner for Apple TV simulator and device.
import Foundation import Foundation
@@ -111,8 +116,7 @@ public class SharedPreferencesPlugin: NSObject, FlutterPlugin, UserDefaultsApi {
return Array(try getAll(allowList: allowList, options: options).keys) return Array(try getAll(allowList: allowList, options: options).keys)
} }
func getAll(allowList: [String]?, options: SharedPreferencesPigeonOptions) throws -> [String: Any] func getAll(allowList: [String]?, options: SharedPreferencesPigeonOptions) throws -> [String: Any] {
{
return try SharedPreferencesPlugin.getAllPrefs(allowList: allowList, options: options) return try SharedPreferencesPlugin.getAllPrefs(allowList: allowList, options: options)
} }
@@ -3,6 +3,10 @@
// found in the LICENSE file. // found in the LICENSE file.
// Autogenerated from Pigeon (v26.1.0), do not edit directly. // Autogenerated from Pigeon (v26.1.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon // See also: https://pub.dev/packages/pigeon
// Plezy tvOS copy: shared_preferences_foundation 2.5.6 from flutter/packages
// commit 18b9cc5f2b0820cb6164f0150958ba76db75c61a. Local delta: add
// `os(tvOS)` to the Flutter import guard. Update this together with
// SharedPreferencesPlugin.swift using the guidance in that file.
import Foundation import Foundation
@@ -146,8 +150,7 @@ struct SharedPreferencesPigeonOptions: Hashable {
suiteName suiteName
] ]
} }
static func == (lhs: SharedPreferencesPigeonOptions, rhs: SharedPreferencesPigeonOptions) -> Bool static func == (lhs: SharedPreferencesPigeonOptions, rhs: SharedPreferencesPigeonOptions) -> Bool {
{
return deepEqualsmessages(lhs.toList(), rhs.toList()) return deepEqualsmessages(lhs.toList(), rhs.toList())
} }
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {