CustomMessage handling: send and receive;

Code re-organization: CastDialogOpener, MethodNames;
This commit is contained in:
gianlucaparadise 2019-11-12 07:10:28 +01:00
parent 9e10c120bc
commit b4b8eb258a
9 changed files with 267 additions and 42 deletions

View file

@ -7,6 +7,8 @@ import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.mediarouter.app.MediaRouteChooserDialog import androidx.mediarouter.app.MediaRouteChooserDialog
import androidx.mediarouter.app.MediaRouteControllerDialog 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.CastContext
import com.google.android.gms.cast.framework.CastSession import com.google.android.gms.cast.framework.CastSession
import com.google.android.gms.cast.framework.SessionManager 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 { init {
ProcessLifecycleOwner.get().lifecycle.addObserver(this) ProcessLifecycleOwner.get().lifecycle.addObserver(this)
@ -57,16 +42,32 @@ class FlutterCastFrameworkPlugin(private val registrar: Registrar, private val c
private lateinit var mSessionManager: SessionManager private lateinit var mSessionManager: SessionManager
private val mSessionManagerListener = CastSessionManagerListener() 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) @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() { fun onCreate() {
Log.d(TAG, "App: ON_CREATE") Log.d(TAG, "App: ON_CREATE")
mSessionManager = CastContext.getSharedInstance(registrar.activeContext()).sessionManager mSessionManager = CastContext.getSharedInstance(registrar.activeContext()).sessionManager
mCastSession = mSessionManager.currentCastSession
} }
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() { fun onResume() {
Log.d(TAG, "App: ON_RESUME") Log.d(TAG, "App: ON_RESUME")
mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java) mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
mCastSession = mSessionManager.currentCastSession
} }
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
@ -76,39 +77,53 @@ class FlutterCastFrameworkPlugin(private val registrar: Registrar, private val c
mSessionManagerListener, mSessionManagerListener,
CastSession::class.java 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) { override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) { val method = call.method
MethodNames.showCastDialog -> showCastDialog() val arguments = call.arguments
when (method) {
MethodNames.showCastDialog -> CastDialogOpener.showCastDialog(registrar)
MethodNames.sendMessage -> this.mMessageCastingChannel.sendMessage(mCastSession, arguments)
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
private fun showCastDialog() { private inner class NamespaceResult(val oldSession: CastSession?, val newSession: CastSession?) : Result {
val castContext = CastContext.getSharedInstance(registrar.activeContext()) override fun notImplemented() {
val castSession = castContext.sessionManager.currentCastSession Log.d(TAG, "Updating mCastSession - notImplemented")
}
val activity = this.registrar.activity() override fun error(p0: String?, p1: String?, p2: Any?) {
val themeResId = activity.packageManager.getActivityInfo(activity.componentName, 0).themeResource Log.d(TAG, "Updating mCastSession - error - $p0 $p1 $p2")
}
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<String>")
if (!args.any()) return // nothing to do here
if (args[0] !is String)
throw IllegalArgumentException("${MethodNames.getSessionMessageNamespaces} method expects an ArrayList<String>")
val namespaces = args as ArrayList<String>
namespaces.forEach {
try { try {
if (castSession != null) { oldSession?.removeMessageReceivedCallbacks(it)
// This dialog allows the user to control or disconnect from the currently selected route. newSession?.setMessageReceivedCallbacks(it, mMessageCastingChannel)
MediaRouteControllerDialog(registrar.activeContext(), themeResId) }
.show() catch (e: java.lang.Exception) {
} else { Log.e(TAG, "Updating mCastSession - Exception while creating channel", e)
// 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(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?) { override fun onSessionStarting(session: CastSession?) {
Log.d(TAG, "onSessionStarting") Log.d(TAG, "onSessionStarting")
channel.invokeMethod(MethodNames.onSessionStarting, null) channel.invokeMethod(MethodNames.onSessionStarting, null)
mCastSession = session
} }
override fun onSessionResuming(session: CastSession?, p1: String?) { override fun onSessionResuming(session: CastSession?, p1: String?) {
Log.d(TAG, "onSessionResuming") Log.d(TAG, "onSessionResuming")
channel.invokeMethod(MethodNames.onSessionResuming, null) channel.invokeMethod(MethodNames.onSessionResuming, null)
mCastSession = session
} }
override fun onSessionEnding(session: CastSession?) { 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) { override fun onSessionStarted(session: CastSession, sessionId: String) {
Log.d(TAG, "onSessionStarted") Log.d(TAG, "onSessionStarted")
channel.invokeMethod(MethodNames.onSessionStarted, null) channel.invokeMethod(MethodNames.onSessionStarted, null)
mCastSession = session
} }
override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) { override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) {
Log.d(TAG, "onSessionResumed") Log.d(TAG, "onSessionResumed")
channel.invokeMethod(MethodNames.onSessionResumed, null) channel.invokeMethod(MethodNames.onSessionResumed, null)
mCastSession = session
} }
override fun onSessionEnded(session: CastSession, error: Int) { override fun onSessionEnded(session: CastSession, error: Int) {

View file

@ -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"
}

View file

@ -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)
}
}
}

View file

@ -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<String, String?> = 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<String, String?>
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())
}
}
}

View file

@ -14,12 +14,25 @@ class MyApp extends StatefulWidget {
class _MyAppState extends State<MyApp> { class _MyAppState extends State<MyApp> {
CastState _castState = CastState.idle; CastState _castState = CastState.idle;
SessionState _sessionState = SessionState.idle; SessionState _sessionState = SessionState.idle;
String _message = '';
final textMessageController = TextEditingController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
FlutterCastFramework.castContext.state.addListener(_onCastStateChanged); 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() { void _onCastStateChanged() {
@ -32,10 +45,24 @@ class _MyAppState extends State<MyApp> {
void _onSessionStateChanged() { void _onSessionStateChanged() {
debugPrint("Session state changed from example"); debugPrint("Session state changed from example");
setState(() { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
@ -47,8 +74,30 @@ class _MyAppState extends State<MyApp> {
child: Column( child: Column(
children: [ children: [
CastButton(), CastButton(),
Text(
'States',
style: Theme.of(context).textTheme.title,
),
Text('Cast State: $_castState'), Text('Cast State: $_castState'),
Text('Cast State: $_sessionState'), 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'),
], ],
), ),
), ),

View file

@ -11,4 +11,8 @@ class PlatformMethodNames {
static const onSessionResumed = "SessionManager.onSessionResumed"; static const onSessionResumed = "SessionManager.onSessionResumed";
static const onSessionResumeFailed = "SessionManager.onSessionResumeFailed"; static const onSessionResumeFailed = "SessionManager.onSessionResumeFailed";
static const onSessionSuspended = "SessionManager.onSessionSuspended"; static const onSessionSuspended = "SessionManager.onSessionSuspended";
static const getSessionMessageNamespaces = "CastSession.getSessionMessageNamespaces";
static const onMessageReceived = "CastSession.onMessageReceived";
static const sendMessage = "CastSession.sendMessage";
} }

View file

@ -18,7 +18,13 @@ class CastContext {
state.value = CastState.values[castState]; state.value = CastState.values[castState];
} }
SessionManager sessionManager = SessionManager(); SessionManager _sessionManager;
SessionManager get sessionManager {
if (_sessionManager == null) {
_sessionManager = SessionManager(_channel);
}
return _sessionManager;
}
} }
enum CastState { enum CastState {

View file

@ -1,7 +1,12 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_cast_framework/MethodNames.dart'; import 'package:flutter_cast_framework/MethodNames.dart';
class SessionManager { class SessionManager {
final MethodChannel _channel;
SessionManager(this._channel);
final ValueNotifier<SessionState> state = ValueNotifier(SessionState.idle); final ValueNotifier<SessionState> state = ValueNotifier(SessionState.idle);
void onSessionStateChanged(String method, dynamic arguments) { void onSessionStateChanged(String method, dynamic arguments) {
@ -35,8 +40,29 @@ class SessionManager {
break; 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 { enum SessionState {
idle, idle,
session_starting, session_starting,

View file

@ -32,10 +32,19 @@ class FlutterCastFramework {
castContext.sessionManager.onSessionStateChanged(method, arguments); castContext.sessionManager.onSessionStateChanged(method, arguments);
break; break;
case PlatformMethodNames.getSessionMessageNamespaces:
return ["urn:x-cast:cast-your-instructions"];
case PlatformMethodNames.onMessageReceived:
castContext.sessionManager.platformOnMessageReceived(arguments);
break;
default: default:
debugPrint("Method not handled: $method"); debugPrint("Method not handled: $method");
break; break;
} }
return null;
}); });
} }