Home | History | Annotate | Download | only in wm
      1 /*
      2  * Copyright (C) 2019 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.EnsureBarContrastTest.TestActivity.EXTRA_ENSURE_CONTRAST;
     20 import static android.server.wm.EnsureBarContrastTest.TestActivity.EXTRA_LIGHT_BARS;
     21 import static android.server.wm.EnsureBarContrastTest.TestActivity.backgroundForBar;
     22 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
     23 
     24 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
     25 
     26 import android.app.Activity;
     27 import android.content.Intent;
     28 import android.graphics.Bitmap;
     29 import android.graphics.Color;
     30 import android.graphics.Insets;
     31 import android.graphics.Rect;
     32 import android.graphics.drawable.ColorDrawable;
     33 import android.os.Bundle;
     34 import android.platform.test.annotations.Presubmit;
     35 import android.util.SparseIntArray;
     36 import android.view.View;
     37 import android.view.ViewGroup;
     38 import android.view.WindowInsets;
     39 
     40 import androidx.test.filters.FlakyTest;
     41 import androidx.test.rule.ActivityTestRule;
     42 
     43 import com.android.compatibility.common.util.PollingCheck;
     44 
     45 import org.hamcrest.CustomTypeSafeMatcher;
     46 import org.hamcrest.Description;
     47 import org.hamcrest.Matcher;
     48 import org.junit.Rule;
     49 import org.junit.Test;
     50 import org.junit.rules.ErrorCollector;
     51 import org.junit.rules.RuleChain;
     52 
     53 import java.util.function.Supplier;
     54 
     55 /**
     56  * Tests for Window's setEnsureStatusBarContrastWhenTransparent and
     57  * setEnsureNavigationBarContrastWhenTransparent.
     58  */
     59 @Presubmit
     60 public class EnsureBarContrastTest {
     61 
     62     private final ErrorCollector mErrorCollector = new ErrorCollector();
     63     private final DumpOnFailure mDumper = new DumpOnFailure();
     64     private final ActivityTestRule<TestActivity> mTestActivity =
     65             new ActivityTestRule<>(TestActivity.class, false /* initialTouchMode */,
     66                     false /* launchActivity */);
     67 
     68     @Rule
     69     public final RuleChain mRuleChain = RuleChain
     70             .outerRule(mDumper)
     71             .around(mErrorCollector)
     72             .around(mTestActivity);
     73 
     74     @Test
     75     public void test_ensureContrast_darkBars() {
     76         final boolean lightBars = false;
     77         runTestEnsureContrast(lightBars);
     78     }
     79 
     80     @Test
     81     public void test_ensureContrast_lightBars() {
     82         final boolean lightBars = true;
     83         runTestEnsureContrast(lightBars);
     84     }
     85 
     86     public void runTestEnsureContrast(boolean lightBars) {
     87         TestActivity activity = launchAndWait(mTestActivity, lightBars, true /* ensureContrast */);
     88         for (Bar bar : Bar.BARS) {
     89             Bitmap bitmap = getOnMainSync(() -> activity.screenshotBar(bar, mDumper));
     90 
     91             if (getOnMainSync(() -> activity.barIsTapThrough(bar))) {
     92                 assertThat(bar.name + "Bar is tap through, therefore must NOT be scrimmed.", bitmap,
     93                         hasNoScrim(lightBars));
     94             } else {
     95                 // Bar is NOT tap through, may therefore have a scrim.
     96             }
     97             assertThat(bar.name + "Bar: Ensure contrast was requested, therefore contrast " +
     98                     "must be ensured", bitmap, hasContrast(lightBars));
     99         }
    100     }
    101 
    102     @Test
    103     public void test_dontEnsureContrast_darkBars() {
    104         final boolean lightBars = false;
    105         runTestDontEnsureContrast(lightBars);
    106     }
    107 
    108     @Test
    109     public void test_dontEnsureContrast_lightBars() {
    110         final boolean lightBars = true;
    111         runTestDontEnsureContrast(lightBars);
    112     }
    113 
    114     public void runTestDontEnsureContrast(boolean lightBars) {
    115         TestActivity activity = launchAndWait(mTestActivity, lightBars, false /* ensureContrast */);
    116         for (Bar bar : Bar.BARS) {
    117             Bitmap bitmap = getOnMainSync(() -> activity.screenshotBar(bar, mDumper));
    118 
    119             assertThat(bar.name + "Bar: contrast NOT requested, therefore must NOT be scrimmed.",
    120                     bitmap, hasNoScrim(lightBars));
    121         }
    122     }
    123 
    124     private static Matcher<Bitmap> hasNoScrim(boolean light) {
    125         return new CustomTypeSafeMatcher<Bitmap>(
    126                 "must not have a " + (light ? "light" : "dark") + " scrim") {
    127             @Override
    128             protected boolean matchesSafely(Bitmap actual) {
    129                 int mostFrequentColor = getMostFrequentColor(actual);
    130                 return mostFrequentColor == expectedMostFrequentColor();
    131             }
    132 
    133             @Override
    134             protected void describeMismatchSafely(Bitmap item, Description mismatchDescription) {
    135                 super.describeMismatchSafely(item, mismatchDescription);
    136                 mismatchDescription.appendText(" mostFrequentColor: expected #" +
    137                         Integer.toHexString(expectedMostFrequentColor()) + ", but was #" +
    138                         Integer.toHexString(getMostFrequentColor(item)));
    139             }
    140 
    141             private int expectedMostFrequentColor() {
    142                 return backgroundForBar(light);
    143             }
    144         };
    145     }
    146 
    147     private static Matcher<Bitmap> hasContrast(boolean light) {
    148         return new CustomTypeSafeMatcher<Bitmap>(
    149                 (light ? "light" : "dark") + " bar must have contrast") {
    150             @Override
    151             protected boolean matchesSafely(Bitmap actual) {
    152                 int[] ps = getPixels(actual);
    153                 int bg = backgroundForBar(light);
    154 
    155                 for (int p : ps) {
    156                     if (!sameColor(p, bg)) {
    157                         return true;
    158                     }
    159                 }
    160                 return false;
    161             }
    162 
    163             @Override
    164             protected void describeMismatchSafely(Bitmap item, Description mismatchDescription) {
    165                 super.describeMismatchSafely(item, mismatchDescription);
    166                 mismatchDescription.appendText(" expected some color different from " +
    167                         backgroundForBar(light));
    168             }
    169         };
    170     }
    171 
    172     private static int[] getPixels(Bitmap bitmap) {
    173         int[] pixels = new int[bitmap.getHeight() * bitmap.getWidth()];
    174         bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
    175         return pixels;
    176     }
    177 
    178     private static int getMostFrequentColor(Bitmap bitmap) {
    179         final int[] ps = getPixels(bitmap);
    180         final SparseIntArray count = new SparseIntArray();
    181         for (int p : ps) {
    182             count.put(p, count.get(p) + 1);
    183         }
    184         int max = 0;
    185         for (int i = 0; i < count.size(); i++) {
    186             if (count.valueAt(i) > count.valueAt(max)) {
    187                 max = i;
    188             }
    189         }
    190         return count.keyAt(max);
    191     }
    192 
    193     private <T> void assertThat(String reason, T actual, Matcher<? super T> matcher) {
    194         mErrorCollector.checkThat(reason, actual, matcher);
    195     }
    196 
    197     private <R> R getOnMainSync(Supplier<R> f) {
    198         final Object[] result = new Object[1];
    199         runOnMainSync(() -> result[0] = f.get());
    200         //noinspection unchecked
    201         return (R) result[0];
    202     }
    203 
    204     private void runOnMainSync(Runnable runnable) {
    205         getInstrumentation().runOnMainSync(runnable);
    206     }
    207 
    208     private <T extends TestActivity> T launchAndWait(ActivityTestRule<T> rule, boolean lightBars,
    209             boolean ensureContrast) {
    210         final T activity = rule.launchActivity(new Intent()
    211                 .putExtra(EXTRA_LIGHT_BARS, lightBars)
    212                 .putExtra(EXTRA_ENSURE_CONTRAST, ensureContrast));
    213         PollingCheck.waitFor(activity::isReady);
    214         activity.onEnterAnimationComplete();
    215         return activity;
    216     }
    217 
    218     private static boolean sameColor(int a, int b) {
    219         return Math.abs(Color.alpha(a) - Color.alpha(b)) +
    220                 Math.abs(Color.red(a) - Color.red(b)) +
    221                 Math.abs(Color.green(a) - Color.green(b)) +
    222                 Math.abs(Color.blue(a) - Color.blue(b)) < 10;
    223     }
    224 
    225     public static class TestActivity extends Activity {
    226 
    227         static final String EXTRA_LIGHT_BARS = "extra.light_bars";
    228         static final String EXTRA_ENSURE_CONTRAST = "extra.ensure_contrast";
    229 
    230         private boolean mReady = false;
    231 
    232         @Override
    233         protected void onCreate(Bundle savedInstanceState) {
    234             super.onCreate(savedInstanceState);
    235 
    236             View view = new View(this);
    237             view.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    238 
    239             if (getIntent() != null) {
    240                 boolean lightBars = getIntent().getBooleanExtra(EXTRA_LIGHT_BARS, false);
    241                 boolean ensureContrast = getIntent().getBooleanExtra(EXTRA_ENSURE_CONTRAST, false);
    242 
    243                 // Install the decor
    244                 getWindow().getDecorView();
    245 
    246                 getWindow().setStatusBarContrastEnforced(ensureContrast);
    247                 getWindow().setNavigationBarContrastEnforced(ensureContrast);
    248 
    249                 getWindow().setStatusBarColor(Color.TRANSPARENT);
    250                 getWindow().setNavigationBarColor(Color.TRANSPARENT);
    251                 getWindow().setBackgroundDrawable(new ColorDrawable(backgroundForBar(lightBars)));
    252 
    253                 view.setSystemUiVisibility(lightBars ? (View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
    254                         | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) : 0);
    255             }
    256             setContentView(view);
    257         }
    258 
    259         @Override
    260         public void onEnterAnimationComplete() {
    261             super.onEnterAnimationComplete();
    262             mReady = true;
    263         }
    264 
    265         public boolean isReady() {
    266             return mReady && hasWindowFocus();
    267         }
    268 
    269         static int backgroundForBar(boolean lightBar) {
    270             return lightBar ? Color.BLACK : Color.WHITE;
    271         }
    272 
    273         boolean barIsTapThrough(Bar bar) {
    274             final WindowInsets insets = getWindow().getDecorView().getRootWindowInsets();
    275 
    276             return bar.getInset(insets.getTappableElementInsets())
    277                     < bar.getInset(insets.getSystemWindowInsets());
    278         }
    279 
    280         Bitmap screenshotBar(Bar bar, DumpOnFailure dumper) {
    281             final View dv = getWindow().getDecorView();
    282             final Insets insets = dv.getRootWindowInsets().getSystemWindowInsets();
    283 
    284             Rect r = bar.getLocation(insets,
    285                     new Rect(dv.getLeft(), dv.getTop(), dv.getRight(), dv.getBottom()));
    286 
    287             Bitmap fullBitmap = getInstrumentation().getUiAutomation().takeScreenshot();
    288             dumper.dumpOnFailure("full" + bar.name, fullBitmap);
    289             Bitmap barBitmap = Bitmap.createBitmap(fullBitmap, r.left, r.top, r.width(),
    290                     r.height());
    291             dumper.dumpOnFailure("bar" + bar.name, barBitmap);
    292             return barBitmap;
    293         }
    294     }
    295 
    296     abstract static class Bar {
    297 
    298         static final Bar STATUS = new Bar("Status") {
    299             @Override
    300             int getInset(Insets insets) {
    301                 return insets.top;
    302             }
    303 
    304             @Override
    305             Rect getLocation(Insets insets, Rect screen) {
    306                 final Rect r = new Rect(screen);
    307                 r.bottom = r.top + getInset(insets);
    308                 return r;
    309             }
    310         };
    311 
    312         static final Bar NAVIGATION = new Bar("Navigation") {
    313             @Override
    314             int getInset(Insets insets) {
    315                 return insets.bottom;
    316             }
    317 
    318             @Override
    319             Rect getLocation(Insets insets, Rect screen) {
    320                 final Rect r = new Rect(screen);
    321                 r.top = r.bottom - getInset(insets);
    322                 return r;
    323             }
    324         };
    325 
    326         static final Bar[] BARS = {STATUS, NAVIGATION};
    327 
    328         final String name;
    329 
    330         public Bar(String name) {
    331             this.name = name;
    332         }
    333 
    334         abstract int getInset(Insets insets);
    335 
    336         abstract Rect getLocation(Insets insets, Rect screen);
    337     }
    338 }
    339