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 selecting a
42  * first app to start split with and then choosing a second app. This class DOES NOT associate
43  * itself with drag-and-drop split screen starts because they come from the bad part of town.
44  *
45  * After setting the correct fields for initial/second.* variables, this converts them into the
46  * correct [PendingIntent] and [ShortcutInfo] objects where applicable and sends the necessary data
47  * back via [getSplitLaunchData]. Note: there should be only one "initial" field and one "second"
48  * field set, with the rest remaining null. (Exception: [Intent] and [UserHandle] are always passed
49  * in together as a set, and are converted to a single [PendingIntent] or
50  * [ShortcutInfo]+[PendingIntent] before launch.)
51  *
52  * [SplitLaunchType] indicates the type of tasks/apps/intents being launched given the provided
53  * state
54  */
55 class SplitSelectDataHolder(var context: Context?) {
56     val TAG = SplitSelectDataHolder::class.simpleName
57 
58     /**
59      * Order of the constant indicates the order of which task/app was selected. Ex.
60      * SPLIT_TASK_SHORTCUT means primary split app identified by task, secondary is shortcut
61      * SPLIT_SHORTCUT_TASK means primary split app is determined by shortcut, secondary is task
62      */
63     companion object {
64         @IntDef(
65             SPLIT_TASK_TASK,
66             SPLIT_TASK_PENDINGINTENT,
67             SPLIT_TASK_SHORTCUT,
68             SPLIT_PENDINGINTENT_TASK,
69             SPLIT_PENDINGINTENT_PENDINGINTENT,
70             SPLIT_SHORTCUT_TASK,
71             SPLIT_SINGLE_TASK_FULLSCREEN,
72             SPLIT_SINGLE_INTENT_FULLSCREEN,
73             SPLIT_SINGLE_SHORTCUT_FULLSCREEN
74         )
75         @Retention(AnnotationRetention.SOURCE)
76         annotation class SplitLaunchType
77 
78         const val SPLIT_TASK_TASK = 0
79         const val SPLIT_TASK_PENDINGINTENT = 1
80         const val SPLIT_TASK_SHORTCUT = 2
81         const val SPLIT_PENDINGINTENT_TASK = 3
82         const val SPLIT_SHORTCUT_TASK = 4
83         const val SPLIT_PENDINGINTENT_PENDINGINTENT = 5
84 
85         // Non-split edge case of launching the initial selected task as a fullscreen task
86         const val SPLIT_SINGLE_TASK_FULLSCREEN = 6
87         const val SPLIT_SINGLE_INTENT_FULLSCREEN = 7
88         const val SPLIT_SINGLE_SHORTCUT_FULLSCREEN = 8
89     }
90 
91     @StagePosition private var initialStagePosition: Int = STAGE_POSITION_UNDEFINED
92     private var itemInfo: ItemInfo? = null
93     private var secondItemInfo: ItemInfo? = null
94     private var splitEvent: EventEnum? = null
95 
96     private var initialTaskId: Int = INVALID_TASK_ID
97     private var secondTaskId: Int = INVALID_TASK_ID
98     private var initialIntent: Intent? = null
99     private var secondIntent: Intent? = null
100     private var widgetSecondIntent: Intent? = null
101     private var initialUser: UserHandle? = null
102     private var secondUser: UserHandle? = null
103     private var initialPendingIntent: PendingIntent? = null
104     private var secondPendingIntent: PendingIntent? = null
105     private var initialShortcut: ShortcutInfo? = null
106     private var secondShortcut: ShortcutInfo? = null
107 
onDestroynull108     fun onDestroy() {
109         context = null
110     }
111 
112     /**
113      * @param alreadyRunningTask if set to [android.app.ActivityTaskManager.INVALID_TASK_ID]
114      *   then @param intent will be used to launch the initial task
115      * @param intent will be ignored if @param alreadyRunningTask is set
116      */
setInitialTaskSelectnull117     fun setInitialTaskSelect(
118         intent: Intent?,
119         @StagePosition stagePosition: Int,
120         itemInfo: ItemInfo?,
121         splitEvent: EventEnum?,
122         alreadyRunningTask: Int
123     ) {
124         if (alreadyRunningTask != INVALID_TASK_ID) {
125             initialTaskId = alreadyRunningTask
126         } else {
127             initialIntent = intent!!
128             initialUser = itemInfo!!.user
129         }
130         setInitialData(stagePosition, splitEvent, itemInfo)
131     }
132 
133     /**
134      * To be called after first task selected from using a split shortcut from the fullscreen
135      * running app.
136      */
setInitialTaskSelectnull137     fun setInitialTaskSelect(
138         info: RunningTaskInfo,
139         @StagePosition stagePosition: Int,
140         itemInfo: ItemInfo?,
141         splitEvent: EventEnum?
142     ) {
143         initialTaskId = info.taskId
144         setInitialData(stagePosition, splitEvent, itemInfo)
145     }
146 
setInitialDatanull147     private fun setInitialData(
148         @StagePosition stagePosition: Int,
149         event: EventEnum?,
150         item: ItemInfo?
151     ) {
152         itemInfo = item
153         initialStagePosition = stagePosition
154         splitEvent = event
155     }
156 
157     /**
158      * To be called as soon as user selects the second task (even if animations aren't complete)
159      *
160      * @param taskId The second task that will be launched.
161      */
setSecondTasknull162     fun setSecondTask(taskId: Int, itemInfo: ItemInfo) {
163         secondTaskId = taskId
164         secondItemInfo = itemInfo
165     }
166 
167     /**
168      * To be called as soon as user selects the second app (even if animations aren't complete)
169      *
170      * @param intent The second intent that will be launched.
171      * @param user The user of that intent.
172      */
setSecondTasknull173     fun setSecondTask(intent: Intent, user: UserHandle, itemInfo: ItemInfo) {
174         secondIntent = intent
175         secondUser = user
176         secondItemInfo = itemInfo
177     }
178 
179     /**
180      * To be called as soon as user selects the second app (even if animations aren't complete) Sets
181      * [secondUser] from that of the pendingIntent
182      *
183      * @param pendingIntent The second PendingIntent that will be launched.
184      */
setSecondTasknull185     fun setSecondTask(pendingIntent: PendingIntent, itemInfo: ItemInfo) {
186         secondPendingIntent = pendingIntent
187         secondUser = pendingIntent.creatorUserHandle
188         secondItemInfo = itemInfo
189     }
190 
191     /**
192      * Similar to [setSecondTask] except this is to be called for widgets which can pass through an
193      * extra intent from their RemoteResponse. See
194      * [android.widget.RemoteViews.RemoteResponse.getLaunchOptions].first
195      */
setSecondWidgetnull196     fun setSecondWidget(pendingIntent: PendingIntent, widgetIntent: Intent?, itemInfo: ItemInfo) {
197         setSecondTask(pendingIntent, itemInfo)
198         widgetSecondIntent = widgetIntent
199     }
200 
getShortcutInfonull201     private fun getShortcutInfo(intent: Intent?, user: UserHandle?): ShortcutInfo? {
202         val intentPackage = intent?.getPackage() ?: return null
203         val shortcutId = intent.getStringExtra(ShortcutKey.EXTRA_SHORTCUT_ID) ?: return null
204         try {
205             val context: Context =
206                 if (user != null) {
207                     context!!.createPackageContextAsUser(intentPackage, 0 /* flags */, user)
208                 } else {
209                     context!!.createPackageContext(intentPackage, 0 /* *flags */)
210                 }
211             return ShortcutInfo.Builder(context, shortcutId).build()
212         } catch (e: PackageManager.NameNotFoundException) {
213             Log.w(TAG, "Failed to create a ShortcutInfo for " + intent.getPackage())
214         }
215         return null
216     }
217 
218     /** Converts intents to pendingIntents, associating the [user] with the intent if provided */
getPendingIntentnull219     private fun getPendingIntent(intent: Intent?, user: UserHandle?): PendingIntent? {
220         if (intent != initialIntent && intent != secondIntent) {
221             throw IllegalStateException("Invalid intent to convert to PendingIntent")
222         }
223 
224         return if (intent == null) {
225             null
226         } else if (user != null) {
227             PendingIntent.getActivityAsUser(
228                 context,
229                 0,
230                 intent,
231                 PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT,
232                 null /* options */,
233                 user
234             )
235         } else {
236             PendingIntent.getActivity(
237                 context,
238                 0,
239                 intent,
240                 PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
241             )
242         }
243     }
244 
245     /**
246      * @return [SplitLaunchData] with the necessary fields populated as determined by
247      *   [SplitLaunchData.splitLaunchType]. This is to be used for launching splitscreen
248      */
getSplitLaunchDatanull249     fun getSplitLaunchData(): SplitLaunchData {
250         // Convert all intents to shortcut infos to see if determine if we launch shortcut or intent
251         convertIntentsToFinalTypes()
252         val splitLaunchType = getSplitLaunchType()
253         if (splitLaunchType == SPLIT_TASK_PENDINGINTENT || splitLaunchType == SPLIT_TASK_SHORTCUT) {
254             // need to get opposite stage position
255             initialStagePosition = getOppositeStagePosition(initialStagePosition)
256         }
257 
258         return generateSplitLaunchData(splitLaunchType)
259     }
260 
261     /**
262      * @return [SplitLaunchData] with the necessary fields populated as determined by
263      *   [SplitLaunchData.splitLaunchType]. This is to be used for launching an initially selected
264      *   split task in fullscreen
265      */
getFullscreenLaunchDatanull266     fun getFullscreenLaunchData(): SplitLaunchData {
267         // Convert all intents to shortcut infos to determine if we launch shortcut or intent
268         convertIntentsToFinalTypes()
269         val splitLaunchType = getFullscreenLaunchType()
270 
271         return generateSplitLaunchData(splitLaunchType)
272     }
273 
generateSplitLaunchDatanull274     private fun generateSplitLaunchData(@SplitLaunchType splitLaunchType: Int): SplitLaunchData {
275         return SplitLaunchData(
276             splitLaunchType,
277             initialTaskId,
278             secondTaskId,
279             initialPendingIntent,
280             secondPendingIntent,
281             widgetSecondIntent,
282             initialUser?.identifier ?: -1,
283             secondUser?.identifier ?: -1,
284             initialShortcut,
285             secondShortcut,
286             itemInfo,
287             splitEvent,
288             initialStagePosition
289         )
290     }
291 
292     /**
293      * Converts our [initialIntent] and [secondIntent] into shortcuts and pendingIntents, if
294      * possible.
295      *
296      * Note that both [initialIntent] and [secondIntent] will be nullified on method return
297      *
298      * One caveat is that if [secondPendingIntent] is set, we will use that and *not* attempt to
299      * convert [secondIntent]. This also leaves [widgetSecondIntent] untouched.
300      */
convertIntentsToFinalTypesnull301     private fun convertIntentsToFinalTypes() {
302         initialShortcut = getShortcutInfo(initialIntent, initialUser)
303         initialPendingIntent = getPendingIntent(initialIntent, initialUser)
304         initialIntent = null
305 
306         // Only one of the two is currently allowed (secondPendingIntent directly set for widgets)
307         if (secondIntent != null && secondPendingIntent != null) {
308             throw IllegalStateException("Both secondIntent and secondPendingIntent non-null")
309         }
310         // If secondPendingIntent already set, no need to convert. Prioritize using that
311         if (secondPendingIntent != null) {
312             secondIntent = null
313             return
314         }
315 
316         secondShortcut = getShortcutInfo(secondIntent, secondUser)
317         secondPendingIntent = getPendingIntent(secondIntent, secondUser)
318         secondIntent = null
319     }
320 
321     /**
322      * Only valid data fields at this point should be tasks, shortcuts, or pendingIntents Intents
323      * need to be converted in [convertIntentsToFinalTypes] prior to calling this method
324      */
325     @VisibleForTesting
326     @SplitLaunchType
getSplitLaunchTypenull327     fun getSplitLaunchType(): Int {
328         if (initialIntent != null || secondIntent != null) {
329             throw IllegalStateException("Intents need to be converted")
330         }
331 
332         // Prioritize task launches first
333         if (initialTaskId != INVALID_TASK_ID) {
334             if (secondTaskId != INVALID_TASK_ID) {
335                 return SPLIT_TASK_TASK
336             }
337             if (secondShortcut != null) {
338                 return SPLIT_TASK_SHORTCUT
339             }
340             if (secondPendingIntent != null) {
341                 return SPLIT_TASK_PENDINGINTENT
342             }
343         }
344 
345         if (secondTaskId != INVALID_TASK_ID) {
346             if (initialShortcut != null) {
347                 return SPLIT_SHORTCUT_TASK
348             }
349             if (initialPendingIntent != null) {
350                 return SPLIT_PENDINGINTENT_TASK
351             }
352         }
353 
354         // All task+shortcut combinations are handled above, only launch left is with multiple
355         // intents (and respective shortcut infos, if necessary)
356         if (initialPendingIntent != null && secondPendingIntent != null) {
357             return SPLIT_PENDINGINTENT_PENDINGINTENT
358         }
359         throw IllegalStateException("Unidentified split launch type")
360     }
361 
362     @SplitLaunchType
getFullscreenLaunchTypenull363     private fun getFullscreenLaunchType(): Int {
364         if (initialTaskId != INVALID_TASK_ID) {
365             return SPLIT_SINGLE_TASK_FULLSCREEN
366         }
367 
368         if (initialShortcut != null) {
369             return SPLIT_SINGLE_SHORTCUT_FULLSCREEN
370         }
371 
372         if (initialPendingIntent != null) {
373             return SPLIT_SINGLE_INTENT_FULLSCREEN
374         }
375         throw IllegalStateException("Unidentified fullscreen launch type")
376     }
377 
378     data class SplitLaunchData(
379         @SplitLaunchType val splitLaunchType: Int,
380         var initialTaskId: Int = INVALID_TASK_ID,
381         var secondTaskId: Int = INVALID_TASK_ID,
382         var initialPendingIntent: PendingIntent? = null,
383         var secondPendingIntent: PendingIntent? = null,
384         var widgetSecondIntent: Intent? = null,
385         var initialUserId: Int = -1,
386         var secondUserId: Int = -1,
387         var initialShortcut: ShortcutInfo? = null,
388         var secondShortcut: ShortcutInfo? = null,
389         var itemInfo: ItemInfo? = null,
390         var splitEvent: EventEnum? = null,
391         val initialStagePosition: Int = STAGE_POSITION_UNDEFINED
392     )
393 
394     /**
395      * @return `true` if first task has been selected and waiting for the second task to be chosen
396      */
isSplitSelectActivenull397     fun isSplitSelectActive(): Boolean {
398         return isInitialTaskIntentSet() && !isSecondTaskIntentSet()
399     }
400 
401     /**
402      * @return `true` if the first and second task have been chosen and split is waiting to be
403      *   launched
404      */
isBothSplitAppsConfirmednull405     fun isBothSplitAppsConfirmed(): Boolean {
406         return isInitialTaskIntentSet() && isSecondTaskIntentSet()
407     }
408 
isInitialTaskIntentSetnull409     private fun isInitialTaskIntentSet(): Boolean {
410         return initialTaskId != INVALID_TASK_ID ||
411             initialIntent != null ||
412             initialPendingIntent != null
413     }
414 
getInitialTaskIdnull415     fun getInitialTaskId(): Int {
416         return initialTaskId
417     }
418 
getSecondTaskIdnull419     fun getSecondTaskId(): Int {
420         return secondTaskId
421     }
422 
getSplitEventnull423     fun getSplitEvent(): EventEnum? {
424         return splitEvent
425     }
426 
getInitialStagePositionnull427     fun getInitialStagePosition(): Int {
428         return initialStagePosition
429     }
430 
getItemInfonull431     fun getItemInfo(): ItemInfo? {
432         return itemInfo
433     }
434 
getSecondItemInfonull435     fun getSecondItemInfo(): ItemInfo? {
436         return secondItemInfo
437     }
438 
isSecondTaskIntentSetnull439     private fun isSecondTaskIntentSet(): Boolean {
440         return secondTaskId != INVALID_TASK_ID ||
441             secondIntent != null ||
442             secondPendingIntent != null
443     }
444 
resetStatenull445     fun resetState() {
446         initialStagePosition = STAGE_POSITION_UNDEFINED
447         initialTaskId = INVALID_TASK_ID
448         secondTaskId = INVALID_TASK_ID
449         initialUser = null
450         secondUser = null
451         initialIntent = null
452         secondIntent = null
453         initialPendingIntent = null
454         secondPendingIntent = null
455         itemInfo = null
456         splitEvent = null
457         initialShortcut = null
458         secondShortcut = null
459     }
460 
dumpnull461     fun dump(prefix: String, writer: PrintWriter) {
462         writer.println("$prefix SplitSelectDataHolder")
463         writer.println("$prefix\tinitialStagePosition= $initialStagePosition")
464         writer.println("$prefix\tinitialTaskId= $initialTaskId")
465         writer.println("$prefix\tsecondTaskId= $secondTaskId")
466         writer.println("$prefix\tinitialUser= $initialUser")
467         writer.println("$prefix\tsecondUser= $secondUser")
468         writer.println("$prefix\tinitialIntent= $initialIntent")
469         writer.println("$prefix\tsecondIntent= $secondIntent")
470         writer.println("$prefix\tsecondPendingIntent= $secondPendingIntent")
471         writer.println("$prefix\titemInfo= $itemInfo")
472         writer.println("$prefix\tsplitEvent= $splitEvent")
473         writer.println("$prefix\tinitialShortcut= $initialShortcut")
474         writer.println("$prefix\tsecondShortcut= $secondShortcut")
475     }
476 }
477