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