chromium/components/search_engines/android/java/src/org/chromium/components/search_engines/TemplateUrlService.java

// Copyright 2013 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.components.search_engines;

import androidx.annotation.Nullable;

import org.jni_zero.CalledByNative;
import org.jni_zero.NativeMethods;

import org.chromium.base.ObserverList;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.url.GURL;

import java.util.ArrayList;
import java.util.List;

/**
 * Android wrapper of the TemplateUrlService which provides access from the Java
 * layer.
 *
 * Only usable from the UI thread as it's primary purpose is for supporting the Android
 * preferences UI.
 *
 * See components/search_engines/template_url_service.h for more details.
 */
public class TemplateUrlService {
    /** This listener will be notified when template url service is done loading. */
    public interface LoadListener {
        void onTemplateUrlServiceLoaded();
    }

    /** Observer to be notified whenever the set of TemplateURLs are modified. */
    public interface TemplateUrlServiceObserver {
        /** Notification that the template url model has changed in some way. */
        void onTemplateURLServiceChanged();
    }

    private final ObserverList<LoadListener> mLoadListeners = new ObserverList<>();
    private final ObserverList<TemplateUrlServiceObserver> mObservers = new ObserverList<>();
    private long mNativeTemplateUrlServiceAndroid;

    private TemplateUrlService(long nativeTemplateUrlServiceAndroid) {
        // Note that this technically leaks the native object, however, TemplateUrlService
        // is a singleton that lives forever and there's no clean shutdown of Chrome on Android.
        mNativeTemplateUrlServiceAndroid = nativeTemplateUrlServiceAndroid;
    }

    @CalledByNative
    private static TemplateUrlService create(long nativeTemplateUrlServiceAndroid) {
        return new TemplateUrlService(nativeTemplateUrlServiceAndroid);
    }

    @CalledByNative
    private void clearNativePtr() {
        mNativeTemplateUrlServiceAndroid = 0;
    }

    public boolean isLoaded() {
        ThreadUtils.assertOnUiThread();
        return TemplateUrlServiceJni.get()
                .isLoaded(mNativeTemplateUrlServiceAndroid, TemplateUrlService.this);
    }

    public void load() {
        ThreadUtils.assertOnUiThread();
        TemplateUrlServiceJni.get().load(mNativeTemplateUrlServiceAndroid, TemplateUrlService.this);
    }

    /**
     * Ensure the TemplateUrlService is loaded before running the specified action.  If the service
     * is already loaded, then run the action immediately.
     * <p>
     * Because this can introduce an arbitrary delay in the action being executed, ensure the state
     * is still valid in the action before interacting with anything that might no longer be
     * available (i.e. an Activity that has since been destroyed).
     *
     * @param action The action to be run.
     */
    public void runWhenLoaded(final Runnable action) {
        if (isLoaded()) {
            action.run();
        } else {
            registerLoadListener(
                    new LoadListener() {
                        @Override
                        public void onTemplateUrlServiceLoaded() {
                            unregisterLoadListener(this);
                            action.run();
                        }
                    });
            load();
        }
    }

    /** Returns a list of the all available search engines. */
    public List<TemplateUrl> getTemplateUrls() {
        ThreadUtils.assertOnUiThread();
        List<TemplateUrl> templateUrls = new ArrayList<>();
        TemplateUrlServiceJni.get()
                .getTemplateUrls(
                        mNativeTemplateUrlServiceAndroid, TemplateUrlService.this, templateUrls);
        return templateUrls;
    }

    /**
     * Called from native to populate the list of all available search engines.
     * @param templateUrls The list of {@link TemplateUrl} to be added.
     * @param templateUrl The {@link TemplateUrl} would add to the list.
     */
    @CalledByNative
    private static void addTemplateUrlToList(
            List<TemplateUrl> templateUrls, TemplateUrl templateUrl) {
        templateUrls.add(templateUrl);
    }

    /** Called from native when template URL service is done loading. */
    @CalledByNative
    private void templateUrlServiceLoaded() {
        ThreadUtils.assertOnUiThread();
        for (LoadListener listener : mLoadListeners) {
            listener.onTemplateUrlServiceLoaded();
        }
    }

    @CalledByNative
    private void onTemplateURLServiceChanged() {
        for (TemplateUrlServiceObserver observer : mObservers) {
            observer.onTemplateURLServiceChanged();
        }
    }

    /**
     * @return {@link TemplateUrl} for the default search engine.  This can
     *         be null if DSEs are disabled entirely by administrators.
     */
    public @Nullable TemplateUrl getDefaultSearchEngineTemplateUrl() {
        if (!isLoaded()) return null;
        return TemplateUrlServiceJni.get()
                .getDefaultSearchEngine(mNativeTemplateUrlServiceAndroid, TemplateUrlService.this);
    }

    public void setSearchEngine(String selectedKeyword, @ChoiceMadeLocation int choiceLocation) {
        ThreadUtils.assertOnUiThread();
        TemplateUrlServiceJni.get()
                .setUserSelectedDefaultSearchProvider(
                        mNativeTemplateUrlServiceAndroid,
                        TemplateUrlService.this,
                        selectedKeyword,
                        choiceLocation);
    }

    public void setSearchEngine(String selectedKeyword) {
        ThreadUtils.assertOnUiThread();
        TemplateUrlServiceJni.get()
                .setUserSelectedDefaultSearchProvider(
                        mNativeTemplateUrlServiceAndroid,
                        TemplateUrlService.this,
                        selectedKeyword,
                        ChoiceMadeLocation.OTHER);
    }

    /**
     * @return Whether the default search engine is managed and controlled by policy.  If true, the
     *         DSE can not be modified by the user.
     */
    public boolean isDefaultSearchManaged() {
        return TemplateUrlServiceJni.get()
                .isDefaultSearchManaged(mNativeTemplateUrlServiceAndroid, TemplateUrlService.this);
    }

    /** @return Whether or not the default search engine has search by image support. */
    public boolean isSearchByImageAvailable() {
        ThreadUtils.assertOnUiThread();
        return TemplateUrlServiceJni.get()
                .isSearchByImageAvailable(
                        mNativeTemplateUrlServiceAndroid, TemplateUrlService.this);
    }

    /**
     * @return Whether or not the default search engine has a logo image to show
     *    on NTP or start surface.
     */
    public boolean doesDefaultSearchEngineHaveLogo() {
        return TemplateUrlServiceJni.get()
                .doesDefaultSearchEngineHaveLogo(
                        mNativeTemplateUrlServiceAndroid, TemplateUrlService.this);
    }

    /** @return Whether the default configured search engine is for a Google property. */
    public boolean isDefaultSearchEngineGoogle() {
        return TemplateUrlServiceJni.get()
                .isDefaultSearchEngineGoogle(
                        mNativeTemplateUrlServiceAndroid, TemplateUrlService.this);
    }

    /**
     * Checks whether a search result page is from a default search provider.
     * @param url The url for the search result page.
     * @return Whether the search result page with the given url from the default search provider.
     */
    public boolean isSearchResultsPageFromDefaultSearchProvider(GURL url) {
        ThreadUtils.assertOnUiThread();
        return TemplateUrlServiceJni.get()
                .isSearchResultsPageFromDefaultSearchProvider(
                        mNativeTemplateUrlServiceAndroid, TemplateUrlService.this, url);
    }

    /**
     * Registers a listener for the callback that indicates that the
     * TemplateURLService has loaded.
     */
    public void registerLoadListener(final LoadListener listener) {
        ThreadUtils.assertOnUiThread();
        boolean added = mLoadListeners.addObserver(listener);
        assert added;

        // If the load has already been completed, post a load complete to the observer.  Done
        // as an asynchronous call to keep the client code predictable in the loaded/unloaded state.
        if (isLoaded()) {
            PostTask.postTask(
                    TaskTraits.UI_DEFAULT,
                    () -> {
                        if (!mLoadListeners.hasObserver(listener)) return;

                        listener.onTemplateUrlServiceLoaded();
                    });
        }
    }

    /**
     * Unregisters a listener for the callback that indicates that the
     * TemplateURLService has loaded.
     */
    public void unregisterLoadListener(LoadListener listener) {
        ThreadUtils.assertOnUiThread();
        boolean removed = mLoadListeners.removeObserver(listener);
        assert removed;
    }

    /**
     * Adds an observer to be notified on changes to the template URLs.
     * @param observer The observer to be added.
     */
    public void addObserver(TemplateUrlServiceObserver observer) {
        mObservers.addObserver(observer);
    }

    /**
     * Removes an observer for changes to the template URLs.
     * @param observer The observer to be removed.
     */
    public void removeObserver(TemplateUrlServiceObserver observer) {
        mObservers.removeObserver(observer);
    }

    /**
     * Finds the default search engine for the default provider and returns the url query
     * {@link String} for {@code query}.
     * @param query The {@link String} that represents the text query the search url should
     *              represent.
     * @return      A {@link String} that contains the url of the default search engine with
     *              {@code query} inserted as the search parameter.
     */
    public String getUrlForSearchQuery(String query) {
        return getUrlForSearchQuery(query, null);
    }

    /**
     * Finds the default search engine for the default provider and returns the url query
     * {@link String} for {@code query}.
     * @param query The {@link String} that represents the text query the search url should
     *              represent.
     * @param searchParams A list of search params to be appended to the query.
     * @return      A {@link String} that contains the url of the default search engine with
     *              {@code query} inserted as the search parameter.
     */
    public String getUrlForSearchQuery(String query, List<String> searchParams) {
        return TemplateUrlServiceJni.get()
                .getUrlForSearchQuery(
                        mNativeTemplateUrlServiceAndroid,
                        TemplateUrlService.this,
                        query,
                        searchParams == null ? null : searchParams.toArray(new String[0]));
    }

    /**
     * See {@link #getSearchQueryForUrl(GURL)}.
     *
     * @deprecated Please use {@link #getSearchQueryForUrl(GURL)} instead.
     */
    @Deprecated
    public String getSearchQueryForUrl(String url) {
        return getSearchQueryForUrl(new GURL(url));
    }

    /** Finds the query in the url, if any. Returns empty if no query is present. */
    public String getSearchQueryForUrl(GURL url) {
        return TemplateUrlServiceJni.get()
                .getSearchQueryForUrl(
                        mNativeTemplateUrlServiceAndroid, TemplateUrlService.this, url);
    }

    /**
     * Finds the default search engine for the default provider and returns the url query
     * {@link String} for {@code query} with voice input source param set.
     * @param query The {@link String} that represents the text query the search url should
     *              represent.
     * @return      A {@link String} that contains the url of the default search engine with
     *              {@code query} inserted as the search parameter and voice input source param set.
     */
    public GURL getUrlForVoiceSearchQuery(String query) {
        return TemplateUrlServiceJni.get()
                .getUrlForVoiceSearchQuery(
                        mNativeTemplateUrlServiceAndroid, TemplateUrlService.this, query);
    }

    /**
     * Finds the default search engine for the default provider and returns the url query
     * {@link String} for {@code query} with the contextual search version param set.
     * @param query The search term to use as the main query in the returned search url.
     * @param alternateTerm The alternate search term to use as an alternate suggestion.
     * @param shouldPrefetch Whether the returned url should include a prefetch parameter.
     * @param protocolVersion The version of the Contextual Search API protocol to use.
     * @return      A {@link String} that contains the url of the default search engine with
     *              {@code query} and {@code alternateTerm} inserted as parameters and contextual
     *              search and prefetch parameters conditionally set.
     */
    public GURL getUrlForContextualSearchQuery(
            String query, String alternateTerm, boolean shouldPrefetch, String protocolVersion) {
        return TemplateUrlServiceJni.get()
                .getUrlForContextualSearchQuery(
                        mNativeTemplateUrlServiceAndroid,
                        TemplateUrlService.this,
                        query,
                        alternateTerm,
                        shouldPrefetch,
                        protocolVersion);
    }

    /**
     * Finds the URL for the search engine for the given keyword.
     * @param keyword The templateUrl keyword to look up.
     * @return      A {@link String} that contains the url of the specified search engine.
     */
    public String getSearchEngineUrlFromTemplateUrl(String keyword) {
        return TemplateUrlServiceJni.get()
                .getSearchEngineUrlFromTemplateUrl(
                        mNativeTemplateUrlServiceAndroid, TemplateUrlService.this, keyword);
    }

    /**
     * Finds the search engine type for the given keyword.
     * @param keyword The templateUrl keyword to look up.
     * @return      The search engine type of the specified search engine that contains the keyword.
     */
    public int getSearchEngineTypeFromTemplateUrl(String keyword) {
        return TemplateUrlServiceJni.get()
                .getSearchEngineTypeFromTemplateUrl(
                        mNativeTemplateUrlServiceAndroid, TemplateUrlService.this, keyword);
    }

    /**
     * Adds a search engine, set by Play API.
     *
     * @param name The name of the search engine to be added.
     * @param keyword The keyword of the search engine to be added.
     * @param searchUrl Search url of the search engine to be added.
     * @param suggestUrl Url for retrieving search suggestions.
     * @param faviconUrl Favicon url of the search engine to be added.
     * @param newTabUrl Url to be shown on a new tab.
     * @param imageUrl Url for reverse image search.
     * @param imageUrlPostParams The POST param name to use for the image payload transmitted.
     * @param imageTranslateUrl Url for image translation.
     * @param imageTranslateSourceLanguageParamKey Key of the url parameter identifying the source
     *     language for an image translation.
     * @param imageTranslateTargetLanguageParamKey Key of the url parameter identifying the target
     *     language for an image translation.
     * @return True if search engine was successfully added, false if search engine from Play API
     *     with such keyword already existed (e.g. from previous attempt to set search engine).
     */
    public boolean setPlayAPISearchEngine(
            String name,
            String keyword,
            String searchUrl,
            String suggestUrl,
            String faviconUrl,
            String newTabUrl,
            String imageUrl,
            String imageUrlPostParams,
            String imageTranslateUrl,
            String imageTranslateSourceLanguageParamKey,
            String imageTranslateTargetLanguageParamKey) {
        return TemplateUrlServiceJni.get()
                .setPlayAPISearchEngine(
                        mNativeTemplateUrlServiceAndroid,
                        TemplateUrlService.this,
                        name,
                        keyword,
                        searchUrl,
                        suggestUrl,
                        faviconUrl,
                        newTabUrl,
                        imageUrl,
                        imageUrlPostParams,
                        imageTranslateUrl,
                        imageTranslateSourceLanguageParamKey,
                        imageTranslateTargetLanguageParamKey);
    }

    public String addSearchEngineForTesting(String keyword, int ageInDays) {
        return TemplateUrlServiceJni.get()
                .addSearchEngineForTesting(
                        mNativeTemplateUrlServiceAndroid,
                        TemplateUrlService.this,
                        keyword,
                        ageInDays);
    }

    /**
     * Finds the image search url for the search engine used and the post content type for the
     * image.
     * @return An array of size 2 with the first element being the image search url, the second
     *         being the post content type.
     */
    public String[] getImageUrlAndPostContent() {
        return TemplateUrlServiceJni.get()
                .getImageUrlAndPostContent(
                        mNativeTemplateUrlServiceAndroid, TemplateUrlService.this);
    }

    /**
     * Whether the device is from an EEA country. This is consistent with countries which are
     * eligible for the EEA default search engine choice prompt. "Default country: or "country at
     * install" are used for SearchEngineChoiceCountry. It might be different than what LocaleUtils
     * returns.
     */
    public boolean isEeaChoiceCountry() {
        return TemplateUrlServiceJni.get().isEeaChoiceCountry(mNativeTemplateUrlServiceAndroid);
    }

    /**
     * Whether the version of the search engines settings screen showing additional search engine
     * info should be shown.
     */
    public boolean shouldShowUpdatedSettings() {
        return TemplateUrlServiceJni.get()
                .shouldShowUpdatedSettings(mNativeTemplateUrlServiceAndroid);
    }

    @NativeMethods
    public interface Natives {
        void load(long nativeTemplateUrlServiceAndroid, TemplateUrlService caller);

        boolean isLoaded(long nativeTemplateUrlServiceAndroid, TemplateUrlService caller);

        void setUserSelectedDefaultSearchProvider(
                long nativeTemplateUrlServiceAndroid,
                TemplateUrlService caller,
                String selectedKeyword,
                int choiceLocation);

        boolean isDefaultSearchManaged(
                long nativeTemplateUrlServiceAndroid, TemplateUrlService caller);

        boolean isSearchResultsPageFromDefaultSearchProvider(
                long nativeTemplateUrlServiceAndroid, TemplateUrlService caller, GURL url);

        boolean isSearchByImageAvailable(
                long nativeTemplateUrlServiceAndroid, TemplateUrlService caller);

        boolean doesDefaultSearchEngineHaveLogo(
                long nativeTemplateUrlServiceAndroid, TemplateUrlService caller);

        boolean isDefaultSearchEngineGoogle(
                long nativeTemplateUrlServiceAndroid, TemplateUrlService caller);

        String getUrlForSearchQuery(
                long nativeTemplateUrlServiceAndroid,
                TemplateUrlService caller,
                String query,
                String[] searchParams);

        String getSearchQueryForUrl(
                long nativeTemplateUrlServiceAndroid, TemplateUrlService caller, GURL url);

        GURL getUrlForVoiceSearchQuery(
                long nativeTemplateUrlServiceAndroid, TemplateUrlService caller, String query);

        GURL getUrlForContextualSearchQuery(
                long nativeTemplateUrlServiceAndroid,
                TemplateUrlService caller,
                String query,
                String alternateTerm,
                boolean shouldPrefetch,
                String protocolVersion);

        String getSearchEngineUrlFromTemplateUrl(
                long nativeTemplateUrlServiceAndroid, TemplateUrlService caller, String keyword);

        int getSearchEngineTypeFromTemplateUrl(
                long nativeTemplateUrlServiceAndroid, TemplateUrlService caller, String keyword);

        String addSearchEngineForTesting(
                long nativeTemplateUrlServiceAndroid,
                TemplateUrlService caller,
                String keyword,
                int offset);

        boolean setPlayAPISearchEngine(
                long nativeTemplateUrlServiceAndroid,
                TemplateUrlService caller,
                String name,
                String keyword,
                String searchUrl,
                String suggestUrl,
                String faviconUrl,
                String newTabUrl,
                String imageUrl,
                String imageUrlPostParams,
                String imageTranslateUrl,
                String imageTranslateSourceLanguageParamKey,
                String imageTranslateTargetLanguageParamKey);

        void getTemplateUrls(
                long nativeTemplateUrlServiceAndroid,
                TemplateUrlService caller,
                List<TemplateUrl> templateUrls);

        TemplateUrl getDefaultSearchEngine(
                long nativeTemplateUrlServiceAndroid, TemplateUrlService caller);

        String[] getImageUrlAndPostContent(
                long nativeTemplateUrlServiceAndroid, TemplateUrlService caller);

        boolean isEeaChoiceCountry(long nativeTemplateUrlServiceAndroid);

        boolean shouldShowUpdatedSettings(long nativeTemplateUrlServiceAndroid);
    }
}