1 /*
2  * Copyright (C) 2023 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 android.input.cts
18 
19 import android.Manifest
20 import android.app.Activity
21 import android.app.ActivityOptions
22 import android.content.Intent
23 import android.content.pm.PackageManager
24 import android.graphics.Bitmap
25 import android.graphics.PixelFormat
26 import android.hardware.display.DisplayManager
27 import android.hardware.display.VirtualDisplay
28 import android.media.ImageReader
29 import android.os.Handler
30 import android.os.Looper
31 import android.server.wm.WindowManagerStateHelper
32 import android.view.Surface
33 import androidx.test.platform.app.InstrumentationRegistry
34 import com.android.compatibility.common.util.SystemUtil
35 import java.nio.ByteBuffer
36 import java.util.concurrent.CountDownLatch
37 import java.util.concurrent.TimeUnit
38 import org.junit.Assert.assertTrue
39 import org.junit.Assume.assumeTrue
40 import org.junit.rules.ExternalResource
41 import org.junit.rules.TestName
42 
43 /**
44  * A test rule that sets up a virtual display, and launches the specified activity on that display.
45  */
46 class VirtualDisplayActivityScenarioRule<A : Activity>(
47     val testName: TestName,
48     val type: Class<A>
49 ) : ExternalResource() {
50     companion object {
51         const val TAG = "VirtualDisplayActivityScenarioRule"
52         const val VIRTUAL_DISPLAY_NAME = "CtsTouchScreenTestVirtualDisplay"
53         const val WIDTH = 480
54         const val HEIGHT = 800
55         const val DENSITY = 160
56         const val ORIENTATION_0 = Surface.ROTATION_0
57         const val ORIENTATION_90 = Surface.ROTATION_90
58         const val ORIENTATION_180 = Surface.ROTATION_180
59         const val ORIENTATION_270 = Surface.ROTATION_270
60 
61         /** See [DisplayManager.VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH].  */
62         const val VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 shl 6
63 
64         /** See [DisplayManager.VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT].  */
65         const val VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 shl 7
invokenull66         inline operator fun <reified A : Activity> invoke(
67             testName: TestName
68         ): VirtualDisplayActivityScenarioRule<A> = VirtualDisplayActivityScenarioRule(
69             testName,
70             A::class.java
71         )
72     }
73 
74     private val instrumentation = InstrumentationRegistry.getInstrumentation()
75     private lateinit var reader: ImageReader
76 
77     lateinit var virtualDisplay: VirtualDisplay
78     lateinit var activity: A
79     val displayId: Int get() = virtualDisplay.display.displayId
80 
81     /**
82      * Before the test starts, set up the virtual display and start the activity A on that
83      * display.
84      */
85     override fun before() {
86         assumeTrue(supportsMultiDisplay())
87         createDisplay()
88 
89         val bundle =
90             ActivityOptions.makeBasic().setLaunchDisplayId(displayId)
91                 .toBundle()
92         val intent = Intent(Intent.ACTION_VIEW)
93             .setClass(instrumentation.targetContext, type)
94             .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
95         SystemUtil.runWithShellPermissionIdentity({
96             @Suppress("UNCHECKED_CAST")
97             activity = instrumentation.startActivitySync(intent, bundle) as A
98         }, Manifest.permission.INTERNAL_SYSTEM_WINDOW)
99         waitUntilActivityReadyForInput()
100     }
101 
102     /**
103      * Clean up after the test completes.
104      */
afternull105     override fun after() {
106         if (!supportsMultiDisplay()) {
107             return
108         }
109         releaseDisplay()
110         activity.finish()
111     }
112 
113     /**
114      * This is a helper methods for tests to make assertions with the display rotated to the given
115      * orientation.
116      *
117      * @param orientation The orientation to which the display should be rotated.
118      * @param runnable The function to run with the display is in the given orientation.
119      */
runInDisplayOrientationnull120     fun runInDisplayOrientation(orientation: Int, runnable: () -> Unit) {
121         val initialUserRotation =
122             SystemUtil.runShellCommandOrThrow("wm user-rotation -d $displayId")!!
123         SystemUtil.runShellCommandOrThrow("wm user-rotation -d $displayId lock $orientation")
124         waitUntilActivityReadyForInput()
125 
126         try {
127             runnable()
128         } finally {
129             SystemUtil.runShellCommandOrThrow(
130                 "wm user-rotation -d $displayId $initialUserRotation"
131             )
132         }
133     }
134 
supportsMultiDisplaynull135     private fun supportsMultiDisplay(): Boolean {
136         return instrumentation.targetContext.packageManager
137             .hasSystemFeature(PackageManager.FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS)
138     }
139 
createDisplaynull140     private fun createDisplay() {
141         val displayManager =
142             instrumentation.targetContext.getSystemService(DisplayManager::class.java)
143 
144         val displayCreated = CountDownLatch(1)
145         displayManager.registerDisplayListener(object : DisplayManager.DisplayListener {
146             override fun onDisplayAdded(displayId: Int) {}
147             override fun onDisplayRemoved(displayId: Int) {}
148             override fun onDisplayChanged(displayId: Int) {
149                 displayCreated.countDown()
150                 displayManager.unregisterDisplayListener(this)
151             }
152         }, Handler(Looper.getMainLooper()))
153         reader = ImageReader.newInstance(WIDTH, HEIGHT, PixelFormat.RGBA_8888, 2)
154         virtualDisplay = displayManager.createVirtualDisplay(
155             VIRTUAL_DISPLAY_NAME, WIDTH, HEIGHT, DENSITY, reader.surface,
156             VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH or VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT)
157 
158         assertTrue(displayCreated.await(5, TimeUnit.SECONDS))
159     }
160 
releaseDisplaynull161     private fun releaseDisplay() {
162         virtualDisplay.release()
163         reader.close()
164     }
165 
waitUntilActivityReadyForInputnull166     private fun waitUntilActivityReadyForInput() {
167         // If we requested an orientation change, just waiting for the window to be visible is not
168         // sufficient. We should first wait for the transitions to stop, and the for app's UI thread
169         // to process them before making sure the window is visible.
170         WindowManagerStateHelper().waitUntilActivityReadyForInputInjection(
171             activity,
172             TAG,
173             "test: ${testName.methodName}, virtualDisplayId=$displayId"
174         )
175     }
176 
177     /**
178      * Retrieves a Bitmap screenshot from the internal ImageReader this virtual display writes to.
179      *
180      * <p>Currently only supports screenshots in the RGBA_8888.
181      */
getScreenshotnull182     fun getScreenshot(): Bitmap? {
183         val image = reader.acquireNextImage()
184         if (image == null || image.format != PixelFormat.RGBA_8888) {
185             return null
186         }
187         val buffer = image.planes[0].getBuffer()
188         val pixelStrideBytes: Int = image.planes[0].getPixelStride()
189         val rowStrideBytes: Int = image.planes[0].getRowStride()
190         val pixelBytesPerRow = pixelStrideBytes * image.width
191         val rowPaddingBytes = rowStrideBytes - pixelBytesPerRow
192 
193         // Remove the row padding bytes from the buffer before converting to a Bitmap
194         val trimmedBuffer = ByteBuffer.allocate(buffer.remaining())
195         buffer.rewind()
196         while (buffer.hasRemaining()) {
197             for (i in 0 until pixelBytesPerRow) {
198                 trimmedBuffer.put(buffer.get())
199             }
200             // Skip the padding bytes
201             buffer.position(buffer.position() + rowPaddingBytes)
202         }
203         trimmedBuffer.flip() // Prepare the trimmed buffer for reading
204 
205         val bitmap = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)
206         bitmap.copyPixelsFromBuffer(trimmedBuffer)
207         return bitmap
208     }
209 }
210