@@ -12,7 +12,10 @@ import android.graphics.RectF
1212import android.os.Bundle
1313import android.os.Handler
1414import android.os.Looper
15+ import android.view.PixelCopy
1516import android.view.SurfaceView
17+ import android.view.View
18+ import android.view.Window
1619import android.widget.FrameLayout
1720import android.widget.LinearLayout
1821import android.widget.LinearLayout.LayoutParams
@@ -36,12 +39,16 @@ import org.junit.runner.RunWith
3639import org.mockito.kotlin.any
3740import org.mockito.kotlin.doAnswer
3841import org.mockito.kotlin.mock
42+ import org.mockito.kotlin.never
43+ import org.mockito.kotlin.times
3944import org.mockito.kotlin.verify
4045import org.mockito.kotlin.whenever
4146import org.robolectric.Robolectric.buildActivity
4247import org.robolectric.Shadows.shadowOf
4348import org.robolectric.annotation.Config
4449import org.robolectric.annotation.GraphicsMode
50+ import org.robolectric.annotation.Implementation
51+ import org.robolectric.annotation.Implements
4552import org.robolectric.shadows.ShadowPixelCopy
4653
4754@Config(shadows = [ShadowPixelCopy ::class ], sdk = [30 ])
@@ -92,6 +99,7 @@ class PixelCopyStrategyTest {
9299 fun setup () {
93100 System .setProperty(" robolectric.areWindowsMarkedVisible" , " true" )
94101 System .setProperty(" robolectric.pixelCopyRenderMode" , " hardware" )
102+ DeferredWindowPixelCopyShadow .reset()
95103 }
96104
97105 @Test
@@ -132,6 +140,68 @@ class PixelCopyStrategyTest {
132140 if (failure.get() != null ) throw failure.get()
133141 }
134142
143+ @Test
144+ @Config(shadows = [DeferredWindowPixelCopyShadow ::class ])
145+ fun `capture skips the first unstable PixelCopy result` () {
146+ val activity = buildActivity(SimpleActivity ::class .java).setup()
147+ shadowOf(Looper .getMainLooper()).idle()
148+ val root = activity.get().findViewById<View >(android.R .id.content)
149+
150+ val strategy = fixture.getSut(executor = fixture.inlineExecutor())
151+ captureUnstableFrame(strategy, root)
152+
153+ assertFalse(strategy.lastCaptureSuccessful())
154+ verify(fixture.callback, never()).onScreenshotRecorded(any<Bitmap >())
155+ }
156+
157+ @Test
158+ @Config(shadows = [DeferredWindowPixelCopyShadow ::class ])
159+ fun `capture emits the second consecutive unstable PixelCopy result` () {
160+ val activity = buildActivity(SimpleActivity ::class .java).setup()
161+ shadowOf(Looper .getMainLooper()).idle()
162+ val root = activity.get().findViewById<View >(android.R .id.content)
163+
164+ val strategy = fixture.getSut(executor = fixture.inlineExecutor())
165+ captureUnstableFrame(strategy, root)
166+ captureUnstableFrame(strategy, root)
167+
168+ assertTrue(strategy.lastCaptureSuccessful())
169+ verify(fixture.callback).onScreenshotRecorded(any<Bitmap >())
170+ }
171+
172+ @Test
173+ @Config(shadows = [DeferredWindowPixelCopyShadow ::class ])
174+ fun `capture keeps emitting after entering continuous instability mode` () {
175+ val activity = buildActivity(SimpleActivity ::class .java).setup()
176+ shadowOf(Looper .getMainLooper()).idle()
177+ val root = activity.get().findViewById<View >(android.R .id.content)
178+
179+ val strategy = fixture.getSut(executor = fixture.inlineExecutor())
180+ captureUnstableFrame(strategy, root)
181+ captureUnstableFrame(strategy, root)
182+ captureUnstableFrame(strategy, root)
183+
184+ assertTrue(strategy.lastCaptureSuccessful())
185+ verify(fixture.callback, times(2 )).onScreenshotRecorded(any<Bitmap >())
186+ }
187+
188+ @Test
189+ @Config(shadows = [DeferredWindowPixelCopyShadow ::class ])
190+ fun `stable capture resets the unstable PixelCopy counter` () {
191+ val activity = buildActivity(SimpleActivity ::class .java).setup()
192+ shadowOf(Looper .getMainLooper()).idle()
193+ val root = activity.get().findViewById<View >(android.R .id.content)
194+
195+ val strategy = fixture.getSut(executor = fixture.inlineExecutor())
196+ captureUnstableFrame(strategy, root)
197+ captureUnstableFrame(strategy, root)
198+ captureStableFrame(strategy, root)
199+ captureUnstableFrame(strategy, root)
200+
201+ assertFalse(strategy.lastCaptureSuccessful())
202+ verify(fixture.callback, times(2 )).onScreenshotRecorded(any<Bitmap >())
203+ }
204+
135205 @Test
136206 fun `capture does not call markContentChanged when option is disabled` () {
137207 val activity = buildActivity(ActivityWithSurfaceView ::class .java).setup()
@@ -250,6 +320,50 @@ class PixelCopyStrategyTest {
250320 assertEquals(0 , dest.getPixel(4 , 4 ))
251321 assertEquals(0 , dest.getPixel(25 , 25 ))
252322 }
323+
324+ private fun captureUnstableFrame (strategy : PixelCopyStrategy , root : View ) {
325+ strategy.capture(root)
326+ strategy.onContentChanged()
327+ DeferredWindowPixelCopyShadow .flush()
328+ shadowOf(Looper .getMainLooper()).idle()
329+ }
330+
331+ private fun captureStableFrame (strategy : PixelCopyStrategy , root : View ) {
332+ strategy.capture(root)
333+ DeferredWindowPixelCopyShadow .flush()
334+ shadowOf(Looper .getMainLooper()).idle()
335+ }
336+ }
337+
338+ @Implements(PixelCopy ::class )
339+ class DeferredWindowPixelCopyShadow {
340+ companion object {
341+ private val pendingCallbacks = mutableListOf< () -> Unit > ()
342+
343+ fun reset () {
344+ pendingCallbacks.clear()
345+ }
346+
347+ fun flush () {
348+ val callbacks = pendingCallbacks.toList()
349+ pendingCallbacks.clear()
350+ callbacks.forEach { it.invoke() }
351+ }
352+
353+ @JvmStatic
354+ @Implementation
355+ @Suppress(" UNUSED_PARAMETER" )
356+ fun request (
357+ _source : Window ,
358+ _dest : Bitmap ,
359+ listener : PixelCopy .OnPixelCopyFinishedListener ,
360+ listenerThread : Handler ,
361+ ) {
362+ pendingCallbacks.add {
363+ listenerThread.post { listener.onPixelCopyFinished(PixelCopy .SUCCESS ) }
364+ }
365+ }
366+ }
253367}
254368
255369private class SimpleActivity : Activity () {
0 commit comments