• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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