chromium/chrome/android/junit/src/org/chromium/chrome/browser/quickactionsearchwidget/QuickActionSearchWidgetProviderTest.java

// Copyright 2021 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.chrome.browser.quickactionsearchwidget;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.util.SizeF;

import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.SmallTest;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLog;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.quickactionsearchwidget.QuickActionSearchWidgetProvider.QuickActionSearchWidgetProviderDino;
import org.chromium.chrome.browser.ui.searchactivityutils.SearchActivityPreferencesManager.SearchActivityPreferences;
import org.chromium.url.GURL;

import java.util.ArrayList;
import java.util.Arrays;

/** Tests for the (@link QuickActionSearchWidgetProvider}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
        manifest = Config.NONE,
        shadows = {ShadowLog.class})
public class QuickActionSearchWidgetProviderTest {
    private static final int WIDGET_A_ID = 123;
    private static final int WIDGET_B_ID = 987;
    // These are random unique identifiers, the value of these numbers have no special meaning.
    // The number of identifiers has no particular meaning either.
    private static final int[] WIDGET_IDS = {WIDGET_A_ID, WIDGET_B_ID};
    public @Rule MockitoRule mMockitoRule = MockitoJUnit.rule();
    private @Mock AppWidgetManager mAppWidgetManagerMock;

    private SearchActivityPreferences mPreferences;
    private QuickActionSearchWidgetProvider mWidgetProvider;
    private Context mContext;
    private Intent mIntent;
    private Bundle mOptionsWidgetA;
    private Bundle mOptionsWidgetB;

    @Before
    public void setUp() {
        ShadowLog.stream = System.out;
        mContext = Mockito.spy(ApplicationProvider.getApplicationContext());
        mOptionsWidgetA = new Bundle();
        mOptionsWidgetB = new Bundle();
        mPreferences =
                new SearchActivityPreferences(
                        "Search Engine", new GURL("https://search.engine.com"), true, true, true);

        // Inflate an actual RemoteViews to avoid stubbing internal methods or making
        // any other assumptions about the class.
        mWidgetProvider = Mockito.spy(new QuickActionSearchWidgetProviderDino());
        when(mContext.getSystemService(Context.APPWIDGET_SERVICE))
                .thenReturn(mAppWidgetManagerMock);
        when(mAppWidgetManagerMock.getAppWidgetOptions(eq(WIDGET_A_ID)))
                .thenReturn(mOptionsWidgetA);
        when(mAppWidgetManagerMock.getAppWidgetOptions(eq(WIDGET_B_ID)))
                .thenReturn(mOptionsWidgetB);

        // Blanket intent that defines which widget IDs we would be testing here.
        mIntent = new Intent();
        mIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, WIDGET_IDS);
    }

    private void updateReportedWidgetSizes(Bundle options, SizeF portrait, SizeF landscape) {
        // - Portrait mode is narrow and tall, hence MIN_WIDTH and MAX_HEIGHT.
        // - Landscape mode is wide and short, hence MAX_WIDTH and MIN_HEIGHT.
        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, (int) portrait.getWidth());
        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, (int) portrait.getHeight());
        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, (int) landscape.getWidth());
        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, (int) landscape.getHeight());
    }

    @Test
    @SmallTest
    public void testAppWidgetInstallationCreatesWidgets() {
        mIntent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
        updateReportedWidgetSizes(mOptionsWidgetA, new SizeF(80, 80), new SizeF(400, 40));
        updateReportedWidgetSizes(mOptionsWidgetB, new SizeF(30, 10), new SizeF(100, 30));
        mWidgetProvider.onReceive(mContext, mIntent);

        // There are 2 fake widgets to be created, and each should be created for portrait and
        // landscape screen orientation.
        // Widget A, Portrait.
        verify(mWidgetProvider).createWidget(any(), any(), eq(80), eq(80));
        // Widget A, Landscape.
        verify(mWidgetProvider).createWidget(any(), any(), eq(400), eq(40));
        // Widget B, Portrait.
        verify(mWidgetProvider).createWidget(any(), any(), eq(30), eq(10));
        // Widget B, Landscape.
        verify(mWidgetProvider).createWidget(any(), any(), eq(100), eq(30));
        // 4 total, no more.
        verify(mWidgetProvider, times(4)).createWidget(any(), any(), anyInt(), anyInt());
    }

    @Test
    @SmallTest
    public void testAppWidgetResizeUpdatesWidgets() {
        updateReportedWidgetSizes(mOptionsWidgetA, new SizeF(80, 80), new SizeF(400, 40));
        updateReportedWidgetSizes(mOptionsWidgetB, new SizeF(30, 10), new SizeF(100, 30));
        mIntent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
        mWidgetProvider.onReceive(mContext, mIntent);

        verify(mWidgetProvider).createWidget(any(), any(), eq(80), eq(80));
        verify(mWidgetProvider).createWidget(any(), any(), eq(400), eq(40));
        verify(mWidgetProvider).createWidget(any(), any(), eq(30), eq(10));
        verify(mWidgetProvider).createWidget(any(), any(), eq(100), eq(30));
        verify(mWidgetProvider, times(4)).createWidget(any(), any(), anyInt(), anyInt());

        clearInvocations(mWidgetProvider);

        // OPTIONS_CHANGED specifies which particular widget needs to be updated. Make sure we
        // reflect that here. Below, we flip the sizes so that Widget B uses sizes of Widget A and
        // vice versa.
        when(mAppWidgetManagerMock.getAppWidgetOptions(eq(WIDGET_A_ID)))
                .thenReturn(mOptionsWidgetB);
        when(mAppWidgetManagerMock.getAppWidgetOptions(eq(WIDGET_B_ID)))
                .thenReturn(mOptionsWidgetA);

        // First, resize Widget A with old Widget B size specs.
        mIntent.setAction(AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED);
        mIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, WIDGET_A_ID);
        mIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, mOptionsWidgetB);
        mWidgetProvider.onReceive(mContext, mIntent);

        verify(mWidgetProvider).createWidget(any(), any(), eq(30), eq(10));
        verify(mWidgetProvider).createWidget(any(), any(), eq(100), eq(30));
        verify(mWidgetProvider, times(2)).createWidget(any(), any(), anyInt(), anyInt());
        clearInvocations(mWidgetProvider);

        // Next, resize Widget B with old Widget A size specs.
        mIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, WIDGET_B_ID);
        mIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, mOptionsWidgetA);
        mWidgetProvider.onReceive(mContext, mIntent);
        verify(mWidgetProvider).createWidget(any(), any(), eq(80), eq(80));
        verify(mWidgetProvider).createWidget(any(), any(), eq(400), eq(40));
        verify(mWidgetProvider, times(2)).createWidget(any(), any(), anyInt(), anyInt());
    }

    @Test
    @SmallTest
    @Config(sdk = Build.VERSION_CODES.S)
    public void testCreateWidgetsFromFallbackValues_missingSizes() {
        updateReportedWidgetSizes(mOptionsWidgetA, new SizeF(80, 80), new SizeF(400, 40));
        mWidgetProvider.getRemoteViews(mContext, mPreferences, mOptionsWidgetA);

        // There are 2 fake widgets that we work with, so expect both being evaluated
        // One for portrait mode (max_width, min_height) and
        // One for landscape mode (min_width, max_height).
        verify(mWidgetProvider).createWidget(any(), any(), eq(400), eq(40));
        verify(mWidgetProvider).createWidget(any(), any(), eq(80), eq(80));
        verify(mWidgetProvider, times(2)).createWidget(any(), any(), anyInt(), anyInt());
    }

    @Test
    @SmallTest
    @Config(sdk = Build.VERSION_CODES.S)
    public void testCreateWidgetFromFallbackValues_emptySizes() {
        updateReportedWidgetSizes(mOptionsWidgetA, new SizeF(80, 80), new SizeF(400, 40));
        // Lastly, set the empty array of sizes.
        mOptionsWidgetA.putParcelableArrayList(
                AppWidgetManager.OPTION_APPWIDGET_SIZES, new ArrayList<SizeF>());
        mWidgetProvider.getRemoteViews(mContext, mPreferences, mOptionsWidgetA);

        // There are 2 fake widgets that we work with, so expect both being evaluated
        // One for portrait mode (max_width, min_height) and
        // One for landscape mode (min_width, max_height).
        verify(mWidgetProvider).createWidget(any(), any(), eq(400), eq(40));
        verify(mWidgetProvider).createWidget(any(), any(), eq(80), eq(80));
        verify(mWidgetProvider, times(2)).createWidget(any(), any(), anyInt(), anyInt());
    }

    @Test
    @SmallTest
    @Config(sdk = Build.VERSION_CODES.S)
    public void testCreateWidgetFromSizeSpecs() {
        updateReportedWidgetSizes(mOptionsWidgetA, new SizeF(80, 80), new SizeF(400, 40));
        // Lastly, set a different array of sizes and confirm it is used instead.
        mOptionsWidgetA.putParcelableArrayList(
                AppWidgetManager.OPTION_APPWIDGET_SIZES,
                new ArrayList<SizeF>(Arrays.asList(new SizeF(50, 50))));
        mWidgetProvider.getRemoteViews(mContext, mPreferences, mOptionsWidgetA);

        // Only one call is expected here, because we declare only one size in our list.
        verify(mWidgetProvider).createWidget(any(), any(), eq(50), eq(50));
        verify(mWidgetProvider, times(1)).createWidget(any(), any(), anyInt(), anyInt());
    }

    @Test
    @SmallTest
    @Config(sdk = Build.VERSION_CODES.R)
    public void testCreateWidgetFromLegacyMeasurements() {
        updateReportedWidgetSizes(mOptionsWidgetA, new SizeF(80, 80), new SizeF(400, 40));
        // Define new sizes. These must be ignored, because RemoteViews constructor is not
        // supported.
        mOptionsWidgetA.putParcelableArrayList(
                AppWidgetManager.OPTION_APPWIDGET_SIZES,
                new ArrayList<SizeF>(Arrays.asList(new SizeF(50, 50))));
        mWidgetProvider.getRemoteViews(mContext, mPreferences, mOptionsWidgetA);

        // There are 2 fake widgets that we work with, so expect both being evaluated
        // One for portrait mode (max_width, min_height) and
        // One for landscape mode (min_width, max_height).
        verify(mWidgetProvider).createWidget(any(), any(), eq(400), eq(40));
        verify(mWidgetProvider).createWidget(any(), any(), eq(80), eq(80));
        verify(mWidgetProvider, times(2)).createWidget(any(), any(), anyInt(), anyInt());
    }
}