chromium/ui/android/junit/src/org/chromium/ui/InsetObserverTest.java

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.ui;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.graphics.Rect;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.view.View;
import android.view.WindowInsets;
import android.widget.LinearLayout;

import androidx.annotation.RequiresApi;
import androidx.core.graphics.Insets;
import androidx.core.view.DisplayCutoutCompat;
import androidx.core.view.WindowInsetsAnimationCompat;
import androidx.core.view.WindowInsetsAnimationCompat.BoundsCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.test.filters.SmallTest;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.ui.InsetObserver.WindowInsetsAnimationListener;
import org.chromium.ui.InsetObserver.WindowInsetsConsumer;
import org.chromium.ui.base.ImmutableWeakReference;

import java.util.Collections;

/** Tests for {@link InsetObserver} class. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class InsetObserverTest {
    /** The rect values if the display cutout is present. */
    private static final Rect DISPLAY_CUTOUT_RECT = new Rect(1, 1, 1, 1);

    /** The rect values if there is no cutout. */
    private static final Rect NO_CUTOUT_RECT = new Rect(0, 0, 0, 0);

    /* Extra bottom inset that will be applied when e2e is enabled. */
    private static final int EDGE_TO_EDGE_BOTTOM_INSET = 2;

    /** The rect values if the display cutout is present in edge-to-edge mode. */
    private static final Rect E2E_DISPLAY_CUTOUT_RECT = new Rect(1, 1, 1, 3);

    /** The rect values if there is no cutout. */
    private static final Rect E2E_NO_CUTOUT_RECT = new Rect(0, 0, 0, 2);

    private static final Insets SYSTEM_BAR_INSETS = Insets.of(1, 1, 1, 1);

    private static final Insets SYSTEM_BAR_INSETS_MODIFIED = Insets.of(1, 1, 1, 2);

    @Mock private InsetObserver.WindowInsetObserver mObserver;

    @Mock private WindowInsetsCompat mInsets;
    @Mock private WindowInsetsCompat mModifiedInsets;
    @Mock private WindowInsets mNonCompatInsets;
    @Mock private WindowInsets mModifiedNonCompatInsets;
    @Mock private WindowInsetsConsumer mInsetsConsumer;
    @Mock private WindowInsetsAnimationListener mInsetsAnimationListener;
    @Mock private LinearLayout mContentView;

    private InsetObserver mInsetObserver;

    private void setCutout(boolean hasCutout) {
        DisplayCutoutCompat cutout =
                hasCutout ? new DisplayCutoutCompat(new Rect(1, 1, 1, 1), null) : null;
        doReturn(cutout).when(mInsets).getDisplayCutout();
    }

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        doReturn(mNonCompatInsets).when(mInsets).toWindowInsets();
        doReturn(mModifiedNonCompatInsets).when(mModifiedInsets).toWindowInsets();
        doReturn(WindowInsetsCompat.CONSUMED.toWindowInsets())
                .when(mContentView)
                .onApplyWindowInsets(mNonCompatInsets);
        doReturn(WindowInsetsCompat.CONSUMED.toWindowInsets())
                .when(mContentView)
                .onApplyWindowInsets(mModifiedNonCompatInsets);

        doReturn(SYSTEM_BAR_INSETS).when(mInsets).getInsets(WindowInsetsCompat.Type.systemBars());
        doReturn(SYSTEM_BAR_INSETS_MODIFIED)
                .when(mModifiedInsets)
                .getInsets(WindowInsetsCompat.Type.systemBars());

        mInsetObserver = new InsetObserver(new ImmutableWeakReference<View>(mContentView));
        mInsetObserver.addObserver(mObserver);
    }

    /** Test that applying new insets notifies observers. */
    @Test
    @SmallTest
    public void applyInsets_NotifiesObservers() {
        mInsetObserver.onApplyWindowInsets(mContentView, mInsets);
        verify(mObserver, times(1)).onInsetChanged(1, 1, 1, 1);

        // Apply the insets a second time; the observer should not be notified.
        mInsetObserver.onApplyWindowInsets(mContentView, mInsets);
        verify(mObserver, times(1)).onInsetChanged(1, 1, 1, 1);

        doReturn(Insets.of(1, 1, 1, 2))
                .when(mInsets)
                .getInsets(WindowInsetsCompat.Type.systemBars());
        mInsetObserver.onApplyWindowInsets(mContentView, mInsets);
        verify(mObserver).onInsetChanged(1, 1, 1, 2);
    }

    @Test
    @SmallTest
    public void applyInsets_withInsetConsumer() {
        mInsetObserver.addInsetsConsumer(mInsetsConsumer);

        doReturn(mModifiedInsets).when(mInsetsConsumer).onApplyWindowInsets(mContentView, mInsets);
        doReturn(Insets.of(14, 17, 31, 43))
                .when(mModifiedInsets)
                .getInsets(WindowInsetsCompat.Type.systemBars());

        mInsetObserver.onApplyWindowInsets(mContentView, mInsets);
        verify(mInsetsConsumer).onApplyWindowInsets(mContentView, mInsets);
        verify(mObserver, times(1)).onInsetChanged(14, 17, 31, 43);
    }

    @Test
    @SmallTest
    public void insetAnimation() {
        mInsetObserver.addWindowInsetsAnimationListener(mInsetsAnimationListener);
        WindowInsetsAnimationCompat.Callback callback =
                mInsetObserver.getInsetAnimationProxyCallbackForTesting();
        WindowInsetsAnimationCompat animationCompat = new WindowInsetsAnimationCompat(8, null, 50);
        callback.onPrepare(animationCompat);
        verify(mInsetsAnimationListener).onPrepare(animationCompat);

        BoundsCompat bounds = new BoundsCompat(Insets.NONE, Insets.of(0, 0, 40, 40));
        callback.onStart(animationCompat, bounds);
        verify(mInsetsAnimationListener).onStart(animationCompat, bounds);

        WindowInsetsCompat insetsCompat = WindowInsetsCompat.CONSUMED;
        callback.onProgress(insetsCompat, Collections.emptyList());
        callback.onProgress(insetsCompat, Collections.emptyList());
        verify(mInsetsAnimationListener, times(2))
                .onProgress(insetsCompat, Collections.emptyList());

        callback.onEnd(animationCompat);
        verify(mInsetsAnimationListener).onEnd(animationCompat);
    }

    /** Test that applying new insets does not notify the observer. */
    @Test
    @SmallTest
    @RequiresApi(Build.VERSION_CODES.P)
    public void applyInsets() {
        setCutout(false);
        mInsetObserver.onApplyWindowInsets(mContentView, mInsets);
        verify(mObserver, never()).onSafeAreaChanged(any());
    }

    /** Test that applying new insets with a cutout notifies the observer. */
    @Test
    @SmallTest
    @RequiresApi(Build.VERSION_CODES.P)
    public void applyInsets_WithCutout() {
        setCutout(true);
        mInsetObserver.onApplyWindowInsets(mContentView, mInsets);
        verify(mObserver).onSafeAreaChanged(DISPLAY_CUTOUT_RECT);
    }

    /** Test applying new insets with a cutout and then remove the cutout. */
    @Test
    @SmallTest
    @RequiresApi(Build.VERSION_CODES.P)
    public void applyInsets_WithCutout_WithoutCutout() {
        setCutout(true);
        mInsetObserver.onApplyWindowInsets(mContentView, mInsets);
        verify(mObserver).onSafeAreaChanged(DISPLAY_CUTOUT_RECT);

        reset(mObserver);
        setCutout(false);
        mInsetObserver.onApplyWindowInsets(mContentView, mInsets);
        verify(mObserver).onSafeAreaChanged(NO_CUTOUT_RECT);
    }

    /** Test that applying new insets with a cutout but no observer is a no-op. */
    @Test
    @SmallTest
    @RequiresApi(Build.VERSION_CODES.P)
    public void applyInsets_WithCutout_NoListener() {
        setCutout(true);
        mInsetObserver.removeObserver(mObserver);
        mInsetObserver.onApplyWindowInsets(mContentView, mInsets);
    }

    /** Test that applying new insets with no observer is a no-op. */
    @Test
    @SmallTest
    @RequiresApi(Build.VERSION_CODES.P)
    public void applyInsets_NoListener() {
        setCutout(false);
        mInsetObserver.removeObserver(mObserver);
        mInsetObserver.onApplyWindowInsets(mContentView, mInsets);
    }

    @Test
    @RequiresApi(Build.VERSION_CODES.P)
    public void addEdgeToEdgeBottomInset() {
        setCutout(true);
        mInsetObserver.updateBottomInsetForEdgeToEdge(EDGE_TO_EDGE_BOTTOM_INSET);
        verify(mObserver).onSafeAreaChanged(E2E_NO_CUTOUT_RECT);

        reset(mObserver);
        mInsetObserver.onApplyWindowInsets(mContentView, mInsets);
        verify(mObserver).onSafeAreaChanged(E2E_DISPLAY_CUTOUT_RECT);
    }

    @Test
    @RequiresApi(Build.VERSION_CODES.P)
    public void addEdgeToEdgeBottomInset_NoCutout() {
        setCutout(false);
        mInsetObserver.updateBottomInsetForEdgeToEdge(EDGE_TO_EDGE_BOTTOM_INSET);
        mInsetObserver.onApplyWindowInsets(mContentView, mInsets);
        verify(mObserver).onSafeAreaChanged(E2E_NO_CUTOUT_RECT);
    }

    @Test
    @RequiresApi(Build.VERSION_CODES.P)
    public void addEdgeToEdgeBottomInset_NoBottomInset() {
        setCutout(true);
        mInsetObserver.updateBottomInsetForEdgeToEdge(0);
        mInsetObserver.onApplyWindowInsets(mContentView, mInsets);
        verify(mObserver).onSafeAreaChanged(DISPLAY_CUTOUT_RECT);
    }

    @Test
    public void checkLastSeenRawWindowInsets() {
        assertNull(
                "WindowInsets does not have initial value.",
                mInsetObserver.getLastRawWindowInsets());

        mInsetObserver.onApplyWindowInsets(mContentView, mInsets);
        assertEquals(
                "WindowInsets is different.", mInsets, mInsetObserver.getLastRawWindowInsets());

        mInsetObserver.onApplyWindowInsets(mContentView, mModifiedInsets);
        assertEquals(
                "WindowInsets is different.",
                mModifiedInsets,
                mInsetObserver.getLastRawWindowInsets());
    }

    @Test
    @Config(sdk = VERSION_CODES.R)
    public void initializeWithLastSeenRawWindowInsets() {
        doReturn(mNonCompatInsets).when(mContentView).getRootWindowInsets();
        mInsetObserver = new InsetObserver(new ImmutableWeakReference<View>(mContentView));
        assertEquals(
                "WindowInsets is different.",
                WindowInsetsCompat.toWindowInsetsCompat(mNonCompatInsets),
                mInsetObserver.getLastRawWindowInsets());
    }
}