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