Files
Lawnchair/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt
T
Colin Cross e07b293994 Fix kotlin nullable errors in Launcher3
Fix kotlin nullable errors that were exposed by setting the retention
of android.annotation.NonNull and android.annotation.Nullable to
class retention.

Bug: 294110802
Test: builds
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:2608381792389b60ba37c08afcea09dca3c6ff9c)
Merged-In: I26edfec35dca14abe90b08e3c74de0446eda95d2
Change-Id: I26edfec35dca14abe90b08e3c74de0446eda95d2

(cherry picked from commit e892df24ca)
Change-Id: I1099bc97725577ce34ea5b617365eaf6dec3d3be
2024-03-14 14:12:51 -07:00

411 lines
16 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].
* [SplitLaunchType] indicates the type of tasks/apps/intents being launched given the provided
* state
*/
class SplitSelectDataHolder(
val 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 initialTaskId: Int = INVALID_TASK_ID
private var secondTaskId: Int = INVALID_TASK_ID
private var initialUser: UserHandle? = null
private var secondUser: UserHandle? = null
private var initialIntent: Intent? = null
private var secondIntent: Intent? = null
private var secondPendingIntent: PendingIntent? = null
private var itemInfo: ItemInfo? = null
private var splitEvent: EventEnum? = null
private var initialShortcut: ShortcutInfo? = null
private var secondShortcut: ShortcutInfo? = null
private var initialPendingIntent: PendingIntent? = 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) {
secondTaskId = taskId
}
/**
* 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) {
secondIntent = intent
secondUser = user
}
/**
* 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) {
secondPendingIntent = pendingIntent
secondUser = pendingIntent.creatorUserHandle
}
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 see if 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,
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]
*/
private fun convertIntentsToFinalTypes() {
initialShortcut = getShortcutInfo(initialIntent, checkNotNull(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, checkNotNull(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 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
}
fun getInitialTaskId(): Int {
return initialTaskId
}
fun getSecondTaskId(): Int {
return secondTaskId
}
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
secondPendingIntent = null
itemInfo = null
splitEvent = null
initialShortcut = null
secondShortcut = null
}
fun dump(prefix: String, writer: PrintWriter) {
writer.println("$prefix ${javaClass.simpleName}")
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")
}
}