chromium/chrome/android/java/src/org/chromium/chrome/browser/searchwidget/SearchActivityClientImpl.java

// Copyright 2024 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.searchwidget;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityOptions;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.IntentUtils;
import org.chromium.base.Log;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.ui.searchactivityutils.SearchActivityClient;
import org.chromium.chrome.browser.ui.searchactivityutils.SearchActivityExtras;
import org.chromium.chrome.browser.ui.searchactivityutils.SearchActivityExtras.IntentOrigin;
import org.chromium.chrome.browser.ui.searchactivityutils.SearchActivityExtras.SearchType;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.common.ResourceRequestBody;
import org.chromium.url.GURL;

/** Class with logic enabling clients to interact with SearchActivity. */
public class SearchActivityClientImpl implements SearchActivityClient {
    private static final String TAG = "SAClient";

    @VisibleForTesting
    /* package */ static final int OMNIBOX_REQUEST_CODE = 'O' << 24 | 'M' << 16 | 'N' << 8 | 'I';

    // Note: while we don't rely on Actions, PendingIntents do require them to be Unique.
    // Responsibility to define values for PendingIntents could be offset to Caller; meantime we
    // offer complimentary default values.
    @VisibleForTesting
    /* package */ static final String ACTION_SEARCH_FORMAT =
            "org.chromium.chrome.browser.ui.searchactivityutils.ACTION_SEARCH:%d:%d";

    @Override
    public Intent createIntent(
            @NonNull Context context,
            @IntentOrigin int origin,
            @Nullable GURL url,
            @SearchType int searchType) {
        // Ensure `action` is unique especially across different Widget implementations.
        // Otherwise, a QuickActionSearchWidget action may override the SearchActivity widget,
        // triggering functionality we might not want to activate.
        @SuppressLint("DefaultLocale")
        String action = String.format(ACTION_SEARCH_FORMAT, origin, searchType);

        var intent = buildTrustedIntent(context, action);
        intent.putExtra(SearchActivityExtras.EXTRA_ORIGIN, origin)
                .putExtra(SearchActivityExtras.EXTRA_SEARCH_TYPE, searchType)
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
                .putExtra(
                        SearchActivityExtras.EXTRA_CURRENT_URL,
                        GURL.isEmptyOrInvalid(url) ? null : url.getSpec());

        return intent;
    }

    /**
     * Call up SearchActivity/Omnibox on behalf of the current Activity.
     *
     * <p>Allows the caller to instantiate the Omnibox and retrieve Suggestions for the supplied
     * webpage. Response will be delivered via {@link Activity#onActivityResult}.
     *
     * @param activity the current activity; may be {@code null}, in which case intent will not be
     *     issued
     * @param url the URL of the page to retrieve suggestions for
     * @param referrer the referrer package name
     */
    public static void requestOmniboxForResult(
            @Nullable Activity activity, @NonNull GURL currentUrl, @Nullable String referrer) {
        if (activity == null) return;

        if (referrer != null && !referrer.matches(SearchActivityExtras.REFERRER_VALIDATION_REGEX)) {
            Log.e(
                    TAG,
                    String.format(
                            "Referrer: '%s' failed to match Re pattern '%s' and will be ignored.",
                            referrer, SearchActivityExtras.REFERRER_VALIDATION_REGEX));
            referrer = null;
        }

        @SuppressLint("DefaultLocale")
        var intent =
                buildTrustedIntent(
                                activity,
                                String.format(
                                        ACTION_SEARCH_FORMAT,
                                        IntentOrigin.CUSTOM_TAB,
                                        SearchType.TEXT))
                        .putExtra(
                                SearchActivityExtras.EXTRA_CURRENT_URL,
                                GURL.isEmptyOrInvalid(currentUrl) ? null : currentUrl.getSpec())
                        .putExtra(SearchActivityExtras.EXTRA_ORIGIN, IntentOrigin.CUSTOM_TAB)
                        .putExtra(
                                SearchActivityExtras.EXTRA_REFERRER,
                                TextUtils.isEmpty(referrer) ? null : referrer)
                        .putExtra(SearchActivityExtras.EXTRA_SEARCH_TYPE, SearchType.TEXT)
                        .addFlags(
                                Intent.FLAG_ACTIVITY_NO_HISTORY
                                        | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);

        activity.startActivityForResult(
                intent,
                OMNIBOX_REQUEST_CODE,
                ActivityOptions.makeCustomAnimation(
                                activity, android.R.anim.fade_in, R.anim.no_anim)
                        .toBundle());
    }

    /**
     * Utility method to determine whether the {@link Activity#onActivityResult} payload carries the
     * response to {@link requestOmniboxForResult}.
     *
     * @param requestCode the request code received in {@link Activity#onActivityResult}
     * @param intent the intent data received in {@link Activity#onActivityResult}
     * @return true if the response captures legitimate Omnibox result.
     */
    public static boolean isOmniboxResult(int requestCode, @NonNull Intent intent) {
        return requestCode == OMNIBOX_REQUEST_CODE
                && IntentUtils.isTrustedIntentFromSelf(intent)
                && !TextUtils.isEmpty(intent.getDataString());
    }

    /**
     * Process the {@link Activity#onActivityResult} payload for Omnibox navigation result.
     *
     * @param requestCode the request code received in {@link Activity#onActivityResult}
     * @param resultCode the result code received in {@link Activity#onActivityResult}
     * @param intent the intent data received in {@link Activity#onActivityResult}
     * @return null, if result is not a valid Omnibox result, otherwise valid LoadUrlParams object
     */
    public static @Nullable LoadUrlParams getOmniboxResult(
            int requestCode, int resultCode, @NonNull Intent intent) {
        if (!isOmniboxResult(requestCode, intent)) return null;
        if (resultCode != Activity.RESULT_OK) return null;
        var url = new GURL(intent.getDataString());
        if (GURL.isEmptyOrInvalid(url)) return null;

        var params = new LoadUrlParams(url);
        byte[] postData = IntentUtils.safeGetByteArrayExtra(intent, IntentHandler.EXTRA_POST_DATA);
        String postDataType =
                IntentUtils.safeGetStringExtra(intent, IntentHandler.EXTRA_POST_DATA_TYPE);
        if (!TextUtils.isEmpty(postDataType) && postData != null && postData.length > 0) {
            params.setVerbatimHeaders("Content-Type: " + postDataType);
            params.setPostData(ResourceRequestBody.createFromBytes(postData));
        }
        return params;
    }

    /**
     * Create a trusted intent that can be used to start the Search Activity.
     *
     * @param context current context
     * @param action action to be associated with the intent
     */
    @VisibleForTesting
    /* package */ static Intent buildTrustedIntent(
            @NonNull Context context, @NonNull String action) {
        var intent =
                new Intent(action).setComponent(new ComponentName(context, SearchActivity.class));
        IntentUtils.addTrustedIntentExtras(intent);
        return intent;
    }
}