diff --git a/README.md b/README.md index 40f52ae..9b6728c 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,26 @@ Widget build (BuildContext context) { ); } ``` +- wesetup new entry point for secondary display so we can decided what we need to call for initialization. Works only for android for now +```dart +@pragma('vm:entry-point') +void secondaryDisplayMain() { + /// do something that don't break plugin registration here. +} +``` +- Able to package android release build. Works fine in example app. + +- Tested example app in android tab and ios tab and things work as expected. Ensure the devices have USB C 3.0 and above else HDMI out is not supported. + +- In case of iOS, please refer to example app app delegate. There are few lines of code which needs to be added to your app's app delegate as well for this to work fine in iOS. + +- Updated optional issues and null checks + +- Added option to hide second display from the first + +- WIP support second main in iOS for extended display + +- WIP Send data back from 2nd to 1st display You can take a look at our example to learn more about how the plugin works diff --git a/android/build.gradle b/android/build.gradle index 7c9b74b..8fc687f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,14 +2,14 @@ group 'com.namit.presentation_displays' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.5.20' + ext.kotlin_version = '1.7.20' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -17,6 +17,7 @@ buildscript { rootProject.allprojects { repositories { google() + mavenCentral() } } @@ -24,13 +25,13 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 31 + compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - minSdkVersion 19 + minSdkVersion 21 } lintOptions { disable 'InvalidPackage' @@ -39,5 +40,6 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'com.google.code.gson:gson:2.8.6' + implementation 'com.google.code.gson:gson:2.10.1' + implementation 'com.android.support:support-annotations:28.0.0' } diff --git a/android/gradle.properties b/android/gradle.properties index 38c8d45..94adc3a 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx1536M -android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 3c9d085..3c472b9 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/android/src/main/kotlin/com/namit/presentation_displays/DisplayJson.kt b/android/src/main/kotlin/com/namit/presentation_displays/DisplayJson.kt index a93232b..3b51244 100644 --- a/android/src/main/kotlin/com/namit/presentation_displays/DisplayJson.kt +++ b/android/src/main/kotlin/com/namit/presentation_displays/DisplayJson.kt @@ -13,4 +13,4 @@ data class DisplayJson( val rotation: Int, @SerializedName("name") val name: String -) +) \ No newline at end of file diff --git a/android/src/main/kotlin/com/namit/presentation_displays/PresentationDisplaysPlugin.kt b/android/src/main/kotlin/com/namit/presentation_displays/PresentationDisplaysPlugin.kt index 150819b..757ce05 100644 --- a/android/src/main/kotlin/com/namit/presentation_displays/PresentationDisplaysPlugin.kt +++ b/android/src/main/kotlin/com/namit/presentation_displays/PresentationDisplaysPlugin.kt @@ -3,17 +3,20 @@ package com.namit.presentation_displays import android.content.ContentValues.TAG import android.content.Context import android.hardware.display.DisplayManager +import android.os.Handler +import android.os.Looper import android.util.Log import android.view.Display import androidx.annotation.NonNull -import androidx.core.content.ContextCompat.getSystemService import com.google.gson.Gson +import io.flutter.FlutterInjector import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngineCache import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.PluginRegistry @@ -22,121 +25,176 @@ import org.json.JSONObject /** PresentationDisplaysPlugin */ class PresentationDisplaysPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler { - private lateinit var channel: MethodChannel - private var flutterEngineChannel: MethodChannel? = null - private var displayManager: DisplayManager? = null - private var context: Context? = null + private lateinit var channel: MethodChannel + private lateinit var eventChannel: EventChannel + private var flutterEngineChannel: MethodChannel? = null + private var context: Context? = null + private var presentation: PresentationDisplay? = null - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - channel = MethodChannel(flutterPluginBinding.flutterEngine.dartExecutor, viewTypeId) - channel.setMethodCallHandler(this) - } + override fun onAttachedToEngine( + @NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding + ) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, viewTypeId) + channel.setMethodCallHandler(this) - companion object { - private const val viewTypeId = "presentation_displays_plugin" + eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, viewTypeEventsId) + displayManager = + flutterPluginBinding.applicationContext.getSystemService(Context.DISPLAY_SERVICE) as + DisplayManager + val displayConnectedStreamHandler = DisplayConnectedStreamHandler(displayManager) + eventChannel.setStreamHandler(displayConnectedStreamHandler) + } - @JvmStatic - fun registerWith(registrar: PluginRegistry.Registrar) { - val channel = MethodChannel(registrar.messenger(), viewTypeId) - channel.setMethodCallHandler(PresentationDisplaysPlugin()) - } - } + companion object { + private const val viewTypeId = "presentation_displays_plugin" + private const val viewTypeEventsId = "presentation_displays_plugin_events" + private var displayManager: DisplayManager? = null + + /** @hide */ + @Suppress("unused", "DEPRECATION") + @JvmStatic + fun registerWith(registrar: PluginRegistry.Registrar) { + val channel = MethodChannel(registrar.messenger(), viewTypeId) + channel.setMethodCallHandler(PresentationDisplaysPlugin()) - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) + val eventChannel = EventChannel(registrar.messenger(), viewTypeEventsId) + displayManager = + registrar.activity()!!.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + val displayConnectedStreamHandler = DisplayConnectedStreamHandler(displayManager) + eventChannel.setStreamHandler(displayConnectedStreamHandler) } + } - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - Log.i(TAG, "Channel: method: ${call.method} | arguments: ${call.arguments}") - when (call.method) { - "showPresentation" -> { - try { - val obj = JSONObject(call.arguments as String) - Log.i( - TAG, - "Channel: method: ${call.method} | displayId: ${obj.getInt("displayId")} | routerName: ${ - obj.getString("routerName") - }" - ) - val displayId: Int = obj.getInt("displayId") - val tag: String = obj.getString("routerName") - val display = displayManager?.getDisplay(displayId) - if (display != null) { - val flutterEngine = createFlutterEngine(tag) - flutterEngine?.let { - flutterEngineChannel = MethodChannel( - it.dartExecutor.binaryMessenger, - "${viewTypeId}_engine" - ) - val presentation = - context?.let { it1 -> PresentationDisplay(it1, tag, display) } - Log.i(TAG, "presentation: $presentation") - presentation?.show() - result.success(true) - } ?: result.error("404", "Can't find FlutterEngine", null) - } else { - result.error("404", "Can't find display with displayId is $displayId", null) - } - } catch (e: Exception) { - result.error(call.method, e.message, null) - } - } - "listDisplay" -> { - val listJson = ArrayList() - val category = call.arguments - val displays = displayManager?.getDisplays(category as String?) - if (displays != null) { - for (display: Display in displays) { - Log.i(TAG, "display: $display") - val d = DisplayJson( - display.displayId, - display.flags, - display.rotation, - display.name - ) - listJson.add(d) - } - } - result.success(Gson().toJson(listJson)) - } - "transferDataToPresentation" -> { - try { - flutterEngineChannel?.invokeMethod("DataTransfer", call.arguments) - result.success(true) - } catch (e: Exception) { - result.success(false) - } + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + eventChannel.setStreamHandler(null) + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + Log.i(TAG, "Channel: method: ${call.method} | arguments: ${call.arguments}") + when (call.method) { + "showPresentation" -> { + try { + val obj = JSONObject(call.arguments as String) + Log.i( + TAG, + "Channel: method: ${call.method} | displayId: ${obj.getInt("displayId")} | routerName: ${ + obj.getString("routerName") + }" + ) + val displayId: Int = obj.getInt("displayId") + val tag: String = obj.getString("routerName") + val display = displayManager?.getDisplay(displayId) + if (display != null) { + val flutterEngine = createFlutterEngine(tag) + flutterEngine?.let { + flutterEngineChannel = + MethodChannel(it.dartExecutor.binaryMessenger, "${viewTypeId}_engine") + presentation = context?.let { it1 -> PresentationDisplay(it1, tag, display) } + Log.i(TAG, "presentation: $presentation") + presentation?.show() + + result.success(true) } + ?: result.error("404", "Can't find FlutterEngine", null) + } else { + result.error("404", "Can't find display with displayId is $displayId", null) + } + } catch (e: Exception) { + result.error(call.method, e.message, null) } - } + } + "hidePresentation" -> { + try { + val obj = JSONObject(call.arguments as String) + Log.i(TAG, "Channel: method: ${call.method} | displayId: ${obj.getInt("displayId")}") - private fun createFlutterEngine(tag: String): FlutterEngine? { - if (context == null) - return null - if (FlutterEngineCache.getInstance().get(tag) == null) { - val flutterEngine = FlutterEngine(context!!) - flutterEngine.navigationChannel.setInitialRoute(tag) - flutterEngine.dartExecutor.executeDartEntrypoint( - DartExecutor.DartEntrypoint.createDefault() - ) - flutterEngine.lifecycleChannel.appIsResumed() - // Cache the FlutterEngine to be used by FlutterActivity. - FlutterEngineCache.getInstance().put(tag, flutterEngine) + presentation?.dismiss() + presentation = null + result.success(true) + } catch (e: Exception) { + result.error(call.method, e.message, null) + } + } + "listDisplay" -> { + val listJson = ArrayList() + val category = call.arguments + val displays = displayManager?.getDisplays(category as String?) + if (displays != null) { + for (display: Display in displays) { + Log.i(TAG, "display: $display") + val d = DisplayJson(display.displayId, display.flags, display.rotation, display.name) + listJson.add(d) + } + } + result.success(Gson().toJson(listJson)) + } + "transferDataToPresentation" -> { + try { + flutterEngineChannel?.invokeMethod("DataTransfer", call.arguments) + result.success(true) + } catch (e: Exception) { + result.success(false) } - return FlutterEngineCache.getInstance().get(tag) + } } + } - override fun onDetachedFromActivity() { + private fun createFlutterEngine(tag: String): FlutterEngine? { + if (context == null) return null + if (FlutterEngineCache.getInstance().get(tag) == null) { + val flutterEngine = FlutterEngine(context!!) + flutterEngine.navigationChannel.setInitialRoute(tag) + FlutterInjector.instance().flutterLoader().startInitialization(context!!) + val path = FlutterInjector.instance().flutterLoader().findAppBundlePath() + val entrypoint = DartExecutor.DartEntrypoint(path, "secondaryDisplayMain") + flutterEngine.dartExecutor.executeDartEntrypoint(entrypoint) + flutterEngine.lifecycleChannel.appIsResumed() + // Cache the FlutterEngine to be used by FlutterActivity. + FlutterEngineCache.getInstance().put(tag, flutterEngine) } + return FlutterEngineCache.getInstance().get(tag) + } - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - } + override fun onDetachedFromActivity() {} - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - this.context = binding.activity - this.displayManager = context?.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager - } + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {} - override fun onDetachedFromActivityForConfigChanges() { - } + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + this.context = binding.activity + displayManager = context?.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + } + + override fun onDetachedFromActivityForConfigChanges() {} +} + +class DisplayConnectedStreamHandler(private var displayManager: DisplayManager?) : + EventChannel.StreamHandler { + private var sink: EventChannel.EventSink? = null + private var handler: Handler? = null + + private val displayListener = + object : DisplayManager.DisplayListener { + override fun onDisplayAdded(displayId: Int) { + sink?.success(1) + } + + override fun onDisplayRemoved(displayId: Int) { + sink?.success(0) + } + + override fun onDisplayChanged(p0: Int) {} + } + + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + sink = events + handler = Handler(Looper.getMainLooper()) + displayManager?.registerDisplayListener(displayListener, handler) + } + + override fun onCancel(arguments: Any?) { + sink = null + handler = null + displayManager?.unregisterDisplayListener(displayListener) + } } diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 7f498e1..8873008 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -8,7 +8,7 @@ if (localPropertiesFile.exists()) { def flutterRoot = localProperties.getProperty('flutter.sdk') if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") + throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -39,7 +39,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.namit.presentation_displays_example" - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 31 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -52,6 +52,10 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + namespace 'com.namit.presentation_displays_example' + lint { + disable 'InvalidPackage' + } } flutter { diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml index 4b69847..f880684 100644 --- a/example/android/app/src/debug/AndroidManifest.xml +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index cbba71d..28838e6 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/example/android/build.gradle b/example/android/build.gradle index 8a8fcd4..6f393b6 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.5.20' + ext.kotlin_version = '1.7.20' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 38c8d45..94adc3a 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx1536M -android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index bc6a58a..6b66533 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index f2872cf..4f8d4d2 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/example/ios/Podfile b/example/ios/Podfile index 1e8c3c9..88359b2 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 32d0913..a8ae179 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -14,9 +14,9 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/presentation_displays/ios" SPEC CHECKSUMS: - Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 presentation_displays: 02a9b05f708afe31d23564988f285b4d8019ddcd -PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c +PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 -COCOAPODS: 1.11.2 +COCOAPODS: 1.14.3 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 63ad7b7..685a1f1 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -13,7 +13,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - DB7291D450DE160A17BFC8FC /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B99F4C113FE0557CB636ABE0 /* Pods_Runner.framework */; }; + DBAE77DD5D21C66D5647DD42 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BB6A46A669E6E9DB5680B64 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -30,14 +30,15 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0BB6A46A669E6E9DB5680B64 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 4312912F6877C2E8B5286C5E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 4A7A3DD543CBD0AC87B9CD6E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 90E829BE21CD01C0487FB55F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 7D838C0D41F8A35C27D4C46C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -45,8 +46,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B99F4C113FE0557CB636ABE0 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - CE01AA7B42925A06705D5099 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + C0599EA2B041B0EDE8716590 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -54,19 +54,19 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB7291D450DE160A17BFC8FC /* Pods_Runner.framework in Frameworks */, + DBAE77DD5D21C66D5647DD42 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 802377E58FE3DBA15DBE284F /* Pods */ = { + 624DAE587250BE9959AB91ED /* Pods */ = { isa = PBXGroup; children = ( - 90E829BE21CD01C0487FB55F /* Pods-Runner.debug.xcconfig */, - 4312912F6877C2E8B5286C5E /* Pods-Runner.release.xcconfig */, - CE01AA7B42925A06705D5099 /* Pods-Runner.profile.xcconfig */, + 7D838C0D41F8A35C27D4C46C /* Pods-Runner.debug.xcconfig */, + 4A7A3DD543CBD0AC87B9CD6E /* Pods-Runner.release.xcconfig */, + C0599EA2B041B0EDE8716590 /* Pods-Runner.profile.xcconfig */, ); name = Pods; path = Pods; @@ -89,8 +89,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - 802377E58FE3DBA15DBE284F /* Pods */, - BE6751830CB670072A3F7846 /* Frameworks */, + 624DAE587250BE9959AB91ED /* Pods */, + B4DF188180DDF6A32C123F4B /* Frameworks */, ); sourceTree = ""; }; @@ -125,10 +125,10 @@ name = "Supporting Files"; sourceTree = ""; }; - BE6751830CB670072A3F7846 /* Frameworks */ = { + B4DF188180DDF6A32C123F4B /* Frameworks */ = { isa = PBXGroup; children = ( - B99F4C113FE0557CB636ABE0 /* Pods_Runner.framework */, + 0BB6A46A669E6E9DB5680B64 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -140,14 +140,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 48725AEC0B6B88BD9E8E2459 /* [CP] Check Pods Manifest.lock */, + 3A83F31962615BA318D35675 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - B8BC531D762D20DFF25FAEEC /* [CP] Embed Pods Frameworks */, + A85E4277070E1E475E143A7B /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -164,7 +164,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -206,44 +206,47 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 3A83F31962615BA318D35675 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "Thin Binary"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; - 48725AEC0B6B88BD9E8E2459 /* [CP] Check Pods Manifest.lock */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); + name = "Thin Binary"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -256,7 +259,7 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - B8BC531D762D20DFF25FAEEC /* [CP] Embed Pods Frameworks */ = { + A85E4277070E1E475E143A7B /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -348,7 +351,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -433,7 +436,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -482,7 +485,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3db53b6..b52b2e6 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/example/lib/main.dart b/example/lib/main.dart index 24374eb..3a55e24 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -19,9 +19,28 @@ Route generateRoute(RouteSettings settings) { } void main() { + debugPrint('first main'); runApp(const MyApp()); } +@pragma('vm:entry-point') +void secondaryDisplayMain() { + debugPrint('second main'); + runApp(const MySecondApp()); +} + +class MySecondApp extends StatelessWidget { + const MySecondApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + onGenerateRoute: generateRoute, + initialRoute: 'presentation', + ); + } +} + class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @@ -77,6 +96,16 @@ class _DisplayManagerScreenState extends State { final TextEditingController _nameOfIndexController = TextEditingController(); String _nameOfIndex = ""; + @override + void initState() { + displayManager.connectedDisplaysChangedStream?.listen( + (event) { + debugPrint("connected displays changed: $event"); + }, + ); + super.initState(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -91,6 +120,7 @@ class _DisplayManagerScreenState extends State { children: [ _getDisplays(), _showPresentation(), + _hidePresentation(), _transferData(), _getDisplayeById(), _getDisplayByIndex(), @@ -166,6 +196,38 @@ class _DisplayManagerScreenState extends State { ); } + Widget _hidePresentation() { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: _indexToShareController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Index to hide screen', + ), + ), + ), + Button( + title: "Hide presentation", + onPressed: () async { + int? displayId = int.tryParse(_indexToShareController.text); + if (displayId != null) { + for (final display in displays) { + if (display?.displayId == displayId) { + displayManager.hideSecondaryDisplay(displayId: displayId); + } + } + } + }), + const Divider(), + ], + ); + } + Widget _transferData() { return Column( mainAxisAlignment: MainAxisAlignment.center, @@ -279,7 +341,7 @@ class _SecondaryScreenState extends State { Widget build(BuildContext context) { return Scaffold( body: SecondaryDisplay( - callback: (argument) { + callback: (dynamic argument) { setState(() { value = argument; }); diff --git a/example/pubspec.lock b/example/pubspec.lock index 56e5676..59595ef 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,58 +5,58 @@ packages: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.11.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.0" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.18.0" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: "1989d917fbe8e6b39806207df5a3fdd3d816cbd090fac2ce26fb45e9a71476e5" + url: "https://pub.dev" source: hosted version: "1.0.4" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.1" flutter: dependency: "direct main" description: flutter @@ -66,7 +66,8 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 + url: "https://pub.dev" source: hosted version: "1.0.4" flutter_test: @@ -78,44 +79,49 @@ packages: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c + url: "https://pub.dev" source: hosted version: "1.0.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" source: hosted - version: "0.12.11" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.5.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.10.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.8.3" presentation_displays: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.2.2" + version: "0.2.4" sky_engine: dependency: transitive description: flutter @@ -125,58 +131,66 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" source: hosted - version: "0.4.8" - typed_data: + version: "0.6.1" + vector_math: dependency: transitive description: - name: typed_data - url: "https://pub.dartlang.org" + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "1.3.0" - vector_math: + version: "2.1.4" + web: dependency: transitive description: - name: vector_math - url: "https://pub.dartlang.org" + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "0.3.0" sdks: - dart: ">=2.14.0 <3.0.0" - flutter: ">=2.10.3" + dart: ">=3.2.0-194.0.dev <4.0.0" + flutter: ">=3.10.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 0f6459e..6bec10d 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -3,14 +3,14 @@ description: Demonstrates how to use the presentation_displays plugin. # The following line prevents the package from being accidentally published to # pub.dev using `pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" # Flutter versions prior to 1.12 did not support the # flutter.plugin.platforms map. - flutter: ">=2.10.3" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/ios/Classes/SwiftPresentationDisplaysPlugin.swift b/ios/Classes/SwiftPresentationDisplaysPlugin.swift index ae136b4..90e66d6 100644 --- a/ios/Classes/SwiftPresentationDisplaysPlugin.swift +++ b/ios/Classes/SwiftPresentationDisplaysPlugin.swift @@ -2,120 +2,192 @@ import Flutter import UIKit public class SwiftPresentationDisplaysPlugin: NSObject, FlutterPlugin { - var additionalWindows = [UIScreen:UIWindow]() - var screens = [UIScreen]() - var flutterEngineChannel:FlutterMethodChannel=FlutterMethodChannel() - public static var controllerAdded: ((FlutterViewController)->Void)? - - public override init() { - super.init() - - screens.append(UIScreen.main) - NotificationCenter.default.addObserver(forName: UIScreen.didConnectNotification, - object: nil, queue: nil) { - notification in - - // Get the new screen information. - guard let newScreen = notification.object as? UIScreen else { - return - } - - let screenDimensions = newScreen.bounds - // Configure a window for the screen. - let newWindow = UIWindow(frame: screenDimensions) - newWindow.screen = newScreen - - // You must show the window explicitly. - newWindow.isHidden = true - - // Save a reference to the window in a local array. - self.screens.append(newScreen) - self.additionalWindows[newScreen] = newWindow - } - - NotificationCenter.default.addObserver(forName: - UIScreen.didDisconnectNotification, - object: nil, - queue: nil) { notification in - guard let screen = notification.object as? UIScreen else { - return - } - - // Remove the window associated with the screen. - for s in self.screens { - if s == screen { - if let index = self.screens.firstIndex(of: s) { - self.screens.remove(at: index) - // Remove the window and its contents. - self.additionalWindows.removeValue(forKey: s) - } + var additionalWindows = [UIScreen:UIWindow]() + var screens = [UIScreen]() + var flutterEngineChannel:FlutterMethodChannel?=nil + public static var controllerAdded: ((FlutterViewController)->Void)? + + public override init() { + super.init() + + screens.append(UIScreen.main) + NotificationCenter.default.addObserver(forName: UIScreen.didConnectNotification, + object: nil, queue: nil) { + notification in + + // Get the new screen information. + guard let newScreen = notification.object as? UIScreen else { + return + } + + let screenDimensions = newScreen.bounds + // Configure a window for the screen. + let newWindow = UIWindow(frame: screenDimensions) + newWindow.screen = newScreen + + // You must show the window explicitly. + newWindow.isHidden = true + + // Save a reference to the window in a local array. + self.screens.append(newScreen) + self.additionalWindows[newScreen] = newWindow + + } + + NotificationCenter.default.addObserver(forName: + UIScreen.didDisconnectNotification, + object: nil, + queue: nil) { notification in + guard let screen = notification.object as? UIScreen else { + return + } + + // Remove the window associated with the screen. + for s in self.screens { + if s == screen { + if let index = self.screens.firstIndex(of: s) { + self.screens.remove(at: index) + // Remove the window and its contents. + self.additionalWindows.removeValue(forKey: s) + } + } + } } - } } - } - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "presentation_displays_plugin", binaryMessenger: registrar.messenger()) - let instance = SwiftPresentationDisplaysPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - if call.method == "listDisplay" { - var jsonDisplaysList = "["; - for i in 0.. - { - print(json) - showPresentation(index:json["displayId"] as? Int ?? 1, routerName: json["routerName"] as? String ?? "presentation") + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "presentation_displays_plugin", binaryMessenger: registrar.messenger()) + let instance = SwiftPresentationDisplaysPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + + let eventChannel = FlutterEventChannel(name: "presentation_displays_plugin_events", binaryMessenger: registrar.messenger()) + let displayConnectedStreamHandler = DisplayConnectedStreamHandler() + eventChannel.setStreamHandler(displayConnectedStreamHandler) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + if call.method == "listDisplay" { + var jsonDisplaysList = "["; + for i in 0.. + { + print(json) + showPresentation(index:json["displayId"] as? Int ?? 1, routerName: json["routerName"] as? String ?? "presentation") + result(true) + } + else { + print("bad json") + result(false) + } + } + catch let error as NSError { + print(error) + result(false) + } } - } - catch let error as NSError { - print(error) - } - } - else if call.method=="transferDataToPresentation"{ - self.flutterEngineChannel.invokeMethod("DataTransfer", arguments: call.arguments) + else if call.method=="hidePresentation"{ + let args = call.arguments as? String + let data = args?.data(using: .utf8)! + do { + if let json = try JSONSerialization.jsonObject(with: data ?? Data(), options : .allowFragments) as? Dictionary + { + print(json) + hidePresentation(index:json["displayId"] as? Int ?? 1) + result(true) + } + else { + print("bad json") + result(false) + } + } + catch let error as NSError { + print(error) + result(false) + } + } + else if call.method=="transferDataToPresentation"{ + self.flutterEngineChannel?.invokeMethod("DataTransfer", arguments: call.arguments) + result(true) + } + else + { + result(FlutterMethodNotImplemented) + } + } - else + + private func showPresentation(index:Int, routerName:String ) { - result(FlutterMethodNotImplemented) + if index>0 && index < self.screens.count && self.additionalWindows.keys.contains(self.screens[index]) + { + let screen=self.screens[index] + let window=self.additionalWindows[screen] + + if (window != nil){ + window!.isHidden=false + if (window!.rootViewController == nil || !(window!.rootViewController is FlutterViewController)){ + let extVC = FlutterViewController(project: nil, initialRoute: routerName, nibName: nil, bundle: nil) + SwiftPresentationDisplaysPlugin.controllerAdded!(extVC) + window?.rootViewController = extVC + + self.flutterEngineChannel = FlutterMethodChannel(name: "presentation_displays_plugin_engine", binaryMessenger: extVC.binaryMessenger) + } + } + } } - } - - private func showPresentation(index:Int, routerName:String ) - { - if index>0 && index < self.screens.count && self.additionalWindows.keys.contains(self.screens[index]) + + private func hidePresentation(index:Int) { - let screen=self.screens[index] - let window=self.additionalWindows[screen] - - // You must show the window explicitly. - window?.isHidden=false - - let extVC = FlutterViewController() - SwiftPresentationDisplaysPlugin.controllerAdded!(extVC) - extVC.setInitialRoute(routerName) - window?.rootViewController = extVC - - - self.flutterEngineChannel = FlutterMethodChannel(name: "presentation_displays_plugin_engine", binaryMessenger: extVC.binaryMessenger) + if index>0 && index < self.screens.count && self.additionalWindows.keys.contains(self.screens[index]) + { + let screen=self.screens[index] + let window=self.additionalWindows[screen] + + window?.isHidden=true + } + } + +} + +class DisplayConnectedStreamHandler: NSObject, FlutterStreamHandler{ + var sink: FlutterEventSink? + var didConnectObserver: NSObjectProtocol? + var didDisconnectObserver: NSObjectProtocol? + + func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + sink = events + didConnectObserver = NotificationCenter.default.addObserver(forName: UIScreen.didConnectNotification, + object: nil, queue: nil) { (notification) in + guard let sink = self.sink else { return } + sink(1) + } + didDisconnectObserver = NotificationCenter.default.addObserver(forName: UIScreen.didDisconnectNotification, + object: nil, queue: nil) { (notification) in + guard let sink = self.sink else { return } + sink(0) + } + return nil + } + + func onCancel(withArguments arguments: Any?) -> FlutterError? { + sink = nil + if (didConnectObserver != nil){ + NotificationCenter.default.removeObserver(didConnectObserver!) + } + if (didDisconnectObserver != nil){ + NotificationCenter.default.removeObserver(didDisconnectObserver!) + } + return nil } - - } - } diff --git a/lib/display.dart b/lib/display.dart index 9834a4c..12b9a10 100644 --- a/lib/display.dart +++ b/lib/display.dart @@ -4,6 +4,13 @@ Display displayFromJson(Map json) => Display( name: json['name'], rotation: json['rotation']); +/// for release please check response from invokeMethod +Display displayReleaseFromJson(Map json) => Display( + displayId: json['a'], + flag: json['b'], + name: json['d'], + rotation: json['c']); + /// The default Display id, which is the id of the built-in primary display /// assuming there is one. const int DEFAULT_DISPLAY = 0; @@ -110,7 +117,7 @@ class Display { /// Each logical display has a unique id. /// The default display has id [DEFAULT_DISPLAY] ///

- int displayId = DEFAULT_DISPLAY; + int? displayId = DEFAULT_DISPLAY; /// Returns a combination of flags that describe the capabilities of the display. /// @return The display flags. @@ -138,7 +145,7 @@ class Display { ///

/// /// @return The display's name. - String name; + String? name; Display( {required this.displayId, this.flag, required this.name, this.rotation}); diff --git a/lib/displays_manager.dart b/lib/displays_manager.dart index 49a12b1..113a108 100644 --- a/lib/displays_manager.dart +++ b/lib/displays_manager.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:presentation_displays/display.dart'; @@ -8,6 +9,7 @@ import 'package:presentation_displays/secondary_display.dart'; const _listDisplay = "listDisplay"; const _showPresentation = "showPresentation"; +const _hidePresentation = "hidePresentation"; const _transferDataToPresentation = "transferDataToPresentation"; /// Display category: secondary display. @@ -21,7 +23,7 @@ const _transferDataToPresentation = "transferDataToPresentation"; /// Use the following methods to query the real display area: /// [DisplayManager.getDisplays], [DisplayManager.getNameByDisplayId], /// [DisplayManager.getNameByIndex], [DisplayManager.showSecondaryDisplay], -/// [DisplayManager.transferDataToPresentation] +/// [DisplayManager.transferDataToPresentation], [DisplayManager.hideSecondaryDisplay] ///

/// /// [DisplayManager.getDisplays] @@ -31,11 +33,15 @@ const String DISPLAY_CATEGORY_PRESENTATION = /// Provide you with the method for you to work with [SecondaryDisplay]. class DisplayManager { - final _displayChannel = "presentation_displays_plugin"; - MethodChannel? _displayMethodChannel; + final _displayMethodChannelId = "presentation_displays_plugin"; + final _displayEventChannelId = "presentation_displays_plugin_events"; + + late MethodChannel? _displayMethodChannel; + late EventChannel? _displayEventChannel; DisplayManager() { - _displayMethodChannel = MethodChannel(_displayChannel); + _displayMethodChannel = MethodChannel(_displayMethodChannelId); + _displayEventChannel = EventChannel(_displayEventChannelId); } /// Gets all currently valid logical displays of the specified category. @@ -52,14 +58,16 @@ class DisplayManager { /// @return An array containing all displays sorted by order of preference. /// /// See [DISPLAY_CATEGORY_PRESENTATION] - FutureOr?> getDisplays({String? category}) async { - List origins = await jsonDecode(await _displayMethodChannel - ?.invokeMethod(_listDisplay, category)) ?? + Future?> getDisplays({String? category}) async { + List origins = await jsonDecode((await _displayMethodChannel + ?.invokeMethod(_listDisplay, category))) ?? []; List displays = []; for (var element in origins) { final map = jsonDecode(jsonEncode(element)); - displays.add(displayFromJson(map)); + displays.add(kReleaseMode + ? displayReleaseFromJson(map as Map) + : displayFromJson(map as Map)); } return displays; } @@ -73,8 +81,7 @@ class DisplayManager { /// /// @return The display's name. /// May be null. - FutureOr getNameByDisplayId(int displayId, - {String? category}) async { + Future getNameByDisplayId(int displayId, {String? category}) async { List displays = await getDisplays(category: category) ?? []; String? name; @@ -93,7 +100,7 @@ class DisplayManager { /// /// @return The display's name /// May be null. - FutureOr getNameByIndex(int index, {String? category}) async { + Future getNameByIndex(int index, {String? category}) async { List displays = await getDisplays(category: category) ?? []; String? name; if (index >= 0 && index <= displays.length) name = displays[index].name; @@ -110,8 +117,8 @@ class DisplayManager { /// /// return [Future] about the status has been display or not Future? showSecondaryDisplay( - {required int displayId, required String routerName}) { - return _displayMethodChannel?.invokeMethod( + {required int displayId, required String routerName}) async { + return await _displayMethodChannel?.invokeMethod( _showPresentation, "{" "\"displayId\": $displayId," @@ -119,6 +126,20 @@ class DisplayManager { "}"); } + /// Hides secondary display that is attached to the specified display + ///

+ /// [displayId] The id of display to which the secondary display should be attached. + ///

+ /// + /// return [Future] about the status has been display or not + Future? hideSecondaryDisplay({required int displayId}) async { + return await _displayMethodChannel?.invokeMethod( + _hidePresentation, + "{" + "\"displayId\": $displayId" + "}"); + } + /// Transfer data to a secondary display ///

/// Transfer data from main screen to a secondary display @@ -171,8 +192,14 @@ class DisplayManager { ///

/// /// return [Future] the value to determine whether or not the data has been transferred successfully - Future? transferDataToPresentation(dynamic arguments) { - return _displayMethodChannel?.invokeMethod( + Future? transferDataToPresentation(dynamic arguments) async { + return await _displayMethodChannel?.invokeMethod( _transferDataToPresentation, arguments); } + + /// Subscribe to the stream to get notifications about connected / disconnected displays + /// Streams [1] for new connected display and [0] for disconnected display + Stream? get connectedDisplaysChangedStream { + return _displayEventChannel?.receiveBroadcastStream().cast(); + } } diff --git a/lib/secondary_display.dart b/lib/secondary_display.dart index 2b5bd73..522f414 100644 --- a/lib/secondary_display.dart +++ b/lib/secondary_display.dart @@ -26,7 +26,7 @@ class SecondaryDisplay extends StatefulWidget { class _SecondaryDisplayState extends State { final _presentationChannel = "presentation_displays_plugin_engine"; - MethodChannel? _presentationMethodChannel; + late MethodChannel? _presentationMethodChannel; @override void initState() { diff --git a/pubspec.lock b/pubspec.lock index a8dba89..6df145f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,51 +5,50 @@ packages: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.11.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.0" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.18.0" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.1" flutter: dependency: "direct main" description: flutter @@ -59,7 +58,8 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 + url: "https://pub.dev" source: hosted version: "1.0.4" flutter_test: @@ -71,37 +71,42 @@ packages: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c + url: "https://pub.dev" source: hosted version: "1.0.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" source: hosted - version: "0.12.11" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.5.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.10.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.8.3" sky_engine: dependency: transitive description: flutter @@ -111,58 +116,66 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" source: hosted - version: "0.4.8" - typed_data: + version: "0.6.1" + vector_math: dependency: transitive description: - name: typed_data - url: "https://pub.dartlang.org" + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "1.3.0" - vector_math: + version: "2.1.4" + web: dependency: transitive description: - name: vector_math - url: "https://pub.dartlang.org" + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "0.3.0" sdks: - dart: ">=2.14.0 <3.0.0" - flutter: ">=2.10.3" + dart: ">=3.2.0-194.0.dev <4.0.0" + flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index e51f61b..8a0a1da 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,13 @@ name: presentation_displays description: Flutter plugin supports to run on two screens. It's basically a tablet connected to another screen via an HDMI or Wireless homepage: https://github.com/VNAPNIC/presentation-displays -version: 0.2.3 +version: 0.2.4 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" # Flutter versions prior to 1.12 did not support the # flutter.plugin.platforms map. - flutter: ">=2.10.3" + flutter: ">=3.10.0" dependencies: flutter: