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