fix(android): reconnect Flutter texture after TV standby
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
package com.edde746.plezy
|
||||
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.TextureView
|
||||
|
||||
internal class DeferredSurfaceTextureListener(
|
||||
private val delegate: TextureView.SurfaceTextureListener,
|
||||
private val handler: Handler = Handler(Looper.getMainLooper()),
|
||||
private val onSurfaceAvailable: () -> Unit = {},
|
||||
private val isCurrentSurface: (SurfaceTexture) -> Boolean
|
||||
) : TextureView.SurfaceTextureListener {
|
||||
|
||||
private var pendingResize: Runnable? = null
|
||||
|
||||
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
|
||||
delegate.onSurfaceTextureAvailable(surface, width, height)
|
||||
onSurfaceAvailable()
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
|
||||
pendingResize?.let(handler::removeCallbacks)
|
||||
pendingResize = Runnable {
|
||||
pendingResize = null
|
||||
if (isCurrentSurface(surface)) {
|
||||
delegate.onSurfaceTextureSizeChanged(surface, width, height)
|
||||
}
|
||||
}
|
||||
handler.postDelayed(pendingResize!!, RESIZE_DELAY_MILLIS)
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
|
||||
delegate.onSurfaceTextureUpdated(surface)
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
|
||||
pendingResize?.let(handler::removeCallbacks)
|
||||
pendingResize = null
|
||||
return delegate.onSurfaceTextureDestroyed(surface)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val RESIZE_DELAY_MILLIS = 100L
|
||||
}
|
||||
}
|
||||
@@ -9,20 +9,16 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Process
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.util.Rational
|
||||
import android.view.InputDevice
|
||||
import android.view.KeyEvent
|
||||
import android.view.TextureView
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowManager
|
||||
@@ -93,6 +89,9 @@ class MainActivity : FlutterActivity() {
|
||||
private var nativeTextInputFocused = false
|
||||
private var pendingExternalPlayerResult: MethodChannel.Result? = null
|
||||
private var originalWindowBrightness: Float? = null
|
||||
private var flutterTextureView: FlutterTextureView? = null
|
||||
private var flutterSurfaceReconnectPending = false
|
||||
private var activityStarted = false
|
||||
|
||||
private inline fun logTextInputDiag(message: () -> String) {
|
||||
if (TEXT_INPUT_DIAGNOSTICS_ENABLED) {
|
||||
@@ -383,6 +382,9 @@ class MainActivity : FlutterActivity() {
|
||||
override fun onDestroy() {
|
||||
pendingExternalPlayerResult?.error("ACTIVITY_DESTROYED", "Activity was destroyed while external player was active", null)
|
||||
pendingExternalPlayerResult = null
|
||||
activityStarted = false
|
||||
flutterSurfaceReconnectPending = false
|
||||
flutterTextureView = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -448,41 +450,49 @@ class MainActivity : FlutterActivity() {
|
||||
}
|
||||
|
||||
override fun onFlutterTextureViewCreated(flutterTextureView: FlutterTextureView) {
|
||||
this.flutterTextureView = flutterTextureView
|
||||
val original = flutterTextureView.surfaceTextureListener ?: return
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
var pendingResize: Runnable? = null
|
||||
var lastWidth = 0
|
||||
var lastHeight = 0
|
||||
flutterTextureView.surfaceTextureListener =
|
||||
DeferredSurfaceTextureListener(
|
||||
delegate = original,
|
||||
onSurfaceAvailable = ::tryReconnectFlutterSurface
|
||||
) { surface ->
|
||||
flutterTextureView.isAvailable && flutterTextureView.surfaceTexture === surface
|
||||
}
|
||||
}
|
||||
|
||||
flutterTextureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
|
||||
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
|
||||
original.onSurfaceTextureAvailable(surface, width, height)
|
||||
override fun onStop() {
|
||||
activityStarted = false
|
||||
if (isAndroidTvDevice()) flutterSurfaceReconnectPending = true
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
activityStarted = true
|
||||
tryReconnectFlutterSurface()
|
||||
}
|
||||
|
||||
private fun tryReconnectFlutterSurface() {
|
||||
if (!activityStarted || !flutterSurfaceReconnectPending) return
|
||||
val textureView = flutterTextureView ?: return
|
||||
textureView.post {
|
||||
if (!activityStarted ||
|
||||
!flutterSurfaceReconnectPending ||
|
||||
textureView !== flutterTextureView ||
|
||||
!textureView.isAttachedToWindow ||
|
||||
!textureView.isAvailable
|
||||
) {
|
||||
return@post
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
|
||||
if (width == lastWidth && height == lastHeight) return
|
||||
lastWidth = width
|
||||
lastHeight = height
|
||||
pendingResize?.let { handler.removeCallbacks(it) }
|
||||
pendingResize = Runnable {
|
||||
if (flutterTextureView.isAvailable) {
|
||||
original.onSurfaceTextureSizeChanged(surface, width, height)
|
||||
}
|
||||
}
|
||||
handler.postDelayed(pendingResize!!, 100)
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
|
||||
original.onSurfaceTextureUpdated(surface)
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
|
||||
pendingResize?.let { handler.removeCallbacks(it) }
|
||||
pendingResize = null
|
||||
lastWidth = 0
|
||||
lastHeight = 0
|
||||
return original.onSurfaceTextureDestroyed(surface)
|
||||
}
|
||||
// Some TV firmware retains an available TextureView while invalidating
|
||||
// its compositor surface during standby. Force Flutter's supported
|
||||
// surface-swap path so rendering resumes without restarting the engine.
|
||||
Log.i(TAG, "Reconnecting Flutter texture surface after Android TV standby")
|
||||
textureView.pause()
|
||||
textureView.resume()
|
||||
flutterSurfaceReconnectPending = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.edde746.plezy
|
||||
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.os.Looper
|
||||
import android.view.TextureView
|
||||
import java.time.Duration
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.LooperMode
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@LooperMode(LooperMode.Mode.PAUSED)
|
||||
class DeferredSurfaceTextureListenerTest {
|
||||
|
||||
private lateinit var surface: SurfaceTexture
|
||||
private lateinit var delegate: RecordingListener
|
||||
private lateinit var listener: DeferredSurfaceTextureListener
|
||||
private var currentSurface: SurfaceTexture? = null
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
surface = SurfaceTexture(0)
|
||||
currentSurface = surface
|
||||
delegate = RecordingListener()
|
||||
listener = DeferredSurfaceTextureListener(delegate) { candidate -> candidate === currentSurface }
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
surface.release()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun forwardsRepeatedSameSizeCallbacks() {
|
||||
listener.onSurfaceTextureSizeChanged(surface, 1920, 1080)
|
||||
idlePastResizeDelay()
|
||||
listener.onSurfaceTextureSizeChanged(surface, 1920, 1080)
|
||||
idlePastResizeDelay()
|
||||
|
||||
assertEquals(listOf(1920 to 1080, 1920 to 1080), delegate.sizes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun coalescesPendingCallbacksToLatestSize() {
|
||||
listener.onSurfaceTextureSizeChanged(surface, 1280, 720)
|
||||
listener.onSurfaceTextureSizeChanged(surface, 1920, 1080)
|
||||
idlePastResizeDelay()
|
||||
|
||||
assertEquals(listOf(1920 to 1080), delegate.sizes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ignoresCallbackForObsoleteSurface() {
|
||||
val replacement = SurfaceTexture(0)
|
||||
try {
|
||||
listener.onSurfaceTextureSizeChanged(surface, 1920, 1080)
|
||||
currentSurface = replacement
|
||||
idlePastResizeDelay()
|
||||
|
||||
assertEquals(emptyList<Pair<Int, Int>>(), delegate.sizes)
|
||||
} finally {
|
||||
replacement.release()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun destroyingSurfaceCancelsPendingCallback() {
|
||||
listener.onSurfaceTextureSizeChanged(surface, 1920, 1080)
|
||||
listener.onSurfaceTextureDestroyed(surface)
|
||||
idlePastResizeDelay()
|
||||
|
||||
assertEquals(emptyList<Pair<Int, Int>>(), delegate.sizes)
|
||||
assertEquals(1, delegate.destroyedCount)
|
||||
}
|
||||
|
||||
private fun idlePastResizeDelay() {
|
||||
shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(101))
|
||||
}
|
||||
|
||||
private class RecordingListener : TextureView.SurfaceTextureListener {
|
||||
val sizes = mutableListOf<Pair<Int, Int>>()
|
||||
var destroyedCount = 0
|
||||
|
||||
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) = Unit
|
||||
|
||||
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
|
||||
sizes += width to height
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) = Unit
|
||||
|
||||
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
|
||||
destroyedCount++
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user