Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 80 additions & 14 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Miscellaneous
*.class
*.lock
*.log
*.pyc
*.swp
Expand All @@ -18,40 +19,83 @@ migrate_working_dir/
*.iws
.idea/

# VS Code related
.vscode/

# Flutter/Dart/Pub related
/pubspec.lock
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
**/generated_plugin_registrant.dart
.packages
.packages.generated
.pub-preload-cache/
.pub/
/build/
/coverage/
flutter_*.png
linked_*.ds
unlinked.ds
unlinked_spec.ds

# Android related
**/android/local.properties
**/android/flutter_media_session_android.iml
**/android/**/gradle-wrapper.jar
.gradle/
**/android/.gradle
**/android/captures/
**/android/gradle/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
**/android/key.properties
*.jks
**/android/.cxx
**/android/.externalNativeBuild
**/android/flutter_media_session_android.iml

# iOS/macOS related
**/ios/Runner/GeneratedPluginRegistrant.[mh]
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/flutter_export_environment.sh
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/DerivedData/
# iOS/Xcode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/.sentry-native/
**/ios/Pods/
**/ios/.symlinks/
**/ios/Config/Generated.xcconfig
**/ios/Flutter/.last_build_id
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/ephemeral
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
**/ios/Runner/project.xcworkspace

# Windows related
# macOS
**/Flutter/ephemeral/
**/Pods/
**/macos/Flutter/GeneratedPluginRegistrant.swift
**/macos/Flutter/ephemeral
**/xcuserdata/

# Windows
**/windows/flutter/generated_plugin_registrant.cc
**/windows/flutter/generated_plugin_registrant.h
**/windows/flutter/generated_plugins.cmake
Expand All @@ -61,4 +105,26 @@ migrate_working_dir/
**/windows/cmake-build-*/
**/windows/x64/
**/windows/out/
**/windows/build/
**/windows/build/

# Linux
**/linux/flutter/generated_plugin_registrant.cc
**/linux/flutter/generated_plugin_registrant.h
**/linux/flutter/generated_plugins.cmake

# Coverage
coverage/
/coverage/

# Symbols
app.*.symbols

# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
!/dev/ci/**/Gemfile.lock

temp/
1 change: 1 addition & 0 deletions android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ android {

defaultConfig {
minSdk = 24
consumerProguardFiles("consumer-rules.pro")
}

testOptions {
Expand Down
4 changes: 4 additions & 0 deletions android/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-keep class androidx.media3.session.** { *; }
-keep interface androidx.media3.session.** { *; }
-keep class android.support.v4.media.session.** { *; }
-keep interface android.support.v4.media.session.** { *; }
3 changes: 1 addition & 2 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.wyrin.flutter_media_session">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,11 @@ class FlutterMediaSessionPlugin: FlutterPlugin, MethodCallHandler, ActivityAware
}
}
try {
ContextCompat.startForegroundService(context, intent)
try {
context.startService(intent)
} catch (e: Exception) {
android.util.Log.w("FlutterMediaSession", "startService failed, falling back to bindService only", e)
}
context.bindService(intent, serviceConnection!!, Context.BIND_AUTO_CREATE)
} catch (e: Exception) {
pendingActivateResult?.error("SERVICE_ERROR", "Failed to start or bind service: ${e.message}", null)
Expand Down Expand Up @@ -173,6 +177,9 @@ class FlutterMediaSessionPlugin: FlutterPlugin, MethodCallHandler, ActivityAware
FlutterMediaSessionService.instance?.applyBackgroundKeepAlive(enabled)
result.success(null)
}
"setSkipIntervals" -> {
result.success(null)
}
else -> result.notImplemented()
}
}
Expand Down Expand Up @@ -207,21 +214,96 @@ class FlutterMediaSessionPlugin: FlutterPlugin, MethodCallHandler, ActivityAware
return false
}

private var originalWindowCallback: android.view.Window.Callback? = null

private fun attachWindowCallback(act: Activity) {
val window = act.window ?: return
val currentCallback = window.callback
if (currentCallback is MediaWindowCallback) return
originalWindowCallback = currentCallback
window.callback = MediaWindowCallback(currentCallback)
}

private fun detachWindowCallback(act: Activity?) {
val window = act?.window ?: return
if (window.callback is MediaWindowCallback) {
window.callback = originalWindowCallback
}
originalWindowCallback = null
}

private inner class MediaWindowCallback(
private val localOriginalCallback: android.view.Window.Callback?
) : android.view.Window.Callback by (localOriginalCallback ?: DummyWindowCallback()) {
override fun dispatchKeyEvent(event: android.view.KeyEvent?): Boolean {
if (event != null) {
when (event.keyCode) {
android.view.KeyEvent.KEYCODE_MEDIA_PLAY,
android.view.KeyEvent.KEYCODE_MEDIA_PAUSE,
android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
android.view.KeyEvent.KEYCODE_HEADSETHOOK,
android.view.KeyEvent.KEYCODE_MEDIA_NEXT,
android.view.KeyEvent.KEYCODE_MEDIA_PREVIOUS,
android.view.KeyEvent.KEYCODE_MEDIA_STOP,
android.view.KeyEvent.KEYCODE_MEDIA_FAST_FORWARD,
android.view.KeyEvent.KEYCODE_MEDIA_REWIND -> {
val handled = FlutterMediaSessionService.instance?.handleMediaKeyEvent(event) ?: false
if (handled) {
return true
}
}
}
}
return localOriginalCallback?.dispatchKeyEvent(event) ?: false
}
}

private class DummyWindowCallback : android.view.Window.Callback {
override fun dispatchKeyEvent(event: android.view.KeyEvent?): Boolean = false
override fun dispatchKeyShortcutEvent(event: android.view.KeyEvent?): Boolean = false
override fun dispatchTouchEvent(event: android.view.MotionEvent?): Boolean = false
override fun dispatchTrackballEvent(event: android.view.MotionEvent?): Boolean = false
override fun dispatchGenericMotionEvent(event: android.view.MotionEvent?): Boolean = false
override fun dispatchPopulateAccessibilityEvent(event: android.view.accessibility.AccessibilityEvent?): Boolean = false
override fun onCreatePanelView(featureId: Int): android.view.View? = null
override fun onCreatePanelMenu(featureId: Int, menu: android.view.Menu): Boolean = false
override fun onPreparePanel(featureId: Int, view: android.view.View?, menu: android.view.Menu): Boolean = false
override fun onMenuOpened(featureId: Int, menu: android.view.Menu): Boolean = false
override fun onMenuItemSelected(featureId: Int, item: android.view.MenuItem): Boolean = false
override fun onWindowAttributesChanged(attrs: android.view.WindowManager.LayoutParams?) {}
override fun onContentChanged() {}
override fun onWindowFocusChanged(hasFocus: Boolean) {}
override fun onAttachedToWindow() {}
override fun onDetachedFromWindow() {}
override fun onPanelClosed(featureId: Int, menu: android.view.Menu) {}
override fun onSearchRequested(): Boolean = false
override fun onSearchRequested(searchEvent: android.view.SearchEvent?): Boolean = false
override fun onWindowStartingActionMode(callback: android.view.ActionMode.Callback?): android.view.ActionMode? = null
override fun onWindowStartingActionMode(callback: android.view.ActionMode.Callback?, type: Int): android.view.ActionMode? = null
override fun onActionModeStarted(mode: android.view.ActionMode?) {}
override fun onActionModeFinished(mode: android.view.ActionMode?) {}
}

override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
binding.activity.volumeControlStream = android.media.AudioManager.STREAM_MUSIC
binding.addRequestPermissionsResultListener(this)
attachWindowCallback(binding.activity)
}

override fun onDetachedFromActivityForConfigChanges() {
detachWindowCallback(activity)
activity = null
}

override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
binding.addRequestPermissionsResultListener(this)
attachWindowCallback(binding.activity)
}

override fun onDetachedFromActivity() {
detachWindowCallback(activity)
activity = null
}

Expand All @@ -242,6 +324,7 @@ class FlutterMediaSessionPlugin: FlutterPlugin, MethodCallHandler, ActivityAware
* @param args Optional arguments for the action (e.g., seek position).
*/
fun sendAction(action: String, args: Any? = null) {
android.util.Log.i("FlutterMediaSession", "sendAction: action=$action, args=$args, eventSink=${eventSink != null}")
android.os.Handler(android.os.Looper.getMainLooper()).post {
if (args != null) {
eventSink?.success(mapOf("action" to action, "args" to args))
Expand Down
Loading
Loading