From 858952929b41480e5ea94d1f86bc152dca03435a Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 12 Jul 2026 15:48:12 +0200 Subject: [PATCH] fix(deps): refresh and document native dependencies --- .../xcshareddata/swiftpm/Package.resolved | 4 +- packages/saf_util/CHANGELOG.md | 30 +- packages/saf_util/analysis_options.yaml | 5 +- packages/saf_util/android/.gitignore | 9 + packages/saf_util/android/build.gradle.kts | 15 +- packages/saf_util/android/settings.gradle.kts | 2 +- .../fluttercavalry/saf_util/SafUtilPlugin.kt | 472 +++++++++++------- .../saf_util/SafUtilPluginTest.kt | 166 +++--- packages/saf_util/lib/saf_util.dart | 87 ++-- .../saf_util/lib/saf_util_method_channel.dart | 211 ++++---- .../lib/saf_util_platform_interface.dart | 65 ++- packages/saf_util/pubspec.lock | 14 +- packages/saf_util/pubspec.yaml | 8 +- packages/wakelock_plus/LICENSE | 29 ++ packages/wakelock_plus/android/.gitignore | 9 + packages/wakelock_plus/android/build.gradle | 71 +++ .../wakelock_plus/android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 3 + .../plus/wakelock/Wakelock.kt | 39 ++ .../plus/wakelock/WakelockPlusMessages.g.kt | 223 +++++++++ .../plus/wakelock/WakelockPlusPlugin.kt | 48 ++ packages/wakelock_plus/ios/.gitignore | 40 ++ .../wakelock_plus/ios/wakelock_plus.podspec | 25 + .../ios/wakelock_plus/Package.swift | 27 + .../Resources/PrivacyInfo.xcprivacy | 14 + .../UIApplication+idleTimerLock.m | 40 ++ .../wakelock_plus/WakelockPlusPlugin.m | 48 ++ .../UIApplication+idleTimerLock.h | 18 + .../wakelock_plus/WakelockPlusPlugin.h | 4 + .../include/wakelock_plus/messages.g.h | 41 ++ .../Sources/wakelock_plus/messages.g.m | 173 +++++++ packages/wakelock_plus/lib/assets/no_sleep.js | 246 +++++++++ .../lib/src/wakelock_plus_io_plugin.dart | 3 + .../lib/src/wakelock_plus_linux_plugin.dart | 91 ++++ .../lib/src/wakelock_plus_macos_plugin.dart | 30 ++ .../lib/src/wakelock_plus_web_plugin.dart | 49 ++ .../lib/src/wakelock_plus_windows_plugin.dart | 39 ++ .../lib/src/web_impl/import_js_library.dart | 113 +++++ .../lib/src/web_impl/js_wakelock.dart | 20 + packages/wakelock_plus/lib/wakelock_plus.dart | 79 +++ .../wakelock_plus/macos/wakelock_plus.podspec | 22 + .../macos/wakelock_plus/Package.swift | 24 + .../Resources/PrivacyInfo.xcprivacy | 12 + .../WakelockPlusMacosPlugin.swift | 48 ++ packages/wakelock_plus/pubspec.yaml | 108 ++++ pubspec.lock | 55 +- pubspec.yaml | 43 +- .../SharedPreferencesPlugin.swift | 8 +- .../messages.g.swift | 7 +- 49 files changed, 2444 insertions(+), 494 deletions(-) create mode 100644 packages/saf_util/android/.gitignore create mode 100644 packages/wakelock_plus/LICENSE create mode 100644 packages/wakelock_plus/android/.gitignore create mode 100644 packages/wakelock_plus/android/build.gradle create mode 100644 packages/wakelock_plus/android/settings.gradle create mode 100644 packages/wakelock_plus/android/src/main/AndroidManifest.xml create mode 100644 packages/wakelock_plus/android/src/main/kotlin/dev/fluttercommunity/plus/wakelock/Wakelock.kt create mode 100644 packages/wakelock_plus/android/src/main/kotlin/dev/fluttercommunity/plus/wakelock/WakelockPlusMessages.g.kt create mode 100644 packages/wakelock_plus/android/src/main/kotlin/dev/fluttercommunity/plus/wakelock/WakelockPlusPlugin.kt create mode 100644 packages/wakelock_plus/ios/.gitignore create mode 100644 packages/wakelock_plus/ios/wakelock_plus.podspec create mode 100644 packages/wakelock_plus/ios/wakelock_plus/Package.swift create mode 100644 packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/Resources/PrivacyInfo.xcprivacy create mode 100644 packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/UIApplication+idleTimerLock.m create mode 100644 packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/WakelockPlusPlugin.m create mode 100644 packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/include/wakelock_plus/UIApplication+idleTimerLock.h create mode 100644 packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/include/wakelock_plus/WakelockPlusPlugin.h create mode 100644 packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/include/wakelock_plus/messages.g.h create mode 100644 packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/messages.g.m create mode 100644 packages/wakelock_plus/lib/assets/no_sleep.js create mode 100644 packages/wakelock_plus/lib/src/wakelock_plus_io_plugin.dart create mode 100644 packages/wakelock_plus/lib/src/wakelock_plus_linux_plugin.dart create mode 100644 packages/wakelock_plus/lib/src/wakelock_plus_macos_plugin.dart create mode 100644 packages/wakelock_plus/lib/src/wakelock_plus_web_plugin.dart create mode 100644 packages/wakelock_plus/lib/src/wakelock_plus_windows_plugin.dart create mode 100644 packages/wakelock_plus/lib/src/web_impl/import_js_library.dart create mode 100644 packages/wakelock_plus/lib/src/web_impl/js_wakelock.dart create mode 100644 packages/wakelock_plus/lib/wakelock_plus.dart create mode 100644 packages/wakelock_plus/macos/wakelock_plus.podspec create mode 100644 packages/wakelock_plus/macos/wakelock_plus/Package.swift create mode 100644 packages/wakelock_plus/macos/wakelock_plus/Sources/wakelock_plus/Resources/PrivacyInfo.xcprivacy create mode 100644 packages/wakelock_plus/macos/wakelock_plus/Sources/wakelock_plus/WakelockPlusMacosPlugin.swift create mode 100644 packages/wakelock_plus/pubspec.yaml diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0b17c220..0ed9a97c 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -58,8 +58,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/getsentry/sentry-cocoa", "state" : { - "revision" : "16cd512711375fa73f25ae5e373f596bdf4251ae", - "version" : "8.58.0" + "revision" : "dad229c665bfd043c5d80ac7aa77717cbd19a1c3", + "version" : "8.58.3" } }, { diff --git a/packages/saf_util/CHANGELOG.md b/packages/saf_util/CHANGELOG.md index 5c525b57..21f69ecb 100644 --- a/packages/saf_util/CHANGELOG.md +++ b/packages/saf_util/CHANGELOG.md @@ -1,10 +1,28 @@ -## 2.0.0 (plezy vendored patch) +## 3.1.0 (Plezy vendored patch) -Vendored from pub.dev saf_util 2.0.0 with a Result-lifecycle fix: -pending picker replies are take-and-clear (no reply to an already-answered -Result → no "Reply already submitted" crash), unrelated activity request -codes no longer consume the pending picker, and failed picker launches -clear the stale pending state. +Vendored from `flutter-cavalry/saf_util` at +`e300a03ea34b49414b42f309e02531ece57cd0d1`. The local Result-lifecycle patch +take-and-clears picker replies, ignores unrelated request codes, reattaches the +listener across configuration changes, and clears state after launch failures. +When updating, reapply those behaviors to every picker request code and run the +Android plugin tests. + +## 3.1.0 + +- Set `length` to -1 when the length is unknown, matching Android `DocumentFile.length()`. + +## 3.0.0 + +- **Breaking**: Migrate to AGP 9. +- **Breaking**: Minimum Flutter version is 3.44.0. + +## 2.2.0 + +- Add `pickMedia`. + +## 2.1.0 + +- Add the `throws` parameter to `stat`. ## 2.0.0 diff --git a/packages/saf_util/analysis_options.yaml b/packages/saf_util/analysis_options.yaml index a5744c1c..266839ab 100644 --- a/packages/saf_util/analysis_options.yaml +++ b/packages/saf_util/analysis_options.yaml @@ -1,4 +1 @@ -include: package:flutter_lints/flutter.yaml - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +include: package:mgenware_dart_lints/flutter_lints.yaml diff --git a/packages/saf_util/android/.gitignore b/packages/saf_util/android/.gitignore new file mode 100644 index 00000000..161bdcda --- /dev/null +++ b/packages/saf_util/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/packages/saf_util/android/build.gradle.kts b/packages/saf_util/android/build.gradle.kts index d9fc5aee..ac54b882 100644 --- a/packages/saf_util/android/build.gradle.kts +++ b/packages/saf_util/android/build.gradle.kts @@ -2,14 +2,14 @@ group = "com.fluttercavalry.saf_util" version = "1.0-SNAPSHOT" buildscript { - val kotlinVersion = "2.2.20" + val kotlinVersion = "2.3.20" repositories { google() mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:8.11.1") + classpath("com.android.tools.build:gradle:9.0.1") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") } } @@ -23,7 +23,6 @@ allprojects { plugins { id("com.android.library") - id("kotlin-android") } android { @@ -36,10 +35,6 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } - sourceSets { getByName("main") { java.srcDirs("src/main/kotlin") @@ -71,6 +66,12 @@ android { } } +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + dependencies { implementation("androidx.documentfile:documentfile:1.1.0") testImplementation("org.jetbrains.kotlin:kotlin-test") diff --git a/packages/saf_util/android/settings.gradle.kts b/packages/saf_util/android/settings.gradle.kts index 420f0d58..74ceabc2 100644 --- a/packages/saf_util/android/settings.gradle.kts +++ b/packages/saf_util/android/settings.gradle.kts @@ -1 +1 @@ -rootProject.name = 'saf_util' +rootProject.name = "saf_util" diff --git a/packages/saf_util/android/src/main/kotlin/com/fluttercavalry/saf_util/SafUtilPlugin.kt b/packages/saf_util/android/src/main/kotlin/com/fluttercavalry/saf_util/SafUtilPlugin.kt index 93d296c3..0931094f 100644 --- a/packages/saf_util/android/src/main/kotlin/com/fluttercavalry/saf_util/SafUtilPlugin.kt +++ b/packages/saf_util/android/src/main/kotlin/com/fluttercavalry/saf_util/SafUtilPlugin.kt @@ -12,8 +12,9 @@ import android.net.Uri import android.os.Build import android.os.ParcelFileDescriptor import android.provider.DocumentsContract -import androidx.documentfile.provider.DocumentFile +import android.provider.MediaStore import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding @@ -22,19 +23,21 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.PluginRegistry +import java.io.File import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import java.io.File - /** SafUtilPlugin */ -class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { - /// The MethodChannel that will the communication between Flutter and native Android - /// - /// This local reference serves to register the plugin with the Flutter Engine and unregister it - /// when the Flutter Engine is detached from the Activity - private lateinit var channel : MethodChannel +class SafUtilPlugin : + FlutterPlugin, + MethodCallHandler, + ActivityAware { + // / The MethodChannel that will the communication between Flutter and native Android + // / + // / This local reference serves to register the plugin with the Flutter Engine and unregister it + // / when the Flutter Engine is detached from the Activity + private lateinit var channel: MethodChannel private lateinit var context: Context private var activity: Activity? = null @@ -44,20 +47,19 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { private var pendingArguments: PendingArguments? = null private val requestCodeOpenDocumentTree = 1001 private val requestCodeOpenFiles = 1002 + private val requestCodePickMedia = 1003 private val activityResultListener = PluginRegistry.ActivityResultListener { requestCode, resultCode, data -> onActivityResult(requestCode, resultCode, data) } + private val fdMap = mutableMapOf() - /// Atomically takes ownership of the pending picker state. Every reply to a - /// pending Result must go through this so no already-answered Result is ever - /// left behind to be answered again ("Reply already submitted" crashes). + /** Takes ownership before replying so a Result can never be answered twice. */ private fun takePendingResult(): Result? { val result = pendingResult pendingResult = null pendingArguments = null return result } - private val fdMap = mutableMapOf() override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "saf_util") @@ -94,7 +96,10 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { activity = null } - override fun onMethodCall(call: MethodCall, result: Result) { + override fun onMethodCall( + call: MethodCall, + result: Result + ) { when (call.method) { "list" -> { CoroutineScope(Dispatchers.IO).launch { @@ -105,25 +110,27 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { val dir = documentFileFromUri(uri, true) ?: throw Exception("Failed to get DocumentFile from $uri") val resolver = context.contentResolver val mUri = dir.uri - val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( - mUri, - DocumentsContract.getDocumentId(mUri) - ) + val childrenUri = + DocumentsContract.buildChildDocumentsUriUsingTree( + mUri, + DocumentsContract.getDocumentId(mUri) + ) val results = mutableListOf>() - cursor = resolver.query( - childrenUri, - arrayOf( - DocumentsContract.Document.COLUMN_DOCUMENT_ID, - DocumentsContract.Document.COLUMN_DISPLAY_NAME, - DocumentsContract.Document.COLUMN_SIZE, - DocumentsContract.Document.COLUMN_MIME_TYPE, - DocumentsContract.Document.COLUMN_LAST_MODIFIED - ), - null, - null, - null - ) + cursor = + resolver.query( + childrenUri, + arrayOf( + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_SIZE, + DocumentsContract.Document.COLUMN_MIME_TYPE, + DocumentsContract.Document.COLUMN_LAST_MODIFIED + ), + null, + null, + null + ) while (cursor?.moveToNext() == true) { val documentId = cursor.getString(0) @@ -137,13 +144,14 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { val isDirectory = DocumentsContract.Document.MIME_TYPE_DIR == mimeType // Create a dictionary (map) for each file with its details - val fileInfo = fileObjMap( - documentUri, - isDirectory, - fileName, - fileSize, - lastModified, - ) + val fileInfo = + fileObjMap( + documentUri, + isDirectory, + fileName, + fileSize, + lastModified + ) results.add(fileInfo) } @@ -191,9 +199,22 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { try { val uri = call.argument("uri") as String val isDir = call.argument("isDir") + val throws = call.argument("throws") ?: false val df = documentFileFromUri(uri, isDir) - if (df == null || !df.exists()) { + if (df == null) { + if (throws) { + throw Exception("Failed to get DocumentFile from $uri") + } + launch(Dispatchers.Main) { + result.success(null) + } + return@launch + } + if (!df.exists()) { + if (throws) { + throw Exception("DocumentFile at $uri does not exist") + } launch(Dispatchers.Main) { result.success(null) } @@ -258,7 +279,8 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { val uri = call.argument("uri") as String val df = documentFileFromUri(uri, false) ?: throw Exception("Failed to get DocumentFile from $uri") - val fd = context.contentResolver.openFileDescriptor(df.uri, "r") ?: throw Exception("Failed to open file descriptor") + val fd = + context.contentResolver.openFileDescriptor(df.uri, "r") ?: throw Exception("Failed to open file descriptor") val fdInt = fd.fd fdMap[fdInt] = fd @@ -308,11 +330,12 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { // There are cases where the created directory has a different name due to concurrent operations. // In this case, we need to find the directory with the correct name again. val findRes2 = findDirectChild(curDocument.uri, curName) - nextDocument = if (findRes2 != null) { - documentFileFromUriObj(findRes2.uri, findRes2.isDir) - } else { - null - } + nextDocument = + if (findRes2 != null) { + documentFileFromUriObj(findRes2.uri, findRes2.isDir) + } else { + null + } } else { nextDocument = createRes } @@ -355,7 +378,9 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { } return@launch } - curDocument = documentFileFromUriObj(findRes.uri, findRes.isDir) ?: throw Exception("Failed to get DocumentFile at $curName") + curDocument = + documentFileFromUriObj(findRes.uri, findRes.isDir) + ?: throw Exception("Failed to get DocumentFile at $curName") } launch(Dispatchers.Main) { @@ -386,10 +411,12 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { result.success(documentFileToMap(df)) } } else { - val newUri = renameFileDocumentFile(df, newName) - ?: throw Exception("Failed to rename to $newName") - val newDF = documentFileFromUriObj(newUri, false) - ?: throw Exception("Failed to get DocumentFile from $newUri") + val newUri = + renameFileDocumentFile(df, newName) + ?: throw Exception("Failed to rename to $newName") + val newDF = + documentFileFromUriObj(newUri, false) + ?: throw Exception("Failed to get DocumentFile from $newUri") launch(Dispatchers.Main) { result.success(documentFileToMap(newDF)) } @@ -414,12 +441,13 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { val parentUriObj = parentUri.toUri() val newParentUriObj = newParentUri.toUri() - val resUri = DocumentsContract.moveDocument( - context.contentResolver, - uriObj, - parentUriObj, - newParentUriObj - ) ?: throw Exception("Failed to move document") + val resUri = + DocumentsContract.moveDocument( + context.contentResolver, + uriObj, + parentUriObj, + newParentUriObj + ) ?: throw Exception("Failed to move document") val resultDF = documentFileFromUriObj(resUri, isDir) ?: throw Exception("Failed to get DocumentFile from $resUri") launch(Dispatchers.Main) { @@ -443,11 +471,12 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { val uriObj = uri.toUri() val newParentUriObj = newParentUri.toUri() - val resUri = DocumentsContract.copyDocument( - context.contentResolver, - uriObj, - newParentUriObj - ) ?: throw Exception("Failed to move document") + val resUri = + DocumentsContract.copyDocument( + context.contentResolver, + uriObj, + newParentUriObj + ) ?: throw Exception("Failed to move document") val resultDF = documentFileFromUriObj(resUri, isDir) ?: throw Exception("Failed to get DocumentFile from $resUri") @@ -492,14 +521,15 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { ) } intent.addFlags( - if (writePermission) Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION - else Intent.FLAG_GRANT_READ_URI_PERMISSION + if (writePermission) { + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION + } else { + Intent.FLAG_GRANT_READ_URI_PERMISSION + } ) activity?.startActivityForResult(intent, requestCodeOpenDocumentTree) } catch (err: Exception) { - // Launch failed after pendingResult was set: drop the pending state - // so a later activity result can't reply to this Result again. takePendingResult() result.error("PluginError", err.message, null) } @@ -508,7 +538,7 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { "pickFiles" -> { try { val initialUri = call.argument("initialUri") - val multiple = call.argument("multiple") ?: false + val multiple = call.argument("multiple") ?: true val mimeTypes = call.argument>("mimeTypes") ?: arrayListOf() if (activity == null) { @@ -544,8 +574,53 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { activity?.startActivityForResult(intent, requestCodeOpenFiles) } catch (err: Exception) { - // Launch failed after pendingResult was set: drop the pending state - // so a later activity result can't reply to this Result again. + takePendingResult() + result.error("PluginError", err.message, null) + } + } + + "pickMedia" -> { + try { + val multiple = call.argument("multiple") ?: false + val mode = call.argument("mode") ?: "all" + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + result.error("NOT_SUPPORTED", "Photo Picker is only supported on Android 13 (API 33) and above", null) + return + } + if (activity == null) { + result.error("NO_ACTIVITY", "Activity is null", null) + return + } + if (pendingResult != null) { + result.error("ALREADY_PICKING", "Another picker process is already in progress", null) + return + } + + val normalizedMode = mode.lowercase() + if (normalizedMode != "photo" && normalizedMode != "video" && normalizedMode != "all") { + result.error("INVALID_ARGUMENT", "mode must be one of: photo, video, all", null) + return + } + + pendingResult = result + pendingArguments = PendingMediaArguments(multiple) + val intent = Intent(MediaStore.ACTION_PICK_IMAGES) + intent.type = + when (normalizedMode) { + "photo" -> "image/*" + "video" -> "video/*" + else -> "*/*" + } + if (multiple) { + intent.putExtra( + MediaStore.EXTRA_PICK_IMAGES_MAX, + MediaStore.getPickImagesMaxLimit() + ) + } + + activity?.startActivityForResult(intent, requestCodePickMedia) + } catch (err: Exception) { takePendingResult() result.error("PluginError", err.message, null) } @@ -558,12 +633,13 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { val checkRead = call.argument("checkRead") ?: true val checkWrite = call.argument("checkWrite") ?: false - val persisted = hasPersistedUriPermission( - context, - uri.toUri(), - checkRead, - checkWrite - ) + val persisted = + hasPersistedUriPermission( + context, + uri.toUri(), + checkRead, + checkWrite + ) launch(Dispatchers.Main) { result.success(persisted) } @@ -584,10 +660,15 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { context.contentResolver.releasePersistableUriPermission( uri.toUri(), - if (read && write) Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - else if (read) Intent.FLAG_GRANT_READ_URI_PERMISSION - else if (write) Intent.FLAG_GRANT_WRITE_URI_PERMISSION - else 0 + if (read && write) { + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + } else if (read) { + Intent.FLAG_GRANT_READ_URI_PERMISSION + } else if (write) { + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + } else { + 0 + } ) launch(Dispatchers.Main) { result.success(null) @@ -626,26 +707,28 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { if (mime.startsWith("video/")) { val mmr = MediaMetadataRetriever() mmr.setDataSource(context, uri) - bitmap = if (Build.VERSION.SDK_INT >= 27) { - mmr.getScaledFrameAtTime(-1, OPTION_CLOSEST_SYNC, width, height) - } else { - mmr.frameAtTime - } + bitmap = + if (Build.VERSION.SDK_INT >= 27) { + mmr.getScaledFrameAtTime(-1, OPTION_CLOSEST_SYNC, width, height) + } else { + mmr.frameAtTime + } } else { // Use DocumentsContract for other files. - bitmap = DocumentsContract.getDocumentThumbnail( - context.contentResolver, - uri, - Point(width, height), - null - ) + bitmap = + DocumentsContract.getDocumentThumbnail( + context.contentResolver, + uri, + Point(width, height), + null + ) } if (bitmap != null) { File(dest).writeBitmap( bitmap, if (isPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG, - quality, + quality ) launch(Dispatchers.Main) { result.success(true) @@ -663,66 +746,69 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { } } - else -> result.notImplemented() + else -> { + result.notImplemented() + } } } - // Handle the result of the folder/file pickers. Returns whether the - // request code was ours — unrelated request codes must not touch (let - // alone answer) the pending picker state. - private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - if (requestCode != requestCodeOpenDocumentTree && requestCode != requestCodeOpenFiles) { + // Handle folder/file/media picker results; unrelated request codes are not ours. + private fun onActivityResult( + requestCode: Int, + resultCode: Int, + data: Intent? + ): Boolean { + if ( + requestCode != requestCodeOpenDocumentTree && + requestCode != requestCodeOpenFiles && + requestCode != requestCodePickMedia + ) { return false } - // Take ownership before replying: a duplicate delivery finds no pending - // state instead of a second reply on an already-answered Result. + val args = pendingArguments val result = takePendingResult() ?: return true try { if (requestCode == requestCodeOpenDocumentTree) { - // Handle the result of the folder picker. if (resultCode == Activity.RESULT_OK && data != null) { val uri: Uri? = data.data - if (uri != null && args is PendingDirArguments) { - if (args.persistablePermission) { - context.contentResolver.takePersistableUriPermission( - uri, - if (args.writePermission) Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION - else Intent.FLAG_GRANT_READ_URI_PERMISSION - ) - } + if (uri != null && args is PendingDirArguments && args.persistablePermission) { + context.contentResolver.takePersistableUriPermission( + uri, + if (args.writePermission) { + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION + } else { + Intent.FLAG_GRANT_READ_URI_PERMISSION + } + ) } val df = documentFileFromUri(uri.toString(), true) - result.success( - if (df != null) documentFileToMap(df) - else null - ) + result.success(if (df != null) documentFileToMap(df) else null) } else { result.success(null) } } else { - // Handle the result of file picker. if (resultCode == Activity.RESULT_OK && data != null) { - val uris: List = if (data.clipData != null) { - val clipData = data.clipData - val uris = mutableListOf() - for (i in 0 until clipData!!.itemCount) { - uris.add(clipData.getItemAt(i).uri) + val allowMultiple = + if (requestCode == requestCodePickMedia) { + (args as? PendingMediaArguments)?.multiple ?: false + } else { + true + } + val uris: List = + 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> = mutableListOf() for (uri in uris) { val df = documentFileFromUri(uri.toString(), false) - if (df != null) { - documentFileMaps.add(documentFileToMap(df)) - } + if (df != null) documentFileMaps.add(documentFileToMap(df)) } - - result.success(documentFileMaps) // Return the URIs to Flutter + result.success(documentFileMaps) } else { result.success(null) } @@ -731,8 +817,7 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { try { result.error("PluginError", err.message, null) } catch (_: IllegalStateException) { - // Reply already submitted — never crash the host activity over a - // picker teardown race. + // A duplicate native delivery must not crash the host Activity. } } return true @@ -742,19 +827,26 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { channel.setMethodCallHandler(null) } - private fun documentFileFromUri(uri: String, isDir: Boolean?): DocumentFile? { + private fun documentFileFromUri( + uri: String, + isDir: Boolean? + ): DocumentFile? { val uriObj = uri.toUri() val isDirRes = isDir ?: DocumentsContract.isTreeUri(uriObj) return documentFileFromUriObj(uriObj, isDirRes) } - private fun documentFileFromUriObj(uriObj: Uri, isDir: Boolean): DocumentFile? { - val res = if (isDir) { - DocumentFile.fromTreeUri(context, uriObj) - } else { - DocumentFile.fromSingleUri(context, uriObj) - } + private fun documentFileFromUriObj( + uriObj: Uri, + isDir: Boolean + ): DocumentFile? { + val res = + if (isDir) { + DocumentFile.fromTreeUri(context, uriObj) + } else { + DocumentFile.fromSingleUri(context, uriObj) + } return res } @@ -775,31 +867,39 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { return false } - private fun areSameDocumentLocation(treeUri: Uri, docUri: Uri): Boolean { + private fun areSameDocumentLocation( + treeUri: Uri, + docUri: Uri + ): Boolean { val treeDocId = DocumentsContract.getTreeDocumentId(treeUri) val docId = DocumentsContract.getDocumentId(docUri) return treeDocId == docId } - private fun findDirectChild(parentUri: Uri, name: String): UriInfo? { + private fun findDirectChild( + parentUri: Uri, + name: String + ): UriInfo? { var cursor: Cursor? = null try { val resolver = context.contentResolver - val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( - parentUri, - DocumentsContract.getDocumentId(parentUri) - ) - cursor = resolver.query( - childrenUri, - arrayOf( - DocumentsContract.Document.COLUMN_DOCUMENT_ID, - DocumentsContract.Document.COLUMN_DISPLAY_NAME, - DocumentsContract.Document.COLUMN_MIME_TYPE, - ), - null, - null, - null - ) + val childrenUri = + DocumentsContract.buildChildDocumentsUriUsingTree( + parentUri, + DocumentsContract.getDocumentId(parentUri) + ) + cursor = + resolver.query( + childrenUri, + arrayOf( + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_MIME_TYPE + ), + null, + null, + null + ) while (cursor?.moveToNext() == true) { val documentId = cursor.getString(0) @@ -821,45 +921,51 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { } } - private fun renameFileDocumentFile(df: DocumentFile, newName: String): Uri? { + private fun renameFileDocumentFile( + df: DocumentFile, + newName: String + ): Uri? { // https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:documentfile/documentfile/src/main/java/androidx/documentfile/provider/TreeDocumentFile.java try { - val result = DocumentsContract.renameDocument( - context.contentResolver, df.uri, newName - ) + val result = + DocumentsContract.renameDocument( + context.contentResolver, + df.uri, + newName + ) return result } catch (err: Exception) { return null } } - private fun documentFileToMap(file: DocumentFile): Map { - return fileObjMap( - file.uri, - file.isDirectory, - file.name ?: "", - file.length(), - file.lastModified(), - ) - } + private fun documentFileToMap(file: DocumentFile): Map = fileObjMap( + file.uri, + file.isDirectory, + file.name ?: "", + file.length(), + file.lastModified() + ) private fun fileObjMap( uri: Uri, isDir: Boolean, name: String, length: Long, - lastMod: Long, - ): Map { - return mapOf( - "uri" to uri.toString(), - "isDir" to isDir, - "name" to name, - "length" to length, - "lastModified" to lastMod, - ) - } + lastMod: Long + ): Map = mapOf( + "uri" to uri.toString(), + "isDir" to isDir, + "name" to name, + "length" to length, + "lastModified" to lastMod + ) - private fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) { + private fun File.writeBitmap( + bitmap: Bitmap, + format: Bitmap.CompressFormat, + quality: Int + ) { outputStream().use { out -> bitmap.compress(format, quality, out) out.flush() @@ -867,11 +973,19 @@ class SafUtilPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { } } -internal data class UriInfo(val uri: Uri, val name: String, val isDir: Boolean) +internal data class UriInfo( + val uri: Uri, + val name: String, + val isDir: Boolean +) internal open class PendingArguments internal class PendingDirArguments( val writePermission: Boolean, - val persistablePermission: Boolean, -): PendingArguments() + val persistablePermission: Boolean +) : PendingArguments() + +internal class PendingMediaArguments( + val multiple: Boolean +) : PendingArguments() diff --git a/packages/saf_util/android/src/test/kotlin/com/fluttercavalry/saf_util/SafUtilPluginTest.kt b/packages/saf_util/android/src/test/kotlin/com/fluttercavalry/saf_util/SafUtilPluginTest.kt index 8601fc18..ffcd5f45 100644 --- a/packages/saf_util/android/src/test/kotlin/com/fluttercavalry/saf_util/SafUtilPluginTest.kt +++ b/packages/saf_util/android/src/test/kotlin/com/fluttercavalry/saf_util/SafUtilPluginTest.kt @@ -6,84 +6,110 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.PluginRegistry +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue import org.mockito.ArgumentCaptor import org.mockito.Mockito.mock import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoInteractions import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.`when` -import kotlin.test.assertEquals -import kotlin.test.Test internal class SafUtilPluginTest { - @Test - fun onMethodCall_unknownMethod_returnsNotImplemented() { - val plugin = SafUtilPlugin() - val result = mock(MethodChannel.Result::class.java) + @Test + fun onMethodCall_unknownMethod_returnsNotImplemented() { + val plugin = SafUtilPlugin() + val result = mock(MethodChannel.Result::class.java) - plugin.onMethodCall(MethodCall("unknown", null), result) + plugin.onMethodCall(MethodCall("unknown", null), result) - verify(result).notImplemented() - verifyNoMoreInteractions(result) - } - - @Test - fun pickDirectory_withoutActivity_returnsNoActivityError() { - val plugin = SafUtilPlugin() - val result = mock(MethodChannel.Result::class.java) - - plugin.onMethodCall(MethodCall("pickDirectory", null), result) - - verify(result).error("NO_ACTIVITY", "Activity is null", null) - verifyNoMoreInteractions(result) - } - - @Suppress("DEPRECATION") - @Test - fun pickDirectory_afterConfigChange_reattachesListenerAndClearsPendingResult() { - val plugin = SafUtilPlugin() - val firstActivity = RecordingActivity() - val firstBinding = mock(ActivityPluginBinding::class.java) - `when`(firstBinding.activity).thenReturn(firstActivity) - - plugin.onAttachedToActivity(firstBinding) - - val firstListenerCaptor = ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener::class.java) - verify(firstBinding).addActivityResultListener(firstListenerCaptor.capture()) - - val firstResult = mock(MethodChannel.Result::class.java) - plugin.onMethodCall(MethodCall("pickDirectory", null), firstResult) - assertEquals(listOf(1001), firstActivity.startedRequestCodes) - - val secondResult = mock(MethodChannel.Result::class.java) - plugin.onMethodCall(MethodCall("pickDirectory", null), secondResult) - verify(secondResult).error("ALREADY_PICKING", "Another picker process is already in progress", null) - - plugin.onDetachedFromActivityForConfigChanges() - verify(firstBinding).removeActivityResultListener(firstListenerCaptor.value) - - val secondActivity = RecordingActivity() - val secondBinding = mock(ActivityPluginBinding::class.java) - `when`(secondBinding.activity).thenReturn(secondActivity) - - plugin.onReattachedToActivityForConfigChanges(secondBinding) - - val secondListenerCaptor = ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener::class.java) - verify(secondBinding).addActivityResultListener(secondListenerCaptor.capture()) - - secondListenerCaptor.value.onActivityResult(1001, Activity.RESULT_CANCELED, null) - verify(firstResult).success(null) - - val thirdResult = mock(MethodChannel.Result::class.java) - plugin.onMethodCall(MethodCall("pickDirectory", null), thirdResult) - assertEquals(listOf(1001), secondActivity.startedRequestCodes) - } - - private class RecordingActivity : Activity() { - val startedRequestCodes = mutableListOf() - - @Deprecated("Deprecated in Android") - override fun startActivityForResult(intent: Intent?, requestCode: Int) { - startedRequestCodes.add(requestCode) - } + verify(result).notImplemented() + verifyNoMoreInteractions(result) + } + + @Test + fun pickDirectory_withoutActivity_returnsNoActivityError() { + val plugin = SafUtilPlugin() + val result = mock(MethodChannel.Result::class.java) + + plugin.onMethodCall(MethodCall("pickDirectory", null), result) + + verify(result).error("NO_ACTIVITY", "Activity is null", null) + verifyNoMoreInteractions(result) + } + + @Suppress("DEPRECATION") + @Test + fun unrelatedActivityResultDoesNotConsumeOrAnswerPendingPicker() { + val plugin = SafUtilPlugin() + val activity = RecordingActivity() + val binding = mock(ActivityPluginBinding::class.java) + `when`(binding.activity).thenReturn(activity) + plugin.onAttachedToActivity(binding) + + val listenerCaptor = ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener::class.java) + verify(binding).addActivityResultListener(listenerCaptor.capture()) + val result = mock(MethodChannel.Result::class.java) + plugin.onMethodCall(MethodCall("pickDirectory", null), result) + + assertFalse(listenerCaptor.value.onActivityResult(9999, Activity.RESULT_CANCELED, null)) + verifyNoInteractions(result) + assertTrue(listenerCaptor.value.onActivityResult(1001, Activity.RESULT_CANCELED, null)) + verify(result).success(null) + + assertTrue(listenerCaptor.value.onActivityResult(1001, Activity.RESULT_CANCELED, null)) + verifyNoMoreInteractions(result) + } + + @Suppress("DEPRECATION") + @Test + fun pickDirectory_afterConfigChange_reattachesListenerAndClearsPendingResult() { + val plugin = SafUtilPlugin() + val firstActivity = RecordingActivity() + val firstBinding = mock(ActivityPluginBinding::class.java) + `when`(firstBinding.activity).thenReturn(firstActivity) + + plugin.onAttachedToActivity(firstBinding) + + val firstListenerCaptor = ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener::class.java) + verify(firstBinding).addActivityResultListener(firstListenerCaptor.capture()) + + val firstResult = mock(MethodChannel.Result::class.java) + plugin.onMethodCall(MethodCall("pickDirectory", null), firstResult) + assertEquals(listOf(1001), firstActivity.startedRequestCodes) + + val secondResult = mock(MethodChannel.Result::class.java) + plugin.onMethodCall(MethodCall("pickDirectory", null), secondResult) + verify(secondResult).error("ALREADY_PICKING", "Another picker process is already in progress", null) + + plugin.onDetachedFromActivityForConfigChanges() + verify(firstBinding).removeActivityResultListener(firstListenerCaptor.value) + + val secondActivity = RecordingActivity() + val secondBinding = mock(ActivityPluginBinding::class.java) + `when`(secondBinding.activity).thenReturn(secondActivity) + + plugin.onReattachedToActivityForConfigChanges(secondBinding) + + val secondListenerCaptor = ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener::class.java) + verify(secondBinding).addActivityResultListener(secondListenerCaptor.capture()) + + secondListenerCaptor.value.onActivityResult(1001, Activity.RESULT_CANCELED, null) + verify(firstResult).success(null) + + val thirdResult = mock(MethodChannel.Result::class.java) + plugin.onMethodCall(MethodCall("pickDirectory", null), thirdResult) + assertEquals(listOf(1001), secondActivity.startedRequestCodes) + } + + private class RecordingActivity : Activity() { + val startedRequestCodes = mutableListOf() + + @Deprecated("Deprecated in Android") + override fun startActivityForResult(intent: Intent?, requestCode: Int) { + startedRequestCodes.add(requestCode) } + } } diff --git a/packages/saf_util/lib/saf_util.dart b/packages/saf_util/lib/saf_util.dart index 5513228f..74a58251 100644 --- a/packages/saf_util/lib/saf_util.dart +++ b/packages/saf_util/lib/saf_util.dart @@ -6,14 +6,16 @@ class SafUtil { /// [initialUri] is the initial URI to show in the dialog. /// [writePermission] is true if the folder should have write permission. /// [persistablePermission] is true if the permission should be persistable. - Future pickDirectory( - {String? initialUri, - bool? writePermission, - bool? persistablePermission}) { + Future pickDirectory({ + String? initialUri, + bool? writePermission, + bool? persistablePermission, + }) { return SafUtilPlatform.instance.pickDirectory( - initialUri: initialUri, - writePermission: writePermission, - persistablePermission: persistablePermission); + initialUri: initialUri, + writePermission: writePermission, + persistablePermission: persistablePermission, + ); } /// Shows a file picker dialog and returns the selected file [SafDocumentFile]. @@ -39,7 +41,7 @@ class SafUtil { Future?> pickFiles({ String? initialUri, List? mimeTypes, - multiple = true, + bool multiple = true, }) { return SafUtilPlatform.instance.pickFiles( initialUri: initialUri, @@ -48,6 +50,20 @@ class SafUtil { ); } + /// Shows a media picker dialog and returns selected media as [SafDocumentFile]. + /// + /// [multiple] is true if multiple media files can be selected. + /// [mode] controls what can be picked and must be one of: + /// - 'photo' + /// - 'video' + /// - 'all' + Future?> pickMedia({ + bool multiple = true, + String mode = 'all', + }) { + return SafUtilPlatform.instance.pickMedia(multiple: multiple, mode: mode); + } + /// Lists the contents of the specified directory URI. /// Returns a list of [SafDocumentFile] objects. /// @@ -72,8 +88,9 @@ class SafUtil { /// [uri] is the URI of the file or directory. /// [isDir] is true if the URI is a directory. [null] means /// auto-detect. - Future stat(String uri, bool? isDir) { - return SafUtilPlatform.instance.stat(uri, isDir); + /// [throws] when true, throws an exception if the URI does not exist or is inaccessible. + Future stat(String uri, bool? isDir, {bool? throws}) { + return SafUtilPlatform.instance.stat(uri, isDir, throws: throws); } /// Checks if the specified file or directory exists. @@ -126,7 +143,11 @@ class SafUtil { /// [parentUri] is the URI of the current parent directory. /// [newParentUri] is the URI of the new parent directory. Future moveTo( - String uri, bool isDir, String parentUri, String newParentUri) { + String uri, + bool isDir, + String parentUri, + String newParentUri, + ) { return SafUtilPlatform.instance.moveTo(uri, isDir, parentUri, newParentUri); } @@ -178,14 +199,16 @@ class SafUtil { /// [writePermission] is true if the folder should have write permission. /// [persistablePermission] is true if the permission should be persistable. @Deprecated('Use [pickDirectory] instead, which returns a [SafDocumentFile].') - Future openDirectory( - {String? initialUri, - bool? writePermission, - bool? persistablePermission}) { + Future openDirectory({ + String? initialUri, + bool? writePermission, + bool? persistablePermission, + }) { return SafUtilPlatform.instance.openDirectory( - initialUri: initialUri, - writePermission: writePermission, - persistablePermission: persistablePermission); + initialUri: initialUri, + writePermission: writePermission, + persistablePermission: persistablePermission, + ); } /// Shows a file picker dialog and returns the selected file URI. @@ -194,10 +217,7 @@ class SafUtil { /// [initialUri] is the initial URI to show in the dialog. /// [mimeTypes] is a list of MIME types to filter the files. @Deprecated('Use [pickFile] instead, which returns a [SafDocumentFile].') - Future openFile({ - String? initialUri, - List? mimeTypes, - }) { + Future openFile({String? initialUri, List? mimeTypes}) { return SafUtilPlatform.instance.openFile( initialUri: initialUri, mimeTypes: mimeTypes, @@ -212,7 +232,7 @@ class SafUtil { Future?> openFiles({ String? initialUri, List? mimeTypes, - multiple = true, + bool multiple = true, }) { return SafUtilPlatform.instance.openFiles( initialUri: initialUri, @@ -240,17 +260,26 @@ class SafUtil { bool checkRead = true, bool checkWrite = false, }) { - return SafUtilPlatform.instance.hasPersistedPermission(uri, - checkRead: checkRead, checkWrite: checkWrite); + return SafUtilPlatform.instance.hasPersistedPermission( + uri, + checkRead: checkRead, + checkWrite: checkWrite, + ); } /// Releases the persisted permission of the specified URI. /// Use [read] and [write] to specify the type of permission to release. /// [read] defaults to true. /// [write] defaults to false. - Future releasePersistedPermission(String uri, - {bool read = true, bool write = false}) async { - return SafUtilPlatform.instance - .releasePersistedPermission(uri, read: read, write: write); + Future releasePersistedPermission( + String uri, { + bool read = true, + bool write = false, + }) async { + return SafUtilPlatform.instance.releasePersistedPermission( + uri, + read: read, + write: write, + ); } } diff --git a/packages/saf_util/lib/saf_util_method_channel.dart b/packages/saf_util/lib/saf_util_method_channel.dart index bfa695a5..2d3997b4 100644 --- a/packages/saf_util/lib/saf_util_method_channel.dart +++ b/packages/saf_util/lib/saf_util_method_channel.dart @@ -10,16 +10,17 @@ class MethodChannelSafUtil extends SafUtilPlatform { final methodChannel = const MethodChannel('saf_util'); @override - Future pickDirectory( - {String? initialUri, - bool? writePermission, - bool? persistablePermission}) async { - final map = - await methodChannel.invokeMapMethod('pickDirectory', { - 'initialUri': initialUri, - 'writePermission': writePermission, - 'persistablePermission': persistablePermission, - }); + Future pickDirectory({ + String? initialUri, + bool? writePermission, + bool? persistablePermission, + }) async { + final map = await methodChannel + .invokeMapMethod('pickDirectory', { + 'initialUri': initialUri, + 'writePermission': writePermission, + 'persistablePermission': persistablePermission, + }); if (map == null) { return null; } @@ -27,10 +28,11 @@ class MethodChannelSafUtil extends SafUtilPlatform { } @override - Future openDirectory( - {String? initialUri, - bool? writePermission, - bool? persistablePermission}) async { + Future openDirectory({ + String? initialUri, + bool? writePermission, + bool? persistablePermission, + }) async { final res = await pickDirectory( initialUri: initialUri, writePermission: writePermission, @@ -57,10 +59,7 @@ class MethodChannelSafUtil extends SafUtilPlatform { String? initialUri, List? mimeTypes, }) async { - final res = await pickFile( - initialUri: initialUri, - mimeTypes: mimeTypes, - ); + final res = await pickFile(initialUri: initialUri, mimeTypes: mimeTypes); return res?.uri; } @@ -68,14 +67,24 @@ class MethodChannelSafUtil extends SafUtilPlatform { Future?> pickFiles({ String? initialUri, List? mimeTypes, - multiple = true, + bool multiple = true, }) async { - final maps = await methodChannel - .invokeListMethod>('pickFiles', { - 'initialUri': initialUri, - 'mimeTypes': mimeTypes, - 'multiple': multiple, - }); + final maps = await methodChannel.invokeListMethod>( + 'pickFiles', + {'initialUri': initialUri, 'mimeTypes': mimeTypes, 'multiple': multiple}, + ); + return maps?.map((map) => SafDocumentFile.fromMap(map)).toList(); + } + + @override + Future?> pickMedia({ + bool multiple = true, + String mode = 'all', + }) async { + final maps = await methodChannel.invokeListMethod>( + 'pickMedia', + {'multiple': multiple, 'mode': mode}, + ); return maps?.map((map) => SafDocumentFile.fromMap(map)).toList(); } @@ -83,7 +92,7 @@ class MethodChannelSafUtil extends SafUtilPlatform { Future?> openFiles({ String? initialUri, List? mimeTypes, - multiple = true, + bool multiple = true, }) async { final res = await pickFiles( initialUri: initialUri, @@ -95,8 +104,10 @@ class MethodChannelSafUtil extends SafUtilPlatform { @override Future> list(String uri) async { - final maps = await methodChannel - .invokeListMethod>('list', {'uri': uri}); + final maps = await methodChannel.invokeListMethod>( + 'list', + {'uri': uri}, + ); return (maps ?? []).map((map) => SafDocumentFile.fromMap(map)).toList(); } @@ -113,11 +124,12 @@ class MethodChannelSafUtil extends SafUtilPlatform { } @override - Future stat(String uri, bool? isDir) async { - final map = await methodChannel.invokeMapMethod( - 'stat', - {'uri': uri, 'isDir': isDir}, - ); + Future stat(String uri, bool? isDir, {bool? throws}) async { + final map = await methodChannel.invokeMapMethod('stat', { + 'uri': uri, + 'isDir': isDir, + 'throws': throws, + }); if (map == null) { return null; } @@ -126,10 +138,10 @@ class MethodChannelSafUtil extends SafUtilPlatform { @override Future exists(String uri, bool isDir) async { - final res = await methodChannel.invokeMethod( - 'exists', - {'uri': uri, 'isDir': isDir}, - ); + final res = await methodChannel.invokeMethod('exists', { + 'uri': uri, + 'isDir': isDir, + }); if (res == null) { throw Exception('Failed to check if file exists: $uri'); } @@ -138,10 +150,10 @@ class MethodChannelSafUtil extends SafUtilPlatform { @override Future delete(String uri, bool isDir) async { - final res = await methodChannel.invokeMethod( - 'delete', - {'uri': uri, 'isDir': isDir}, - ); + final res = await methodChannel.invokeMethod('delete', { + 'uri': uri, + 'isDir': isDir, + }); if (res != true) { throw Exception('Failed to delete file: $uri'); } @@ -149,10 +161,10 @@ class MethodChannelSafUtil extends SafUtilPlatform { @override Future mkdirp(String uri, List names) async { - final map = await methodChannel.invokeMapMethod( - 'mkdirp', - {'uri': uri, 'names': names}, - ); + final map = await methodChannel.invokeMapMethod('mkdirp', { + 'uri': uri, + 'names': names, + }); if (map == null) { throw Exception('Failed to create directory: $uri'); } @@ -161,10 +173,10 @@ class MethodChannelSafUtil extends SafUtilPlatform { @override Future child(String uri, List names) async { - final map = await methodChannel.invokeMapMethod( - 'child', - {'uri': uri, 'names': names}, - ); + final map = await methodChannel.invokeMapMethod('child', { + 'uri': uri, + 'names': names, + }); if (map == null) { return null; } @@ -173,10 +185,11 @@ class MethodChannelSafUtil extends SafUtilPlatform { @override Future rename(String uri, bool isDir, String newName) async { - final map = await methodChannel.invokeMapMethod( - 'rename', - {'uri': uri, 'isDir': isDir, 'newName': newName}, - ); + final map = await methodChannel.invokeMapMethod('rename', { + 'uri': uri, + 'isDir': isDir, + 'newName': newName, + }); if (map == null) { throw Exception('Failed to rename: $uri'); } @@ -185,16 +198,17 @@ class MethodChannelSafUtil extends SafUtilPlatform { @override Future moveTo( - String uri, bool isDir, String parentUri, String newParentUri) async { - final map = await methodChannel.invokeMapMethod( - 'moveTo', - { - 'uri': uri, - 'isDir': isDir, - 'parentUri': parentUri, - 'newParentUri': newParentUri - }, - ); + String uri, + bool isDir, + String parentUri, + String newParentUri, + ) async { + final map = await methodChannel.invokeMapMethod('moveTo', { + 'uri': uri, + 'isDir': isDir, + 'parentUri': parentUri, + 'newParentUri': newParentUri, + }); if (map == null) { throw Exception('Failed to move: $uri'); } @@ -203,11 +217,15 @@ class MethodChannelSafUtil extends SafUtilPlatform { @override Future copyTo( - String uri, bool isDir, String newParentUri) async { - final map = await methodChannel.invokeMapMethod( - 'copyTo', - {'uri': uri, 'isDir': isDir, 'newParentUri': newParentUri}, - ); + String uri, + bool isDir, + String newParentUri, + ) async { + final map = await methodChannel.invokeMapMethod('copyTo', { + 'uri': uri, + 'isDir': isDir, + 'newParentUri': newParentUri, + }); if (map == null) { throw Exception('Failed to copy: $uri'); } @@ -223,26 +241,22 @@ class MethodChannelSafUtil extends SafUtilPlatform { String? format, int? quality, }) async { - final res = await methodChannel.invokeMethod( - 'saveThumbnailToFile', - { - 'uri': uri.toString(), - 'width': width, - 'height': height, - 'destPath': destPath, - 'format': format, - 'quality': quality, - }, - ); + final res = await methodChannel.invokeMethod('saveThumbnailToFile', { + 'uri': uri.toString(), + 'width': width, + 'height': height, + 'destPath': destPath, + 'format': format, + 'quality': quality, + }); return res ?? false; } @override Future getFileDescriptor(String uri) async { - final res = await methodChannel.invokeMethod( - 'getFileDescriptor', - {'uri': uri}, - ); + final res = await methodChannel.invokeMethod('getFileDescriptor', { + 'uri': uri, + }); if (res == null) { throw Exception('Failed to get file descriptor: $uri'); } @@ -251,10 +265,7 @@ class MethodChannelSafUtil extends SafUtilPlatform { @override Future closeFileDescriptor(int fd) { - return methodChannel.invokeMethod( - 'closeFileDescriptor', - {'fd': fd}, - ); + return methodChannel.invokeMethod('closeFileDescriptor', {'fd': fd}); } @override @@ -265,11 +276,7 @@ class MethodChannelSafUtil extends SafUtilPlatform { }) async { final res = await methodChannel.invokeMethod( 'hasPersistedPermission', - { - 'uri': uri, - 'checkRead': checkRead, - 'checkWrite': checkWrite, - }, + {'uri': uri, 'checkRead': checkRead, 'checkWrite': checkWrite}, ); if (res == null) { throw Exception('Failed to check persisted permission: $uri'); @@ -278,15 +285,15 @@ class MethodChannelSafUtil extends SafUtilPlatform { } @override - Future releasePersistedPermission(String uri, - {bool read = true, bool write = false}) async { - await methodChannel.invokeMethod( - 'releasePersistedPermission', - { - 'uri': uri, - 'read': read, - 'write': write, - }, - ); + Future releasePersistedPermission( + String uri, { + bool read = true, + bool write = false, + }) async { + await methodChannel.invokeMethod('releasePersistedPermission', { + 'uri': uri, + 'read': read, + 'write': write, + }); } } diff --git a/packages/saf_util/lib/saf_util_platform_interface.dart b/packages/saf_util/lib/saf_util_platform_interface.dart index 44e80df3..7f0b591a 100644 --- a/packages/saf_util/lib/saf_util_platform_interface.dart +++ b/packages/saf_util/lib/saf_util_platform_interface.dart @@ -19,11 +19,11 @@ class SafDocumentFile { static SafDocumentFile fromMap(Map map) { return SafDocumentFile( - uri: map['uri'], - name: map['name'], - isDir: map['isDir'] ?? false, - length: map['length'] ?? 0, - lastModified: map['lastModified'] ?? 0, + uri: map['uri'] as String, + name: map['name'] as String, + isDir: map['isDir'] as bool? ?? false, + length: map['length'] as int? ?? -1, + lastModified: map['lastModified'] as int? ?? 0, ); } @@ -54,17 +54,19 @@ abstract class SafUtilPlatform extends PlatformInterface { _instance = instance; } - Future pickDirectory( - {String? initialUri, - bool? writePermission, - bool? persistablePermission}) { + Future pickDirectory({ + String? initialUri, + bool? writePermission, + bool? persistablePermission, + }) { throw UnimplementedError('pickDirectory() has not been implemented.'); } - Future openDirectory( - {String? initialUri, - bool? writePermission, - bool? persistablePermission}) { + Future openDirectory({ + String? initialUri, + bool? writePermission, + bool? persistablePermission, + }) { throw UnimplementedError('openDirectory() has not been implemented.'); } @@ -75,25 +77,29 @@ abstract class SafUtilPlatform extends PlatformInterface { throw UnimplementedError('pickFile() has not been implemented.'); } - Future openFile({ - String? initialUri, - List? mimeTypes, - }) { + Future openFile({String? initialUri, List? mimeTypes}) { throw UnimplementedError('openFile() has not been implemented.'); } Future?> pickFiles({ String? initialUri, List? mimeTypes, - multiple = true, + bool multiple = true, }) { throw UnimplementedError('pickFiles() has not been implemented.'); } + Future?> pickMedia({ + bool multiple = true, + String mode = 'all', + }) { + throw UnimplementedError('pickMedia() has not been implemented.'); + } + Future?> openFiles({ String? initialUri, List? mimeTypes, - multiple = true, + bool multiple = true, }) { throw UnimplementedError('openFiles() has not been implemented.'); } @@ -106,7 +112,7 @@ abstract class SafUtilPlatform extends PlatformInterface { throw UnimplementedError('documentFileFromUri() has not been implemented.'); } - Future stat(String uri, bool? isDir) { + Future stat(String uri, bool? isDir, {bool? throws}) { throw UnimplementedError('stat() has not been implemented.'); } @@ -131,7 +137,11 @@ abstract class SafUtilPlatform extends PlatformInterface { } Future moveTo( - String uri, bool isDir, String parentUri, String newParentUri) { + String uri, + bool isDir, + String parentUri, + String newParentUri, + ) { throw UnimplementedError('moveTo() has not been implemented.'); } @@ -164,12 +174,17 @@ abstract class SafUtilPlatform extends PlatformInterface { bool checkWrite = false, }) { throw UnimplementedError( - 'hasPersistedPermission() has not been implemented.'); + 'hasPersistedPermission() has not been implemented.', + ); } - Future releasePersistedPermission(String uri, - {bool read = true, bool write = false}) { + Future releasePersistedPermission( + String uri, { + bool read = true, + bool write = false, + }) { throw UnimplementedError( - 'releasePersistedPermission() has not been implemented.'); + 'releasePersistedPermission() has not been implemented.', + ); } } diff --git a/packages/saf_util/pubspec.lock b/packages/saf_util/pubspec.lock index 159565b3..9fd8e7b2 100644 --- a/packages/saf_util/pubspec.lock +++ b/packages/saf_util/pubspec.lock @@ -55,7 +55,7 @@ packages: source: sdk version: "0.0.0" flutter_lints: - dependency: "direct dev" + dependency: transitive description: name: flutter_lints sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" @@ -123,6 +123,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + mgenware_dart_lints: + dependency: "direct dev" + description: + name: mgenware_dart_lints + sha256: "14f47c0ba0073c1980298ee10f602c19fa244151c68ec75fde602642c3de3a4e" + url: "https://pub.dev" + source: hosted + version: "8.1.0" path: dependency: transitive description: @@ -209,5 +217,5 @@ packages: source: hosted version: "15.2.0" sdks: - dart: ">=3.10.0-0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" diff --git a/packages/saf_util/pubspec.yaml b/packages/saf_util/pubspec.yaml index 7044d1dc..68087705 100644 --- a/packages/saf_util/pubspec.yaml +++ b/packages/saf_util/pubspec.yaml @@ -1,11 +1,11 @@ name: saf_util description: "Util functions for SAF (Storage Access Framework)." -version: 2.0.0 +version: 3.1.0 homepage: https://github.com/flutter-cavalry/saf_util environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=2.5.0" + sdk: ^3.12.0 + flutter: ">=3.44.0" dependencies: flutter: @@ -15,7 +15,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^6.0.0 + mgenware_dart_lints: ^8.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/packages/wakelock_plus/LICENSE b/packages/wakelock_plus/LICENSE new file mode 100644 index 00000000..840ee46c --- /dev/null +++ b/packages/wakelock_plus/LICENSE @@ -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. \ No newline at end of file diff --git a/packages/wakelock_plus/android/.gitignore b/packages/wakelock_plus/android/.gitignore new file mode 100644 index 00000000..161bdcda --- /dev/null +++ b/packages/wakelock_plus/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/packages/wakelock_plus/android/build.gradle b/packages/wakelock_plus/android/build.gradle new file mode 100644 index 00000000..d2a1ffe3 --- /dev/null +++ b/packages/wakelock_plus/android/build.gradle @@ -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 + } + } + } +} diff --git a/packages/wakelock_plus/android/settings.gradle b/packages/wakelock_plus/android/settings.gradle new file mode 100644 index 00000000..27c21d9d --- /dev/null +++ b/packages/wakelock_plus/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'wakelock_plus' diff --git a/packages/wakelock_plus/android/src/main/AndroidManifest.xml b/packages/wakelock_plus/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..0587acb3 --- /dev/null +++ b/packages/wakelock_plus/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/packages/wakelock_plus/android/src/main/kotlin/dev/fluttercommunity/plus/wakelock/Wakelock.kt b/packages/wakelock_plus/android/src/main/kotlin/dev/fluttercommunity/plus/wakelock/Wakelock.kt new file mode 100644 index 00000000..8ba445a1 --- /dev/null +++ b/packages/wakelock_plus/android/src/main/kotlin/dev/fluttercommunity/plus/wakelock/Wakelock.kt @@ -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") diff --git a/packages/wakelock_plus/android/src/main/kotlin/dev/fluttercommunity/plus/wakelock/WakelockPlusMessages.g.kt b/packages/wakelock_plus/android/src/main/kotlin/dev/fluttercommunity/plus/wakelock/WakelockPlusMessages.g.kt new file mode 100644 index 00000000..2d4b1d0c --- /dev/null +++ b/packages/wakelock_plus/android/src/main/kotlin/dev/fluttercommunity/plus/wakelock/WakelockPlusMessages.g.kt @@ -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 { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + 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).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): ToggleMessage { + val enable = pigeonVar_list[0] as Boolean? + return ToggleMessage(enable) + } + } + fun toList(): List { + 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): IsEnabledMessage { + val enabled = pigeonVar_list[0] as Boolean? + return IsEnabledMessage(enabled) + } + } + fun toList(): List { + 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)?.let { + ToggleMessage.fromList(it) + } + } + 130.toByte() -> { + return (readValue(buffer) as? List)?.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 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(binaryMessenger, "dev.flutter.pigeon.wakelock_plus_platform_interface.WakelockPlusApi.toggle$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val msgArg = args[0] as ToggleMessage + val wrapped: List = try { + api.toggle(msgArg) + listOf(null) + } catch (exception: Throwable) { + WakelockPlusMessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.wakelock_plus_platform_interface.WakelockPlusApi.isEnabled$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.isEnabled()) + } catch (exception: Throwable) { + WakelockPlusMessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/packages/wakelock_plus/android/src/main/kotlin/dev/fluttercommunity/plus/wakelock/WakelockPlusPlugin.kt b/packages/wakelock_plus/android/src/main/kotlin/dev/fluttercommunity/plus/wakelock/WakelockPlusPlugin.kt new file mode 100644 index 00000000..bd182ec5 --- /dev/null +++ b/packages/wakelock_plus/android/src/main/kotlin/dev/fluttercommunity/plus/wakelock/WakelockPlusPlugin.kt @@ -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() + } +} diff --git a/packages/wakelock_plus/ios/.gitignore b/packages/wakelock_plus/ios/.gitignore new file mode 100644 index 00000000..55a386be --- /dev/null +++ b/packages/wakelock_plus/ios/.gitignore @@ -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 \ No newline at end of file diff --git a/packages/wakelock_plus/ios/wakelock_plus.podspec b/packages/wakelock_plus/ios/wakelock_plus.podspec new file mode 100644 index 00000000..c9c166ad --- /dev/null +++ b/packages/wakelock_plus/ios/wakelock_plus.podspec @@ -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 diff --git a/packages/wakelock_plus/ios/wakelock_plus/Package.swift b/packages/wakelock_plus/ios/wakelock_plus/Package.swift new file mode 100644 index 00000000..790ec858 --- /dev/null +++ b/packages/wakelock_plus/ios/wakelock_plus/Package.swift @@ -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") + ] + ) + ] +) \ No newline at end of file diff --git a/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/Resources/PrivacyInfo.xcprivacy b/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..a34b7e2e --- /dev/null +++ b/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/UIApplication+idleTimerLock.m b/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/UIApplication+idleTimerLock.m new file mode 100644 index 00000000..9a3ba767 --- /dev/null +++ b/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/UIApplication+idleTimerLock.m @@ -0,0 +1,40 @@ +// +// UIApplication+idleTimerLock.m +// wakelock +// +// Created by suyao on 2021/12/17. +// + +#import "./include/wakelock_plus/UIApplication+idleTimerLock.h" +#import + +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 diff --git a/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/WakelockPlusPlugin.m b/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/WakelockPlusPlugin.m new file mode 100644 index 00000000..a2ab44b0 --- /dev/null +++ b/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/WakelockPlusPlugin.m @@ -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 () + +@property (nonatomic, assign) BOOL enable; + +@end + +@implementation WakelockPlusPlugin ++ (void)registerWithRegistrar:(NSObject*)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 diff --git a/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/include/wakelock_plus/UIApplication+idleTimerLock.h b/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/include/wakelock_plus/UIApplication+idleTimerLock.h new file mode 100644 index 00000000..058d36e4 --- /dev/null +++ b/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/include/wakelock_plus/UIApplication+idleTimerLock.h @@ -0,0 +1,18 @@ +// +// UIApplication+idleTimerLock.h +// wakelock +// +// Created by suyao on 2021/12/17. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UIApplication (idleTimerLock) + +- (void)lock_idleTimerlockEnable:(BOOL)enable; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/include/wakelock_plus/WakelockPlusPlugin.h b/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/include/wakelock_plus/WakelockPlusPlugin.h new file mode 100644 index 00000000..2c4fdf95 --- /dev/null +++ b/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/include/wakelock_plus/WakelockPlusPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface WakelockPlusPlugin : NSObject +@end diff --git a/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/include/wakelock_plus/messages.g.h b/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/include/wakelock_plus/messages.g.h new file mode 100644 index 00000000..eea50a3b --- /dev/null +++ b/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/include/wakelock_plus/messages.g.h @@ -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 *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 binaryMessenger, NSObject *_Nullable api); + +extern void SetUpWAKELOCKPLUSWakelockPlusApiWithSuffix(id binaryMessenger, NSObject *_Nullable api, NSString *messageChannelSuffix); + +NS_ASSUME_NONNULL_END diff --git a/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/messages.g.m b/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/messages.g.m new file mode 100644 index 00000000..911784bc --- /dev/null +++ b/packages/wakelock_plus/ios/wakelock_plus/Sources/wakelock_plus/messages.g.m @@ -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 *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 *array, NSInteger key) { + id result = array[key]; + return (result == [NSNull null]) ? nil : result; +} + +@interface WAKELOCKPLUSToggleMessage () ++ (WAKELOCKPLUSToggleMessage *)fromList:(NSArray *)list; ++ (nullable WAKELOCKPLUSToggleMessage *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface WAKELOCKPLUSIsEnabledMessage () ++ (WAKELOCKPLUSIsEnabledMessage *)fromList:(NSArray *)list; ++ (nullable WAKELOCKPLUSIsEnabledMessage *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@implementation WAKELOCKPLUSToggleMessage ++ (instancetype)makeWithEnable:(nullable NSNumber *)enable { + WAKELOCKPLUSToggleMessage* pigeonResult = [[WAKELOCKPLUSToggleMessage alloc] init]; + pigeonResult.enable = enable; + return pigeonResult; +} ++ (WAKELOCKPLUSToggleMessage *)fromList:(NSArray *)list { + WAKELOCKPLUSToggleMessage *pigeonResult = [[WAKELOCKPLUSToggleMessage alloc] init]; + pigeonResult.enable = GetNullableObjectAtIndex(list, 0); + return pigeonResult; +} ++ (nullable WAKELOCKPLUSToggleMessage *)nullableFromList:(NSArray *)list { + return (list) ? [WAKELOCKPLUSToggleMessage fromList:list] : nil; +} +- (NSArray *)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 *)list { + WAKELOCKPLUSIsEnabledMessage *pigeonResult = [[WAKELOCKPLUSIsEnabledMessage alloc] init]; + pigeonResult.enabled = GetNullableObjectAtIndex(list, 0); + return pigeonResult; +} ++ (nullable WAKELOCKPLUSIsEnabledMessage *)nullableFromList:(NSArray *)list { + return (list) ? [WAKELOCKPLUSIsEnabledMessage fromList:list] : nil; +} +- (NSArray *)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 *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 binaryMessenger, NSObject *api) { + SetUpWAKELOCKPLUSWakelockPlusApiWithSuffix(binaryMessenger, api, @""); +} + +void SetUpWAKELOCKPLUSWakelockPlusApiWithSuffix(id binaryMessenger, NSObject *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 *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]; + } + } +} diff --git a/packages/wakelock_plus/lib/assets/no_sleep.js b/packages/wakelock_plus/lib/assets/no_sleep.js new file mode 100644 index 00000000..3ae1af0e --- /dev/null +++ b/packages/wakelock_plus/lib/assets/no_sleep.js @@ -0,0 +1,246 @@ +/*! Based On NoSleep.js v0.12.0 - git.io/vfn01 - Rich Tibbett - MIT license */ + +class PromiseCompleter { + _promise; + _resolve; + _reject; + constructor() { + this._promise = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); + } + + isCompleted = false; + + get future() { + return this._promise; + } + + complete(value) { + this.isCompleted = true; + this._resolve(value); + } + + completeError(error) { + this._reject(error); + } +} + +var webm = + 'data:video/webm;base64,GkXfo0AgQoaBAUL3gQFC8oEEQvOBCEKCQAR3ZWJtQoeBAkKFgQIYU4BnQI0VSalmQCgq17FAAw9CQE2AQAZ3aGFtbXlXQUAGd2hhbW15RIlACECPQAAAAAAAFlSua0AxrkAu14EBY8WBAZyBACK1nEADdW5khkAFVl9WUDglhohAA1ZQOIOBAeBABrCBCLqBCB9DtnVAIueBAKNAHIEAAIAwAQCdASoIAAgAAUAmJaQAA3AA/vz0AAA=' +var mp4 = + 'data:video/mp4;base64,AAAAIGZ0eXBtcDQyAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAACKBtZGF0AAAC8wYF///v3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE0MiByMjQ3OSBkZDc5YTYxIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAxNCAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTEgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MToweDExMSBtZT1oZXggc3VibWU9MiBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0wIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MCA4eDhkY3Q9MCBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0wIHRocmVhZHM9NiBsb29rYWhlYWRfdGhyZWFkcz0xIHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNpbWF0ZT0xIGludGVybGFjZWQ9MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9MCBiZnJhbWVzPTMgYl9weXJhbWlkPTIgYl9hZGFwdD0xIGJfYmlhcz0wIGRpcmVjdD0xIHdlaWdodGI9MSBvcGVuX2dvcD0wIHdlaWdodHA9MSBrZXlpbnQ9MzAwIGtleWludF9taW49MzAgc2NlbmVjdXQ9NDAgaW50cmFfcmVmcmVzaD0wIHJjX2xvb2thaGVhZD0xMCByYz1jcmYgbWJ0cmVlPTEgY3JmPTIwLjAgcWNvbXA9MC42MCBxcG1pbj0wIHFwbWF4PTY5IHFwc3RlcD00IHZidl9tYXhyYXRlPTIwMDAwIHZidl9idWZzaXplPTI1MDAwIGNyZl9tYXg9MC4wIG5hbF9ocmQ9bm9uZSBmaWxsZXI9MCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAAAOWWIhAA3//p+C7v8tDDSTjf97w55i3SbRPO4ZY+hkjD5hbkAkL3zpJ6h/LR1CAABzgB1kqqzUorlhQAAAAxBmiQYhn/+qZYADLgAAAAJQZ5CQhX/AAj5IQADQGgcIQADQGgcAAAACQGeYUQn/wALKCEAA0BoHAAAAAkBnmNEJ/8ACykhAANAaBwhAANAaBwAAAANQZpoNExDP/6plgAMuSEAA0BoHAAAAAtBnoZFESwr/wAI+SEAA0BoHCEAA0BoHAAAAAkBnqVEJ/8ACykhAANAaBwAAAAJAZ6nRCf/AAsoIQADQGgcIQADQGgcAAAADUGarDRMQz/+qZYADLghAANAaBwAAAALQZ7KRRUsK/8ACPkhAANAaBwAAAAJAZ7pRCf/AAsoIQADQGgcIQADQGgcAAAACQGe60Qn/wALKCEAA0BoHAAAAA1BmvA0TEM//qmWAAy5IQADQGgcIQADQGgcAAAAC0GfDkUVLCv/AAj5IQADQGgcAAAACQGfLUQn/wALKSEAA0BoHCEAA0BoHAAAAAkBny9EJ/8ACyghAANAaBwAAAANQZs0NExDP/6plgAMuCEAA0BoHAAAAAtBn1JFFSwr/wAI+SEAA0BoHCEAA0BoHAAAAAkBn3FEJ/8ACyghAANAaBwAAAAJAZ9zRCf/AAsoIQADQGgcIQADQGgcAAAADUGbeDRMQz/+qZYADLkhAANAaBwAAAALQZ+WRRUsK/8ACPghAANAaBwhAANAaBwAAAAJAZ+1RCf/AAspIQADQGgcAAAACQGft0Qn/wALKSEAA0BoHCEAA0BoHAAAAA1Bm7w0TEM//qmWAAy4IQADQGgcAAAAC0Gf2kUVLCv/AAj5IQADQGgcAAAACQGf+UQn/wALKCEAA0BoHCEAA0BoHAAAAAkBn/tEJ/8ACykhAANAaBwAAAANQZvgNExDP/6plgAMuSEAA0BoHCEAA0BoHAAAAAtBnh5FFSwr/wAI+CEAA0BoHAAAAAkBnj1EJ/8ACyghAANAaBwhAANAaBwAAAAJAZ4/RCf/AAspIQADQGgcAAAADUGaJDRMQz/+qZYADLghAANAaBwAAAALQZ5CRRUsK/8ACPkhAANAaBwhAANAaBwAAAAJAZ5hRCf/AAsoIQADQGgcAAAACQGeY0Qn/wALKSEAA0BoHCEAA0BoHAAAAA1Bmmg0TEM//qmWAAy5IQADQGgcAAAAC0GehkUVLCv/AAj5IQADQGgcIQADQGgcAAAACQGepUQn/wALKSEAA0BoHAAAAAkBnqdEJ/8ACyghAANAaBwAAAANQZqsNExDP/6plgAMuCEAA0BoHCEAA0BoHAAAAAtBnspFFSwr/wAI+SEAA0BoHAAAAAkBnulEJ/8ACyghAANAaBwhAANAaBwAAAAJAZ7rRCf/AAsoIQADQGgcAAAADUGa8DRMQz/+qZYADLkhAANAaBwhAANAaBwAAAALQZ8ORRUsK/8ACPkhAANAaBwAAAAJAZ8tRCf/AAspIQADQGgcIQADQGgcAAAACQGfL0Qn/wALKCEAA0BoHAAAAA1BmzQ0TEM//qmWAAy4IQADQGgcAAAAC0GfUkUVLCv/AAj5IQADQGgcIQADQGgcAAAACQGfcUQn/wALKCEAA0BoHAAAAAkBn3NEJ/8ACyghAANAaBwhAANAaBwAAAANQZt4NExC//6plgAMuSEAA0BoHAAAAAtBn5ZFFSwr/wAI+CEAA0BoHCEAA0BoHAAAAAkBn7VEJ/8ACykhAANAaBwAAAAJAZ+3RCf/AAspIQADQGgcAAAADUGbuzRMQn/+nhAAYsAhAANAaBwhAANAaBwAAAAJQZ/aQhP/AAspIQADQGgcAAAACQGf+UQn/wALKCEAA0BoHCEAA0BoHCEAA0BoHCEAA0BoHCEAA0BoHCEAA0BoHAAACiFtb292AAAAbG12aGQAAAAA1YCCX9WAgl8AAAPoAAAH/AABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAGGlvZHMAAAAAEICAgAcAT////v7/AAAF+XRyYWsAAABcdGtoZAAAAAPVgIJf1YCCXwAAAAEAAAAAAAAH0AAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAygAAAMoAAAAAACRlZHRzAAAAHGVsc3QAAAAAAAAAAQAAB9AAABdwAAEAAAAABXFtZGlhAAAAIG1kaGQAAAAA1YCCX9WAgl8AAV+QAAK/IFXEAAAAAAAtaGRscgAAAAAAAAAAdmlkZQAAAAAAAAAAAAAAAFZpZGVvSGFuZGxlcgAAAAUcbWluZgAAABR2bWhkAAAAAQAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAAE3HN0YmwAAACYc3RzZAAAAAAAAAABAAAAiGF2YzEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAygDKAEgAAABIAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY//8AAAAyYXZjQwFNQCj/4QAbZ01AKOyho3ySTUBAQFAAAAMAEAAr8gDxgxlgAQAEaO+G8gAAABhzdHRzAAAAAAAAAAEAAAA8AAALuAAAABRzdHNzAAAAAAAAAAEAAAABAAAB8GN0dHMAAAAAAAAAPAAAAAEAABdwAAAAAQAAOpgAAAABAAAXcAAAAAEAAAAAAAAAAQAAC7gAAAABAAA6mAAAAAEAABdwAAAAAQAAAAAAAAABAAALuAAAAAEAADqYAAAAAQAAF3AAAAABAAAAAAAAAAEAAAu4AAAAAQAAOpgAAAABAAAXcAAAAAEAAAAAAAAAAQAAC7gAAAABAAA6mAAAAAEAABdwAAAAAQAAAAAAAAABAAALuAAAAAEAADqYAAAAAQAAF3AAAAABAAAAAAAAAAEAAAu4AAAAAQAAOpgAAAABAAAXcAAAAAEAAAAAAAAAAQAAC7gAAAABAAA6mAAAAAEAABdwAAAAAQAAAAAAAAABAAALuAAAAAEAADqYAAAAAQAAF3AAAAABAAAAAAAAAAEAAAu4AAAAAQAAOpgAAAABAAAXcAAAAAEAAAAAAAAAAQAAC7gAAAABAAA6mAAAAAEAABdwAAAAAQAAAAAAAAABAAALuAAAAAEAADqYAAAAAQAAF3AAAAABAAAAAAAAAAEAAAu4AAAAAQAAOpgAAAABAAAXcAAAAAEAAAAAAAAAAQAAC7gAAAABAAA6mAAAAAEAABdwAAAAAQAAAAAAAAABAAALuAAAAAEAAC7gAAAAAQAAF3AAAAABAAAAAAAAABxzdHNjAAAAAAAAAAEAAAABAAAAAQAAAAEAAAEEc3RzegAAAAAAAAAAAAAAPAAAAzQAAAAQAAAADQAAAA0AAAANAAAAEQAAAA8AAAANAAAADQAAABEAAAAPAAAADQAAAA0AAAARAAAADwAAAA0AAAANAAAAEQAAAA8AAAANAAAADQAAABEAAAAPAAAADQAAAA0AAAARAAAADwAAAA0AAAANAAAAEQAAAA8AAAANAAAADQAAABEAAAAPAAAADQAAAA0AAAARAAAADwAAAA0AAAANAAAAEQAAAA8AAAANAAAADQAAABEAAAAPAAAADQAAAA0AAAARAAAADwAAAA0AAAANAAAAEQAAAA8AAAANAAAADQAAABEAAAANAAAADQAAAQBzdGNvAAAAAAAAADwAAAAwAAADZAAAA3QAAAONAAADoAAAA7kAAAPQAAAD6wAAA/4AAAQXAAAELgAABEMAAARcAAAEbwAABIwAAAShAAAEugAABM0AAATkAAAE/wAABRIAAAUrAAAFQgAABV0AAAVwAAAFiQAABaAAAAW1AAAFzgAABeEAAAX+AAAGEwAABiwAAAY/AAAGVgAABnEAAAaEAAAGnQAABrQAAAbPAAAG4gAABvUAAAcSAAAHJwAAB0AAAAdTAAAHcAAAB4UAAAeeAAAHsQAAB8gAAAfjAAAH9gAACA8AAAgmAAAIQQAACFQAAAhnAAAIhAAACJcAAAMsdHJhawAAAFx0a2hkAAAAA9WAgl/VgIJfAAAAAgAAAAAAAAf8AAAAAAAAAAAAAAABAQAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAACsm1kaWEAAAAgbWRoZAAAAADVgIJf1YCCXwAArEQAAWAAVcQAAAAAACdoZGxyAAAAAAAAAABzb3VuAAAAAAAAAAAAAAAAU3RlcmVvAAAAAmNtaW5mAAAAEHNtaGQAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAidzdGJsAAAAZ3N0c2QAAAAAAAAAAQAAAFdtcDRhAAAAAAAAAAEAAAAAAAAAAAACABAAAAAArEQAAAAAADNlc2RzAAAAAAOAgIAiAAIABICAgBRAFQAAAAADDUAAAAAABYCAgAISEAaAgIABAgAAABhzdHRzAAAAAAAAAAEAAABYAAAEAAAAABxzdHNjAAAAAAAAAAEAAAABAAAAAQAAAAEAAAAUc3RzegAAAAAAAAAGAAAAWAAAAXBzdGNvAAAAAAAAAFgAAAOBAAADhwAAA5oAAAOtAAADswAAA8oAAAPfAAAD5QAAA/gAAAQLAAAEEQAABCgAAAQ9AAAEUAAABFYAAARpAAAEgAAABIYAAASbAAAErgAABLQAAATHAAAE3gAABPMAAAT5AAAFDAAABR8AAAUlAAAFPAAABVEAAAVXAAAFagAABX0AAAWDAAAFmgAABa8AAAXCAAAFyAAABdsAAAXyAAAF+AAABg0AAAYgAAAGJgAABjkAAAZQAAAGZQAABmsAAAZ+AAAGkQAABpcAAAauAAAGwwAABskAAAbcAAAG7wAABwYAAAcMAAAHIQAABzQAAAc6AAAHTQAAB2QAAAdqAAAHfwAAB5IAAAeYAAAHqwAAB8IAAAfXAAAH3QAAB/AAAAgDAAAICQAACCAAAAg1AAAIOwAACE4AAAhhAAAIeAAACH4AAAiRAAAIpAAACKoAAAiwAAAItgAACLwAAAjCAAAAFnVkdGEAAAAObmFtZVN0ZXJlbwAAAHB1ZHRhAAAAaG1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAAO2lsc3QAAAAzqXRvbwAAACtkYXRhAAAAAQAAAABIYW5kQnJha2UgMC4xMC4yIDIwMTUwNjExMDA=' + +var _createClass = (function () { + function defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i] + descriptor.enumerable = descriptor.enumerable || false + descriptor.configurable = true + if ('value' in descriptor) descriptor.writable = true + Object.defineProperty(target, descriptor.key, descriptor) + } + } + return function (Constructor, protoProps, staticProps) { + if (protoProps) defineProperties(Constructor.prototype, protoProps) + if (staticProps) defineProperties(Constructor, staticProps) + return Constructor + } +})() + +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError('Cannot call a class as a function') + } +} + +// Detect native Wake Lock API support +var nativeWakeLock = 'wakeLock' in navigator + +var NoSleep = (function () { + var _nativeEnabledCompleter; + var _playVideoCompleter; + + function NoSleep() { + var _this = this + + _classCallCheck(this, NoSleep) + + this.nativeEnabled = false + if (nativeWakeLock) { + this._wakeLock = null + var handleVisibilityChange = function handleVisibilityChange() { + if (_this._wakeLock !== null && document.visibilityState === 'visible') { + _this.enable() + } + } + document.addEventListener('visibilitychange', handleVisibilityChange) + document.addEventListener('fullscreenchange', handleVisibilityChange) + } else { + // Set up no sleep video element + this.noSleepVideo = document.createElement('video') + + this.noSleepVideo.setAttribute('title', 'No Sleep') + this.noSleepVideo.setAttribute('playsinline', '') + + this._addSourceToVideo(this.noSleepVideo, 'webm', webm) + this._addSourceToVideo(this.noSleepVideo, 'mp4', mp4) + + this.noSleepVideo.addEventListener('loadedmetadata', function () { + if (_this.noSleepVideo.duration <= 1) { + // webm source + _this.noSleepVideo.setAttribute('loop', '') + } else { + // mp4 source + _this.noSleepVideo.addEventListener('timeupdate', function () { + if (_this.noSleepVideo.currentTime > 0.5) { + _this.noSleepVideo.currentTime = Math.random() + } + }) + } + }) + } + } + + _createClass(NoSleep, [ + { + key: '_addSourceToVideo', + value: function _addSourceToVideo(element, type, dataURI) { + var source = document.createElement('source') + source.src = dataURI + source.type = 'video/' + type + element.appendChild(source) + }, + }, + { + key: 'enable', + value: async function enable() { + var _this2 = this + + if (nativeWakeLock) { + // Disable any previously held wakelocks. + await this.disable() + if (_nativeEnabledCompleter == null) { + _nativeEnabledCompleter = new PromiseCompleter() + } + navigator.wakeLock + .request('screen') + .then(function (wakeLock) { + _this2._wakeLock = wakeLock + _this2.nativeEnabled = true + + // We now have a wakelock. Notify all of the existing callers. + _this2._wakeLock.addEventListener('release', function () { + _this2.nativeEnabled = false + _this2._wakeLock = null + }) + + _nativeEnabledCompleter.complete() + _nativeEnabledCompleter = null + }) + .catch(function (err) { + _this2.nativeEnabled = false + var errorMessage = err.name + ', ' + err.message + _nativeEnabledCompleter.completeError(errorMessage) + _nativeEnabledCompleter = null + }) + // We then wait for screen to be made available. + return _nativeEnabledCompleter.future + } else { + if (_playVideoCompleter == null) { + _playVideoCompleter = new PromiseCompleter() + } + var playPromise = this.noSleepVideo.play() + playPromise.then(function (res) { + _playVideoCompleter.complete() + _playVideoCompleter = null + }).catch(function (err) { + var errorMessage = err.name + ', ' + err.message + _playVideoCompleter.completeError(errorMessage) + _playVideoCompleter = null + }); + return _playVideoCompleter.future + } + }, + }, + { + key: 'disable', + value: async function disable() { + if (nativeWakeLock) { + // If we're still trying to enable the wakelock, wait for it to be enabled + if (_nativeEnabledCompleter != null) { + await _nativeEnabledCompleter.future + } + if (this._wakeLock != null) { + this.nativeEnabled = false + this._wakeLock.release() + } + + this._wakeLock = null + } else { + if (_playVideoCompleter != null) { + await _playVideoCompleter.future + } + this.noSleepVideo.pause() + } + return Promise.resolve(); + }, + }, + { + key: 'isEnabled', + value: async function isEnabled() { + if (nativeWakeLock) { + // If we're still trying to enable the wakelock, wait for it to be enabled + if (_nativeEnabledCompleter != null) { + await _nativeEnabledCompleter.future + } + + return this.nativeEnabled + } else { + if (_playVideoCompleter != null) { + await _playVideoCompleter.future + } + if (this.noSleepVideo == undefined) { + return false + } + + return !this.noSleepVideo.paused + } + }, + }, + ]) + + return NoSleep +})() + +var noSleep = new NoSleep() + +var Wakelock = { + enabled: async function () { + try { + return noSleep.isEnabled() + } catch (e) { + return false + } + }, + toggle: async function (enable) { + try { + if (enable) { + await noSleep.enable() + } else { + await noSleep.disable() + } + } catch (e) { + return Promise.reject(e); + } + return Promise.resolve() + }, +} + +if (nativeWakeLock != true) { + // The first non-native call sometimes throws an error, however, + // the error does not leak the try-catch above. Therefore, this + // is an easy fix that realiably works. + Wakelock.enabled() +} diff --git a/packages/wakelock_plus/lib/src/wakelock_plus_io_plugin.dart b/packages/wakelock_plus/lib/src/wakelock_plus_io_plugin.dart new file mode 100644 index 00000000..31b438f0 --- /dev/null +++ b/packages/wakelock_plus/lib/src/wakelock_plus_io_plugin.dart @@ -0,0 +1,3 @@ +export 'wakelock_plus_linux_plugin.dart'; +export 'wakelock_plus_macos_plugin.dart'; +export 'wakelock_plus_windows_plugin.dart'; diff --git a/packages/wakelock_plus/lib/src/wakelock_plus_linux_plugin.dart b/packages/wakelock_plus/lib/src/wakelock_plus_linux_plugin.dart new file mode 100644 index 00000000..a36f21ed --- /dev/null +++ b/packages/wakelock_plus/lib/src/wakelock_plus_linux_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 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 Function()? _appNameGetter; + DBusObjectPath? _requestHandle; + + Future get _appName => + _appNameGetter?.call() ?? + PackageInfo.fromPlatform().then((info) => info.appName); + + @override + Future 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 get enabled async => _requestHandle != null; +} diff --git a/packages/wakelock_plus/lib/src/wakelock_plus_macos_plugin.dart b/packages/wakelock_plus/lib/src/wakelock_plus_macos_plugin.dart new file mode 100644 index 00000000..03ac0a8e --- /dev/null +++ b/packages/wakelock_plus/lib/src/wakelock_plus_macos_plugin.dart @@ -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 toggle({required bool enable}) async { + await _channel.invokeMethod('toggle', {'enable': enable}); + } + + @override + Future get enabled async => + await _channel.invokeMethod('enabled') as bool; +} diff --git a/packages/wakelock_plus/lib/src/wakelock_plus_web_plugin.dart b/packages/wakelock_plus/lib/src/wakelock_plus_web_plugin.dart new file mode 100644 index 00000000..efea3941 --- /dev/null +++ b/packages/wakelock_plus/lib/src/wakelock_plus_web_plugin.dart @@ -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? _jsLoaded; + + // + // Lazily imports the JS library once, then awaits to ensure that + // it's loaded into the DOM. + // + Future _ensureJsLoaded() async { + _jsLoaded ??= importJsLibrary( + url: 'assets/no_sleep.js', + flutterPluginName: 'wakelock_plus', + ); + return _jsLoaded; + } + + @override + Future 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 get enabled async { + // Make sure the JS library is loaded before calling it. + await _ensureJsLoaded(); + return wakelock_plus_web.enabled(); + } +} diff --git a/packages/wakelock_plus/lib/src/wakelock_plus_windows_plugin.dart b/packages/wakelock_plus/lib/src/wakelock_plus_windows_plugin.dart new file mode 100644 index 00000000..67a3d6c6 --- /dev/null +++ b/packages/wakelock_plus/lib/src/wakelock_plus_windows_plugin.dart @@ -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 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 get enabled async => _enabled; + + @override + bool get isMock => false; +} diff --git a/packages/wakelock_plus/lib/src/web_impl/import_js_library.dart b/packages/wakelock_plus/lib/src/web_impl/import_js_library.dart new file mode 100644 index 00000000..88f4fa7e --- /dev/null +++ b/packages/wakelock_plus/lib/src/web_impl/import_js_library.dart @@ -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 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 _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 `` and returns a +/// Future that resolves when all load. +Future _importJSLibraries(List 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 = >[]; + 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; +} diff --git a/packages/wakelock_plus/lib/src/web_impl/js_wakelock.dart b/packages/wakelock_plus/lib/src/web_impl/js_wakelock.dart new file mode 100644 index 00000000..972c77ee --- /dev/null +++ b/packages/wakelock_plus/lib/src/web_impl/js_wakelock.dart @@ -0,0 +1,20 @@ +@JS('Wakelock') +library; + +import 'dart:js_interop'; + +@JS('toggle') +external JSPromise _toggle(JSBoolean enable); + +/// Toggles the JS wakelock. +Future toggle(bool enable) { + return _toggle(enable.toJS).toDart.then((_) => null); +} + +@JS('enabled') +external JSPromise _enabled(); + +/// Returns a JS promise of whether the wakelock is enabled or not. +Future enabled() { + return _enabled().toDart.then((enabled) => enabled.toDart); +} diff --git a/packages/wakelock_plus/lib/wakelock_plus.dart b/packages/wakelock_plus/lib/wakelock_plus.dart new file mode 100644 index 00000000..23f85dac --- /dev/null +++ b/packages/wakelock_plus/lib/wakelock_plus.dart @@ -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 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 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 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 get enabled => wakelockPlusPlatformInstance.enabled; +} diff --git a/packages/wakelock_plus/macos/wakelock_plus.podspec b/packages/wakelock_plus/macos/wakelock_plus.podspec new file mode 100644 index 00000000..9e2cf7d2 --- /dev/null +++ b/packages/wakelock_plus/macos/wakelock_plus.podspec @@ -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 diff --git a/packages/wakelock_plus/macos/wakelock_plus/Package.swift b/packages/wakelock_plus/macos/wakelock_plus/Package.swift new file mode 100644 index 00000000..35bcca25 --- /dev/null +++ b/packages/wakelock_plus/macos/wakelock_plus/Package.swift @@ -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"), + ] + ) + ] +) \ No newline at end of file diff --git a/packages/wakelock_plus/macos/wakelock_plus/Sources/wakelock_plus/Resources/PrivacyInfo.xcprivacy b/packages/wakelock_plus/macos/wakelock_plus/Sources/wakelock_plus/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..918d80be --- /dev/null +++ b/packages/wakelock_plus/macos/wakelock_plus/Sources/wakelock_plus/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,12 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/packages/wakelock_plus/macos/wakelock_plus/Sources/wakelock_plus/WakelockPlusMacosPlugin.swift b/packages/wakelock_plus/macos/wakelock_plus/Sources/wakelock_plus/WakelockPlusMacosPlugin.swift new file mode 100644 index 00000000..ef435e4f --- /dev/null +++ b/packages/wakelock_plus/macos/wakelock_plus/Sources/wakelock_plus/WakelockPlusMacosPlugin.swift @@ -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 + 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 + } + } +} diff --git a/packages/wakelock_plus/pubspec.yaml b/packages/wakelock_plus/pubspec.yaml new file mode 100644 index 00000000..af6c15b9 --- /dev/null +++ b/packages/wakelock_plus/pubspec.yaml @@ -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 \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 6bac7215..d633b372 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -242,11 +242,11 @@ packages: dependency: "direct main" description: path: "packages/connectivity_plus/connectivity_plus" - ref: "4c0d4071150481f76d753c6ce1ff325bb959b9b5" - resolved-ref: "4c0d4071150481f76d753c6ce1ff325bb959b9b5" - url: "https://github.com/edde746/plus_plugins" + ref: bf04cdf66598dc3fca274b8b1db2b92b0bf6b73e + resolved-ref: bf04cdf66598dc3fca274b8b1db2b92b0bf6b73e + url: "https://github.com/fluttercommunity/plus_plugins" source: git - version: "7.1.1" + version: "7.2.0" connectivity_plus_platform_interface: dependency: transitive description: @@ -702,12 +702,11 @@ packages: material_symbols_icons: dependency: "direct main" description: - path: "." - ref: "1d8cd83" - resolved-ref: "1d8cd8325db29691a01738a25f220dd3515bd1e2" - url: "https://github.com/edde746/material_symbols_icons" - source: git - version: "4.2906.0" + name: material_symbols_icons + sha256: "49c532dd0b74544e9d8d93ec0f821d52ec532e7c9263c889ebe71b1be4f34ba7" + url: "https://pub.dev" + source: hosted + version: "4.2951.0" meta: dependency: transitive description: @@ -987,7 +986,7 @@ packages: path: "packages/saf_util" relative: true source: path - version: "2.0.0" + version: "3.1.0" screen_retriever: dependency: transitive description: @@ -1029,14 +1028,13 @@ packages: source: hosted version: "0.2.0" sentry: - dependency: "direct overridden" + dependency: transitive description: - path: "packages/dart" - ref: "build/fetch-native-zip" - resolved-ref: "711f54f34d6cda9cccfb57519b9e3b72dd116b69" - url: "https://github.com/edde746/sentry-dart" - source: git - version: "9.16.0" + name: sentry + sha256: "09c573f98ff6c5e98d527f351f037eb8ea9ff684a6e9e84b2e91357144016254" + url: "https://pub.dev" + source: hosted + version: "9.24.0" sentry_dart_plugin: dependency: "direct dev" description: @@ -1048,12 +1046,11 @@ packages: sentry_flutter: dependency: "direct main" description: - path: "packages/flutter" - ref: "build/fetch-native-zip" - resolved-ref: "711f54f34d6cda9cccfb57519b9e3b72dd116b69" - url: "https://github.com/edde746/sentry-dart" - source: git - version: "9.16.0" + name: sentry_flutter + sha256: "4a04b7e1901b8128df783b6d36d48706b07b0cd055e621517c998de87f5dafb9" + url: "https://pub.dev" + source: hosted + version: "9.24.0" serial_csv: dependency: transitive description: @@ -1406,12 +1403,10 @@ packages: wakelock_plus: dependency: "direct main" description: - path: wakelock_plus - ref: "8595fee595c952b73d430f2eab05f2e0d858290e" - resolved-ref: "8595fee595c952b73d430f2eab05f2e0d858290e" - url: "https://github.com/edde746/wakelock_plus" - source: git - version: "1.4.0" + path: "packages/wakelock_plus" + relative: true + source: path + version: "1.5.2" wakelock_plus_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2790cb6c..af61e7a2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,8 +29,8 @@ dependencies: duration: ^4.0.3 connectivity_plus: git: - url: https://github.com/edde746/plus_plugins - ref: 4c0d4071150481f76d753c6ce1ff325bb959b9b5 + url: https://github.com/fluttercommunity/plus_plugins + ref: bf04cdf66598dc3fca274b8b1db2b92b0bf6b73e path: packages/connectivity_plus/connectivity_plus os_media_controls: git: @@ -38,10 +38,7 @@ dependencies: ref: 75556a968a4a1ebc42fbb8c31942c2418d4326e1 rate_limiter: ^1.0.0 wakelock_plus: - git: - url: https://github.com/edde746/wakelock_plus - ref: 8595fee595c952b73d430f2eab05f2e0d858290e - path: wakelock_plus + path: packages/wakelock_plus path_provider: ^2.1.0 path: ^1.9.0 universal_gamepad: ^1.5.7 @@ -51,23 +48,27 @@ dependencies: cryptography: ^2.9.0 file_picker: ^10.3.10 saf_util: - # Vendored 2.0.0 with a pendingResult lifecycle fix (see its CHANGELOG) — - # upstream double-replies MethodChannel results and crashes the activity. + # Vendored upstream 3.1.0 with a pending Result lifecycle fix (CHANGELOG) — + # upstream still double-replies MethodChannel results and crashes the Activity. path: packages/saf_util saf_stream: ^2.0.0 - material_symbols_icons: ^4.2906.0 + material_symbols_icons: ^4.2951.0 web_socket_channel: ^3.0.1 dart_discord_presence: ^1.2.0 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: git: url: https://github.com/edde746/background_downloader ref: 021e2075260e324b7440a5c81b57bf0242d35020 - sentry_flutter: - git: - url: https://github.com/edde746/sentry-dart - path: packages/flutter - ref: build/fetch-native-zip + sentry_flutter: ^9.24.0 auto_updater: git: url: https://github.com/edde746/auto_updater @@ -113,20 +114,6 @@ dependency_overrides: url: https://github.com/edde746/auto_updater ref: 9e150f7 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: org: plezy project: plezy diff --git a/tvos/Runner/Plugins/shared_preferences_foundation/SharedPreferencesPlugin.swift b/tvos/Runner/Plugins/shared_preferences_foundation/SharedPreferencesPlugin.swift index 37f135f0..54610531 100644 --- a/tvos/Runner/Plugins/shared_preferences_foundation/SharedPreferencesPlugin.swift +++ b/tvos/Runner/Plugins/shared_preferences_foundation/SharedPreferencesPlugin.swift @@ -1,6 +1,11 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // 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 @@ -111,8 +116,7 @@ public class SharedPreferencesPlugin: NSObject, FlutterPlugin, UserDefaultsApi { 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) } diff --git a/tvos/Runner/Plugins/shared_preferences_foundation/messages.g.swift b/tvos/Runner/Plugins/shared_preferences_foundation/messages.g.swift index 5c019a7c..3afc843a 100644 --- a/tvos/Runner/Plugins/shared_preferences_foundation/messages.g.swift +++ b/tvos/Runner/Plugins/shared_preferences_foundation/messages.g.swift @@ -3,6 +3,10 @@ // found in the LICENSE file. // Autogenerated from Pigeon (v26.1.0), do not edit directly. // 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 @@ -146,8 +150,7 @@ struct SharedPreferencesPigeonOptions: Hashable { suiteName ] } - static func == (lhs: SharedPreferencesPigeonOptions, rhs: SharedPreferencesPigeonOptions) -> Bool - { + static func == (lhs: SharedPreferencesPigeonOptions, rhs: SharedPreferencesPigeonOptions) -> Bool { return deepEqualsmessages(lhs.toList(), rhs.toList()) } func hash(into hasher: inout Hasher) {