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