• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  *  Copyright (C) 2023 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 
18 package com.android.quickstep.util
19 
20 import android.annotation.IntDef
21 import android.app.ActivityManager.RunningTaskInfo
22 import android.app.ActivityTaskManager.INVALID_TASK_ID
23 import android.app.PendingIntent
24 import android.content.Context
25 import android.content.Intent
26 import android.content.pm.PackageManager
27 import android.content.pm.ShortcutInfo
28 import android.os.UserHandle
29 import android.util.Log
30 import com.android.internal.annotations.VisibleForTesting
31 import com.android.launcher3.logging.StatsLogManager.EventEnum
32 import com.android.launcher3.model.data.ItemInfo
33 import com.android.launcher3.shortcuts.ShortcutKey
34 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
35 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition
36 import com.android.launcher3.util.SplitConfigurationOptions.getOppositeStagePosition
37 import com.android.quickstep.util.SplitSelectDataHolder.Companion.SplitLaunchType
38 import java.io.PrintWriter
39 
40 /**
41  * Holds/transforms/signs/seals/delivers information for the transient state of the user
42  * selecting a first app to start split with and then choosing a second app.
43  * This class DOES NOT associate itself with drag-and-drop split screen starts because they come
44  * from the bad part of town.
45  *
46  * After setting the correct fields for initial/second.* variables, this converts them into the
47  * correct [PendingIntent] and [ShortcutInfo] objects where applicable and sends the necessary
48  * data back via [getSplitLaunchData]. Note: there should be only one "initial" field and one
49  * "second" field set, with the rest remaining null. (Exception: [Intent] and [UserHandle] are
50  * always passed in together as a set, and are converted to a single [PendingIntent] or
51  * [ShortcutInfo]+[PendingIntent] before launch.)
52  *
53  * [SplitLaunchType] indicates the type of tasks/apps/intents being launched given the provided
54  * state
55  */
56 class SplitSelectDataHolder(
57         val context: Context
58 ) {
59     val TAG = SplitSelectDataHolder::class.simpleName
60 
61     /**
62      * Order of the constant indicates the order of which task/app was selected.
63      * Ex. SPLIT_TASK_SHORTCUT means primary split app identified by task, secondary is shortcut
64      * SPLIT_SHORTCUT_TASK means primary split app is determined by shortcut, secondary is task
65      */
66     companion object {
67         @IntDef(SPLIT_TASK_TASK, SPLIT_TASK_PENDINGINTENT, SPLIT_TASK_SHORTCUT,
68                 SPLIT_PENDINGINTENT_TASK, SPLIT_PENDINGINTENT_PENDINGINTENT, SPLIT_SHORTCUT_TASK,
69                 SPLIT_SINGLE_TASK_FULLSCREEN, SPLIT_SINGLE_INTENT_FULLSCREEN,
70                 SPLIT_SINGLE_SHORTCUT_FULLSCREEN)
71         @Retention(AnnotationRetention.SOURCE)
72         annotation class SplitLaunchType
73 
74         const val SPLIT_TASK_TASK = 0
75         const val SPLIT_TASK_PENDINGINTENT = 1
76         const val SPLIT_TASK_SHORTCUT = 2
77         const val SPLIT_PENDINGINTENT_TASK = 3
78         const val SPLIT_SHORTCUT_TASK = 4
79         const val SPLIT_PENDINGINTENT_PENDINGINTENT = 5
80 
81         // Non-split edge case of launching the initial selected task as a fullscreen task
82         const val SPLIT_SINGLE_TASK_FULLSCREEN = 6
83         const val SPLIT_SINGLE_INTENT_FULLSCREEN = 7
84         const val SPLIT_SINGLE_SHORTCUT_FULLSCREEN = 8
85     }
86 
87     @StagePosition
88     private var initialStagePosition: Int = STAGE_POSITION_UNDEFINED
89     private var itemInfo: ItemInfo? = null
90     private var splitEvent: EventEnum? = null
91 
92     private var initialTaskId: Int = INVALID_TASK_ID
93     private var secondTaskId: Int = INVALID_TASK_ID
94     private var initialIntent: Intent? = null
95     private var secondIntent: Intent? = null
96     private var initialUser: UserHandle? = null
97     private var secondUser: UserHandle? = null
98     private var initialPendingIntent: PendingIntent? = null
99     private var secondPendingIntent: PendingIntent? = null
100     private var initialShortcut: ShortcutInfo? = null
101     private var secondShortcut: ShortcutInfo? = null
102 
103     /**
104      * @param alreadyRunningTask if set to [android.app.ActivityTaskManager.INVALID_TASK_ID]
105      * then @param intent will be used to launch the initial task
106      * @param intent will be ignored if @param alreadyRunningTask is set
107      */
setInitialTaskSelectnull108     fun setInitialTaskSelect(intent: Intent?, @StagePosition stagePosition: Int,
109                              itemInfo: ItemInfo?, splitEvent: EventEnum?,
110                              alreadyRunningTask: Int) {
111         if (alreadyRunningTask != INVALID_TASK_ID) {
112             initialTaskId = alreadyRunningTask
113         } else {
114             initialIntent = intent!!
115             initialUser = itemInfo!!.user
116         }
117         setInitialData(stagePosition, splitEvent, itemInfo)
118     }
119 
120     /**
121      * To be called after first task selected from using a split shortcut from the fullscreen
122      * running app.
123      */
setInitialTaskSelectnull124     fun setInitialTaskSelect(info: RunningTaskInfo,
125                              @StagePosition stagePosition: Int, itemInfo: ItemInfo?,
126                              splitEvent: EventEnum?) {
127         initialTaskId = info.taskId
128         setInitialData(stagePosition, splitEvent, itemInfo)
129     }
130 
setInitialDatanull131     private fun setInitialData(@StagePosition stagePosition: Int,
132                                event: EventEnum?, item: ItemInfo?) {
133         itemInfo = item
134         initialStagePosition = stagePosition
135         splitEvent = event
136     }
137 
138     /**
139      * To be called as soon as user selects the second task (even if animations aren't complete)
140      * @param taskId The second task that will be launched.
141      */
setSecondTasknull142     fun setSecondTask(taskId: Int) {
143         secondTaskId = taskId
144     }
145 
146     /**
147      * To be called as soon as user selects the second app (even if animations aren't complete)
148      * @param intent The second intent that will be launched.
149      * @param user The user of that intent.
150      */
setSecondTasknull151     fun setSecondTask(intent: Intent, user: UserHandle) {
152         secondIntent = intent
153         secondUser = user
154     }
155 
156     /**
157      * To be called as soon as user selects the second app (even if animations aren't complete)
158      * Sets [secondUser] from that of the pendingIntent
159      * @param pendingIntent The second PendingIntent that will be launched.
160      */
setSecondTasknull161     fun setSecondTask(pendingIntent: PendingIntent) {
162         secondPendingIntent = pendingIntent
163         secondUser = pendingIntent.creatorUserHandle!!
164     }
165 
getShortcutInfonull166     private fun getShortcutInfo(intent: Intent?, user: UserHandle?): ShortcutInfo? {
167         if (intent?.getPackage() == null) {
168             return null
169         }
170         val shortcutId = intent.getStringExtra(ShortcutKey.EXTRA_SHORTCUT_ID)
171                 ?: return null
172         try {
173             val context: Context = context.createPackageContextAsUser(
174                     intent.getPackage(), 0 /* flags */, user)
175             return ShortcutInfo.Builder(context, shortcutId).build()
176         } catch (e: PackageManager.NameNotFoundException) {
177             Log.w(TAG, "Failed to create a ShortcutInfo for " + intent.getPackage())
178         }
179         return null
180     }
181 
182     /**
183      * Converts intents to pendingIntents, associating the [user] with the intent if provided
184      */
getPendingIntentnull185     private fun getPendingIntent(intent: Intent?, user: UserHandle?): PendingIntent? {
186         if (intent != initialIntent && intent != secondIntent) {
187             throw IllegalStateException("Invalid intent to convert to PendingIntent")
188         }
189 
190         return if (intent == null) {
191             null
192         } else if (user != null) {
193             PendingIntent.getActivityAsUser(context, 0, intent,
194                     PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT,
195                     null /* options */, user)
196         } else {
197             PendingIntent.getActivity(context, 0, intent,
198                     PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT)
199         }
200     }
201 
202     /**
203      * @return [SplitLaunchData] with the necessary fields populated as determined by
204      *   [SplitLaunchData.splitLaunchType]. This is to be used for launching splitscreen
205      */
getSplitLaunchDatanull206     fun getSplitLaunchData() : SplitLaunchData {
207         // Convert all intents to shortcut infos to see if determine if we launch shortcut or intent
208         convertIntentsToFinalTypes()
209         val splitLaunchType = getSplitLaunchType()
210         if (splitLaunchType == SPLIT_TASK_PENDINGINTENT || splitLaunchType == SPLIT_TASK_SHORTCUT) {
211             // need to get opposite stage position
212             initialStagePosition = getOppositeStagePosition(initialStagePosition)
213         }
214 
215         return generateSplitLaunchData(splitLaunchType)
216     }
217 
218     /**
219      * @return [SplitLaunchData] with the necessary fields populated as determined by
220      *   [SplitLaunchData.splitLaunchType]. This is to be used for launching an initially selected
221      *   split task in fullscreen
222      */
getFullscreenLaunchDatanull223     fun getFullscreenLaunchData() : SplitLaunchData {
224         // Convert all intents to shortcut infos to determine if we launch shortcut or intent
225         convertIntentsToFinalTypes()
226         val splitLaunchType = getFullscreenLaunchType()
227 
228         return generateSplitLaunchData(splitLaunchType)
229     }
230 
generateSplitLaunchDatanull231     private fun generateSplitLaunchData(@SplitLaunchType splitLaunchType: Int) : SplitLaunchData {
232         return SplitLaunchData(
233                 splitLaunchType,
234                 initialTaskId,
235                 secondTaskId,
236                 initialPendingIntent,
237                 secondPendingIntent,
238                 initialUser?.identifier ?: -1,
239                 secondUser?.identifier ?: -1,
240                 initialShortcut,
241                 secondShortcut,
242                 itemInfo,
243                 splitEvent,
244                 initialStagePosition)
245     }
246 
247     /**
248      * Converts our [initialIntent] and [secondIntent] into shortcuts and pendingIntents, if
249      * possible.
250      *
251      * Note that both [initialIntent] and [secondIntent] will be nullified on method return
252      *
253      * One caveat is that if [secondPendingIntent] is set, we will use that and *not* attempt to
254      * convert [secondIntent]
255      */
convertIntentsToFinalTypesnull256     private fun convertIntentsToFinalTypes() {
257         initialShortcut = getShortcutInfo(initialIntent, initialUser)
258         initialPendingIntent = getPendingIntent(initialIntent, initialUser)
259         initialIntent = null
260 
261         // Only one of the two is currently allowed (secondPendingIntent directly set for widgets)
262         if (secondIntent != null && secondPendingIntent != null) {
263             throw IllegalStateException("Both secondIntent and secondPendingIntent non-null")
264         }
265         // If secondPendingIntent already set, no need to convert. Prioritize using that
266         if (secondPendingIntent != null) {
267             secondIntent = null
268             return
269         }
270 
271         secondShortcut = getShortcutInfo(secondIntent, secondUser)
272         secondPendingIntent = getPendingIntent(secondIntent, secondUser)
273         secondIntent = null
274     }
275 
276     /**
277      * Only valid data fields at this point should be tasks, shortcuts, or pendingIntents
278      * Intents need to be converted in [convertIntentsToFinalTypes] prior to calling this method
279      */
280     @VisibleForTesting
281     @SplitLaunchType
getSplitLaunchTypenull282     fun getSplitLaunchType(): Int {
283         if (initialIntent != null || secondIntent != null) {
284             throw IllegalStateException("Intents need to be converted")
285         }
286 
287         // Prioritize task launches first
288         if (initialTaskId != INVALID_TASK_ID) {
289             if (secondTaskId != INVALID_TASK_ID) {
290                 return SPLIT_TASK_TASK
291             }
292             if (secondShortcut != null) {
293                 return SPLIT_TASK_SHORTCUT
294             }
295             if (secondPendingIntent != null) {
296                 return SPLIT_TASK_PENDINGINTENT
297             }
298         }
299 
300         if (secondTaskId != INVALID_TASK_ID) {
301             if (initialShortcut != null) {
302                 return SPLIT_SHORTCUT_TASK
303             }
304             if (initialPendingIntent != null) {
305                 return SPLIT_PENDINGINTENT_TASK
306             }
307         }
308 
309         // All task+shortcut combinations are handled above, only launch left is with multiple
310         // intents (and respective shortcut infos, if necessary)
311         if (initialPendingIntent != null && secondPendingIntent != null) {
312             return SPLIT_PENDINGINTENT_PENDINGINTENT
313         }
314         throw IllegalStateException("Unidentified split launch type")
315     }
316 
317     @SplitLaunchType
getFullscreenLaunchTypenull318     private fun getFullscreenLaunchType(): Int {
319         if (initialTaskId != INVALID_TASK_ID) {
320             return SPLIT_SINGLE_TASK_FULLSCREEN
321         }
322 
323         if (initialShortcut != null) {
324             return SPLIT_SINGLE_SHORTCUT_FULLSCREEN
325         }
326 
327         if (initialPendingIntent != null) {
328             return SPLIT_SINGLE_INTENT_FULLSCREEN
329         }
330         throw IllegalStateException("Unidentified fullscreen launch type")
331     }
332 
333     data class SplitLaunchData(
334             @SplitLaunchType
335             val splitLaunchType: Int,
336             var initialTaskId: Int = INVALID_TASK_ID,
337             var secondTaskId: Int = INVALID_TASK_ID,
338             var initialPendingIntent: PendingIntent? = null,
339             var secondPendingIntent: PendingIntent? = null,
340             var initialUserId: Int = -1,
341             var secondUserId: Int = -1,
342             var initialShortcut: ShortcutInfo? = null,
343             var secondShortcut: ShortcutInfo? = null,
344             var itemInfo: ItemInfo? = null,
345             var splitEvent: EventEnum? = null,
346             val initialStagePosition: Int = STAGE_POSITION_UNDEFINED
347     )
348 
349     /**
350      * @return `true` if first task has been selected and waiting for the second task to be
351      * chosen
352      */
isSplitSelectActivenull353     fun isSplitSelectActive(): Boolean {
354         return isInitialTaskIntentSet() && !isSecondTaskIntentSet()
355     }
356 
357     /**
358      * @return `true` if the first and second task have been chosen and split is waiting to
359      * be launched
360      */
isBothSplitAppsConfirmednull361     fun isBothSplitAppsConfirmed(): Boolean {
362         return isInitialTaskIntentSet() && isSecondTaskIntentSet()
363     }
364 
isInitialTaskIntentSetnull365     private fun isInitialTaskIntentSet(): Boolean {
366         return initialTaskId != INVALID_TASK_ID || initialIntent != null
367     }
368 
getInitialTaskIdnull369     fun getInitialTaskId(): Int {
370         return initialTaskId
371     }
372 
getSecondTaskIdnull373     fun getSecondTaskId(): Int {
374         return secondTaskId
375     }
376 
getSplitEventnull377     fun getSplitEvent(): EventEnum? {
378         return splitEvent
379     }
380 
getInitialStagePositionnull381     fun getInitialStagePosition(): Int {
382         return initialStagePosition
383     }
384 
getItemInfonull385     fun getItemInfo(): ItemInfo? {
386         return itemInfo
387     }
388 
isSecondTaskIntentSetnull389     private fun isSecondTaskIntentSet(): Boolean {
390         return secondTaskId != INVALID_TASK_ID || secondIntent != null
391                 || secondPendingIntent != null
392     }
393 
resetStatenull394     fun resetState() {
395         initialStagePosition = STAGE_POSITION_UNDEFINED
396         initialTaskId = INVALID_TASK_ID
397         secondTaskId = INVALID_TASK_ID
398         initialUser = null
399         secondUser = null
400         initialIntent = null
401         secondIntent = null
402         secondPendingIntent = null
403         itemInfo = null
404         splitEvent = null
405         initialShortcut = null
406         secondShortcut = null
407     }
408 
dumpnull409     fun dump(prefix: String, writer: PrintWriter) {
410         writer.println("$prefix ${javaClass.simpleName}")
411         writer.println("$prefix\tinitialStagePosition= $initialStagePosition")
412         writer.println("$prefix\tinitialTaskId= $initialTaskId")
413         writer.println("$prefix\tsecondTaskId= $secondTaskId")
414         writer.println("$prefix\tinitialUser= $initialUser")
415         writer.println("$prefix\tsecondUser= $secondUser")
416         writer.println("$prefix\tinitialIntent= $initialIntent")
417         writer.println("$prefix\tsecondIntent= $secondIntent")
418         writer.println("$prefix\tsecondPendingIntent= $secondPendingIntent")
419         writer.println("$prefix\titemInfo= $itemInfo")
420         writer.println("$prefix\tsplitEvent= $splitEvent")
421         writer.println("$prefix\tinitialShortcut= $initialShortcut")
422         writer.println("$prefix\tsecondShortcut= $secondShortcut")
423     }
424 }