From e2ab988bba13210aecf2303f7e319293d5038eeb Mon Sep 17 00:00:00 2001 From: crow Date: Mon, 22 Jul 2024 16:04:29 -0700 Subject: [PATCH 01/22] Basic wiring with simple views --- .../com/airship/flutter/AirshipPlugin.kt | 1 + .../airship/flutter/EmbeddedViewFactory.kt | 21 ++++ .../airship/flutter/FlutterEmbeddedView.kt | 27 +++++ example/lib/screens/home.dart | 7 ++ ios/Classes/AirshipEmbeddedView.swift | 111 ++++++++++++++++++ ios/Classes/SwiftAirshipPlugin.swift | 2 + lib/airship_flutter.dart | 2 + lib/src/airship_embedded_view.dart | 90 ++++++++++++++ 8 files changed, 261 insertions(+) create mode 100644 android/src/main/kotlin/com/airship/flutter/EmbeddedViewFactory.kt create mode 100644 android/src/main/kotlin/com/airship/flutter/FlutterEmbeddedView.kt create mode 100644 ios/Classes/AirshipEmbeddedView.swift create mode 100644 lib/src/airship_embedded_view.dart diff --git a/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt b/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt index 03662d39..cf14c42a 100644 --- a/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt +++ b/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt @@ -79,6 +79,7 @@ class AirshipPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { this.streams = generateEventStreams(binaryMessenger) platformViewRegistry.registerViewFactory("com.airship.flutter/InboxMessageView", InboxMessageViewFactory(binaryMessenger)) + platformViewRegistry.registerViewFactory("com.airship.flutter/EmbeddedView", EmbeddedViewFactory(binaryMessenger)) scope.launch { EventEmitter.shared().pendingEventListener.collect { diff --git a/android/src/main/kotlin/com/airship/flutter/EmbeddedViewFactory.kt b/android/src/main/kotlin/com/airship/flutter/EmbeddedViewFactory.kt new file mode 100644 index 00000000..683c07c6 --- /dev/null +++ b/android/src/main/kotlin/com/airship/flutter/EmbeddedViewFactory.kt @@ -0,0 +1,21 @@ +package com.airship.flutter + +import android.content.Context +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.StandardMessageCodec +import io.flutter.plugin.platform.PlatformView +import io.flutter.plugin.platform.PlatformViewFactory + +class EmbeddedViewFactory(private val binaryMessenger: BinaryMessenger) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { + override fun create(context: Context?, viewId: Int, args: Any?): PlatformView { + val channel = MethodChannel(binaryMessenger, "com.airship.flutter/EmbeddedView_$viewId") + + // Extracting embeddedId from args + val params = args as? Map + val embeddedId = params?.get("embeddedId") as? String ?: "defaultId" + + val view = FlutterEmbeddedView(checkNotNull(context), channel, embeddedId) + return view + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/airship/flutter/FlutterEmbeddedView.kt b/android/src/main/kotlin/com/airship/flutter/FlutterEmbeddedView.kt new file mode 100644 index 00000000..deba9973 --- /dev/null +++ b/android/src/main/kotlin/com/airship/flutter/FlutterEmbeddedView.kt @@ -0,0 +1,27 @@ +package com.airship.flutter + +import android.graphics.Color +import android.widget.TextView +import android.content.Context +import android.view.View +import android.widget.FrameLayout +import com.urbanairship.UAirship +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.platform.PlatformView +import com.urbanairship.embedded.AirshipEmbeddedView + +class FlutterEmbeddedView( + private var context: Context, + channel: MethodChannel, + private val embeddedId: String +) : PlatformView, MethodChannel.MethodCallHandler { + + override fun getView(): View { + return AirshipEmbeddedView(context, embeddedId) + } + override fun dispose() {} + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + // Handle method calls if needed + } +} \ No newline at end of file diff --git a/example/lib/screens/home.dart b/example/lib/screens/home.dart index 21cad39d..41d01160 100644 --- a/example/lib/screens/home.dart +++ b/example/lib/screens/home.dart @@ -34,6 +34,13 @@ class _HomeState extends State { child: Container( alignment: Alignment.center, child: Wrap(children: [ + Container( + width: 420, + height: 420, + child: Center( + child: EmbeddedView(embeddedId: "test"), + ), + ), Image.asset( 'assets/airship.png', ), diff --git a/ios/Classes/AirshipEmbeddedView.swift b/ios/Classes/AirshipEmbeddedView.swift new file mode 100644 index 00000000..cfd3a167 --- /dev/null +++ b/ios/Classes/AirshipEmbeddedView.swift @@ -0,0 +1,111 @@ +import Foundation +import AirshipKit +import SwiftUI + +class AirshipEmbeddedViewFactory : NSObject, FlutterPlatformViewFactory { + let registrar : FlutterPluginRegistrar + + init(_ registrar: FlutterPluginRegistrar) { + self.registrar = registrar + } + + func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView { + return AirshipEmbeddedViewWrapper(frame: frame, viewId: viewId, registrar: self.registrar, args: args) + } + + func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { + return FlutterStandardMessageCodec.sharedInstance() + } +} + +/// The Flutter wrapper for the Airship embedded view +class AirshipEmbeddedViewWrapper : NSObject, FlutterPlatformView { + private static let embeddedIdKey: String = "embeddedId" + + @ObservedObject + var viewModel = FlutterAirshipEmbeddedView.ViewModel() + + public var viewController: UIViewController? + + let channel : FlutterMethodChannel + private var _view: UIView + + init(frame: CGRect, viewId: Int64, registrar: FlutterPluginRegistrar, args: Any?) { + let channelName = "com.airship.flutter/EmbeddedView_\(viewId)" + self.channel = FlutterMethodChannel(name: channelName, binaryMessenger: registrar.messenger()) + _view = UIView(frame: frame) + + super.init() + + let rootView = FlutterAirshipEmbeddedView(viewModel: viewModel) + self.viewController = UIHostingController( + rootView: rootView + ) + + _view.translatesAutoresizingMaskIntoConstraints = false + _view.addSubview(self.viewController!.view) + self.viewController?.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + + Task { @MainActor in + if let params = args as? [String: Any], let embeddedId = params[Self.embeddedIdKey] as? String { + rootView.viewModel.embeddedID = embeddedId + } + + rootView.viewModel.size = frame.size + } + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) async { + switch call.method { + default: + result(FlutterError(code:"UNAVAILABLE", + message:"Unknown method: \(call.method)", + details:nil)) + } + } + + func view() -> UIView { + return _view + } +} + +struct FlutterAirshipEmbeddedView: View { + @ObservedObject + var viewModel:ViewModel + + var body: some View { + if let embeddedID = viewModel.embeddedID { + AirshipEmbeddedView(embeddedID: embeddedID, + embeddedSize: .init( + parentWidth: viewModel.width, + parentHeight: viewModel.height + ) + ) { + Text("Placeholder: \(embeddedID) \(viewModel.size ?? CGSize())") + } + } else { + Text("Please set embeddedId") + } + Text("Size: \(viewModel.width)x\(viewModel.height) pts") + } + + @MainActor + class ViewModel: ObservableObject { + @Published var embeddedID: String? + @Published var size: CGSize? + + var height: CGFloat { + guard let height = self.size?.height, height > 0 else { + return try! AirshipUtils.mainWindow()?.screen.bounds.height ?? 420 + } + return height + } + + var width: CGFloat { + guard let width = self.size?.width, width > 0 else { + return try! AirshipUtils.mainWindow()?.screen.bounds.width ?? 420 + } + return width + } + } +} diff --git a/ios/Classes/SwiftAirshipPlugin.swift b/ios/Classes/SwiftAirshipPlugin.swift index 9dd7bb1a..ddb038a3 100644 --- a/ios/Classes/SwiftAirshipPlugin.swift +++ b/ios/Classes/SwiftAirshipPlugin.swift @@ -45,6 +45,8 @@ public class SwiftAirshipPlugin: NSObject, FlutterPlugin { } registrar.register(AirshipInboxMessageViewFactory(registrar), withId: "com.airship.flutter/InboxMessageView") + registrar.register(AirshipEmbeddedViewFactory(registrar), withId: "com.airship.flutter/EmbeddedView") + registrar.addApplicationDelegate(self) AirshipProxyEventEmitter.shared.pendingEventPublisher.sink { [weak self] (event: any AirshipProxyEvent) in diff --git a/lib/airship_flutter.dart b/lib/airship_flutter.dart index 0a4bbcd6..0a2eb0fb 100644 --- a/lib/airship_flutter.dart +++ b/lib/airship_flutter.dart @@ -15,10 +15,12 @@ export 'src/airship_privacy_manager.dart'; export 'src/airship_push.dart'; export 'src/airship_events.dart'; export 'src/airship_feature_flag_manager.dart'; +export 'src/airship_embedded_view.dart'; export 'src/push_notification_status.dart'; export 'src/push_payload.dart'; export 'src/ios_push_options.dart'; + export 'src/attribute_editor.dart'; export 'src/channel_scope.dart'; export 'src/custom_event.dart'; diff --git a/lib/src/airship_embedded_view.dart b/lib/src/airship_embedded_view.dart new file mode 100644 index 00000000..6edf518c --- /dev/null +++ b/lib/src/airship_embedded_view.dart @@ -0,0 +1,90 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +/// Embedded view component. +class EmbeddedView extends StatelessWidget { + /// The embedded view Id. + final String embeddedId; + + /// A flag to use flutter hybrid composition method or not. Default to false. + static bool hybridComposition = false; + + EmbeddedView({required this.embeddedId}); + + Future onPlatformViewCreated(id) async { + var channel = MethodChannel('com.airship.flutter/EmbeddedView_$id'); + channel.setMethodCallHandler(methodCallHandler); + } + + Future methodCallHandler(MethodCall call) async { + switch (call.method) { + default: + print('Unknown method.'); + } + } + + @override + Widget build(BuildContext context) { + if (defaultTargetPlatform == TargetPlatform.android) { + return getAndroidView(); + } else if (defaultTargetPlatform == TargetPlatform.iOS) { + return UiKitView( + viewType: 'com.airship.flutter/EmbeddedView', + onPlatformViewCreated: onPlatformViewCreated, + creationParams: { + 'embeddedId': embeddedId, + }, + creationParamsCodec: const StandardMessageCodec(), + ); + } + + return Text('$defaultTargetPlatform is not yet supported by this plugin'); + } + + Widget getAndroidView() { + if (hybridComposition) { + // Hybrid Composition method + return PlatformViewLink( + viewType: 'com.airship.flutter/EmbeddedView', + surfaceFactory: (BuildContext context, PlatformViewController controller) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + return PlatformViewsService.initSurfaceAndroidView( + id: params.id, + viewType: 'com.airship.flutter/EmbeddedView', + layoutDirection: TextDirection.ltr, + creationParams: { + 'embeddedId': embeddedId, + }, + creationParamsCodec: const StandardMessageCodec(), + onFocus: () { + params.onFocusChanged(true); + }, + ) + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..create(); + }, + ); + } else { + // Display View method + return AndroidView( + viewType: 'com.airship.flutter/EmbeddedView', + onPlatformViewCreated: onPlatformViewCreated, + creationParams: { + 'embeddedId': embeddedId, + }, + creationParamsCodec: const StandardMessageCodec(), + ); + } + } +} From 89304b578ac1a593d4ca28e01c403a43696f349d Mon Sep 17 00:00:00 2001 From: crow Date: Fri, 2 Aug 2024 14:53:39 -0700 Subject: [PATCH 02/22] Move factories to the same file as the views they produce --- .../airship/flutter/EmbeddedViewFactory.kt | 21 ------------------- .../airship/flutter/FlutterEmbeddedView.kt | 17 +++++++++++++++ .../flutter/FlutterInboxMessageView.kt | 13 ++++++++++++ .../flutter/InboxMessageViewFactory.kt | 17 --------------- 4 files changed, 30 insertions(+), 38 deletions(-) delete mode 100644 android/src/main/kotlin/com/airship/flutter/EmbeddedViewFactory.kt delete mode 100644 android/src/main/kotlin/com/airship/flutter/InboxMessageViewFactory.kt diff --git a/android/src/main/kotlin/com/airship/flutter/EmbeddedViewFactory.kt b/android/src/main/kotlin/com/airship/flutter/EmbeddedViewFactory.kt deleted file mode 100644 index 683c07c6..00000000 --- a/android/src/main/kotlin/com/airship/flutter/EmbeddedViewFactory.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.airship.flutter - -import android.content.Context -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.StandardMessageCodec -import io.flutter.plugin.platform.PlatformView -import io.flutter.plugin.platform.PlatformViewFactory - -class EmbeddedViewFactory(private val binaryMessenger: BinaryMessenger) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { - override fun create(context: Context?, viewId: Int, args: Any?): PlatformView { - val channel = MethodChannel(binaryMessenger, "com.airship.flutter/EmbeddedView_$viewId") - - // Extracting embeddedId from args - val params = args as? Map - val embeddedId = params?.get("embeddedId") as? String ?: "defaultId" - - val view = FlutterEmbeddedView(checkNotNull(context), channel, embeddedId) - return view - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/airship/flutter/FlutterEmbeddedView.kt b/android/src/main/kotlin/com/airship/flutter/FlutterEmbeddedView.kt index deba9973..0274b74c 100644 --- a/android/src/main/kotlin/com/airship/flutter/FlutterEmbeddedView.kt +++ b/android/src/main/kotlin/com/airship/flutter/FlutterEmbeddedView.kt @@ -10,6 +10,9 @@ import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.platform.PlatformView import com.urbanairship.embedded.AirshipEmbeddedView +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.platform.PlatformView +import io.flutter.plugin.platform.PlatformViewFactory class FlutterEmbeddedView( private var context: Context, @@ -24,4 +27,18 @@ class FlutterEmbeddedView( override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { // Handle method calls if needed } +} + + +class EmbeddedViewFactory(private val binaryMessenger: BinaryMessenger) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { + override fun create(context: Context?, viewId: Int, args: Any?): PlatformView { + val channel = MethodChannel(binaryMessenger, "com.airship.flutter/EmbeddedView_$viewId") + + // Extracting embeddedId from args + val params = args as? Map + val embeddedId = params?.get("embeddedId") as? String ?: "defaultId" + + val view = FlutterEmbeddedView(checkNotNull(context), channel, embeddedId) + return view + } } \ No newline at end of file diff --git a/android/src/main/kotlin/com/airship/flutter/FlutterInboxMessageView.kt b/android/src/main/kotlin/com/airship/flutter/FlutterInboxMessageView.kt index de7d0c43..750a321b 100644 --- a/android/src/main/kotlin/com/airship/flutter/FlutterInboxMessageView.kt +++ b/android/src/main/kotlin/com/airship/flutter/FlutterInboxMessageView.kt @@ -11,6 +11,10 @@ import com.urbanairship.messagecenter.webkit.MessageWebViewClient import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.platform.PlatformView +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.StandardMessageCodec +import io.flutter.plugin.platform.PlatformView +import io.flutter.plugin.platform.PlatformViewFactory class FlutterInboxMessageView(private var context: Context, channel: MethodChannel) : PlatformView, MethodChannel.MethodCallHandler { @@ -75,4 +79,13 @@ class FlutterInboxMessageView(private var context: Context, channel: MethodChann result.error("InvalidMessage", "Unable to load message: ${call.arguments}", null) } } +} + +class InboxMessageViewFactory(private val binaryMessenger: BinaryMessenger) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { + override fun create(context: Context?, viewId: Int, args: Any?): PlatformView { + val channel = MethodChannel(binaryMessenger, "com.airship.flutter/InboxMessageView_$viewId") + val view = FlutterInboxMessageView(checkNotNull(context), channel) + channel.setMethodCallHandler(view) + return view + } } \ No newline at end of file diff --git a/android/src/main/kotlin/com/airship/flutter/InboxMessageViewFactory.kt b/android/src/main/kotlin/com/airship/flutter/InboxMessageViewFactory.kt deleted file mode 100644 index 687f3f2d..00000000 --- a/android/src/main/kotlin/com/airship/flutter/InboxMessageViewFactory.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.airship.flutter - -import android.content.Context -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.StandardMessageCodec -import io.flutter.plugin.platform.PlatformView -import io.flutter.plugin.platform.PlatformViewFactory - -class InboxMessageViewFactory(private val binaryMessenger: BinaryMessenger) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { - override fun create(context: Context?, viewId: Int, args: Any?): PlatformView { - val channel = MethodChannel(binaryMessenger, "com.airship.flutter/InboxMessageView_$viewId") - val view = FlutterInboxMessageView(checkNotNull(context), channel) - channel.setMethodCallHandler(view) - return view - } -} \ No newline at end of file From f388662e305849c8c513df99447a3d77d3fe34db Mon Sep 17 00:00:00 2001 From: crow Date: Mon, 5 Aug 2024 16:38:43 -0700 Subject: [PATCH 03/22] Fix imports and formatting --- .../kotlin/com/airship/flutter/FlutterEmbeddedView.kt | 7 ++++--- .../com/airship/flutter/FlutterInboxMessageView.kt | 10 +++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/android/src/main/kotlin/com/airship/flutter/FlutterEmbeddedView.kt b/android/src/main/kotlin/com/airship/flutter/FlutterEmbeddedView.kt index 0274b74c..92739693 100644 --- a/android/src/main/kotlin/com/airship/flutter/FlutterEmbeddedView.kt +++ b/android/src/main/kotlin/com/airship/flutter/FlutterEmbeddedView.kt @@ -8,11 +8,11 @@ import android.widget.FrameLayout import com.urbanairship.UAirship import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.platform.PlatformView import com.urbanairship.embedded.AirshipEmbeddedView import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.platform.PlatformView import io.flutter.plugin.platform.PlatformViewFactory +import io.flutter.plugin.common.StandardMessageCodec class FlutterEmbeddedView( private var context: Context, @@ -29,8 +29,9 @@ class FlutterEmbeddedView( } } - -class EmbeddedViewFactory(private val binaryMessenger: BinaryMessenger) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { +class EmbeddedViewFactory( + private val binaryMessenger: BinaryMessenger +) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { override fun create(context: Context?, viewId: Int, args: Any?): PlatformView { val channel = MethodChannel(binaryMessenger, "com.airship.flutter/EmbeddedView_$viewId") diff --git a/android/src/main/kotlin/com/airship/flutter/FlutterInboxMessageView.kt b/android/src/main/kotlin/com/airship/flutter/FlutterInboxMessageView.kt index 750a321b..03decda0 100644 --- a/android/src/main/kotlin/com/airship/flutter/FlutterInboxMessageView.kt +++ b/android/src/main/kotlin/com/airship/flutter/FlutterInboxMessageView.kt @@ -13,10 +13,12 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.platform.PlatformView import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.StandardMessageCodec -import io.flutter.plugin.platform.PlatformView import io.flutter.plugin.platform.PlatformViewFactory -class FlutterInboxMessageView(private var context: Context, channel: MethodChannel) : PlatformView, MethodChannel.MethodCallHandler { +class FlutterInboxMessageView( + private var context: Context, + channel: MethodChannel +) : PlatformView, MethodChannel.MethodCallHandler { private lateinit var webviewResult: MethodChannel.Result @@ -81,7 +83,9 @@ class FlutterInboxMessageView(private var context: Context, channel: MethodChann } } -class InboxMessageViewFactory(private val binaryMessenger: BinaryMessenger) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { +class InboxMessageViewFactor( + private val binaryMessenger: BinaryMessenger +) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { override fun create(context: Context?, viewId: Int, args: Any?): PlatformView { val channel = MethodChannel(binaryMessenger, "com.airship.flutter/InboxMessageView_$viewId") val view = FlutterInboxMessageView(checkNotNull(context), channel) From bbbf832811303f3769590c16ecbaa2c96cbd5649 Mon Sep 17 00:00:00 2001 From: crow Date: Wed, 7 Aug 2024 16:23:26 -0700 Subject: [PATCH 04/22] Get ios and android views rendering and mostly sizing correctly --- .../airship/flutter/FlutterEmbeddedView.kt | 51 ++++-- .../flutter/FlutterInboxMessageView.kt | 2 +- example/lib/screens/home.dart | 8 +- ios/Classes/AirshipEmbeddedView.swift | 1 - lib/src/airship_embedded_view.dart | 146 ++++++++++++------ 5 files changed, 142 insertions(+), 66 deletions(-) diff --git a/android/src/main/kotlin/com/airship/flutter/FlutterEmbeddedView.kt b/android/src/main/kotlin/com/airship/flutter/FlutterEmbeddedView.kt index 92739693..31160bce 100644 --- a/android/src/main/kotlin/com/airship/flutter/FlutterEmbeddedView.kt +++ b/android/src/main/kotlin/com/airship/flutter/FlutterEmbeddedView.kt @@ -1,37 +1,65 @@ package com.airship.flutter -import android.graphics.Color -import android.widget.TextView import android.content.Context +import android.graphics.Color import android.view.View import android.widget.FrameLayout -import com.urbanairship.UAirship -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel import com.urbanairship.embedded.AirshipEmbeddedView import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.StandardMessageCodec import io.flutter.plugin.platform.PlatformView import io.flutter.plugin.platform.PlatformViewFactory -import io.flutter.plugin.common.StandardMessageCodec class FlutterEmbeddedView( private var context: Context, - channel: MethodChannel, + private val channel: MethodChannel, private val embeddedId: String ) : PlatformView, MethodChannel.MethodCallHandler { + private val frameLayout: FrameLayout = FrameLayout(context) + private var airshipEmbeddedView: AirshipEmbeddedView? = null + + init { + setupAirshipEmbeddedView() + channel.setMethodCallHandler(this) + } + private fun setupAirshipEmbeddedView() { + + airshipEmbeddedView = AirshipEmbeddedView(context, embeddedId) + airshipEmbeddedView?.layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ) + frameLayout.addView(airshipEmbeddedView) + } + override fun getView(): View { - return AirshipEmbeddedView(context, embeddedId) + frameLayout.layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ) + return frameLayout } - override fun dispose() {} + + override fun dispose() { + channel.setMethodCallHandler(null) + } + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - // Handle method calls if needed + when (call.method) { + else -> { + result.error("UNAVAILABLE", "Unknown method: ${call.method}", null) + } + } } } class EmbeddedViewFactory( private val binaryMessenger: BinaryMessenger ) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { + override fun create(context: Context?, viewId: Int, args: Any?): PlatformView { val channel = MethodChannel(binaryMessenger, "com.airship.flutter/EmbeddedView_$viewId") @@ -39,7 +67,6 @@ class EmbeddedViewFactory( val params = args as? Map val embeddedId = params?.get("embeddedId") as? String ?: "defaultId" - val view = FlutterEmbeddedView(checkNotNull(context), channel, embeddedId) - return view + return FlutterEmbeddedView(checkNotNull(context), channel, embeddedId) } } \ No newline at end of file diff --git a/android/src/main/kotlin/com/airship/flutter/FlutterInboxMessageView.kt b/android/src/main/kotlin/com/airship/flutter/FlutterInboxMessageView.kt index 03decda0..1af94efd 100644 --- a/android/src/main/kotlin/com/airship/flutter/FlutterInboxMessageView.kt +++ b/android/src/main/kotlin/com/airship/flutter/FlutterInboxMessageView.kt @@ -83,7 +83,7 @@ class FlutterInboxMessageView( } } -class InboxMessageViewFactor( +class InboxMessageViewFactory( private val binaryMessenger: BinaryMessenger ) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { override fun create(context: Context?, viewId: Int, args: Any?): PlatformView { diff --git a/example/lib/screens/home.dart b/example/lib/screens/home.dart index 41d01160..f2611dce 100644 --- a/example/lib/screens/home.dart +++ b/example/lib/screens/home.dart @@ -34,12 +34,8 @@ class _HomeState extends State { child: Container( alignment: Alignment.center, child: Wrap(children: [ - Container( - width: 420, - height: 420, - child: Center( - child: EmbeddedView(embeddedId: "test"), - ), + Center( + child:EmbeddedView(embeddedId: "hundredpxhundredpx"), ), Image.asset( 'assets/airship.png', diff --git a/ios/Classes/AirshipEmbeddedView.swift b/ios/Classes/AirshipEmbeddedView.swift index cfd3a167..04fde64b 100644 --- a/ios/Classes/AirshipEmbeddedView.swift +++ b/ios/Classes/AirshipEmbeddedView.swift @@ -86,7 +86,6 @@ struct FlutterAirshipEmbeddedView: View { } else { Text("Please set embeddedId") } - Text("Size: \(viewModel.width)x\(viewModel.height) pts") } @MainActor diff --git a/lib/src/airship_embedded_view.dart b/lib/src/airship_embedded_view.dart index 6edf518c..c5a1aefb 100644 --- a/lib/src/airship_embedded_view.dart +++ b/lib/src/airship_embedded_view.dart @@ -1,13 +1,13 @@ import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; /// Embedded view component. -class EmbeddedView extends StatelessWidget { +class EmbeddedView extends StatefulWidget { /// The embedded view Id. final String embeddedId; @@ -16,75 +16,129 @@ class EmbeddedView extends StatelessWidget { EmbeddedView({required this.embeddedId}); - Future onPlatformViewCreated(id) async { - var channel = MethodChannel('com.airship.flutter/EmbeddedView_$id'); - channel.setMethodCallHandler(methodCallHandler); + @override + _EmbeddedViewState createState() => _EmbeddedViewState(); +} + +class _EmbeddedViewState extends State { + late MethodChannel _channel; + + @override + void initState() { + super.initState(); + _channel = MethodChannel('com.airship.flutter/EmbeddedView_${widget.embeddedId}'); + _channel.setMethodCallHandler(_methodCallHandler); } - Future methodCallHandler(MethodCall call) async { + Future _methodCallHandler(MethodCall call) async { switch (call.method) { default: print('Unknown method.'); } } + Future _onPlatformViewCreated(int id) async { + _channel = MethodChannel('com.airship.flutter/EmbeddedView_$id'); + _channel.setMethodCallHandler(_methodCallHandler); + } + + /// Fall back to screen-sized constraints when constraints can be inferred + Widget wrapWithLayoutBuilder(Widget view) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + double width = constraints.maxWidth; + double height = constraints.maxHeight; + + if (width == 0 || width == double.infinity) { + width = MediaQuery.of(context).size.width; + } + + if (height == 0 || height == double.infinity) { + height = MediaQuery.of(context).size.height; + } + + return FittedBox( + fit: BoxFit.contain, + alignment: Alignment.center, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: width, + maxHeight: height, + ), + child: view, + ), + ); + }, + ); + } + @override Widget build(BuildContext context) { if (defaultTargetPlatform == TargetPlatform.android) { - return getAndroidView(); + return _getAndroidView(); } else if (defaultTargetPlatform == TargetPlatform.iOS) { - return UiKitView( - viewType: 'com.airship.flutter/EmbeddedView', - onPlatformViewCreated: onPlatformViewCreated, - creationParams: { - 'embeddedId': embeddedId, - }, - creationParamsCodec: const StandardMessageCodec(), + return wrapWithLayoutBuilder( + UiKitView( + viewType: 'com.airship.flutter/EmbeddedView', + onPlatformViewCreated: _onPlatformViewCreated, + creationParams: { + 'embeddedId': widget.embeddedId, + }, + creationParamsCodec: const StandardMessageCodec(), + ), ); } return Text('$defaultTargetPlatform is not yet supported by this plugin'); } - Widget getAndroidView() { - if (hybridComposition) { - // Hybrid Composition method - return PlatformViewLink( - viewType: 'com.airship.flutter/EmbeddedView', - surfaceFactory: (BuildContext context, PlatformViewController controller) { - return AndroidViewSurface( - controller: controller as AndroidViewController, - gestureRecognizers: const >{}, - hitTestBehavior: PlatformViewHitTestBehavior.opaque, - ); - }, - onCreatePlatformView: (PlatformViewCreationParams params) { - return PlatformViewsService.initSurfaceAndroidView( - id: params.id, + Widget _getAndroidView() { + if (EmbeddedView.hybridComposition) { + return wrapWithLayoutBuilder( + PlatformViewLink( viewType: 'com.airship.flutter/EmbeddedView', - layoutDirection: TextDirection.ltr, - creationParams: { - 'embeddedId': embeddedId, + surfaceFactory: (BuildContext context, PlatformViewController controller) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); }, - creationParamsCodec: const StandardMessageCodec(), - onFocus: () { - params.onFocusChanged(true); + onCreatePlatformView: (PlatformViewCreationParams params) { + return PlatformViewsService.initSurfaceAndroidView( + id: params.id, + viewType: 'com.airship.flutter/EmbeddedView', + layoutDirection: TextDirection.ltr, + creationParams: { + 'embeddedId': widget.embeddedId, + }, + creationParamsCodec: const StandardMessageCodec(), + onFocus: () { + params.onFocusChanged(true); + }, + ) + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..create(); }, ) - ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) - ..create(); - }, ); } else { - // Display View method - return AndroidView( - viewType: 'com.airship.flutter/EmbeddedView', - onPlatformViewCreated: onPlatformViewCreated, - creationParams: { - 'embeddedId': embeddedId, - }, - creationParamsCodec: const StandardMessageCodec(), + return wrapWithLayoutBuilder( + AndroidView( + viewType: 'com.airship.flutter/EmbeddedView', + onPlatformViewCreated: _onPlatformViewCreated, + creationParams: { + 'embeddedId': widget.embeddedId, + }, + creationParamsCodec: const StandardMessageCodec(), + ) ); } } + + @override + void dispose() { + _channel.setMethodCallHandler(null); + super.dispose(); + } } From b12f669a7fa3f3505ee78cf8a5acd69384d10be8 Mon Sep 17 00:00:00 2001 From: crow Date: Wed, 7 Aug 2024 16:31:41 -0700 Subject: [PATCH 05/22] Apply suggested gradle update --- android/.idea/kotlinc.xml | 6 ++++++ android/.idea/migrations.xml | 10 ++++++++++ gradle/wrapper/gradle-wrapper.properties | 4 ++-- 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 android/.idea/kotlinc.xml create mode 100644 android/.idea/migrations.xml diff --git a/android/.idea/kotlinc.xml b/android/.idea/kotlinc.xml new file mode 100644 index 00000000..fdf8d994 --- /dev/null +++ b/android/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/android/.idea/migrations.xml b/android/.idea/migrations.xml new file mode 100644 index 00000000..f8051a6f --- /dev/null +++ b/android/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e59f3fc2..8b781980 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Sep 26 12:36:27 PDT 2019 +#Tue Aug 06 11:27:54 PDT 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip From c1ff6d0e86280d54b2716d12d2909522865ccd64 Mon Sep 17 00:00:00 2001 From: crow Date: Wed, 7 Aug 2024 16:46:29 -0700 Subject: [PATCH 06/22] Fix access for EmbeddedViewState --- lib/src/airship_embedded_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/airship_embedded_view.dart b/lib/src/airship_embedded_view.dart index c5a1aefb..a9f298b5 100644 --- a/lib/src/airship_embedded_view.dart +++ b/lib/src/airship_embedded_view.dart @@ -17,10 +17,10 @@ class EmbeddedView extends StatefulWidget { EmbeddedView({required this.embeddedId}); @override - _EmbeddedViewState createState() => _EmbeddedViewState(); + EmbeddedViewState createState() => EmbeddedViewState(); } -class _EmbeddedViewState extends State { +class EmbeddedViewState extends State { late MethodChannel _channel; @override From a5f22273845383eaccc031812973add95467281f Mon Sep 17 00:00:00 2001 From: crow Date: Thu, 8 Aug 2024 08:16:07 -0700 Subject: [PATCH 07/22] Fix method channel setup and get size method --- example/lib/screens/home.dart | 14 ++++++------ ios/Classes/AirshipEmbeddedView.swift | 31 ++++++++++++++++----------- lib/src/airship_embedded_view.dart | 18 ++++++++++++++-- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/example/lib/screens/home.dart b/example/lib/screens/home.dart index f2611dce..83765c86 100644 --- a/example/lib/screens/home.dart +++ b/example/lib/screens/home.dart @@ -34,8 +34,8 @@ class _HomeState extends State { child: Container( alignment: Alignment.center, child: Wrap(children: [ - Center( - child:EmbeddedView(embeddedId: "hundredpxhundredpx"), + Center( + child:EmbeddedView(embeddedId: "test"), ), Image.asset( 'assets/airship.png', @@ -48,11 +48,11 @@ class _HomeState extends State { bool pushEnabled = snapshot.data ?? false; enableNotificationsButton = Center(child: NotificationsEnabledButton( - onPressed: () { - Airship.push.setUserNotificationsEnabled(true); - setState(() {}); - }, - )); + onPressed: () { + Airship.push.setUserNotificationsEnabled(true); + setState(() {}); + }, + )); return Visibility( visible: !pushEnabled, child: enableNotificationsButton); diff --git a/ios/Classes/AirshipEmbeddedView.swift b/ios/Classes/AirshipEmbeddedView.swift index 04fde64b..fbbe3cda 100644 --- a/ios/Classes/AirshipEmbeddedView.swift +++ b/ios/Classes/AirshipEmbeddedView.swift @@ -2,8 +2,8 @@ import Foundation import AirshipKit import SwiftUI -class AirshipEmbeddedViewFactory : NSObject, FlutterPlatformViewFactory { - let registrar : FlutterPluginRegistrar +class AirshipEmbeddedViewFactory: NSObject, FlutterPlatformViewFactory { + let registrar: FlutterPluginRegistrar init(_ registrar: FlutterPluginRegistrar) { self.registrar = registrar @@ -18,8 +18,7 @@ class AirshipEmbeddedViewFactory : NSObject, FlutterPlatformViewFactory { } } -/// The Flutter wrapper for the Airship embedded view -class AirshipEmbeddedViewWrapper : NSObject, FlutterPlatformView { +class AirshipEmbeddedViewWrapper: NSObject, FlutterPlatformView { private static let embeddedIdKey: String = "embeddedId" @ObservedObject @@ -27,7 +26,7 @@ class AirshipEmbeddedViewWrapper : NSObject, FlutterPlatformView { public var viewController: UIViewController? - let channel : FlutterMethodChannel + let channel: FlutterMethodChannel private var _view: UIView init(frame: CGRect, viewId: Int64, registrar: FlutterPluginRegistrar, args: Any?) { @@ -53,14 +52,22 @@ class AirshipEmbeddedViewWrapper : NSObject, FlutterPlatformView { rootView.viewModel.size = frame.size } + + channel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) in + self?.handle(call, result: result) + } } - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) async { + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { + case "getSize": + let width = _view.bounds.width + let height = _view.bounds.height + result(["width": width, "height": height]) default: - result(FlutterError(code:"UNAVAILABLE", - message:"Unknown method: \(call.method)", - details:nil)) + result(FlutterError(code: "UNAVAILABLE", + message: "Unknown method: \(call.method)", + details: nil)) } } @@ -71,7 +78,7 @@ class AirshipEmbeddedViewWrapper : NSObject, FlutterPlatformView { struct FlutterAirshipEmbeddedView: View { @ObservedObject - var viewModel:ViewModel + var viewModel: ViewModel var body: some View { if let embeddedID = viewModel.embeddedID { @@ -95,14 +102,14 @@ struct FlutterAirshipEmbeddedView: View { var height: CGFloat { guard let height = self.size?.height, height > 0 else { - return try! AirshipUtils.mainWindow()?.screen.bounds.height ?? 420 + return (try? AirshipUtils.mainWindow()?.screen.bounds.height) ?? 420 } return height } var width: CGFloat { guard let width = self.size?.width, width > 0 else { - return try! AirshipUtils.mainWindow()?.screen.bounds.width ?? 420 + return (try? AirshipUtils.mainWindow()?.screen.bounds.width) ?? 420 } return width } diff --git a/lib/src/airship_embedded_view.dart b/lib/src/airship_embedded_view.dart index a9f298b5..904f6bfb 100644 --- a/lib/src/airship_embedded_view.dart +++ b/lib/src/airship_embedded_view.dart @@ -23,11 +23,24 @@ class EmbeddedView extends StatefulWidget { class EmbeddedViewState extends State { late MethodChannel _channel; + double _nativeViewWidth = 0; + double _nativeViewHeight = 0; + @override void initState() { super.initState(); - _channel = MethodChannel('com.airship.flutter/EmbeddedView_${widget.embeddedId}'); - _channel.setMethodCallHandler(_methodCallHandler); + } + + Future _getNativeViewSize() async { + try { + final size = await _channel.invokeMethod('getSize'); + setState(() { + _nativeViewWidth = size?['width']; + _nativeViewHeight = size?['height']; + }); + } on PlatformException catch (e) { + print("Failed to get native view size: '${e.message}'."); + } } Future _methodCallHandler(MethodCall call) async { @@ -40,6 +53,7 @@ class EmbeddedViewState extends State { Future _onPlatformViewCreated(int id) async { _channel = MethodChannel('com.airship.flutter/EmbeddedView_$id'); _channel.setMethodCallHandler(_methodCallHandler); + _getNativeViewSize(); } /// Fall back to screen-sized constraints when constraints can be inferred From f4229fe2d54a4d6c3e02f898400b01b7f9197b09 Mon Sep 17 00:00:00 2001 From: crow Date: Sun, 18 Aug 2024 10:02:09 -0700 Subject: [PATCH 08/22] Make embedded wrapper a UIView --- ios/Classes/AirshipEmbeddedView.swift | 88 ++++++++++++++++++++------- lib/src/airship_embedded_view.dart | 19 +++--- 2 files changed, 74 insertions(+), 33 deletions(-) diff --git a/ios/Classes/AirshipEmbeddedView.swift b/ios/Classes/AirshipEmbeddedView.swift index fbbe3cda..d81c5b7e 100644 --- a/ios/Classes/AirshipEmbeddedView.swift +++ b/ios/Classes/AirshipEmbeddedView.swift @@ -18,52 +18,61 @@ class AirshipEmbeddedViewFactory: NSObject, FlutterPlatformViewFactory { } } -class AirshipEmbeddedViewWrapper: NSObject, FlutterPlatformView { +class AirshipEmbeddedViewWrapper: UIView, FlutterPlatformView { private static let embeddedIdKey: String = "embeddedId" - @ObservedObject var viewModel = FlutterAirshipEmbeddedView.ViewModel() - public var viewController: UIViewController? + public var viewController: UIViewController + + public var isAdded: Bool = false let channel: FlutterMethodChannel - private var _view: UIView - init(frame: CGRect, viewId: Int64, registrar: FlutterPluginRegistrar, args: Any?) { + required init(frame: CGRect, viewId: Int64, registrar: FlutterPluginRegistrar, args: Any?) { let channelName = "com.airship.flutter/EmbeddedView_\(viewId)" self.channel = FlutterMethodChannel(name: channelName, binaryMessenger: registrar.messenger()) - _view = UIView(frame: frame) - super.init() + self.viewController = UIHostingController( + rootView: FlutterAirshipEmbeddedView(viewModel: self.viewModel) + ) - let rootView = FlutterAirshipEmbeddedView(viewModel: viewModel) + self.viewController.view.backgroundColor = UIColor.purple + + let rootView = FlutterAirshipEmbeddedView(viewModel: self.viewModel) self.viewController = UIHostingController( rootView: rootView ) - _view.translatesAutoresizingMaskIntoConstraints = false - _view.addSubview(self.viewController!.view) - self.viewController?.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + super.init(frame:frame) - Task { @MainActor in - if let params = args as? [String: Any], let embeddedId = params[Self.embeddedIdKey] as? String { - rootView.viewModel.embeddedID = embeddedId - } + self.translatesAutoresizingMaskIntoConstraints = false + self.addSubview(self.viewController.view) + self.viewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] - rootView.viewModel.size = frame.size + if let params = args as? [String: Any], let embeddedId = params[Self.embeddedIdKey] as? String { + rootView.viewModel.embeddedID = embeddedId } + rootView.viewModel.size = frame.size + channel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) in self?.handle(call, result: result) } } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case "getSize": - let width = _view.bounds.width - let height = _view.bounds.height + let width = self.bounds.width + let height = self.bounds.height result(["width": width, "height": height]) + case "getIsAdded": + result(["isAdded": self.isAdded]) default: result(FlutterError(code: "UNAVAILABLE", message: "Unknown method: \(call.method)", @@ -72,7 +81,22 @@ class AirshipEmbeddedViewWrapper: NSObject, FlutterPlatformView { } func view() -> UIView { - return _view + return self + } + + public override func didMoveToWindow() { + super.didMoveToWindow() + guard !self.isAdded else { return } + self.viewController.willMove(toParent: self.parentViewController()) + self.parentViewController().addChild(self.viewController) + self.viewController.didMove(toParent: self.parentViewController()) + self.viewController.view.isUserInteractionEnabled = true + isAdded = true + } + + override func layoutSubviews() { + super.layoutSubviews() + self.viewModel.size = bounds.size } } @@ -80,12 +104,16 @@ struct FlutterAirshipEmbeddedView: View { @ObservedObject var viewModel: ViewModel + init(viewModel: ViewModel) { + self.viewModel = viewModel + } + var body: some View { if let embeddedID = viewModel.embeddedID { AirshipEmbeddedView(embeddedID: embeddedID, embeddedSize: .init( - parentWidth: viewModel.width, - parentHeight: viewModel.height + parentWidth: viewModel.size?.width, + parentHeight: viewModel.size?.height ) ) { Text("Placeholder: \(embeddedID) \(viewModel.size ?? CGSize())") @@ -102,16 +130,30 @@ struct FlutterAirshipEmbeddedView: View { var height: CGFloat { guard let height = self.size?.height, height > 0 else { - return (try? AirshipUtils.mainWindow()?.screen.bounds.height) ?? 420 + return try! AirshipUtils.mainWindow()?.screen.bounds.height ?? 500 } return height } var width: CGFloat { guard let width = self.size?.width, width > 0 else { - return (try? AirshipUtils.mainWindow()?.screen.bounds.width) ?? 420 + return try! AirshipUtils.mainWindow()?.screen.bounds.width ?? 500 } return width } } } + +extension UIView { + //Get Parent View Controller from any view + func parentViewController() -> UIViewController { + var responder: UIResponder? = self + while !(responder is UIViewController) { + responder = responder?.next + if nil == responder { + break + } + } + return (responder as? UIViewController)! + } +} diff --git a/lib/src/airship_embedded_view.dart b/lib/src/airship_embedded_view.dart index 904f6bfb..4119c4f8 100644 --- a/lib/src/airship_embedded_view.dart +++ b/lib/src/airship_embedded_view.dart @@ -56,12 +56,13 @@ class EmbeddedViewState extends State { _getNativeViewSize(); } + /// Fall back to screen-sized constraints when constraints can be inferred Widget wrapWithLayoutBuilder(Widget view) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { - double width = constraints.maxWidth; - double height = constraints.maxHeight; + double width = _nativeViewWidth; + double height = _nativeViewHeight; if (width == 0 || width == double.infinity) { width = MediaQuery.of(context).size.width; @@ -71,17 +72,15 @@ class EmbeddedViewState extends State { height = MediaQuery.of(context).size.height; } - return FittedBox( - fit: BoxFit.contain, - alignment: Alignment.center, - child: ConstrainedBox( + return ConstrainedBox( constraints: BoxConstraints( - maxWidth: width, - maxHeight: height, + minWidth: 10, + minHeight: 10, + maxWidth: MediaQuery.of(context).size.width, + maxHeight: MediaQuery.of(context).size.height ), child: view, - ), - ); + ); }, ); } From 0467c99a06949afee7a3f07390c529550929727f Mon Sep 17 00:00:00 2001 From: crow Date: Sun, 18 Aug 2024 12:13:37 -0700 Subject: [PATCH 09/22] Use setup-pubdev-credentials 0.1.0 --- .github/workflows/release.yml | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4c784d70..c1adb3a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,20 +61,11 @@ jobs: - run: flutter pub get - name: Setup pub credentials - run: | - mkdir -p ~/.pub-cache - cat < ~/.pub-cache/credentials.json - { - "accessToken":"${{ secrets.OAUTH_ACCESS_TOKEN }}", - "refreshToken":"${{ secrets.OAUTH_REFRESH_TOKEN }}", - "tokenEndpoint":"https://accounts.google.com/o/oauth2/token", - "scopes": [ "openid", "https://www.googleapis.com/auth/userinfo.email" ], - "expiration": ${{secrets.OAUTH_EXPIRATION }} - } - EOF - mkdir -p ~/Library/Application\ Support/dart/ - cp ~/.pub-cache/credentials.json ~/Library/Application\ Support/dart/pub-credentials.json - cp ~/.pub-cache/credentials.json $PUB_CACHE/credentials.json || true + uses: setup-pubdev-credentials@v0.1.0 + with: + oauth_access_token: ${{ secrets.OAUTH_ACCESS_TOKEN }} + oauth_refresh_token: ${{ secrets.OAUTH_REFRESH_TOKEN }} + oauth_expiration: ${{ secrets.OAUTH_EXPIRATION }} - name: Publish Dart/Flutter package run: flutter pub publish -f From 13900c80855fd81e615eabd4470d0bf17b3d758d Mon Sep 17 00:00:00 2001 From: crow Date: Sun, 18 Aug 2024 12:17:37 -0700 Subject: [PATCH 10/22] Revert "Use setup-pubdev-credentials 0.1.0" This reverts commit 0467c99a06949afee7a3f07390c529550929727f. --- .github/workflows/release.yml | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c1adb3a4..4c784d70 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,11 +61,20 @@ jobs: - run: flutter pub get - name: Setup pub credentials - uses: setup-pubdev-credentials@v0.1.0 - with: - oauth_access_token: ${{ secrets.OAUTH_ACCESS_TOKEN }} - oauth_refresh_token: ${{ secrets.OAUTH_REFRESH_TOKEN }} - oauth_expiration: ${{ secrets.OAUTH_EXPIRATION }} + run: | + mkdir -p ~/.pub-cache + cat < ~/.pub-cache/credentials.json + { + "accessToken":"${{ secrets.OAUTH_ACCESS_TOKEN }}", + "refreshToken":"${{ secrets.OAUTH_REFRESH_TOKEN }}", + "tokenEndpoint":"https://accounts.google.com/o/oauth2/token", + "scopes": [ "openid", "https://www.googleapis.com/auth/userinfo.email" ], + "expiration": ${{secrets.OAUTH_EXPIRATION }} + } + EOF + mkdir -p ~/Library/Application\ Support/dart/ + cp ~/.pub-cache/credentials.json ~/Library/Application\ Support/dart/pub-credentials.json + cp ~/.pub-cache/credentials.json $PUB_CACHE/credentials.json || true - name: Publish Dart/Flutter package run: flutter pub publish -f From 09a54034579a58134316d0d71aa3a4374baabee1 Mon Sep 17 00:00:00 2001 From: crow Date: Tue, 27 Aug 2024 11:52:59 -0700 Subject: [PATCH 11/22] Add embedded view isReady support to iOS via new automation module --- example/lib/main.dart | 3 + example/lib/screens/home.dart | 13 +-- ios/Classes/AirshipEmbeddedView.swift | 10 +- ios/Classes/SwiftAirshipPlugin.swift | 3 +- lib/src/airship_automation.dart | 39 +++++++ lib/src/airship_embedded_view.dart | 161 ++++++++++++++------------ lib/src/airship_events.dart | 34 ++++-- lib/src/airship_flutter.dart | 19 ++- 8 files changed, 180 insertions(+), 102 deletions(-) create mode 100644 lib/src/airship_automation.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 738303a7..a3e8159a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -135,6 +135,9 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { } }); + Airship.automation.onEmbeddedInfoUpdated + .listen((event) => debugPrint('Embedded info updated $event')); + Airship.messageCenter.onInboxUpdated .listen((event) => debugPrint('Inbox updated $event')); diff --git a/example/lib/screens/home.dart b/example/lib/screens/home.dart index 83765c86..d025dba9 100644 --- a/example/lib/screens/home.dart +++ b/example/lib/screens/home.dart @@ -35,8 +35,7 @@ class _HomeState extends State { alignment: Alignment.center, child: Wrap(children: [ Center( - child:EmbeddedView(embeddedId: "test"), - ), + child: EmbeddedView(embeddedId: "test", parentHeight: 200)), Image.asset( 'assets/airship.png', ), @@ -48,11 +47,11 @@ class _HomeState extends State { bool pushEnabled = snapshot.data ?? false; enableNotificationsButton = Center(child: NotificationsEnabledButton( - onPressed: () { - Airship.push.setUserNotificationsEnabled(true); - setState(() {}); - }, - )); + onPressed: () { + Airship.push.setUserNotificationsEnabled(true); + setState(() {}); + }, + )); return Visibility( visible: !pushEnabled, child: enableNotificationsButton); diff --git a/ios/Classes/AirshipEmbeddedView.swift b/ios/Classes/AirshipEmbeddedView.swift index d81c5b7e..97e13b56 100644 --- a/ios/Classes/AirshipEmbeddedView.swift +++ b/ios/Classes/AirshipEmbeddedView.swift @@ -67,12 +67,6 @@ class AirshipEmbeddedViewWrapper: UIView, FlutterPlatformView { public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { - case "getSize": - let width = self.bounds.width - let height = self.bounds.height - result(["width": width, "height": height]) - case "getIsAdded": - result(["isAdded": self.isAdded]) default: result(FlutterError(code: "UNAVAILABLE", message: "Unknown method: \(call.method)", @@ -115,9 +109,7 @@ struct FlutterAirshipEmbeddedView: View { parentWidth: viewModel.size?.width, parentHeight: viewModel.size?.height ) - ) { - Text("Placeholder: \(embeddedID) \(viewModel.size ?? CGSize())") - } + ) } else { Text("Please set embeddedId") } diff --git a/ios/Classes/SwiftAirshipPlugin.swift b/ios/Classes/SwiftAirshipPlugin.swift index ddb038a3..4a1143bd 100644 --- a/ios/Classes/SwiftAirshipPlugin.swift +++ b/ios/Classes/SwiftAirshipPlugin.swift @@ -15,7 +15,8 @@ public class SwiftAirshipPlugin: NSObject, FlutterPlugin { .displayPreferenceCenter: "com.airship.flutter/event/display_preference_center", .notificationResponseReceived: "com.airship.flutter/event/notification_response", .pushReceived: "com.airship.flutter/event/push_received", - .notificationStatusChanged: "com.airship.flutter/event/notification_status_changed" + .notificationStatusChanged: "com.airship.flutter/event/notification_status_changed", + .embeddedInfoUpdated: "com.airship.flutter/event/embedded_info_updated" ] private let streams: [AirshipProxyEventType: AirshipEventStream] = { diff --git a/lib/src/airship_automation.dart b/lib/src/airship_automation.dart new file mode 100644 index 00000000..8105b9a1 --- /dev/null +++ b/lib/src/airship_automation.dart @@ -0,0 +1,39 @@ +import 'dart:async'; +import 'airship_module.dart'; +import 'airship_events.dart'; + +class AirshipAutomation { + final AirshipModule _module; + final Map> _isReadyControllers = {}; + List _embeddedIds = []; + late StreamSubscription _subscription; + + AirshipAutomation(this._module) { + _subscription = onEmbeddedInfoUpdated.listen(_updateEmbeddedIds); + } + + Stream isReadyStream({required String embeddedId}) => + (_isReadyControllers[embeddedId] ??= StreamController.broadcast() + ..add(isReady(embeddedId: embeddedId))) + .stream; + + bool isReady({required String embeddedId}) => + _embeddedIds.contains(embeddedId); + + Stream get onEmbeddedInfoUpdated => _module + .getEventStream("com.airship.flutter/event/embedded_info_updated") + .map(EmbeddedInfoUpdatedEvent.fromJson); + + void _updateEmbeddedIds(EmbeddedInfoUpdatedEvent event) { + _embeddedIds = event.embeddedIds; + _isReadyControllers + .forEach((id, controller) => controller.add(_embeddedIds.contains(id))); + } + + void dispose() { + _subscription.cancel(); + for (var controller in _isReadyControllers.values) { + controller.close(); + } + } +} diff --git a/lib/src/airship_embedded_view.dart b/lib/src/airship_embedded_view.dart index 4119c4f8..729caa58 100644 --- a/lib/src/airship_embedded_view.dart +++ b/lib/src/airship_embedded_view.dart @@ -5,42 +5,56 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart'; +import 'package:airship_flutter/airship_flutter.dart'; -/// Embedded view component. +/// Embedded platform view. +/// +/// Note: When an embedded view is set to display with its height set to `auto` +/// the embedded view will size to its native aspect ratio. Any remaining space +/// in the parent view will be apparent. class EmbeddedView extends StatefulWidget { /// The embedded view Id. final String embeddedId; + /// Optional parent width. If not provided, the widget will use available width. + final double? parentWidth; + + /// Optional parent height. If not provided, the widget will use available height. + /// Use parentHeight for constant height instead of a height-constrained container. + /// This allows proper collapse to 0 height when the view is dismissed. + final double? parentHeight; + /// A flag to use flutter hybrid composition method or not. Default to false. static bool hybridComposition = false; - EmbeddedView({required this.embeddedId}); + EmbeddedView({ + required this.embeddedId, + this.parentWidth, + this.parentHeight, + }); @override EmbeddedViewState createState() => EmbeddedViewState(); } -class EmbeddedViewState extends State { +class EmbeddedViewState extends State + with AutomaticKeepAliveClientMixin { late MethodChannel _channel; - - double _nativeViewWidth = 0; - double _nativeViewHeight = 0; + late Stream _readyStream; + bool? _isReady; @override void initState() { super.initState(); - } - - Future _getNativeViewSize() async { - try { - final size = await _channel.invokeMethod('getSize'); - setState(() { - _nativeViewWidth = size?['width']; - _nativeViewHeight = size?['height']; - }); - } on PlatformException catch (e) { - print("Failed to get native view size: '${e.message}'."); - } + _readyStream = + Airship.automation.isReadyStream(embeddedId: widget.embeddedId); + _readyStream.listen((isReady) { + if (mounted) { + setState(() { + _isReady = isReady; + }); + } + }); } Future _methodCallHandler(MethodCall call) async { @@ -53,40 +67,39 @@ class EmbeddedViewState extends State { Future _onPlatformViewCreated(int id) async { _channel = MethodChannel('com.airship.flutter/EmbeddedView_$id'); _channel.setMethodCallHandler(_methodCallHandler); - _getNativeViewSize(); } + Widget buildReadyView(BuildContext context, Widget view, Size availableSize) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition(opacity: animation, child: child); + }, + child: _isReady == true + ? SizedBox( + key: ValueKey(true), + width: widget.parentWidth ?? availableSize.width, + height: widget.parentHeight ?? availableSize.height, + child: view, + ) + : SizedBox(key: ValueKey(false), height: 0), + ); + } - /// Fall back to screen-sized constraints when constraints can be inferred Widget wrapWithLayoutBuilder(Widget view) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { - double width = _nativeViewWidth; - double height = _nativeViewHeight; - - if (width == 0 || width == double.infinity) { - width = MediaQuery.of(context).size.width; - } - - if (height == 0 || height == double.infinity) { - height = MediaQuery.of(context).size.height; - } - - return ConstrainedBox( - constraints: BoxConstraints( - minWidth: 10, - minHeight: 10, - maxWidth: MediaQuery.of(context).size.width, - maxHeight: MediaQuery.of(context).size.height - ), - child: view, - ); + final availableSize = MediaQuery.of(context).size; + + return Center(child: buildReadyView(context, view, availableSize)); }, ); } @override Widget build(BuildContext context) { + super.build(context); + if (defaultTargetPlatform == TargetPlatform.android) { return _getAndroidView(); } else if (defaultTargetPlatform == TargetPlatform.iOS) { @@ -107,45 +120,42 @@ class EmbeddedViewState extends State { Widget _getAndroidView() { if (EmbeddedView.hybridComposition) { - return wrapWithLayoutBuilder( - PlatformViewLink( + return wrapWithLayoutBuilder(PlatformViewLink( + viewType: 'com.airship.flutter/EmbeddedView', + surfaceFactory: + (BuildContext context, PlatformViewController controller) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + return PlatformViewsService.initSurfaceAndroidView( + id: params.id, viewType: 'com.airship.flutter/EmbeddedView', - surfaceFactory: (BuildContext context, PlatformViewController controller) { - return AndroidViewSurface( - controller: controller as AndroidViewController, - gestureRecognizers: const >{}, - hitTestBehavior: PlatformViewHitTestBehavior.opaque, - ); + layoutDirection: TextDirection.ltr, + creationParams: { + 'embeddedId': widget.embeddedId, }, - onCreatePlatformView: (PlatformViewCreationParams params) { - return PlatformViewsService.initSurfaceAndroidView( - id: params.id, - viewType: 'com.airship.flutter/EmbeddedView', - layoutDirection: TextDirection.ltr, - creationParams: { - 'embeddedId': widget.embeddedId, - }, - creationParamsCodec: const StandardMessageCodec(), - onFocus: () { - params.onFocusChanged(true); - }, - ) - ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) - ..create(); + creationParamsCodec: const StandardMessageCodec(), + onFocus: () { + params.onFocusChanged(true); }, ) - ); + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..create(); + }, + )); } else { - return wrapWithLayoutBuilder( - AndroidView( - viewType: 'com.airship.flutter/EmbeddedView', - onPlatformViewCreated: _onPlatformViewCreated, - creationParams: { - 'embeddedId': widget.embeddedId, - }, - creationParamsCodec: const StandardMessageCodec(), - ) - ); + return wrapWithLayoutBuilder(AndroidView( + viewType: 'com.airship.flutter/EmbeddedView', + onPlatformViewCreated: _onPlatformViewCreated, + creationParams: { + 'embeddedId': widget.embeddedId, + }, + creationParamsCodec: const StandardMessageCodec(), + )); } } @@ -154,4 +164,7 @@ class EmbeddedViewState extends State { _channel.setMethodCallHandler(null); super.dispose(); } + + @override + bool get wantKeepAlive => true; } diff --git a/lib/src/airship_events.dart b/lib/src/airship_events.dart index 814f3332..eb977339 100644 --- a/lib/src/airship_events.dart +++ b/lib/src/airship_events.dart @@ -8,14 +8,14 @@ class IOSAuthorizedNotificationSettingsChangedEvent { // The authorized settings final List authorizedSettings; - const IOSAuthorizedNotificationSettingsChangedEvent._internal(this.authorizedSettings); + const IOSAuthorizedNotificationSettingsChangedEvent._internal( + this.authorizedSettings); static IOSAuthorizedNotificationSettingsChangedEvent fromJson(dynamic json) { var authorizedSettings = List.from(json["authorizedSettings"]); return IOSAuthorizedNotificationSettingsChangedEvent._internal( - AirshipUtils.parseIOSAuthorizedSettings(authorizedSettings) - ); + AirshipUtils.parseIOSAuthorizedSettings(authorizedSettings)); } @override @@ -42,22 +42,22 @@ class DisplayMessageCenterEvent { } } - /// Event fired when the message center updates. class MessageCenterUpdatedEvent { - /// Unread count final int messageUnreadCount; /// Message count final int messageCount; - const MessageCenterUpdatedEvent._internal(this.messageUnreadCount, this.messageCount); + const MessageCenterUpdatedEvent._internal( + this.messageUnreadCount, this.messageCount); static MessageCenterUpdatedEvent fromJson(dynamic json) { var messageUnreadCount = json["messageUnreadCount"]; var messageCount = json["messageCount"]; - return MessageCenterUpdatedEvent._internal(messageUnreadCount, messageCount); + return MessageCenterUpdatedEvent._internal( + messageUnreadCount, messageCount); } @override @@ -66,6 +66,24 @@ class MessageCenterUpdatedEvent { } } +/// Event fired when embedded view info updates. +class EmbeddedInfoUpdatedEvent { + /// Embedded IDs + final List embeddedIds; + + const EmbeddedInfoUpdatedEvent._internal(this.embeddedIds); + + static EmbeddedInfoUpdatedEvent fromJson(dynamic json) { + List embeddedIds = List.from(json["embeddedIds"] ?? []); + return EmbeddedInfoUpdatedEvent._internal(embeddedIds); + } + + @override + String toString() { + return "EmbeddedInfoUpdatedEvent(embeddedIds=$embeddedIds)"; + } +} + /// Event fired when a channel is created. class ChannelCreatedEvent { /// The channel ID. @@ -204,4 +222,4 @@ class PushReceivedEvent { String toString() { return "PushReceivedEvent(pushPayload=$pushPayload)"; } -} \ No newline at end of file +} diff --git a/lib/src/airship_flutter.dart b/lib/src/airship_flutter.dart index 9191c68e..044dd95f 100644 --- a/lib/src/airship_flutter.dart +++ b/lib/src/airship_flutter.dart @@ -1,38 +1,51 @@ import 'dart:async'; import 'package:airship_flutter/airship_flutter.dart'; +import 'package:airship_flutter/src/airship_automation.dart'; import 'airship_module.dart'; /// The Main Airship API. class Airship { - static final _module = AirshipModule(); /// The [AirshipChannel] instance. static final channel = AirshipChannel(_module); + /// The [AirshipPush] instance. static final push = AirshipPush(_module); + /// The [AirshipContact] instance. static final contact = AirshipContact(_module); + /// The [AirshipInApp] instance. static final inApp = AirshipInApp(_module); + /// The [AirshipMessageCenter] instance. static final messageCenter = AirshipMessageCenter(_module); + /// The [AirshipPrivacyManager] instance. static final privacyManager = AirshipPrivacyManager(_module); + /// The [AirshipPreferenceCenter] instance. static final preferenceCenter = AirshipPreferenceCenter(_module); + /// The [AirshipLocale] instance. static final locale = AirshipLocale(_module); + /// The [AirshipAnalytics] instance. static final analytics = AirshipAnalytics(_module); + /// The [AirshipActions] instance. static final actions = AirshipActions(_module); + /// The [AirshipFeatureFlagManager] instance. static final featureFlagManager = AirshipFeatureFlagManager(_module); + /// The [AirshipAutomation] instance. + static final automation = AirshipAutomation(_module); + // /// Initializes Airship with the given config. Airship will store the config - /// and automatically use it during the next app init. Any chances to config + /// and automatically use it during the next app init. Any chances to config /// could take an extra app init to apply. /// /// Returns true if Airship has been initialized, otherwise returns false. @@ -46,4 +59,4 @@ class Airship { .getEventStream("com.airship.flutter/event/deep_link_received") .map((dynamic value) => DeepLinkEvent.fromJson(value)); } -} \ No newline at end of file +} From 9d19ab3849a477bbef47620f1c89f6745e2980a0 Mon Sep 17 00:00:00 2001 From: crow Date: Thu, 29 Aug 2024 13:55:10 -0700 Subject: [PATCH 12/22] Use EmbeddedInfo intermediate object --- lib/src/airship_automation.dart | 52 +++++++++++++++++++++++------- lib/src/airship_embedded_view.dart | 12 +++---- lib/src/airship_events.dart | 28 +++++++++++----- 3 files changed, 66 insertions(+), 26 deletions(-) diff --git a/lib/src/airship_automation.dart b/lib/src/airship_automation.dart index 8105b9a1..97dcfc71 100644 --- a/lib/src/airship_automation.dart +++ b/lib/src/airship_automation.dart @@ -1,38 +1,66 @@ import 'dart:async'; import 'airship_module.dart'; -import 'airship_events.dart'; + +class EmbeddedInfo { + final String embeddedId; + + EmbeddedInfo(this.embeddedId); + + @override + String toString() => "EmbeddedInfo(embeddedId=$embeddedId)"; +} + +class EmbeddedInfoUpdatedEvent { + final List embeddedInfos; + + const EmbeddedInfoUpdatedEvent(this.embeddedInfos); + + static EmbeddedInfoUpdatedEvent fromJson(dynamic json) { + List pendingList = json['pending'] as List? ?? []; + + List embeddedInfos = + pendingList.map((item) => EmbeddedInfo(item['embeddedId'])).toList(); + + return EmbeddedInfoUpdatedEvent(embeddedInfos); + } + + @override + String toString() => "EmbeddedInfoUpdatedEvent(embeddedInfos=$embeddedInfos)"; +} class AirshipAutomation { final AirshipModule _module; - final Map> _isReadyControllers = {}; - List _embeddedIds = []; + final Map> _isEmbeddedAvailableControllers = + {}; + List _embeddedInfos = []; late StreamSubscription _subscription; AirshipAutomation(this._module) { _subscription = onEmbeddedInfoUpdated.listen(_updateEmbeddedIds); } - Stream isReadyStream({required String embeddedId}) => - (_isReadyControllers[embeddedId] ??= StreamController.broadcast() - ..add(isReady(embeddedId: embeddedId))) + Stream isEmbeddedAvailableStream({required String embeddedId}) => + (_isEmbeddedAvailableControllers[embeddedId] ??= + StreamController.broadcast() + ..add(isEmbeddedAvailable(embeddedId: embeddedId))) .stream; - bool isReady({required String embeddedId}) => - _embeddedIds.contains(embeddedId); + bool isEmbeddedAvailable({required String embeddedId}) => + _embeddedInfos.any((info) => info.embeddedId == embeddedId); Stream get onEmbeddedInfoUpdated => _module .getEventStream("com.airship.flutter/event/embedded_info_updated") .map(EmbeddedInfoUpdatedEvent.fromJson); void _updateEmbeddedIds(EmbeddedInfoUpdatedEvent event) { - _embeddedIds = event.embeddedIds; - _isReadyControllers - .forEach((id, controller) => controller.add(_embeddedIds.contains(id))); + _embeddedInfos = event.embeddedInfos; + _isEmbeddedAvailableControllers.forEach((id, controller) => + controller.add(isEmbeddedAvailable(embeddedId: id))); } void dispose() { _subscription.cancel(); - for (var controller in _isReadyControllers.values) { + for (var controller in _isEmbeddedAvailableControllers.values) { controller.close(); } } diff --git a/lib/src/airship_embedded_view.dart b/lib/src/airship_embedded_view.dart index 729caa58..f5fd3a1d 100644 --- a/lib/src/airship_embedded_view.dart +++ b/lib/src/airship_embedded_view.dart @@ -41,17 +41,17 @@ class EmbeddedViewState extends State with AutomaticKeepAliveClientMixin { late MethodChannel _channel; late Stream _readyStream; - bool? _isReady; + bool? _isEmbeddedAvailable; @override void initState() { super.initState(); - _readyStream = - Airship.automation.isReadyStream(embeddedId: widget.embeddedId); - _readyStream.listen((isReady) { + _readyStream = Airship.automation + .isEmbeddedAvailableStream(embeddedId: widget.embeddedId); + _readyStream.listen((isEmbeddedAvailable) { if (mounted) { setState(() { - _isReady = isReady; + _isEmbeddedAvailable = isEmbeddedAvailable; }); } }); @@ -75,7 +75,7 @@ class EmbeddedViewState extends State transitionBuilder: (Widget child, Animation animation) { return FadeTransition(opacity: animation, child: child); }, - child: _isReady == true + child: _isEmbeddedAvailable == true ? SizedBox( key: ValueKey(true), width: widget.parentWidth ?? availableSize.width, diff --git a/lib/src/airship_events.dart b/lib/src/airship_events.dart index eb977339..57b0292f 100644 --- a/lib/src/airship_events.dart +++ b/lib/src/airship_events.dart @@ -1,3 +1,5 @@ +import 'package:airship_flutter/src/airship_automation.dart'; + import 'push_payload.dart'; import 'push_notification_status.dart'; import 'ios_push_options.dart'; @@ -66,22 +68,32 @@ class MessageCenterUpdatedEvent { } } +class EmbeddedInfo { + final String embeddedId; + + EmbeddedInfo(this.embeddedId); + + @override + String toString() => "EmbeddedInfo(embeddedId=$embeddedId)"; +} + /// Event fired when embedded view info updates. class EmbeddedInfoUpdatedEvent { - /// Embedded IDs - final List embeddedIds; + final List embeddedInfos; - const EmbeddedInfoUpdatedEvent._internal(this.embeddedIds); + const EmbeddedInfoUpdatedEvent(this.embeddedInfos); static EmbeddedInfoUpdatedEvent fromJson(dynamic json) { - List embeddedIds = List.from(json["embeddedIds"] ?? []); - return EmbeddedInfoUpdatedEvent._internal(embeddedIds); + List pendingList = json['pending'] as List? ?? []; + + List embeddedInfos = + pendingList.map((item) => EmbeddedInfo(item['embeddedId'])).toList(); + + return EmbeddedInfoUpdatedEvent(embeddedInfos); } @override - String toString() { - return "EmbeddedInfoUpdatedEvent(embeddedIds=$embeddedIds)"; - } + String toString() => "EmbeddedInfoUpdatedEvent(embeddedInfos=$embeddedInfos)"; } /// Event fired when a channel is created. From aceacf650de528078815be909887637e7ec9cac6 Mon Sep 17 00:00:00 2001 From: crow Date: Thu, 29 Aug 2024 14:05:55 -0700 Subject: [PATCH 13/22] Add embedded infos getter --- lib/src/airship_automation.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/airship_automation.dart b/lib/src/airship_automation.dart index 97dcfc71..033a595c 100644 --- a/lib/src/airship_automation.dart +++ b/lib/src/airship_automation.dart @@ -39,14 +39,16 @@ class AirshipAutomation { _subscription = onEmbeddedInfoUpdated.listen(_updateEmbeddedIds); } + bool isEmbeddedAvailable({required String embeddedId}) => + _embeddedInfos.any((info) => info.embeddedId == embeddedId); + Stream isEmbeddedAvailableStream({required String embeddedId}) => (_isEmbeddedAvailableControllers[embeddedId] ??= StreamController.broadcast() ..add(isEmbeddedAvailable(embeddedId: embeddedId))) .stream; - bool isEmbeddedAvailable({required String embeddedId}) => - _embeddedInfos.any((info) => info.embeddedId == embeddedId); + List getEmbeddedInfos() => _embeddedInfos; Stream get onEmbeddedInfoUpdated => _module .getEventStream("com.airship.flutter/event/embedded_info_updated") From 7df24446138472cbbc3c1f7f0aab8c31255b6e4b Mon Sep 17 00:00:00 2001 From: crow Date: Thu, 29 Aug 2024 15:23:19 -0700 Subject: [PATCH 14/22] Remove automation module --- example/lib/main.dart | 2 +- lib/src/airship_embedded_view.dart | 4 +-- lib/src/airship_events.dart | 3 +- lib/src/airship_flutter.dart | 4 --- lib/src/airship_in_app.dart | 46 +++++++++++++++++++++++++++--- 5 files changed, 46 insertions(+), 13 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index a3e8159a..0eb1ff40 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -135,7 +135,7 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { } }); - Airship.automation.onEmbeddedInfoUpdated + Airship.inApp.onEmbeddedInfoUpdated .listen((event) => debugPrint('Embedded info updated $event')); Airship.messageCenter.onInboxUpdated diff --git a/lib/src/airship_embedded_view.dart b/lib/src/airship_embedded_view.dart index f5fd3a1d..55356c5a 100644 --- a/lib/src/airship_embedded_view.dart +++ b/lib/src/airship_embedded_view.dart @@ -46,8 +46,8 @@ class EmbeddedViewState extends State @override void initState() { super.initState(); - _readyStream = Airship.automation - .isEmbeddedAvailableStream(embeddedId: widget.embeddedId); + _readyStream = + Airship.inApp.isEmbeddedAvailableStream(embeddedId: widget.embeddedId); _readyStream.listen((isEmbeddedAvailable) { if (mounted) { setState(() { diff --git a/lib/src/airship_events.dart b/lib/src/airship_events.dart index 57b0292f..1ef9fcc9 100644 --- a/lib/src/airship_events.dart +++ b/lib/src/airship_events.dart @@ -1,9 +1,8 @@ -import 'package:airship_flutter/src/airship_automation.dart'; - import 'push_payload.dart'; import 'push_notification_status.dart'; import 'ios_push_options.dart'; import 'airship_utils.dart'; +import 'package:airship_flutter/src/airship_in_app.dart'; /// Event fired when the iOS authorized settings change. class IOSAuthorizedNotificationSettingsChangedEvent { diff --git a/lib/src/airship_flutter.dart b/lib/src/airship_flutter.dart index 044dd95f..e50dd904 100644 --- a/lib/src/airship_flutter.dart +++ b/lib/src/airship_flutter.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'package:airship_flutter/airship_flutter.dart'; -import 'package:airship_flutter/src/airship_automation.dart'; import 'airship_module.dart'; /// The Main Airship API. @@ -40,9 +39,6 @@ class Airship { /// The [AirshipFeatureFlagManager] instance. static final featureFlagManager = AirshipFeatureFlagManager(_module); - /// The [AirshipAutomation] instance. - static final automation = AirshipAutomation(_module); - // /// Initializes Airship with the given config. Airship will store the config /// and automatically use it during the next app init. Any chances to config diff --git a/lib/src/airship_in_app.dart b/lib/src/airship_in_app.dart index 8e6f3c4f..087c6d6a 100644 --- a/lib/src/airship_in_app.dart +++ b/lib/src/airship_in_app.dart @@ -1,10 +1,19 @@ +import 'package:airship_flutter/airship_flutter.dart'; + import 'airship_module.dart'; +import 'dart:async'; class AirshipInApp { - final AirshipModule _module; - AirshipInApp(AirshipModule module) : _module = module; + AirshipInApp(this._module) { + _subscription = onEmbeddedInfoUpdated.listen(_updateEmbeddedIds); + } + + final Map> _isEmbeddedAvailableControllers = + {}; + List _embeddedInfos = []; + late StreamSubscription _subscription; /// Pauses or unpauses in-app automation. Future setPaused(bool paused) async { @@ -18,11 +27,40 @@ class AirshipInApp { /// Sets the display interval for messages. Future setDisplayInterval(int milliseconds) async { - return _module.channel.invokeMethod('inApp#setDisplayInterval', milliseconds); + return _module.channel + .invokeMethod('inApp#setDisplayInterval', milliseconds); } /// Gets the display interval for messages. Future get displayInterval async { return await _module.channel.invokeMethod('inApp#getDisplayInterval'); } -} \ No newline at end of file + + bool isEmbeddedAvailable({required String embeddedId}) => + _embeddedInfos.any((info) => info.embeddedId == embeddedId); + + Stream isEmbeddedAvailableStream({required String embeddedId}) => + (_isEmbeddedAvailableControllers[embeddedId] ??= + StreamController.broadcast() + ..add(isEmbeddedAvailable(embeddedId: embeddedId))) + .stream; + + List getEmbeddedInfos() => _embeddedInfos; + + Stream get onEmbeddedInfoUpdated => _module + .getEventStream("com.airship.flutter/event/embedded_info_updated") + .map(EmbeddedInfoUpdatedEvent.fromJson); + + void _updateEmbeddedIds(EmbeddedInfoUpdatedEvent event) { + _embeddedInfos = event.embeddedInfos; + _isEmbeddedAvailableControllers.forEach((id, controller) => + controller.add(isEmbeddedAvailable(embeddedId: id))); + } + + void dispose() { + _subscription.cancel(); + for (var controller in _isEmbeddedAvailableControllers.values) { + controller.close(); + } + } +} From fbe48e7247c1ee0e011011586accea52055f8178 Mon Sep 17 00:00:00 2001 From: crow Date: Thu, 29 Aug 2024 15:29:26 -0700 Subject: [PATCH 15/22] Event naming --- ios/Classes/SwiftAirshipPlugin.swift | 2 +- lib/src/airship_in_app.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/Classes/SwiftAirshipPlugin.swift b/ios/Classes/SwiftAirshipPlugin.swift index 4a1143bd..f001d23f 100644 --- a/ios/Classes/SwiftAirshipPlugin.swift +++ b/ios/Classes/SwiftAirshipPlugin.swift @@ -16,7 +16,7 @@ public class SwiftAirshipPlugin: NSObject, FlutterPlugin { .notificationResponseReceived: "com.airship.flutter/event/notification_response", .pushReceived: "com.airship.flutter/event/push_received", .notificationStatusChanged: "com.airship.flutter/event/notification_status_changed", - .embeddedInfoUpdated: "com.airship.flutter/event/embedded_info_updated" + .pendingEmbeddedUpdated: "com.airship.flutter/event/pending_embedded_updated" ] private let streams: [AirshipProxyEventType: AirshipEventStream] = { diff --git a/lib/src/airship_in_app.dart b/lib/src/airship_in_app.dart index 087c6d6a..1b78054a 100644 --- a/lib/src/airship_in_app.dart +++ b/lib/src/airship_in_app.dart @@ -48,7 +48,7 @@ class AirshipInApp { List getEmbeddedInfos() => _embeddedInfos; Stream get onEmbeddedInfoUpdated => _module - .getEventStream("com.airship.flutter/event/embedded_info_updated") + .getEventStream("com.airship.flutter/event/pending_embedded_updated") .map(EmbeddedInfoUpdatedEvent.fromJson); void _updateEmbeddedIds(EmbeddedInfoUpdatedEvent event) { From acc6deacb48aef61c5ae91800565dd20ac6ea699 Mon Sep 17 00:00:00 2001 From: crow Date: Fri, 30 Aug 2024 10:49:56 -0700 Subject: [PATCH 16/22] Clean up streams and create a wrapped object for AirshipEventStream --- .../com/airship/flutter/AirshipPlugin.kt | 3 +- ios/Classes/SwiftAirshipPlugin.swift | 84 +++++++++++-------- lib/src/airship_automation.dart | 69 --------------- lib/src/airship_events.dart | 1 - lib/src/airship_in_app.dart | 65 +++++++------- 5 files changed, 86 insertions(+), 136 deletions(-) delete mode 100644 lib/src/airship_automation.dart diff --git a/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt b/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt index cf14c42a..50c42a7f 100644 --- a/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt +++ b/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt @@ -60,7 +60,8 @@ class AirshipPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { EventType.PUSH_TOKEN_RECEIVED to "com.airship.flutter/event/push_token_received", EventType.FOREGROUND_PUSH_RECEIVED to "com.airship.flutter/event/push_received", EventType.BACKGROUND_PUSH_RECEIVED to "com.airship.flutter/event/background_push_received", - EventType.NOTIFICATION_STATUS_CHANGED to "com.airship.flutter/event/notification_status_changed" + EventType.NOTIFICATION_STATUS_CHANGED to "com.airship.flutter/event/notification_status_changed", + EventType.PENDING_EMBEDDED_UPDATED to "com.airship.flutter/event/pending_embedded_updated" ) } diff --git a/ios/Classes/SwiftAirshipPlugin.swift b/ios/Classes/SwiftAirshipPlugin.swift index f001d23f..71d2dc77 100644 --- a/ios/Classes/SwiftAirshipPlugin.swift +++ b/ios/Classes/SwiftAirshipPlugin.swift @@ -542,67 +542,85 @@ extension FlutterMethodCall { } } -internal class AirshipEventStream : NSObject, FlutterStreamHandler { +protocol AirshipEventStreamHandlerDelegate: AnyObject { + func handlerWasRemoved(_ handler: AirshipEventStreamHandler) +} + +class AirshipEventStreamHandler: NSObject, FlutterStreamHandler { + private var eventSink: FlutterEventSink? + weak var delegate: AirshipEventStreamHandlerDelegate? + + func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + self.eventSink = events + return nil + } + + func onCancel(withArguments arguments: Any?) -> FlutterError? { + self.eventSink = nil + delegate?.handlerWasRemoved(self) + return nil + } + + func notify(_ event: Any) -> Bool { + if let sink = self.eventSink { + sink(event) + return true + } + return false + } +} - private var eventSink : FlutterEventSink? +class AirshipEventStream: NSObject, AirshipEventStreamHandlerDelegate { private let eventType: AirshipProxyEventType - private let lock = AirshipLock() private let name: String - + private let lock = AirshipLock() + private var handlers: [AirshipEventStreamHandler] = [] + init(_ eventType: AirshipProxyEventType, name: String) { self.eventType = eventType self.name = name } - @MainActor - private func notify(_ event: AirshipProxyEvent) -> Bool { - var result = false - lock.sync { - if let sink = self.eventSink { - sink(event.body) - result = true - } - } - - return result - } - func register(registrar: FlutterPluginRegistrar) { let eventChannel = FlutterEventChannel( name: self.name, binaryMessenger: registrar.messenger() ) - eventChannel.setStreamHandler(self) + let handler = AirshipEventStreamHandler() + handler.delegate = self + eventChannel.setStreamHandler(handler) + + lock.sync { + handlers.append(handler) + } } - + @MainActor func processPendingEvents() async { await AirshipProxyEventEmitter.shared.processPendingEvents( type: eventType, - handler: { event in + handler: { [weak self] event in + guard let self = self else { return false } return self.notify(event) } ) } - func onCancel(withArguments arguments: Any?) -> FlutterError? { + private func notify(_ event: AirshipProxyEvent) -> Bool { + var result = false lock.sync { - self.eventSink = nil + for handler in handlers { + if handler.notify(event.body) { + result = true + } + } } - - return nil + return result } - - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + func handlerWasRemoved(_ handler: AirshipEventStreamHandler) { lock.sync { - self.eventSink = events - } - - Task { - await processPendingEvents() + handlers.removeAll { $0 === handler } } - - return nil } } diff --git a/lib/src/airship_automation.dart b/lib/src/airship_automation.dart deleted file mode 100644 index 033a595c..00000000 --- a/lib/src/airship_automation.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'dart:async'; -import 'airship_module.dart'; - -class EmbeddedInfo { - final String embeddedId; - - EmbeddedInfo(this.embeddedId); - - @override - String toString() => "EmbeddedInfo(embeddedId=$embeddedId)"; -} - -class EmbeddedInfoUpdatedEvent { - final List embeddedInfos; - - const EmbeddedInfoUpdatedEvent(this.embeddedInfos); - - static EmbeddedInfoUpdatedEvent fromJson(dynamic json) { - List pendingList = json['pending'] as List? ?? []; - - List embeddedInfos = - pendingList.map((item) => EmbeddedInfo(item['embeddedId'])).toList(); - - return EmbeddedInfoUpdatedEvent(embeddedInfos); - } - - @override - String toString() => "EmbeddedInfoUpdatedEvent(embeddedInfos=$embeddedInfos)"; -} - -class AirshipAutomation { - final AirshipModule _module; - final Map> _isEmbeddedAvailableControllers = - {}; - List _embeddedInfos = []; - late StreamSubscription _subscription; - - AirshipAutomation(this._module) { - _subscription = onEmbeddedInfoUpdated.listen(_updateEmbeddedIds); - } - - bool isEmbeddedAvailable({required String embeddedId}) => - _embeddedInfos.any((info) => info.embeddedId == embeddedId); - - Stream isEmbeddedAvailableStream({required String embeddedId}) => - (_isEmbeddedAvailableControllers[embeddedId] ??= - StreamController.broadcast() - ..add(isEmbeddedAvailable(embeddedId: embeddedId))) - .stream; - - List getEmbeddedInfos() => _embeddedInfos; - - Stream get onEmbeddedInfoUpdated => _module - .getEventStream("com.airship.flutter/event/embedded_info_updated") - .map(EmbeddedInfoUpdatedEvent.fromJson); - - void _updateEmbeddedIds(EmbeddedInfoUpdatedEvent event) { - _embeddedInfos = event.embeddedInfos; - _isEmbeddedAvailableControllers.forEach((id, controller) => - controller.add(isEmbeddedAvailable(embeddedId: id))); - } - - void dispose() { - _subscription.cancel(); - for (var controller in _isEmbeddedAvailableControllers.values) { - controller.close(); - } - } -} diff --git a/lib/src/airship_events.dart b/lib/src/airship_events.dart index 1ef9fcc9..45e6c46f 100644 --- a/lib/src/airship_events.dart +++ b/lib/src/airship_events.dart @@ -2,7 +2,6 @@ import 'push_payload.dart'; import 'push_notification_status.dart'; import 'ios_push_options.dart'; import 'airship_utils.dart'; -import 'package:airship_flutter/src/airship_in_app.dart'; /// Event fired when the iOS authorized settings change. class IOSAuthorizedNotificationSettingsChangedEvent { diff --git a/lib/src/airship_in_app.dart b/lib/src/airship_in_app.dart index 1b78054a..8aee9117 100644 --- a/lib/src/airship_in_app.dart +++ b/lib/src/airship_in_app.dart @@ -1,66 +1,67 @@ import 'package:airship_flutter/airship_flutter.dart'; - import 'airship_module.dart'; import 'dart:async'; class AirshipInApp { final AirshipModule _module; - - AirshipInApp(this._module) { - _subscription = onEmbeddedInfoUpdated.listen(_updateEmbeddedIds); - } - final Map> _isEmbeddedAvailableControllers = {}; List _embeddedInfos = []; - late StreamSubscription _subscription; - - /// Pauses or unpauses in-app automation. - Future setPaused(bool paused) async { - return await _module.channel.invokeMethod('inApp#setPaused', paused); - } - - /// Checks if in-app automation is paused or not. - Future get isPaused async { - return await _module.channel.invokeMethod('inApp#isPaused'); - } + late final StreamController + _embeddedInfoUpdatedController; + late final StreamSubscription _subscription; - /// Sets the display interval for messages. - Future setDisplayInterval(int milliseconds) async { - return _module.channel - .invokeMethod('inApp#setDisplayInterval', milliseconds); - } + List getEmbeddedInfos() => _embeddedInfos; - /// Gets the display interval for messages. - Future get displayInterval async { - return await _module.channel.invokeMethod('inApp#getDisplayInterval'); + AirshipInApp(this._module) { + _embeddedInfoUpdatedController = + StreamController.broadcast(); + _setupEventStream(); + _subscription = onEmbeddedInfoUpdated.listen(_updateEmbeddedIds); } + /// A flag to determine if an embedded id is available that is not live updated bool isEmbeddedAvailable({required String embeddedId}) => _embeddedInfos.any((info) => info.embeddedId == embeddedId); + /// A live updated stream to determine if an embedded id is available Stream isEmbeddedAvailableStream({required String embeddedId}) => (_isEmbeddedAvailableControllers[embeddedId] ??= StreamController.broadcast() ..add(isEmbeddedAvailable(embeddedId: embeddedId))) .stream; - List getEmbeddedInfos() => _embeddedInfos; + Stream get onEmbeddedInfoUpdated => + _embeddedInfoUpdatedController.stream; - Stream get onEmbeddedInfoUpdated => _module - .getEventStream("com.airship.flutter/event/pending_embedded_updated") - .map(EmbeddedInfoUpdatedEvent.fromJson); + void _setupEventStream() { + _module + .getEventStream("com.airship.flutter/event/pending_embedded_updated") + .listen((event) { + try { + _embeddedInfoUpdatedController + .add(EmbeddedInfoUpdatedEvent.fromJson(event)); + } catch (e) { + print("Error parsing EmbeddedInfoUpdatedEvent: $e"); + } + }); + } void _updateEmbeddedIds(EmbeddedInfoUpdatedEvent event) { + /// Update the embedded infos list _embeddedInfos = event.embeddedInfos; + + /// Update stream controllers for each embedded id so everything remains synced _isEmbeddedAvailableControllers.forEach((id, controller) => controller.add(isEmbeddedAvailable(embeddedId: id))); } void dispose() { _subscription.cancel(); - for (var controller in _isEmbeddedAvailableControllers.values) { - controller.close(); - } + + /// Remove and close all stream controllers for each embedded id + _isEmbeddedAvailableControllers.values + .forEach((controller) => controller.close()); + _embeddedInfoUpdatedController.close(); } } From 68683a6a8263f0a00f761c177dfe47b035d43ba8 Mon Sep 17 00:00:00 2001 From: crow Date: Fri, 30 Aug 2024 11:51:39 -0700 Subject: [PATCH 17/22] Use latest proxy android --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index ef324231..7d0becf6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,7 +7,7 @@ buildscript { ext.kotlin_version = '1.9.0' ext.coroutine_version = '1.5.2' ext.datastore_preferences_version = '1.1.1' - ext.airship_framework_proxy_version = '7.1.1' + ext.airship_framework_proxy_version = '7.3.0' repositories { google() From 7a050dfe0342225212bc614bf0726f6c664135f5 Mon Sep 17 00:00:00 2001 From: crow Date: Fri, 30 Aug 2024 13:54:28 -0700 Subject: [PATCH 18/22] More clean up for streams implementation and add resend last module method --- ios/Classes/SwiftAirshipPlugin.swift | 19 +++------ lib/src/airship_in_app.dart | 61 ++++++++++++++-------------- 2 files changed, 36 insertions(+), 44 deletions(-) diff --git a/ios/Classes/SwiftAirshipPlugin.swift b/ios/Classes/SwiftAirshipPlugin.swift index 71d2dc77..35802db2 100644 --- a/ios/Classes/SwiftAirshipPlugin.swift +++ b/ios/Classes/SwiftAirshipPlugin.swift @@ -285,6 +285,10 @@ public class SwiftAirshipPlugin: NSObject, FlutterPlugin { case "inApp#getDisplayInterval": return try AirshipProxy.shared.inApp.getDisplayInterval() + case "inApp#resendEmbeddedEvent": + AirshipProxy.shared.inApp.resendLastEmbeddedEvent() + return nil + // Analytics case "analytics#trackScreen": try AirshipProxy.shared.analytics.trackScreen( @@ -457,6 +461,7 @@ public class SwiftAirshipPlugin: NSObject, FlutterPlugin { try AirshipProxy.shared.featureFlagManager.trackInteraction(flag: featureFlagProxy) return nil + default: return FlutterError( code:"UNAVAILABLE", @@ -542,13 +547,9 @@ extension FlutterMethodCall { } } -protocol AirshipEventStreamHandlerDelegate: AnyObject { - func handlerWasRemoved(_ handler: AirshipEventStreamHandler) -} class AirshipEventStreamHandler: NSObject, FlutterStreamHandler { private var eventSink: FlutterEventSink? - weak var delegate: AirshipEventStreamHandlerDelegate? func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { self.eventSink = events @@ -557,7 +558,6 @@ class AirshipEventStreamHandler: NSObject, FlutterStreamHandler { func onCancel(withArguments arguments: Any?) -> FlutterError? { self.eventSink = nil - delegate?.handlerWasRemoved(self) return nil } @@ -570,7 +570,7 @@ class AirshipEventStreamHandler: NSObject, FlutterStreamHandler { } } -class AirshipEventStream: NSObject, AirshipEventStreamHandlerDelegate { +class AirshipEventStream: NSObject { private let eventType: AirshipProxyEventType private let name: String private let lock = AirshipLock() @@ -587,7 +587,6 @@ class AirshipEventStream: NSObject, AirshipEventStreamHandlerDelegate { binaryMessenger: registrar.messenger() ) let handler = AirshipEventStreamHandler() - handler.delegate = self eventChannel.setStreamHandler(handler) lock.sync { @@ -617,10 +616,4 @@ class AirshipEventStream: NSObject, AirshipEventStreamHandlerDelegate { } return result } - - func handlerWasRemoved(_ handler: AirshipEventStreamHandler) { - lock.sync { - handlers.removeAll { $0 === handler } - } - } } diff --git a/lib/src/airship_in_app.dart b/lib/src/airship_in_app.dart index 8aee9117..f5ea13b5 100644 --- a/lib/src/airship_in_app.dart +++ b/lib/src/airship_in_app.dart @@ -4,64 +4,63 @@ import 'dart:async'; class AirshipInApp { final AirshipModule _module; - final Map> _isEmbeddedAvailableControllers = - {}; List _embeddedInfos = []; - late final StreamController + late final StreamController> _embeddedInfoUpdatedController; - late final StreamSubscription _subscription; - - List getEmbeddedInfos() => _embeddedInfos; + StreamSubscription? _eventSubscription; + bool _isFirstStream = true; AirshipInApp(this._module) { - _embeddedInfoUpdatedController = - StreamController.broadcast(); _setupEventStream(); - _subscription = onEmbeddedInfoUpdated.listen(_updateEmbeddedIds); + _setupController(); } - /// A flag to determine if an embedded id is available that is not live updated + List getEmbeddedInfos() => _embeddedInfos; + bool isEmbeddedAvailable({required String embeddedId}) => _embeddedInfos.any((info) => info.embeddedId == embeddedId); - /// A live updated stream to determine if an embedded id is available Stream isEmbeddedAvailableStream({required String embeddedId}) => - (_isEmbeddedAvailableControllers[embeddedId] ??= - StreamController.broadcast() - ..add(isEmbeddedAvailable(embeddedId: embeddedId))) - .stream; + onEmbeddedInfoUpdated.map((embeddedInfos) => + embeddedInfos.any((info) => info.embeddedId == embeddedId)); - Stream get onEmbeddedInfoUpdated => + Stream> get onEmbeddedInfoUpdated => _embeddedInfoUpdatedController.stream; + void _setupController() { + _embeddedInfoUpdatedController = + StreamController>.broadcast(onListen: () { + if (_isFirstStream) { + _isFirstStream = false; + _resendLastEmbeddedUpdate(); + } + }); + } + void _setupEventStream() { - _module + _eventSubscription = _module .getEventStream("com.airship.flutter/event/pending_embedded_updated") .listen((event) { try { - _embeddedInfoUpdatedController - .add(EmbeddedInfoUpdatedEvent.fromJson(event)); + final updatedEvent = EmbeddedInfoUpdatedEvent.fromJson(event); + _embeddedInfos = updatedEvent.embeddedInfos; + _embeddedInfoUpdatedController.add(_embeddedInfos); } catch (e) { print("Error parsing EmbeddedInfoUpdatedEvent: $e"); } }); } - void _updateEmbeddedIds(EmbeddedInfoUpdatedEvent event) { - /// Update the embedded infos list - _embeddedInfos = event.embeddedInfos; - - /// Update stream controllers for each embedded id so everything remains synced - _isEmbeddedAvailableControllers.forEach((id, controller) => - controller.add(isEmbeddedAvailable(embeddedId: id))); + Future _resendLastEmbeddedUpdate() async { + try { + await _module.channel.invokeMethod("inApp#resendEmbeddedEvent"); + } catch (e) { + print("Error resending embedded update: $e"); + } } void dispose() { - _subscription.cancel(); - - /// Remove and close all stream controllers for each embedded id - _isEmbeddedAvailableControllers.values - .forEach((controller) => controller.close()); + _eventSubscription?.cancel(); _embeddedInfoUpdatedController.close(); } } From 1c97fb53cc2aa451e6fad2313ac61b3c7cf75d4c Mon Sep 17 00:00:00 2001 From: crow Date: Fri, 30 Aug 2024 17:50:16 -0700 Subject: [PATCH 19/22] Use latest proxy ios --- ios/airship_flutter.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/airship_flutter.podspec b/ios/airship_flutter.podspec index 2f639401..b8895516 100644 --- a/ios/airship_flutter.podspec +++ b/ios/airship_flutter.podspec @@ -20,6 +20,6 @@ Airship flutter plugin. s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' s.ios.deployment_target = "14.0" - s.dependency "AirshipFrameworkProxy", "7.1.2" + s.dependency "AirshipFrameworkProxy", "7.3.0" end From 9597519e5213a0cb0b8ef5991440acdf328e87e6 Mon Sep 17 00:00:00 2001 From: crow Date: Mon, 2 Sep 2024 16:41:03 -0700 Subject: [PATCH 20/22] EmbeddedView -> AirshipEmbeddedView --- example/lib/screens/home.dart | 3 ++- lib/src/airship_embedded_view.dart | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/example/lib/screens/home.dart b/example/lib/screens/home.dart index d025dba9..8da2d8fc 100644 --- a/example/lib/screens/home.dart +++ b/example/lib/screens/home.dart @@ -35,7 +35,8 @@ class _HomeState extends State { alignment: Alignment.center, child: Wrap(children: [ Center( - child: EmbeddedView(embeddedId: "test", parentHeight: 200)), + child: AirshipEmbeddedView( + embeddedId: "test", parentHeight: 200)), Image.asset( 'assets/airship.png', ), diff --git a/lib/src/airship_embedded_view.dart b/lib/src/airship_embedded_view.dart index 55356c5a..bc18a01c 100644 --- a/lib/src/airship_embedded_view.dart +++ b/lib/src/airship_embedded_view.dart @@ -12,7 +12,7 @@ import 'package:airship_flutter/airship_flutter.dart'; /// Note: When an embedded view is set to display with its height set to `auto` /// the embedded view will size to its native aspect ratio. Any remaining space /// in the parent view will be apparent. -class EmbeddedView extends StatefulWidget { +class AirshipEmbeddedView extends StatefulWidget { /// The embedded view Id. final String embeddedId; @@ -27,18 +27,18 @@ class EmbeddedView extends StatefulWidget { /// A flag to use flutter hybrid composition method or not. Default to false. static bool hybridComposition = false; - EmbeddedView({ + AirshipEmbeddedView({ required this.embeddedId, this.parentWidth, this.parentHeight, }); @override - EmbeddedViewState createState() => EmbeddedViewState(); + AirshipEmbeddedViewState createState() => AirshipEmbeddedViewState(); } -class EmbeddedViewState extends State - with AutomaticKeepAliveClientMixin { +class AirshipEmbeddedViewState extends State + with AutomaticKeepAliveClientMixin { late MethodChannel _channel; late Stream _readyStream; bool? _isEmbeddedAvailable; @@ -119,7 +119,7 @@ class EmbeddedViewState extends State } Widget _getAndroidView() { - if (EmbeddedView.hybridComposition) { + if (AirshipEmbeddedView.hybridComposition) { return wrapWithLayoutBuilder(PlatformViewLink( viewType: 'com.airship.flutter/EmbeddedView', surfaceFactory: From 2ba77d24f25526c73a66e136f5d7688008629af3 Mon Sep 17 00:00:00 2001 From: crow Date: Tue, 3 Sep 2024 11:19:24 -0700 Subject: [PATCH 21/22] Add resendLastEmbeddedEvent to Android, update naming and use latest proxy --- .../src/main/kotlin/com/airship/flutter/AirshipPlugin.kt | 1 + ios/Classes/SwiftAirshipPlugin.swift | 2 +- lib/src/airship_in_app.dart | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt b/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt index 50c42a7f..9f03429e 100644 --- a/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt +++ b/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt @@ -179,6 +179,7 @@ class AirshipPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { "inApp#isPaused" -> result.resolveResult(call) { proxy.inApp.isPaused() } "inApp#setDisplayInterval" -> result.resolveResult(call) { proxy.inApp.setDisplayInterval(call.longArg()) } "inApp#getDisplayInterval" -> result.resolveResult(call) { proxy.inApp.getDisplayInterval() } + "inApp#resendLastEmbeddedEvent" -> result.resolveResult(call) { proxy.inApp.resendLastEmbeddedEvent() } // Analytics "analytics#trackScreen" -> result.resolveResult(call) { proxy.analytics.trackScreen(call.optStringArg()) } diff --git a/ios/Classes/SwiftAirshipPlugin.swift b/ios/Classes/SwiftAirshipPlugin.swift index 35802db2..16ccda7b 100644 --- a/ios/Classes/SwiftAirshipPlugin.swift +++ b/ios/Classes/SwiftAirshipPlugin.swift @@ -285,7 +285,7 @@ public class SwiftAirshipPlugin: NSObject, FlutterPlugin { case "inApp#getDisplayInterval": return try AirshipProxy.shared.inApp.getDisplayInterval() - case "inApp#resendEmbeddedEvent": + case "inApp#resendLastEmbeddedEvent": AirshipProxy.shared.inApp.resendLastEmbeddedEvent() return nil diff --git a/lib/src/airship_in_app.dart b/lib/src/airship_in_app.dart index f5ea13b5..01ac4533 100644 --- a/lib/src/airship_in_app.dart +++ b/lib/src/airship_in_app.dart @@ -32,7 +32,7 @@ class AirshipInApp { StreamController>.broadcast(onListen: () { if (_isFirstStream) { _isFirstStream = false; - _resendLastEmbeddedUpdate(); + _resendLastEmbeddedEvent(); } }); } @@ -51,9 +51,9 @@ class AirshipInApp { }); } - Future _resendLastEmbeddedUpdate() async { + Future _resendLastEmbeddedEvent() async { try { - await _module.channel.invokeMethod("inApp#resendEmbeddedEvent"); + await _module.channel.invokeMethod("inApp#resendLastEmbeddedEvent"); } catch (e) { print("Error resending embedded update: $e"); } From 9deed145b0c4ab2e8b2893b3f9f628d283345a3b Mon Sep 17 00:00:00 2001 From: crow Date: Tue, 3 Sep 2024 15:27:41 -0700 Subject: [PATCH 22/22] Add multiple stream handling to Android --- .../com/airship/flutter/AirshipPlugin.kt | 128 ++++++++++-------- 1 file changed, 75 insertions(+), 53 deletions(-) diff --git a/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt b/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt index 9f03429e..7e84ce19 100644 --- a/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt +++ b/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.content.Context import android.os.Build import com.urbanairship.actions.ActionResult +import com.urbanairship.android.framework.proxy.Event import com.urbanairship.android.framework.proxy.EventType import com.urbanairship.android.framework.proxy.events.EventEmitter import com.urbanairship.android.framework.proxy.proxies.AirshipProxy @@ -22,18 +23,16 @@ import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.PluginRegistry.Registrar import io.flutter.plugin.platform.PlatformViewRegistry import kotlinx.coroutines.* +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock class AirshipPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { private lateinit var channel: MethodChannel - private lateinit var context: Context - private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main) + SupervisorJob() - private var mainActivity: Activity? = null - - private lateinit var streams : Map + private lateinit var streams: Map companion object { @JvmStatic @@ -50,18 +49,18 @@ class AirshipPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { internal const val AIRSHIP_SHARED_PREFS = "com.urbanairship.flutter" internal val EVENT_NAME_MAP = mapOf( - EventType.BACKGROUND_NOTIFICATION_RESPONSE_RECEIVED to "com.airship.flutter/event/notification_response", - EventType.FOREGROUND_NOTIFICATION_RESPONSE_RECEIVED to "com.airship.flutter/event/notification_response", - EventType.CHANNEL_CREATED to "com.airship.flutter/event/channel_created", - EventType.DEEP_LINK_RECEIVED to "com.airship.flutter/event/deep_link_received", - EventType.DISPLAY_MESSAGE_CENTER to "com.airship.flutter/event/display_message_center", - EventType.DISPLAY_PREFERENCE_CENTER to "com.airship.flutter/event/display_preference_center", - EventType.MESSAGE_CENTER_UPDATED to "com.airship.flutter/event/message_center_updated", - EventType.PUSH_TOKEN_RECEIVED to "com.airship.flutter/event/push_token_received", - EventType.FOREGROUND_PUSH_RECEIVED to "com.airship.flutter/event/push_received", - EventType.BACKGROUND_PUSH_RECEIVED to "com.airship.flutter/event/background_push_received", - EventType.NOTIFICATION_STATUS_CHANGED to "com.airship.flutter/event/notification_status_changed", - EventType.PENDING_EMBEDDED_UPDATED to "com.airship.flutter/event/pending_embedded_updated" + EventType.BACKGROUND_NOTIFICATION_RESPONSE_RECEIVED to "com.airship.flutter/event/notification_response", + EventType.FOREGROUND_NOTIFICATION_RESPONSE_RECEIVED to "com.airship.flutter/event/notification_response", + EventType.CHANNEL_CREATED to "com.airship.flutter/event/channel_created", + EventType.DEEP_LINK_RECEIVED to "com.airship.flutter/event/deep_link_received", + EventType.DISPLAY_MESSAGE_CENTER to "com.airship.flutter/event/display_message_center", + EventType.DISPLAY_PREFERENCE_CENTER to "com.airship.flutter/event/display_preference_center", + EventType.MESSAGE_CENTER_UPDATED to "com.airship.flutter/event/message_center_updated", + EventType.PUSH_TOKEN_RECEIVED to "com.airship.flutter/event/push_token_received", + EventType.FOREGROUND_PUSH_RECEIVED to "com.airship.flutter/event/push_received", + EventType.BACKGROUND_PUSH_RECEIVED to "com.airship.flutter/event/background_push_received", + EventType.NOTIFICATION_STATUS_CHANGED to "com.airship.flutter/event/notification_status_changed", + EventType.PENDING_EMBEDDED_UPDATED to "com.airship.flutter/event/pending_embedded_updated" ) } @@ -99,13 +98,15 @@ class AirshipPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { val streamMap = mutableMapOf() streamGroups.forEach { entry -> val stream = AirshipEventStream(entry.value, entry.key, binaryMessenger) - entry.value.forEach {type -> + stream.register() /// Set up handlers for each stream + entry.value.forEach { type -> streamMap[type] = stream } } return streamMap } + override fun onMethodCall(call: MethodCall, result: Result) { val proxy = AirshipProxy.shared(context) @@ -187,8 +188,8 @@ class AirshipPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { "analytics#associateIdentifier" -> { val args = call.stringList() proxy.analytics.associateIdentifier( - args[0], - args.getOrNull(1) + args[0], + args.getOrNull(1) ) } @@ -217,8 +218,8 @@ class AirshipPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { "preferenceCenter#setAutoLaunch" -> result.resolveResult(call) { val args = call.jsonArgs().requireList() proxy.preferenceCenter.setAutoLaunchPreferenceCenter( - args.get(0).requireString(), - args.get(1).getBoolean(false) + args.get(0).requireString(), + args.get(1).getBoolean(false) ) } @@ -239,13 +240,13 @@ class AirshipPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { val args = call.jsonArgs().requireList().list proxy.actions.runAction(args[0].requireString(), args.getOrNull(1)) - .addResultCallback { actionResult -> - if (actionResult != null && actionResult.status == ActionResult.STATUS_COMPLETED) { - callback(actionResult.value, null) - } else { - callback(null, Exception("Action failed ${actionResult?.status}")) - } + .addResultCallback { actionResult -> + if (actionResult != null && actionResult.status == ActionResult.STATUS_COMPLETED) { + callback(actionResult.value, null) + } else { + callback(null, Exception("Action failed ${actionResult?.status}")) } + } } // Feature Flag @@ -308,40 +309,61 @@ class AirshipPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { override fun onDetachedFromActivity() { mainActivity = null } - - internal class AirshipEventStream( - private val eventTypes: List, - channelName: String, - binaryMessenger: BinaryMessenger - ) { - + class AirshipEventStreamHandler : EventChannel.StreamHandler { private var eventSink: EventChannel.EventSink? = null - init { - val eventChannel = EventChannel(binaryMessenger, channelName) - eventChannel.setStreamHandler(object:EventChannel.StreamHandler { - override fun onListen(arguments: Any?, eventSink: EventChannel.EventSink?) { - this@AirshipEventStream.eventSink = eventSink - processPendingEvents() - } - - override fun onCancel(p0: Any?) { - this@AirshipEventStream.eventSink = null - } - }) + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + this.eventSink = events } - fun processPendingEvents() { - val sink = eventSink ?: return + override fun onCancel(arguments: Any?) { + this.eventSink = null + } - EventEmitter.shared().processPending(eventTypes) { event -> - sink.success(event.body.unwrap()) + fun notify(event: Any): Boolean { + val sink = eventSink + return if (sink != null) { + sink.success(event) true + } else { + false } } } + class AirshipEventStream( + private val eventTypes: List, + private val name: String, + private val binaryMessenger: BinaryMessenger + ) { + private val handlers = mutableListOf() + private val lock = ReentrantLock() + private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) -} - + fun register() { + val eventChannel = EventChannel(binaryMessenger, name) + val handler = AirshipEventStreamHandler() + eventChannel.setStreamHandler(handler) + lock.withLock { + handlers.add(handler) + } + } + fun processPendingEvents() { + EventEmitter.shared().processPending(eventTypes) { event -> + val unwrappedEvent = event.body.unwrap() + if (unwrappedEvent != null) { + notify(unwrappedEvent) + } else { + /// If it can't be unwrapped we've processed all we can + true + } + } + } + private fun notify(event: Any): Boolean { + return lock.withLock { + handlers.any { it.notify(event) } + } + } + } +} \ No newline at end of file