1 /* 2 * Copyright (C) 2021 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.server.wm; 18 19 import static android.server.wm.UiDeviceUtils.pressUnlockButton; 20 import static android.server.wm.UiDeviceUtils.pressWakeupButton; 21 import static android.view.Display.DEFAULT_DISPLAY; 22 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; 23 import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_BOUNDS; 24 import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_HASH_ALGORITHM; 25 import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN; 26 import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_TOO_MANY_REQUESTS; 27 import static android.widget.LinearLayout.VERTICAL; 28 29 import static org.junit.Assert.assertArrayEquals; 30 import static org.junit.Assert.assertEquals; 31 import static org.junit.Assert.assertNotEquals; 32 import static org.junit.Assert.assertNotNull; 33 import static org.junit.Assert.assertNull; 34 35 import android.app.Activity; 36 import android.app.Instrumentation; 37 import android.app.KeyguardManager; 38 import android.content.Context; 39 import android.content.Intent; 40 import android.graphics.Color; 41 import android.graphics.Point; 42 import android.graphics.Rect; 43 import android.os.Bundle; 44 import android.os.PowerManager; 45 import android.platform.test.annotations.Presubmit; 46 import android.service.displayhash.DisplayHashParams; 47 import android.util.Size; 48 import android.view.Gravity; 49 import android.view.SurfaceControl; 50 import android.view.View; 51 import android.view.ViewTreeObserver; 52 import android.view.WindowManager; 53 import android.view.displayhash.DisplayHash; 54 import android.view.displayhash.DisplayHashManager; 55 import android.view.displayhash.DisplayHashResultCallback; 56 import android.view.displayhash.VerifiedDisplayHash; 57 import android.widget.LinearLayout; 58 import android.widget.RelativeLayout; 59 60 import androidx.annotation.NonNull; 61 import androidx.test.platform.app.InstrumentationRegistry; 62 import androidx.test.rule.ActivityTestRule; 63 64 import com.android.compatibility.common.util.SystemUtil; 65 66 import org.junit.After; 67 import org.junit.Before; 68 import org.junit.Rule; 69 import org.junit.Test; 70 71 import java.util.ArrayList; 72 import java.util.Objects; 73 import java.util.Set; 74 import java.util.concurrent.CountDownLatch; 75 import java.util.concurrent.Executor; 76 import java.util.concurrent.TimeUnit; 77 78 @Presubmit 79 public class DisplayHashManagerTest { 80 private static final int WAIT_TIME_S = 5; 81 82 private final Point mCenter = new Point(); 83 private final Point mTestViewSize = new Point(200, 300); 84 85 private Instrumentation mInstrumentation; 86 private RelativeLayout mMainView; 87 private TestActivity mActivity; 88 89 private View mTestView; 90 91 private DisplayHashManager mDisplayHashManager; 92 private String mPhashAlgorithm; 93 94 private Executor mExecutor; 95 96 private SyncDisplayHashResultCallback mSyncDisplayHashResultCallback; 97 98 protected WindowManagerStateHelper mWmState = new WindowManagerStateHelper(); 99 100 @Rule 101 public ActivityTestRule<TestActivity> mActivityRule = 102 new ActivityTestRule<>(TestActivity.class); 103 104 @Before setUp()105 public void setUp() throws Exception { 106 mInstrumentation = InstrumentationRegistry.getInstrumentation(); 107 final Context context = mInstrumentation.getContext(); 108 final KeyguardManager km = context.getSystemService(KeyguardManager.class); 109 Intent intent = new Intent(Intent.ACTION_MAIN); 110 intent.setClass(context, TestActivity.class); 111 mActivity = mActivityRule.getActivity(); 112 113 if (km != null && km.isKeyguardLocked() || !Objects.requireNonNull( 114 context.getSystemService(PowerManager.class)).isInteractive()) { 115 pressWakeupButton(); 116 pressUnlockButton(); 117 } 118 119 mActivity.runOnUiThread(() -> { 120 mMainView = new RelativeLayout(mActivity); 121 mActivity.setContentView(mMainView); 122 }); 123 mInstrumentation.waitForIdleSync(); 124 mActivity.runOnUiThread(() -> { 125 mCenter.set((mMainView.getWidth() - mTestViewSize.x) / 2, 126 (mMainView.getHeight() - mTestViewSize.y) / 2); 127 }); 128 mDisplayHashManager = context.getSystemService(DisplayHashManager.class); 129 130 Set<String> algorithms = mDisplayHashManager.getSupportedHashAlgorithms(); 131 assertNotNull(algorithms); 132 assertNotEquals(0, algorithms.size()); 133 for (String algorithm : algorithms) { 134 if ("pHash".equalsIgnoreCase(algorithm)) { 135 mPhashAlgorithm = algorithm; 136 break; 137 } 138 } 139 assertNotNull(mPhashAlgorithm); 140 141 mExecutor = context.getMainExecutor(); 142 mSyncDisplayHashResultCallback = new SyncDisplayHashResultCallback(); 143 SystemUtil.runWithShellPermissionIdentity( 144 () -> mDisplayHashManager.setDisplayHashThrottlingEnabled(false)); 145 } 146 147 @After tearDown()148 public void tearDown() { 149 SystemUtil.runWithShellPermissionIdentity( 150 () -> mDisplayHashManager.setDisplayHashThrottlingEnabled(true)); 151 } 152 153 @Test testGenerateAndVerifyDisplayHash()154 public void testGenerateAndVerifyDisplayHash() { 155 setupChildView(); 156 157 // A solid color image has expected hash of all 0s 158 byte[] expectedImageHash = new byte[8]; 159 160 DisplayHash displayHash = generateDisplayHash(null); 161 VerifiedDisplayHash verifiedDisplayHash = mDisplayHashManager.verifyDisplayHash( 162 displayHash); 163 assertNotNull(verifiedDisplayHash); 164 165 assertEquals(mTestViewSize.x, verifiedDisplayHash.getBoundsInWindow().width()); 166 assertEquals(mTestViewSize.y, verifiedDisplayHash.getBoundsInWindow().height()); 167 assertArrayEquals(expectedImageHash, verifiedDisplayHash.getImageHash()); 168 } 169 170 @Test testGenerateAndVerifyDisplayHash_BoundsInView()171 public void testGenerateAndVerifyDisplayHash_BoundsInView() { 172 setupChildView(); 173 174 Rect bounds = new Rect(10, 20, mTestViewSize.x / 2, mTestViewSize.y / 2); 175 DisplayHash displayHash = generateDisplayHash(new Rect(bounds)); 176 177 VerifiedDisplayHash verifiedDisplayHash = mDisplayHashManager.verifyDisplayHash( 178 displayHash); 179 assertNotNull(verifiedDisplayHash); 180 assertEquals(bounds.width(), verifiedDisplayHash.getBoundsInWindow().width()); 181 assertEquals(bounds.height(), verifiedDisplayHash.getBoundsInWindow().height()); 182 } 183 184 @Test testGenerateAndVerifyDisplayHash_EmptyBounds()185 public void testGenerateAndVerifyDisplayHash_EmptyBounds() { 186 setupChildView(); 187 188 mTestView.generateDisplayHash(mPhashAlgorithm, new Rect(), mExecutor, 189 mSyncDisplayHashResultCallback); 190 191 int errorCode = mSyncDisplayHashResultCallback.getError(); 192 assertEquals(DISPLAY_HASH_ERROR_INVALID_BOUNDS, errorCode); 193 } 194 195 @Test testGenerateAndVerifyDisplayHash_BoundsBiggerThanView()196 public void testGenerateAndVerifyDisplayHash_BoundsBiggerThanView() { 197 setupChildView(); 198 199 Rect bounds = new Rect(0, 0, mTestViewSize.x + 100, mTestViewSize.y + 100); 200 201 DisplayHash displayHash = generateDisplayHash(new Rect(bounds)); 202 203 VerifiedDisplayHash verifiedDisplayHash = mDisplayHashManager.verifyDisplayHash( 204 displayHash); 205 assertNotNull(verifiedDisplayHash); 206 assertEquals(mTestViewSize.x, verifiedDisplayHash.getBoundsInWindow().width()); 207 assertEquals(mTestViewSize.y, verifiedDisplayHash.getBoundsInWindow().height()); 208 } 209 210 @Test testGenerateDisplayHash_BoundsOutOfView()211 public void testGenerateDisplayHash_BoundsOutOfView() { 212 setupChildView(); 213 214 Rect bounds = new Rect(mTestViewSize.x + 1, mTestViewSize.y + 1, mTestViewSize.x + 100, 215 mTestViewSize.y + 100); 216 217 mTestView.generateDisplayHash(mPhashAlgorithm, new Rect(bounds), 218 mExecutor, mSyncDisplayHashResultCallback); 219 int errorCode = mSyncDisplayHashResultCallback.getError(); 220 assertEquals(DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN, errorCode); 221 } 222 223 @Test testGenerateDisplayHash_ViewOffscreen()224 public void testGenerateDisplayHash_ViewOffscreen() { 225 final CountDownLatch committedCallbackLatch = new CountDownLatch(1); 226 final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 227 t.addTransactionCommittedListener(mExecutor, committedCallbackLatch::countDown); 228 229 mInstrumentation.runOnMainSync(() -> { 230 final RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(mTestViewSize.x, 231 mTestViewSize.y); 232 mTestView = new View(mActivity); 233 mTestView.setBackgroundColor(Color.BLUE); 234 mTestView.setX(-mTestViewSize.x); 235 236 mMainView.addView(mTestView, p); 237 mMainView.getRootSurfaceControl().applyTransactionOnDraw(t); 238 }); 239 mInstrumentation.waitForIdleSync(); 240 try { 241 committedCallbackLatch.await(WAIT_TIME_S, TimeUnit.SECONDS); 242 } catch (InterruptedException e) { 243 } 244 245 mTestView.generateDisplayHash(mPhashAlgorithm, null, mExecutor, 246 mSyncDisplayHashResultCallback); 247 248 int errorCode = mSyncDisplayHashResultCallback.getError(); 249 assertEquals(DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN, errorCode); 250 } 251 252 @Test testGenerateDisplayHash_WindowOffscreen()253 public void testGenerateDisplayHash_WindowOffscreen() { 254 final WindowManager wm = mActivity.getWindowManager(); 255 final WindowManager.LayoutParams windowParams = new WindowManager.LayoutParams(); 256 257 final CountDownLatch committedCallbackLatch = new CountDownLatch(1); 258 final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 259 t.addTransactionCommittedListener(mExecutor, committedCallbackLatch::countDown); 260 mInstrumentation.runOnMainSync(() -> { 261 mMainView = new RelativeLayout(mActivity); 262 windowParams.width = mTestViewSize.x; 263 windowParams.height = mTestViewSize.y; 264 windowParams.gravity = Gravity.LEFT | Gravity.TOP; 265 windowParams.flags = FLAG_LAYOUT_NO_LIMITS; 266 mActivity.addWindow(mMainView, windowParams); 267 268 mMainView.getViewTreeObserver().addOnWindowAttachListener( 269 new ViewTreeObserver.OnWindowAttachListener() { 270 @Override 271 public void onWindowAttached() { 272 final RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams( 273 mTestViewSize.x, 274 mTestViewSize.y); 275 mTestView = new View(mActivity); 276 mTestView.setBackgroundColor(Color.BLUE); 277 mMainView.addView(mTestView, p); 278 mMainView.getRootSurfaceControl().applyTransactionOnDraw(t); 279 } 280 281 @Override 282 public void onWindowDetached() { 283 } 284 }); 285 }); 286 mInstrumentation.waitForIdleSync(); 287 try { 288 committedCallbackLatch.await(WAIT_TIME_S, TimeUnit.SECONDS); 289 } catch (InterruptedException e) { 290 } 291 292 generateDisplayHash(null); 293 294 mInstrumentation.runOnMainSync(() -> { 295 int[] mainViewLocationOnScreen = new int[2]; 296 mMainView.getLocationOnScreen(mainViewLocationOnScreen); 297 298 windowParams.x = -mTestViewSize.x - mainViewLocationOnScreen[0]; 299 wm.updateViewLayout(mMainView, windowParams); 300 }); 301 mInstrumentation.waitForIdleSync(); 302 303 mSyncDisplayHashResultCallback.reset(); 304 mTestView.generateDisplayHash(mPhashAlgorithm, null, mExecutor, 305 mSyncDisplayHashResultCallback); 306 307 int errorCode = mSyncDisplayHashResultCallback.getError(); 308 assertEquals(DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN, errorCode); 309 } 310 311 @Test testGenerateDisplayHash_InvalidHashAlgorithm()312 public void testGenerateDisplayHash_InvalidHashAlgorithm() { 313 setupChildView(); 314 315 mTestView.generateDisplayHash("fake hash", null, mExecutor, 316 mSyncDisplayHashResultCallback); 317 int errorCode = mSyncDisplayHashResultCallback.getError(); 318 assertEquals(DISPLAY_HASH_ERROR_INVALID_HASH_ALGORITHM, errorCode); 319 } 320 321 @Test testVerifyDisplayHash_ValidDisplayHash()322 public void testVerifyDisplayHash_ValidDisplayHash() { 323 setupChildView(); 324 325 DisplayHash displayHash = generateDisplayHash(null); 326 VerifiedDisplayHash verifiedDisplayHash = mDisplayHashManager.verifyDisplayHash( 327 displayHash); 328 329 assertNotNull(verifiedDisplayHash); 330 assertEquals(displayHash.getTimeMillis(), verifiedDisplayHash.getTimeMillis()); 331 assertEquals(displayHash.getBoundsInWindow(), verifiedDisplayHash.getBoundsInWindow()); 332 assertEquals(displayHash.getHashAlgorithm(), verifiedDisplayHash.getHashAlgorithm()); 333 assertArrayEquals(displayHash.getImageHash(), verifiedDisplayHash.getImageHash()); 334 } 335 336 @Test testVerifyDisplayHash_InvalidDisplayHash()337 public void testVerifyDisplayHash_InvalidDisplayHash() { 338 setupChildView(); 339 340 DisplayHash displayHash = generateDisplayHash(null); 341 DisplayHash fakeDisplayHash = new DisplayHash( 342 displayHash.getTimeMillis(), displayHash.getBoundsInWindow(), 343 displayHash.getHashAlgorithm(), new byte[32], displayHash.getHmac()); 344 VerifiedDisplayHash verifiedDisplayHash = mDisplayHashManager.verifyDisplayHash( 345 fakeDisplayHash); 346 347 assertNull(verifiedDisplayHash); 348 } 349 350 @Test testVerifiedDisplayHash()351 public void testVerifiedDisplayHash() { 352 long timeMillis = 1000; 353 Rect boundsInWindow = new Rect(0, 0, 50, 100); 354 String hashAlgorithm = "hashAlgorithm"; 355 byte[] imageHash = new byte[]{2, 4, 1, 5, 6, 2}; 356 VerifiedDisplayHash verifiedDisplayHash = new VerifiedDisplayHash(timeMillis, 357 boundsInWindow, hashAlgorithm, imageHash); 358 359 assertEquals(timeMillis, verifiedDisplayHash.getTimeMillis()); 360 assertEquals(boundsInWindow, verifiedDisplayHash.getBoundsInWindow()); 361 assertEquals(hashAlgorithm, verifiedDisplayHash.getHashAlgorithm()); 362 assertArrayEquals(imageHash, verifiedDisplayHash.getImageHash()); 363 } 364 365 @Test testGenerateDisplayHash_Throttle()366 public void testGenerateDisplayHash_Throttle() { 367 SystemUtil.runWithShellPermissionIdentity( 368 () -> mDisplayHashManager.setDisplayHashThrottlingEnabled(true)); 369 370 setupChildView(); 371 372 mTestView.generateDisplayHash(mPhashAlgorithm, null, mExecutor, 373 mSyncDisplayHashResultCallback); 374 mSyncDisplayHashResultCallback.getDisplayHash(); 375 mSyncDisplayHashResultCallback.reset(); 376 // Generate a second display hash right away. 377 mTestView.generateDisplayHash(mPhashAlgorithm, null, mExecutor, 378 mSyncDisplayHashResultCallback); 379 int errorCode = mSyncDisplayHashResultCallback.getError(); 380 assertEquals(DISPLAY_HASH_ERROR_TOO_MANY_REQUESTS, errorCode); 381 } 382 383 @Test testGenerateAndVerifyDisplayHash_MultiColor()384 public void testGenerateAndVerifyDisplayHash_MultiColor() { 385 final CountDownLatch committedCallbackLatch = new CountDownLatch(1); 386 final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 387 t.addTransactionCommittedListener(mExecutor, committedCallbackLatch::countDown); 388 mInstrumentation.runOnMainSync(() -> { 389 final RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(mTestViewSize.x, 390 mTestViewSize.y); 391 LinearLayout linearLayout = new LinearLayout(mActivity); 392 linearLayout.setOrientation(VERTICAL); 393 LinearLayout.LayoutParams blueParams = new LinearLayout.LayoutParams(mTestViewSize.x, 394 mTestViewSize.y / 2); 395 View blueView = new View(mActivity); 396 blueView.setBackgroundColor(Color.BLUE); 397 LinearLayout.LayoutParams redParams = new LinearLayout.LayoutParams(mTestViewSize.x, 398 mTestViewSize.y / 2); 399 View redView = new View(mActivity); 400 redView.setBackgroundColor(Color.RED); 401 402 linearLayout.addView(blueView, blueParams); 403 linearLayout.addView(redView, redParams); 404 mTestView = linearLayout; 405 406 mTestView.setX(mCenter.x); 407 mTestView.setY(mCenter.y); 408 mMainView.addView(mTestView, p); 409 mMainView.getRootSurfaceControl().applyTransactionOnDraw(t); 410 }); 411 mInstrumentation.waitForIdleSync(); 412 mWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY); 413 try { 414 committedCallbackLatch.await(WAIT_TIME_S, TimeUnit.SECONDS); 415 } catch (InterruptedException e) { 416 } 417 418 byte[] expectedImageHash = new byte[]{-1, -1, 127, -1, -1, -1, 127, 127}; 419 420 DisplayHash displayHash = generateDisplayHash(null); 421 VerifiedDisplayHash verifiedDisplayHash = mDisplayHashManager.verifyDisplayHash( 422 displayHash); 423 assertNotNull(verifiedDisplayHash); 424 425 assertEquals(mTestViewSize.x, verifiedDisplayHash.getBoundsInWindow().width()); 426 assertEquals(mTestViewSize.y, verifiedDisplayHash.getBoundsInWindow().height()); 427 assertArrayEquals(expectedImageHash, verifiedDisplayHash.getImageHash()); 428 } 429 430 @Test testDisplayHashParams()431 public void testDisplayHashParams() { 432 int width = 10; 433 int height = 20; 434 boolean isGrayscale = true; 435 DisplayHashParams displayHashParams = new DisplayHashParams.Builder() 436 .setBufferSize(width, height) 437 .setGrayscaleBuffer(isGrayscale) 438 .build(); 439 440 Size bufferSize = displayHashParams.getBufferSize(); 441 assertEquals(width, bufferSize.getWidth()); 442 assertEquals(height, bufferSize.getHeight()); 443 assertEquals(isGrayscale, displayHashParams.isGrayscaleBuffer()); 444 } 445 generateDisplayHash(Rect bounds)446 private DisplayHash generateDisplayHash(Rect bounds) { 447 mTestView.generateDisplayHash(mPhashAlgorithm, bounds, mExecutor, 448 mSyncDisplayHashResultCallback); 449 DisplayHash displayHash = mSyncDisplayHashResultCallback.getDisplayHash(); 450 451 assertNotNull(displayHash); 452 return displayHash; 453 } 454 setupChildView()455 private void setupChildView() { 456 final CountDownLatch committedCallbackLatch = new CountDownLatch(1); 457 final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 458 t.addTransactionCommittedListener(mExecutor, committedCallbackLatch::countDown); 459 460 mInstrumentation.runOnMainSync(() -> { 461 final RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(mTestViewSize.x, 462 mTestViewSize.y); 463 mTestView = new View(mActivity); 464 mTestView.setX(mCenter.x); 465 mTestView.setY(mCenter.y); 466 mTestView.setBackgroundColor(Color.BLUE); 467 mMainView.addView(mTestView, p); 468 mMainView.getRootSurfaceControl().applyTransactionOnDraw(t); 469 }); 470 mInstrumentation.waitForIdleSync(); 471 try { 472 committedCallbackLatch.await(WAIT_TIME_S, TimeUnit.SECONDS); 473 } catch (InterruptedException e) { 474 } 475 } 476 477 public static class TestActivity extends Activity { 478 private final ArrayList<View> mViews = new ArrayList<>(); 479 480 @Override onCreate(Bundle savedInstanceState)481 protected void onCreate(Bundle savedInstanceState) { 482 super.onCreate(savedInstanceState); 483 } 484 addWindow(View view, WindowManager.LayoutParams attrs)485 void addWindow(View view, WindowManager.LayoutParams attrs) { 486 getWindowManager().addView(view, attrs); 487 mViews.add(view); 488 } 489 removeAllWindows()490 void removeAllWindows() { 491 for (View view : mViews) { 492 getWindowManager().removeViewImmediate(view); 493 } 494 mViews.clear(); 495 } 496 497 @Override onPause()498 protected void onPause() { 499 super.onPause(); 500 removeAllWindows(); 501 } 502 } 503 504 private static class SyncDisplayHashResultCallback implements DisplayHashResultCallback { 505 private static final int SCREENSHOT_WAIT_TIME_S = 1; 506 private DisplayHash mDisplayHash; 507 private int mError; 508 private CountDownLatch mCountDownLatch = new CountDownLatch(1); 509 reset()510 public void reset() { 511 mCountDownLatch = new CountDownLatch(1); 512 } 513 getDisplayHash()514 public DisplayHash getDisplayHash() { 515 try { 516 mCountDownLatch.await(SCREENSHOT_WAIT_TIME_S, TimeUnit.SECONDS); 517 } catch (Exception e) { 518 } 519 return mDisplayHash; 520 } 521 getError()522 public int getError() { 523 try { 524 mCountDownLatch.await(SCREENSHOT_WAIT_TIME_S, TimeUnit.SECONDS); 525 } catch (Exception e) { 526 } 527 return mError; 528 } 529 530 @Override onDisplayHashResult(@onNull DisplayHash displayHash)531 public void onDisplayHashResult(@NonNull DisplayHash displayHash) { 532 mDisplayHash = displayHash; 533 mCountDownLatch.countDown(); 534 } 535 536 @Override onDisplayHashError(int errorCode)537 public void onDisplayHashError(int errorCode) { 538 mError = errorCode; 539 mCountDownLatch.countDown(); 540 } 541 } 542 } 543