c9eda41c31
class.getSimpleName() causes issues with obfuscation. Bug: 332171918 Test: Manual Flag: NA Change-Id: I5119a7ba0e4e27d8745c94df67881934fed282a4
477 lines
18 KiB
Kotlin
477 lines
18 KiB
Kotlin
/*
|
|
* Copyright (C) 2023 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
|
|
package com.android.quickstep.util
|
|
|
|
import android.annotation.IntDef
|
|
import android.app.ActivityManager.RunningTaskInfo
|
|
import android.app.ActivityTaskManager.INVALID_TASK_ID
|
|
import android.app.PendingIntent
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.content.pm.PackageManager
|
|
import android.content.pm.ShortcutInfo
|
|
import android.os.UserHandle
|
|
import android.util.Log
|
|
import com.android.internal.annotations.VisibleForTesting
|
|
import com.android.launcher3.logging.StatsLogManager.EventEnum
|
|
import com.android.launcher3.model.data.ItemInfo
|
|
import com.android.launcher3.shortcuts.ShortcutKey
|
|
import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
|
|
import com.android.launcher3.util.SplitConfigurationOptions.StagePosition
|
|
import com.android.launcher3.util.SplitConfigurationOptions.getOppositeStagePosition
|
|
import com.android.quickstep.util.SplitSelectDataHolder.Companion.SplitLaunchType
|
|
import java.io.PrintWriter
|
|
|
|
/**
|
|
* Holds/transforms/signs/seals/delivers information for the transient state of the user selecting a
|
|
* first app to start split with and then choosing a second app. This class DOES NOT associate
|
|
* itself with drag-and-drop split screen starts because they come from the bad part of town.
|
|
*
|
|
* After setting the correct fields for initial/second.* variables, this converts them into the
|
|
* correct [PendingIntent] and [ShortcutInfo] objects where applicable and sends the necessary data
|
|
* back via [getSplitLaunchData]. Note: there should be only one "initial" field and one "second"
|
|
* field set, with the rest remaining null. (Exception: [Intent] and [UserHandle] are always passed
|
|
* in together as a set, and are converted to a single [PendingIntent] or
|
|
* [ShortcutInfo]+[PendingIntent] before launch.)
|
|
*
|
|
* [SplitLaunchType] indicates the type of tasks/apps/intents being launched given the provided
|
|
* state
|
|
*/
|
|
class SplitSelectDataHolder(var context: Context?) {
|
|
val TAG = SplitSelectDataHolder::class.simpleName
|
|
|
|
/**
|
|
* Order of the constant indicates the order of which task/app was selected. Ex.
|
|
* SPLIT_TASK_SHORTCUT means primary split app identified by task, secondary is shortcut
|
|
* SPLIT_SHORTCUT_TASK means primary split app is determined by shortcut, secondary is task
|
|
*/
|
|
companion object {
|
|
@IntDef(
|
|
SPLIT_TASK_TASK,
|
|
SPLIT_TASK_PENDINGINTENT,
|
|
SPLIT_TASK_SHORTCUT,
|
|
SPLIT_PENDINGINTENT_TASK,
|
|
SPLIT_PENDINGINTENT_PENDINGINTENT,
|
|
SPLIT_SHORTCUT_TASK,
|
|
SPLIT_SINGLE_TASK_FULLSCREEN,
|
|
SPLIT_SINGLE_INTENT_FULLSCREEN,
|
|
SPLIT_SINGLE_SHORTCUT_FULLSCREEN
|
|
)
|
|
@Retention(AnnotationRetention.SOURCE)
|
|
annotation class SplitLaunchType
|
|
|
|
const val SPLIT_TASK_TASK = 0
|
|
const val SPLIT_TASK_PENDINGINTENT = 1
|
|
const val SPLIT_TASK_SHORTCUT = 2
|
|
const val SPLIT_PENDINGINTENT_TASK = 3
|
|
const val SPLIT_SHORTCUT_TASK = 4
|
|
const val SPLIT_PENDINGINTENT_PENDINGINTENT = 5
|
|
|
|
// Non-split edge case of launching the initial selected task as a fullscreen task
|
|
const val SPLIT_SINGLE_TASK_FULLSCREEN = 6
|
|
const val SPLIT_SINGLE_INTENT_FULLSCREEN = 7
|
|
const val SPLIT_SINGLE_SHORTCUT_FULLSCREEN = 8
|
|
}
|
|
|
|
@StagePosition private var initialStagePosition: Int = STAGE_POSITION_UNDEFINED
|
|
private var itemInfo: ItemInfo? = null
|
|
private var secondItemInfo: ItemInfo? = null
|
|
private var splitEvent: EventEnum? = null
|
|
|
|
private var initialTaskId: Int = INVALID_TASK_ID
|
|
private var secondTaskId: Int = INVALID_TASK_ID
|
|
private var initialIntent: Intent? = null
|
|
private var secondIntent: Intent? = null
|
|
private var widgetSecondIntent: Intent? = null
|
|
private var initialUser: UserHandle? = null
|
|
private var secondUser: UserHandle? = null
|
|
private var initialPendingIntent: PendingIntent? = null
|
|
private var secondPendingIntent: PendingIntent? = null
|
|
private var initialShortcut: ShortcutInfo? = null
|
|
private var secondShortcut: ShortcutInfo? = null
|
|
|
|
fun onDestroy() {
|
|
context = null
|
|
}
|
|
|
|
/**
|
|
* @param alreadyRunningTask if set to [android.app.ActivityTaskManager.INVALID_TASK_ID]
|
|
* then @param intent will be used to launch the initial task
|
|
* @param intent will be ignored if @param alreadyRunningTask is set
|
|
*/
|
|
fun setInitialTaskSelect(
|
|
intent: Intent?,
|
|
@StagePosition stagePosition: Int,
|
|
itemInfo: ItemInfo?,
|
|
splitEvent: EventEnum?,
|
|
alreadyRunningTask: Int
|
|
) {
|
|
if (alreadyRunningTask != INVALID_TASK_ID) {
|
|
initialTaskId = alreadyRunningTask
|
|
} else {
|
|
initialIntent = intent!!
|
|
initialUser = itemInfo!!.user
|
|
}
|
|
setInitialData(stagePosition, splitEvent, itemInfo)
|
|
}
|
|
|
|
/**
|
|
* To be called after first task selected from using a split shortcut from the fullscreen
|
|
* running app.
|
|
*/
|
|
fun setInitialTaskSelect(
|
|
info: RunningTaskInfo,
|
|
@StagePosition stagePosition: Int,
|
|
itemInfo: ItemInfo?,
|
|
splitEvent: EventEnum?
|
|
) {
|
|
initialTaskId = info.taskId
|
|
setInitialData(stagePosition, splitEvent, itemInfo)
|
|
}
|
|
|
|
private fun setInitialData(
|
|
@StagePosition stagePosition: Int,
|
|
event: EventEnum?,
|
|
item: ItemInfo?
|
|
) {
|
|
itemInfo = item
|
|
initialStagePosition = stagePosition
|
|
splitEvent = event
|
|
}
|
|
|
|
/**
|
|
* To be called as soon as user selects the second task (even if animations aren't complete)
|
|
*
|
|
* @param taskId The second task that will be launched.
|
|
*/
|
|
fun setSecondTask(taskId: Int, itemInfo: ItemInfo) {
|
|
secondTaskId = taskId
|
|
secondItemInfo = itemInfo
|
|
}
|
|
|
|
/**
|
|
* To be called as soon as user selects the second app (even if animations aren't complete)
|
|
*
|
|
* @param intent The second intent that will be launched.
|
|
* @param user The user of that intent.
|
|
*/
|
|
fun setSecondTask(intent: Intent, user: UserHandle, itemInfo: ItemInfo) {
|
|
secondIntent = intent
|
|
secondUser = user
|
|
secondItemInfo = itemInfo
|
|
}
|
|
|
|
/**
|
|
* To be called as soon as user selects the second app (even if animations aren't complete) Sets
|
|
* [secondUser] from that of the pendingIntent
|
|
*
|
|
* @param pendingIntent The second PendingIntent that will be launched.
|
|
*/
|
|
fun setSecondTask(pendingIntent: PendingIntent, itemInfo: ItemInfo) {
|
|
secondPendingIntent = pendingIntent
|
|
secondUser = pendingIntent.creatorUserHandle
|
|
secondItemInfo = itemInfo
|
|
}
|
|
|
|
/**
|
|
* Similar to [setSecondTask] except this is to be called for widgets which can pass through an
|
|
* extra intent from their RemoteResponse. See
|
|
* [android.widget.RemoteViews.RemoteResponse.getLaunchOptions].first
|
|
*/
|
|
fun setSecondWidget(pendingIntent: PendingIntent, widgetIntent: Intent?, itemInfo: ItemInfo) {
|
|
setSecondTask(pendingIntent, itemInfo)
|
|
widgetSecondIntent = widgetIntent
|
|
}
|
|
|
|
private fun getShortcutInfo(intent: Intent?, user: UserHandle?): ShortcutInfo? {
|
|
val intentPackage = intent?.getPackage() ?: return null
|
|
val shortcutId = intent.getStringExtra(ShortcutKey.EXTRA_SHORTCUT_ID) ?: return null
|
|
try {
|
|
val context: Context =
|
|
if (user != null) {
|
|
context!!.createPackageContextAsUser(intentPackage, 0 /* flags */, user)
|
|
} else {
|
|
context!!.createPackageContext(intentPackage, 0 /* *flags */)
|
|
}
|
|
return ShortcutInfo.Builder(context, shortcutId).build()
|
|
} catch (e: PackageManager.NameNotFoundException) {
|
|
Log.w(TAG, "Failed to create a ShortcutInfo for " + intent.getPackage())
|
|
}
|
|
return null
|
|
}
|
|
|
|
/** Converts intents to pendingIntents, associating the [user] with the intent if provided */
|
|
private fun getPendingIntent(intent: Intent?, user: UserHandle?): PendingIntent? {
|
|
if (intent != initialIntent && intent != secondIntent) {
|
|
throw IllegalStateException("Invalid intent to convert to PendingIntent")
|
|
}
|
|
|
|
return if (intent == null) {
|
|
null
|
|
} else if (user != null) {
|
|
PendingIntent.getActivityAsUser(
|
|
context,
|
|
0,
|
|
intent,
|
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT,
|
|
null /* options */,
|
|
user
|
|
)
|
|
} else {
|
|
PendingIntent.getActivity(
|
|
context,
|
|
0,
|
|
intent,
|
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return [SplitLaunchData] with the necessary fields populated as determined by
|
|
* [SplitLaunchData.splitLaunchType]. This is to be used for launching splitscreen
|
|
*/
|
|
fun getSplitLaunchData(): SplitLaunchData {
|
|
// Convert all intents to shortcut infos to see if determine if we launch shortcut or intent
|
|
convertIntentsToFinalTypes()
|
|
val splitLaunchType = getSplitLaunchType()
|
|
if (splitLaunchType == SPLIT_TASK_PENDINGINTENT || splitLaunchType == SPLIT_TASK_SHORTCUT) {
|
|
// need to get opposite stage position
|
|
initialStagePosition = getOppositeStagePosition(initialStagePosition)
|
|
}
|
|
|
|
return generateSplitLaunchData(splitLaunchType)
|
|
}
|
|
|
|
/**
|
|
* @return [SplitLaunchData] with the necessary fields populated as determined by
|
|
* [SplitLaunchData.splitLaunchType]. This is to be used for launching an initially selected
|
|
* split task in fullscreen
|
|
*/
|
|
fun getFullscreenLaunchData(): SplitLaunchData {
|
|
// Convert all intents to shortcut infos to determine if we launch shortcut or intent
|
|
convertIntentsToFinalTypes()
|
|
val splitLaunchType = getFullscreenLaunchType()
|
|
|
|
return generateSplitLaunchData(splitLaunchType)
|
|
}
|
|
|
|
private fun generateSplitLaunchData(@SplitLaunchType splitLaunchType: Int): SplitLaunchData {
|
|
return SplitLaunchData(
|
|
splitLaunchType,
|
|
initialTaskId,
|
|
secondTaskId,
|
|
initialPendingIntent,
|
|
secondPendingIntent,
|
|
widgetSecondIntent,
|
|
initialUser?.identifier ?: -1,
|
|
secondUser?.identifier ?: -1,
|
|
initialShortcut,
|
|
secondShortcut,
|
|
itemInfo,
|
|
splitEvent,
|
|
initialStagePosition
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Converts our [initialIntent] and [secondIntent] into shortcuts and pendingIntents, if
|
|
* possible.
|
|
*
|
|
* Note that both [initialIntent] and [secondIntent] will be nullified on method return
|
|
*
|
|
* One caveat is that if [secondPendingIntent] is set, we will use that and *not* attempt to
|
|
* convert [secondIntent]. This also leaves [widgetSecondIntent] untouched.
|
|
*/
|
|
private fun convertIntentsToFinalTypes() {
|
|
initialShortcut = getShortcutInfo(initialIntent, initialUser)
|
|
initialPendingIntent = getPendingIntent(initialIntent, initialUser)
|
|
initialIntent = null
|
|
|
|
// Only one of the two is currently allowed (secondPendingIntent directly set for widgets)
|
|
if (secondIntent != null && secondPendingIntent != null) {
|
|
throw IllegalStateException("Both secondIntent and secondPendingIntent non-null")
|
|
}
|
|
// If secondPendingIntent already set, no need to convert. Prioritize using that
|
|
if (secondPendingIntent != null) {
|
|
secondIntent = null
|
|
return
|
|
}
|
|
|
|
secondShortcut = getShortcutInfo(secondIntent, secondUser)
|
|
secondPendingIntent = getPendingIntent(secondIntent, secondUser)
|
|
secondIntent = null
|
|
}
|
|
|
|
/**
|
|
* Only valid data fields at this point should be tasks, shortcuts, or pendingIntents Intents
|
|
* need to be converted in [convertIntentsToFinalTypes] prior to calling this method
|
|
*/
|
|
@VisibleForTesting
|
|
@SplitLaunchType
|
|
fun getSplitLaunchType(): Int {
|
|
if (initialIntent != null || secondIntent != null) {
|
|
throw IllegalStateException("Intents need to be converted")
|
|
}
|
|
|
|
// Prioritize task launches first
|
|
if (initialTaskId != INVALID_TASK_ID) {
|
|
if (secondTaskId != INVALID_TASK_ID) {
|
|
return SPLIT_TASK_TASK
|
|
}
|
|
if (secondShortcut != null) {
|
|
return SPLIT_TASK_SHORTCUT
|
|
}
|
|
if (secondPendingIntent != null) {
|
|
return SPLIT_TASK_PENDINGINTENT
|
|
}
|
|
}
|
|
|
|
if (secondTaskId != INVALID_TASK_ID) {
|
|
if (initialShortcut != null) {
|
|
return SPLIT_SHORTCUT_TASK
|
|
}
|
|
if (initialPendingIntent != null) {
|
|
return SPLIT_PENDINGINTENT_TASK
|
|
}
|
|
}
|
|
|
|
// All task+shortcut combinations are handled above, only launch left is with multiple
|
|
// intents (and respective shortcut infos, if necessary)
|
|
if (initialPendingIntent != null && secondPendingIntent != null) {
|
|
return SPLIT_PENDINGINTENT_PENDINGINTENT
|
|
}
|
|
throw IllegalStateException("Unidentified split launch type")
|
|
}
|
|
|
|
@SplitLaunchType
|
|
private fun getFullscreenLaunchType(): Int {
|
|
if (initialTaskId != INVALID_TASK_ID) {
|
|
return SPLIT_SINGLE_TASK_FULLSCREEN
|
|
}
|
|
|
|
if (initialShortcut != null) {
|
|
return SPLIT_SINGLE_SHORTCUT_FULLSCREEN
|
|
}
|
|
|
|
if (initialPendingIntent != null) {
|
|
return SPLIT_SINGLE_INTENT_FULLSCREEN
|
|
}
|
|
throw IllegalStateException("Unidentified fullscreen launch type")
|
|
}
|
|
|
|
data class SplitLaunchData(
|
|
@SplitLaunchType val splitLaunchType: Int,
|
|
var initialTaskId: Int = INVALID_TASK_ID,
|
|
var secondTaskId: Int = INVALID_TASK_ID,
|
|
var initialPendingIntent: PendingIntent? = null,
|
|
var secondPendingIntent: PendingIntent? = null,
|
|
var widgetSecondIntent: Intent? = null,
|
|
var initialUserId: Int = -1,
|
|
var secondUserId: Int = -1,
|
|
var initialShortcut: ShortcutInfo? = null,
|
|
var secondShortcut: ShortcutInfo? = null,
|
|
var itemInfo: ItemInfo? = null,
|
|
var splitEvent: EventEnum? = null,
|
|
val initialStagePosition: Int = STAGE_POSITION_UNDEFINED
|
|
)
|
|
|
|
/**
|
|
* @return `true` if first task has been selected and waiting for the second task to be chosen
|
|
*/
|
|
fun isSplitSelectActive(): Boolean {
|
|
return isInitialTaskIntentSet() && !isSecondTaskIntentSet()
|
|
}
|
|
|
|
/**
|
|
* @return `true` if the first and second task have been chosen and split is waiting to be
|
|
* launched
|
|
*/
|
|
fun isBothSplitAppsConfirmed(): Boolean {
|
|
return isInitialTaskIntentSet() && isSecondTaskIntentSet()
|
|
}
|
|
|
|
private fun isInitialTaskIntentSet(): Boolean {
|
|
return initialTaskId != INVALID_TASK_ID ||
|
|
initialIntent != null ||
|
|
initialPendingIntent != null
|
|
}
|
|
|
|
fun getInitialTaskId(): Int {
|
|
return initialTaskId
|
|
}
|
|
|
|
fun getSecondTaskId(): Int {
|
|
return secondTaskId
|
|
}
|
|
|
|
fun getSplitEvent(): EventEnum? {
|
|
return splitEvent
|
|
}
|
|
|
|
fun getInitialStagePosition(): Int {
|
|
return initialStagePosition
|
|
}
|
|
|
|
fun getItemInfo(): ItemInfo? {
|
|
return itemInfo
|
|
}
|
|
|
|
fun getSecondItemInfo(): ItemInfo? {
|
|
return secondItemInfo
|
|
}
|
|
|
|
private fun isSecondTaskIntentSet(): Boolean {
|
|
return secondTaskId != INVALID_TASK_ID ||
|
|
secondIntent != null ||
|
|
secondPendingIntent != null
|
|
}
|
|
|
|
fun resetState() {
|
|
initialStagePosition = STAGE_POSITION_UNDEFINED
|
|
initialTaskId = INVALID_TASK_ID
|
|
secondTaskId = INVALID_TASK_ID
|
|
initialUser = null
|
|
secondUser = null
|
|
initialIntent = null
|
|
secondIntent = null
|
|
initialPendingIntent = null
|
|
secondPendingIntent = null
|
|
itemInfo = null
|
|
splitEvent = null
|
|
initialShortcut = null
|
|
secondShortcut = null
|
|
}
|
|
|
|
fun dump(prefix: String, writer: PrintWriter) {
|
|
writer.println("$prefix SplitSelectDataHolder")
|
|
writer.println("$prefix\tinitialStagePosition= $initialStagePosition")
|
|
writer.println("$prefix\tinitialTaskId= $initialTaskId")
|
|
writer.println("$prefix\tsecondTaskId= $secondTaskId")
|
|
writer.println("$prefix\tinitialUser= $initialUser")
|
|
writer.println("$prefix\tsecondUser= $secondUser")
|
|
writer.println("$prefix\tinitialIntent= $initialIntent")
|
|
writer.println("$prefix\tsecondIntent= $secondIntent")
|
|
writer.println("$prefix\tsecondPendingIntent= $secondPendingIntent")
|
|
writer.println("$prefix\titemInfo= $itemInfo")
|
|
writer.println("$prefix\tsplitEvent= $splitEvent")
|
|
writer.println("$prefix\tinitialShortcut= $initialShortcut")
|
|
writer.println("$prefix\tsecondShortcut= $secondShortcut")
|
|
}
|
|
}
|