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 android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND;
20 import static android.view.inputmethod.cts.util.TestUtils.getOnMainSync;
21 
22 import android.app.AlertDialog;
23 import android.app.Instrumentation;
24 import android.content.Intent;
25 import android.graphics.Bitmap;
26 import android.graphics.Point;
27 import android.graphics.Rect;
28 import android.util.Size;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.WindowInsets;
32 import android.widget.TextView;
33 
34 import androidx.annotation.ColorInt;
35 import androidx.annotation.NonNull;
36 import androidx.annotation.Nullable;
37 import androidx.test.platform.app.InstrumentationRegistry;
38 
39 import java.util.concurrent.TimeUnit;
40 import java.util.concurrent.atomic.AtomicReference;
41 
42 /**
43  * A utility class to write tests that depend on some capabilities related to navigation bar.
44  */
45 public class NavigationBarInfo {
46     private static final long BEFORE_SCREENSHOT_WAIT = TimeUnit.SECONDS.toMillis(1);
47 
48     private final boolean mHasBottomNavigationBar;
49     private final int mBottomNavigationBerHeight;
50     private final boolean mSupportsNavigationBarColor;
51     private final boolean mSupportsLightNavigationBar;
52     private final boolean mSupportsDimmingWindowLightNavigationBarOverride;
53 
NavigationBarInfo(boolean hasBottomNavigationBar, int bottomNavigationBerHeight, boolean supportsNavigationBarColor, boolean supportsLightNavigationBar, boolean supportsDimmingWindowLightNavigationBarOverride)54     private NavigationBarInfo(boolean hasBottomNavigationBar, int bottomNavigationBerHeight,
55             boolean supportsNavigationBarColor, boolean supportsLightNavigationBar,
56             boolean supportsDimmingWindowLightNavigationBarOverride) {
57         mHasBottomNavigationBar = hasBottomNavigationBar;
58         mBottomNavigationBerHeight = bottomNavigationBerHeight;
59         mSupportsNavigationBarColor = supportsNavigationBarColor;
60         mSupportsLightNavigationBar = supportsLightNavigationBar;
61         mSupportsDimmingWindowLightNavigationBarOverride =
62                 supportsDimmingWindowLightNavigationBarOverride;
63     }
64 
65     @Nullable
66     private static NavigationBarInfo sInstance;
67 
68     /**
69      * Returns a {@link NavigationBarInfo} instance.
70      *
71      * <p>As a performance optimizations, this method internally caches the previous result and
72      * returns the same result if this gets called multiple times.</p>
73      *
74      * <p>Note: The caller should be aware that this method may launch {@link TestActivity}
75      * internally.</p>
76      *
77      * @return {@link NavigationBarInfo} obtained with {@link TestActivity}.
78      */
79     @NonNull
getInstance()80     public static NavigationBarInfo getInstance() throws Exception {
81         if (sInstance != null) {
82             return sInstance;
83         }
84 
85         final int actualBottomInset;
86         {
87             final AtomicReference<View> viewRef = new AtomicReference<>();
88             new TestActivity.Starter()
89                     .withAdditionalFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
90                     .startSync(activity -> {
91                         final View view = new View(activity);
92                         view.setLayoutParams(new ViewGroup.LayoutParams(
93                                 ViewGroup.LayoutParams.MATCH_PARENT,
94                                 ViewGroup.LayoutParams.MATCH_PARENT));
95                         viewRef.set(view);
96                         return view;
97                     }, TestActivity.class);
98 
99             final View view = viewRef.get();
100 
101             final WindowInsets windowInsets = getOnMainSync(() -> view.getRootWindowInsets());
102             if (!windowInsets.hasStableInsets() || windowInsets.getStableInsetBottom() <= 0) {
103                 return new NavigationBarInfo(false, 0, false, false, false);
104             }
105             final Size displaySize = getOnMainSync(() -> {
106                 final Point size = new Point();
107                 view.getDisplay().getRealSize(size);
108                 return new Size(size.x, size.y);
109             });
110 
111             final Rect viewBoundsOnScreen = getOnMainSync(() -> {
112                 final int[] xy = new int[2];
113                 view.getLocationOnScreen(xy);
114                 final int x = xy[0];
115                 final int y = xy[1];
116                 return new Rect(x, y, x + view.getWidth(), y + view.getHeight());
117             });
118             actualBottomInset = displaySize.getHeight() - viewBoundsOnScreen.bottom;
119             if (actualBottomInset != windowInsets.getStableInsetBottom()) {
120                 sInstance = new NavigationBarInfo(false, 0, false, false, false);
121                 return sInstance;
122             }
123         }
124 
125         final boolean colorSupported = NavigationBarColorVerifier.verify(
126                 color -> getBottomNavigationBarBitmapForActivity(
127                         color, false /* lightNavigationBar */, actualBottomInset,
128                         false /* showDimmingDialog */)).getResult()
129                 == NavigationBarColorVerifier.ResultType.SUPPORTED;
130 
131         final boolean lightModeSupported = LightNavigationBarVerifier.verify(
132                 (color, lightNavigationBar) -> getBottomNavigationBarBitmapForActivity(
133                         color, lightNavigationBar, actualBottomInset,
134                         false /* showDimmingDialog */)).getResult()
135                 == LightNavigationBarVerifier.ResultType.SUPPORTED;
136 
137         final boolean dimmingSupported = lightModeSupported && LightNavigationBarVerifier.verify(
138                 (color, lightNavigationBar) -> getBottomNavigationBarBitmapForActivity(
139                         color, lightNavigationBar, actualBottomInset,
140                         true /* showDimmingDialog */)).getResult()
141                 == LightNavigationBarVerifier.ResultType.SUPPORTED;
142 
143         sInstance = new NavigationBarInfo(
144                 true, actualBottomInset, colorSupported, lightModeSupported, dimmingSupported);
145         return sInstance;
146     }
147 
148     @NonNull
getBottomNavigationBarBitmapForActivity( @olorInt int navigationBarColor, boolean lightNavigationBar, int bottomNavigationBarHeight, boolean showDimmingDialog)149     private static Bitmap getBottomNavigationBarBitmapForActivity(
150             @ColorInt int navigationBarColor, boolean lightNavigationBar,
151             int bottomNavigationBarHeight, boolean showDimmingDialog) throws Exception {
152         final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
153 
154 
155         final TestActivity testActivity = TestActivity.startSync(activity -> {
156             final View view = new View(activity);
157             activity.getWindow().setNavigationBarColor(navigationBarColor);
158 
159             // Set/unset SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR if necessary.
160             final int currentVis = view.getSystemUiVisibility();
161             final int newVis = (currentVis & ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR)
162                     | (lightNavigationBar ? View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR : 0);
163             if (currentVis != newVis) {
164                 view.setSystemUiVisibility(newVis);
165             }
166 
167             view.setLayoutParams(new ViewGroup.LayoutParams(
168                     ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
169             return view;
170         });
171         instrumentation.waitForIdleSync();
172 
173         final AlertDialog dialog;
174         if (showDimmingDialog) {
175             dialog = getOnMainSync(() -> {
176                 final TextView textView = new TextView(testActivity);
177                 textView.setText("Dimming Window");
178                 final AlertDialog alertDialog = new AlertDialog.Builder(testActivity)
179                         .setView(textView)
180                         .create();
181                 alertDialog.getWindow().setFlags(FLAG_DIM_BEHIND, FLAG_DIM_BEHIND);
182                 alertDialog.show();
183                 return alertDialog;
184             });
185         } else {
186             dialog = null;
187         }
188 
189         Thread.sleep(BEFORE_SCREENSHOT_WAIT);
190 
191         final Bitmap fullBitmap = instrumentation.getUiAutomation().takeScreenshot();
192         final Bitmap bottomNavBarBitmap = Bitmap.createBitmap(fullBitmap, 0,
193                 fullBitmap.getHeight() - bottomNavigationBarHeight, fullBitmap.getWidth(),
194                 bottomNavigationBarHeight);
195         if (dialog != null) {
196             // Dialog#dismiss() is a thread safe method so we don't need to call this from the UI
197             // thread.
198             dialog.dismiss();
199         }
200         return bottomNavBarBitmap;
201     }
202 
203     /**
204      * @return {@code true} if this device seems to have bottom navigation bar.
205      */
hasBottomNavigationBar()206     public boolean hasBottomNavigationBar() {
207         return mHasBottomNavigationBar;
208     }
209 
210     /**
211      * @return height of the bottom navigation bar. Valid only when
212      *         {@link #hasBottomNavigationBar()} returns {@code true}
213      */
getBottomNavigationBerHeight()214     public int getBottomNavigationBerHeight() {
215         return mBottomNavigationBerHeight;
216     }
217 
218     /**
219      * @return {@code true} if {@link android.view.Window#setNavigationBarColor(int)} seem to take
220      *         effect on this device. Valid only when {@link #hasBottomNavigationBar()} returns
221      *         {@code true}
222      */
supportsNavigationBarColor()223     public boolean supportsNavigationBarColor() {
224         return mSupportsNavigationBarColor;
225     }
226 
227     /**
228      * @return {@code true} if {@link android.view.View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} seem to
229      *         take effect on this device. Valid only when {@link #hasBottomNavigationBar()} returns
230      *         {@code true}
231      */
supportsLightNavigationBar()232     public boolean supportsLightNavigationBar() {
233         return mSupportsLightNavigationBar;
234     }
235 
236     /**
237      * @return {@code true} if {@link android.view.View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} will be
238      *         canceled when a {@link android.view.Window} with
239      *         {@link android.view.WindowManager.LayoutParams#FLAG_DIM_BEHIND} is shown.
240      */
supportsDimmingWindowLightNavigationBarOverride()241     public boolean supportsDimmingWindowLightNavigationBarOverride() {
242         return mSupportsDimmingWindowLightNavigationBarOverride;
243     }
244 }
245