Home | History | Annotate | Download | only in util
      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.support.test.InstrumentationRegistry.getInstrumentation;
     20 import static android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND;
     21 import static android.view.inputmethod.cts.util.TestUtils.getOnMainSync;
     22 
     23 import android.app.AlertDialog;
     24 import android.app.Instrumentation;
     25 import android.content.Intent;
     26 import android.graphics.Bitmap;
     27 import android.graphics.Point;
     28 import android.graphics.Rect;
     29 import androidx.annotation.ColorInt;
     30 import androidx.annotation.NonNull;
     31 import androidx.annotation.Nullable;
     32 import android.support.test.InstrumentationRegistry;
     33 import android.util.Size;
     34 import android.view.View;
     35 import android.view.ViewGroup;
     36 import android.view.WindowInsets;
     37 import android.widget.TextView;
     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 
     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
     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             TestActivity.startSync(activity -> {
     89                 final View view = new View(activity);
     90                 view.setLayoutParams(new ViewGroup.LayoutParams(
     91                         ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
     92                 viewRef.set(view);
     93                 return view;
     94             }, Intent.FLAG_ACTIVITY_NO_ANIMATION);
     95 
     96             final View view = viewRef.get();
     97 
     98             final WindowInsets windowInsets = getOnMainSync(() -> view.getRootWindowInsets());
     99             if (!windowInsets.hasStableInsets() || windowInsets.getStableInsetBottom() <= 0) {
    100                 return new NavigationBarInfo(false, 0, false, false, false);
    101             }
    102             final Size displaySize = getOnMainSync(() -> {
    103                 final Point size = new Point();
    104                 view.getDisplay().getRealSize(size);
    105                 return new Size(size.x, size.y);
    106             });
    107 
    108             final Rect viewBoundsOnScreen = getOnMainSync(() -> {
    109                 final int[] xy = new int[2];
    110                 view.getLocationOnScreen(xy);
    111                 final int x = xy[0];
    112                 final int y = xy[1];
    113                 return new Rect(x, y, x + view.getWidth(), y + view.getHeight());
    114             });
    115             actualBottomInset = displaySize.getHeight() - viewBoundsOnScreen.bottom;
    116             if (actualBottomInset != windowInsets.getStableInsetBottom()) {
    117                 sInstance = new NavigationBarInfo(false, 0, false, false, false);
    118                 return sInstance;
    119             }
    120         }
    121 
    122         final boolean colorSupported = NavigationBarColorVerifier.verify(
    123                 color -> getBottomNavigationBarBitmapForActivity(
    124                         color, false /* lightNavigationBar */, actualBottomInset,
    125                         false /* showDimmingDialog */)).getResult()
    126                 == NavigationBarColorVerifier.ResultType.SUPPORTED;
    127 
    128         final boolean lightModeSupported = LightNavigationBarVerifier.verify(
    129                 (color, lightNavigationBar) -> getBottomNavigationBarBitmapForActivity(
    130                         color, lightNavigationBar, actualBottomInset,
    131                         false /* showDimmingDialog */)).getResult()
    132                 == LightNavigationBarVerifier.ResultType.SUPPORTED;
    133 
    134         final boolean dimmingSupported = lightModeSupported && LightNavigationBarVerifier.verify(
    135                 (color, lightNavigationBar) -> getBottomNavigationBarBitmapForActivity(
    136                         color, lightNavigationBar, actualBottomInset,
    137                         true /* showDimmingDialog */)).getResult()
    138                 == LightNavigationBarVerifier.ResultType.NOT_SUPPORTED;
    139 
    140         sInstance = new NavigationBarInfo(
    141                 true, actualBottomInset, colorSupported, lightModeSupported, dimmingSupported);
    142         return sInstance;
    143     }
    144 
    145     @NonNull
    146     private static Bitmap getBottomNavigationBarBitmapForActivity(
    147             @ColorInt int navigationBarColor, boolean lightNavigationBar,
    148             int bottomNavigationBarHeight, boolean showDimmingDialog) throws Exception {
    149         final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
    150 
    151 
    152         final TestActivity testActivity = TestActivity.startSync(activity -> {
    153             final View view = new View(activity);
    154             activity.getWindow().setNavigationBarColor(navigationBarColor);
    155 
    156             // Set/unset SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR if necessary.
    157             final int currentVis = view.getSystemUiVisibility();
    158             final int newVis = (currentVis & ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR)
    159                     | (lightNavigationBar ? View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR : 0);
    160             if (currentVis != newVis) {
    161                 view.setSystemUiVisibility(newVis);
    162             }
    163 
    164             view.setLayoutParams(new ViewGroup.LayoutParams(
    165                     ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
    166             return view;
    167         });
    168         instrumentation.waitForIdleSync();
    169 
    170         final AlertDialog dialog;
    171         if (showDimmingDialog) {
    172             dialog = getOnMainSync(() -> {
    173                 final TextView textView = new TextView(testActivity);
    174                 textView.setText("Dimming Window");
    175                 final AlertDialog alertDialog = new AlertDialog.Builder(testActivity)
    176                         .setView(textView)
    177                         .create();
    178                 alertDialog.getWindow().setFlags(FLAG_DIM_BEHIND, FLAG_DIM_BEHIND);
    179                 alertDialog.show();
    180                 return alertDialog;
    181             });
    182         } else {
    183             dialog = null;
    184         }
    185 
    186         Thread.sleep(BEFORE_SCREENSHOT_WAIT);
    187 
    188         final Bitmap fullBitmap = getInstrumentation().getUiAutomation().takeScreenshot();
    189         final Bitmap bottomNavBarBitmap = Bitmap.createBitmap(fullBitmap, 0,
    190                 fullBitmap.getHeight() - bottomNavigationBarHeight, fullBitmap.getWidth(),
    191                 bottomNavigationBarHeight);
    192         if (dialog != null) {
    193             // Dialog#dismiss() is a thread safe method so we don't need to call this from the UI
    194             // thread.
    195             dialog.dismiss();
    196         }
    197         return bottomNavBarBitmap;
    198     }
    199 
    200     /**
    201      * @return {@code true} if this device seems to have bottom navigation bar.
    202      */
    203     public boolean hasBottomNavigationBar() {
    204         return mHasBottomNavigationBar;
    205     }
    206 
    207     /**
    208      * @return height of the bottom navigation bar. Valid only when
    209      *         {@link #hasBottomNavigationBar()} returns {@code true}
    210      */
    211     public int getBottomNavigationBerHeight() {
    212         return mBottomNavigationBerHeight;
    213     }
    214 
    215     /**
    216      * @return {@code true} if {@link android.view.Window#setNavigationBarColor(int)} seem to take
    217      *         effect on this device. Valid only when {@link #hasBottomNavigationBar()} returns
    218      *         {@code true}
    219      */
    220     public boolean supportsNavigationBarColor() {
    221         return mSupportsNavigationBarColor;
    222     }
    223 
    224     /**
    225      * @return {@code true} if {@link android.view.View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} seem to
    226      *         take effect on this device. Valid only when {@link #hasBottomNavigationBar()} returns
    227      *         {@code true}
    228      */
    229     public boolean supportsLightNavigationBar() {
    230         return mSupportsLightNavigationBar;
    231     }
    232 
    233     /**
    234      * @return {@code true} if {@link android.view.View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} will be
    235      *         canceled when a {@link android.view.Window} with
    236      *         {@link android.view.WindowManager.LayoutParams#FLAG_DIM_BEHIND} is shown.
    237      */
    238     public boolean supportsDimmingWindowLightNavigationBarOverride() {
    239         return mSupportsDimmingWindowLightNavigationBarOverride;
    240     }
    241 }
    242