From b4b8eb258a86c7a9eab0d97fea347c5bb2eebd84 Mon Sep 17 00:00:00 2001 From: gianlucaparadise Date: Tue, 12 Nov 2019 07:10:28 +0100 Subject: [PATCH] CustomMessage handling: send and receive; Code re-organization: CastDialogOpener, MethodNames; --- .../FlutterCastFrameworkPlugin.kt | 101 +++++++++++------- .../flutter_cast_framework/MethodNames.kt | 22 ++++ .../cast/CastDialogOpener.kt | 37 +++++++ .../cast/MessageCastingChannel.kt | 49 +++++++++ example/lib/main.dart | 53 ++++++++- lib/MethodNames.dart | 4 + lib/cast/CastContext.dart | 8 +- lib/cast/SessionManager.dart | 26 +++++ lib/flutter_cast_framework.dart | 9 ++ 9 files changed, 267 insertions(+), 42 deletions(-) create mode 100644 android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/MethodNames.kt create mode 100644 android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/cast/CastDialogOpener.kt create mode 100644 android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/cast/MessageCastingChannel.kt diff --git a/android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/FlutterCastFrameworkPlugin.kt b/android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/FlutterCastFrameworkPlugin.kt index 7e1cbd5..a92144c 100644 --- a/android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/FlutterCastFrameworkPlugin.kt +++ b/android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/FlutterCastFrameworkPlugin.kt @@ -7,6 +7,8 @@ import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ProcessLifecycleOwner import androidx.mediarouter.app.MediaRouteChooserDialog import androidx.mediarouter.app.MediaRouteControllerDialog +import com.gianlucaparadise.flutter_cast_framework.cast.CastDialogOpener +import com.gianlucaparadise.flutter_cast_framework.cast.MessageCastingChannel import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastSession import com.google.android.gms.cast.framework.SessionManager @@ -28,23 +30,6 @@ class FlutterCastFrameworkPlugin(private val registrar: Registrar, private val c } } - private object MethodNames { - const val onCastStateChanged = "CastContext.onCastStateChanged" - const val showCastDialog = "showCastDialog" - - // region SessionManager - const val onSessionStarting = "SessionManager.onSessionStarting" - const val onSessionStarted = "SessionManager.onSessionStarted" - const val onSessionStartFailed = "SessionManager.onSessionStartFailed" - const val onSessionEnding = "SessionManager.onSessionEnding" - const val onSessionEnded = "SessionManager.onSessionEnded" - const val onSessionResuming = "SessionManager.onSessionResuming" - const val onSessionResumed = "SessionManager.onSessionResumed" - const val onSessionResumeFailed = "SessionManager.onSessionResumeFailed" - const val onSessionSuspended = "SessionManager.onSessionSuspended" - // end-region - } - init { ProcessLifecycleOwner.get().lifecycle.addObserver(this) @@ -57,16 +42,32 @@ class FlutterCastFrameworkPlugin(private val registrar: Registrar, private val c private lateinit var mSessionManager: SessionManager private val mSessionManagerListener = CastSessionManagerListener() + private val mMessageCastingChannel = MessageCastingChannel(channel) + + private var mCastSession: CastSession? = null + set(value) { + Log.d(TAG, "Updating mCastSession - castSession changed: ${field != value}") + // if (field == value) return // Despite the instances are the same, I need to re-attach the listener to every new session instance + + val result = NamespaceResult(oldSession = field, newSession = value) + + field = value + + channel.invokeMethod(MethodNames.getSessionMessageNamespaces, null, result) + } + @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) fun onCreate() { Log.d(TAG, "App: ON_CREATE") mSessionManager = CastContext.getSharedInstance(registrar.activeContext()).sessionManager + mCastSession = mSessionManager.currentCastSession } @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun onResume() { Log.d(TAG, "App: ON_RESUME") mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java) + mCastSession = mSessionManager.currentCastSession } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) @@ -76,39 +77,53 @@ class FlutterCastFrameworkPlugin(private val registrar: Registrar, private val c mSessionManagerListener, CastSession::class.java ) + // I can't set this to null because I need the cast session to send commands from notification + // mCastSession = null } override fun onMethodCall(call: MethodCall, result: Result) { - when (call.method) { - MethodNames.showCastDialog -> showCastDialog() + val method = call.method + val arguments = call.arguments + + when (method) { + MethodNames.showCastDialog -> CastDialogOpener.showCastDialog(registrar) + MethodNames.sendMessage -> this.mMessageCastingChannel.sendMessage(mCastSession, arguments) else -> result.notImplemented() } } - private fun showCastDialog() { - val castContext = CastContext.getSharedInstance(registrar.activeContext()) - val castSession = castContext.sessionManager.currentCastSession + private inner class NamespaceResult(val oldSession: CastSession?, val newSession: CastSession?) : Result { + override fun notImplemented() { + Log.d(TAG, "Updating mCastSession - notImplemented") + } - val activity = this.registrar.activity() - val themeResId = activity.packageManager.getActivityInfo(activity.componentName, 0).themeResource + override fun error(p0: String?, p1: String?, p2: Any?) { + Log.d(TAG, "Updating mCastSession - error - $p0 $p1 $p2") + } - try { - if (castSession != null) { - // This dialog allows the user to control or disconnect from the currently selected route. - MediaRouteControllerDialog(registrar.activeContext(), themeResId) - .show() - } else { - // This dialog allows the user to choose a route that matches a given selector. - MediaRouteChooserDialog(registrar.activeContext(), themeResId).apply { - routeSelector = castContext.mergedSelector - show() + override fun success(args: Any?) { + Log.d(TAG, "Updating mCastSession - success - param: $args") + if (oldSession == null && newSession == null) return // nothing to do here + if (args == null) return // nothing to do here + + if (args !is ArrayList<*>) + throw IllegalArgumentException("${MethodNames.getSessionMessageNamespaces} method expects an ArrayList") + + if (!args.any()) return // nothing to do here + + if (args[0] !is String) + throw IllegalArgumentException("${MethodNames.getSessionMessageNamespaces} method expects an ArrayList") + + val namespaces = args as ArrayList + namespaces.forEach { + try { + oldSession?.removeMessageReceivedCallbacks(it) + newSession?.setMessageReceivedCallbacks(it, mMessageCastingChannel) + } + catch (e: java.lang.Exception) { + Log.e(TAG, "Updating mCastSession - Exception while creating channel", e) } } - } catch (ex: IllegalArgumentException) { - Log.d(TAG, "Exception while opening Dialog") - throw IllegalArgumentException("Error while opening MediaRouteDialog." + - " Did you use AppCompat theme on your activity?" + - " Check https://developers.google.com/cast/docs/android_sender/integrate#androidtheme", ex) } } @@ -123,11 +138,15 @@ class FlutterCastFrameworkPlugin(private val registrar: Registrar, private val c override fun onSessionStarting(session: CastSession?) { Log.d(TAG, "onSessionStarting") channel.invokeMethod(MethodNames.onSessionStarting, null) + + mCastSession = session } override fun onSessionResuming(session: CastSession?, p1: String?) { Log.d(TAG, "onSessionResuming") channel.invokeMethod(MethodNames.onSessionResuming, null) + + mCastSession = session } override fun onSessionEnding(session: CastSession?) { @@ -148,11 +167,15 @@ class FlutterCastFrameworkPlugin(private val registrar: Registrar, private val c override fun onSessionStarted(session: CastSession, sessionId: String) { Log.d(TAG, "onSessionStarted") channel.invokeMethod(MethodNames.onSessionStarted, null) + + mCastSession = session } override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) { Log.d(TAG, "onSessionResumed") channel.invokeMethod(MethodNames.onSessionResumed, null) + + mCastSession = session } override fun onSessionEnded(session: CastSession, error: Int) { diff --git a/android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/MethodNames.kt b/android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/MethodNames.kt new file mode 100644 index 0000000..333db8c --- /dev/null +++ b/android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/MethodNames.kt @@ -0,0 +1,22 @@ +package com.gianlucaparadise.flutter_cast_framework + +object MethodNames { + const val onCastStateChanged = "CastContext.onCastStateChanged" + const val showCastDialog = "showCastDialog" + + // region SessionManager + const val onSessionStarting = "SessionManager.onSessionStarting" + const val onSessionStarted = "SessionManager.onSessionStarted" + const val onSessionStartFailed = "SessionManager.onSessionStartFailed" + const val onSessionEnding = "SessionManager.onSessionEnding" + const val onSessionEnded = "SessionManager.onSessionEnded" + const val onSessionResuming = "SessionManager.onSessionResuming" + const val onSessionResumed = "SessionManager.onSessionResumed" + const val onSessionResumeFailed = "SessionManager.onSessionResumeFailed" + const val onSessionSuspended = "SessionManager.onSessionSuspended" + // end-region + + const val getSessionMessageNamespaces = "CastSession.getSessionMessageNamespaces" + const val onMessageReceived = "CastSession.onMessageReceived" + const val sendMessage = "CastSession.sendMessage" +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/cast/CastDialogOpener.kt b/android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/cast/CastDialogOpener.kt new file mode 100644 index 0000000..79e4632 --- /dev/null +++ b/android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/cast/CastDialogOpener.kt @@ -0,0 +1,37 @@ +package com.gianlucaparadise.flutter_cast_framework.cast + +import android.util.Log +import androidx.mediarouter.app.MediaRouteChooserDialog +import androidx.mediarouter.app.MediaRouteControllerDialog +import com.gianlucaparadise.flutter_cast_framework.FlutterCastFrameworkPlugin +import com.google.android.gms.cast.framework.CastContext +import io.flutter.plugin.common.PluginRegistry + +object CastDialogOpener { + fun showCastDialog(registrar: PluginRegistry.Registrar) { + val castContext = CastContext.getSharedInstance(registrar.activeContext()) + val castSession = castContext.sessionManager.currentCastSession + + val activity = registrar.activity() + val themeResId = activity.packageManager.getActivityInfo(activity.componentName, 0).themeResource + + try { + if (castSession != null) { + // This dialog allows the user to control or disconnect from the currently selected route. + MediaRouteControllerDialog(registrar.activeContext(), themeResId) + .show() + } else { + // This dialog allows the user to choose a route that matches a given selector. + MediaRouteChooserDialog(registrar.activeContext(), themeResId).apply { + routeSelector = castContext.mergedSelector + show() + } + } + } catch (ex: IllegalArgumentException) { + Log.d(FlutterCastFrameworkPlugin.TAG, "Exception while opening Dialog") + throw IllegalArgumentException("Error while opening MediaRouteDialog." + + " Did you use AppCompat theme on your activity?" + + " Check https://developers.google.com/cast/docs/android_sender/integrate#androidtheme", ex) + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/cast/MessageCastingChannel.kt b/android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/cast/MessageCastingChannel.kt new file mode 100644 index 0000000..309456c --- /dev/null +++ b/android/src/main/kotlin/com/gianlucaparadise/flutter_cast_framework/cast/MessageCastingChannel.kt @@ -0,0 +1,49 @@ +package com.gianlucaparadise.flutter_cast_framework.cast + +import android.util.Log +import com.gianlucaparadise.flutter_cast_framework.MethodNames +import com.google.android.gms.cast.Cast +import com.google.android.gms.cast.CastDevice +import com.google.android.gms.cast.framework.CastSession +import io.flutter.plugin.common.MethodChannel + +class MessageCastingChannel(private val channel: MethodChannel) : Cast.MessageReceivedCallback { + companion object { + const val TAG = "MessageCastingChannel" + } + + override fun onMessageReceived(castDevice: CastDevice?, namespace: String?, message: String?) { + Log.d(TAG, "Message received: $message:") + val argsMap: HashMap = hashMapOf( + "namespace" to namespace, + "message" to message + ) + + channel.invokeMethod(MethodNames.onMessageReceived, argsMap) + } + + fun sendMessage(castSession: CastSession?, arguments: Any) { + Log.d(TAG, "Send Message arguments: $arguments:") + val argsMap = arguments as HashMap + val namespace = argsMap["namespace"] + val message = argsMap["message"] + + sendMessage(castSession, namespace, message) + } + + private fun sendMessage(castSession: CastSession?, namespace: String?, message: String?) { + try { + if (castSession == null) { + Log.d(TAG, "No session") + return + } + + castSession.sendMessage(namespace, message) + + } catch (ex: Exception) { + Log.e(TAG, "Error while sending ${message}:") + Log.e(TAG, ex.toString()) + } + } + +} \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index fc7868d..5f5d81d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -14,12 +14,25 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { CastState _castState = CastState.idle; SessionState _sessionState = SessionState.idle; + String _message = ''; + + final textMessageController = TextEditingController(); @override void initState() { super.initState(); FlutterCastFramework.castContext.state.addListener(_onCastStateChanged); - FlutterCastFramework.castContext.sessionManager.state.addListener(_onSessionStateChanged); + FlutterCastFramework.castContext.sessionManager.state + .addListener(_onSessionStateChanged); + FlutterCastFramework.castContext.sessionManager.onMessageReceived = + _onMessageReceived; + } + + @override + void dispose() { + // Clean up the controller when the widget is disposed. + textMessageController.dispose(); + super.dispose(); } void _onCastStateChanged() { @@ -32,10 +45,24 @@ class _MyAppState extends State { void _onSessionStateChanged() { debugPrint("Session state changed from example"); setState(() { - _sessionState = FlutterCastFramework.castContext.sessionManager.state.value; + _sessionState = + FlutterCastFramework.castContext.sessionManager.state.value; }); } + void _onMessageReceived(String namespace, String message) { + debugPrint("Message received from example"); + setState(() { + _message = message; + }); + } + + void _onSendMessage() { + String message = this.textMessageController.text; + FlutterCastFramework.castContext.sessionManager + .sendMessage('urn:x-cast:cast-your-instructions', message); + } + @override Widget build(BuildContext context) { return MaterialApp( @@ -47,8 +74,30 @@ class _MyAppState extends State { child: Column( children: [ CastButton(), + Text( + 'States', + style: Theme.of(context).textTheme.title, + ), Text('Cast State: $_castState'), Text('Cast State: $_sessionState'), + Text( + 'Message', + style: Theme.of(context).textTheme.title, + ), + Row( + children: [ + Expanded( + child: TextField( + controller: textMessageController, + ), + ), + RaisedButton( + child: Text('Send'), + onPressed: _onSendMessage, + ) + ], + ), + Text('Received Message: $_message'), ], ), ), diff --git a/lib/MethodNames.dart b/lib/MethodNames.dart index b44230c..ab6bf19 100644 --- a/lib/MethodNames.dart +++ b/lib/MethodNames.dart @@ -11,4 +11,8 @@ class PlatformMethodNames { static const onSessionResumed = "SessionManager.onSessionResumed"; static const onSessionResumeFailed = "SessionManager.onSessionResumeFailed"; static const onSessionSuspended = "SessionManager.onSessionSuspended"; + + static const getSessionMessageNamespaces = "CastSession.getSessionMessageNamespaces"; + static const onMessageReceived = "CastSession.onMessageReceived"; + static const sendMessage = "CastSession.sendMessage"; } \ No newline at end of file diff --git a/lib/cast/CastContext.dart b/lib/cast/CastContext.dart index 29d0d2d..2ecfa42 100644 --- a/lib/cast/CastContext.dart +++ b/lib/cast/CastContext.dart @@ -18,7 +18,13 @@ class CastContext { state.value = CastState.values[castState]; } - SessionManager sessionManager = SessionManager(); + SessionManager _sessionManager; + SessionManager get sessionManager { + if (_sessionManager == null) { + _sessionManager = SessionManager(_channel); + } + return _sessionManager; + } } enum CastState { diff --git a/lib/cast/SessionManager.dart b/lib/cast/SessionManager.dart index 999a74e..f0cca88 100644 --- a/lib/cast/SessionManager.dart +++ b/lib/cast/SessionManager.dart @@ -1,7 +1,12 @@ import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_cast_framework/MethodNames.dart'; class SessionManager { + final MethodChannel _channel; + + SessionManager(this._channel); + final ValueNotifier state = ValueNotifier(SessionState.idle); void onSessionStateChanged(String method, dynamic arguments) { @@ -35,8 +40,29 @@ class SessionManager { break; } } + + MessageReceivedCallback onMessageReceived; + + void platformOnMessageReceived(dynamic arguments) { + if (onMessageReceived == null) return; + final namespace = arguments['namespace']; + final message = arguments['message']; + + onMessageReceived(namespace, message); + } + + void sendMessage(String namespace, String message) { + final argsMap = { + 'namespace': namespace, + 'message': '{"message":"$message"}' + }; + _channel.invokeMethod(PlatformMethodNames.sendMessage, argsMap); + } } +typedef MessageReceivedCallback = void Function( + String namespace, String message); + enum SessionState { idle, session_starting, diff --git a/lib/flutter_cast_framework.dart b/lib/flutter_cast_framework.dart index 0252bb2..2340cff 100644 --- a/lib/flutter_cast_framework.dart +++ b/lib/flutter_cast_framework.dart @@ -32,10 +32,19 @@ class FlutterCastFramework { castContext.sessionManager.onSessionStateChanged(method, arguments); break; + case PlatformMethodNames.getSessionMessageNamespaces: + return ["urn:x-cast:cast-your-instructions"]; + + case PlatformMethodNames.onMessageReceived: + castContext.sessionManager.platformOnMessageReceived(arguments); + break; + default: debugPrint("Method not handled: $method"); break; } + + return null; }); }