1 /*
<lambda>null2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.graphics.surface
18 
19 import android.app.Instrumentation
20 import android.graphics.Bitmap
21 import android.graphics.Color
22 import android.graphics.Rect
23 import android.hardware.HardwareBuffer
24 import android.os.Build
25 import android.os.SystemClock
26 import android.view.SurfaceHolder
27 import android.view.SurfaceView
28 import android.view.Window
29 import androidx.annotation.RequiresApi
30 import androidx.lifecycle.Lifecycle
31 import androidx.test.core.app.ActivityScenario
32 import androidx.test.filters.SdkSuppress
33 import androidx.test.platform.app.InstrumentationRegistry
34 import java.util.concurrent.CountDownLatch
35 import java.util.concurrent.Executors
36 import java.util.concurrent.TimeUnit
37 import org.junit.Assert
38 
39 @SdkSuppress(minSdkVersion = 29)
40 internal class SurfaceControlUtils {
41     companion object {
42 
43         @RequiresApi(Build.VERSION_CODES.Q)
44         fun surfaceControlTestHelper(
45             onSurfaceCreated: (SurfaceView, CountDownLatch) -> Unit,
46             verifyOutput: (Bitmap, Rect) -> Boolean
47         ) {
48             val setupLatch = CountDownLatch(1)
49             var surfaceView: SurfaceView? = null
50             val destroyLatch = CountDownLatch(1)
51             val scenario =
52                 ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java).onActivity {
53                     it.setDestroyCallback { destroyLatch.countDown() }
54                     val callback =
55                         object : SurfaceHolder.Callback {
56                             override fun surfaceCreated(sh: SurfaceHolder) {
57                                 surfaceView = it.mSurfaceView
58                                 onSurfaceCreated(surfaceView!!, setupLatch)
59                             }
60 
61                             override fun surfaceChanged(
62                                 holder: SurfaceHolder,
63                                 format: Int,
64                                 width: Int,
65                                 height: Int
66                             ) {
67                                 // NO-OP
68                             }
69 
70                             override fun surfaceDestroyed(holder: SurfaceHolder) {
71                                 // NO-OP
72                             }
73                         }
74 
75                     it.addSurface(it.mSurfaceView, callback)
76                     surfaceView = it.mSurfaceView
77                 }
78 
79             Assert.assertTrue(setupLatch.await(3000, TimeUnit.MILLISECONDS))
80             val coords = intArrayOf(0, 0)
81             surfaceView!!.getLocationOnScreen(coords)
82             try {
83                 validateOutput { bitmap ->
84                     verifyOutput(
85                         bitmap,
86                         Rect(
87                             coords[0],
88                             coords[1],
89                             coords[0] + SurfaceControlWrapperTestActivity.DEFAULT_WIDTH,
90                             coords[1] + SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT
91                         )
92                     )
93                 }
94             } finally {
95                 scenario.moveToState(Lifecycle.State.DESTROYED)
96                 Assert.assertTrue(destroyLatch.await(3000, TimeUnit.MILLISECONDS))
97             }
98         }
99 
100         @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
101         fun validateOutput(window: Window, block: (bitmap: Bitmap) -> Boolean) {
102             val uiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation
103             val bitmap = uiAutomation.takeScreenshot(window)
104             if (bitmap != null) {
105                 block(bitmap)
106             } else {
107                 throw IllegalArgumentException("Unable to obtain bitmap from screenshot")
108             }
109         }
110 
111         private fun flushSurfaceFlinger() {
112             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
113                 var commitLatch: CountDownLatch? = null
114                 // Android S only requires 1 additional transaction
115                 val maxTransactions =
116                     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
117                         1
118                     } else {
119                         3
120                     }
121                 for (i in 0 until maxTransactions) {
122                     val transaction = SurfaceControlCompat.Transaction()
123                     // CommittedListener only added on Android S
124                     if (
125                         i == maxTransactions - 1 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
126                     ) {
127                         commitLatch = CountDownLatch(1)
128                         val executor = Executors.newSingleThreadExecutor()
129                         transaction.addTransactionCommittedListener(
130                             executor,
131                             object : SurfaceControlCompat.TransactionCommittedListener {
132                                 override fun onTransactionCommitted() {
133                                     executor.shutdownNow()
134                                     commitLatch.countDown()
135                                 }
136                             }
137                         )
138                     }
139                     transaction.commit()
140                 }
141 
142                 if (commitLatch != null) {
143                     commitLatch.await(3000, TimeUnit.MILLISECONDS)
144                 } else {
145                     // Wait for transactions to flush
146                     SystemClock.sleep(maxTransactions * 16L)
147                 }
148             }
149         }
150 
151         fun validateOutput(block: (bitmap: Bitmap) -> Boolean) {
152             flushSurfaceFlinger()
153             var sleepDurationMillis = 1000L
154             var success = false
155             for (i in 0..3) {
156                 val bitmap = getScreenshot(InstrumentationRegistry.getInstrumentation())
157                 success = block(bitmap)
158                 if (!success) {
159                     SystemClock.sleep(sleepDurationMillis)
160                     sleepDurationMillis *= 2
161                 } else {
162                     break
163                 }
164             }
165             Assert.assertTrue(success)
166         }
167 
168         fun checkNullCrop(bitmap: Bitmap, coord: IntArray): Boolean {
169             // check top left
170             return Color.RED == bitmap.getPixel(coord[0], coord[1]) &&
171                 // check top right
172                 Color.RED ==
173                     bitmap.getPixel(
174                         coord[0] + SurfaceControlWrapperTestActivity.DEFAULT_WIDTH - 1,
175                         coord[1]
176                     ) &&
177                 // check  bottom right
178                 Color.RED ==
179                     bitmap.getPixel(
180                         coord[0] + SurfaceControlWrapperTestActivity.DEFAULT_WIDTH - 1,
181                         coord[1] + SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT - 1
182                     ) &&
183                 // check bottom left
184                 Color.RED ==
185                     bitmap.getPixel(
186                         coord[0],
187                         coord[1] + SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT - 1
188                     ) &&
189                 // check center
190                 Color.RED ==
191                     bitmap.getPixel(
192                         coord[0] + SurfaceControlWrapperTestActivity.DEFAULT_WIDTH / 2,
193                         coord[1] + SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT / 2
194                     )
195         }
196 
197         fun checkStandardCrop(bitmap: Bitmap, coord: IntArray): Boolean {
198             // check left crop
199             return Color.BLACK ==
200                 bitmap.getPixel(
201                     coord[0] + 19,
202                     coord[1] + SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT / 2
203                 ) &&
204                 Color.RED ==
205                     bitmap.getPixel(
206                         coord[0] + 20,
207                         coord[1] + SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT / 2
208                     ) &&
209                 // check top crop
210                 Color.BLACK ==
211                     bitmap.getPixel(
212                         coord[0] + SurfaceControlWrapperTestActivity.DEFAULT_WIDTH / 2,
213                         coord[1] + 29
214                     ) &&
215                 Color.RED ==
216                     bitmap.getPixel(
217                         coord[0] + SurfaceControlWrapperTestActivity.DEFAULT_WIDTH / 2,
218                         coord[1] + 30
219                     ) &&
220                 // check right crop
221                 Color.BLACK ==
222                     bitmap.getPixel(
223                         coord[0] + SurfaceControlWrapperTestActivity.DEFAULT_WIDTH - 10,
224                         coord[1] + SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT / 2
225                     ) &&
226                 Color.RED ==
227                     bitmap.getPixel(
228                         coord[0] + SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT - 11,
229                         coord[1] + SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT / 2
230                     ) &&
231                 // check bottom crop
232                 Color.BLACK ==
233                     bitmap.getPixel(
234                         coord[0] + SurfaceControlWrapperTestActivity.DEFAULT_WIDTH / 2,
235                         coord[1] + SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT - 40
236                     ) &&
237                 Color.RED ==
238                     bitmap.getPixel(
239                         coord[0] + SurfaceControlWrapperTestActivity.DEFAULT_WIDTH / 2,
240                         coord[1] + SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT - 41
241                     )
242         }
243 
244         fun getScreenshot(instrumentation: Instrumentation): Bitmap {
245             val uiAutomation = instrumentation.uiAutomation
246             val screenshot = uiAutomation.takeScreenshot()
247             return screenshot
248         }
249 
250         fun getSolidBuffer(width: Int, height: Int, color: Int): HardwareBuffer {
251             return nGetSolidBuffer(width, height, color)
252         }
253 
254         fun getQuadrantBuffer(
255             width: Int,
256             height: Int,
257             colorTopLeft: Int,
258             colorTopRight: Int,
259             colorBottomRight: Int,
260             colorBottomLeft: Int
261         ): HardwareBuffer {
262             return nGetQuadrantBuffer(
263                 width,
264                 height,
265                 colorTopLeft,
266                 colorTopRight,
267                 colorBottomRight,
268                 colorBottomLeft
269             )
270         }
271 
272         private external fun nGetSolidBuffer(width: Int, height: Int, color: Int): HardwareBuffer
273 
274         private external fun nGetQuadrantBuffer(
275             width: Int,
276             height: Int,
277             colorTopLeft: Int,
278             colorTopRight: Int,
279             colorBottomRight: Int,
280             colorBottomLeft: Int
281         ): HardwareBuffer
282 
283         init {
284             System.loadLibrary("graphics-core")
285         }
286     }
287 }
288