<lambda>null1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
2  * Any copyright is dedicated to the Public Domain.
3    http://creativecommons.org/publicdomain/zero/1.0/ */
4 
5 package org.mozilla.geckoview.test
6 
7 
8 import android.graphics.*
9 import androidx.test.filters.MediumTest
10 import androidx.test.ext.junit.runners.AndroidJUnit4
11 import android.view.Surface
12 import org.hamcrest.Matchers.*
13 import org.junit.Assert
14 import org.junit.Rule
15 import org.junit.Test
16 import org.junit.rules.ExpectedException
17 import org.junit.runner.RunWith
18 import org.mozilla.geckoview.GeckoResult
19 import org.mozilla.geckoview.GeckoResult.OnExceptionListener
20 import org.mozilla.geckoview.GeckoResult.fromException
21 import org.mozilla.geckoview.GeckoSession
22 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
23 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
24 import org.mozilla.geckoview.test.util.Callbacks
25 import kotlin.math.absoluteValue
26 import kotlin.math.max
27 import android.graphics.BitmapFactory
28 import android.graphics.Bitmap
29 import androidx.test.platform.app.InstrumentationRegistry
30 import org.junit.Assume.assumeThat
31 import java.lang.IllegalStateException
32 import java.lang.NullPointerException
33 
34 
35 private const val SCREEN_HEIGHT = 800
36 private const val SCREEN_WIDTH = 800
37 private const val BIG_SCREEN_HEIGHT = 999999
38 private const val BIG_SCREEN_WIDTH = 999999
39 
40 @RunWith(AndroidJUnit4::class)
41 @MediumTest
42 class ScreenshotTest : BaseSessionTest() {
43 
44     @get:Rule
45     val expectedEx: ExpectedException = ExpectedException.none()
46 
47     private fun getComparisonScreenshot(width: Int, height: Int): Bitmap {
48         val screenshotFile = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
49         val canvas = Canvas(screenshotFile)
50         val paint = Paint()
51         paint.shader = LinearGradient(0f, 0f, width.toFloat(), height.toFloat(), Color.RED, Color.WHITE, Shader.TileMode.MIRROR)
52         canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
53         return screenshotFile
54     }
55 
56     companion object {
57         /**
58          * Compares two Bitmaps and returns the largest color element difference (red, green or blue)
59          */
60         public fun imageElementDifference(b1: Bitmap, b2: Bitmap): Int {
61             return if (b1.width == b2.width && b1.height == b2.height) {
62                 val pixels1 = IntArray(b1.width * b1.height)
63                 val pixels2 = IntArray(b2.width * b2.height)
64                 b1.getPixels(pixels1, 0, b1.width, 0, 0, b1.width, b1.height)
65                 b2.getPixels(pixels2, 0, b2.width, 0, 0, b2.width, b2.height)
66                 var maxDiff = 0
67                 for (i in 0 until pixels1.size) {
68                     val redDiff = (Color.red(pixels1[i]) - Color.red(pixels2[i])).absoluteValue
69                     val greenDiff = (Color.green(pixels1[i]) - Color.green(pixels2[i])).absoluteValue
70                     val blueDiff = (Color.blue(pixels1[i]) - Color.blue(pixels2[i])).absoluteValue
71                     maxDiff = max(maxDiff, max(redDiff, max(greenDiff, blueDiff)))
72                 }
73                 maxDiff
74             } else {
75                 256
76             }
77         }
78     }
79 
80     private fun assertScreenshotResult(result: GeckoResult<Bitmap>, comparisonImage: Bitmap) {
81         sessionRule.waitForResult(result).let {
82             assertThat("Screenshot is not null",
83                     it, notNullValue())
84             assertThat("Widths are the same", comparisonImage.width, equalTo(it.width))
85             assertThat("Heights are the same", comparisonImage.height, equalTo(it.height))
86             assertThat("Byte counts are the same", comparisonImage.byteCount, equalTo(it.byteCount))
87             assertThat("Configs are the same", comparisonImage.config, equalTo(it.config))
88             assertThat("Images are almost identical",
89                     imageElementDifference(comparisonImage, it), lessThanOrEqualTo(1))
90         }
91     }
92 
93     @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
94     @Test
95     fun capturePixelsSucceeds() {
96         val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)
97 
98         sessionRule.session.loadTestPath(COLORS_HTML_PATH)
99         sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
100             @AssertCalled(count = 1)
101             override fun onFirstContentfulPaint(session: GeckoSession) {
102             }
103         })
104 
105         sessionRule.display?.let {
106             assertScreenshotResult(it.capturePixels(), screenshotFile)
107         }
108     }
109 
110     @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
111     @Test
112     fun capturePixelsCanBeCalledMultipleTimes() {
113         val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)
114 
115         sessionRule.session.loadTestPath(COLORS_HTML_PATH)
116         sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
117             @AssertCalled(count = 1)
118             override fun onFirstContentfulPaint(session: GeckoSession) {
119             }
120         })
121 
122         sessionRule.display?.let {
123             val call1 = it.capturePixels()
124             val call2 = it.capturePixels()
125             val call3 = it.capturePixels()
126             assertScreenshotResult(call1, screenshotFile)
127             assertScreenshotResult(call2, screenshotFile)
128             assertScreenshotResult(call3, screenshotFile)
129         }
130     }
131 
132     @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
133     @Test
134     fun capturePixelsCompletesCompositorPausedRestarted() {
135         sessionRule.display?.let {
136             it.surfaceDestroyed()
137             val result = it.capturePixels()
138             val texture = SurfaceTexture(0)
139             texture.setDefaultBufferSize(SCREEN_WIDTH, SCREEN_HEIGHT)
140             val surface = Surface(texture)
141             it.surfaceChanged(surface, SCREEN_WIDTH, SCREEN_HEIGHT)
142             sessionRule.waitForResult(result)
143         }
144     }
145 
146     // This tests tries to catch problems like Bug 1644561.
147     @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
148     @Test
149     fun capturePixelsStressTest() {
150         val screenshots = mutableListOf<GeckoResult<Bitmap>>()
151         sessionRule.display?.let {
152             for (i in 0..100) {
153                 screenshots.add(it.capturePixels())
154             }
155 
156             for (i in 0..50) {
157                 sessionRule.waitForResult(screenshots[i])
158             }
159 
160             it.surfaceDestroyed()
161             screenshots.add(it.capturePixels())
162             it.surfaceDestroyed()
163 
164             val texture = SurfaceTexture(0)
165             texture.setDefaultBufferSize(SCREEN_WIDTH, SCREEN_HEIGHT)
166             val surface = Surface(texture)
167             it.surfaceChanged(surface, SCREEN_WIDTH, SCREEN_HEIGHT)
168 
169             for (i in 0..100) {
170                 screenshots.add(it.capturePixels())
171             }
172 
173             for (i in 0..100) {
174                 it.surfaceDestroyed()
175                 screenshots.add(it.capturePixels())
176                 val newTexture = SurfaceTexture(0)
177                 newTexture.setDefaultBufferSize(SCREEN_WIDTH, SCREEN_HEIGHT)
178                 val newSurface = Surface(newTexture)
179                 it.surfaceChanged(newSurface, SCREEN_WIDTH, SCREEN_HEIGHT)
180             }
181 
182             try {
183                 for (result in screenshots) {
184                     sessionRule.waitForResult(result)
185                 }
186             } catch (ex: RuntimeException) {
187                 // Rejecting the screenshot is fine
188             }
189         }
190     }
191 
192     @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
193     @Test(expected = IllegalStateException::class)
194     fun capturePixelsFailsCompositorPaused() {
195         sessionRule.display?.let {
196             it.surfaceDestroyed()
197             val result = it.capturePixels()
198             it.surfaceDestroyed()
199 
200             sessionRule.waitForResult(result)
201         }
202     }
203 
204     @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
205     @Test
206     fun capturePixelsWhileSessionDeactivated() {
207         // TODO: Bug 1673955
208         assumeThat(sessionRule.env.isFission, equalTo(false))
209         val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)
210 
211         sessionRule.session.loadTestPath(COLORS_HTML_PATH)
212         sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
213             @AssertCalled(count = 1)
214             override fun onFirstContentfulPaint(session: GeckoSession) {
215             }
216         })
217 
218         sessionRule.session.setActive(false)
219 
220         // Deactivating the session should trigger a flush state change
221         sessionRule.waitUntilCalled(object : Callbacks.ProgressDelegate {
222             @AssertCalled(count = 1)
223             override fun onSessionStateChange(session: GeckoSession,
224                                               sessionState: GeckoSession.SessionState) {}
225         })
226 
227         sessionRule.display?.let {
228             assertScreenshotResult(it.capturePixels(), screenshotFile)
229         }
230     }
231 
232     @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
233     @Test
234     fun screenshotToBitmap() {
235         val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)
236 
237         sessionRule.session.loadTestPath(COLORS_HTML_PATH)
238         sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
239             @AssertCalled(count = 1)
240             override fun onFirstContentfulPaint(session: GeckoSession) {
241             }
242         })
243 
244         sessionRule.display?.let {
245             assertScreenshotResult(it.screenshot().capture(), screenshotFile)
246         }
247     }
248 
249     @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
250     @Test
251     fun screenshotScaledToSize() {
252         val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
253 
254         sessionRule.session.loadTestPath(COLORS_HTML_PATH)
255         sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
256             @AssertCalled(count = 1)
257             override fun onFirstContentfulPaint(session: GeckoSession) {
258             }
259         })
260 
261         sessionRule.display?.let {
262             assertScreenshotResult(it.screenshot().size(SCREEN_WIDTH/2, SCREEN_HEIGHT/2).capture(), screenshotFile)
263         }
264     }
265 
266     @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
267     @Test
268     fun screenShotScaledWithScale() {
269         val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
270 
271         sessionRule.session.loadTestPath(COLORS_HTML_PATH)
272         sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
273             @AssertCalled(count = 1)
274             override fun onFirstContentfulPaint(session: GeckoSession) {
275             }
276         })
277 
278         sessionRule.display?.let {
279             assertScreenshotResult(it.screenshot().scale(0.5f).capture(), screenshotFile)
280         }
281     }
282 
283     @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
284     @Test
285     fun screenShotScaledWithAspectPreservingSize() {
286         val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
287 
288         sessionRule.session.loadTestPath(COLORS_HTML_PATH)
289         sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
290             @AssertCalled(count = 1)
291             override fun onFirstContentfulPaint(session: GeckoSession) {
292             }
293         })
294 
295         sessionRule.display?.let {
296             assertScreenshotResult(it.screenshot().aspectPreservingSize(SCREEN_WIDTH/2).capture(), screenshotFile)
297         }
298     }
299 
300     @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
301     @Test
302     fun recycleBitmap() {
303         val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)
304 
305         sessionRule.session.loadTestPath(COLORS_HTML_PATH)
306         sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
307             @AssertCalled(count = 1)
308             override fun onFirstContentfulPaint(session: GeckoSession) {
309             }
310         })
311 
312         sessionRule.display?.let {
313             val call1 = it.screenshot().capture()
314             assertScreenshotResult(call1, screenshotFile)
315             val call2 = it.screenshot().bitmap(call1.poll(1000)).capture()
316             assertScreenshotResult(call2, screenshotFile)
317             val call3 = it.screenshot().bitmap(call2.poll(1000)).capture()
318             assertScreenshotResult(call3, screenshotFile)
319         }
320     }
321 
322     @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
323     @Test
324     fun screenshotWholeRegion() {
325         val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)
326 
327         sessionRule.session.loadTestPath(COLORS_HTML_PATH)
328         sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
329             @AssertCalled(count = 1)
330             override fun onFirstContentfulPaint(session: GeckoSession) {
331             }
332         })
333 
334         sessionRule.display?.let {
335             assertScreenshotResult(it.screenshot().source(0,0,SCREEN_WIDTH, SCREEN_HEIGHT).capture(), screenshotFile)
336         }
337     }
338 
339     @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
340     @Test
341     fun screenshotWholeRegionScaled() {
342         val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
343 
344         sessionRule.session.loadTestPath(COLORS_HTML_PATH)
345         sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
346             @AssertCalled(count = 1)
347             override fun onFirstContentfulPaint(session: GeckoSession) {
348             }
349         })
350 
351         sessionRule.display?.let {
352             assertScreenshotResult(it.screenshot()
353                     .source(0,0,SCREEN_WIDTH, SCREEN_HEIGHT)
354                     .size(SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
355                     .capture(), screenshotFile)
356         }
357     }
358 
359     @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
360     @Test
361     fun screenshotQuarters() {
362         val res = InstrumentationRegistry.getInstrumentation().targetContext.resources
363         sessionRule.session.loadTestPath(COLORS_HTML_PATH)
364         sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
365             @AssertCalled(count = 1)
366             override fun onFirstContentfulPaint(session: GeckoSession) {
367             }
368         })
369 
370         sessionRule.display?.let {
371             assertScreenshotResult(
372                     it.screenshot()
373                             .source(0,0,SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
374                             .capture(), BitmapFactory.decodeResource(res, R.drawable.colors_tl))
375             assertScreenshotResult(
376                     it.screenshot()
377                             .source(SCREEN_WIDTH/2,SCREEN_HEIGHT/2,SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
378                             .capture(), BitmapFactory.decodeResource(res, R.drawable.colors_br))
379         }
380     }
381 
382     @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
383     @Test
384     fun screenshotQuartersScaled() {
385         val res = InstrumentationRegistry.getInstrumentation().targetContext.resources
386         sessionRule.session.loadTestPath(COLORS_HTML_PATH)
387         sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
388             @AssertCalled(count = 1)
389             override fun onFirstContentfulPaint(session: GeckoSession) {
390             }
391         })
392 
393         sessionRule.display?.let {
394             assertScreenshotResult(
395                     it.screenshot()
396                             .source(0,0,SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
397                             .size(SCREEN_WIDTH/4, SCREEN_WIDTH/4)
398                             .capture(), BitmapFactory.decodeResource(res, R.drawable.colors_tl_scaled))
399             assertScreenshotResult(
400                     it.screenshot()
401                             .source(SCREEN_WIDTH/2,SCREEN_HEIGHT/2,SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
402                             .size(SCREEN_WIDTH/4, SCREEN_WIDTH/4)
403                             .capture(), BitmapFactory.decodeResource(res, R.drawable.colors_br_scaled))
404         }
405     }
406 
407     @WithDisplay(height = BIG_SCREEN_HEIGHT, width = BIG_SCREEN_WIDTH)
408     @Test
409     fun giantScreenshot() {
410         sessionRule.session.loadTestPath(COLORS_HTML_PATH)
411         sessionRule.display?.screenshot()!!.source(0,0, BIG_SCREEN_WIDTH, BIG_SCREEN_HEIGHT)
412                 .size(BIG_SCREEN_WIDTH, BIG_SCREEN_HEIGHT)
413                 .capture()
414                 .exceptionally(OnExceptionListener<Throwable> { error: Throwable ->
415                     Assert.assertTrue(error is OutOfMemoryError)
416                     fromException(error)
417                 })
418     }
419 }
420