diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 9988d4941..64f920264 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -205,6 +205,14 @@ This change eventually prevents building the app with Flutter v3.3.3.
android:resource="@xml/app_widget_info" />
+
+
+
+
+
+
dart
+ analysisStreamHandler = AnalysisStreamHandler().apply {
+ EventChannel(messenger, AnalysisStreamHandler.CHANNEL).setStreamHandler(this)
+ }
+ errorStreamHandler = ErrorStreamHandler().apply {
+ EventChannel(messenger, ErrorStreamHandler.CHANNEL).setStreamHandler(this)
+ }
+ val mediaCommandHandler = MediaCommandStreamHandler().apply {
+ EventChannel(messenger, MediaCommandStreamHandler.CHANNEL).setStreamHandler(this)
+ }
+
// dart -> platform -> dart
// - need Context
analysisHandler = AnalysisHandler(this, ::onAnalysisCompleted)
@@ -83,7 +94,7 @@ open class MainActivity : FlutterActivity() {
MethodChannel(messenger, HomeWidgetHandler.CHANNEL).setMethodCallHandler(HomeWidgetHandler(this))
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this))
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this))
- MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(MediaSessionHandler(this))
+ MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(MediaSessionHandler(this, mediaCommandHandler))
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
@@ -128,16 +139,6 @@ open class MainActivity : FlutterActivity() {
}
}
- // notification: platform -> dart
- analysisStreamHandler = AnalysisStreamHandler().apply {
- EventChannel(messenger, AnalysisStreamHandler.CHANNEL).setStreamHandler(this)
- }
-
- // notification: platform -> dart
- errorStreamHandler = ErrorStreamHandler().apply {
- EventChannel(messenger, ErrorStreamHandler.CHANNEL).setStreamHandler(this)
- }
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
setupShortcuts()
}
@@ -431,7 +432,7 @@ open class MainActivity : FlutterActivity() {
}
}
- var errorStreamHandler: ErrorStreamHandler? = null
+ private var errorStreamHandler: ErrorStreamHandler? = null
suspend fun notifyError(error: String) {
Log.e(LOG_TAG, "notifyError error=$error")
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaSessionHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaSessionHandler.kt
index e96903269..327a9eded 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaSessionHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaSessionHandler.kt
@@ -2,20 +2,16 @@ package deckers.thibault.aves.channel.calls
import android.content.ComponentName
import android.content.Context
-import android.content.Intent
import android.media.session.PlaybackState
import android.net.Uri
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
-import android.util.Log
-import android.view.KeyEvent
import androidx.media.session.MediaButtonReceiver
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
+import deckers.thibault.aves.channel.streams.MediaCommandStreamHandler
import deckers.thibault.aves.utils.FlutterUtils
-import deckers.thibault.aves.utils.LogUtils
-import deckers.thibault.aves.utils.getParcelableExtraCompat
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
@@ -24,10 +20,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
-class MediaSessionHandler(private val context: Context) : MethodCallHandler {
+class MediaSessionHandler(private val context: Context, private val mediaCommandHandler: MediaCommandStreamHandler) : MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
- private val sessions = HashMap()
+ private var session: MediaSessionCompat? = null
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
@@ -77,77 +73,41 @@ class MediaSessionHandler(private val context: Context) : MethodCallHandler {
.setActions(actions)
.build()
- var session = sessions[uri]
- if (session == null) {
- val mbrIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE)
- val mbrName = ComponentName(context, MediaButtonReceiver::class.java)
- session = MediaSessionCompat(context, "aves-$uri", mbrName, mbrIntent)
- sessions[uri] = session
-
- val metadata = MediaMetadataCompat.Builder()
- .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
- .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
- .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMillis)
- .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, uri.toString())
- .build()
- session.setMetadata(metadata)
-
- val callback: MediaSessionCompat.Callback = object : MediaSessionCompat.Callback() {
- override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
- val keyEvent = mediaButtonEvent.getParcelableExtraCompat(Intent.EXTRA_KEY_EVENT) ?: return false
- Log.d(LOG_TAG, "TLAD onMediaButtonEvent keyEvent=$keyEvent")
- return super.onMediaButtonEvent(mediaButtonEvent)
- }
-
- override fun onPlay() {
- super.onPlay()
- Log.d(LOG_TAG, "TLAD onPlay uri=$uri")
- }
-
- override fun onPause() {
- super.onPause()
- Log.d(LOG_TAG, "TLAD onPause uri=$uri")
- }
-
- override fun onStop() {
- super.onStop()
- Log.d(LOG_TAG, "TLAD onStop uri=$uri")
- }
-
- override fun onSeekTo(pos: Long) {
- super.onSeekTo(pos)
- Log.d(LOG_TAG, "TLAD onSeekTo uri=$uri pos=$pos")
+ FlutterUtils.runOnUiThread {
+ if (session == null) {
+ val mbrIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE)
+ val mbrName = ComponentName(context, MediaButtonReceiver::class.java)
+ session = MediaSessionCompat(context, "aves", mbrName, mbrIntent).apply {
+ setCallback(mediaCommandHandler)
}
}
- FlutterUtils.runOnUiThread {
- session.setCallback(callback)
+ session!!.apply {
+ val metadata = MediaMetadataCompat.Builder()
+ .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
+ .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
+ .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMillis)
+ .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, uri.toString())
+ .build()
+ setMetadata(metadata)
+ setPlaybackState(playbackState)
+ if (!isActive) {
+ isActive = true
+ }
}
}
- session.setPlaybackState(playbackState)
-
- if (!session.isActive) {
- session.isActive = true
- }
-
result.success(null)
}
- private fun release(call: MethodCall, result: MethodChannel.Result) {
- val uri = call.argument("uri")?.let { Uri.parse(it) }
-
- if (uri == null) {
- result.error("release-args", "missing arguments", null)
- return
+ private fun release(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
+ session?.let {
+ it.release()
+ session = null
}
-
- sessions[uri]?.release()
-
result.success(null)
}
companion object {
- private val LOG_TAG = LogUtils.createTag()
const val CHANNEL = "deckers.thibault/aves/media_session"
const val STATE_STOPPED = "stopped"
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt
index cd30bf1a7..c2e9d364d 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt
@@ -199,7 +199,9 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
activity.startActivityForResult(intent, MainActivity.PICK_COLLECTION_FILTERS_REQUEST)
}
- override fun onCancel(arguments: Any?) {}
+ override fun onCancel(arguments: Any?) {
+ Log.i(LOG_TAG, "onCancel arguments=$arguments")
+ }
private fun success(result: Any?) {
handler.post {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/AnalysisStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/AnalysisStreamHandler.kt
index 2e199a30d..3cbaad3e2 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/AnalysisStreamHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/AnalysisStreamHandler.kt
@@ -1,5 +1,7 @@
package deckers.thibault.aves.channel.streams
+import android.util.Log
+import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
@@ -13,13 +15,16 @@ class AnalysisStreamHandler : EventChannel.StreamHandler {
this.eventSink = eventSink
}
- override fun onCancel(arguments: Any?) {}
+ override fun onCancel(arguments: Any?) {
+ Log.i(LOG_TAG, "onCancel arguments=$arguments")
+ }
fun notifyCompletion() {
eventSink?.success(true)
}
companion object {
+ private val LOG_TAG = LogUtils.createTag()
const val CHANNEL = "deckers.thibault/aves/analysis_events"
}
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ErrorStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ErrorStreamHandler.kt
index 1896fd0ef..a71e16b88 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ErrorStreamHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ErrorStreamHandler.kt
@@ -1,6 +1,8 @@
package deckers.thibault.aves.channel.streams
+import android.util.Log
import deckers.thibault.aves.utils.FlutterUtils
+import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
@@ -14,7 +16,9 @@ class ErrorStreamHandler : EventChannel.StreamHandler {
this.eventSink = eventSink
}
- override fun onCancel(arguments: Any?) {}
+ override fun onCancel(arguments: Any?) {
+ Log.i(LOG_TAG, "onCancel arguments=$arguments")
+ }
suspend fun notifyError(error: String) {
FlutterUtils.runOnUiThread {
@@ -23,6 +27,7 @@ class ErrorStreamHandler : EventChannel.StreamHandler {
}
companion object {
+ private val LOG_TAG = LogUtils.createTag()
const val CHANNEL = "deckers.thibault/aves/error"
}
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt
index e1734338c..a5626196b 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt
@@ -1,5 +1,7 @@
package deckers.thibault.aves.channel.streams
+import android.util.Log
+import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
@@ -13,13 +15,16 @@ class IntentStreamHandler : EventChannel.StreamHandler {
this.eventSink = eventSink
}
- override fun onCancel(arguments: Any?) {}
+ override fun onCancel(arguments: Any?) {
+ Log.i(LOG_TAG, "onCancel arguments=$arguments")
+ }
fun notifyNewIntent(intentData: MutableMap?) {
eventSink?.success(intentData)
}
companion object {
+ private val LOG_TAG = LogUtils.createTag()
const val CHANNEL = "deckers.thibault/aves/new_intent_stream"
}
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaCommandStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaCommandStreamHandler.kt
new file mode 100644
index 000000000..b81815622
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaCommandStreamHandler.kt
@@ -0,0 +1,76 @@
+package deckers.thibault.aves.channel.streams
+
+import android.os.Handler
+import android.os.Looper
+import android.support.v4.media.session.MediaSessionCompat
+import android.util.Log
+import deckers.thibault.aves.model.FieldMap
+import deckers.thibault.aves.utils.LogUtils
+import io.flutter.plugin.common.EventChannel
+import io.flutter.plugin.common.EventChannel.EventSink
+
+class MediaCommandStreamHandler : EventChannel.StreamHandler, MediaSessionCompat.Callback() {
+ // cannot use `lateinit` because we cannot guarantee
+ // its initialization in `onListen` at the right time
+ private var eventSink: EventSink? = null
+ private var handler: Handler? = null
+
+ override fun onListen(arguments: Any?, eventSink: EventSink) {
+ this.eventSink = eventSink
+ handler = Handler(Looper.getMainLooper())
+ }
+
+ override fun onCancel(arguments: Any?) {
+ Log.i(LOG_TAG, "onCancel arguments=$arguments")
+ }
+
+ private fun success(fields: FieldMap) {
+ handler?.post {
+ try {
+ eventSink?.success(fields)
+ } catch (e: Exception) {
+ Log.w(LOG_TAG, "failed to use event sink", e)
+ }
+ }
+ }
+
+ // media session callback
+
+ override fun onPlay() {
+ super.onPlay()
+ success(hashMapOf(KEY_COMMAND to COMMAND_PLAY))
+ }
+
+ override fun onPause() {
+ super.onPause()
+ success(hashMapOf(KEY_COMMAND to COMMAND_PAUSE))
+ }
+
+ override fun onStop() {
+ super.onStop()
+ success(hashMapOf(KEY_COMMAND to COMMAND_STOP))
+ }
+
+ override fun onSeekTo(pos: Long) {
+ super.onSeekTo(pos)
+ success(
+ hashMapOf(
+ KEY_COMMAND to COMMAND_SEEK,
+ KEY_POSITION to pos,
+ )
+ )
+ }
+
+ companion object {
+ private val LOG_TAG = LogUtils.createTag()
+ const val CHANNEL = "deckers.thibault/aves/media_command"
+
+ const val KEY_COMMAND = "command"
+ const val KEY_POSITION = "position"
+
+ const val COMMAND_PLAY = "play"
+ const val COMMAND_PAUSE = "pause"
+ const val COMMAND_STOP = "stop"
+ const val COMMAND_SEEK = "seek"
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreChangeStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreChangeStreamHandler.kt
index 7fdcddffc..05ede13b6 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreChangeStreamHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreChangeStreamHandler.kt
@@ -41,7 +41,9 @@ class MediaStoreChangeStreamHandler(private val context: Context) : EventChannel
handler = Handler(Looper.getMainLooper())
}
- override fun onCancel(arguments: Any?) {}
+ override fun onCancel(arguments: Any?) {
+ Log.i(LOG_TAG, "onCancel arguments=$arguments")
+ }
fun dispose() {
context.contentResolver.unregisterContentObserver(contentObserver)
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt
index c61c18ad7..77a753bfc 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt
@@ -79,7 +79,9 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
handler = Handler(Looper.getMainLooper())
}
- override fun onCancel(arguments: Any?) {}
+ override fun onCancel(arguments: Any?) {
+ Log.i(LOG_TAG, "onCancel arguments=$arguments")
+ }
fun dispose() {
context.contentResolver.unregisterContentObserver(contentObserver)
diff --git a/lib/services/media/media_session_service.dart b/lib/services/media/media_session_service.dart
index 4914e9237..c6cb94b8a 100644
--- a/lib/services/media/media_session_service.dart
+++ b/lib/services/media/media_session_service.dart
@@ -1,18 +1,33 @@
import 'dart:async';
+import 'package:aves/services/common/optional_event_channel.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/viewer/video/controller.dart';
+import 'package:equatable/equatable.dart';
+import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
abstract class MediaSessionService {
+ Stream get mediaCommands;
+
Future update(AvesVideoController controller);
- Future release(String uri);
+ Future release();
}
class PlatformMediaSessionService implements MediaSessionService {
static const _platformObject = MethodChannel('deckers.thibault/aves/media_session');
+ final EventChannel _mediaCommandChannel = const OptionalEventChannel('deckers.thibault/aves/media_command');
+ final StreamController _streamController = StreamController.broadcast();
+
+ PlatformMediaSessionService() {
+ _mediaCommandChannel.receiveBroadcastStream().listen((event) => _onMediaCommand(event as Map?));
+ }
+
+ @override
+ Stream get mediaCommands => _streamController.stream.where((event) => event is MediaCommandEvent).cast();
+
@override
Future update(AvesVideoController controller) async {
final entry = controller.entry;
@@ -31,11 +46,9 @@ class PlatformMediaSessionService implements MediaSessionService {
}
@override
- Future release(String uri) async {
+ Future release() async {
try {
- await _platformObject.invokeMethod('release', {
- 'uri': uri,
- });
+ await _platformObject.invokeMethod('release');
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
@@ -54,4 +67,52 @@ class PlatformMediaSessionService implements MediaSessionService {
return 'stopped';
}
}
+
+ void _onMediaCommand(Map? fields) {
+ if (fields == null) return;
+ final command = fields['command'] as String?;
+ MediaCommandEvent? event;
+ switch (command) {
+ case 'play':
+ event = const MediaCommandEvent(MediaCommand.play);
+ break;
+ case 'pause':
+ event = const MediaCommandEvent(MediaCommand.pause);
+ break;
+ case 'stop':
+ event = const MediaCommandEvent(MediaCommand.stop);
+ break;
+ case 'seek':
+ final position = fields['position'] as int?;
+ if (position != null) {
+ event = MediaSeekCommandEvent(MediaCommand.stop, position: position);
+ }
+ break;
+ }
+ if (event != null) {
+ _streamController.add(event);
+ }
+ }
+}
+
+enum MediaCommand { play, pause, stop, seek }
+
+@immutable
+class MediaCommandEvent extends Equatable {
+ final MediaCommand command;
+
+ @override
+ List