1 /* 2 * Copyright (C) 2024 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 package com.android.wm.shell.splitscreen 18 19 import android.content.Context 20 import com.android.internal.protolog.ProtoLog 21 import com.android.launcher3.icons.IconProvider 22 import com.android.wm.shell.ShellTaskOrganizer 23 import com.android.wm.shell.common.SyncTransactionQueue 24 import com.android.wm.shell.protolog.ShellProtoLogGroup 25 import com.android.wm.shell.shared.split.SplitScreenConstants 26 import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_NONE 27 import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_0 28 import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_1 29 import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_2 30 import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT 31 import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT 32 import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED 33 import com.android.wm.shell.shared.split.SplitScreenConstants.SnapPosition 34 import com.android.wm.shell.shared.split.SplitScreenConstants.SplitIndex 35 import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition 36 import com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_A 37 import com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_B 38 import com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_C 39 import com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString 40 import com.android.wm.shell.windowdecor.WindowDecorViewModel 41 import java.util.Collections 42 import java.util.Optional 43 44 /** 45 * Responsible for creating [StageTaskListener]s and maintaining their ordering on screen. 46 * Must be notified whenever stages positions change via swapping or starting/ending tasks 47 */ 48 class StageOrderOperator ( 49 context: Context, 50 taskOrganizer: ShellTaskOrganizer, 51 displayId: Int, 52 stageCallbacks: StageTaskListener.StageListenerCallbacks, 53 syncQueue: SyncTransactionQueue, 54 iconProvider: IconProvider, 55 windowDecorViewModel: Optional<WindowDecorViewModel> 56 ) { 57 58 private val MAX_STAGES = 3 59 /** 60 * This somewhat acts as a replacement to stageTypes in the intermediary, so we want to start 61 * it after the @StageType constant values just to be safe and avoid potentially subtle bugs. 62 */ 63 private var stageIds = listOf(STAGE_TYPE_A, STAGE_TYPE_B, STAGE_TYPE_C) 64 65 /** 66 * Active Stages, this list represent the current, ordered list of stages that are 67 * currently visible to the user. This map should be empty if the user is currently 68 * not in split screen. Note that this is different than if split screen is visible, which 69 * is determined by [StageListenerImpl.mVisible]. 70 * Split stages can be active and in the background 71 */ 72 val activeStages = mutableListOf<StageTaskListener>() 73 val allStages = mutableListOf<StageTaskListener>() 74 var isActive: Boolean = false 75 var isVisible: Boolean = false 76 @SnapPosition private var currentLayout: Int = SNAP_TO_NONE 77 78 init { 79 for(i in 0 until MAX_STAGES) { 80 allStages.add(StageTaskListener(context, 81 taskOrganizer, 82 displayId, 83 stageCallbacks, 84 syncQueue, 85 iconProvider, 86 windowDecorViewModel, 87 stageIds[i]) 88 ) 89 } 90 } 91 92 /** 93 * Updates internal state to keep record of "active" stages. Note that this does NOT call 94 * [StageTaskListener.activate] on the stages. 95 */ onEnteringSplitnull96 fun onEnteringSplit(@SnapPosition goingToLayout: Int) { 97 if (goingToLayout == currentLayout) { 98 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, 99 "Entering Split requested same layout split is in: %d", goingToLayout) 100 return 101 } 102 val freeStages: List<StageTaskListener> = 103 allStages.filterNot { activeStages.contains(it) } 104 when(goingToLayout) { 105 SplitScreenConstants.SNAP_TO_2_50_50, 106 SplitScreenConstants.SNAP_TO_2_33_66, 107 SplitScreenConstants.SNAP_TO_2_66_33 -> { 108 if (activeStages.size < 2) { 109 // take from allStages and add into activeStages 110 for (i in 0 until (2 - activeStages.size)) { 111 val stage = freeStages[i] 112 activeStages.add(stage) 113 } 114 } 115 } 116 } 117 ProtoLog.d( 118 ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, 119 "Activated stages: %d ids=%s", 120 activeStages.size, 121 activeStages.joinToString(",") { stageTypeToString(it.id) } 122 ) 123 isActive = true 124 } 125 onExitingSplitnull126 fun onExitingSplit() { 127 activeStages.clear() 128 isActive = false 129 } 130 131 /** 132 * Given a legacy [SplitPosition] returns one of the stages from the actives stages. 133 * If there are no active stages and [checkAllStagesIfNotActive] is not true, then will return 134 * null 135 */ getStageForLegacyPositionnull136 fun getStageForLegacyPosition(@SplitPosition position: Int, 137 checkAllStagesIfNotActive : Boolean = false) : 138 StageTaskListener? { 139 if (activeStages.size != 2 && !checkAllStagesIfNotActive) { 140 return null 141 } 142 val listToCheck = if (activeStages.isEmpty() and checkAllStagesIfNotActive) 143 allStages else 144 activeStages 145 if (position == SPLIT_POSITION_TOP_OR_LEFT) { 146 return listToCheck[0] 147 } else if (position == SPLIT_POSITION_BOTTOM_OR_RIGHT) { 148 return listToCheck[1] 149 } else { 150 throw IllegalArgumentException("No stage for invalid position") 151 } 152 } 153 154 /** 155 * This will swap the stages for the two stages on either side of the given divider. 156 * Note: This will keep [activeStages] and [allStages] in sync by swapping both of them 157 * If there are no [activeStages] then this will be a no-op. 158 * 159 * TODO(b/379984874): Take in a divider identifier to determine which array indices to swap 160 */ onDoubleTappedDividernull161 fun onDoubleTappedDivider() { 162 if (activeStages.isEmpty()) { 163 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, 164 "Stages not active, ignoring swap request") 165 return 166 } 167 168 Collections.swap(activeStages, 0, 1) 169 Collections.swap(allStages, 0, 1) 170 } 171 172 /** 173 * Returns a legacy split position for the given stage. If no stages are active then this will 174 * return [SPLIT_POSITION_UNDEFINED] 175 */ 176 @SplitPosition getLegacyPositionForStagenull177 fun getLegacyPositionForStage(stage: StageTaskListener) : Int { 178 if (allStages[0] == stage) { 179 return SPLIT_POSITION_TOP_OR_LEFT 180 } else if (allStages[1] == stage) { 181 return SPLIT_POSITION_BOTTOM_OR_RIGHT 182 } else { 183 return SPLIT_POSITION_UNDEFINED 184 } 185 } 186 187 /** 188 * Returns the stageId from a given splitIndex. This will default to checking from all stages if 189 * [isActive] is false, otherwise will only check active stages. 190 */ getStageForIndexnull191 fun getStageForIndex(@SplitIndex splitIndex: Int) : StageTaskListener { 192 // Probably should do a check for index to be w/in the bounds of the current split layout 193 // that we're currently in 194 val listToCheck = if (isActive) activeStages else allStages 195 if (splitIndex == SPLIT_INDEX_0) { 196 return listToCheck[0] 197 } else if (splitIndex == SPLIT_INDEX_1) { 198 return listToCheck[1] 199 } else if (splitIndex == SPLIT_INDEX_2) { 200 return listToCheck[2] 201 } else { 202 // Though I guess what if we're adding to the end? Maybe that indexing needs to be 203 // resolved elsewhere 204 throw IllegalStateException("No stage for the given splitIndex") 205 } 206 } 207 }