Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 90dce8b8 authored by Vinit Nayak's avatar Vinit Nayak Committed by Android (Google) Code Review
Browse files

Merge "Refactor handling data launching split screen to separate class" into udc-dev

parents 5f456635 2a014d4c
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -1317,5 +1317,8 @@ public class QuickstepLauncher extends Launcher {
        writer.println("\nQuickstepLauncher:");
        writer.println(prefix + "\tmOrientationState: " + (recentsView == null ? "recentsNull" :
                recentsView.getPagedViewOrientedState()));
        if (recentsView != null) {
            recentsView.getSplitSelectController().dump(prefix, writer);
        }
    }
}
+364 −0
Original line number Diff line number Diff line
/*
 *  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)
        @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
    }


    @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? {
        if (intent?.getPackage() == null) {
            return null
        }
        val shortcutId = intent.getStringExtra(ShortcutKey.EXTRA_SHORTCUT_ID)
                ?: return null
        try {
            val context: Context = context.createPackageContextAsUser(
                    intent.getPackage(), 0 /* flags */, user)
            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]
     */
    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 SplitLaunchData(
                splitLaunchType,
                initialTaskId,
                secondTaskId,
                initialPendingIntent,
                secondPendingIntent,
                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, 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")
    }

    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 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")
    }
}
 No newline at end of file
+167 −4

File changed.

Preview size limit exceeded, changes collapsed.

+6 −1
Original line number Diff line number Diff line
@@ -407,7 +407,12 @@ public final class FeatureFlags {
            "USE_SEARCH_REQUEST_TIMEOUT_OVERRIDES", DISABLED,
            "Use local overrides for search request timeout");

    // TODO(Block 31): Empty block
    // TODO(Block 31)
    public static final BooleanFlag ENABLE_SPLIT_LAUNCH_DATA_REFACTOR = getDebugFlag(279494325,
            "ENABLE_SPLIT_LAUNCH_DATA_REFACTOR", DISABLED,
            "Use refactored split launching code path");

    // TODO(Block 32): Empty block

    public static class BooleanFlag {