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