chromium/chrome/browser/ui/android/searchactivityutils/java/src/org/chromium/chrome/browser/ui/searchactivityutils/SearchActivityPreferencesManagerTest.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.ui.searchactivityutils;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
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 static org.mockito.Mockito.verifyNoMoreInteractions;

import static org.chromium.chrome.browser.preferences.ChromePreferenceKeys.SEARCH_WIDGET_IS_GOOGLE_LENS_AVAILABLE;
import static org.chromium.chrome.browser.preferences.ChromePreferenceKeys.SEARCH_WIDGET_IS_INCOGNITO_AVAILABLE;
import static org.chromium.chrome.browser.preferences.ChromePreferenceKeys.SEARCH_WIDGET_IS_VOICE_SEARCH_AVAILABLE;
import static org.chromium.chrome.browser.preferences.ChromePreferenceKeys.SEARCH_WIDGET_SEARCH_ENGINE_SHORTNAME;
import static org.chromium.chrome.browser.preferences.ChromePreferenceKeys.SEARCH_WIDGET_SEARCH_ENGINE_URL;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implements;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.ContextUtils;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.shared_preferences.SharedPreferencesManager;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.incognito.IncognitoUtils;
import org.chromium.chrome.browser.lens.LensController;
import org.chromium.chrome.browser.omnibox.voice.VoiceRecognitionUtil;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.ui.searchactivityutils.SearchActivityPreferencesManager.SearchActivityPreferences;
import org.chromium.components.search_engines.TemplateUrl;
import org.chromium.components.search_engines.TemplateUrlService;
import org.chromium.components.search_engines.TemplateUrlService.LoadListener;
import org.chromium.components.search_engines.TemplateUrlService.TemplateUrlServiceObserver;
import org.chromium.ui.permissions.AndroidPermissionDelegate;
import org.chromium.url.GURL;

import java.util.function.Consumer;

/** Tests for {@link SearchActivityPreferencesManager}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
        shadows = {
            SearchActivityPreferencesManagerTest.ShadowLensController.class,
            SearchActivityPreferencesManagerTest.ShadowVoiceRecognitionUtil.class,
        })
public class SearchActivityPreferencesManagerTest {
    @Mock private TemplateUrlService mTemplateUrlServiceMock;
    @Mock private LibraryLoader mLibraryLoaderMock;
    @Mock private TemplateUrl mTemplateUrlMock;
    @Mock private Profile mProfile;

    private LoadListener mTemplateUrlServiceLoadListener;
    private TemplateUrlServiceObserver mTemplateUrlServiceObserver;

    @Implements(LensController.class)
    public static class ShadowLensController {
        public static boolean sIsAvailable = true;

        public static LensController getInstance() {
            var controller = mock(LensController.class);
            doAnswer(i -> sIsAvailable).when(controller).isLensEnabled(any());
            return controller;
        }
    }

    @Implements(VoiceRecognitionUtil.class)
    public static class ShadowVoiceRecognitionUtil {
        public static boolean sIsAvailable = true;

        public static boolean isVoiceSearchEnabled(AndroidPermissionDelegate delegate) {
            return sIsAvailable;
        }
    }

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        TemplateUrlServiceFactory.setInstanceForTesting(mTemplateUrlServiceMock);
        ProfileManager.setLastUsedProfileForTesting(mProfile);

        doAnswer(
                        invocation -> {
                            mTemplateUrlServiceLoadListener =
                                    (LoadListener) invocation.getArguments()[0];
                            return null;
                        })
                .when(mTemplateUrlServiceMock)
                .registerLoadListener(any());

        doAnswer(
                        invocation -> {
                            mTemplateUrlServiceObserver =
                                    (TemplateUrlServiceObserver) invocation.getArguments()[0];
                            return null;
                        })
                .when(mTemplateUrlServiceMock)
                .addObserver(any());

        SearchActivityPreferencesManager.resetForTesting();
        // Reset any cached values so we consistently start with a predictable state.
        SearchActivityPreferencesManager.resetCachedValues();

        // Make sure there were no premature attempts to register observers.
        Assert.assertNull(mTemplateUrlServiceLoadListener);
        Assert.assertNull(mTemplateUrlServiceObserver);

        // Purge any pending propagate actions to ensure no side effets later in the tests.
        // Needed because `resetCachedValues()` will likely post a task to notify listeners.
        ShadowLooper.runUiThreadTasks();
    }

    @After
    public void tearDown() {
        ShadowLooper.runUiThreadTasks();
        TemplateUrlServiceFactory.setInstanceForTesting(null);
        ProfileManager.setLastUsedProfileForTesting(null);
        SearchActivityPreferencesManager.resetForTesting();
    }

    @Test
    public void preferenceTest_equalWithSameContent() {
        SearchActivityPreferences p1 =
                new SearchActivityPreferences(
                        "test", new GURL("https://test.url"), true, true, true);
        SearchActivityPreferences p2 =
                new SearchActivityPreferences(
                        "test", new GURL("https://test.url"), true, true, true);
        Assert.assertEquals(p1, p1);
        Assert.assertEquals(p2, p2);
        Assert.assertEquals(p1, p2);
        Assert.assertEquals(p1.hashCode(), p2.hashCode());

        p1 = new SearchActivityPreferences(null, new GURL("https://test.url"), true, false, true);
        p2 = new SearchActivityPreferences(null, new GURL("https://test.url"), true, false, true);
        Assert.assertEquals(p1, p2);
        Assert.assertEquals(p1.hashCode(), p2.hashCode());

        p1 = new SearchActivityPreferences("test", null, false, true, true);
        p2 = new SearchActivityPreferences("test", null, false, true, true);
        Assert.assertEquals(p1, p2);
        Assert.assertEquals(p1.hashCode(), p2.hashCode());

        p1 = new SearchActivityPreferences(null, null, false, false, false);
        p2 = new SearchActivityPreferences(null, null, false, false, false);
        Assert.assertEquals(p1, p2);
        Assert.assertEquals(p1.hashCode(), p2.hashCode());
    }

    @Test
    public void preferenceTest_notEqualWithDifferentVoiceAvailability() {
        SearchActivityPreferences p1 =
                new SearchActivityPreferences(
                        "test", new GURL("https://test.url"), true, false, false);
        SearchActivityPreferences p2 =
                new SearchActivityPreferences(
                        "test", new GURL("https://test.url"), false, false, false);
        Assert.assertNotEquals(p1, p2);
        Assert.assertNotEquals(p1.hashCode(), p2.hashCode());
    }

    @Test
    public void preferenceTest_notEqualWithDifferentLensAvailability() {
        SearchActivityPreferences p1 =
                new SearchActivityPreferences(
                        "test", new GURL("https://test.url"), true, true, false);
        SearchActivityPreferences p2 =
                new SearchActivityPreferences(
                        "test", new GURL("https://test.url"), true, false, false);
        Assert.assertNotEquals(p1, p2);
        Assert.assertNotEquals(p1.hashCode(), p2.hashCode());
    }

    @Test
    public void preferenceTest_notEqualWithDifferentIncognitoAvailability() {
        SearchActivityPreferences p1 =
                new SearchActivityPreferences(
                        "test", new GURL("https://test.url"), true, true, true);
        SearchActivityPreferences p2 =
                new SearchActivityPreferences(
                        "test", new GURL("https://test.url"), true, true, false);
        Assert.assertNotEquals(p1, p2);
        Assert.assertNotEquals(p1.hashCode(), p2.hashCode());
    }

    @Test
    public void preferenceTest_notEqualWithDifferentSearchEngineName() {
        SearchActivityPreferences p1 =
                new SearchActivityPreferences(
                        "Search Engine 1", new GURL("https://test.url"), true, true, true);
        SearchActivityPreferences p2 =
                new SearchActivityPreferences(
                        "Search Engine 2", new GURL("https://test.url"), true, true, true);
        Assert.assertNotEquals(p1, p2);
        Assert.assertNotEquals(p1.hashCode(), p2.hashCode());
    }

    @Test
    public void preferenceTest_notEqualWithDifferentSearchEngineUrl() {
        SearchActivityPreferences p1 =
                new SearchActivityPreferences(
                        "Google", new GURL("https://www.google.com"), true, true, true);
        SearchActivityPreferences p2 =
                new SearchActivityPreferences(
                        "Google", new GURL("https://www.google.pl"), true, true, true);
        Assert.assertNotEquals(p1, p2);
        Assert.assertNotEquals(p1.hashCode(), p2.hashCode());
    }

    @Test
    public void managerTest_updateIsPropagatedToAllObservers() {
        Consumer<SearchActivityPreferences> observer1 = mock(Consumer.class);
        Consumer<SearchActivityPreferences> observer2 = mock(Consumer.class);

        // Add 2 distinct listeners and confirm everybody gets called immediately with initial
        // values.
        SearchActivityPreferencesManager.addObserver(observer1);
        verify(observer1).accept(any());
        SearchActivityPreferencesManager.addObserver(observer2);
        verify(observer1).accept(any());
        clearInvocations(observer1, observer2);

        // Perform an update and check the number of calls.
        var newSettings =
                new SearchActivityPreferences(
                        "Search Engine", new GURL("https://URL"), false, true, true);
        SearchActivityPreferencesManager.setCurrentlyLoadedPreferences(newSettings, false);
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        verify(observer1).accept(eq(newSettings));
        verify(observer2).accept(eq(newSettings));
        clearInvocations(observer1, observer2);

        // Add a new listener.
        Consumer<SearchActivityPreferences> observer3 = mock(Consumer.class);
        SearchActivityPreferencesManager.addObserver(observer3);
        verify(observer3).accept(eq(newSettings));
        clearInvocations(observer1, observer2, observer3);

        // Perform an update and check the number of calls.
        newSettings =
                new SearchActivityPreferences(
                        "Search Engine", new GURL("https://URL"), true, true, true);
        SearchActivityPreferencesManager.setCurrentlyLoadedPreferences(newSettings, false);
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        verify(observer1).accept(eq(newSettings));
        verify(observer2).accept(eq(newSettings));
        verify(observer3).accept(eq(newSettings));
        clearInvocations(observer1, observer2, observer3);

        // Finally, reset settings to safe defaults. All listeners should be notified.
        SearchActivityPreferencesManager.resetCachedValues();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        verify(observer1).accept(any());
        verify(observer2).accept(any());
        verify(observer3).accept(any());
    }

    @Test
    public void managerTest_eachObserverCanOnlyBeAddedOnce() {
        final Consumer<SearchActivityPreferences> listener1 = mock(Consumer.class);

        // Add same listener a few times.
        SearchActivityPreferencesManager.addObserver(listener1);
        verify(listener1).accept(any());
        clearInvocations(listener1);

        SearchActivityPreferencesManager.addObserver(listener1);
        verify(listener1, never()).accept(any());

        // Add a different listener.
        Consumer<SearchActivityPreferences> listener2 = mock(Consumer.class);
        SearchActivityPreferencesManager.addObserver(listener2);
        verify(listener1, never()).accept(any());
        verify(listener2).accept(any());
        clearInvocations(listener1, listener2);

        SearchActivityPreferencesManager.addObserver(listener2);
        SearchActivityPreferencesManager.addObserver(listener1);
        verify(listener1, never()).accept(any());
        verify(listener2, never()).accept(any());

        // Verify that we don't get excessive update notifications.
        SearchActivityPreferencesManager.setCurrentlyLoadedPreferences(
                new SearchActivityPreferences(
                        "ABC", new GURL("https://abc.xyz"), false, true, true),
                false);
        verify(listener1, never()).accept(any());
        verify(listener2, never()).accept(any());
        ShadowLooper.runUiThreadTasks();
        verify(listener1).accept(any());
        verify(listener2).accept(any());
        clearInvocations(listener1, listener2);

        // Finally, confirm reset.
        SearchActivityPreferencesManager.resetCachedValues();
        verify(listener1, never()).accept(any());
        verify(listener2, never()).accept(any());
        ShadowLooper.runUiThreadTasks();
        verify(listener1).accept(any());
        verify(listener2).accept(any());
    }

    @Test
    public void managerTest_preferencesRetentionTest() {
        final SharedPreferencesManager manager = ChromeSharedPreferences.getInstance();

        // Make sure we don't have anything on disk.
        Assert.assertFalse(manager.contains(SEARCH_WIDGET_SEARCH_ENGINE_SHORTNAME));
        Assert.assertFalse(manager.contains(SEARCH_WIDGET_SEARCH_ENGINE_URL));
        Assert.assertFalse(manager.contains(SEARCH_WIDGET_IS_VOICE_SEARCH_AVAILABLE));
        Assert.assertFalse(manager.contains(SEARCH_WIDGET_IS_GOOGLE_LENS_AVAILABLE));
        Assert.assertFalse(manager.contains(SEARCH_WIDGET_IS_INCOGNITO_AVAILABLE));

        // Install receiver of the async pref update notification.
        // We expect the on-disk prefs to be already updated when this call is made.
        Consumer<SearchActivityPreferences> listener = mock(Consumer.class);
        SearchActivityPreferencesManager.addObserver(listener);
        clearInvocations(listener);

        // Save settings to disk.
        var persistedUrl = new GURL("https://URL");
        var preference =
                new SearchActivityPreferences("Search Engine", persistedUrl, false, true, true);
        SearchActivityPreferencesManager.setCurrentlyLoadedPreferences(preference, true);
        // Should not be live right away - expect posted task.
        verify(listener, never()).accept(any());
        ShadowLooper.runUiThreadTasks();
        verify(listener).accept(eq(preference));

        // Note: we provide different default values than stored ones to make sure everything works.
        Assert.assertEquals(
                "Search Engine",
                manager.readString(
                        SEARCH_WIDGET_SEARCH_ENGINE_SHORTNAME, "Engine Name Doesn't work"));

        GURL deserializedUrl =
                GURL.deserialize(manager.readString(SEARCH_WIDGET_SEARCH_ENGINE_URL, ""));
        Assert.assertEquals(persistedUrl, deserializedUrl);
        Assert.assertEquals(
                false, manager.readBoolean(SEARCH_WIDGET_IS_VOICE_SEARCH_AVAILABLE, true));
        Assert.assertEquals(
                true, manager.readBoolean(SEARCH_WIDGET_IS_GOOGLE_LENS_AVAILABLE, false));
        Assert.assertEquals(true, manager.readBoolean(SEARCH_WIDGET_IS_INCOGNITO_AVAILABLE, false));

        // Reset values to defaults / "clear application data". Make sure we don't have anything on
        // disk.
        SearchActivityPreferencesManager.resetCachedValues();
        Assert.assertFalse(manager.contains(SEARCH_WIDGET_SEARCH_ENGINE_SHORTNAME));
        Assert.assertFalse(manager.contains(SEARCH_WIDGET_SEARCH_ENGINE_URL));
        Assert.assertFalse(manager.contains(SEARCH_WIDGET_IS_VOICE_SEARCH_AVAILABLE));
        Assert.assertFalse(manager.contains(SEARCH_WIDGET_IS_GOOGLE_LENS_AVAILABLE));
        Assert.assertFalse(manager.contains(SEARCH_WIDGET_IS_INCOGNITO_AVAILABLE));
    }

    @Test
    public void managerTest_earlyInitializationOfTemplateUrlService() {
        // Install event listener.
        Consumer<SearchActivityPreferences> listener = mock(Consumer.class);
        SearchActivityPreferencesManager.addObserver(listener);
        clearInvocations(listener);
        verifyNoMoreInteractions(mTemplateUrlServiceMock);

        // Signal the Manager that Native Libraries are ready.
        doReturn(true).when(mLibraryLoaderMock).isInitialized();
        SearchActivityPreferencesManager.onNativeLibraryReady();
        verify(mTemplateUrlServiceMock, times(1)).registerLoadListener(any());
        verify(mTemplateUrlServiceMock, times(1)).addObserver(any());
        Assert.assertNotNull(mTemplateUrlServiceLoadListener);
        Assert.assertNotNull(mTemplateUrlServiceObserver);
        reset(mTemplateUrlServiceMock);

        // Confirm no crash if we don't have no DSE at the time of first call.
        // Confirm that we deregister load observer since it should no longer be needed.
        doReturn(true).when(mTemplateUrlServiceMock).isLoaded();
        mTemplateUrlServiceLoadListener.onTemplateUrlServiceLoaded();
        verify(mTemplateUrlServiceMock, times(1)).getDefaultSearchEngineTemplateUrl();
        verify(mTemplateUrlServiceMock, times(1))
                .unregisterLoadListener(eq(mTemplateUrlServiceLoadListener));

        // Confirm no data and no updates.
        Assert.assertNull(SearchActivityPreferencesManager.getCurrent().searchEngineName);
        Assert.assertTrue(SearchActivityPreferencesManager.getCurrent().searchEngineUrl.isEmpty());
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        verify(listener, never()).accept(any());
    }

    @Test
    public void managerTest_lateInitializationOfTemplateUrlService() {
        // Install event listener.
        Consumer<SearchActivityPreferences> listener = mock(Consumer.class);
        ArgumentCaptor<SearchActivityPreferences> refPrefs =
                ArgumentCaptor.forClass(SearchActivityPreferences.class);

        SearchActivityPreferencesManager.addObserver(listener);
        clearInvocations(listener);

        // Set up template url to have some data.
        doReturn("Cowabunga").when(mTemplateUrlMock).getShortName();
        doReturn("keyword").when(mTemplateUrlMock).getKeyword();
        doReturn("https://www.cowabunga.com/are-turtles-still-awesome?woooo")
                .when(mTemplateUrlServiceMock)
                .getSearchEngineUrlFromTemplateUrl(eq("keyword"));
        doReturn(mTemplateUrlMock)
                .when(mTemplateUrlServiceMock)
                .getDefaultSearchEngineTemplateUrl();

        // Signal the Manager that Native Libraries are ready.
        doReturn(true).when(mLibraryLoaderMock).isInitialized();
        SearchActivityPreferencesManager.onNativeLibraryReady();

        // Simulate the event where we had everything readily available when TemplateUrlService is
        // loaded.
        doReturn(true).when(mTemplateUrlServiceMock).isLoaded();
        doReturn(mTemplateUrlMock)
                .when(mTemplateUrlServiceMock)
                .getDefaultSearchEngineTemplateUrl();
        mTemplateUrlServiceLoadListener.onTemplateUrlServiceLoaded();

        // Confirm data is available and update is pushed.
        ShadowLooper.runUiThreadTasks();
        verify(listener).accept(refPrefs.capture());
        Assert.assertEquals("Cowabunga", refPrefs.getValue().searchEngineName);
        Assert.assertEquals(
                "https://www.cowabunga.com/", refPrefs.getValue().searchEngineUrl.getSpec());
    }

    @Test
    public void initializeFromCache_withOldStyleUrl() {
        final SharedPreferencesManager manager = ChromeSharedPreferences.getInstance();

        manager.writeString(SEARCH_WIDGET_SEARCH_ENGINE_SHORTNAME, "Engine");
        manager.writeString(SEARCH_WIDGET_SEARCH_ENGINE_URL, "https://engine.com");

        // Force re-read persisted data.
        SearchActivityPreferencesManager.resetForTesting();
        SearchActivityPreferences data = SearchActivityPreferencesManager.getCurrent();

        Assert.assertEquals("Engine", data.searchEngineName);
        Assert.assertEquals("https://engine.com/", data.searchEngineUrl.getSpec());
    }

    @Test
    public void initializeFromCache_withSerializedUrl() {
        final SharedPreferencesManager manager = ChromeSharedPreferences.getInstance();

        manager.writeString(SEARCH_WIDGET_SEARCH_ENGINE_SHORTNAME, "Engine");
        manager.writeString(
                SEARCH_WIDGET_SEARCH_ENGINE_URL, new GURL("https://engine.com").serialize());

        // Force re-read persisted data.
        SearchActivityPreferencesManager.resetForTesting();
        SearchActivityPreferences data = SearchActivityPreferencesManager.getCurrent();

        Assert.assertEquals("Engine", data.searchEngineName);
        Assert.assertEquals("https://engine.com/", data.searchEngineUrl.getSpec());
    }

    @Test
    public void updateFeatureAvailability() {
        ShadowLensController.sIsAvailable = true;
        ShadowVoiceRecognitionUtil.sIsAvailable = true;
        IncognitoUtils.setEnabledForTesting(true);

        SearchActivityPreferencesManager.updateFeatureAvailability(
                ContextUtils.getApplicationContext(), null);
        var data = SearchActivityPreferencesManager.getCurrent();
        Assert.assertTrue(data.googleLensAvailable);
        Assert.assertTrue(data.voiceSearchAvailable);
        Assert.assertTrue(data.incognitoAvailable);

        // Disable Lens.
        ShadowLensController.sIsAvailable = false;
        SearchActivityPreferencesManager.updateFeatureAvailability(
                ContextUtils.getApplicationContext(), null);
        data = SearchActivityPreferencesManager.getCurrent();
        Assert.assertFalse(data.googleLensAvailable);
        Assert.assertTrue(data.voiceSearchAvailable);
        Assert.assertTrue(data.incognitoAvailable);

        // Disable Voice.
        ShadowVoiceRecognitionUtil.sIsAvailable = false;
        SearchActivityPreferencesManager.updateFeatureAvailability(
                ContextUtils.getApplicationContext(), null);
        data = SearchActivityPreferencesManager.getCurrent();
        Assert.assertFalse(data.googleLensAvailable);
        Assert.assertFalse(data.voiceSearchAvailable);
        Assert.assertTrue(data.incognitoAvailable);

        // Disable Incognito.
        IncognitoUtils.setEnabledForTesting(false);
        SearchActivityPreferencesManager.updateFeatureAvailability(
                ContextUtils.getApplicationContext(), null);
        data = SearchActivityPreferencesManager.getCurrent();
        Assert.assertFalse(data.googleLensAvailable);
        Assert.assertFalse(data.voiceSearchAvailable);
        Assert.assertFalse(data.incognitoAvailable);
    }

    @Test
    public void onTemplateUrlServiceChanged_retrieveNewEngineNameAndUrl() {
        var oldData = SearchActivityPreferencesManager.getCurrent();

        // Simulate change.
        doReturn("Engine").when(mTemplateUrlMock).getShortName();
        doReturn("keyword").when(mTemplateUrlMock).getKeyword();
        doReturn("https://www.engine.com/some/path?with=query")
                .when(mTemplateUrlServiceMock)
                .getSearchEngineUrlFromTemplateUrl(eq("keyword"));
        doReturn(mTemplateUrlMock)
                .when(mTemplateUrlServiceMock)
                .getDefaultSearchEngineTemplateUrl();
        SearchActivityPreferencesManager.get().onTemplateURLServiceChanged();

        var newData = SearchActivityPreferencesManager.getCurrent();
        Assert.assertNotEquals(oldData, newData);
        Assert.assertEquals("Engine", newData.searchEngineName);
        // We only expect origin: no path, no query.
        Assert.assertEquals("https://www.engine.com/", newData.searchEngineUrl.getSpec());
    }
}