ContextualSearch API initial implementation

Test: CTS
Bug: 309689654
Change-Id: I0b820180eca2dfe32287132b941a5719ad7e26ca
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);