ContextualSearch API initial implementation

Test: CTS
Bug: 309689654
Change-Id: I0b820180eca2dfe32287132b941a5719ad7e26ca
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index a80194c..9f0e838 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -21,6 +21,7 @@
         // !!! KEEP THIS LIST ALPHABETICAL !!!
         "aconfig_mediacodec_flags_java_lib",
         "android.adaptiveauth.flags-aconfig-java",
+        "android.app.contextualsearch.flags-aconfig-java",
         "android.app.flags-aconfig-java",
         "android.app.ondeviceintelligence-aconfig-java",
         "android.app.smartspace.flags-aconfig-java",
@@ -960,6 +961,19 @@
     ],
 }
 
+// Contextual Search
+aconfig_declarations {
+    name: "android.app.contextualsearch.flags-aconfig",
+    package: "android.app.contextualsearch.flags",
+    srcs: ["core/java/android/app/contextualsearch/flags.aconfig"],
+}
+
+java_aconfig_library {
+    name: "android.app.contextualsearch.flags-aconfig-java",
+    aconfig_declarations: "android.app.contextualsearch.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
 // Smartspace
 aconfig_declarations {
     name: "android.app.smartspace.flags-aconfig",
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index ee69ce1..00cd68e 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -7,6 +7,7 @@
     field public static final String ACCESS_BROADCAST_RADIO = "android.permission.ACCESS_BROADCAST_RADIO";
     field public static final String ACCESS_BROADCAST_RESPONSE_STATS = "android.permission.ACCESS_BROADCAST_RESPONSE_STATS";
     field public static final String ACCESS_CACHE_FILESYSTEM = "android.permission.ACCESS_CACHE_FILESYSTEM";
+    field @FlaggedApi("android.app.contextualsearch.flags.enable_service") public static final String ACCESS_CONTEXTUAL_SEARCH = "android.permission.ACCESS_CONTEXTUAL_SEARCH";
     field public static final String ACCESS_CONTEXT_HUB = "android.permission.ACCESS_CONTEXT_HUB";
     field public static final String ACCESS_DRM_CERTIFICATES = "android.permission.ACCESS_DRM_CERTIFICATES";
     field @Deprecated public static final String ACCESS_FM_RADIO = "android.permission.ACCESS_FM_RADIO";
@@ -2176,6 +2177,39 @@
 
 }
 
+package android.app.contextualsearch {
+
+  @FlaggedApi("android.app.contextualsearch.flags.enable_service") public class ContextualSearchManager {
+    method public void getContextualSearchState(@NonNull android.os.IBinder, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.app.contextualsearch.ContextualSearchState,java.lang.Throwable>);
+    method @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXTUAL_SEARCH) public void startContextualSearch(int);
+    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
+    field public static final int ENTRYPOINT_LONG_PRESS_NAV_HANDLE = 1; // 0x1
+    field public static final int ENTRYPOINT_LONG_PRESS_OVERVIEW = 3; // 0x3
+    field public static final int ENTRYPOINT_OVERVIEW_ACTION = 4; // 0x4
+    field public static final int ENTRYPOINT_OVERVIEW_MENU = 5; // 0x5
+    field public static final int ENTRYPOINT_SYSTEM_ACTION = 9; // 0x9
+    field public static final String EXTRA_ENTRYPOINT = "android.app.contextualsearch.extra.ENTRYPOINT";
+    field public static final String EXTRA_FLAG_SECURE_FOUND = "android.app.contextualsearch.extra.FLAG_SECURE_FOUND";
+    field public static final String EXTRA_IS_MANAGED_PROFILE_VISIBLE = "android.app.contextualsearch.extra.IS_MANAGED_PROFILE_VISIBLE";
+    field public static final String EXTRA_SCREENSHOT = "android.app.contextualsearch.extra.SCREENSHOT";
+    field public static final String EXTRA_TOKEN = "android.app.contextualsearch.extra.TOKEN";
+    field public static final String EXTRA_VISIBLE_PACKAGE_NAMES = "android.app.contextualsearch.extra.VISIBLE_PACKAGE_NAMES";
+  }
+
+  @FlaggedApi("android.app.contextualsearch.flags.enable_service") public final class ContextualSearchState implements android.os.Parcelable {
+    ctor public ContextualSearchState(@Nullable android.app.assist.AssistStructure, @Nullable android.app.assist.AssistContent, @NonNull android.os.Bundle);
+    method public int describeContents();
+    method @Nullable public android.app.assist.AssistContent getContent();
+    method @NonNull public android.os.Bundle getExtras();
+    method @Nullable public android.app.assist.AssistStructure getStructure();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.contextualsearch.ContextualSearchState> CREATOR;
+  }
+
+}
+
 package android.app.job {
 
   public abstract class JobScheduler {
@@ -3758,6 +3792,7 @@
     field public static final String CLOUDSEARCH_SERVICE = "cloudsearch";
     field public static final String CONTENT_SUGGESTIONS_SERVICE = "content_suggestions";
     field public static final String CONTEXTHUB_SERVICE = "contexthub";
+    field @FlaggedApi("android.app.contextualsearch.flags.enable_service") public static final String CONTEXTUAL_SEARCH_SERVICE = "contextual_search";
     field @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") public static final String ECM_ENHANCED_CONFIRMATION_SERVICE = "ecm_enhanced_confirmation";
     field public static final String ETHERNET_SERVICE = "ethernet";
     field public static final String EUICC_CARD_SERVICE = "euicc_card";
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index d01626e..3b20921 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -32,6 +32,7 @@
 import android.app.blob.BlobStoreManagerFrameworkInitializer;
 import android.app.contentsuggestions.ContentSuggestionsManager;
 import android.app.contentsuggestions.IContentSuggestionsManager;
+import android.app.contextualsearch.ContextualSearchManager;
 import android.app.ecm.EnhancedConfirmationFrameworkInitializer;
 import android.app.job.JobSchedulerFrameworkInitializer;
 import android.app.people.PeopleManager;
@@ -1280,6 +1281,16 @@
                 }
             });
 
+        registerService(Context.CONTEXTUAL_SEARCH_SERVICE, ContextualSearchManager.class,
+                new CachedServiceFetcher<>() {
+                    @Override
+                    public ContextualSearchManager createService(ContextImpl ctx)
+                            throws ServiceNotFoundException {
+                        IBinder b = ServiceManager.getService(Context.CONTEXTUAL_SEARCH_SERVICE);
+                        return b == null ? null : new ContextualSearchManager();
+                    }
+                });
+
         registerService(Context.APP_PREDICTION_SERVICE, AppPredictionManager.class,
                 new CachedServiceFetcher<AppPredictionManager>() {
             @Override
diff --git a/core/java/android/app/contextualsearch/ContextualSearchManager.java b/core/java/android/app/contextualsearch/ContextualSearchManager.java
new file mode 100644
index 0000000..693de21
--- /dev/null
+++ b/core/java/android/app/contextualsearch/ContextualSearchManager.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2024 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 static android.Manifest.permission.ACCESS_CONTEXTUAL_SEARCH;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.app.contextualsearch.flags.Flags;
+import android.content.Context;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.OutcomeReceiver;
+import android.os.ParcelableException;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
+
+/**
+ * {@link ContextualSearchManager} is a system service to facilitate contextual search experience on
+ * configured Android devices.
+ * <p>
+ * This class lets
+ * <ul>
+ *   <li> a caller start contextual search by calling {@link #startContextualSearch} method.
+ *   <li> a handler request {@link ContextualSearchState} by calling the
+ *   {@link #getContextualSearchState} method.
+ * </ul>
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_ENABLE_SERVICE)
+public class ContextualSearchManager {
+
+    /**
+     * Key to get the entrypoint from the extras of the activity launched by contextual search.
+     * Only supposed to be used with ACTON_LAUNCH_CONTEXTUAL_SEARCH.
+     */
+    public static final String EXTRA_ENTRYPOINT =
+            "android.app.contextualsearch.extra.ENTRYPOINT";
+    /**
+     * Key to get the flag_secure value from the extras of the activity launched by contextual
+     * search. The value will be true if flag_secure is found in any of the visible activities.
+     * Only supposed to be used with ACTON_LAUNCH_CONTEXTUAL_SEARCH.
+     */
+    public static final String EXTRA_FLAG_SECURE_FOUND =
+            "android.app.contextualsearch.extra.FLAG_SECURE_FOUND";
+    /**
+     * Key to get the screenshot from the extras of the activity launched by contextual search.
+     * Only supposed to be used with ACTON_LAUNCH_CONTEXTUAL_SEARCH.
+     */
+    public static final String EXTRA_SCREENSHOT =
+            "android.app.contextualsearch.extra.SCREENSHOT";
+    /**
+     * Key to check whether managed profile is visible from the extras of the activity launched by
+     * contextual search. The value will be true if any one of the visible apps is managed.
+     * Only supposed to be used with ACTON_LAUNCH_CONTEXTUAL_SEARCH.
+     */
+    public static final String EXTRA_IS_MANAGED_PROFILE_VISIBLE =
+            "android.app.contextualsearch.extra.IS_MANAGED_PROFILE_VISIBLE";
+    /**
+     * Key to get the list of visible packages from the extras of the activity launched by
+     * contextual search.
+     * Only supposed to be used with ACTON_LAUNCH_CONTEXTUAL_SEARCH.
+     */
+    public static final String EXTRA_VISIBLE_PACKAGE_NAMES =
+            "android.app.contextualsearch.extra.VISIBLE_PACKAGE_NAMES";
+
+    /**
+     * Key to get the binder token from the extras of the activity launched by contextual search.
+     * This token is needed to invoke {@link #getContextualSearchState} method.
+     * Only supposed to be used with ACTON_LAUNCH_CONTEXTUAL_SEARCH.
+     */
+    public static final String EXTRA_TOKEN = "android.app.contextualsearch.extra.TOKEN";
+    /**
+     * Intent action for contextual search invocation. The app providing the contextual search
+     * experience must add this intent filter action to the activity it wants to be launched.
+     * <br>
+     * <b>Note</b> This activity must not be exported.
+     */
+    public static final String ACTION_LAUNCH_CONTEXTUAL_SEARCH =
+            "android.app.contextualsearch.action.LAUNCH_CONTEXTUAL_SEARCH";
+
+    /** Entrypoint to be used when a user long presses on the nav handle. */
+    public static final int ENTRYPOINT_LONG_PRESS_NAV_HANDLE = 1;
+    /** Entrypoint to be used when a user long presses on the home button. */
+    public static final int ENTRYPOINT_LONG_PRESS_HOME = 2;
+    /** Entrypoint to be used when a user long presses on the overview button. */
+    public static final int ENTRYPOINT_LONG_PRESS_OVERVIEW = 3;
+    /** Entrypoint to be used when a user presses the action button in overview. */
+    public static final int ENTRYPOINT_OVERVIEW_ACTION = 4;
+    /** Entrypoint to be used when a user presses the context menu button in overview. */
+    public static final int ENTRYPOINT_OVERVIEW_MENU = 5;
+    /** Entrypoint to be used by system actions like TalkBack, Accessibility etc. */
+    public static final int ENTRYPOINT_SYSTEM_ACTION = 9;
+    /** Entrypoint to be used when a user long presses on the meta key. */
+    public static final int ENTRYPOINT_LONG_PRESS_META = 10;
+    /**
+     * The {@link Entrypoint} annotation is used to standardize the entrypoints supported by
+     * {@link #startContextualSearch} method.
+     *
+     * @hide
+     */
+    @IntDef(prefix = {"ENTRYPOINT_"}, value = {
+            ENTRYPOINT_LONG_PRESS_NAV_HANDLE,
+            ENTRYPOINT_LONG_PRESS_HOME,
+            ENTRYPOINT_LONG_PRESS_OVERVIEW,
+            ENTRYPOINT_OVERVIEW_ACTION,
+            ENTRYPOINT_OVERVIEW_MENU,
+            ENTRYPOINT_SYSTEM_ACTION,
+            ENTRYPOINT_LONG_PRESS_META
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Entrypoint {
+    }
+    private static final String TAG = ContextualSearchManager.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    private final IContextualSearchManager mService;
+
+    /** @hide */
+    public ContextualSearchManager() {
+        if (DEBUG) Log.d(TAG, "ContextualSearchManager created");
+        IBinder b = ServiceManager.getService(Context.CONTEXTUAL_SEARCH_SERVICE);
+        mService = IContextualSearchManager.Stub.asInterface(b);
+    }
+
+    /**
+     * Used to start contextual search.
+     * <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.
+     *         <li>Launches the activity.
+     *     </ul>
+     * </p>
+     *
+     * @param entrypoint the invocation entrypoint
+     */
+    @RequiresPermission(ACCESS_CONTEXTUAL_SEARCH)
+    public void startContextualSearch(@Entrypoint int entrypoint) {
+        if (DEBUG) Log.d(TAG, "startContextualSearch for entrypoint: " + entrypoint);
+        try {
+            mService.startContextualSearch(entrypoint);
+        } catch (RemoteException e) {
+            if (DEBUG) Log.d(TAG, "Failed to startContextualSearch", e);
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the {@link ContextualSearchState} to the handler via the provided callback.
+     *
+     * @param token The caller is expected to get the token from the launch extras of the handling
+     *              activity using {@link Bundle#getIBinder} with {@link #EXTRA_TOKEN} key.
+     *              <br>
+     *              <b>Note</b> This token is for one time use only. Subsequent uses will invoke
+     *              callback's {@link OutcomeReceiver#onError}.
+     * @param executor The executor which will be used to invoke the callback.
+     * @param callback The callback which will be used to return {@link ContextualSearchState}
+     *                 if/when it is available via {@link OutcomeReceiver#onResult}. It will also be
+     *                 used to return errors via {@link OutcomeReceiver#onError}.
+     */
+    public void getContextualSearchState(@NonNull IBinder token,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<ContextualSearchState, Throwable> callback) {
+        if (DEBUG) Log.d(TAG, "getContextualSearchState for token:" + token);
+        try {
+            final CallbackWrapper wrapper = new CallbackWrapper(executor, callback);
+            mService.getContextualSearchState(token, wrapper);
+        } catch (RemoteException e) {
+            if (DEBUG) Log.d(TAG, "Failed to getContextualSearchState", e);
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    private static class CallbackWrapper extends IContextualSearchCallback.Stub {
+        private final OutcomeReceiver<ContextualSearchState, Throwable> mCallback;
+        private final Executor mExecutor;
+
+        CallbackWrapper(@NonNull Executor callbackExecutor,
+                        @NonNull OutcomeReceiver<ContextualSearchState, Throwable> callback) {
+            mCallback = callback;
+            mExecutor = callbackExecutor;
+        }
+
+        @Override
+        public void onResult(ContextualSearchState state) {
+            Binder.withCleanCallingIdentity(() -> {
+                if (DEBUG) Log.d(TAG, "onResult state:" + state);
+                mExecutor.execute(() -> mCallback.onResult(state));
+            });
+        }
+
+        @Override
+        public void onError(ParcelableException error) {
+            Binder.withCleanCallingIdentity(() -> {
+                if (DEBUG) Log.w(TAG, "onError", error);
+                mExecutor.execute(() -> mCallback.onError(error));
+            });
+        }
+    }
+}
diff --git a/core/java/android/app/contextualsearch/ContextualSearchState.aidl b/core/java/android/app/contextualsearch/ContextualSearchState.aidl
new file mode 100644
index 0000000..7f64484
--- /dev/null
+++ b/core/java/android/app/contextualsearch/ContextualSearchState.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2021, 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;
+
+parcelable ContextualSearchState;
\ No newline at end of file
diff --git a/core/java/android/app/contextualsearch/ContextualSearchState.java b/core/java/android/app/contextualsearch/ContextualSearchState.java
new file mode 100644
index 0000000..5c04bc89
--- /dev/null
+++ b/core/java/android/app/contextualsearch/ContextualSearchState.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 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.Nullable;
+import android.annotation.SystemApi;
+import android.app.assist.AssistContent;
+import android.app.assist.AssistStructure;
+import android.app.contextualsearch.flags.Flags;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+/**
+ * {@link ContextualSearchState} contains additional data a contextual search handler can request
+ * via {@link ContextualSearchManager#getContextualSearchState} method.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_ENABLE_SERVICE)
+@SystemApi
+public final class ContextualSearchState implements Parcelable {
+    private final @NonNull Bundle mExtras;
+    private final @Nullable AssistStructure mStructure;
+    private final @Nullable AssistContent mContent;
+
+    public ContextualSearchState(@Nullable AssistStructure structure,
+            @Nullable AssistContent content, @NonNull Bundle extras) {
+        mStructure = structure;
+        mContent = content;
+        mExtras = extras;
+    }
+
+    private ContextualSearchState(Parcel source) {
+        this.mStructure = source.readTypedObject(AssistStructure.CREATOR);
+        this.mContent = source.readTypedObject(AssistContent.CREATOR);
+        Bundle extras = source.readBundle(getClass().getClassLoader());
+        this.mExtras = extras != null ? extras : Bundle.EMPTY;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeTypedObject(this.mStructure, flags);
+        dest.writeTypedObject(this.mContent, flags);
+        dest.writeBundle(this.mExtras);
+    }
+
+    /** Gets an instance of {@link AssistContent}. */
+    @Nullable
+    public AssistContent getContent() {
+        return mContent;
+    }
+
+    /** Gets an instance of {@link AssistStructure}. */
+    @Nullable
+    public AssistStructure getStructure() {
+        return mStructure;
+    }
+
+    /**
+     * Gets an instance of {@link Bundle} containing the extras added by the system server.
+     * The contents of this bundle vary by usecase. When Contextual is invoked via Launcher, this
+     * bundle is empty.
+     */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+
+    @NonNull
+    public static final Creator<ContextualSearchState> CREATOR = new Creator<>() {
+        @Override
+        public ContextualSearchState createFromParcel(Parcel source) {
+            return new ContextualSearchState(source);
+        }
+
+        @Override
+        public ContextualSearchState[] newArray(int size) {
+            return new ContextualSearchState[size];
+        }
+    };
+}
diff --git a/core/java/android/app/contextualsearch/IContextualSearchCallback.aidl b/core/java/android/app/contextualsearch/IContextualSearchCallback.aidl
new file mode 100644
index 0000000..5fe5fd2
--- /dev/null
+++ b/core/java/android/app/contextualsearch/IContextualSearchCallback.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2021 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.os.ParcelableException;
+import android.app.contextualsearch.ContextualSearchState;
+/**
+ * @hide
+ */
+oneway interface IContextualSearchCallback {
+    void onResult(in ContextualSearchState state);
+    void onError(in ParcelableException error);
+}
diff --git a/core/java/android/app/contextualsearch/IContextualSearchManager.aidl b/core/java/android/app/contextualsearch/IContextualSearchManager.aidl
new file mode 100644
index 0000000..1735a71
--- /dev/null
+++ b/core/java/android/app/contextualsearch/IContextualSearchManager.aidl
@@ -0,0 +1,11 @@
+package android.app.contextualsearch;
+
+
+import android.app.contextualsearch.IContextualSearchCallback;
+/**
+ * @hide
+ */
+oneway interface IContextualSearchManager {
+  void startContextualSearch(int entrypoint);
+  void getContextualSearchState(in IBinder token, in IContextualSearchCallback callback);
+}
diff --git a/core/java/android/app/contextualsearch/flags.aconfig b/core/java/android/app/contextualsearch/flags.aconfig
new file mode 100644
index 0000000..5ab0762
--- /dev/null
+++ b/core/java/android/app/contextualsearch/flags.aconfig
@@ -0,0 +1,8 @@
+package: "android.app.contextualsearch.flags"
+
+flag {
+  name: "enable_service"
+  namespace: "machine_learning"
+  description: "Flag to enable the service"
+  bug: "309689654"
+}
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 70d2c7a..4459647 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -5397,6 +5397,19 @@
     public static final String SMARTSPACE_SERVICE = "smartspace";
 
     /**
+     * Used for getting the contextual search service.
+     *
+     * <p><b>NOTE: </b> this service is optional; callers of
+     * {@code Context.getSystemServiceName(CONTEXTUAL_SEARCH_SERVICE)} must check for {@code null}.
+     *
+     * @hide
+     * @see #getSystemService(String)
+     */
+    @FlaggedApi(android.app.contextualsearch.flags.Flags.FLAG_ENABLE_SERVICE)
+    @SystemApi
+    public static final String CONTEXTUAL_SEARCH_SERVICE = "contextual_search";
+
+    /**
      * Used for getting the cloudsearch service.
      *
      * <p><b>NOTE: </b> this service is optional; callers of
diff --git a/core/res/Android.bp b/core/res/Android.bp
index e2e419f..f579e27 100644
--- a/core/res/Android.bp
+++ b/core/res/Android.bp
@@ -156,6 +156,7 @@
     generate_product_characteristics_rro: true,
 
     flags_packages: [
+        "android.app.contextualsearch.flags-aconfig",
         "android.content.pm.flags-aconfig",
         "android.provider.flags-aconfig",
         "camera_platform_flags",
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 9d80d153..e66b850 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7208,6 +7208,13 @@
     <permission android:name="android.permission.ACCESS_SMARTSPACE"
         android:protectionLevel="signature|privileged|development" />
 
+    <!-- @SystemApi Allows an application to start a contextual search.
+         @FlaggedApi("android.app.contextualsearch.flags.enable_service")
+         @hide  <p>Not for use by third-party applications.</p> -->
+    <permission android:name="android.permission.ACCESS_CONTEXTUAL_SEARCH"
+        android:protectionLevel="signature|privileged"
+        android:featureFlag="android.app.contextualsearch.flags.enable_service"/>
+
     <!-- @SystemApi Allows an application to manage the wallpaper effects
      generation service.
      @hide  <p>Not for use by third-party applications.</p> -->
diff --git a/services/Android.bp b/services/Android.bp
index 8709692..98a7979 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -119,6 +119,7 @@
         ":services.companion-sources",
         ":services.contentcapture-sources",
         ":services.contentsuggestions-sources",
+        ":services.contextualsearch-sources",
         ":services.coverage-sources",
         ":services.credentials-sources",
         ":services.devicepolicy-sources",
@@ -208,6 +209,7 @@
         "services.companion",
         "services.contentcapture",
         "services.contentsuggestions",
+        "services.contextualsearch",
         "services.coverage",
         "services.credentials",
         "services.devicepolicy",
diff --git a/services/contextualsearch/Android.bp b/services/contextualsearch/Android.bp
new file mode 100644
index 0000000..b4dd20e
--- /dev/null
+++ b/services/contextualsearch/Android.bp
@@ -0,0 +1,22 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+    name: "services.contextualsearch-sources",
+    srcs: ["java/**/*.java"],
+    path: "java",
+    visibility: ["//frameworks/base/services"],
+}
+
+java_library_static {
+    name: "services.contextualsearch",
+    defaults: ["platform_service_defaults"],
+    srcs: [":services.contextualsearch-sources"],
+    libs: ["services.core"],
+}
diff --git a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
new file mode 100644
index 0000000..b28bc1f
--- /dev/null
+++ b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.contextualsearch;
+
+import static android.Manifest.permission.ACCESS_CONTEXTUAL_SEARCH;
+import static android.content.Context.CONTEXTUAL_SEARCH_SERVICE;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION;
+import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
+import static android.content.pm.PackageManager.MATCH_FACTORY_ONLY;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.app.ActivityOptions;
+import android.app.admin.DevicePolicyManagerInternal;
+import android.app.contextualsearch.ContextualSearchManager;
+import android.app.contextualsearch.ContextualSearchState;
+import android.app.contextualsearch.IContextualSearchCallback;
+import android.app.contextualsearch.IContextualSearchManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.graphics.Bitmap;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.ParcelableException;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
+import android.util.Log;
+import android.util.Slog;
+import android.window.ScreenCapture;
+
+import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
+import com.android.server.wm.ActivityAssistInfo;
+import com.android.server.wm.ActivityTaskManagerInternal;
+import com.android.server.wm.WindowManagerInternal;
+
+import java.io.FileDescriptor;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class ContextualSearchManagerService extends SystemService {
+
+    private static final int MSG_RESET_TEMPORARY_PACKAGE = 0;
+    private static final int MAX_TEMP_PACKAGE_DURATION_MS = 1_000 * 60 * 2; // 2 minutes
+    private final Context mContext;
+    private final ActivityTaskManagerInternal mAtmInternal;
+    private final WindowManagerInternal mWmInternal;
+    private final DevicePolicyManagerInternal mDpmInternal;
+
+    private Handler mTemporaryHandler;
+
+    @GuardedBy("this")
+    private String mTemporaryPackage = null;
+    private static final String TAG = ContextualSearchManagerService.class.getSimpleName();
+
+    public ContextualSearchManagerService(@NonNull Context context) {
+        super(context);
+        if (DEBUG_USER) Log.d(TAG, "ContextualSearchManagerService created");
+        mContext = context;
+        mAtmInternal = Objects.requireNonNull(
+                LocalServices.getService(ActivityTaskManagerInternal.class));
+        mWmInternal = Objects.requireNonNull(LocalServices.getService(WindowManagerInternal.class));
+        mDpmInternal = LocalServices.getService(DevicePolicyManagerInternal.class);
+    }
+
+    @Override
+    public void onStart() {
+        publishBinderService(CONTEXTUAL_SEARCH_SERVICE, new ContextualSearchManagerStub());
+    }
+
+    void resetTemporaryPackage() {
+        synchronized (this) {
+            enforceOverridingPermission("resetTemporaryPackage");
+            if (mTemporaryHandler != null) {
+                mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_PACKAGE);
+                mTemporaryHandler = null;
+            }
+            if (DEBUG_USER) Log.d(TAG, "mTemporaryPackage reset.");
+            mTemporaryPackage = null;
+        }
+    }
+
+    void setTemporaryPackage(@NonNull String temporaryPackage, int durationMs) {
+        synchronized (this) {
+            enforceOverridingPermission("setTemporaryPackage");
+            final int maxDurationMs = MAX_TEMP_PACKAGE_DURATION_MS;
+            if (durationMs > maxDurationMs) {
+                throw new IllegalArgumentException(
+                        "Max duration is " + maxDurationMs + " (called with " + durationMs + ")");
+            }
+            if (mTemporaryHandler == null) {
+                mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) {
+                    @Override
+                    public void handleMessage(Message msg) {
+                        if (msg.what == MSG_RESET_TEMPORARY_PACKAGE) {
+                            synchronized (this) {
+                                resetTemporaryPackage();
+                            }
+                        } else {
+                            Slog.wtf(TAG, "invalid handler msg: " + msg);
+                        }
+                    }
+                };
+            } else {
+                mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_PACKAGE);
+            }
+            mTemporaryPackage = temporaryPackage;
+            mTemporaryHandler.sendEmptyMessageDelayed(MSG_RESET_TEMPORARY_PACKAGE, durationMs);
+            if (DEBUG_USER) Log.d(TAG, "mTemporaryPackage set to " + mTemporaryPackage);
+        }
+    }
+
+    private Intent getResolvedLaunchIntent() {
+        synchronized (this) {
+            // If mTemporaryPackage is not null, use it to get the ContextualSearch intent.
+            String csPkgName = mTemporaryPackage != null ? mTemporaryPackage : mContext
+                    .getResources().getString(R.string.config_defaultContextualSearchPackageName);
+            if (csPkgName.isEmpty()) {
+                // Return null if csPackageName is not specified.
+                return null;
+            }
+            Intent launchIntent = new Intent(
+                    ContextualSearchManager.ACTION_LAUNCH_CONTEXTUAL_SEARCH);
+            launchIntent.setPackage(csPkgName);
+            ResolveInfo resolveInfo = mContext.getPackageManager().resolveActivity(
+                    launchIntent, MATCH_FACTORY_ONLY);
+            if (resolveInfo == null) {
+                return null;
+            }
+            ComponentName componentName = resolveInfo.getComponentInfo().getComponentName();
+            if (componentName == null) {
+                return null;
+            }
+            launchIntent.setComponent(componentName);
+            return launchIntent;
+        }
+    }
+
+    private Intent getContextualSearchIntent(int entrypoint, IBinder mToken) {
+        final Intent launchIntent = getResolvedLaunchIntent();
+        if (launchIntent == null) {
+            return null;
+        }
+
+        if (DEBUG_USER) Log.d(TAG, "Launch component: " + launchIntent.getComponent());
+        launchIntent.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_NO_ANIMATION
+                | FLAG_ACTIVITY_NO_USER_ACTION);
+        launchIntent.putExtra(ContextualSearchManager.EXTRA_ENTRYPOINT, entrypoint);
+        launchIntent.putExtra(ContextualSearchManager.EXTRA_TOKEN, mToken);
+        boolean isAssistDataAllowed = mAtmInternal.isAssistDataAllowed();
+        final List<ActivityAssistInfo> records = mAtmInternal.getTopVisibleActivities();
+        ArrayList<String> visiblePackageNames = new ArrayList<>();
+        boolean isManagedProfileVisible = false;
+        for (ActivityAssistInfo record : records) {
+            // Add the package name to the list only if assist data is allowed.
+            if (isAssistDataAllowed) {
+                visiblePackageNames.add(record.getComponentName().getPackageName());
+            }
+            if (mDpmInternal != null
+                    && mDpmInternal.isUserOrganizationManaged(record.getUserId())) {
+                isManagedProfileVisible = true;
+            }
+        }
+        final ScreenCapture.ScreenshotHardwareBuffer shb;
+        if (mWmInternal != null) {
+            shb = mWmInternal.takeAssistScreenshot();
+        } else {
+            shb = null;
+        }
+        final Bitmap bm = shb != null ? shb.asBitmap() : null;
+        // Now that everything is fetched, putting it in the launchIntent.
+        if (bm != null) {
+            launchIntent.putExtra(ContextualSearchManager.EXTRA_FLAG_SECURE_FOUND,
+                    shb.containsSecureLayers());
+            // Only put the screenshot if assist data is allowed
+            if (isAssistDataAllowed) {
+                launchIntent.putExtra(ContextualSearchManager.EXTRA_SCREENSHOT, bm.asShared());
+            }
+        }
+        launchIntent.putExtra(ContextualSearchManager.EXTRA_IS_MANAGED_PROFILE_VISIBLE,
+                isManagedProfileVisible);
+        // Only put the list of visible package names if assist data is allowed
+        if (isAssistDataAllowed) {
+            launchIntent.putExtra(ContextualSearchManager.EXTRA_VISIBLE_PACKAGE_NAMES,
+                    visiblePackageNames);
+        }
+        return launchIntent;
+    }
+
+    @RequiresPermission(android.Manifest.permission.START_TASKS_FROM_RECENTS)
+    private int invokeContextualSearchIntent(Intent launchIntent) {
+        // Contextual search starts with a frozen screen - so we launch without
+        // any system animations or starting window.
+        final ActivityOptions opts = ActivityOptions.makeCustomTaskAnimation(mContext,
+                /* enterResId= */ 0, /* exitResId= */ 0, null, null, null);
+        opts.setDisableStartingWindow(true);
+        return mAtmInternal.startActivityWithScreenshot(launchIntent,
+                mContext.getPackageName(), Binder.getCallingUid(), Binder.getCallingPid(), null,
+                opts.toBundle(), Binder.getCallingUserHandle().getIdentifier());
+    }
+
+    private void enforcePermission(@NonNull final String func) {
+        Context ctx = getContext();
+        if (!(ctx.checkCallingPermission(ACCESS_CONTEXTUAL_SEARCH) == PERMISSION_GRANTED
+                || isCallerTemporary())) {
+            String msg = "Permission Denial: Cannot call " + func + " from pid="
+                    + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid();
+            throw new SecurityException(msg);
+        }
+    }
+
+    private void enforceOverridingPermission(@NonNull final String func) {
+        if (!(Binder.getCallingUid() == Process.SHELL_UID
+                || Binder.getCallingUid() == Process.ROOT_UID
+                || Binder.getCallingUid() == Process.SYSTEM_UID)) {
+            String msg = "Permission Denial: Cannot override Contextual Search. Called " + func
+                    + " from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid();
+            throw new SecurityException(msg);
+        }
+    }
+
+    private boolean isCallerTemporary() {
+        synchronized (this) {
+            return mTemporaryPackage != null
+                    && mTemporaryPackage.equals(
+                    getContext().getPackageManager().getNameForUid(Binder.getCallingUid()));
+        }
+    }
+
+    private class ContextualSearchManagerStub extends IContextualSearchManager.Stub {
+        private @Nullable IBinder mToken;
+
+        @Override
+        public void startContextualSearch(int entrypoint) {
+            synchronized (this) {
+                if (DEBUG_USER) Log.d(TAG, "startContextualSearch");
+                enforcePermission("startContextualSearch");
+                mToken = new Binder();
+                // We get the launch intent with the system server's identity because the system
+                // server has READ_FRAME_BUFFER permission to get the screenshot and because only
+                // the system server can invoke non-exported activities.
+                Binder.withCleanCallingIdentity(() -> {
+                    Intent launchIntent = getContextualSearchIntent(entrypoint, mToken);
+                    if (launchIntent != null) {
+                        int result = invokeContextualSearchIntent(launchIntent);
+                        if (DEBUG_USER) Log.d(TAG, "Launch result: " + result);
+                    }
+                });
+            }
+        }
+
+        @Override
+        public void getContextualSearchState(
+                @NonNull IBinder token,
+                @NonNull IContextualSearchCallback callback) {
+            if (DEBUG_USER) {
+                Log.i(TAG, "getContextualSearchState token: " + token + ", callback: " + callback);
+            }
+            if (mToken == null || !mToken.equals(token)) {
+                if (DEBUG_USER) {
+                    Log.e(TAG, "getContextualSearchState: invalid token, returning error");
+                }
+                try {
+                    callback.onError(
+                            new ParcelableException(new IllegalArgumentException("Invalid token")));
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Could not invoke onError callback", e);
+                }
+                return;
+            }
+            mToken = null;
+            // Process data request
+            try {
+                callback.onResult(new ContextualSearchState(null, null, Bundle.EMPTY));
+            } catch (RemoteException e) {
+                Log.e(TAG, "Could not invoke onResult callback", e);
+            }
+        }
+
+        public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
+                @Nullable FileDescriptor err, @NonNull String[] args,
+                @Nullable ShellCallback callback, @NonNull ResultReceiver resultReceiver) {
+            new ContextualSearchManagerShellCommand(ContextualSearchManagerService.this)
+                    .exec(this, in, out, err, args, callback, resultReceiver);
+        }
+    }
+}
diff --git a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerShellCommand.java b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerShellCommand.java
new file mode 100644
index 0000000..5777e1d
--- /dev/null
+++ b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerShellCommand.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.contextualsearch;
+
+import android.annotation.NonNull;
+import android.os.ShellCommand;
+
+import java.io.PrintWriter;
+
+public class ContextualSearchManagerShellCommand extends ShellCommand {
+
+    private final ContextualSearchManagerService mService;
+
+    ContextualSearchManagerShellCommand(@NonNull ContextualSearchManagerService service) {
+        mService = service;
+    }
+
+    @Override
+    public int onCommand(String cmd) {
+        if (cmd == null) {
+            return handleDefaultCommands(cmd);
+        }
+        final PrintWriter pw = getOutPrintWriter();
+        switch (cmd) {
+            case "set": {
+                final String what = getNextArgRequired();
+                switch (what) {
+                    case "temporary-package": {
+                        String packageName = getNextArg();
+                        if (packageName == null) {
+                            mService.resetTemporaryPackage();
+                            pw.println("ContextualSearchManagerService reset.");
+                            return 0;
+                        }
+                        final int duration = Integer.parseInt(getNextArgRequired());
+                        mService.setTemporaryPackage(packageName, duration);
+                        pw.println("ContextualSearchManagerService temporarily set to "
+                                + packageName + " for " + duration + "ms");
+                        break;
+                    }
+                }
+            }
+            break;
+            default:
+                return handleDefaultCommands(cmd);
+        }
+        return 0;
+    }
+
+    @Override
+    public void onHelp() {
+        try (PrintWriter pw = getOutPrintWriter()) {
+            pw.println("ContextualSearchService commands:");
+            pw.println("  help");
+            pw.println("    Prints this help text.");
+            pw.println("");
+            pw.println("  set temporary-package [PACKAGE_NAME DURATION]");
+            pw.println("    Temporarily (for DURATION ms) changes the Contextual Search "
+                    + "implementation.");
+            pw.println("    To reset, call without any arguments.");
+            pw.println("");
+        }
+    }
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 9d95c5b..cb4239e 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -406,6 +406,8 @@
             "com.android.server.searchui.SearchUiManagerService";
     private static final String SMARTSPACE_MANAGER_SERVICE_CLASS =
             "com.android.server.smartspace.SmartspaceManagerService";
+    private static final String CONTEXTUAL_SEARCH_MANAGER_SERVICE_CLASS =
+            "com.android.server.contextualsearch.ContextualSearchManagerService";
     private static final String DEVICE_IDLE_CONTROLLER_CLASS =
             "com.android.server.DeviceIdleController";
     private static final String BLOB_STORE_MANAGER_SERVICE_CLASS =
@@ -2012,6 +2014,16 @@
                 Slog.d(TAG, "SmartspaceManagerService not defined by OEM or disabled by flag");
             }
 
+            // Contextual search manager service
+            if (deviceHasConfigString(context,
+                    R.string.config_defaultContextualSearchPackageName)) {
+                t.traceBegin("StartContextualSearchService");
+                mSystemServiceManager.startService(CONTEXTUAL_SEARCH_MANAGER_SERVICE_CLASS);
+                t.traceEnd();
+            } else {
+                Slog.d(TAG, "ContextualSearchManagerService not defined or disabled by flag");
+            }
+
             t.traceBegin("InitConnectivityModuleConnector");
             try {
                 ConnectivityModuleConnector.getInstance().init(context);