BackgroundFallback: Cover all cases where the fallback is needed
Fixes an issue where uncovered regions were not
covered if they are to the right or bottom of the
content view.
Fixes: 78661186
Test: Install test app from bug, enable cuotut, open test app, go to landscape, verify white fallback background is drawn in both landscape and seascape
Test: atest BackgroundFallbackTest
Change-Id: I442f03395a71550a534d64233762aa84002319dd
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index eadefc9..cfc5c4c 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -310,10 +310,8 @@
public void onDraw(Canvas c) {
super.onDraw(c);
- // When we are resizing, we need the fallback background to cover the area where we have our
- // system bar background views as the navigation bar will be hidden during resizing.
- mBackgroundFallback.draw(isResizing() ? this : mContentRoot, mContentRoot, c,
- mWindow.mContentParent);
+ mBackgroundFallback.draw(this, mContentRoot, c, mWindow.mContentParent,
+ mStatusColorViewState.view, mNavigationColorViewState.view);
}
@Override
diff --git a/core/java/com/android/internal/widget/BackgroundFallback.java b/core/java/com/android/internal/widget/BackgroundFallback.java
index 309f80c..2b05f1e 100644
--- a/core/java/com/android/internal/widget/BackgroundFallback.java
+++ b/core/java/com/android/internal/widget/BackgroundFallback.java
@@ -46,8 +46,11 @@
* @param root The view group containing the content.
* @param c The canvas to draw the background onto.
* @param content The view where the actual app content is contained in.
+ * @param coveringView1 A potentially opaque view drawn atop the content
+ * @param coveringView2 A potentially opaque view drawn atop the content
*/
- public void draw(ViewGroup boundsView, ViewGroup root, Canvas c, View content) {
+ public void draw(ViewGroup boundsView, ViewGroup root, Canvas c, View content,
+ View coveringView1, View coveringView2) {
if (!hasFallback()) {
return;
}
@@ -55,6 +58,10 @@
// Draw the fallback in the padding.
final int width = boundsView.getWidth();
final int height = boundsView.getHeight();
+
+ final int rootOffsetX = root.getLeft();
+ final int rootOffsetY = root.getTop();
+
int left = width;
int top = height;
int right = 0;
@@ -71,17 +78,58 @@
((ViewGroup) child).getChildCount() == 0) {
continue;
}
- } else if (child.getVisibility() != View.VISIBLE || childBg == null ||
- childBg.getOpacity() != PixelFormat.OPAQUE) {
+ } else if (child.getVisibility() != View.VISIBLE || !isOpaque(childBg)) {
// Potentially translucent or invisible children don't count, and we assume
// the content view will cover the whole area if we're in a background
// fallback situation.
continue;
}
- left = Math.min(left, child.getLeft());
- top = Math.min(top, child.getTop());
- right = Math.max(right, child.getRight());
- bottom = Math.max(bottom, child.getBottom());
+ left = Math.min(left, rootOffsetX + child.getLeft());
+ top = Math.min(top, rootOffsetY + child.getTop());
+ right = Math.max(right, rootOffsetX + child.getRight());
+ bottom = Math.max(bottom, rootOffsetY + child.getBottom());
+ }
+
+ // If one of the bar backgrounds is a solid color and covers the entire padding on a side
+ // we can drop that padding.
+ boolean eachBarCoversTopInY = true;
+ for (int i = 0; i < 2; i++) {
+ View v = (i == 0) ? coveringView1 : coveringView2;
+ if (v == null || v.getVisibility() != View.VISIBLE
+ || v.getAlpha() != 1f || !isOpaque(v.getBackground())) {
+ eachBarCoversTopInY = false;
+ continue;
+ }
+
+ // Bar covers entire left padding
+ if (v.getTop() <= 0 && v.getBottom() >= height
+ && v.getLeft() <= 0 && v.getRight() >= left) {
+ left = 0;
+ }
+ // Bar covers entire right padding
+ if (v.getTop() <= 0 && v.getBottom() >= height
+ && v.getLeft() <= right && v.getRight() >= width) {
+ right = width;
+ }
+ // Bar covers entire top padding
+ if (v.getTop() <= 0 && v.getBottom() >= top
+ && v.getLeft() <= 0 && v.getRight() >= width) {
+ top = 0;
+ }
+ // Bar covers entire bottom padding
+ if (v.getTop() <= bottom && v.getBottom() >= height
+ && v.getLeft() <= 0 && v.getRight() >= width) {
+ bottom = height;
+ }
+
+ eachBarCoversTopInY &= v.getTop() <= 0 && v.getBottom() >= top;
+ }
+
+ // Special case: Sometimes, both covering views together may cover the top inset, but
+ // neither does on its own.
+ if (eachBarCoversTopInY && (viewsCoverEntireWidth(coveringView1, coveringView2, width)
+ || viewsCoverEntireWidth(coveringView2, coveringView1, width))) {
+ top = 0;
}
if (left >= right || top >= bottom) {
@@ -106,4 +154,24 @@
mBackgroundFallback.draw(c);
}
}
+
+ private boolean isOpaque(Drawable childBg) {
+ return childBg != null && childBg.getOpacity() == PixelFormat.OPAQUE;
+ }
+
+ /**
+ * Returns true if {@code view1} starts before or on {@code 0} and extends at least
+ * up to {@code view2}, and that view extends at least to {@code width}.
+ *
+ * @param view1 the first view to check if it covers the width
+ * @param view2 the second view to check if it covers the width
+ * @param width the width to check for
+ * @return returns true if both views together cover the entire width (and view1 is to the left
+ * of view2)
+ */
+ private boolean viewsCoverEntireWidth(View view1, View view2, int width) {
+ return view1.getLeft() <= 0
+ && view1.getRight() >= view2.getLeft()
+ && view2.getRight() >= width;
+ }
}
diff --git a/core/tests/coretests/src/com/android/internal/widget/BackgroundFallbackTest.java b/core/tests/coretests/src/com/android/internal/widget/BackgroundFallbackTest.java
new file mode 100644
index 0000000..e21143d
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/widget/BackgroundFallbackTest.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import static android.view.View.VISIBLE;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.support.test.InstrumentationRegistry;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+public class BackgroundFallbackTest {
+
+ private static final int NAVBAR_BOTTOM = 0;
+ private static final int NAVBAR_LEFT = 1;
+ private static final int NAVBAR_RIGHT = 2;
+
+ private static final int SCREEN_HEIGHT = 2000;
+ private static final int SCREEN_WIDTH = 1000;
+ private static final int STATUS_HEIGHT = 100;
+ private static final int NAV_SIZE = 200;
+
+ private static final boolean INSET_CONTENT_VIEWS = true;
+ private static final boolean DONT_INSET_CONTENT_VIEWS = false;
+
+ BackgroundFallback mFallback;
+ Drawable mDrawableMock;
+
+ ViewGroup mStatusBarView;
+ ViewGroup mNavigationBarView;
+
+ ViewGroup mDecorViewMock;
+ ViewGroup mContentRootMock;
+ ViewGroup mContentContainerMock;
+ ViewGroup mContentMock;
+
+ int mLastTop = 0;
+
+ @Before
+ public void setUp() throws Exception {
+ mFallback = new BackgroundFallback();
+ mDrawableMock = mock(Drawable.class);
+
+ mFallback.setDrawable(mDrawableMock);
+
+ }
+
+ @Test
+ public void hasFallback_withDrawable_true() {
+ mFallback.setDrawable(mDrawableMock);
+ assertThat(mFallback.hasFallback(), is(true));
+ }
+
+ @Test
+ public void hasFallback_withoutDrawable_false() {
+ mFallback.setDrawable(null);
+ assertThat(mFallback.hasFallback(), is(false));
+ }
+
+ @Test
+ public void draw_portrait_noFallback() {
+ setUpViewHierarchy(INSET_CONTENT_VIEWS, NAVBAR_BOTTOM);
+
+ mFallback.draw(mDecorViewMock, mContentRootMock, null /* canvas */, mContentContainerMock,
+ mStatusBarView, mNavigationBarView);
+
+ verifyNoMoreInteractions(mDrawableMock);
+ }
+
+ @Test
+ public void draw_portrait_translucentBars_fallback() {
+ setUpViewHierarchy(INSET_CONTENT_VIEWS, NAVBAR_BOTTOM);
+ setTranslucent(mStatusBarView);
+ setTranslucent(mNavigationBarView);
+
+ mFallback.draw(mDecorViewMock, mContentRootMock, null /* canvas */, mContentContainerMock,
+ mStatusBarView, mNavigationBarView);
+
+ verifyFallbackTop(STATUS_HEIGHT);
+ verifyFallbackBottom(NAV_SIZE);
+ verifyNoMoreInteractions(mDrawableMock);
+ }
+
+ @Test
+ public void draw_landscape_translucentBars_fallback() {
+ setUpViewHierarchy(INSET_CONTENT_VIEWS, NAVBAR_RIGHT);
+ setTranslucent(mStatusBarView);
+ setTranslucent(mNavigationBarView);
+
+ mFallback.draw(mDecorViewMock, mContentRootMock, null /* canvas */, mContentContainerMock,
+ mStatusBarView, mNavigationBarView);
+
+ verifyFallbackTop(STATUS_HEIGHT);
+ verifyFallbackRight(NAV_SIZE);
+ verifyNoMoreInteractions(mDrawableMock);
+ }
+
+ @Test
+ public void draw_seascape_translucentBars_fallback() {
+ setUpViewHierarchy(INSET_CONTENT_VIEWS, NAVBAR_LEFT);
+ setTranslucent(mStatusBarView);
+ setTranslucent(mNavigationBarView);
+
+ mFallback.draw(mDecorViewMock, mContentRootMock, null /* canvas */, mContentContainerMock,
+ mStatusBarView, mNavigationBarView);
+
+ verifyFallbackTop(STATUS_HEIGHT);
+ verifyFallbackLeft(NAV_SIZE);
+ verifyNoMoreInteractions(mDrawableMock);
+ }
+
+ @Test
+ public void draw_landscape_noFallback() {
+ setUpViewHierarchy(INSET_CONTENT_VIEWS, NAVBAR_RIGHT);
+
+ mFallback.draw(mDecorViewMock, mContentRootMock, null /* canvas */, mContentContainerMock,
+ mStatusBarView, mNavigationBarView);
+
+ verifyNoMoreInteractions(mDrawableMock);
+ }
+
+ @Test
+ public void draw_seascape_noFallback() {
+ setUpViewHierarchy(INSET_CONTENT_VIEWS, NAVBAR_LEFT);
+
+ mFallback.draw(mDecorViewMock, mContentRootMock, null /* canvas */, mContentContainerMock,
+ mStatusBarView, mNavigationBarView);
+
+ verifyNoMoreInteractions(mDrawableMock);
+ }
+
+ @Test
+ public void draw_seascape_translucentBars_noInsets_noFallback() {
+ setUpViewHierarchy(DONT_INSET_CONTENT_VIEWS, NAVBAR_LEFT);
+ setTranslucent(mStatusBarView);
+ setTranslucent(mNavigationBarView);
+
+ mFallback.draw(mDecorViewMock, mContentRootMock, null /* canvas */, mContentContainerMock,
+ mStatusBarView, mNavigationBarView);
+
+ verifyNoMoreInteractions(mDrawableMock);
+ }
+
+ private void verifyFallbackTop(int size) {
+ verify(mDrawableMock).setBounds(0, 0, SCREEN_WIDTH, size);
+ verify(mDrawableMock, atLeastOnce()).draw(any());
+ mLastTop = size;
+ }
+
+ private void verifyFallbackLeft(int size) {
+ verify(mDrawableMock).setBounds(0, mLastTop, size, SCREEN_HEIGHT);
+ verify(mDrawableMock, atLeastOnce()).draw(any());
+ }
+
+ private void verifyFallbackRight(int size) {
+ verify(mDrawableMock).setBounds(SCREEN_WIDTH - size, mLastTop, SCREEN_WIDTH, SCREEN_HEIGHT);
+ verify(mDrawableMock, atLeastOnce()).draw(any());
+ }
+
+ private void verifyFallbackBottom(int size) {
+ verify(mDrawableMock).setBounds(0, SCREEN_HEIGHT - size, SCREEN_WIDTH, SCREEN_HEIGHT);
+ verify(mDrawableMock, atLeastOnce()).draw(any());
+ }
+
+ private void setUpViewHierarchy(boolean insetContentViews, int navBarPosition) {
+ int insetLeft = 0;
+ int insetTop = 0;
+ int insetRight = 0;
+ int insetBottom = 0;
+
+ mStatusBarView = mockView(0, 0, SCREEN_WIDTH, STATUS_HEIGHT,
+ new ColorDrawable(Color.BLACK), VISIBLE, emptyList());
+ if (insetContentViews) {
+ insetTop = STATUS_HEIGHT;
+ }
+
+ switch (navBarPosition) {
+ case NAVBAR_BOTTOM:
+ mNavigationBarView = mockView(0, SCREEN_HEIGHT - NAV_SIZE, SCREEN_WIDTH,
+ SCREEN_HEIGHT, new ColorDrawable(Color.BLACK), VISIBLE, emptyList());
+ if (insetContentViews) {
+ insetBottom = NAV_SIZE;
+ }
+ break;
+ case NAVBAR_LEFT:
+ mNavigationBarView = mockView(0, 0, NAV_SIZE, SCREEN_HEIGHT,
+ new ColorDrawable(Color.BLACK), VISIBLE, emptyList());
+ if (insetContentViews) {
+ insetLeft = NAV_SIZE;
+ }
+ break;
+ case NAVBAR_RIGHT:
+ mNavigationBarView = mockView(SCREEN_WIDTH - NAV_SIZE, 0, SCREEN_WIDTH,
+ SCREEN_HEIGHT, new ColorDrawable(Color.BLACK), VISIBLE, emptyList());
+ if (insetContentViews) {
+ insetRight = NAV_SIZE;
+ }
+ break;
+ }
+
+ mContentMock = mockView(0, 0, SCREEN_WIDTH - insetLeft - insetRight,
+ SCREEN_HEIGHT - insetTop - insetBottom, null, VISIBLE, emptyList());
+ mContentContainerMock = mockView(0, 0, SCREEN_WIDTH - insetLeft - insetRight,
+ SCREEN_HEIGHT - insetTop - insetBottom, null, VISIBLE, asList(mContentMock));
+ mContentRootMock = mockView(insetLeft, insetTop, SCREEN_WIDTH - insetRight,
+ SCREEN_HEIGHT - insetBottom, null, VISIBLE, asList(mContentContainerMock));
+
+ mDecorViewMock = mockView(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, null, VISIBLE,
+ asList(mContentRootMock, mStatusBarView, mNavigationBarView));
+ }
+
+ private void setTranslucent(ViewGroup bar) {
+ bar.setBackground(new ColorDrawable(Color.TRANSPARENT));
+ }
+
+ private ViewGroup mockView(int left, int top, int right, int bottom, Drawable background,
+ int visibility, List<ViewGroup> children) {
+ final ViewGroup v = new FrameLayout(InstrumentationRegistry.getTargetContext());
+
+ v.layout(left, top, right, bottom);
+ v.setBackground(background);
+ v.setVisibility(visibility);
+
+ for (ViewGroup c : children) {
+ v.addView(c);
+ }
+
+ return v;
+ }
+}
\ No newline at end of file