/* * 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.quickstep.util import android.app.contextualsearch.ContextualSearchManager import android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_LONG_PRESS_HOME import android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH import android.content.Context import android.util.Log import android.view.Display.DEFAULT_DISPLAY import androidx.annotation.VisibleForTesting import com.android.internal.app.AssistUtils import com.android.launcher3.logging.StatsLogManager import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME import com.android.quickstep.BaseContainerInterface import com.android.quickstep.DeviceConfigWrapper import com.android.quickstep.OverviewComponentObserver import com.android.quickstep.SystemUiProxy import com.android.quickstep.TopTaskTracker import com.android.quickstep.views.RecentsView import com.android.systemui.shared.system.QuickStepContract /** Handles invocations and checks for Contextual Search. */ class ContextualSearchInvoker internal constructor( private val context: Context, private val contextualSearchStateManager: ContextualSearchStateManager, private val topTaskTracker: TopTaskTracker, private val systemUiProxy: SystemUiProxy, private val statsLogManager: StatsLogManager, private val contextualSearchHapticManager: ContextualSearchHapticManager, private val contextualSearchManager: ContextualSearchManager?, ) { constructor( context: Context ) : this( context, ContextualSearchStateManager.INSTANCE[context], TopTaskTracker.INSTANCE[context], SystemUiProxy.INSTANCE[context], StatsLogManager.newInstance(context), ContextualSearchHapticManager.INSTANCE[context], context.getSystemService(ContextualSearchManager::class.java), ) /** @return Array of AssistUtils.INVOCATION_TYPE_* that we want to handle instead of SysUI. */ fun getSysUiAssistOverrideInvocationTypes(): IntArray { val overrideInvocationTypes = com.android.launcher3.util.IntArray() if (context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) { overrideInvocationTypes.add(AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS) } return overrideInvocationTypes.toArray() } /** * @return `true` if the override was handled, i.e. an assist surface was shown or the request * should be ignored. `false` means the caller should start assist another way. */ fun tryStartAssistOverride(invocationType: Int): Boolean { if (invocationType == AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS) { if (!context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) { // When Contextual Search is disabled, fall back to Assistant. return false } val success = show(ENTRYPOINT_LONG_PRESS_HOME) if (success) { val runningPackage = TopTaskTracker.INSTANCE[context].getCachedTopTask( /* filterOnlyVisibleRecents */ true, DEFAULT_DISPLAY, ) .getPackageName() statsLogManager .logger() .withPackageName(runningPackage) .log(LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME) } // Regardless of success, do not fall back to other assistant. return true } return false } /** * Invoke Contextual Search via ContextualSearchService if availability checks are successful * * @param entryPoint one of the ENTRY_POINT_* constants defined in this class * @return true if invocation was successful, false otherwise */ fun show(entryPoint: Int): Boolean { return if (!runContextualSearchInvocationChecksAndLogFailures()) false else invokeContextualSearchUnchecked(entryPoint) } /** * Run availability checks and log errors to WW. If successful the caller is expected to call * {@link invokeContextualSearchUnchecked} * * @return true if availability checks were successful, false otherwise. */ fun runContextualSearchInvocationChecksAndLogFailures(): Boolean { if ( contextualSearchManager == null || !context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH) ) { Log.i(TAG, "Contextual Search invocation failed: no ContextualSearchManager") statsLogManager.logger().log(LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR) return false } if (!contextualSearchStateManager.isContextualSearchSettingEnabled) { Log.i(TAG, "Contextual Search invocation failed: setting disabled") statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED) return false } if (isNotificationShadeShowing()) { Log.i(TAG, "Contextual Search invocation failed: notification shade") statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE) return false } if (isKeyguardShowing()) { Log.i(TAG, "Contextual Search invocation attempted: keyguard") statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD) if (!contextualSearchStateManager.isInvocationAllowedOnKeyguard) { Log.i(TAG, "Contextual Search invocation failed: keyguard not allowed") return false } else if (!contextualSearchStateManager.supportsShowWhenLocked()) { Log.i(TAG, "Contextual Search invocation failed: AGA doesn't support keyguard") return false } } if (isInSplitscreen()) { Log.i(TAG, "Contextual Search invocation attempted: splitscreen") statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN) if (!contextualSearchStateManager.isInvocationAllowedInSplitscreen) { Log.i(TAG, "Contextual Search invocation failed: splitscreen not allowed") return false } } if (!contextualSearchStateManager.isContextualSearchIntentAvailable) { Log.i(TAG, "Contextual Search invocation failed: no matching CSS intent filter") statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE) return false } if (isFakeLandscape()) { // TODO (b/383421642): Fake landscape is to be removed in 25Q3 and this entire block // can be removed when that happens. return false } return true } /** * Invoke Contextual Search via ContextualSearchService and do haptic * * @param entryPoint Entry point identifier, passed to ContextualSearchService. * @return true if invocation was successful, false otherwise */ fun invokeContextualSearchUncheckedWithHaptic(entryPoint: Int): Boolean { return invokeContextualSearchUnchecked(entryPoint, withHaptic = true) } private fun invokeContextualSearchUnchecked( entryPoint: Int, withHaptic: Boolean = false, ): Boolean { if (withHaptic && DeviceConfigWrapper.get().enableSearchHapticCommit) { contextualSearchHapticManager.vibrateForSearch() } if (contextualSearchManager == null) { return false } val recentsContainerInterface = getRecentsContainerInterface() if (recentsContainerInterface?.isInLiveTileMode() == true) { Log.i(TAG, "Contextual Search invocation attempted: live tile") endLiveTileMode(recentsContainerInterface) { contextualSearchManager.startContextualSearch(entryPoint) } } else { contextualSearchManager.startContextualSearch(entryPoint) } return true } private fun isFakeLandscape(): Boolean = getRecentsContainerInterface() ?.getCreatedContainer() ?.getOverviewPanel>() ?.getPagedOrientationHandler() ?.isLayoutNaturalToLauncher == false private fun isInSplitscreen(): Boolean { return topTaskTracker.getRunningSplitTaskIds().isNotEmpty() } private fun isNotificationShadeShowing(): Boolean { return systemUiProxy.lastSystemUiStateFlags and SHADE_EXPANDED_SYSUI_FLAGS != 0L } private fun isKeyguardShowing(): Boolean { return systemUiProxy.lastSystemUiStateFlags and KEYGUARD_SHOWING_SYSUI_FLAGS != 0L } @VisibleForTesting fun getRecentsContainerInterface(): BaseContainerInterface<*, *>? { return OverviewComponentObserver.INSTANCE.get(context) .getContainerInterface(DEFAULT_DISPLAY) } /** * End the live tile mode. * * @param onCompleteRunnable Runnable to run when the live tile is paused. May run immediately. */ private fun endLiveTileMode( recentsContainerInterface: BaseContainerInterface<*, *>?, onCompleteRunnable: Runnable, ) { val recentsViewContainer = recentsContainerInterface?.createdContainer if (recentsViewContainer == null) { onCompleteRunnable.run() return } val recentsView: RecentsView<*, *> = recentsViewContainer.getOverviewPanel() recentsView.switchToScreenshot { recentsView.finishRecentsAnimation( true, /* toRecents */ false, /* shouldPip */ onCompleteRunnable, ) } } companion object { private const val TAG = "ContextualSearchInvoker" const val SHADE_EXPANDED_SYSUI_FLAGS = QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED or QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED const val KEYGUARD_SHOWING_SYSUI_FLAGS = (QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING or QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING or QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED) } }