1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.quickstep.util 18 19 import android.app.contextualsearch.ContextualSearchManager 20 import android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_LONG_PRESS_HOME 21 import android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH 22 import android.content.Context 23 import android.util.Log 24 import android.view.Display.DEFAULT_DISPLAY 25 import androidx.annotation.VisibleForTesting 26 import com.android.internal.app.AssistUtils 27 import com.android.launcher3.logging.StatsLogManager 28 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR 29 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD 30 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE 31 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN 32 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE 33 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED 34 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME 35 import com.android.quickstep.BaseContainerInterface 36 import com.android.quickstep.DeviceConfigWrapper 37 import com.android.quickstep.OverviewComponentObserver 38 import com.android.quickstep.SystemUiProxy 39 import com.android.quickstep.TopTaskTracker 40 import com.android.quickstep.views.RecentsView 41 import com.android.systemui.shared.system.QuickStepContract 42 43 /** Handles invocations and checks for Contextual Search. */ 44 class ContextualSearchInvoker 45 internal constructor( 46 private val context: Context, 47 private val contextualSearchStateManager: ContextualSearchStateManager, 48 private val topTaskTracker: TopTaskTracker, 49 private val systemUiProxy: SystemUiProxy, 50 private val statsLogManager: StatsLogManager, 51 private val contextualSearchHapticManager: ContextualSearchHapticManager, 52 private val contextualSearchManager: ContextualSearchManager?, 53 ) { 54 constructor( 55 context: Context 56 ) : this( 57 context, 58 ContextualSearchStateManager.INSTANCE[context], 59 TopTaskTracker.INSTANCE[context], 60 SystemUiProxy.INSTANCE[context], 61 StatsLogManager.newInstance(context), 62 ContextualSearchHapticManager.INSTANCE[context], 63 context.getSystemService(ContextualSearchManager::class.java), 64 ) 65 66 /** @return Array of AssistUtils.INVOCATION_TYPE_* that we want to handle instead of SysUI. */ getSysUiAssistOverrideInvocationTypesnull67 fun getSysUiAssistOverrideInvocationTypes(): IntArray { 68 val overrideInvocationTypes = com.android.launcher3.util.IntArray() 69 if (context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) { 70 overrideInvocationTypes.add(AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS) 71 } 72 return overrideInvocationTypes.toArray() 73 } 74 75 /** 76 * @return `true` if the override was handled, i.e. an assist surface was shown or the request 77 * should be ignored. `false` means the caller should start assist another way. 78 */ tryStartAssistOverridenull79 fun tryStartAssistOverride(invocationType: Int): Boolean { 80 if (invocationType == AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS) { 81 if (!context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) { 82 // When Contextual Search is disabled, fall back to Assistant. 83 return false 84 } 85 86 val success = show(ENTRYPOINT_LONG_PRESS_HOME) 87 if (success) { 88 val runningPackage = 89 TopTaskTracker.INSTANCE[context].getCachedTopTask( 90 /* filterOnlyVisibleRecents */ true, 91 DEFAULT_DISPLAY, 92 ) 93 .getPackageName() 94 statsLogManager 95 .logger() 96 .withPackageName(runningPackage) 97 .log(LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME) 98 } 99 100 // Regardless of success, do not fall back to other assistant. 101 return true 102 } 103 return false 104 } 105 106 /** 107 * Invoke Contextual Search via ContextualSearchService if availability checks are successful 108 * 109 * @param entryPoint one of the ENTRY_POINT_* constants defined in this class 110 * @return true if invocation was successful, false otherwise 111 */ shownull112 fun show(entryPoint: Int): Boolean { 113 return if (!runContextualSearchInvocationChecksAndLogFailures()) false 114 else invokeContextualSearchUnchecked(entryPoint) 115 } 116 117 /** 118 * Run availability checks and log errors to WW. If successful the caller is expected to call 119 * {@link invokeContextualSearchUnchecked} 120 * 121 * @return true if availability checks were successful, false otherwise. 122 */ runContextualSearchInvocationChecksAndLogFailuresnull123 fun runContextualSearchInvocationChecksAndLogFailures(): Boolean { 124 if ( 125 contextualSearchManager == null || 126 !context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH) 127 ) { 128 Log.i(TAG, "Contextual Search invocation failed: no ContextualSearchManager") 129 statsLogManager.logger().log(LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR) 130 return false 131 } 132 if (!contextualSearchStateManager.isContextualSearchSettingEnabled) { 133 Log.i(TAG, "Contextual Search invocation failed: setting disabled") 134 statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED) 135 return false 136 } 137 if (isNotificationShadeShowing()) { 138 Log.i(TAG, "Contextual Search invocation failed: notification shade") 139 statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE) 140 return false 141 } 142 if (isKeyguardShowing()) { 143 Log.i(TAG, "Contextual Search invocation attempted: keyguard") 144 statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD) 145 if (!contextualSearchStateManager.isInvocationAllowedOnKeyguard) { 146 Log.i(TAG, "Contextual Search invocation failed: keyguard not allowed") 147 return false 148 } else if (!contextualSearchStateManager.supportsShowWhenLocked()) { 149 Log.i(TAG, "Contextual Search invocation failed: AGA doesn't support keyguard") 150 return false 151 } 152 } 153 if (isInSplitscreen()) { 154 Log.i(TAG, "Contextual Search invocation attempted: splitscreen") 155 statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN) 156 if (!contextualSearchStateManager.isInvocationAllowedInSplitscreen) { 157 Log.i(TAG, "Contextual Search invocation failed: splitscreen not allowed") 158 return false 159 } 160 } 161 if (!contextualSearchStateManager.isContextualSearchIntentAvailable) { 162 Log.i(TAG, "Contextual Search invocation failed: no matching CSS intent filter") 163 statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE) 164 return false 165 } 166 if (isFakeLandscape()) { 167 // TODO (b/383421642): Fake landscape is to be removed in 25Q3 and this entire block 168 // can be removed when that happens. 169 return false 170 } 171 return true 172 } 173 174 /** 175 * Invoke Contextual Search via ContextualSearchService and do haptic 176 * 177 * @param entryPoint Entry point identifier, passed to ContextualSearchService. 178 * @return true if invocation was successful, false otherwise 179 */ invokeContextualSearchUncheckedWithHapticnull180 fun invokeContextualSearchUncheckedWithHaptic(entryPoint: Int): Boolean { 181 return invokeContextualSearchUnchecked(entryPoint, withHaptic = true) 182 } 183 invokeContextualSearchUncheckednull184 private fun invokeContextualSearchUnchecked( 185 entryPoint: Int, 186 withHaptic: Boolean = false, 187 ): Boolean { 188 if (withHaptic && DeviceConfigWrapper.get().enableSearchHapticCommit) { 189 contextualSearchHapticManager.vibrateForSearch() 190 } 191 if (contextualSearchManager == null) { 192 return false 193 } 194 val recentsContainerInterface = getRecentsContainerInterface() 195 if (recentsContainerInterface?.isInLiveTileMode() == true) { 196 Log.i(TAG, "Contextual Search invocation attempted: live tile") 197 endLiveTileMode(recentsContainerInterface) { 198 contextualSearchManager.startContextualSearch(entryPoint) 199 } 200 } else { 201 contextualSearchManager.startContextualSearch(entryPoint) 202 } 203 return true 204 } 205 isFakeLandscapenull206 private fun isFakeLandscape(): Boolean = 207 getRecentsContainerInterface() 208 ?.getCreatedContainer() 209 ?.getOverviewPanel<RecentsView<*, *>>() 210 ?.getPagedOrientationHandler() 211 ?.isLayoutNaturalToLauncher == false 212 213 private fun isInSplitscreen(): Boolean { 214 return topTaskTracker.getRunningSplitTaskIds().isNotEmpty() 215 } 216 isNotificationShadeShowingnull217 private fun isNotificationShadeShowing(): Boolean { 218 return systemUiProxy.lastSystemUiStateFlags and SHADE_EXPANDED_SYSUI_FLAGS != 0L 219 } 220 isKeyguardShowingnull221 private fun isKeyguardShowing(): Boolean { 222 return systemUiProxy.lastSystemUiStateFlags and KEYGUARD_SHOWING_SYSUI_FLAGS != 0L 223 } 224 225 @VisibleForTesting getRecentsContainerInterfacenull226 fun getRecentsContainerInterface(): BaseContainerInterface<*, *>? { 227 return OverviewComponentObserver.INSTANCE.get(context) 228 .getContainerInterface(DEFAULT_DISPLAY) 229 } 230 231 /** 232 * End the live tile mode. 233 * 234 * @param onCompleteRunnable Runnable to run when the live tile is paused. May run immediately. 235 */ endLiveTileModenull236 private fun endLiveTileMode( 237 recentsContainerInterface: BaseContainerInterface<*, *>?, 238 onCompleteRunnable: Runnable, 239 ) { 240 val recentsViewContainer = recentsContainerInterface?.createdContainer 241 if (recentsViewContainer == null) { 242 onCompleteRunnable.run() 243 return 244 } 245 val recentsView: RecentsView<*, *> = recentsViewContainer.getOverviewPanel() 246 recentsView.switchToScreenshot { 247 recentsView.finishRecentsAnimation( 248 true, /* toRecents */ 249 false, /* shouldPip */ 250 onCompleteRunnable, 251 ) 252 } 253 } 254 255 companion object { 256 private const val TAG = "ContextualSearchInvoker" 257 const val SHADE_EXPANDED_SYSUI_FLAGS = 258 QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED or 259 QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED 260 const val KEYGUARD_SHOWING_SYSUI_FLAGS = 261 (QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING or 262 QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING or 263 QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED) 264 } 265 } 266