1 /*
2  * Copyright (C) 2017 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.view.inputmethod.cts.util;
18 
19 import static org.junit.Assert.assertEquals;
20 
21 import android.graphics.Bitmap;
22 import android.graphics.Color;
23 import android.util.SparseIntArray;
24 
25 import androidx.annotation.ColorInt;
26 import androidx.annotation.NonNull;
27 import androidx.annotation.Nullable;
28 
29 import java.util.Arrays;
30 import java.util.OptionalDouble;
31 import java.util.function.Supplier;
32 
33 /**
34  * A utility class to evaluate if {@link android.view.View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} is
35  * supported on the device or not.
36  */
37 public class LightNavigationBarVerifier {
38 
39     /** This value actually does not have strong rationale. */
40     private static final float LIGHT_NAVBAR_SUPPORTED_THRESHOLD = 20.0f;
41 
42     /** This value actually does not have strong rationale. */
43     private static final float LIGHT_NAVBAR_NOT_SUPPORTED_THRESHOLD = 5.0f;
44 
45     @FunctionalInterface
46     public interface ScreenshotSupplier {
47         @NonNull
takeScreenshot(@olorInt int navigationBarColor, boolean lightMode)48         Bitmap takeScreenshot(@ColorInt int navigationBarColor, boolean lightMode) throws Exception;
49     }
50 
51     public enum ResultType {
52         /**
53          * {@link android.view.View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} seems to be not supported.
54          */
55         NOT_SUPPORTED,
56         /**
57          * {@link android.view.View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} seems to be supported.
58          */
59         SUPPORTED,
60         /**
61          * Not sure if {@link android.view.View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} is supported.
62          */
63         UNKNOWN,
64     }
65 
66     static final class Result {
67         @NonNull
68         public final ResultType mResult;
69         @NonNull
70         public final Supplier<String> mAssertionMessageProvider;
71 
Result(@onNull ResultType result, @Nullable Supplier<String> assertionMessageProvider)72         Result(@NonNull ResultType result,
73                 @Nullable Supplier<String> assertionMessageProvider) {
74             mResult = result;
75             mAssertionMessageProvider = assertionMessageProvider;
76         }
77 
78         @NonNull
getResult()79         public ResultType getResult() {
80             return mResult;
81         }
82 
83         @NonNull
getAssertionMessage()84         public String getAssertionMessage() {
85             return mAssertionMessageProvider.get();
86         }
87     }
88 
89     /**
90      * Asserts that {@link android.view.View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} is supported on
91      * this device.
92      *
93      * @param screenshotSupplier callback to provide {@link Bitmap} of the navigation bar region
94      */
expectLightNavigationBarSupported( @onNull ScreenshotSupplier screenshotSupplier)95     public static void expectLightNavigationBarSupported(
96             @NonNull ScreenshotSupplier screenshotSupplier) throws Exception {
97         final Result result = verify(screenshotSupplier);
98         assertEquals(result.getAssertionMessage(), ResultType.SUPPORTED, result.getResult());
99     }
100 
101     /**
102      * Asserts that {@link android.view.View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} is not supported
103      * on this device.
104      *
105      * @param screenshotSupplier callback to provide {@link Bitmap} of the navigation bar region
106      */
expectLightNavigationBarNotSupported( @onNull ScreenshotSupplier screenshotSupplier)107     public static void expectLightNavigationBarNotSupported(
108             @NonNull ScreenshotSupplier screenshotSupplier) throws Exception {
109         final Result result = verify(screenshotSupplier);
110         assertEquals(result.getAssertionMessage(), ResultType.NOT_SUPPORTED, result.getResult());
111     }
112 
113     @FunctionalInterface
114     private interface ColorOperator {
operate(@olorInt int color1, @ColorInt int color2)115         int operate(@ColorInt int color1, @ColorInt int color2);
116     }
117 
operateColorArrays(@onNull int[] pixels1, @NonNull int[] pixels2, @NonNull ColorOperator operator)118     private static int[] operateColorArrays(@NonNull int[] pixels1, @NonNull int[] pixels2,
119             @NonNull ColorOperator operator) {
120         assertEquals(pixels1.length, pixels2.length);
121         final int numPixels = pixels1.length;
122         final int[] result = new int[numPixels];
123         for (int i = 0; i < numPixels; ++i) {
124             result[i] = operator.operate(pixels1[i], pixels2[i]);
125         }
126         return result;
127     }
128 
129     @NonNull
getPixels(@onNull Bitmap bitmap)130     private static int[] getPixels(@NonNull Bitmap bitmap) {
131         final int width = bitmap.getWidth();
132         final int height = bitmap.getHeight();
133         final int[] pixels = new int[width * height];
134         bitmap.getPixels(pixels, 0 /* offset */, width /* stride */, 0 /* x */, 0 /* y */,
135                 width, height);
136         return pixels;
137     }
138 
139     @NonNull
verify( @onNull ScreenshotSupplier screenshotSupplier)140     static Result verify(
141             @NonNull ScreenshotSupplier screenshotSupplier) throws Exception {
142         final int[] darkNavBarPixels = getPixels(
143                 screenshotSupplier.takeScreenshot(Color.BLACK, false));
144         final int[] lightNavBarPixels = getPixels(
145                 screenshotSupplier.takeScreenshot(Color.BLACK, true));
146 
147         if (darkNavBarPixels.length != lightNavBarPixels.length) {
148             throw new IllegalStateException("Pixel count mismatch."
149                     + " dark=" + darkNavBarPixels.length + " light=" + lightNavBarPixels.length);
150         }
151 
152         final int[][] channelDiffs = new int[][] {
153                 operateColorArrays(darkNavBarPixels, lightNavBarPixels,
154                         (dark, light) -> Color.red(dark) - Color.red(light)),
155                 operateColorArrays(darkNavBarPixels, lightNavBarPixels,
156                         (dark, light) -> Color.green(dark) - Color.green(light)),
157                 operateColorArrays(darkNavBarPixels, lightNavBarPixels,
158                         (dark, light) -> Color.blue(dark) - Color.blue(light)),
159         };
160 
161         if (Arrays.stream(channelDiffs).allMatch(
162                 diffs -> Arrays.stream(diffs).allMatch(diff -> diff == 0))) {
163             // Exactly the same image.  Safe to conclude that light navigation bar is not supported.
164             return new Result(ResultType.NOT_SUPPORTED, () -> dumpDiffStreams(channelDiffs));
165         }
166 
167         if (Arrays.stream(channelDiffs).anyMatch(diffs -> {
168             final OptionalDouble average = Arrays.stream(diffs).filter(diff -> diff != 0).average();
169             return average.isPresent() && average.getAsDouble() > LIGHT_NAVBAR_SUPPORTED_THRESHOLD;
170         })) {
171             // If darkNavBarPixels have brighter pixels in at least one color channel
172             // (red, green, blue), we consider that it is because light navigation bar takes effect.
173             // Keep in mind that with SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR navigation bar buttons
174             // are expected to become darker.  So if everything works fine, darkNavBarPixels should
175             // have brighter pixels.
176             return new Result(ResultType.SUPPORTED, () -> dumpDiffStreams(channelDiffs));
177         }
178 
179         if (Arrays.stream(channelDiffs).allMatch(diffs -> {
180             final OptionalDouble average = Arrays.stream(diffs).filter(diff -> diff != 0).average();
181             return average.isPresent()
182                     && Math.abs(average.getAsDouble()) < LIGHT_NAVBAR_NOT_SUPPORTED_THRESHOLD;
183         })) {
184             // If all color channels (red, green, blue) have diffs less than a certain threshold,
185             // consider light navigation bar is not supported.  For instance, some devices may
186             // intentionally add some fluctuations to the navigation bar button colors/positions.
187             return new Result(ResultType.NOT_SUPPORTED, () -> dumpDiffStreams(channelDiffs));
188         }
189 
190         return new Result(ResultType.UNKNOWN, () -> dumpDiffStreams(channelDiffs));
191     }
192 
193     @NonNull
dumpDiffStreams(@onNull int[][] diffStreams)194     private static String dumpDiffStreams(@NonNull int[][] diffStreams) {
195         final String[] channelNames = {"red", "green", "blue"};
196         final StringBuilder sb = new StringBuilder();
197         sb.append("diff histogram: ");
198         for (int i = 0; i < diffStreams.length; ++i) {
199             if (i != 0) {
200                 sb.append(", ");
201             }
202             final int[] channel = diffStreams[i];
203             final SparseIntArray histogram = new SparseIntArray();
204             Arrays.stream(channel).sorted().forEachOrdered(
205                     diff -> histogram.put(diff, histogram.get(diff, 0) + 1));
206             sb.append(channelNames[i]).append(": ").append(histogram);
207         }
208         return sb.toString();
209     }
210 }
211