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