#475 receiver declaration, media command handling
This commit is contained in:
parent
60dd4d5ab2
commit
1ab0760adc
14 changed files with 241 additions and 90 deletions
|
@ -205,6 +205,14 @@ This change eventually prevents building the app with Flutter v3.3.3.
|
||||||
android:resource="@xml/app_widget_info" />
|
android:resource="@xml/app_widget_info" />
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name="androidx.media.session.MediaButtonReceiver"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".AnalysisService"
|
android:name=".AnalysisService"
|
||||||
android:description="@string/analysis_service_description"
|
android:description="@string/analysis_service_description"
|
||||||
|
|
|
@ -70,6 +70,17 @@ open class MainActivity : FlutterActivity() {
|
||||||
|
|
||||||
val messenger = flutterEngine!!.dartExecutor
|
val messenger = flutterEngine!!.dartExecutor
|
||||||
|
|
||||||
|
// notification: platform -> 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
|
// dart -> platform -> dart
|
||||||
// - need Context
|
// - need Context
|
||||||
analysisHandler = AnalysisHandler(this, ::onAnalysisCompleted)
|
analysisHandler = AnalysisHandler(this, ::onAnalysisCompleted)
|
||||||
|
@ -83,7 +94,7 @@ open class MainActivity : FlutterActivity() {
|
||||||
MethodChannel(messenger, HomeWidgetHandler.CHANNEL).setMethodCallHandler(HomeWidgetHandler(this))
|
MethodChannel(messenger, HomeWidgetHandler.CHANNEL).setMethodCallHandler(HomeWidgetHandler(this))
|
||||||
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this))
|
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this))
|
||||||
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(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, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
|
||||||
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
|
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
|
||||||
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(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) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
setupShortcuts()
|
setupShortcuts()
|
||||||
}
|
}
|
||||||
|
@ -431,7 +432,7 @@ open class MainActivity : FlutterActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var errorStreamHandler: ErrorStreamHandler? = null
|
private var errorStreamHandler: ErrorStreamHandler? = null
|
||||||
|
|
||||||
suspend fun notifyError(error: String) {
|
suspend fun notifyError(error: String) {
|
||||||
Log.e(LOG_TAG, "notifyError error=$error")
|
Log.e(LOG_TAG, "notifyError error=$error")
|
||||||
|
|
|
@ -2,20 +2,16 @@ package deckers.thibault.aves.channel.calls
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.media.session.PlaybackState
|
import android.media.session.PlaybackState
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import android.util.Log
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import androidx.media.session.MediaButtonReceiver
|
import androidx.media.session.MediaButtonReceiver
|
||||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
|
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.FlutterUtils
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
|
||||||
import deckers.thibault.aves.utils.getParcelableExtraCompat
|
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||||
|
@ -24,10 +20,10 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
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 ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
private val sessions = HashMap<Uri, MediaSessionCompat>()
|
private var session: MediaSessionCompat? = null
|
||||||
|
|
||||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
|
@ -77,77 +73,41 @@ class MediaSessionHandler(private val context: Context) : MethodCallHandler {
|
||||||
.setActions(actions)
|
.setActions(actions)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
var session = sessions[uri]
|
FlutterUtils.runOnUiThread {
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
val mbrIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE)
|
val mbrIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE)
|
||||||
val mbrName = ComponentName(context, MediaButtonReceiver::class.java)
|
val mbrName = ComponentName(context, MediaButtonReceiver::class.java)
|
||||||
session = MediaSessionCompat(context, "aves-$uri", mbrName, mbrIntent)
|
session = MediaSessionCompat(context, "aves", mbrName, mbrIntent).apply {
|
||||||
sessions[uri] = session
|
setCallback(mediaCommandHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session!!.apply {
|
||||||
val metadata = MediaMetadataCompat.Builder()
|
val metadata = MediaMetadataCompat.Builder()
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
||||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMillis)
|
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMillis)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, uri.toString())
|
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, uri.toString())
|
||||||
.build()
|
.build()
|
||||||
session.setMetadata(metadata)
|
setMetadata(metadata)
|
||||||
|
setPlaybackState(playbackState)
|
||||||
val callback: MediaSessionCompat.Callback = object : MediaSessionCompat.Callback() {
|
if (!isActive) {
|
||||||
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
|
isActive = true
|
||||||
val keyEvent = mediaButtonEvent.getParcelableExtraCompat<KeyEvent?>(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 {
|
|
||||||
session.setCallback(callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
session.setPlaybackState(playbackState)
|
|
||||||
|
|
||||||
if (!session.isActive) {
|
|
||||||
session.isActive = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result.success(null)
|
result.success(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun release(call: MethodCall, result: MethodChannel.Result) {
|
private fun release(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
session?.let {
|
||||||
|
it.release()
|
||||||
if (uri == null) {
|
session = null
|
||||||
result.error("release-args", "missing arguments", null)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sessions[uri]?.release()
|
|
||||||
|
|
||||||
result.success(null)
|
result.success(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val LOG_TAG = LogUtils.createTag<MediaSessionHandler>()
|
|
||||||
const val CHANNEL = "deckers.thibault/aves/media_session"
|
const val CHANNEL = "deckers.thibault/aves/media_session"
|
||||||
|
|
||||||
const val STATE_STOPPED = "stopped"
|
const val STATE_STOPPED = "stopped"
|
||||||
|
|
|
@ -199,7 +199,9 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
||||||
activity.startActivityForResult(intent, MainActivity.PICK_COLLECTION_FILTERS_REQUEST)
|
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?) {
|
private fun success(result: Any?) {
|
||||||
handler.post {
|
handler.post {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package deckers.thibault.aves.channel.streams
|
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
|
||||||
import io.flutter.plugin.common.EventChannel.EventSink
|
import io.flutter.plugin.common.EventChannel.EventSink
|
||||||
|
|
||||||
|
@ -13,13 +15,16 @@ class AnalysisStreamHandler : EventChannel.StreamHandler {
|
||||||
this.eventSink = eventSink
|
this.eventSink = eventSink
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCancel(arguments: Any?) {}
|
override fun onCancel(arguments: Any?) {
|
||||||
|
Log.i(LOG_TAG, "onCancel arguments=$arguments")
|
||||||
|
}
|
||||||
|
|
||||||
fun notifyCompletion() {
|
fun notifyCompletion() {
|
||||||
eventSink?.success(true)
|
eventSink?.success(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val LOG_TAG = LogUtils.createTag<AnalysisStreamHandler>()
|
||||||
const val CHANNEL = "deckers.thibault/aves/analysis_events"
|
const val CHANNEL = "deckers.thibault/aves/analysis_events"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
package deckers.thibault.aves.channel.streams
|
package deckers.thibault.aves.channel.streams
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import deckers.thibault.aves.utils.FlutterUtils
|
import deckers.thibault.aves.utils.FlutterUtils
|
||||||
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import io.flutter.plugin.common.EventChannel
|
import io.flutter.plugin.common.EventChannel
|
||||||
import io.flutter.plugin.common.EventChannel.EventSink
|
import io.flutter.plugin.common.EventChannel.EventSink
|
||||||
|
|
||||||
|
@ -14,7 +16,9 @@ class ErrorStreamHandler : EventChannel.StreamHandler {
|
||||||
this.eventSink = eventSink
|
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) {
|
suspend fun notifyError(error: String) {
|
||||||
FlutterUtils.runOnUiThread {
|
FlutterUtils.runOnUiThread {
|
||||||
|
@ -23,6 +27,7 @@ class ErrorStreamHandler : EventChannel.StreamHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val LOG_TAG = LogUtils.createTag<ErrorStreamHandler>()
|
||||||
const val CHANNEL = "deckers.thibault/aves/error"
|
const val CHANNEL = "deckers.thibault/aves/error"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
package deckers.thibault.aves.channel.streams
|
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
|
||||||
import io.flutter.plugin.common.EventChannel.EventSink
|
import io.flutter.plugin.common.EventChannel.EventSink
|
||||||
|
|
||||||
|
@ -13,13 +15,16 @@ class IntentStreamHandler : EventChannel.StreamHandler {
|
||||||
this.eventSink = eventSink
|
this.eventSink = eventSink
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCancel(arguments: Any?) {}
|
override fun onCancel(arguments: Any?) {
|
||||||
|
Log.i(LOG_TAG, "onCancel arguments=$arguments")
|
||||||
|
}
|
||||||
|
|
||||||
fun notifyNewIntent(intentData: MutableMap<String, Any?>?) {
|
fun notifyNewIntent(intentData: MutableMap<String, Any?>?) {
|
||||||
eventSink?.success(intentData)
|
eventSink?.success(intentData)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val LOG_TAG = LogUtils.createTag<IntentStreamHandler>()
|
||||||
const val CHANNEL = "deckers.thibault/aves/new_intent_stream"
|
const val CHANNEL = "deckers.thibault/aves/new_intent_stream"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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<MediaCommandStreamHandler>()
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,7 +41,9 @@ class MediaStoreChangeStreamHandler(private val context: Context) : EventChannel
|
||||||
handler = Handler(Looper.getMainLooper())
|
handler = Handler(Looper.getMainLooper())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCancel(arguments: Any?) {}
|
override fun onCancel(arguments: Any?) {
|
||||||
|
Log.i(LOG_TAG, "onCancel arguments=$arguments")
|
||||||
|
}
|
||||||
|
|
||||||
fun dispose() {
|
fun dispose() {
|
||||||
context.contentResolver.unregisterContentObserver(contentObserver)
|
context.contentResolver.unregisterContentObserver(contentObserver)
|
||||||
|
|
|
@ -79,7 +79,9 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
|
||||||
handler = Handler(Looper.getMainLooper())
|
handler = Handler(Looper.getMainLooper())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCancel(arguments: Any?) {}
|
override fun onCancel(arguments: Any?) {
|
||||||
|
Log.i(LOG_TAG, "onCancel arguments=$arguments")
|
||||||
|
}
|
||||||
|
|
||||||
fun dispose() {
|
fun dispose() {
|
||||||
context.contentResolver.unregisterContentObserver(contentObserver)
|
context.contentResolver.unregisterContentObserver(contentObserver)
|
||||||
|
|
|
@ -1,18 +1,33 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:aves/services/common/optional_event_channel.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/widgets/viewer/video/controller.dart';
|
import 'package:aves/widgets/viewer/video/controller.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
abstract class MediaSessionService {
|
abstract class MediaSessionService {
|
||||||
|
Stream<MediaCommandEvent> get mediaCommands;
|
||||||
|
|
||||||
Future<void> update(AvesVideoController controller);
|
Future<void> update(AvesVideoController controller);
|
||||||
|
|
||||||
Future<void> release(String uri);
|
Future<void> release();
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlatformMediaSessionService implements MediaSessionService {
|
class PlatformMediaSessionService implements MediaSessionService {
|
||||||
static const _platformObject = MethodChannel('deckers.thibault/aves/media_session');
|
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<MediaCommandEvent> get mediaCommands => _streamController.stream.where((event) => event is MediaCommandEvent).cast<MediaCommandEvent>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> update(AvesVideoController controller) async {
|
Future<void> update(AvesVideoController controller) async {
|
||||||
final entry = controller.entry;
|
final entry = controller.entry;
|
||||||
|
@ -31,11 +46,9 @@ class PlatformMediaSessionService implements MediaSessionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> release(String uri) async {
|
Future<void> release() async {
|
||||||
try {
|
try {
|
||||||
await _platformObject.invokeMethod('release', <String, dynamic>{
|
await _platformObject.invokeMethod('release');
|
||||||
'uri': uri,
|
|
||||||
});
|
|
||||||
} on PlatformException catch (e, stack) {
|
} on PlatformException catch (e, stack) {
|
||||||
await reportService.recordError(e, stack);
|
await reportService.recordError(e, stack);
|
||||||
}
|
}
|
||||||
|
@ -54,4 +67,52 @@ class PlatformMediaSessionService implements MediaSessionService {
|
||||||
return 'stopped';
|
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<Object?> get props => [command];
|
||||||
|
|
||||||
|
const MediaCommandEvent(this.command);
|
||||||
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class MediaSeekCommandEvent extends MediaCommandEvent {
|
||||||
|
final int position;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [...super.props, position];
|
||||||
|
|
||||||
|
const MediaSeekCommandEvent(super.command, {required this.position});
|
||||||
}
|
}
|
||||||
|
|
|
@ -686,7 +686,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
||||||
if (settings.keepScreenOn == KeepScreenOn.viewerOnly) {
|
if (settings.keepScreenOn == KeepScreenOn.viewerOnly) {
|
||||||
await windowService.keepScreenOn(false);
|
await windowService.keepScreenOn(false);
|
||||||
}
|
}
|
||||||
|
await mediaSessionService.release();
|
||||||
await AvesApp.showSystemUI();
|
await AvesApp.showSystemUI();
|
||||||
AvesApp.setSystemUIStyle(context);
|
AvesApp.setSystemUIStyle(context);
|
||||||
if (!settings.useTvLayout) {
|
if (!settings.useTvLayout) {
|
||||||
|
|
|
@ -30,7 +30,6 @@ abstract class AvesVideoController {
|
||||||
_subscriptions
|
_subscriptions
|
||||||
..forEach((sub) => sub.cancel())
|
..forEach((sub) => sub.cancel())
|
||||||
..clear();
|
..clear();
|
||||||
await mediaSessionService.release(entry.uri);
|
|
||||||
entry.visualChangeNotifier.removeListener(onVisualChanged);
|
entry.visualChangeNotifier.removeListener(onVisualChanged);
|
||||||
await _savePlaybackState();
|
await _savePlaybackState();
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@ import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/entry_images.dart';
|
import 'package:aves/model/entry_images.dart';
|
||||||
import 'package:aves/model/settings/enums/accessibility_animations.dart';
|
import 'package:aves/model/settings/enums/accessibility_animations.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
import 'package:aves/services/common/services.dart';
|
||||||
|
import 'package:aves/services/media/media_session_service.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||||
|
@ -107,6 +109,7 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
|
||||||
_subscriptions.add(_magnifierController.stateStream.listen(_onViewStateChanged));
|
_subscriptions.add(_magnifierController.stateStream.listen(_onViewStateChanged));
|
||||||
_subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged));
|
_subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged));
|
||||||
if (entry.isVideo) {
|
if (entry.isVideo) {
|
||||||
|
_subscriptions.add(mediaSessionService.mediaCommands.listen(_onMediaCommand));
|
||||||
_videoCoverStreamListener = ImageStreamListener((image, _) => _videoCoverInfoNotifier.value = image);
|
_videoCoverStreamListener = ImageStreamListener((image, _) => _videoCoverInfoNotifier.value = image);
|
||||||
_videoCoverStream = videoCoverUriImage.resolve(ImageConfiguration.empty);
|
_videoCoverStream = videoCoverUriImage.resolve(ImageConfiguration.empty);
|
||||||
_videoCoverStream!.addListener(_videoCoverStreamListener);
|
_videoCoverStream!.addListener(_videoCoverStreamListener);
|
||||||
|
@ -430,6 +433,28 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
|
||||||
const ToggleOverlayNotification().dispatch(context);
|
const ToggleOverlayNotification().dispatch(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onMediaCommand(MediaCommandEvent event) {
|
||||||
|
final videoController = context.read<VideoConductor>().getController(entry);
|
||||||
|
if (videoController == null) return;
|
||||||
|
|
||||||
|
switch (event.command) {
|
||||||
|
case MediaCommand.play:
|
||||||
|
videoController.play();
|
||||||
|
break;
|
||||||
|
case MediaCommand.pause:
|
||||||
|
videoController.pause();
|
||||||
|
break;
|
||||||
|
case MediaCommand.stop:
|
||||||
|
videoController.pause();
|
||||||
|
break;
|
||||||
|
case MediaCommand.seek:
|
||||||
|
if (event is MediaSeekCommandEvent) {
|
||||||
|
videoController.seekTo(event.position);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _onViewStateChanged(MagnifierState v) {
|
void _onViewStateChanged(MagnifierState v) {
|
||||||
_viewStateNotifier.value = _viewStateNotifier.value.copyWith(
|
_viewStateNotifier.value = _viewStateNotifier.value.copyWith(
|
||||||
position: v.position,
|
position: v.position,
|
||||||
|
|
Loading…
Reference in a new issue