• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }