chromium/chrome/android/java/src/org/chromium/chrome/browser/searchwidget/SearchActivityUtils.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.app.Activity;
import android.app.SearchManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;

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

import org.chromium.base.ContextUtils;
import org.chromium.base.IntentUtils;
import org.chromium.base.Log;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.document.ChromeLauncherActivity;
import org.chromium.chrome.browser.omnibox.suggestions.OmniboxLoadUrlParams;
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.components.url_formatter.UrlFormatter;
import org.chromium.url.GURL;

/** Class facilitating interactions with the SearchActivity and the Omnibox. */
public class SearchActivityUtils {
    private static final String TAG = "SAUtils";

    /**
     * Retrieve the intent origin.
     *
     * @param intent intent received by SearchActivity
     * @return the origin of an intent
     */
    /* package */ static @IntentOrigin int getIntentOrigin(@NonNull Intent intent) {
        if (IntentUtils.isTrustedIntentFromSelf(intent)) {
            return IntentUtils.safeGetIntExtra(
                    intent, SearchActivityExtras.EXTRA_ORIGIN, IntentOrigin.UNKNOWN);
        }

        return IntentOrigin.UNKNOWN;
    }

    /**
     * @return the document url associated with the intent, if the intent is trusted and carries
     *     valid URL.
     */
    /* package */ static @Nullable GURL getIntentUrl(@NonNull Intent intent) {
        if (IntentUtils.isTrustedIntentFromSelf(intent)) {
            var gurl =
                    new GURL(
                            IntentUtils.safeGetStringExtra(
                                    intent, SearchActivityExtras.EXTRA_CURRENT_URL));
            if (!GURL.isEmptyOrInvalid(gurl)) return gurl;
        }
        return null;
    }

    /**
     * @return the package name on behalf of which the intent was issued.
     */
    /* package */ static @Nullable String getReferrer(@NonNull Intent intent) {
        String referrer = null;
        if (IntentUtils.isTrustedIntentFromSelf(intent)) {
            referrer = IntentUtils.safeGetStringExtra(intent, SearchActivityExtras.EXTRA_REFERRER);
            if (referrer != null
                    && !referrer.matches(SearchActivityExtras.REFERRER_VALIDATION_REGEX)) {
                Log.e(
                        TAG,
                        String.format(
                                "Invalid referrer: '%s' found. Referrer will be removed.",
                                referrer));
                referrer = null;
            }
        }
        return TextUtils.isEmpty(referrer) ? null : referrer;
    }

    /** Returns the caller-supplied initial search query. */
    /* package */ static @Nullable String getIntentQuery(@NonNull Intent intent) {
        // Unlike most other intents, this does not require trusted extras.
        return IntentUtils.safeGetStringExtra(intent, SearchManager.QUERY);
    }

    /**
     * Retrieve the intent search type.
     *
     * @param intent intent received by SearchActivity
     * @return the requested search type
     */
    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    public static @SearchType int getIntentSearchType(@NonNull Intent intent) {
        if (IntentUtils.isTrustedIntentFromSelf(intent)) {
            return IntentUtils.safeGetIntExtra(
                    intent, SearchActivityExtras.EXTRA_SEARCH_TYPE, SearchType.TEXT);
        }

        return SearchType.TEXT;
    }

    /**
     * Resolve the {@link requestOmniboxForResult}.
     *
     * @param activity the activity resolving the request
     * @param url optional URL dictating how to resolve the request: null/invalid/empty value
     *     results with canceled request; anything else resolves request successfully
     */
    /* package */ static void resolveOmniboxRequestForResult(
            @NonNull Activity activity, @NonNull OmniboxLoadUrlParams params) {
        var intent = createLoadUrlIntent(activity, activity.getCallingActivity(), params);
        if (intent != null) {
            activity.setResult(Activity.RESULT_OK, intent);
        } else {
            activity.setResult(Activity.RESULT_CANCELED);
        }
    }

    /**
     * Creates an intent that can be used to launch Chrome.
     *
     * @param context current context
     * @param params information about what url to load and what additional data to pass
     * @return the intent will be passed to ChromeLauncherActivity, or null if page cannot be loaded
     */
    /* package */ static @Nullable Intent createIntentForStartActivity(
            Context context, OmniboxLoadUrlParams params) {
        var intent =
                createLoadUrlIntent(
                        context,
                        new ComponentName(
                                ContextUtils.getApplicationContext(), ChromeLauncherActivity.class),
                        params);
        if (intent == null) return null;

        intent.setAction(Intent.ACTION_VIEW);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);

        return intent;
    }

    /**
     * Create a base intent that can be further expanded to request URL loading.
     *
     * @param context the current context
     * @param recipient the activity being targeted
     * @param params the OmniboxLoadUrlParams describing what URL to load and what extra data to
     *     pass
     * @return Intent, if all the supplied data is valid, otherwise null
     */
    @VisibleForTesting
    /* package */ static @Nullable Intent createLoadUrlIntent(
            Context context, ComponentName recipient, OmniboxLoadUrlParams params) {
        // Don't do anything if the input was empty.
        if (params == null || TextUtils.isEmpty(params.url)) return null;

        // Fix up the URL and send it to the full browser.
        GURL fixedUrl = UrlFormatter.fixupUrl(params.url);
        if (GURL.isEmptyOrInvalid(fixedUrl)) return null;

        var intent =
                new Intent()
                        .putExtra(SearchActivity.EXTRA_FROM_SEARCH_ACTIVITY, true)
                        .setComponent(recipient)
                        .setData(Uri.parse(fixedUrl.getSpec()));

        // Do not pass any of these information if the calling package is something we did not
        // expect, but somehow it managed to fabricate a trust token.
        if (!IntentUtils.intentTargetsSelf(context, intent)) {
            return null;
        }

        if (!TextUtils.isEmpty(params.postDataType)
                && params.postData != null
                && params.postData.length != 0) {
            intent.putExtra(IntentHandler.EXTRA_POST_DATA_TYPE, params.postDataType)
                    .putExtra(IntentHandler.EXTRA_POST_DATA, params.postData);
        }
        IntentUtils.addTrustedIntentExtras(intent);

        return intent;
    }
}