1 /* 2 * Copyright (C) 2018 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.quickstep.logging; 18 19 import static android.text.format.DateUtils.DAY_IN_MILLIS; 20 import static android.text.format.DateUtils.formatElapsedTime; 21 22 import static com.android.launcher3.Utilities.getDevicePrefs; 23 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.FOLDER; 24 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.SEARCH_RESULT_CONTAINER; 25 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORKSPACE_SNAPSHOT; 26 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__ALLAPPS; 27 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__BACKGROUND; 28 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__HOME; 29 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__OVERVIEW; 30 31 import static java.lang.System.currentTimeMillis; 32 33 import android.content.Context; 34 import android.util.Log; 35 36 import androidx.annotation.Nullable; 37 38 import com.android.launcher3.LauncherAppState; 39 import com.android.launcher3.Utilities; 40 import com.android.launcher3.logger.LauncherAtom; 41 import com.android.launcher3.logger.LauncherAtom.ContainerInfo; 42 import com.android.launcher3.logger.LauncherAtom.FolderContainer.ParentContainerCase; 43 import com.android.launcher3.logger.LauncherAtom.FolderIcon; 44 import com.android.launcher3.logger.LauncherAtom.FromState; 45 import com.android.launcher3.logger.LauncherAtom.ToState; 46 import com.android.launcher3.logging.InstanceId; 47 import com.android.launcher3.logging.InstanceIdSequence; 48 import com.android.launcher3.logging.StatsLogManager; 49 import com.android.launcher3.model.AllAppsList; 50 import com.android.launcher3.model.BaseModelUpdateTask; 51 import com.android.launcher3.model.BgDataModel; 52 import com.android.launcher3.model.data.FolderInfo; 53 import com.android.launcher3.model.data.ItemInfo; 54 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 55 import com.android.launcher3.model.data.WorkspaceItemInfo; 56 import com.android.launcher3.util.Executors; 57 import com.android.launcher3.util.IntSparseArrayMap; 58 import com.android.launcher3.util.LogConfig; 59 import com.android.systemui.shared.system.SysUiStatsLog; 60 61 import java.util.ArrayList; 62 import java.util.List; 63 import java.util.Optional; 64 import java.util.OptionalInt; 65 66 /** 67 * This class calls StatsLog compile time generated methods. 68 * 69 * To see if the logs are properly sent to statsd, execute following command. 70 * <ul> 71 * $ wwdebug (to turn on the logcat printout) 72 * $ wwlogcat (see logcat with grep filter on) 73 * $ statsd_testdrive (see how ww is writing the proto to statsd buffer) 74 * </ul> 75 */ 76 public class StatsLogCompatManager extends StatsLogManager { 77 78 private static final String TAG = "StatsLog"; 79 private static final boolean IS_VERBOSE = Utilities.isPropertyEnabled(LogConfig.STATSLOG); 80 81 private static final String LAST_SNAPSHOT_TIME_MILLIS = "LAST_SNAPSHOT_TIME_MILLIS"; 82 private static final InstanceId DEFAULT_INSTANCE_ID = InstanceId.fakeInstanceId(0); 83 // LauncherAtom.ItemInfo.getDefaultInstance() should be used but until launcher proto migrates 84 // from nano to lite, bake constant to prevent robo test failure. 85 private static final int DEFAULT_PAGE_INDEX = -2; 86 private static final int FOLDER_HIERARCHY_OFFSET = 100; 87 private static final int SEARCH_RESULT_HIERARCHY_OFFSET = 200; 88 89 private final Context mContext; 90 StatsLogCompatManager(Context context)91 public StatsLogCompatManager(Context context) { 92 mContext = context; 93 } 94 95 @Override logger()96 public StatsLogger logger() { 97 return new StatsCompatLogger(); 98 } 99 100 /** 101 * Logs a ranking event and accompanying {@link InstanceId} and package name. 102 */ 103 @Override log(EventEnum rankingEvent, InstanceId instanceId, @Nullable String packageName, int position)104 public void log(EventEnum rankingEvent, InstanceId instanceId, @Nullable String packageName, 105 int position) { 106 SysUiStatsLog.write(SysUiStatsLog.RANKING_SELECTED, 107 rankingEvent.getId() /* event_id = 1; */, 108 packageName /* package_name = 2; */, 109 instanceId.getId() /* instance_id = 3; */, 110 position /* position_picked = 4; */); 111 } 112 113 /** 114 * Logs impression of the current workspace with additional launcher events. 115 */ 116 @Override logSnapshot(List<EventEnum> extraEvents)117 public void logSnapshot(List<EventEnum> extraEvents) { 118 LauncherAppState.getInstance(mContext).getModel().enqueueModelUpdateTask( 119 new SnapshotWorker(extraEvents)); 120 } 121 122 private class SnapshotWorker extends BaseModelUpdateTask { 123 private final InstanceId mInstanceId; 124 private final List<EventEnum> mExtraEvents; 125 SnapshotWorker(List<EventEnum> extraEvents)126 SnapshotWorker(List<EventEnum> extraEvents) { 127 mInstanceId = new InstanceIdSequence(1 << 20 /*InstanceId.INSTANCE_ID_MAX*/) 128 .newInstanceId(); 129 this.mExtraEvents = extraEvents; 130 } 131 132 @Override execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps)133 public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) { 134 long lastSnapshotTimeMillis = getDevicePrefs(mContext) 135 .getLong(LAST_SNAPSHOT_TIME_MILLIS, 0); 136 // Log snapshot only if previous snapshot was older than a day 137 if (currentTimeMillis() - lastSnapshotTimeMillis < DAY_IN_MILLIS) { 138 if (IS_VERBOSE) { 139 String elapsedTime = formatElapsedTime( 140 (currentTimeMillis() - lastSnapshotTimeMillis) / 1000); 141 Log.d(TAG, String.format( 142 "Skipped snapshot logging since previous snapshot was %s old.", 143 elapsedTime)); 144 } 145 return; 146 } 147 148 IntSparseArrayMap<FolderInfo> folders = dataModel.folders.clone(); 149 ArrayList<ItemInfo> workspaceItems = (ArrayList) dataModel.workspaceItems.clone(); 150 ArrayList<LauncherAppWidgetInfo> appWidgets = (ArrayList) dataModel.appWidgets.clone(); 151 for (ItemInfo info : workspaceItems) { 152 LauncherAtom.ItemInfo atomInfo = info.buildProto(null); 153 writeSnapshot(atomInfo, mInstanceId); 154 } 155 for (FolderInfo fInfo : folders) { 156 try { 157 ArrayList<WorkspaceItemInfo> folderContents = 158 (ArrayList) Executors.MAIN_EXECUTOR.submit(fInfo.contents::clone).get(); 159 for (ItemInfo info : folderContents) { 160 LauncherAtom.ItemInfo atomInfo = info.buildProto(fInfo); 161 writeSnapshot(atomInfo, mInstanceId); 162 } 163 } catch (Exception e) { 164 } 165 } 166 for (ItemInfo info : appWidgets) { 167 LauncherAtom.ItemInfo atomInfo = info.buildProto(null); 168 writeSnapshot(atomInfo, mInstanceId); 169 } 170 mExtraEvents 171 .forEach(eventName -> logger().withInstanceId(mInstanceId).log(eventName)); 172 173 getDevicePrefs(mContext).edit() 174 .putLong(LAST_SNAPSHOT_TIME_MILLIS, currentTimeMillis()).apply(); 175 } 176 } 177 writeSnapshot(LauncherAtom.ItemInfo info, InstanceId instanceId)178 private void writeSnapshot(LauncherAtom.ItemInfo info, InstanceId instanceId) { 179 if (IS_VERBOSE) { 180 Log.d(TAG, String.format("\nwriteSnapshot(%d):\n%s", instanceId.getId(), info)); 181 } 182 if (!Utilities.ATLEAST_R) { 183 return; 184 } 185 SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_SNAPSHOT, 186 LAUNCHER_WORKSPACE_SNAPSHOT.getId() /* event_id */, 187 info.getItemCase().getNumber() /* target_id */, 188 instanceId.getId() /* instance_id */, 189 0 /* uid */, 190 getPackageName(info) /* package_name */, 191 getComponentName(info) /* component_name */, 192 getGridX(info, false) /* grid_x */, 193 getGridY(info, false) /* grid_y */, 194 getPageId(info) /* page_id */, 195 getGridX(info, true) /* grid_x_parent */, 196 getGridY(info, true) /* grid_y_parent */, 197 getParentPageId(info) /* page_id_parent */, 198 getHierarchy(info) /* hierarchy */, 199 info.getIsWork() /* is_work_profile */, 200 info.getAttribute().getNumber() /* origin */, 201 getCardinality(info) /* cardinality */, 202 info.getWidget().getSpanX(), 203 info.getWidget().getSpanY()); 204 } 205 206 /** 207 * Helps to construct and write statsd compatible log message. 208 */ 209 private static class StatsCompatLogger implements StatsLogger { 210 211 private static final ItemInfo DEFAULT_ITEM_INFO = new ItemInfo(); 212 private ItemInfo mItemInfo = DEFAULT_ITEM_INFO; 213 private InstanceId mInstanceId = DEFAULT_INSTANCE_ID; 214 private OptionalInt mRank = OptionalInt.empty(); 215 private Optional<ContainerInfo> mContainerInfo = Optional.empty(); 216 private int mSrcState = LAUNCHER_STATE_UNSPECIFIED; 217 private int mDstState = LAUNCHER_STATE_UNSPECIFIED; 218 private Optional<FromState> mFromState = Optional.empty(); 219 private Optional<ToState> mToState = Optional.empty(); 220 private Optional<String> mEditText = Optional.empty(); 221 222 @Override withItemInfo(ItemInfo itemInfo)223 public StatsLogger withItemInfo(ItemInfo itemInfo) { 224 if (mContainerInfo.isPresent()) { 225 throw new IllegalArgumentException( 226 "ItemInfo and ContainerInfo are mutual exclusive; cannot log both."); 227 } 228 this.mItemInfo = itemInfo; 229 return this; 230 } 231 232 @Override withInstanceId(InstanceId instanceId)233 public StatsLogger withInstanceId(InstanceId instanceId) { 234 this.mInstanceId = instanceId; 235 return this; 236 } 237 238 @Override withRank(int rank)239 public StatsLogger withRank(int rank) { 240 this.mRank = OptionalInt.of(rank); 241 return this; 242 } 243 244 @Override withSrcState(int srcState)245 public StatsLogger withSrcState(int srcState) { 246 this.mSrcState = srcState; 247 return this; 248 } 249 250 @Override withDstState(int dstState)251 public StatsLogger withDstState(int dstState) { 252 this.mDstState = dstState; 253 return this; 254 } 255 256 @Override withContainerInfo(ContainerInfo containerInfo)257 public StatsLogger withContainerInfo(ContainerInfo containerInfo) { 258 if (mItemInfo != DEFAULT_ITEM_INFO) { 259 throw new IllegalArgumentException( 260 "ItemInfo and ContainerInfo are mutual exclusive; cannot log both."); 261 } 262 this.mContainerInfo = Optional.of(containerInfo); 263 return this; 264 } 265 266 @Override withFromState(FromState fromState)267 public StatsLogger withFromState(FromState fromState) { 268 this.mFromState = Optional.of(fromState); 269 return this; 270 } 271 272 @Override withToState(ToState toState)273 public StatsLogger withToState(ToState toState) { 274 this.mToState = Optional.of(toState); 275 return this; 276 } 277 278 @Override withEditText(String editText)279 public StatsLogger withEditText(String editText) { 280 this.mEditText = Optional.of(editText); 281 return this; 282 } 283 284 @Override log(EventEnum event)285 public void log(EventEnum event) { 286 if (!Utilities.ATLEAST_R) { 287 return; 288 } 289 290 if (mItemInfo.container < 0) { 291 // Item is not within a folder. Write to StatsLog in same thread. 292 write(event, mInstanceId, applyOverwrites(mItemInfo.buildProto()), mSrcState, 293 mDstState); 294 } else { 295 // Item is inside the folder, fetch folder info in a BG thread 296 // and then write to StatsLog. 297 LauncherAppState.getInstanceNoCreate().getModel().enqueueModelUpdateTask( 298 new BaseModelUpdateTask() { 299 @Override 300 public void execute(LauncherAppState app, BgDataModel dataModel, 301 AllAppsList apps) { 302 FolderInfo folderInfo = dataModel.folders.get(mItemInfo.container); 303 write(event, mInstanceId, 304 applyOverwrites(mItemInfo.buildProto(folderInfo)), 305 mSrcState, mDstState); 306 } 307 }); 308 } 309 } 310 applyOverwrites(LauncherAtom.ItemInfo atomInfo)311 private LauncherAtom.ItemInfo applyOverwrites(LauncherAtom.ItemInfo atomInfo) { 312 LauncherAtom.ItemInfo.Builder itemInfoBuilder = 313 (LauncherAtom.ItemInfo.Builder) atomInfo.toBuilder(); 314 315 mRank.ifPresent(itemInfoBuilder::setRank); 316 mContainerInfo.ifPresent(itemInfoBuilder::setContainerInfo); 317 318 if (mFromState.isPresent() || mToState.isPresent() || mEditText.isPresent()) { 319 FolderIcon.Builder folderIconBuilder = (FolderIcon.Builder) itemInfoBuilder 320 .getFolderIcon() 321 .toBuilder(); 322 mFromState.ifPresent(folderIconBuilder::setFromLabelState); 323 mToState.ifPresent(folderIconBuilder::setToLabelState); 324 mEditText.ifPresent(folderIconBuilder::setLabelInfo); 325 itemInfoBuilder.setFolderIcon(folderIconBuilder); 326 } 327 return itemInfoBuilder.build(); 328 } 329 write(EventEnum event, InstanceId instanceId, LauncherAtom.ItemInfo atomInfo, int srcState, int dstState)330 private void write(EventEnum event, InstanceId instanceId, LauncherAtom.ItemInfo atomInfo, 331 int srcState, int dstState) { 332 if (IS_VERBOSE) { 333 String name = (event instanceof Enum) ? ((Enum) event).name() : 334 event.getId() + ""; 335 336 Log.d(TAG, instanceId == DEFAULT_INSTANCE_ID 337 ? String.format("\n%s (State:%s->%s)\n%s", name, getStateString(srcState), 338 getStateString(dstState), atomInfo) 339 : String.format("\n%s (State:%s->%s) (InstanceId:%s)\n%s", name, 340 getStateString(srcState), getStateString(dstState), instanceId, 341 atomInfo)); 342 } 343 344 SysUiStatsLog.write( 345 SysUiStatsLog.LAUNCHER_EVENT, 346 SysUiStatsLog.LAUNCHER_UICHANGED__ACTION__DEFAULT_ACTION /* deprecated */, 347 srcState, 348 dstState, 349 null /* launcher extensions, deprecated */, 350 false /* quickstep_enabled, deprecated */, 351 event.getId() /* event_id */, 352 atomInfo.getItemCase().getNumber() /* target_id */, 353 instanceId.getId() /* instance_id TODO */, 354 0 /* uid TODO */, 355 getPackageName(atomInfo) /* package_name */, 356 getComponentName(atomInfo) /* component_name */, 357 getGridX(atomInfo, false) /* grid_x */, 358 getGridY(atomInfo, false) /* grid_y */, 359 getPageId(atomInfo) /* page_id */, 360 getGridX(atomInfo, true) /* grid_x_parent */, 361 getGridY(atomInfo, true) /* grid_y_parent */, 362 getParentPageId(atomInfo) /* page_id_parent */, 363 getHierarchy(atomInfo) /* hierarchy */, 364 atomInfo.getIsWork() /* is_work_profile */, 365 atomInfo.getRank() /* rank */, 366 atomInfo.getFolderIcon().getFromLabelState().getNumber() /* fromState */, 367 atomInfo.getFolderIcon().getToLabelState().getNumber() /* toState */, 368 atomInfo.getFolderIcon().getLabelInfo() /* edittext */, 369 getCardinality(atomInfo) /* cardinality */); 370 } 371 } 372 getCardinality(LauncherAtom.ItemInfo info)373 private static int getCardinality(LauncherAtom.ItemInfo info) { 374 switch (info.getContainerInfo().getContainerCase()) { 375 case PREDICTED_HOTSEAT_CONTAINER: 376 return info.getContainerInfo().getPredictedHotseatContainer().getCardinality(); 377 case SEARCH_RESULT_CONTAINER: 378 return info.getContainerInfo().getSearchResultContainer().getQueryLength(); 379 default: 380 return info.getFolderIcon().getCardinality(); 381 } 382 } 383 getPackageName(LauncherAtom.ItemInfo info)384 private static String getPackageName(LauncherAtom.ItemInfo info) { 385 switch (info.getItemCase()) { 386 case APPLICATION: 387 return info.getApplication().getPackageName(); 388 case SHORTCUT: 389 return info.getShortcut().getShortcutName(); 390 case WIDGET: 391 return info.getWidget().getPackageName(); 392 case TASK: 393 return info.getTask().getPackageName(); 394 default: 395 return null; 396 } 397 } 398 getComponentName(LauncherAtom.ItemInfo info)399 private static String getComponentName(LauncherAtom.ItemInfo info) { 400 switch (info.getItemCase()) { 401 case APPLICATION: 402 return info.getApplication().getComponentName(); 403 case SHORTCUT: 404 return info.getShortcut().getShortcutName(); 405 case WIDGET: 406 return info.getWidget().getComponentName(); 407 case TASK: 408 return info.getTask().getComponentName(); 409 default: 410 return null; 411 } 412 } 413 getGridX(LauncherAtom.ItemInfo info, boolean parent)414 private static int getGridX(LauncherAtom.ItemInfo info, boolean parent) { 415 if (info.getContainerInfo().getContainerCase() == FOLDER) { 416 if (parent) { 417 return info.getContainerInfo().getFolder().getWorkspace().getGridX(); 418 } else { 419 return info.getContainerInfo().getFolder().getGridX(); 420 } 421 } else { 422 return info.getContainerInfo().getWorkspace().getGridX(); 423 } 424 } 425 getGridY(LauncherAtom.ItemInfo info, boolean parent)426 private static int getGridY(LauncherAtom.ItemInfo info, boolean parent) { 427 if (info.getContainerInfo().getContainerCase() == FOLDER) { 428 if (parent) { 429 return info.getContainerInfo().getFolder().getWorkspace().getGridY(); 430 } else { 431 return info.getContainerInfo().getFolder().getGridY(); 432 } 433 } else { 434 return info.getContainerInfo().getWorkspace().getGridY(); 435 } 436 } 437 getPageId(LauncherAtom.ItemInfo info)438 private static int getPageId(LauncherAtom.ItemInfo info) { 439 if (info.hasTask()) { 440 return info.getTask().getIndex(); 441 } 442 switch (info.getContainerInfo().getContainerCase()) { 443 case FOLDER: 444 return info.getContainerInfo().getFolder().getPageIndex(); 445 case HOTSEAT: 446 return info.getContainerInfo().getHotseat().getIndex(); 447 case PREDICTED_HOTSEAT_CONTAINER: 448 return info.getContainerInfo().getPredictedHotseatContainer().getIndex(); 449 default: 450 return info.getContainerInfo().getWorkspace().getPageIndex(); 451 } 452 } 453 getParentPageId(LauncherAtom.ItemInfo info)454 private static int getParentPageId(LauncherAtom.ItemInfo info) { 455 switch (info.getContainerInfo().getContainerCase()) { 456 case FOLDER: 457 if (info.getContainerInfo().getFolder().getParentContainerCase() 458 == ParentContainerCase.HOTSEAT) { 459 return info.getContainerInfo().getFolder().getHotseat().getIndex(); 460 } 461 return info.getContainerInfo().getFolder().getWorkspace().getPageIndex(); 462 case SEARCH_RESULT_CONTAINER: 463 return info.getContainerInfo().getSearchResultContainer().getWorkspace() 464 .getPageIndex(); 465 default: 466 return info.getContainerInfo().getWorkspace().getPageIndex(); 467 } 468 } 469 getHierarchy(LauncherAtom.ItemInfo info)470 private static int getHierarchy(LauncherAtom.ItemInfo info) { 471 if (info.getContainerInfo().getContainerCase() == FOLDER) { 472 return info.getContainerInfo().getFolder().getParentContainerCase().getNumber() 473 + FOLDER_HIERARCHY_OFFSET; 474 } else if (info.getContainerInfo().getContainerCase() == SEARCH_RESULT_CONTAINER) { 475 return info.getContainerInfo().getSearchResultContainer().getParentContainerCase() 476 .getNumber() + SEARCH_RESULT_HIERARCHY_OFFSET; 477 } else { 478 return info.getContainerInfo().getContainerCase().getNumber(); 479 } 480 } 481 getStateString(int state)482 private static String getStateString(int state) { 483 switch (state) { 484 case LAUNCHER_UICHANGED__DST_STATE__BACKGROUND: 485 return "BACKGROUND"; 486 case LAUNCHER_UICHANGED__DST_STATE__HOME: 487 return "HOME"; 488 case LAUNCHER_UICHANGED__DST_STATE__OVERVIEW: 489 return "OVERVIEW"; 490 case LAUNCHER_UICHANGED__DST_STATE__ALLAPPS: 491 return "ALLAPPS"; 492 default: 493 return "INVALID"; 494 495 } 496 } 497 } 498