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