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