1 /*
2  * Copyright 2021 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.launcher3.util;
18 
19 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_ICON_MENU_SPLIT_LEFT_TOP;
20 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_ICON_MENU_SPLIT_RIGHT_BOTTOM;
21 
22 import static java.lang.annotation.RetentionPolicy.SOURCE;
23 
24 import android.content.Intent;
25 import android.graphics.Rect;
26 import android.graphics.drawable.Drawable;
27 import android.view.View;
28 
29 import androidx.annotation.IntDef;
30 
31 import com.android.launcher3.logging.StatsLogManager;
32 import com.android.launcher3.model.data.ItemInfo;
33 
34 import java.lang.annotation.Retention;
35 
36 public final class SplitConfigurationOptions {
37 
38     ///////////////////////////////////
39     // Taken from
40     // frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
41     /**
42      * Stage position isn't specified normally meaning to use what ever it is currently set to.
43      */
44     public static final int STAGE_POSITION_UNDEFINED = -1;
45     /**
46      * Specifies that a stage is positioned at the top half of the screen if
47      * in portrait mode or at the left half of the screen if in landscape mode.
48      */
49     public static final int STAGE_POSITION_TOP_OR_LEFT = 0;
50 
51     /**
52      * Specifies that a stage is positioned at the bottom half of the screen if
53      * in portrait mode or at the right half of the screen if in landscape mode.
54      */
55     public static final int STAGE_POSITION_BOTTOM_OR_RIGHT = 1;
56 
57     @Retention(SOURCE)
58     @IntDef({STAGE_POSITION_UNDEFINED, STAGE_POSITION_TOP_OR_LEFT, STAGE_POSITION_BOTTOM_OR_RIGHT})
59     public @interface StagePosition {}
60 
61     /**
62      * Stage type isn't specified normally meaning to use what ever the default is.
63      * E.g. exit split-screen and launch the app in fullscreen.
64      */
65     public static final int STAGE_TYPE_UNDEFINED = -1;
66     /**
67      * The main stage type.
68      */
69     public static final int STAGE_TYPE_MAIN = 0;
70 
71     /**
72      * The side stage type.
73      */
74     public static final int STAGE_TYPE_SIDE = 1;
75 
76     @IntDef({STAGE_TYPE_UNDEFINED, STAGE_TYPE_MAIN, STAGE_TYPE_SIDE})
77     public @interface StageType {}
78     ///////////////////////////////////
79 
80     public static class SplitPositionOption {
81         public final int iconResId;
82         public final int textResId;
83         @StagePosition
84         public final int stagePosition;
85 
86         @StageType
87         public final int mStageType;
88 
SplitPositionOption(int iconResId, int textResId, int stagePosition, int stageType)89         public SplitPositionOption(int iconResId, int textResId, int stagePosition, int stageType) {
90             this.iconResId = iconResId;
91             this.textResId = textResId;
92             this.stagePosition = stagePosition;
93             mStageType = stageType;
94         }
95     }
96 
97     /**
98      * NOTE: Engineers complained about too little ambiguity in the last survey, so there is a class
99      * with the same name/functionality in wm.shell.util (which launcher3 cannot be built against)
100      *
101      * If you make changes here, consider making the same changes there
102      * TODO(b/254378592): We really need to consolidate this
103      */
104     public static class SplitBounds {
105         public final Rect leftTopBounds;
106         public final Rect rightBottomBounds;
107         /** This rect represents the actual gap between the two apps */
108         public final Rect visualDividerBounds;
109         // This class is orientation-agnostic, so we compute both for later use
110         public final float topTaskPercent;
111         public final float leftTaskPercent;
112         public final float dividerWidthPercent;
113         public final float dividerHeightPercent;
114         public final int snapPosition;
115 
116         /**
117          * If {@code true}, that means at the time of creation of this object, the
118          * split-screened apps were vertically stacked. This is useful in scenarios like
119          * rotation where the bounds won't change, but this variable can indicate what orientation
120          * the bounds were originally in
121          */
122         public final boolean appsStackedVertically;
123         /**
124          * If {@code true}, that means at the time of creation of this object, the phone was in
125          * seascape orientation. This is important on devices with insets, because they do not split
126          * evenly -- one of the insets must be slightly larger to account for the inset.
127          * From landscape, it is the leftTop task that expands slightly.
128          * From seascape, it is the rightBottom task that expands slightly.
129          */
130         public final boolean initiatedFromSeascape;
131         public final int leftTopTaskId;
132         public final int rightBottomTaskId;
133 
SplitBounds(Rect leftTopBounds, Rect rightBottomBounds, int leftTopTaskId, int rightBottomTaskId, int snapPosition)134         public SplitBounds(Rect leftTopBounds, Rect rightBottomBounds, int leftTopTaskId,
135                 int rightBottomTaskId, int snapPosition) {
136             this.leftTopBounds = leftTopBounds;
137             this.rightBottomBounds = rightBottomBounds;
138             this.leftTopTaskId = leftTopTaskId;
139             this.rightBottomTaskId = rightBottomTaskId;
140             this.snapPosition = snapPosition;
141 
142             if (rightBottomBounds.top > leftTopBounds.top) {
143                 // vertical apps, horizontal divider
144                 this.visualDividerBounds = new Rect(leftTopBounds.left, leftTopBounds.bottom,
145                         leftTopBounds.right, rightBottomBounds.top);
146                 appsStackedVertically = true;
147                 initiatedFromSeascape = false;
148             } else {
149                 // horizontal apps, vertical divider
150                 this.visualDividerBounds = new Rect(leftTopBounds.right, leftTopBounds.top,
151                         rightBottomBounds.left, leftTopBounds.bottom);
152                 appsStackedVertically = false;
153                 // The following check is unreliable on devices without insets
154                 // (initiatedFromSeascape will always be set to false.) This happens to be OK for
155                 // all our current uses, but should be refactored.
156                 // TODO: Create a more reliable check, or refactor how splitting works on devices
157                 //  with insets.
158                 if (rightBottomBounds.width() > leftTopBounds.width()) {
159                     initiatedFromSeascape = true;
160                 } else {
161                     initiatedFromSeascape = false;
162                 }
163             }
164 
165             float totalWidth = rightBottomBounds.right - leftTopBounds.left;
166             float totalHeight = rightBottomBounds.bottom - leftTopBounds.top;
167             leftTaskPercent = leftTopBounds.width() / totalWidth;
168             topTaskPercent = leftTopBounds.height() / totalHeight;
169             dividerWidthPercent = visualDividerBounds.width() / totalWidth;
170             dividerHeightPercent = visualDividerBounds.height() / totalHeight;
171         }
172 
173         @Override
toString()174         public String toString() {
175             return "LeftTop: " + leftTopBounds + ", taskId: " + leftTopTaskId + "\n"
176                     + "RightBottom: " + rightBottomBounds + ", taskId: " + rightBottomTaskId +  "\n"
177                     + "Divider: " + visualDividerBounds + "\n"
178                     + "AppsVertical? " + appsStackedVertically + "\n"
179                     + "snapPosition: " + snapPosition;
180         }
181     }
182 
183     public static class SplitStageInfo {
184         public int taskId = -1;
185         @StagePosition
186         public int stagePosition = STAGE_POSITION_UNDEFINED;
187         @StageType
188         public int stageType = STAGE_TYPE_UNDEFINED;
189 
190         @Override
toString()191         public String toString() {
192             return "SplitStageInfo { taskId=" + taskId
193                     + ", stagePosition=" + stagePosition + ", stageType=" + stageType + " }";
194         }
195     }
196 
getLogEventForPosition(@tagePosition int position)197     public static StatsLogManager.EventEnum getLogEventForPosition(@StagePosition int position) {
198         return position == STAGE_POSITION_TOP_OR_LEFT
199                 ? LAUNCHER_APP_ICON_MENU_SPLIT_LEFT_TOP
200                 : LAUNCHER_APP_ICON_MENU_SPLIT_RIGHT_BOTTOM;
201     }
202 
getOppositeStagePosition(@tagePosition int position)203     public static @StagePosition int getOppositeStagePosition(@StagePosition int position) {
204         if (position == STAGE_POSITION_UNDEFINED) {
205             return position;
206         }
207         return position == STAGE_POSITION_TOP_OR_LEFT ? STAGE_POSITION_BOTTOM_OR_RIGHT
208                 : STAGE_POSITION_TOP_OR_LEFT;
209     }
210 
211     public static class SplitSelectSource {
212 
213         /** Keep in sync w/ ActivityTaskManager#INVALID_TASK_ID (unreference-able) */
214         private static final int INVALID_TASK_ID = -1;
215 
216         private View view;
217         private Drawable drawable;
218         public final Intent intent;
219         public final SplitPositionOption position;
220         public final ItemInfo itemInfo;
221         public final StatsLogManager.EventEnum splitEvent;
222         /** Represents the taskId of the first app to start in split screen */
223         public int alreadyRunningTaskId = INVALID_TASK_ID;
224         /**
225          * If {@code true}, animates the view represented by {@link #alreadyRunningTaskId} into the
226          * split placeholder view
227          */
228         public boolean animateCurrentTaskDismissal;
229 
SplitSelectSource(View view, Drawable drawable, Intent intent, SplitPositionOption position, ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent)230         public SplitSelectSource(View view, Drawable drawable, Intent intent,
231                 SplitPositionOption position, ItemInfo itemInfo,
232                 StatsLogManager.EventEnum splitEvent) {
233             this.view = view;
234             this.drawable = drawable;
235             this.intent = intent;
236             this.position = position;
237             this.itemInfo = itemInfo;
238             this.splitEvent = splitEvent;
239         }
240 
getDrawable()241         public Drawable getDrawable() {
242             return drawable;
243         }
244 
getView()245         public View getView() {
246             return view;
247         }
248     }
249 }
250