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