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