Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 272de368 authored by Andy Wickham's avatar Andy Wickham
Browse files

Add ContextualSearchConfig to add display id, etc.

Current fields:
 - Display ID (where the screenshot and interaction happen)
 - Source bounds of the invocation (navbar, home button, etc)
 - Intent extras to be passed to the Contextual Search app

NO_EXPORTED_FLAG_DELETION_CHECK=The API guarded by the
self_invocation flag was never released and is being replaced
by new APIs guarded by config_parameters.

Bug: 371552433
Bug: 401598818
Bug: 368653769
Test: Manual with stashed taskbar and Launcher support app.
Flag: android.app.contextualsearch.flags.config_parameters
Change-Id: I35e3c55edaf0b25f95e75632a6a8b4b335159db1
parent 7b9ee772
Loading
Loading
Loading
Loading
+21 −2
Original line number Diff line number Diff line
@@ -2285,10 +2285,29 @@ package android.app.contextualsearch {
    field @NonNull public static final android.os.Parcelable.Creator<android.app.contextualsearch.CallbackToken> CREATOR;
  }
  @FlaggedApi("android.app.contextualsearch.flags.config_parameters") public final class ContextualSearchConfig implements android.os.Parcelable {
    method public int describeContents();
    method public int getDisplayId();
    method @NonNull public android.os.Bundle getIntentExtras();
    method @Nullable public android.graphics.Rect getSourceBounds();
    method public void writeToParcel(@NonNull android.os.Parcel, int);
    field @NonNull public static final android.os.Parcelable.Creator<android.app.contextualsearch.ContextualSearchConfig> CREATOR;
  }
  public static final class ContextualSearchConfig.Builder {
    ctor public ContextualSearchConfig.Builder();
    ctor public ContextualSearchConfig.Builder(@NonNull android.app.contextualsearch.ContextualSearchConfig);
    method @NonNull public android.app.contextualsearch.ContextualSearchConfig build();
    method @NonNull public android.app.contextualsearch.ContextualSearchConfig.Builder setDisplayId(int);
    method @NonNull public android.app.contextualsearch.ContextualSearchConfig.Builder setIntentExtras(@Nullable android.os.Bundle);
    method @NonNull public android.app.contextualsearch.ContextualSearchConfig.Builder setSourceBounds(@Nullable android.graphics.Rect);
  }
  public final class ContextualSearchManager {
    method @FlaggedApi("android.app.contextualsearch.flags.self_invocation") public boolean isContextualSearchAvailable();
    method @FlaggedApi("android.app.contextualsearch.flags.config_parameters") public boolean isContextualSearchAvailable();
    method @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXTUAL_SEARCH) public void startContextualSearch(int);
    method @FlaggedApi("android.app.contextualsearch.flags.self_invocation") public void startContextualSearch();
    method @FlaggedApi("android.app.contextualsearch.flags.config_parameters") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXTUAL_SEARCH) public void startContextualSearch(int, @Nullable android.app.contextualsearch.ContextualSearchConfig);
    method @FlaggedApi("android.app.contextualsearch.flags.config_parameters") public void startContextualSearch(@NonNull android.app.Activity, @Nullable android.app.contextualsearch.ContextualSearchConfig);
    field public static final String ACTION_LAUNCH_CONTEXTUAL_SEARCH = "android.app.contextualsearch.action.LAUNCH_CONTEXTUAL_SEARCH";
    field public static final int ENTRYPOINT_LONG_PRESS_HOME = 2; // 0x2
    field public static final int ENTRYPOINT_LONG_PRESS_META = 10; // 0xa
+208 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.app.contextualsearch;

import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.app.contextualsearch.flags.Flags;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.Display;

import java.util.Objects;

/**
 * Configuration for Contextual Search invocations. Typically the parameters added here are passed
 * to the Contextual Search provider app as specified by the device configuration.
 *
 * @hide
 */
@FlaggedApi(Flags.FLAG_CONFIG_PARAMETERS)
@SystemApi
public final class ContextualSearchConfig implements Parcelable {

    private final int mDisplayId;
    @Nullable private final Rect mSourceBounds;
    @NonNull private final Bundle mIntentExtras;

    public static final @NonNull Creator<ContextualSearchConfig> CREATOR =
            new Creator<>() {
                @Override
                public ContextualSearchConfig createFromParcel(@NonNull Parcel in) {
                    return new ContextualSearchConfig(in);
                }

                @Override
                public ContextualSearchConfig[] newArray(int size) {
                    return new ContextualSearchConfig[size];
                }
            };

    ContextualSearchConfig(@NonNull Parcel in) {
        mDisplayId = in.readInt();
        mSourceBounds = in.readTypedObject(Rect.CREATOR);
        mIntentExtras = Objects.requireNonNull(
                in.readBundle(ContextualSearchConfig.class.getClassLoader()));
    }

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeInt(mDisplayId);
        dest.writeTypedObject(mSourceBounds, flags);
        dest.writeBundle(mIntentExtras);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    private ContextualSearchConfig(@NonNull Builder builder) {
        mDisplayId = builder.mDisplayId;
        mSourceBounds = builder.mSourceBounds;
        mIntentExtras = builder.mIntentExtras;
    }

    /**
     * @return The ID of the display where the search was triggered. This determines where the
     *         screenshot is taken and displayed for user interaction. If the display ID is invalid,
     *         the invocation will fail silently. If not specified, the system will use
     *         {@link Display#DEFAULT_DISPLAY}, or the Activity's display if launched from an
     *         Activity.
     */
    public int getDisplayId() {
        return mDisplayId;
    }

    /**
     * @return The bounds of the source element that triggered the search, in screen coordinates.
     *         Can be null if not available.
     */
    @Nullable
    public Rect getSourceBounds() {
        return mSourceBounds == null ? null : new Rect(mSourceBounds);
    }

    /**
     * @return Extras to be added to the Intent sent to the Contextual Search app. These will be
     *         merged with any other extras added to the Intent by ContextualSearchManagerService.
     */
    @NonNull
    public Bundle getIntentExtras() {
        return new Bundle(mIntentExtras);
    }

    @Override
    public String toString() {
        return "ContextualSearchConfig{"
            + "mDisplayId=" + mDisplayId + ", "
            + "mSourceBounds=" + mSourceBounds + ", "
            + "mIntentExtras=" + mIntentExtras
            + '}';
    }

    /**
     * Builder to create a {@link ContextualSearchConfig}.
     */
    public static final class Builder {

        private int mDisplayId;
        @Nullable private Rect mSourceBounds;
        @NonNull private final Bundle mIntentExtras;

        /**
         * Creates a new Builder with default values.
         */
        public Builder() {
            mDisplayId = Display.INVALID_DISPLAY;
            mSourceBounds = null;
            mIntentExtras = new Bundle();
        }

        /**
         * Creates a new builder and initializes it with the values from the given
         * {@link ContextualSearchConfig}.
         *
         * @param config The config to copy values from.
         */
        public Builder(@NonNull ContextualSearchConfig config) {
            mDisplayId = config.getDisplayId();
            mSourceBounds = config.getSourceBounds();
            mIntentExtras = config.getIntentExtras();
        }

        /**
         * Sets the display ID for the contextual search invocation.
         *
         * @param displayId The ID of the display where the search was triggered. This determines
         *                  where the screenshot is taken and displayed for user interaction. If the
         *                  display ID is invalid, the invocation will fail silently. If not
         *                  specified, the system will use {@link Display#DEFAULT_DISPLAY}, or the
         *                  Activity's display if launched from an Activity.
         * @return This Builder object to allow for chaining of calls.
         */
        @NonNull
        public Builder setDisplayId(int displayId) {
            mDisplayId = displayId;
            return this;
        }

        /**
         * Sets the source bounds for the contextual search invocation.
         *
         * @param sourceBounds The bounds of the source element that triggered the search, in screen
         *                     coordinates. Can be null if not available.
         * @return This Builder object to allow for chaining of calls.
         */
        @NonNull
        public Builder setSourceBounds(@Nullable Rect sourceBounds) {
            mSourceBounds = sourceBounds == null ? null : new Rect(sourceBounds);
            return this;
        }

        /**
         * Sets any additional extras to be added to the intent sent to the Contextual Search app.
         *
         * @param intentExtras This will be merged with any other extras added to the intent by
         *                     ContextualSearchManagerService. To avoid having your extras
         *                     overwritten, prefix the keys with an agreed package name.
         * @return This Builder object to allow for chaining of calls.
         */
        @NonNull
        public Builder setIntentExtras(@Nullable Bundle intentExtras) {
            mIntentExtras.clear();
            if (intentExtras != null) {
                mIntentExtras.putAll(intentExtras);
            }
            return this;
        }

        /**
         * Builds the {@link ContextualSearchConfig} instance.
         *
         * @return The built {@link ContextualSearchConfig} object.
         */
        @NonNull
        public ContextualSearchConfig build() {
            return new ContextualSearchConfig(this);
        }
    }
}
+77 −22
Original line number Diff line number Diff line
@@ -20,8 +20,11 @@ import static android.Manifest.permission.ACCESS_CONTEXTUAL_SEARCH;

import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.app.Activity;
import android.app.contextualsearch.flags.Flags;
import android.content.Context;
import android.os.IBinder;
@@ -29,19 +32,20 @@ import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.util.Log;
import android.view.Display;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

/**
 * {@link ContextualSearchManager} is a system service to facilitate contextual search experience on
 * configured Android devices.
 * <p>
 * This class lets a caller start contextual search by calling {@link #startContextualSearch}
 * method.
 * configured Android devices. This involves capturing screenshots that the Contextual Search system
 * app presents to the user for interaction, such as selecting content on the screenshot to get a
 * search result or take an action such as calling a phone number or translating text.
 *
 * @hide
 */
@@ -256,16 +260,18 @@ public final class ContextualSearchManager {
    }

    /**
     * Used to check whether contextual search is available on the device. This method should be
     * called before calling {@link #startContextualSearch()} or adding any UI related to it to
     * ensure that the device is configured to support contextual search.
     * Used to check whether contextual search is available on the device. If this method returns
     * {code false}, you should not add any UI related to this feature, nor call
     * {@link #startContextualSearch(Activity, ContextualSearchConfig)}. It's rare but possible that
     * the return value of this method will change in subsequent calls, e.g. if the Contextual
     * Search app is disabled or enabled by the user.
     *
     * @see #startContextualSearch()
     * @return true if contextual search is available on the device, false otherwise.
     * @see #startContextualSearch(Activity, ContextualSearchConfig)
     * @return {@code true} if contextual search is currently available, {@code false} otherwise
     *
     * @hide
     */
    @FlaggedApi(Flags.FLAG_SELF_INVOCATION)
    @FlaggedApi(Flags.FLAG_CONFIG_PARAMETERS)
    @SystemApi
    public boolean isContextualSearchAvailable() {
        if (DEBUG) Log.d(TAG, "isContextualSearchAvailable");
@@ -295,20 +301,60 @@ public final class ContextualSearchManager {
     * <p>This method will fail silently if Contextual Search is not available on the device.
     *
     * @param entrypoint the invocation entrypoint
     * @throws SecurityException if the caller does not have the {@link ACCESS_CONTEXTUAL_SEARCH}
     * permission.
     *
     * @hide
     */
    @RequiresPermission(ACCESS_CONTEXTUAL_SEARCH)
    @SystemApi
    public void startContextualSearch(@Entrypoint int entrypoint) {
        if (DEBUG) Log.d(TAG, "startContextualSearch; entrypoint: " + entrypoint);
        startContextualSearchInternal(entrypoint, null);
    }

    /**
     * Used to start contextual search for a given system entrypoint.
     * <p>
     *     When {@link #startContextualSearch} is called, the system server does the following:
     *     <ul>
     *         <li>Resolves the activity using the package name and intent filter. The package name
     *             is fetched from the config specified in ContextualSearchManagerService.
     *             The activity must have ACTION_LAUNCH_CONTEXTUAL_SEARCH specified in its manifest.
     *         <li>Puts the required extras in the launch intent, which may include a
     *         {@link android.media.projection.MediaProjection} session.
     *         <li>Launches the activity.
     *     </ul>
     * </p>
     *
     * <p>This method will fail silently if Contextual Search is not available on the device.
     *
     * @param entrypoint the invocation entrypoint
     * @param config the invocation configuration parameters. If {@code null}, default configuration
     *               will be applied, including launching on {@link Display#DEFAULT_DISPLAY}.
     *
     * @hide
     */
    @RequiresPermission(ACCESS_CONTEXTUAL_SEARCH)
    @FlaggedApi(Flags.FLAG_CONFIG_PARAMETERS)
    @SystemApi
    public void startContextualSearch(@Entrypoint int entrypoint,
            @Nullable ContextualSearchConfig config) {
        startContextualSearchInternal(entrypoint, config);
    }

    /**
     * Internal method to start contextual search with an entrypoint and optional config.
     */
    @RequiresPermission(ACCESS_CONTEXTUAL_SEARCH)
    private void startContextualSearchInternal(@Entrypoint int entrypoint,
            @Nullable ContextualSearchConfig config) {
        if (!VALID_ENTRYPOINT_VALUES.contains(entrypoint)) {
            throw new IllegalArgumentException("Invalid entrypoint: " + entrypoint);
        }
        if (DEBUG) Log.d(TAG, "startContextualSearch for entrypoint: " + entrypoint);
        if (DEBUG) {
            Log.d(TAG, "startContextualSearch; entrypoint: " + entrypoint + "; config: " + config);
        }
        try {
            mService.startContextualSearch(entrypoint);
            mService.startContextualSearch(entrypoint, config);
        } catch (RemoteException e) {
            if (DEBUG) Log.d(TAG, "Failed to startContextualSearch", e);
            e.rethrowFromSystemServer();
@@ -319,27 +365,36 @@ public final class ContextualSearchManager {
     * Used to start Contextual Search from within an app. This will send a screenshot to the
     * Contextual Search app designated by the device manufacturer. The user can then select content
     * on the screenshot to get a search result or take an action such as calling a phone number or
     * translating the text.
     * translating the text. Note that the screenshot will capture the full display and may include
     * content outside of your Activity, e.g. in split screen mode.
     *
     * <p>Prior to calling this method or showing any UI related to it, you should verify that
     * Contextual Search is available on the device by using {@link #isContextualSearchAvailable()}.
     * Otherwise, this method will fail silently.
     *
     * <p>This method should only be called from an app that has a foreground Activity.
     * <p>Note: The system will use the display ID of your activity unless a displayId is specified
     * in the config. This is strongly discouraged unless you have a specific reason to specify a
     * different display.
     *
     * @see #isContextualSearchAvailable()
     * @throws SecurityException if the caller does not have a foreground Activity.
     * @param activity your foreground Activity from which the search is started
     * @param config the invocation configuration parameters. If {@code null}, default configuration
     *               will be applied, including launching the search on the same display as your
     *               activity.
     * @throws SecurityException if the caller does not have a foreground Activity
     *
     * @hide
     */
    @FlaggedApi(Flags.FLAG_SELF_INVOCATION)
    @FlaggedApi(Flags.FLAG_CONFIG_PARAMETERS)
    @SystemApi
    public void startContextualSearch() {
        if (DEBUG) Log.d(TAG, "startContextualSearch from app");
    public void startContextualSearch(@NonNull Activity activity,
            @Nullable ContextualSearchConfig config) {
        Objects.requireNonNull(activity);
        if (DEBUG) Log.d(TAG, "startContextualSearchForActivity(" + activity + ", " + config + ")");
        try {
            mService.startContextualSearchForForegroundApp();
            mService.startContextualSearchForActivity(activity.getActivityToken(), config);
        } catch (RemoteException e) {
            if (DEBUG) Log.d(TAG, "Failed to startContextualSearch", e);
            if (DEBUG) Log.d(TAG, "Failed to startContextualSearchForActivity", e);
            e.rethrowFromSystemServer();
        }
    }
+5 −2
Original line number Diff line number Diff line
package android.app.contextualsearch;

import android.app.contextualsearch.IContextualSearchCallback;

parcelable ContextualSearchConfig;

/**
 * @hide
 */
interface IContextualSearchManager {
  boolean isContextualSearchAvailable();
  void startContextualSearchForForegroundApp();
  oneway void startContextualSearch(int entrypoint);
  void startContextualSearchForActivity(in IBinder activityToken, in ContextualSearchConfig config);
  oneway void startContextualSearch(int entrypoint, in ContextualSearchConfig config);
  oneway void getContextualSearchState(in IBinder token, in IContextualSearchCallback callback);
}
+17 −14
Original line number Diff line number Diff line
@@ -51,9 +51,12 @@ flag {
}

flag {
  name: "self_invocation"
    name: "config_parameters"
    namespace: "sysui_integrations"
  description: "Enable apps to self-invoke Contextual Search."
  bug: "368653769"
    description: "Allow invocation parameters to be passed to Contextual Search, including from apps."
    bug: "371552433"
    is_exported: true
    metadata {
        purpose: PURPOSE_BUGFIX
    }
}
Loading