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