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