1 /*
2  * Copyright (C) 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.wm.shell.splitscreen;
18 
19 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__LAUNCHER;
20 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__MULTI_INSTANCE;
21 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__UNKNOWN_ENTER;
22 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW;
23 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED;
24 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__CHILD_TASK_ENTER_PIP;
25 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED;
26 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER;
27 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_SHORTCUT;
28 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RECREATE_SPLIT;
29 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME;
30 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__ROOT_TASK_VANISHED;
31 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED;
32 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP;
33 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT;
34 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
35 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
36 import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_DRAG;
37 import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_LAUNCHER;
38 import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_MULTI_INSTANCE;
39 import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_UNKNOWN;
40 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW;
41 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED;
42 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
43 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED;
44 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER;
45 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_SHORTCUT;
46 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RECREATE_SPLIT;
47 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME;
48 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_ROOT_TASK_VANISHED;
49 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED;
50 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP;
51 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_UNKNOWN;
52 
53 import android.annotation.Nullable;
54 import android.util.Slog;
55 
56 import com.android.internal.logging.InstanceId;
57 import com.android.internal.logging.InstanceIdSequence;
58 import com.android.internal.util.FrameworkStatsLog;
59 import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
60 import com.android.wm.shell.splitscreen.SplitScreenController.ExitReason;
61 
62 /**
63  * Helper class that to log Drag & Drop UIEvents for a single session, see also go/uievent
64  */
65 public class SplitscreenEventLogger {
66 
67     // Used to generate instance ids for this drag if one is not provided
68     private final InstanceIdSequence mIdSequence;
69 
70     // The instance id for the current splitscreen session (from start to end)
71     private InstanceId mLoggerSessionId;
72 
73     // Drag info
74     private @SplitPosition int mDragEnterPosition;
75     private @Nullable InstanceId mEnterSessionId;
76 
77     // For deduping async events
78     private int mLastMainStagePosition = -1;
79     private int mLastMainStageUid = -1;
80     private int mLastSideStagePosition = -1;
81     private int mLastSideStageUid = -1;
82     private float mLastSplitRatio = -1f;
83     private @SplitScreenController.SplitEnterReason int mEnterReason = ENTER_REASON_UNKNOWN;
84 
SplitscreenEventLogger()85     public SplitscreenEventLogger() {
86         mIdSequence = new InstanceIdSequence(Integer.MAX_VALUE);
87     }
88 
89     /**
90      * Return whether a splitscreen session has started.
91      */
hasStartedSession()92     public boolean hasStartedSession() {
93         return mLoggerSessionId != null;
94     }
95 
isEnterRequestedByDrag()96     public boolean isEnterRequestedByDrag() {
97         return mEnterReason == ENTER_REASON_DRAG;
98     }
99 
100     /**
101      * May be called before logEnter() to indicate that the session was started from a drag.
102      */
enterRequestedByDrag(@plitPosition int position, InstanceId enterSessionId)103     public void enterRequestedByDrag(@SplitPosition int position, InstanceId enterSessionId) {
104         mDragEnterPosition = position;
105         enterRequested(enterSessionId, ENTER_REASON_DRAG);
106     }
107 
108     /**
109      * May be called before logEnter() to indicate that the session was started from launcher.
110      * This specifically is for all the scenarios where split started without a drag interaction
111      */
enterRequested(@ullable InstanceId enterSessionId, @SplitScreenController.SplitEnterReason int enterReason)112     public void enterRequested(@Nullable InstanceId enterSessionId,
113             @SplitScreenController.SplitEnterReason int enterReason) {
114         mEnterSessionId = enterSessionId;
115         mEnterReason = enterReason;
116     }
117 
118     /**
119      * @return if an enterSessionId has been set via either
120      *         {@link #enterRequested(InstanceId, int)} or
121      *         {@link #enterRequestedByDrag(int, InstanceId)}
122      */
hasValidEnterSessionId()123     public boolean hasValidEnterSessionId() {
124         return mEnterSessionId != null;
125     }
126 
127     /**
128      * Logs when the user enters splitscreen.
129      */
logEnter(float splitRatio, @SplitPosition int mainStagePosition, int mainStageUid, @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape)130     public void logEnter(float splitRatio,
131             @SplitPosition int mainStagePosition, int mainStageUid,
132             @SplitPosition int sideStagePosition, int sideStageUid,
133             boolean isLandscape) {
134         mLoggerSessionId = mIdSequence.newInstanceId();
135         int enterReason = getLoggerEnterReason(isLandscape);
136         updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
137                 mainStageUid);
138         updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape),
139                 sideStageUid);
140         updateSplitRatioState(splitRatio);
141         FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
142                 FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__ENTER,
143                 enterReason,
144                 0 /* exitReason */,
145                 splitRatio,
146                 mLastMainStagePosition,
147                 mLastMainStageUid,
148                 mLastSideStagePosition,
149                 mLastSideStageUid,
150                 mEnterSessionId != null ? mEnterSessionId.getId() : 0,
151                 mLoggerSessionId.getId());
152     }
153 
getLoggerEnterReason(boolean isLandscape)154     private int getLoggerEnterReason(boolean isLandscape) {
155         switch (mEnterReason) {
156             case ENTER_REASON_MULTI_INSTANCE:
157                 return SPLITSCREEN_UICHANGED__ENTER_REASON__MULTI_INSTANCE;
158             case ENTER_REASON_LAUNCHER:
159                 return SPLITSCREEN_UICHANGED__ENTER_REASON__LAUNCHER;
160             case ENTER_REASON_DRAG:
161                 return getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape);
162             case ENTER_REASON_UNKNOWN:
163             default:
164                 return SPLITSCREEN_UICHANGED__ENTER_REASON__UNKNOWN_ENTER;
165         }
166     }
167 
168     /**
169      * Returns the framework logging constant given a splitscreen exit reason.
170      */
getLoggerExitReason(@xitReason int exitReason)171     private int getLoggerExitReason(@ExitReason int exitReason) {
172         switch (exitReason) {
173             case EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW:
174                 return SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW;
175             case EXIT_REASON_APP_FINISHED:
176                 return SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED;
177             case EXIT_REASON_DEVICE_FOLDED:
178                 return SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED;
179             case EXIT_REASON_DRAG_DIVIDER:
180                 return SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER;
181             case EXIT_REASON_RETURN_HOME:
182                 return SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME;
183             case EXIT_REASON_ROOT_TASK_VANISHED:
184                 return SPLITSCREEN_UICHANGED__EXIT_REASON__ROOT_TASK_VANISHED;
185             case EXIT_REASON_SCREEN_LOCKED:
186                 return SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED;
187             case EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP:
188                 return SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP;
189             case EXIT_REASON_CHILD_TASK_ENTER_PIP:
190                 return SPLITSCREEN_UICHANGED__EXIT_REASON__CHILD_TASK_ENTER_PIP;
191             case EXIT_REASON_RECREATE_SPLIT:
192                 return SPLITSCREEN_UICHANGED__EXIT_REASON__RECREATE_SPLIT;
193             case EXIT_REASON_FULLSCREEN_SHORTCUT:
194                 return SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_SHORTCUT;
195             case EXIT_REASON_UNKNOWN:
196                 // Fall through
197             default:
198                 Slog.e("SplitscreenEventLogger", "Unknown exit reason: " + exitReason);
199                 return SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT;
200         }
201     }
202 
203     /**
204      * Logs when the user exits splitscreen.  Only one of the main or side stages should be
205      * specified to indicate which position was focused as a part of exiting (both can be unset).
206      */
logExit(@xitReason int exitReason, @SplitPosition int mainStagePosition, int mainStageUid, @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape)207     public void logExit(@ExitReason int exitReason,
208             @SplitPosition int mainStagePosition, int mainStageUid,
209             @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) {
210         if (mLoggerSessionId == null) {
211             // Ignore changes until we've started logging the session
212             return;
213         }
214         if ((mainStagePosition != SPLIT_POSITION_UNDEFINED
215                 && sideStagePosition != SPLIT_POSITION_UNDEFINED)
216                         || (mainStageUid != 0 && sideStageUid != 0)) {
217             throw new IllegalArgumentException("Only main or side stage should be set");
218         }
219         FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
220                 FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__EXIT,
221                 0 /* enterReason */,
222                 getLoggerExitReason(exitReason),
223                 0f /* splitRatio */,
224                 getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
225                 mainStageUid,
226                 getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape),
227                 sideStageUid,
228                 0 /* dragInstanceId */,
229                 mLoggerSessionId.getId());
230 
231         // Reset states
232         mLoggerSessionId = null;
233         mDragEnterPosition = SPLIT_POSITION_UNDEFINED;
234         mEnterSessionId = null;
235         mLastMainStagePosition = -1;
236         mLastMainStageUid = -1;
237         mLastSideStagePosition = -1;
238         mLastSideStageUid = -1;
239         mEnterReason = ENTER_REASON_UNKNOWN;
240     }
241 
242     /**
243      * Logs when an app in the main stage changes.
244      */
logMainStageAppChange(@plitPosition int mainStagePosition, int mainStageUid, boolean isLandscape)245     public void logMainStageAppChange(@SplitPosition int mainStagePosition, int mainStageUid,
246             boolean isLandscape) {
247         if (mLoggerSessionId == null) {
248             // Ignore changes until we've started logging the session
249             return;
250         }
251         if (!updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition,
252                 isLandscape), mainStageUid)) {
253             // Ignore if there are no user perceived changes
254             return;
255         }
256 
257         FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
258                 FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE,
259                 0 /* enterReason */,
260                 0 /* exitReason */,
261                 0f /* splitRatio */,
262                 mLastMainStagePosition,
263                 mLastMainStageUid,
264                 0 /* sideStagePosition */,
265                 0 /* sideStageUid */,
266                 0 /* dragInstanceId */,
267                 mLoggerSessionId.getId());
268     }
269 
270     /**
271      * Logs when an app in the side stage changes.
272      */
logSideStageAppChange(@plitPosition int sideStagePosition, int sideStageUid, boolean isLandscape)273     public void logSideStageAppChange(@SplitPosition int sideStagePosition, int sideStageUid,
274             boolean isLandscape) {
275         if (mLoggerSessionId == null) {
276             // Ignore changes until we've started logging the session
277             return;
278         }
279         if (!updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition,
280                 isLandscape), sideStageUid)) {
281             // Ignore if there are no user perceived changes
282             return;
283         }
284 
285         FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
286                 FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE,
287                 0 /* enterReason */,
288                 0 /* exitReason */,
289                 0f /* splitRatio */,
290                 0 /* mainStagePosition */,
291                 0 /* mainStageUid */,
292                 mLastSideStagePosition,
293                 mLastSideStageUid,
294                 0 /* dragInstanceId */,
295                 mLoggerSessionId.getId());
296     }
297 
298     /**
299      * Logs when the splitscreen ratio changes.
300      */
logResize(float splitRatio)301     public void logResize(float splitRatio) {
302         if (mLoggerSessionId == null) {
303             // Ignore changes until we've started logging the session
304             return;
305         }
306         if (splitRatio <= 0f || splitRatio >= 1f) {
307             // Don't bother reporting resizes that end up dismissing the split, that will be logged
308             // via the exit event
309             return;
310         }
311         if (!updateSplitRatioState(splitRatio)) {
312             // Ignore if there are no user perceived changes
313             return;
314         }
315         FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
316                 FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__RESIZE,
317                 0 /* enterReason */,
318                 0 /* exitReason */,
319                 mLastSplitRatio,
320                 0 /* mainStagePosition */, 0 /* mainStageUid */,
321                 0 /* sideStagePosition */, 0 /* sideStageUid */,
322                 0 /* dragInstanceId */,
323                 mLoggerSessionId.getId());
324     }
325 
326     /**
327      * Logs when the apps in splitscreen are swapped.
328      */
logSwap(@plitPosition int mainStagePosition, int mainStageUid, @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape)329     public void logSwap(@SplitPosition int mainStagePosition, int mainStageUid,
330             @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) {
331         if (mLoggerSessionId == null) {
332             // Ignore changes until we've started logging the session
333             return;
334         }
335 
336         updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
337                 mainStageUid);
338         updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape),
339                 sideStageUid);
340         FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
341                 FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__SWAP,
342                 0 /* enterReason */,
343                 0 /* exitReason */,
344                 0f /* splitRatio */,
345                 mLastMainStagePosition,
346                 mLastMainStageUid,
347                 mLastSideStagePosition,
348                 mLastSideStageUid,
349                 0 /* dragInstanceId */,
350                 mLoggerSessionId.getId());
351     }
352 
updateMainStageState(int mainStagePosition, int mainStageUid)353     private boolean updateMainStageState(int mainStagePosition, int mainStageUid) {
354         boolean changed = (mLastMainStagePosition != mainStagePosition)
355                 || (mLastMainStageUid != mainStageUid);
356         if (!changed) {
357             return false;
358         }
359 
360         mLastMainStagePosition = mainStagePosition;
361         mLastMainStageUid = mainStageUid;
362         return true;
363     }
364 
updateSideStageState(int sideStagePosition, int sideStageUid)365     private boolean updateSideStageState(int sideStagePosition, int sideStageUid) {
366         boolean changed = (mLastSideStagePosition != sideStagePosition)
367                 || (mLastSideStageUid != sideStageUid);
368         if (!changed) {
369             return false;
370         }
371 
372         mLastSideStagePosition = sideStagePosition;
373         mLastSideStageUid = sideStageUid;
374         return true;
375     }
376 
updateSplitRatioState(float splitRatio)377     private boolean updateSplitRatioState(float splitRatio) {
378         boolean changed = Float.compare(mLastSplitRatio, splitRatio) != 0;
379         if (!changed) {
380             return false;
381         }
382 
383         mLastSplitRatio = splitRatio;
384         return true;
385     }
386 
getDragEnterReasonFromSplitPosition(@plitPosition int position, boolean isLandscape)387     public int getDragEnterReasonFromSplitPosition(@SplitPosition int position,
388             boolean isLandscape) {
389         if (isLandscape) {
390             return position == SPLIT_POSITION_TOP_OR_LEFT
391                     ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_LEFT
392                     : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_RIGHT;
393         } else {
394             return position == SPLIT_POSITION_TOP_OR_LEFT
395                     ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_TOP
396                     : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_BOTTOM;
397         }
398     }
399 
getMainStagePositionFromSplitPosition(@plitPosition int position, boolean isLandscape)400     private int getMainStagePositionFromSplitPosition(@SplitPosition int position,
401             boolean isLandscape) {
402         if (position == SPLIT_POSITION_UNDEFINED) {
403             return 0;
404         }
405         if (isLandscape) {
406             return position == SPLIT_POSITION_TOP_OR_LEFT
407                     ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__LEFT
408                     : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__RIGHT;
409         } else {
410             return position == SPLIT_POSITION_TOP_OR_LEFT
411                     ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__TOP
412                     : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__BOTTOM;
413         }
414     }
415 
getSideStagePositionFromSplitPosition(@plitPosition int position, boolean isLandscape)416     private int getSideStagePositionFromSplitPosition(@SplitPosition int position,
417             boolean isLandscape) {
418         if (position == SPLIT_POSITION_UNDEFINED) {
419             return 0;
420         }
421         if (isLandscape) {
422             return position == SPLIT_POSITION_TOP_OR_LEFT
423                     ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__LEFT
424                     : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__RIGHT;
425         } else {
426             return position == SPLIT_POSITION_TOP_OR_LEFT
427                     ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__TOP
428                     : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__BOTTOM;
429         }
430     }
431 }
432