blob: 366f4dd36880e9399cde11c5d67967b7bddd71c6 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.server.wm;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_BOUNDS;
import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_HASH_ALGORITHM;
import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN;
import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_TOO_MANY_REQUESTS;
import static android.widget.LinearLayout.VERTICAL;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Bundle;
import android.platform.test.annotations.Presubmit;
import android.view.Gravity;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.view.displayhash.DisplayHash;
import android.view.displayhash.DisplayHashManager;
import android.view.displayhash.DisplayHashResultCallback;
import android.view.displayhash.VerifiedDisplayHash;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import com.android.compatibility.common.util.SystemUtil;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
@Presubmit
public class DisplayHashManagerTest {
//TODO (b/195136026): There's currently know way to know when the buffer has been drawn in
// SurfaceFlinger. Use sleep for now to make sure it's been drawn. Once b/195136026 is
// completed, port this code to listen for the transaction complete so we can be sure the buffer
// has been latched.
private static final int SLEEP_TIME_MS = 1000;
private final Point mTestViewSize = new Point(200, 300);
private Instrumentation mInstrumentation;
private RelativeLayout mMainView;
private TestActivity mActivity;
private View mTestView;
private DisplayHashManager mDisplayHashManager;
private String mPhashAlgorithm;
private Executor mExecutor;
private SyncDisplayHashResultCallback mSyncDisplayHashResultCallback;
@Rule
public ActivityTestRule<TestActivity> mActivityRule =
new ActivityTestRule<>(TestActivity.class);
@Before
public void setUp() throws Exception {
mInstrumentation = InstrumentationRegistry.getInstrumentation();
Context context = mInstrumentation.getContext();
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setClass(context, TestActivity.class);
mActivity = mActivityRule.getActivity();
mActivity.runOnUiThread(() -> {
mMainView = new RelativeLayout(mActivity);
mActivity.setContentView(mMainView);
});
mInstrumentation.waitForIdleSync();
mDisplayHashManager = context.getSystemService(DisplayHashManager.class);
Set<String> algorithms = mDisplayHashManager.getSupportedHashAlgorithms();
assertNotNull(algorithms);
assertNotEquals(0, algorithms.size());
for (String algorithm : algorithms) {
if ("pHash".equalsIgnoreCase(algorithm)) {
mPhashAlgorithm = algorithm;
break;
}
}
assertNotNull(mPhashAlgorithm);
mExecutor = context.getMainExecutor();
mSyncDisplayHashResultCallback = new SyncDisplayHashResultCallback();
SystemUtil.runWithShellPermissionIdentity(
() -> mDisplayHashManager.setDisplayHashThrottlingEnabled(false));
}
@After
public void tearDown() {
SystemUtil.runWithShellPermissionIdentity(
() -> mDisplayHashManager.setDisplayHashThrottlingEnabled(true));
}
@Test
public void testGenerateAndVerifyDisplayHash() {
setupChildView();
// A solid color image has expected hash of all 0s
byte[] expectedImageHash = new byte[8];
DisplayHash displayHash = generateDisplayHash(null);
VerifiedDisplayHash verifiedDisplayHash = mDisplayHashManager.verifyDisplayHash(
displayHash);
assertNotNull(verifiedDisplayHash);
assertEquals(mTestViewSize.x, verifiedDisplayHash.getBoundsInWindow().width());
assertEquals(mTestViewSize.y, verifiedDisplayHash.getBoundsInWindow().height());
assertArrayEquals(expectedImageHash, verifiedDisplayHash.getImageHash());
}
@Test
public void testGenerateAndVerifyDisplayHash_BoundsInView() {
setupChildView();
Rect bounds = new Rect(10, 20, mTestViewSize.x / 2, mTestViewSize.y / 2);
DisplayHash displayHash = generateDisplayHash(new Rect(bounds));
VerifiedDisplayHash verifiedDisplayHash = mDisplayHashManager.verifyDisplayHash(
displayHash);
assertNotNull(verifiedDisplayHash);
assertEquals(bounds.width(), verifiedDisplayHash.getBoundsInWindow().width());
assertEquals(bounds.height(), verifiedDisplayHash.getBoundsInWindow().height());
}
@Test
public void testGenerateAndVerifyDisplayHash_EmptyBounds() {
setupChildView();
mTestView.generateDisplayHash(mPhashAlgorithm, new Rect(), mExecutor,
mSyncDisplayHashResultCallback);
int errorCode = mSyncDisplayHashResultCallback.getError();
assertEquals(DISPLAY_HASH_ERROR_INVALID_BOUNDS, errorCode);
}
@Test
public void testGenerateAndVerifyDisplayHash_BoundsBiggerThanView() {
setupChildView();
Rect bounds = new Rect(0, 0, mTestViewSize.x + 100, mTestViewSize.y + 100);
DisplayHash displayHash = generateDisplayHash(new Rect(bounds));
VerifiedDisplayHash verifiedDisplayHash = mDisplayHashManager.verifyDisplayHash(
displayHash);
assertNotNull(verifiedDisplayHash);
assertEquals(mTestViewSize.x, verifiedDisplayHash.getBoundsInWindow().width());
assertEquals(mTestViewSize.y, verifiedDisplayHash.getBoundsInWindow().height());
}
@Test
public void testGenerateDisplayHash_BoundsOutOfView() {
setupChildView();
Rect bounds = new Rect(mTestViewSize.x + 1, mTestViewSize.y + 1, mTestViewSize.x + 100,
mTestViewSize.y + 100);
mTestView.generateDisplayHash(mPhashAlgorithm, new Rect(bounds),
mExecutor, mSyncDisplayHashResultCallback);
int errorCode = mSyncDisplayHashResultCallback.getError();
assertEquals(DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN, errorCode);
}
@Test
public void testGenerateDisplayHash_ViewOffscreen() {
final CountDownLatch viewLayoutLatch = new CountDownLatch(2);
mInstrumentation.runOnMainSync(() -> {
final RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(mTestViewSize.x,
mTestViewSize.y);
mTestView = new View(mActivity);
mTestView.setBackgroundColor(Color.BLUE);
mTestView.setX(-mTestViewSize.x);
ViewTreeObserver viewTreeObserver = mTestView.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(viewLayoutLatch::countDown);
viewTreeObserver.registerFrameCommitCallback(viewLayoutLatch::countDown);
mMainView.addView(mTestView, p);
mMainView.invalidate();
});
mInstrumentation.waitForIdleSync();
try {
viewLayoutLatch.await(5, TimeUnit.SECONDS);
Thread.sleep(SLEEP_TIME_MS);
} catch (InterruptedException e) {
}
mTestView.generateDisplayHash(mPhashAlgorithm, null, mExecutor,
mSyncDisplayHashResultCallback);
int errorCode = mSyncDisplayHashResultCallback.getError();
assertEquals(DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN, errorCode);
}
@Test
public void testGenerateDisplayHash_WindowOffscreen() {
final WindowManager wm = mActivity.getWindowManager();
final WindowManager.LayoutParams windowParams = new WindowManager.LayoutParams();
final CountDownLatch viewLayoutLatch = new CountDownLatch(2);
mInstrumentation.runOnMainSync(() -> {
mMainView = new RelativeLayout(mActivity);
windowParams.width = mTestViewSize.x;
windowParams.height = mTestViewSize.y;
windowParams.gravity = Gravity.LEFT | Gravity.TOP;
windowParams.flags = FLAG_LAYOUT_NO_LIMITS;
mActivity.addWindow(mMainView, windowParams);
final RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(mTestViewSize.x,
mTestViewSize.y);
mTestView = new View(mActivity);
mTestView.setBackgroundColor(Color.BLUE);
ViewTreeObserver viewTreeObserver = mTestView.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(viewLayoutLatch::countDown);
viewTreeObserver.registerFrameCommitCallback(viewLayoutLatch::countDown);
mMainView.addView(mTestView, p);
});
mInstrumentation.waitForIdleSync();
try {
viewLayoutLatch.await(5, TimeUnit.SECONDS);
Thread.sleep(SLEEP_TIME_MS);
} catch (InterruptedException e) {
}
generateDisplayHash(null);
mInstrumentation.runOnMainSync(() -> {
int[] mainViewLocationOnScreen = new int[2];
mMainView.getLocationOnScreen(mainViewLocationOnScreen);
windowParams.x = -mTestViewSize.x - mainViewLocationOnScreen[0];
wm.updateViewLayout(mMainView, windowParams);
});
mInstrumentation.waitForIdleSync();
mSyncDisplayHashResultCallback.reset();
mTestView.generateDisplayHash(mPhashAlgorithm, null, mExecutor,
mSyncDisplayHashResultCallback);
int errorCode = mSyncDisplayHashResultCallback.getError();
assertEquals(DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN, errorCode);
}
@Test
public void testGenerateDisplayHash_InvalidHashAlgorithm() {
setupChildView();
mTestView.generateDisplayHash("fake hash", null, mExecutor,
mSyncDisplayHashResultCallback);
int errorCode = mSyncDisplayHashResultCallback.getError();
assertEquals(DISPLAY_HASH_ERROR_INVALID_HASH_ALGORITHM, errorCode);
}
@Test
public void testVerifyDisplayHash_ValidDisplayHash() {
setupChildView();
DisplayHash displayHash = generateDisplayHash(null);
VerifiedDisplayHash verifiedDisplayHash = mDisplayHashManager.verifyDisplayHash(
displayHash);
assertNotNull(verifiedDisplayHash);
assertEquals(displayHash.getTimeMillis(), verifiedDisplayHash.getTimeMillis());
assertEquals(displayHash.getBoundsInWindow(), verifiedDisplayHash.getBoundsInWindow());
assertEquals(displayHash.getHashAlgorithm(), verifiedDisplayHash.getHashAlgorithm());
assertArrayEquals(displayHash.getImageHash(), verifiedDisplayHash.getImageHash());
}
@Test
public void testVerifyDisplayHash_InvalidDisplayHash() {
setupChildView();
DisplayHash displayHash = generateDisplayHash(null);
DisplayHash fakeDisplayHash = new DisplayHash(
displayHash.getTimeMillis(), displayHash.getBoundsInWindow(),
displayHash.getHashAlgorithm(), new byte[32], displayHash.getHmac());
VerifiedDisplayHash verifiedDisplayHash = mDisplayHashManager.verifyDisplayHash(
fakeDisplayHash);
assertNull(verifiedDisplayHash);
}
@Test
public void testVerifiedDisplayHash() {
long timeMillis = 1000;
Rect boundsInWindow = new Rect(0, 0, 50, 100);
String hashAlgorithm = "hashAlgorithm";
byte[] imageHash = new byte[]{2, 4, 1, 5, 6, 2};
VerifiedDisplayHash verifiedDisplayHash = new VerifiedDisplayHash(timeMillis,
boundsInWindow, hashAlgorithm, imageHash);
assertEquals(timeMillis, verifiedDisplayHash.getTimeMillis());
assertEquals(boundsInWindow, verifiedDisplayHash.getBoundsInWindow());
assertEquals(hashAlgorithm, verifiedDisplayHash.getHashAlgorithm());
assertArrayEquals(imageHash, verifiedDisplayHash.getImageHash());
}
@Test
public void testGenerateDisplayHash_Throttle() {
SystemUtil.runWithShellPermissionIdentity(
() -> mDisplayHashManager.setDisplayHashThrottlingEnabled(true));
setupChildView();
mTestView.generateDisplayHash(mPhashAlgorithm, null, mExecutor,
mSyncDisplayHashResultCallback);
mSyncDisplayHashResultCallback.getDisplayHash();
mSyncDisplayHashResultCallback.reset();
// Generate a second display hash right away.
mTestView.generateDisplayHash(mPhashAlgorithm, null, mExecutor,
mSyncDisplayHashResultCallback);
int errorCode = mSyncDisplayHashResultCallback.getError();
assertEquals(DISPLAY_HASH_ERROR_TOO_MANY_REQUESTS, errorCode);
}
@Test
public void testGenerateAndVerifyDisplayHash_MultiColor() {
final CountDownLatch viewLayoutLatch = new CountDownLatch(2);
mInstrumentation.runOnMainSync(() -> {
final RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(mTestViewSize.x,
mTestViewSize.y);
LinearLayout linearLayout = new LinearLayout(mActivity);
linearLayout.setOrientation(VERTICAL);
LinearLayout.LayoutParams blueParams = new LinearLayout.LayoutParams(mTestViewSize.x,
mTestViewSize.y / 2);
View blueView = new View(mActivity);
blueView.setBackgroundColor(Color.BLUE);
LinearLayout.LayoutParams redParams = new LinearLayout.LayoutParams(mTestViewSize.x,
mTestViewSize.y / 2);
View redView = new View(mActivity);
redView.setBackgroundColor(Color.RED);
linearLayout.addView(blueView, blueParams);
linearLayout.addView(redView, redParams);
mTestView = linearLayout;
ViewTreeObserver viewTreeObserver = mTestView.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(viewLayoutLatch::countDown);
viewTreeObserver.registerFrameCommitCallback(viewLayoutLatch::countDown);
mMainView.addView(mTestView, p);
mMainView.invalidate();
});
mInstrumentation.waitForIdleSync();
try {
viewLayoutLatch.await(5, TimeUnit.SECONDS);
Thread.sleep(SLEEP_TIME_MS);
} catch (InterruptedException e) {
}
byte[] expectedImageHash = new byte[]{-1, -1, 127, -1, -1, -1, 127, 127};
DisplayHash displayHash = generateDisplayHash(null);
VerifiedDisplayHash verifiedDisplayHash = mDisplayHashManager.verifyDisplayHash(
displayHash);
assertNotNull(verifiedDisplayHash);
assertEquals(mTestViewSize.x, verifiedDisplayHash.getBoundsInWindow().width());
assertEquals(mTestViewSize.y, verifiedDisplayHash.getBoundsInWindow().height());
assertArrayEquals(expectedImageHash, verifiedDisplayHash.getImageHash());
}
private DisplayHash generateDisplayHash(Rect bounds) {
mTestView.generateDisplayHash(mPhashAlgorithm, bounds, mExecutor,
mSyncDisplayHashResultCallback);
DisplayHash displayHash = mSyncDisplayHashResultCallback.getDisplayHash();
assertNotNull(displayHash);
return displayHash;
}
private void setupChildView() {
final CountDownLatch viewLayoutLatch = new CountDownLatch(2);
mInstrumentation.runOnMainSync(() -> {
final RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(mTestViewSize.x,
mTestViewSize.y);
mTestView = new View(mActivity);
mTestView.setBackgroundColor(Color.BLUE);
ViewTreeObserver viewTreeObserver = mTestView.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(viewLayoutLatch::countDown);
viewTreeObserver.registerFrameCommitCallback(viewLayoutLatch::countDown);
mMainView.addView(mTestView, p);
mMainView.invalidate();
});
mInstrumentation.waitForIdleSync();
try {
viewLayoutLatch.await(5, TimeUnit.SECONDS);
Thread.sleep(SLEEP_TIME_MS);
} catch (InterruptedException e) {
}
}
public static class TestActivity extends Activity {
private final ArrayList<View> mViews = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
void addWindow(View view, WindowManager.LayoutParams attrs) {
getWindowManager().addView(view, attrs);
mViews.add(view);
}
void removeAllWindows() {
for (View view : mViews) {
getWindowManager().removeViewImmediate(view);
}
mViews.clear();
}
@Override
protected void onPause() {
super.onPause();
removeAllWindows();
}
}
private static class SyncDisplayHashResultCallback implements DisplayHashResultCallback {
private static final int SCREENSHOT_WAIT_TIME_S = 1;
private DisplayHash mDisplayHash;
private int mError;
private CountDownLatch mCountDownLatch = new CountDownLatch(1);
public void reset() {
mCountDownLatch = new CountDownLatch(1);
}
public DisplayHash getDisplayHash() {
try {
mCountDownLatch.await(SCREENSHOT_WAIT_TIME_S, TimeUnit.SECONDS);
} catch (Exception e) {
}
return mDisplayHash;
}
public int getError() {
try {
mCountDownLatch.await(SCREENSHOT_WAIT_TIME_S, TimeUnit.SECONDS);
} catch (Exception e) {
}
return mError;
}
@Override
public void onDisplayHashResult(@NonNull DisplayHash displayHash) {
mDisplayHash = displayHash;
mCountDownLatch.countDown();
}
@Override
public void onDisplayHashError(int errorCode) {
mError = errorCode;
mCountDownLatch.countDown();
}
}
}