1 /* 2 * Copyright (C) 2018 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 com.android.internal.widget; 18 19 import static android.view.DisplayCutout.NO_CUTOUT; 20 import static android.view.View.MeasureSpec.EXACTLY; 21 import static android.view.View.MeasureSpec.makeMeasureSpec; 22 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 23 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 24 25 import static org.hamcrest.CoreMatchers.nullValue; 26 import static org.hamcrest.Matchers.is; 27 import static org.junit.Assert.assertNotNull; 28 import static org.junit.Assert.assertThat; 29 30 import android.content.Context; 31 import android.graphics.Insets; 32 import android.graphics.Rect; 33 import android.platform.test.annotations.Presubmit; 34 import android.view.DisplayCutout; 35 import android.view.View; 36 import android.view.View.OnApplyWindowInsetsListener; 37 import android.view.ViewGroup; 38 import android.view.WindowInsets; 39 import android.widget.FrameLayout; 40 import android.widget.Toolbar; 41 42 import androidx.test.InstrumentationRegistry; 43 import androidx.test.filters.SmallTest; 44 import androidx.test.runner.AndroidJUnit4; 45 46 import org.junit.Before; 47 import org.junit.Test; 48 import org.junit.runner.RunWith; 49 50 import java.lang.reflect.Field; 51 52 @RunWith(AndroidJUnit4.class) 53 @SmallTest 54 @Presubmit 55 public class ActionBarOverlayLayoutTest { 56 57 private static final Insets TOP_INSET_5 = Insets.of(0, 5, 0, 0); 58 private static final Insets TOP_INSET_25 = Insets.of(0, 25, 0, 0); 59 private static final DisplayCutout CONSUMED_CUTOUT = null; 60 private static final DisplayCutout CUTOUT_5 = new DisplayCutout( 61 TOP_INSET_5, 62 null /* boundLeft */, 63 new Rect(100, 0, 200, 5), 64 null /* boundRight */, 65 null /* boundBottom*/); 66 private static final int EXACTLY_1000 = makeMeasureSpec(1000, EXACTLY); 67 68 private Context mContext; 69 private TestActionBarOverlayLayout mLayout; 70 71 private ViewGroup mContent; 72 private ViewGroup mActionBarTop; 73 private ViewGroup mActionBarView; 74 private Toolbar mToolbar; 75 private FakeOnApplyWindowListener mContentInsetsListener; 76 77 78 @Before setUp()79 public void setUp() throws Exception { 80 mContext = InstrumentationRegistry.getContext(); 81 mLayout = new TestActionBarOverlayLayout(mContext); 82 mLayout.makeOptionalFitsSystemWindows(); 83 84 mContent = createViewGroupWithId(com.android.internal.R.id.content); 85 mContent.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 86 mLayout.addView(mContent); 87 88 mContentInsetsListener = new FakeOnApplyWindowListener(); 89 mContent.setOnApplyWindowInsetsListener(mContentInsetsListener); 90 91 // mActionBarView and mToolbar are supposed to be the same view. Here makes mToolbar a child 92 // of mActionBarView is to control the height of mActionBarView. In this way, the child 93 // views of mToolbar won't affect the measurement of mActionBarView or mActionBarTop. 94 mActionBarView = new FrameLayout(mContext); 95 mActionBarView.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, 20)); 96 97 mToolbar = new Toolbar(mContext); 98 mToolbar.setId(com.android.internal.R.id.action_bar); 99 mActionBarView.addView(mToolbar); 100 101 mActionBarTop = new ActionBarContainer(mContext); 102 mActionBarTop.setId(com.android.internal.R.id.action_bar_container); 103 mActionBarTop.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)); 104 mActionBarTop.addView(mActionBarView); 105 mLayout.addView(mActionBarTop); 106 mLayout.setActionBarHeight(20); 107 } 108 109 @Test topInset_consumedCutout_stable()110 public void topInset_consumedCutout_stable() { 111 mLayout.setStable(true); 112 mLayout.dispatchApplyWindowInsets(insetsWith(TOP_INSET_5, CONSUMED_CUTOUT)); 113 114 assertThat(mContentInsetsListener.captured, nullValue()); 115 116 mLayout.measure(EXACTLY_1000, EXACTLY_1000); 117 118 // Action bar height is added to the top inset 119 assertThat(mContentInsetsListener.captured, is(insetsWith(TOP_INSET_25, CONSUMED_CUTOUT))); 120 } 121 122 @Test topInset_consumedCutout_notStable()123 public void topInset_consumedCutout_notStable() { 124 mLayout.dispatchApplyWindowInsets(insetsWith(TOP_INSET_5, CONSUMED_CUTOUT)); 125 126 assertThat(mContentInsetsListener.captured, nullValue()); 127 128 mLayout.measure(EXACTLY_1000, EXACTLY_1000); 129 130 assertThat(mContentInsetsListener.captured, is(insetsWith(Insets.NONE, CONSUMED_CUTOUT))); 131 } 132 133 @Test topInset_noCutout_stable()134 public void topInset_noCutout_stable() { 135 mLayout.setStable(true); 136 mLayout.dispatchApplyWindowInsets(insetsWith(TOP_INSET_5, NO_CUTOUT)); 137 138 assertThat(mContentInsetsListener.captured, nullValue()); 139 140 mLayout.measure(EXACTLY_1000, EXACTLY_1000); 141 142 // Action bar height is added to the top inset 143 assertThat(mContentInsetsListener.captured, is(insetsWith(TOP_INSET_25, NO_CUTOUT))); 144 } 145 146 @Test topInset_noCutout_notStable()147 public void topInset_noCutout_notStable() { 148 mLayout.dispatchApplyWindowInsets(insetsWith(TOP_INSET_5, NO_CUTOUT)); 149 150 assertThat(mContentInsetsListener.captured, nullValue()); 151 152 mLayout.measure(EXACTLY_1000, EXACTLY_1000); 153 154 assertThat(mContentInsetsListener.captured, is(insetsWith(Insets.NONE, NO_CUTOUT))); 155 } 156 157 @Test topInset_cutout_stable()158 public void topInset_cutout_stable() { 159 mLayout.setStable(true); 160 mLayout.dispatchApplyWindowInsets(insetsWith(TOP_INSET_5, CUTOUT_5)); 161 162 assertThat(mContentInsetsListener.captured, nullValue()); 163 164 mLayout.measure(EXACTLY_1000, EXACTLY_1000); 165 166 // Action bar height is added to the top inset 167 assertThat(mContentInsetsListener.captured, is(insetsWith(TOP_INSET_25, CUTOUT_5))); 168 } 169 170 @Test topInset_cutout_notStable()171 public void topInset_cutout_notStable() { 172 mLayout.dispatchApplyWindowInsets(insetsWith(TOP_INSET_5, CUTOUT_5)); 173 174 assertThat(mContentInsetsListener.captured, nullValue()); 175 176 mLayout.measure(EXACTLY_1000, EXACTLY_1000); 177 178 assertThat(mContentInsetsListener.captured, is(insetsWith(Insets.NONE, NO_CUTOUT))); 179 } 180 181 @Test topInset_cutout_noContentOnApplyWindowInsetsListener()182 public void topInset_cutout_noContentOnApplyWindowInsetsListener() { 183 mLayout.setHasContentOnApplyWindowInsetsListener(false); 184 mLayout.dispatchApplyWindowInsets(insetsWith(TOP_INSET_5, CUTOUT_5)); 185 186 assertThat(mContentInsetsListener.captured, nullValue()); 187 188 mLayout.measure(EXACTLY_1000, EXACTLY_1000); 189 190 // Action bar height is added to the top inset 191 assertThat(mContentInsetsListener.captured, is(insetsWith(TOP_INSET_25, CUTOUT_5))); 192 } 193 194 @Test topInset_cutout__hasContentOnApplyWindowInsetsListener()195 public void topInset_cutout__hasContentOnApplyWindowInsetsListener() { 196 mLayout.setHasContentOnApplyWindowInsetsListener(true); 197 mLayout.dispatchApplyWindowInsets(insetsWith(TOP_INSET_5, CUTOUT_5)); 198 199 assertThat(mContentInsetsListener.captured, nullValue()); 200 201 mLayout.measure(EXACTLY_1000, EXACTLY_1000); 202 203 assertThat(mContentInsetsListener.captured, is(insetsWith(Insets.NONE, NO_CUTOUT))); 204 } 205 206 @Test topInset_noCutout_noContentOnApplyWindowInsetsListener()207 public void topInset_noCutout_noContentOnApplyWindowInsetsListener() { 208 mLayout.setHasContentOnApplyWindowInsetsListener(false); 209 mLayout.dispatchApplyWindowInsets(insetsWith(TOP_INSET_5, NO_CUTOUT)); 210 211 assertThat(mContentInsetsListener.captured, nullValue()); 212 213 mLayout.measure(EXACTLY_1000, EXACTLY_1000); 214 215 // Action bar height is added to the top inset 216 assertThat(mContentInsetsListener.captured, is(insetsWith(TOP_INSET_25, NO_CUTOUT))); 217 } 218 219 @Test topInset_noCutout__hasContentOnApplyWindowInsetsListener()220 public void topInset_noCutout__hasContentOnApplyWindowInsetsListener() { 221 mLayout.setHasContentOnApplyWindowInsetsListener(true); 222 mLayout.dispatchApplyWindowInsets(insetsWith(TOP_INSET_5, NO_CUTOUT)); 223 224 assertThat(mContentInsetsListener.captured, nullValue()); 225 226 mLayout.measure(EXACTLY_1000, EXACTLY_1000); 227 228 assertThat(mContentInsetsListener.captured, is(insetsWith(Insets.NONE, NO_CUTOUT))); 229 } 230 insetsWith(Insets content, DisplayCutout cutout)231 private WindowInsets insetsWith(Insets content, DisplayCutout cutout) { 232 final Insets cutoutInsets = cutout != null 233 ? Insets.of(cutout.getSafeInsets()) 234 : Insets.NONE; 235 return new WindowInsets.Builder() 236 .setSystemWindowInsets(content) 237 .setDisplayCutout(cutout) 238 .setInsets(WindowInsets.Type.displayCutout(), cutoutInsets) 239 .setInsetsIgnoringVisibility(WindowInsets.Type.displayCutout(), cutoutInsets) 240 .setVisible(WindowInsets.Type.displayCutout(), true) 241 .build(); 242 } 243 createViewGroupWithId(int id)244 private ViewGroup createViewGroupWithId(int id) { 245 final FrameLayout v = new FrameLayout(mContext); 246 v.setId(id); 247 return v; 248 } 249 250 static class TestActionBarOverlayLayout extends ActionBarOverlayLayout { 251 private boolean mStable; 252 private boolean mHasContentOnApplyWindowInsetsListener; 253 TestActionBarOverlayLayout(Context context)254 public TestActionBarOverlayLayout(Context context) { 255 super(context); 256 mHasContentOnApplyWindowInsetsListener = true; 257 } 258 259 @Override computeSystemWindowInsets(WindowInsets in, Rect outLocalInsets)260 public WindowInsets computeSystemWindowInsets(WindowInsets in, Rect outLocalInsets) { 261 if (mStable || !hasContentOnApplyWindowInsetsListener()) { 262 // Emulate the effect of makeOptionalFitsSystemWindows, because we can't do that 263 // without being attached to a window. 264 outLocalInsets.setEmpty(); 265 return in; 266 } 267 return super.computeSystemWindowInsets(in, outLocalInsets); 268 } 269 setStable(boolean stable)270 void setStable(boolean stable) { 271 mStable = stable; 272 setSystemUiVisibility(stable ? SYSTEM_UI_FLAG_LAYOUT_STABLE : 0); 273 } 274 setHasContentOnApplyWindowInsetsListener(boolean hasListener)275 void setHasContentOnApplyWindowInsetsListener(boolean hasListener) { 276 mHasContentOnApplyWindowInsetsListener = hasListener; 277 } 278 279 @Override hasContentOnApplyWindowInsetsListener()280 protected boolean hasContentOnApplyWindowInsetsListener() { 281 return mHasContentOnApplyWindowInsetsListener; 282 } 283 284 @Override getWindowSystemUiVisibility()285 public int getWindowSystemUiVisibility() { 286 return getSystemUiVisibility(); 287 } 288 setActionBarHeight(int height)289 void setActionBarHeight(int height) { 290 try { 291 final Field field = ActionBarOverlayLayout.class.getDeclaredField( 292 "mActionBarHeight"); 293 field.setAccessible(true); 294 field.setInt(this, height); 295 } catch (ReflectiveOperationException e) { 296 throw new RuntimeException(e); 297 } 298 } 299 } 300 301 static class FakeOnApplyWindowListener implements OnApplyWindowInsetsListener { 302 WindowInsets captured; 303 304 @Override onApplyWindowInsets(View v, WindowInsets insets)305 public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { 306 assertNotNull(insets); 307 captured = insets; 308 return v.onApplyWindowInsets(insets); 309 } 310 } 311 } 312