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 androidx.annotation.VisibleForTesting 25 import com.android.internal.app.AssistUtils 26 import com.android.launcher3.logging.StatsLogManager 27 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR 28 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD 29 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE 30 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN 31 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE 32 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED 33 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME 34 import com.android.quickstep.BaseContainerInterface 35 import com.android.quickstep.DeviceConfigWrapper 36 import com.android.quickstep.OverviewComponentObserver 37 import com.android.quickstep.SystemUiProxy 38 import com.android.quickstep.TopTaskTracker 39 import com.android.quickstep.views.RecentsView 40 import com.android.systemui.shared.system.QuickStepContract 41 42 /** Handles invocations and checks for Contextual Search. */ 43 class ContextualSearchInvoker 44 internal constructor( 45 private val context: Context, 46 private val contextualSearchStateManager: ContextualSearchStateManager, 47 private val topTaskTracker: TopTaskTracker, 48 private val systemUiProxy: SystemUiProxy, 49 private val statsLogManager: StatsLogManager, 50 private val contextualSearchHapticManager: ContextualSearchHapticManager, 51 private val contextualSearchManager: ContextualSearchManager?, 52 ) { 53 constructor( 54 context: Context 55 ) : this( 56 context, 57 ContextualSearchStateManager.INSTANCE[context], 58 TopTaskTracker.INSTANCE[context], 59 SystemUiProxy.INSTANCE[context], 60 StatsLogManager.newInstance(context), 61 ContextualSearchHapticManager.INSTANCE[context], 62 context.getSystemService(ContextualSearchManager::class.java), 63 ) 64 65 /** @return Array of AssistUtils.INVOCATION_TYPE_* that we want to handle instead of SysUI. */ getSysUiAssistOverrideInvocationTypesnull66 fun getSysUiAssistOverrideInvocationTypes(): IntArray { 67 val overrideInvocationTypes = com.android.launcher3.util.IntArray() 68 if (context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) { 69 overrideInvocationTypes.add(AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS) 70 } 71 return overrideInvocationTypes.toArray() 72 } 73 74 /** 75 * @return `true` if the override was handled, i.e. an assist surface was shown or the request 76 * should be ignored. `false` means the caller should start assist another way. 77 */ tryStartAssistOverridenull78 fun tryStartAssistOverride(invocationType: Int): Boolean { 79 if (invocationType == AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS) { 80 if (!context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) { 81 // When Contextual Search is disabled, fall back to Assistant. 82 return false 83 } 84 85 val success = show(ENTRYPOINT_LONG_PRESS_HOME) 86 if (success) { 87 val runningPackage = 88 TopTaskTracker.INSTANCE[context].getCachedTopTask( 89 /* filterOnlyVisibleRecents */ true 90 ) 91 .getPackageName() 92 statsLogManager 93 .logger() 94 .withPackageName(runningPackage) 95 .log(LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME) 96 } 97 98 // Regardless of success, do not fall back to other assistant. 99 return true 100 } 101 return false 102 } 103 104 /** 105 * Invoke Contextual Search via ContextualSearchService if availability checks are successful 106 * 107 * @param entryPoint one of the ENTRY_POINT_* constants defined in this class 108 * @return true if invocation was successful, false otherwise 109 */ shownull110 fun show(entryPoint: Int): Boolean { 111 return if (!runContextualSearchInvocationChecksAndLogFailures()) false 112 else invokeContextualSearchUnchecked(entryPoint) 113 } 114 115 /** 116 * Run availability checks and log errors to WW. If successful the caller is expected to call 117 * {@link invokeContextualSearchUnchecked} 118 * 119 * @return true if availability checks were successful, false otherwise. 120 */ runContextualSearchInvocationChecksAndLogFailuresnull121 fun runContextualSearchInvocationChecksAndLogFailures(): Boolean { 122 if ( 123 contextualSearchManager == null || 124 !context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH) 125 ) { 126 Log.i(TAG, "Contextual Search invocation failed: no ContextualSearchManager") 127 statsLogManager.logger().log(LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR) 128 return false 129 } 130 if (!contextualSearchStateManager.isContextualSearchSettingEnabled) { 131 Log.i(TAG, "Contextual Search invocation failed: setting disabled") 132 statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED) 133 return false 134 } 135 if (isNotificationShadeShowing()) { 136 Log.i(TAG, "Contextual Search invocation failed: notification shade") 137 statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE) 138 return false 139 } 140 if (isKeyguardShowing()) { 141 Log.i(TAG, "Contextual Search invocation attempted: keyguard") 142 statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD) 143 if (!contextualSearchStateManager.isInvocationAllowedOnKeyguard) { 144 Log.i(TAG, "Contextual Search invocation failed: keyguard not allowed") 145 return false 146 } else if (!contextualSearchStateManager.supportsShowWhenLocked()) { 147 Log.i(TAG, "Contextual Search invocation failed: AGA doesn't support keyguard") 148 return false 149 } 150 } 151 if (isInSplitscreen()) { 152 Log.i(TAG, "Contextual Search invocation attempted: splitscreen") 153 statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN) 154 if (!contextualSearchStateManager.isInvocationAllowedInSplitscreen) { 155 Log.i(TAG, "Contextual Search invocation failed: splitscreen not allowed") 156 return false 157 } 158 } 159 if (!contextualSearchStateManager.isContextualSearchIntentAvailable) { 160 Log.i(TAG, "Contextual Search invocation failed: no matching CSS intent filter") 161 statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE) 162 return false 163 } 164 if (isFakeLandscape()) { 165 // TODO (b/383421642): Fake landscape is to be removed in 25Q3 and this entire block 166 // can be removed when that happens. 167 return false 168 } 169 return true 170 } 171 172 /** 173 * Invoke Contextual Search via ContextualSearchService and do haptic 174 * 175 * @param entryPoint Entry point identifier, passed to ContextualSearchService. 176 * @return true if invocation was successful, false otherwise 177 */ invokeContextualSearchUncheckedWithHapticnull178 fun invokeContextualSearchUncheckedWithHaptic(entryPoint: Int): Boolean { 179 return invokeContextualSearchUnchecked(entryPoint, withHaptic = true) 180 } 181 invokeContextualSearchUncheckednull182 private fun invokeContextualSearchUnchecked( 183 entryPoint: Int, 184 withHaptic: Boolean = false, 185 ): Boolean { 186 if (withHaptic && DeviceConfigWrapper.get().enableSearchHapticCommit) { 187 contextualSearchHapticManager.vibrateForSearch() 188 } 189 if (contextualSearchManager == null) { 190 return false 191 } 192 val recentsContainerInterface = getRecentsContainerInterface() 193 if (recentsContainerInterface?.isInLiveTileMode() == true) { 194 Log.i(TAG, "Contextual Search invocation attempted: live tile") 195 endLiveTileMode(recentsContainerInterface) { 196 contextualSearchManager.startContextualSearch(entryPoint) 197 } 198 } else { 199 contextualSearchManager.startContextualSearch(entryPoint) 200 } 201 return true 202 } 203 isFakeLandscapenull204 private fun isFakeLandscape(): Boolean = 205 getRecentsContainerInterface() 206 ?.getCreatedContainer() 207 ?.getOverviewPanel<RecentsView<*, *>>() 208 ?.getPagedOrientationHandler() 209 ?.isLayoutNaturalToLauncher == false 210 211 private fun isInSplitscreen(): Boolean { 212 return topTaskTracker.getRunningSplitTaskIds().isNotEmpty() 213 } 214 isNotificationShadeShowingnull215 private fun isNotificationShadeShowing(): Boolean { 216 return systemUiProxy.lastSystemUiStateFlags and SHADE_EXPANDED_SYSUI_FLAGS != 0L 217 } 218 isKeyguardShowingnull219 private fun isKeyguardShowing(): Boolean { 220 return systemUiProxy.lastSystemUiStateFlags and KEYGUARD_SHOWING_SYSUI_FLAGS != 0L 221 } 222 223 @VisibleForTesting getRecentsContainerInterfacenull224 fun getRecentsContainerInterface(): BaseContainerInterface<*, *>? { 225 return OverviewComponentObserver.INSTANCE.get(context).containerInterface 226 } 227 228 /** 229 * End the live tile mode. 230 * 231 * @param onCompleteRunnable Runnable to run when the live tile is paused. May run immediately. 232 */ endLiveTileModenull233 private fun endLiveTileMode( 234 recentsContainerInterface: BaseContainerInterface<*, *>?, 235 onCompleteRunnable: Runnable, 236 ) { 237 val recentsViewContainer = recentsContainerInterface?.createdContainer 238 if (recentsViewContainer == null) { 239 onCompleteRunnable.run() 240 return 241 } 242 val recentsView: RecentsView<*, *> = recentsViewContainer.getOverviewPanel() 243 recentsView.switchToScreenshot { 244 recentsView.finishRecentsAnimation( 245 true, /* toRecents */ 246 false, /* shouldPip */ 247 onCompleteRunnable, 248 ) 249 } 250 } 251 252 companion object { 253 private const val TAG = "ContextualSearchInvoker" 254 const val SHADE_EXPANDED_SYSUI_FLAGS = 255 QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED or 256 QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED 257 const val KEYGUARD_SHOWING_SYSUI_FLAGS = 258 (QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING or 259 QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING or 260 QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED) 261 } 262 } 263