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 }