fix: android thread safety
- ExoPlayerPlugin: move all MPV JNI calls off main thread via runOnExecutor - WatchNextPlugin: run ContentProvider ops on IO executor with error handling - MpvPlayerCore: guard executor methods against dispose race (RejectedExecutionException) - MainActivity: fix PiP null engine NPE with safe flutterEngine?.let
This commit is contained in:
@@ -265,11 +265,11 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean,newConfig: Configuration) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||
MethodChannel( flutterEngine!!.dartExecutor.binaryMessenger, PIP_CHANNEL ).invokeMethod( "onPipChanged" , isInPictureInPictureMode)
|
||||
|
||||
// Notify ExoPlayer plugin to resize video surface for PiP
|
||||
flutterEngine?.plugins?.get(ExoPlayerPlugin::class.java)?.let { plugin ->
|
||||
(plugin as? ExoPlayerPlugin)?.onPipModeChanged(isInPictureInPictureMode)
|
||||
flutterEngine?.let { engine ->
|
||||
MethodChannel(engine.dartExecutor.binaryMessenger, PIP_CHANNEL).invokeMethod("onPipChanged", isInPictureInPictureMode)
|
||||
engine.plugins.get(ExoPlayerPlugin::class.java)?.let { plugin ->
|
||||
(plugin as? ExoPlayerPlugin)?.onPipModeChanged(isInPictureInPictureMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -224,7 +224,9 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
val optionsStr = options.joinToString(",")
|
||||
// Convert content:// URIs to fdclose:// for MPV (SAF SD card downloads)
|
||||
val mpvUri = openContentFd(uri)?.let { "fdclose://$it" } ?: uri
|
||||
mpvCore?.command(arrayOf("loadfile", mpvUri, "replace", "-1", optionsStr))
|
||||
mpvCore?.runOnExecutor {
|
||||
mpvCore?.command(arrayOf("loadfile", mpvUri, "replace", "-1", optionsStr))
|
||||
}
|
||||
} else {
|
||||
playerCore?.open(uri, headers, startPositionMs, autoPlay, isLive)
|
||||
}
|
||||
@@ -235,7 +237,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
private fun handlePlay(result: MethodChannel.Result) {
|
||||
activity?.runOnUiThread {
|
||||
if (usingMpvFallback) {
|
||||
mpvCore?.setProperty("pause", "no")
|
||||
mpvCore?.runOnExecutor { mpvCore?.setProperty("pause", "no") }
|
||||
} else {
|
||||
playerCore?.play()
|
||||
}
|
||||
@@ -246,7 +248,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
private fun handlePause(result: MethodChannel.Result) {
|
||||
activity?.runOnUiThread {
|
||||
if (usingMpvFallback) {
|
||||
mpvCore?.setProperty("pause", "yes")
|
||||
mpvCore?.runOnExecutor { mpvCore?.setProperty("pause", "yes") }
|
||||
} else {
|
||||
playerCore?.pause()
|
||||
}
|
||||
@@ -257,8 +259,10 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
private fun handleStop(result: MethodChannel.Result) {
|
||||
activity?.runOnUiThread {
|
||||
if (usingMpvFallback) {
|
||||
mpvCore?.command(arrayOf("stop"))
|
||||
mpvCore?.setVisible(false)
|
||||
mpvCore?.runOnExecutor {
|
||||
mpvCore?.command(arrayOf("stop"))
|
||||
mpvCore?.setVisible(false)
|
||||
}
|
||||
} else {
|
||||
playerCore?.stop()
|
||||
}
|
||||
@@ -277,7 +281,9 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
activity?.runOnUiThread {
|
||||
if (usingMpvFallback) {
|
||||
val positionSeconds = positionMs / 1000.0
|
||||
mpvCore?.command(arrayOf("seek", positionSeconds.toString(), "absolute"))
|
||||
mpvCore?.runOnExecutor {
|
||||
mpvCore?.command(arrayOf("seek", positionSeconds.toString(), "absolute"))
|
||||
}
|
||||
} else {
|
||||
playerCore?.seekTo(positionMs)
|
||||
}
|
||||
@@ -295,7 +301,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
|
||||
activity?.runOnUiThread {
|
||||
if (usingMpvFallback) {
|
||||
mpvCore?.setProperty("volume", volume.toString())
|
||||
mpvCore?.runOnExecutor { mpvCore?.setProperty("volume", volume.toString()) }
|
||||
} else {
|
||||
playerCore?.setVolume(volume / 100f) // Convert 0-100 to 0-1
|
||||
}
|
||||
@@ -313,7 +319,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
|
||||
activity?.runOnUiThread {
|
||||
if (usingMpvFallback) {
|
||||
mpvCore?.setProperty("speed", rate.toString())
|
||||
mpvCore?.runOnExecutor { mpvCore?.setProperty("speed", rate.toString()) }
|
||||
} else {
|
||||
playerCore?.setPlaybackSpeed(rate)
|
||||
}
|
||||
@@ -333,7 +339,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
if (usingMpvFallback) {
|
||||
// MPV uses numeric track IDs - extract from string format
|
||||
val numericId = trackId.split("_").lastOrNull()?.toIntOrNull() ?: 1
|
||||
mpvCore?.setProperty("aid", numericId.toString())
|
||||
mpvCore?.runOnExecutor { mpvCore?.setProperty("aid", numericId.toString()) }
|
||||
} else {
|
||||
playerCore?.selectAudioTrack(trackId)
|
||||
}
|
||||
@@ -347,11 +353,13 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
// trackId can be null or "no" to disable subtitles
|
||||
activity?.runOnUiThread {
|
||||
if (usingMpvFallback) {
|
||||
if (trackId == null || trackId == "no") {
|
||||
mpvCore?.setProperty("sid", "no")
|
||||
} else {
|
||||
val numericId = trackId.split("_").lastOrNull()?.toIntOrNull() ?: 1
|
||||
mpvCore?.setProperty("sid", numericId.toString())
|
||||
mpvCore?.runOnExecutor {
|
||||
if (trackId == null || trackId == "no") {
|
||||
mpvCore?.setProperty("sid", "no")
|
||||
} else {
|
||||
val numericId = trackId.split("_").lastOrNull()?.toIntOrNull() ?: 1
|
||||
mpvCore?.setProperty("sid", numericId.toString())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
playerCore?.selectSubtitleTrack(trackId)
|
||||
@@ -375,7 +383,9 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
activity?.runOnUiThread {
|
||||
if (usingMpvFallback) {
|
||||
val selectFlag = if (select) "select" else "auto"
|
||||
mpvCore?.command(arrayOf("sub-add", uri, selectFlag, title ?: "External"))
|
||||
mpvCore?.runOnExecutor {
|
||||
mpvCore?.command(arrayOf("sub-add", uri, selectFlag, title ?: "External"))
|
||||
}
|
||||
} else {
|
||||
playerCore?.addSubtitleTrack(uri, title, language, mimeType, select)
|
||||
}
|
||||
@@ -484,7 +494,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
}
|
||||
|
||||
if (usingMpvFallback) {
|
||||
mpvCore?.setProperty(name, value)
|
||||
mpvCore?.runOnExecutor { mpvCore?.setProperty(name, value) }
|
||||
} else {
|
||||
// Store for later application if ExoPlayer falls back to MPV
|
||||
pendingMpvProperties.add(Pair(name, value))
|
||||
@@ -493,16 +503,17 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
}
|
||||
|
||||
private fun handleGetStats(result: MethodChannel.Result) {
|
||||
activity?.runOnUiThread {
|
||||
val stats = if (usingMpvFallback) {
|
||||
// For MPV fallback, query MPV properties directly
|
||||
getMpvStats()
|
||||
} else {
|
||||
if (usingMpvFallback) {
|
||||
mpvCore?.runOnExecutor {
|
||||
val stats = getMpvStats()
|
||||
activity?.runOnUiThread { result.success(stats) }
|
||||
} ?: result.success(mapOf("playerType" to "mpv"))
|
||||
} else {
|
||||
activity?.runOnUiThread {
|
||||
val coreStats = playerCore?.getStats() ?: emptyMap()
|
||||
coreStats + mapOf("playerType" to "exoplayer")
|
||||
}
|
||||
result.success(stats)
|
||||
} ?: result.success(mapOf("playerType" to "unknown"))
|
||||
result.success(coreStats + mapOf("playerType" to "exoplayer"))
|
||||
} ?: result.success(mapOf("playerType" to "unknown"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -652,71 +663,80 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
usingMpvFallback = true
|
||||
fallbackInProgress = false
|
||||
|
||||
// Configure basic MPV properties for Plex playback
|
||||
mpvCore?.setProperty("hwdec", "mediacodec,mediacodec-copy")
|
||||
mpvCore?.setProperty("vo", "gpu")
|
||||
mpvCore?.setProperty("ao", "audiotrack")
|
||||
|
||||
// Forward user's buffer config to MPV fallback
|
||||
configuredBufferSizeBytes?.let { bytes ->
|
||||
if (bytes > 0) {
|
||||
mpvCore?.setProperty("demuxer-max-bytes", bytes.toString())
|
||||
}
|
||||
}
|
||||
|
||||
// Apply any pending MPV properties from Dart
|
||||
for ((propName, propValue) in pendingMpvProperties) {
|
||||
mpvCore?.setProperty(propName, propValue)
|
||||
}
|
||||
// Snapshot pending properties on main thread before clearing
|
||||
val pendingProps = pendingMpvProperties.toList()
|
||||
pendingMpvProperties.clear()
|
||||
|
||||
// Setup property observers
|
||||
mpvCore?.observeProperty("time-pos", "double")
|
||||
mpvCore?.observeProperty("duration", "double")
|
||||
mpvCore?.observeProperty("seekable", "flag")
|
||||
mpvCore?.observeProperty("pause", "flag")
|
||||
mpvCore?.observeProperty("paused-for-cache", "flag")
|
||||
mpvCore?.observeProperty("demuxer-cache-time", "double")
|
||||
mpvCore?.observeProperty("eof-reached", "flag")
|
||||
mpvCore?.observeProperty("track-list", "string")
|
||||
mpvCore?.observeProperty("aid", "string")
|
||||
mpvCore?.observeProperty("sid", "string")
|
||||
mpvCore?.observeProperty("volume", "double")
|
||||
mpvCore?.observeProperty("speed", "double")
|
||||
|
||||
// Show the MPV surface
|
||||
mpvCore?.setVisible(true)
|
||||
|
||||
// Load media at the same position
|
||||
val startSeconds = positionMs / 1000.0
|
||||
val options = mutableListOf<String>()
|
||||
options.add("start=$startSeconds")
|
||||
headers?.forEach { (key, value) ->
|
||||
options.add("http-header-fields-append=$key: $value")
|
||||
}
|
||||
val optionsStr = options.joinToString(",")
|
||||
// Convert content:// URIs to fdclose:// for MPV (SAF SD card downloads)
|
||||
// Compute content FD on main thread (needs contentResolver)
|
||||
val mpvUri = openContentFd(uri)?.let { "fdclose://$it" } ?: uri
|
||||
mpvCore?.command(arrayOf("loadfile", mpvUri, "replace", "-1", optionsStr))
|
||||
|
||||
// On GPUs without compute shaders, MPV can't do dynamic peak detection
|
||||
// and spline tone-mapping produces dim/washed-out results with extreme
|
||||
// static HDR peak metadata. Use reinhard which handles this better.
|
||||
val peakDetection = mpvCore?.getProperty("hdr-compute-peak")
|
||||
if (peakDetection == "no") {
|
||||
Log.i(TAG, "No compute shaders — overriding tone-mapping to reinhard")
|
||||
mpvCore?.setProperty("tone-mapping", "reinhard")
|
||||
mpvCore?.setProperty("tone-mapping-param", "0.7")
|
||||
mpvCore?.setProperty("tone-mapping-mode", "luma")
|
||||
// Buffer size for closure
|
||||
val bufferSize = configuredBufferSizeBytes
|
||||
|
||||
mpvCore?.runOnExecutor {
|
||||
// Configure basic MPV properties for Plex playback
|
||||
mpvCore?.setProperty("hwdec", "mediacodec,mediacodec-copy")
|
||||
mpvCore?.setProperty("vo", "gpu")
|
||||
mpvCore?.setProperty("ao", "audiotrack")
|
||||
|
||||
// Forward user's buffer config to MPV fallback
|
||||
if (bufferSize != null && bufferSize > 0) {
|
||||
mpvCore?.setProperty("demuxer-max-bytes", bufferSize.toString())
|
||||
}
|
||||
|
||||
// Apply pending MPV properties from Dart
|
||||
for ((propName, propValue) in pendingProps) {
|
||||
mpvCore?.setProperty(propName, propValue)
|
||||
}
|
||||
|
||||
// Setup property observers
|
||||
mpvCore?.observeProperty("time-pos", "double")
|
||||
mpvCore?.observeProperty("duration", "double")
|
||||
mpvCore?.observeProperty("seekable", "flag")
|
||||
mpvCore?.observeProperty("pause", "flag")
|
||||
mpvCore?.observeProperty("paused-for-cache", "flag")
|
||||
mpvCore?.observeProperty("demuxer-cache-time", "double")
|
||||
mpvCore?.observeProperty("eof-reached", "flag")
|
||||
mpvCore?.observeProperty("track-list", "string")
|
||||
mpvCore?.observeProperty("aid", "string")
|
||||
mpvCore?.observeProperty("sid", "string")
|
||||
mpvCore?.observeProperty("volume", "double")
|
||||
mpvCore?.observeProperty("speed", "double")
|
||||
|
||||
// Show the MPV surface (internally posts to UI)
|
||||
mpvCore?.setVisible(true)
|
||||
|
||||
// Load media at the same position
|
||||
val startSeconds = positionMs / 1000.0
|
||||
val options = mutableListOf<String>()
|
||||
options.add("start=$startSeconds")
|
||||
headers?.forEach { (key, value) ->
|
||||
options.add("http-header-fields-append=$key: $value")
|
||||
}
|
||||
val optionsStr = options.joinToString(",")
|
||||
mpvCore?.command(arrayOf("loadfile", mpvUri, "replace", "-1", optionsStr))
|
||||
|
||||
// On GPUs without compute shaders, MPV can't do dynamic peak detection
|
||||
// and spline tone-mapping produces dim/washed-out results with extreme
|
||||
// static HDR peak metadata. Use reinhard which handles this better.
|
||||
val peakDetection = mpvCore?.getProperty("hdr-compute-peak")
|
||||
if (peakDetection == "no") {
|
||||
Log.i(TAG, "No compute shaders — overriding tone-mapping to reinhard")
|
||||
mpvCore?.setProperty("tone-mapping", "reinhard")
|
||||
mpvCore?.setProperty("tone-mapping-param", "0.7")
|
||||
mpvCore?.setProperty("tone-mapping-mode", "luma")
|
||||
}
|
||||
|
||||
// Request audio focus
|
||||
mpvCore?.requestAudioFocus()
|
||||
|
||||
// Emit backend-switched event on main thread
|
||||
activity?.runOnUiThread {
|
||||
onEvent("backend-switched", null)
|
||||
}
|
||||
|
||||
Log.i(TAG, "Successfully switched to MPV fallback")
|
||||
}
|
||||
|
||||
// Request audio focus
|
||||
mpvCore?.requestAudioFocus()
|
||||
|
||||
// Emit backend-switched event so Flutter can show notification
|
||||
onEvent("backend-switched", null)
|
||||
|
||||
Log.i(TAG, "Successfully switched to MPV fallback")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
fallbackInProgress = false
|
||||
|
||||
@@ -439,17 +439,22 @@ class MpvPlayerCore(private val activity: Activity) :
|
||||
* This prevents ANR when mpv_get_property blocks waiting for internal locks.
|
||||
*/
|
||||
fun getPropertyAsync(name: String, result: MethodChannel.Result) {
|
||||
if (!isInitialized) {
|
||||
if (!isInitialized || disposing) {
|
||||
result.success(null)
|
||||
return
|
||||
}
|
||||
commandExecutor.execute {
|
||||
try {
|
||||
val value = MPVLib.getPropertyString(name)
|
||||
activity.runOnUiThread { result.success(value) }
|
||||
} catch (e: Exception) {
|
||||
activity.runOnUiThread { result.success(null) }
|
||||
try {
|
||||
commandExecutor.execute {
|
||||
try {
|
||||
val value = MPVLib.getPropertyString(name)
|
||||
activity.runOnUiThread { result.success(value) }
|
||||
} catch (e: Exception) {
|
||||
activity.runOnUiThread { result.success(null) }
|
||||
}
|
||||
}
|
||||
} catch (e: java.util.concurrent.RejectedExecutionException) {
|
||||
Log.w(TAG, "getPropertyAsync rejected (executor shut down)")
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,18 +463,23 @@ class MpvPlayerCore(private val activity: Activity) :
|
||||
* This prevents ANR when mpv_set_property blocks waiting for internal locks.
|
||||
*/
|
||||
fun setPropertyAsync(name: String, value: String, result: MethodChannel.Result) {
|
||||
if (!isInitialized) {
|
||||
if (!isInitialized || disposing) {
|
||||
result.success(null)
|
||||
return
|
||||
}
|
||||
commandExecutor.execute {
|
||||
try {
|
||||
MPVLib.setPropertyString(name, value)
|
||||
activity.runOnUiThread { result.success(null) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Async setProperty failed: ${e.message}", e)
|
||||
activity.runOnUiThread { result.success(null) }
|
||||
try {
|
||||
commandExecutor.execute {
|
||||
try {
|
||||
MPVLib.setPropertyString(name, value)
|
||||
activity.runOnUiThread { result.success(null) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Async setProperty failed: ${e.message}", e)
|
||||
activity.runOnUiThread { result.success(null) }
|
||||
}
|
||||
}
|
||||
} catch (e: java.util.concurrent.RejectedExecutionException) {
|
||||
Log.w(TAG, "setPropertyAsync rejected (executor shut down)")
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,7 +488,7 @@ class MpvPlayerCore(private val activity: Activity) :
|
||||
* This prevents ANR when mpv_observe_property blocks waiting for internal locks.
|
||||
*/
|
||||
fun observePropertyAsync(name: String, format: String, result: MethodChannel.Result) {
|
||||
if (!isInitialized) {
|
||||
if (!isInitialized || disposing) {
|
||||
result.success(null)
|
||||
return
|
||||
}
|
||||
@@ -489,14 +499,19 @@ class MpvPlayerCore(private val activity: Activity) :
|
||||
"node" -> MPVLib.MPV_FORMAT_NODE
|
||||
else -> MPVLib.MPV_FORMAT_NONE
|
||||
}
|
||||
commandExecutor.execute {
|
||||
try {
|
||||
MPVLib.observeProperty(name, mpvFormat)
|
||||
activity.runOnUiThread { result.success(null) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Async observeProperty failed: ${e.message}", e)
|
||||
activity.runOnUiThread { result.success(null) }
|
||||
try {
|
||||
commandExecutor.execute {
|
||||
try {
|
||||
MPVLib.observeProperty(name, mpvFormat)
|
||||
activity.runOnUiThread { result.success(null) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Async observeProperty failed: ${e.message}", e)
|
||||
activity.runOnUiThread { result.success(null) }
|
||||
}
|
||||
}
|
||||
} catch (e: java.util.concurrent.RejectedExecutionException) {
|
||||
Log.w(TAG, "observePropertyAsync rejected (executor shut down)")
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,7 +520,12 @@ class MpvPlayerCore(private val activity: Activity) :
|
||||
* Used by the plugin for I/O operations that shouldn't block the main thread.
|
||||
*/
|
||||
fun runOnExecutor(block: () -> Unit) {
|
||||
commandExecutor.execute(block)
|
||||
if (disposing) return
|
||||
try {
|
||||
commandExecutor.execute(block)
|
||||
} catch (e: java.util.concurrent.RejectedExecutionException) {
|
||||
Log.w(TAG, "runOnExecutor rejected (executor shut down)")
|
||||
}
|
||||
}
|
||||
|
||||
fun command(args: Array<String>) {
|
||||
@@ -519,23 +539,28 @@ class MpvPlayerCore(private val activity: Activity) :
|
||||
* The result is called back on the UI thread when the command completes.
|
||||
*/
|
||||
fun commandAsync(args: Array<String>, result: MethodChannel.Result) {
|
||||
if (!isInitialized || args.isEmpty()) {
|
||||
if (!isInitialized || disposing || args.isEmpty()) {
|
||||
result.success(null)
|
||||
return
|
||||
}
|
||||
|
||||
commandExecutor.execute {
|
||||
try {
|
||||
MPVLib.command(args)
|
||||
activity.runOnUiThread {
|
||||
result.success(null)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Async command failed: ${e.message}", e)
|
||||
activity.runOnUiThread {
|
||||
result.error("COMMAND_FAILED", e.message, null)
|
||||
try {
|
||||
commandExecutor.execute {
|
||||
try {
|
||||
MPVLib.command(args)
|
||||
activity.runOnUiThread {
|
||||
result.success(null)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Async command failed: ${e.message}", e)
|
||||
activity.runOnUiThread {
|
||||
result.error("COMMAND_FAILED", e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: java.util.concurrent.RejectedExecutionException) {
|
||||
Log.w(TAG, "commandAsync rejected (executor shut down)")
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,14 @@ package com.edde746.plezy.watchnext
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.tvprovider.media.tv.TvContractCompat
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Flutter plugin for Android TV Watch Next integration.
|
||||
@@ -37,6 +40,8 @@ class WatchNextPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
private lateinit var methodChannel: MethodChannel
|
||||
private var applicationContext: Context? = null
|
||||
private var watchNextProvider: WatchNextProvider? = null
|
||||
private val ioExecutor by lazy { Executors.newSingleThreadExecutor() }
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
applicationContext = binding.applicationContext
|
||||
@@ -49,6 +54,7 @@ class WatchNextPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
methodChannel.setMethodCallHandler(null)
|
||||
applicationContext = null
|
||||
watchNextProvider = null
|
||||
ioExecutor.shutdown()
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
@@ -85,7 +91,7 @@ class WatchNextPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
val items = itemsData.mapNotNull { parseWatchNextItem(it) }
|
||||
result.success(provider.syncWatchNextPrograms(items))
|
||||
executeOnIo(result) { provider.syncWatchNextPrograms(items) }
|
||||
}
|
||||
|
||||
private fun handleClear(result: MethodChannel.Result) {
|
||||
@@ -94,7 +100,7 @@ class WatchNextPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
result.error("NOT_INITIALIZED", "WatchNextProvider not initialized", null)
|
||||
return
|
||||
}
|
||||
result.success(provider.clearAll())
|
||||
executeOnIo(result) { provider.clearAll() }
|
||||
}
|
||||
|
||||
private fun handleRemove(call: MethodCall, result: MethodChannel.Result) {
|
||||
@@ -109,7 +115,23 @@ class WatchNextPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
result.error("INVALID_ARGS", "Missing 'contentId' argument", null)
|
||||
return
|
||||
}
|
||||
result.success(provider.removeItem(contentId))
|
||||
executeOnIo(result) { provider.removeItem(contentId) }
|
||||
}
|
||||
|
||||
private fun executeOnIo(result: MethodChannel.Result, block: () -> Any?) {
|
||||
try {
|
||||
ioExecutor.execute {
|
||||
try {
|
||||
val value = block()
|
||||
mainHandler.post { result.success(value) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "IO operation failed: ${e.message}", e)
|
||||
mainHandler.post { result.error("IO_ERROR", e.message, null) }
|
||||
}
|
||||
}
|
||||
} catch (e: java.util.concurrent.RejectedExecutionException) {
|
||||
result.error("SHUTDOWN", "Plugin is shutting down", null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleGetInitialDeepLink(result: MethodChannel.Result) {
|
||||
|
||||
Reference in New Issue
Block a user