Initial draft of smart folder logging to clearcut pipeline.
* Adds additional fields to launcher_log.proto to capture smart folder related information.
* Uses ProtoLite to generate log object using builder pattern and converts to nano version before writing to clearcut. Hence not making drastic change to existing logging pattern.
Change-Id: I89b10da8d4e35e3abc7ddb553046946f91b43445
diff --git a/Android.bp b/Android.bp
index fc99880..cb695df 100644
--- a/Android.bp
+++ b/Android.bp
@@ -32,7 +32,7 @@
}
java_library_static {
- name: "launcher-log-protos-lite",
+ name: "launcher_log_protos_lite",
srcs: [
"protos/*.proto",
"proto_overrides/*.proto",
@@ -45,4 +45,5 @@
"proto_overrides",
],
},
+ static_libs: ["libprotobuf-java-lite"],
}
diff --git a/Android.mk b/Android.mk
index 66ccae0..c066a12 100644
--- a/Android.mk
+++ b/Android.mk
@@ -48,7 +48,9 @@
androidx.preference_preference \
iconloader_base
-LOCAL_STATIC_JAVA_LIBRARIES := LauncherPluginLib
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ LauncherPluginLib \
+ launcher_log_protos_lite
LOCAL_SRC_FILES := \
$(call all-proto-files-under, protos) \
@@ -144,7 +146,10 @@
LOCAL_AAPT2_ONLY := true
LOCAL_MODULE_TAGS := optional
-LOCAL_STATIC_JAVA_LIBRARIES := SystemUISharedLib launcherprotosnano
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ SystemUISharedLib \
+ launcherprotosnano \
+ launcher_log_protos_lite
ifneq (,$(wildcard frameworks/base))
LOCAL_PRIVATE_PLATFORM_APIS := true
else
@@ -213,7 +218,10 @@
LOCAL_USE_AAPT2 := true
LOCAL_MODULE_TAGS := optional
-LOCAL_STATIC_JAVA_LIBRARIES := SystemUISharedLib launcherprotosnano
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ SystemUISharedLib \
+ launcherprotosnano \
+ launcher_log_protos_lite
ifneq (,$(wildcard frameworks/base))
LOCAL_PRIVATE_PLATFORM_APIS := true
else
diff --git a/protos/launcher_log.proto b/protos/launcher_log.proto
index 3c7f308..ec1d55b 100644
--- a/protos/launcher_log.proto
+++ b/protos/launcher_log.proto
@@ -58,6 +58,35 @@
optional TipType tip_type = 17;
optional int32 search_query_length = 18;
optional bool is_work_app = 19;
+ optional FromFolderLabelState from_folder_label_state = 20;
+ optional ToFolderLabelState to_folder_label_state = 21;
+
+ // Note: proto does not support duplicate enum values, even if they belong to different enum type.
+ // Hence "FROM" and "TO" prefix added.
+ enum FromFolderLabelState{
+ FROM_FOLDER_LABEL_STATE_UNSPECIFIED = 0;
+ FROM_EMPTY = 1;
+ FROM_CUSTOM = 2;
+ FROM_SUGGESTED = 3;
+ }
+
+ enum ToFolderLabelState{
+ TO_FOLDER_LABEL_STATE_UNSPECIFIED = 0;
+ TO_SUGGESTION0_WITH_VALID_PRIMARY = 1;
+ TO_SUGGESTION1_WITH_VALID_PRIMARY = 2;
+ TO_SUGGESTION1_WITH_EMPTY_PRIMARY = 3;
+ TO_SUGGESTION2_WITH_VALID_PRIMARY = 4;
+ TO_SUGGESTION2_WITH_EMPTY_PRIMARY = 5;
+ TO_SUGGESTION3_WITH_VALID_PRIMARY = 6;
+ TO_SUGGESTION3_WITH_EMPTY_PRIMARY = 7;
+ TO_EMPTY_WITH_VALID_SUGGESTIONS = 8;
+ TO_EMPTY_WITH_EMPTY_SUGGESTIONS = 9;
+ TO_EMPTY_WITH_SUGGESTIONS_DISABLED = 10;
+ TO_CUSTOM_WITH_VALID_SUGGESTIONS = 11;
+ TO_CUSTOM_WITH_EMPTY_SUGGESTIONS = 12;
+ TO_CUSTOM_WITH_SUGGESTIONS_DISABLED = 13;
+ UNCHANGED = 14;
+ }
}
// Used to define what type of item a Target would represent.
@@ -141,7 +170,8 @@
AUTOMATED = 1;
COMMAND = 2;
TIP = 3;
- // SOFT_KEYBOARD, HARD_KEYBOARD, ASSIST
+ SOFT_KEYBOARD = 4;
+ // HARD_KEYBOARD, ASSIST
}
enum Touch {
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 544efd5..1d315dd 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -16,9 +16,23 @@
package com.android.launcher3.folder;
+import static android.text.TextUtils.isEmpty;
+
+import static androidx.core.util.Preconditions.checkNotNull;
+
+import static com.android.launcher3.FolderInfo.FLAG_MANUAL_FOLDER_NAME;
import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
+import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_CUSTOM;
+import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_EMPTY;
+import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_FOLDER_LABEL_STATE_UNSPECIFIED;
+import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_SUGGESTED;
+
+import static java.util.Arrays.asList;
+import static java.util.Arrays.stream;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -75,22 +89,24 @@
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.logging.LoggerUtils;
import com.android.launcher3.pageindicators.PageIndicatorDots;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
+import com.android.launcher3.userevent.LauncherLogProto.Action;
+import com.android.launcher3.userevent.LauncherLogProto.ContainerType;
+import com.android.launcher3.userevent.LauncherLogProto.ItemType;
+import com.android.launcher3.userevent.LauncherLogProto.LauncherEvent;
+import com.android.launcher3.userevent.LauncherLogProto.Target;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.views.ClipPathView;
import com.android.launcher3.widget.PendingAddShortcutInfo;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
+import java.util.Optional;
import java.util.stream.Collectors;
+import java.util.stream.IntStream;
/**
* Represents a set of icons chosen by the user or generated by the system.
@@ -188,6 +204,9 @@
@Thunk int mScrollHintDir = SCROLL_NONE;
@Thunk int mCurrentScrollDir = SCROLL_NONE;
+ private String mPreviousLabel;
+ private boolean mIsPreviousLabelSuggested;
+
/**
* Used to inflate the Workspace from XML.
*
@@ -302,7 +321,7 @@
public void startEditingFolderName() {
post(() -> {
if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
- if (TextUtils.isEmpty(mFolderName.getText())) {
+ if (isEmpty(mFolderName.getText())) {
FolderNameInfo[] nameInfos =
(FolderNameInfo[]) mInfo.suggestedFolderNames.getParcelableArrayExtra(
FolderInfo.EXTRA_FOLDER_SUGGESTIONS);
@@ -326,7 +345,7 @@
}
mInfo.title = newTitle;
- mInfo.setOption(FolderInfo.FLAG_MANUAL_FOLDER_NAME, mFolderName.isEnteredCompose(),
+ mInfo.setOption(FLAG_MANUAL_FOLDER_NAME, mFolderName.isEnteredCompose(),
mLauncher.getModelWriter());
mFolderIcon.onTitleChanged(newTitle);
mLauncher.getModelWriter().updateItemInDatabase(mInfo);
@@ -337,7 +356,7 @@
// suggested, apply different hint.
mFolderName.setHint("");
} else {
- if (TextUtils.isEmpty(mInfo.title)) {
+ if (isEmpty(mInfo.title)) {
mFolderName.setHint(R.string.folder_hint_text);
mFolderName.setText("");
} else {
@@ -425,8 +444,10 @@
mItemsInvalidated = true;
mInfo.addListener(this);
- if (!TextUtils.isEmpty(mInfo.title)) {
+ if (!isEmpty(mInfo.title)) {
mFolderName.setText(mInfo.title);
+ mPreviousLabel = mInfo.title.toString();
+ mIsPreviousLabelSuggested = !mInfo.hasOption(FLAG_MANUAL_FOLDER_NAME);
mFolderName.setHint(null);
} else {
mFolderName.setText("");
@@ -452,8 +473,8 @@
if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
mInfo.suggestedFolderNames = new Intent().putExtra(FolderInfo.EXTRA_FOLDER_SUGGESTIONS,
nameInfos);
- if (TextUtils.isEmpty(mFolderName.getText().toString())
- && !mInfo.hasOption(FolderInfo.FLAG_MANUAL_FOLDER_NAME)) {
+ if (isEmpty(mFolderName.getText().toString())
+ && !mInfo.hasOption(FLAG_MANUAL_FOLDER_NAME)) {
showLabelSuggestion(nameInfos);
}
}
@@ -469,14 +490,14 @@
}
// Open the Folder and Keyboard when the first or second suggestion is valid non-empty
// string.
- boolean shouldOpen = nameInfos.length > 0 && nameInfos[0] != null && !TextUtils.isEmpty(
+ boolean shouldOpen = nameInfos.length > 0 && nameInfos[0] != null && !isEmpty(
nameInfos[0].getLabel())
- || nameInfos.length > 1 && nameInfos[1] != null && !TextUtils.isEmpty(
+ || nameInfos.length > 1 && nameInfos[1] != null && !isEmpty(
nameInfos[1].getLabel());
CharSequence firstLabel = nameInfos[0].getLabel();
if (shouldOpen) {
- if (!TextUtils.isEmpty(firstLabel)) {
+ if (!isEmpty(firstLabel)) {
mFolderName.setHint("");
mFolderName.setText(firstLabel);
mInfo.title = firstLabel;
@@ -484,7 +505,7 @@
animateOpen(mInfo.contents, 0, true);
mFolderName.showKeyboard();
mFolderName.displayCompletions(
- Arrays.asList(nameInfos).subList(1, nameInfos.length).stream()
+ asList(nameInfos).subList(1, nameInfos.length).stream()
.filter(Objects::nonNull)
.map(s -> s.getLabel().toString())
.collect(Collectors.toList()));
@@ -636,9 +657,9 @@
if (!skipUserEventLog) {
mLauncher.getUserEventDispatcher().logActionOnItem(
- Touch.TAP,
- Direction.NONE,
- ItemType.FOLDER_ICON, mInfo.cellX, mInfo.cellY);
+ LauncherLogProto.Action.Touch.TAP,
+ LauncherLogProto.Action.Direction.NONE,
+ LauncherLogProto.ItemType.FOLDER_ICON, mInfo.cellX, mInfo.cellY);
}
@@ -1420,6 +1441,7 @@
if (hasFocus) {
startEditingFolderName();
} else {
+ logEditFolderLabel();
mFolderName.dispatchBackKey();
}
}
@@ -1433,11 +1455,12 @@
}
@Override
- public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
+ public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target,
+ LauncherLogProto.Target targetParent) {
target.gridX = info.cellX;
target.gridY = info.cellY;
target.pageIndex = mContent.getCurrentPage();
- targetParent.containerType = ContainerType.FOLDER;
+ targetParent.containerType = LauncherLogProto.ContainerType.FOLDER;
}
private class OnScrollHintListener implements OnAlarmListener {
@@ -1535,7 +1558,7 @@
@Override
public int getLogContainerType() {
- return ContainerType.FOLDER;
+ return LauncherLogProto.ContainerType.FOLDER;
}
/**
@@ -1570,7 +1593,7 @@
}
} else {
mLauncher.getUserEventDispatcher().logActionTapOutside(
- LoggerUtils.newContainerTarget(ContainerType.FOLDER));
+ LoggerUtils.newContainerTarget(LauncherLogProto.ContainerType.FOLDER));
close(true);
return true;
}
@@ -1600,4 +1623,112 @@
super.draw(canvas);
}
}
+
+ private void logEditFolderLabel() {
+ LauncherEvent launcherEvent = LauncherEvent.newBuilder()
+ .setAction(Action.newBuilder().setType(Action.Type.SOFT_KEYBOARD))
+ .addSrcTarget(newEditTextTargetBuilder()
+ .setFromFolderLabelState(getFromFolderLabelState())
+ .setToFolderLabelState(getToFolderLabelState()))
+ .addSrcTarget(newFolderTargetBuilder())
+ .addSrcTarget(newParentContainerTarget())
+ .build();
+ mLauncher.getUserEventDispatcher().logLauncherEvent(launcherEvent);
+ mPreviousLabel = mFolderName.getText().toString();
+ mIsPreviousLabelSuggested = !mInfo.hasOption(FLAG_MANUAL_FOLDER_NAME);
+ }
+
+ private Target.FromFolderLabelState getFromFolderLabelState() {
+ return mPreviousLabel == null
+ ? FROM_FOLDER_LABEL_STATE_UNSPECIFIED
+ : mPreviousLabel.isEmpty()
+ ? FROM_EMPTY
+ : mIsPreviousLabelSuggested
+ ? FROM_SUGGESTED
+ : FROM_CUSTOM;
+ }
+
+ private Target.ToFolderLabelState getToFolderLabelState() {
+ String newLabel =
+ checkNotNull(mFolderName.getText().toString(),
+ "Expected valid folder label, but found null");
+
+ Optional<String[]> suggestedLabels = Optional.ofNullable(
+ (FolderNameInfo[]) mInfo.suggestedFolderNames
+ .getParcelableArrayExtra(FolderInfo.EXTRA_FOLDER_SUGGESTIONS))
+ .map(folderNameInfoArray ->
+ stream(folderNameInfoArray)
+ .map(FolderNameInfo::getLabel)
+ .map(CharSequence::toString)
+ .toArray(String[]::new));
+
+
+ int accepted_suggestion_index = suggestedLabels
+ .map(folderNameInfoArray ->
+ IntStream.range(0, folderNameInfoArray.length)
+ .filter(index -> newLabel.equalsIgnoreCase(
+ folderNameInfoArray[index]))
+ .findFirst()
+ .orElse(-1)
+ ).orElse(-1);
+
+ boolean hasValidPrimary = suggestedLabels
+ .map(labels -> labels.length > 0 && !isEmpty(labels[0]))
+ .orElse(false);
+ String primarySuffix = hasValidPrimary
+ ? "_WITH_VALID_PRIMARY"
+ : "_WITH_EMPTY_PRIMARY";
+
+ boolean isEmptySuggestions = suggestedLabels
+ .map(labels -> stream(labels).allMatch(TextUtils::isEmpty))
+ .orElse(true);
+ boolean isSuggestionsEnabled = FeatureFlags.FOLDER_NAME_SUGGEST.get();
+ String suggestionsSuffix = isSuggestionsEnabled
+ ? isEmptySuggestions
+ ? "_WITH_EMPTY_SUGGESTIONS"
+ : "_WITH_VALID_SUGGESTIONS"
+ : "_WITH_SUGGESTIONS_DISABLED";
+
+ return newLabel.equals(mPreviousLabel)
+ ? Target.ToFolderLabelState.UNCHANGED
+ : newLabel.isEmpty()
+ ? Target.ToFolderLabelState.valueOf("TO_EMPTY" + suggestionsSuffix)
+ : accepted_suggestion_index >= 0
+ ? Target.ToFolderLabelState.valueOf("TO_SUGGESTION"
+ + accepted_suggestion_index
+ + primarySuffix)
+ : Target.ToFolderLabelState.valueOf("TO_CUSTOM" + suggestionsSuffix);
+ }
+
+
+ private Target.Builder newEditTextTargetBuilder() {
+ return Target.newBuilder().setType(Target.Type.ITEM).setItemType(ItemType.EDITTEXT);
+ }
+
+ private Target.Builder newFolderTargetBuilder() {
+ return Target.newBuilder()
+ .setType(Target.Type.CONTAINER)
+ .setContainerType(ContainerType.FOLDER)
+ .setPageIndex(mInfo.screenId)
+ .setGridX(mInfo.cellX)
+ .setGridY(mInfo.cellY)
+ .setCardinality(mInfo.contents.size());
+ }
+
+ private Target.Builder newParentContainerTarget() {
+ Target.Builder builder = Target.newBuilder().setType(Target.Type.CONTAINER);
+
+ switch (mInfo.container) {
+ case CONTAINER_HOTSEAT:
+ return builder.setContainerType(ContainerType.HOTSEAT);
+ case CONTAINER_DESKTOP:
+ return builder.setContainerType(ContainerType.WORKSPACE);
+ default:
+ throw new AssertionError(String
+ .format("Expected container to be either %s or %s but found %s.",
+ CONTAINER_HOTSEAT,
+ CONTAINER_DESKTOP,
+ mInfo.container));
+ }
+ }
}
diff --git a/src/com/android/launcher3/logging/UserEventDispatcher.java b/src/com/android/launcher3/logging/UserEventDispatcher.java
index 8289da9..199d13f 100644
--- a/src/com/android/launcher3/logging/UserEventDispatcher.java
+++ b/src/com/android/launcher3/logging/UserEventDispatcher.java
@@ -69,8 +69,7 @@
public class UserEventDispatcher implements ResourceBasedOverride {
private static final String TAG = "UserEvent";
- private static final boolean IS_VERBOSE =
- FeatureFlags.IS_DOGFOOD_BUILD && Utilities.isPropertyEnabled(LogConfig.USEREVENT);
+ private static final boolean IS_VERBOSE = Utilities.isPropertyEnabled(LogConfig.USEREVENT);
private static final String UUID_STORAGE = "uuid";
public static UserEventDispatcher newInstance(Context context,
@@ -372,6 +371,25 @@
dispatchUserEvent(event, null);
}
+ /**
+ * Logs proto lite version of LauncherEvent object to clearcut.
+ */
+ public void logLauncherEvent(
+ com.android.launcher3.userevent.LauncherLogProto.LauncherEvent launcherEvent) {
+
+ if (mPreviousHomeGesture) {
+ mPreviousHomeGesture = false;
+ }
+ mAppOrTaskLaunch = false;
+ launcherEvent.toBuilder()
+ .setElapsedContainerMillis(SystemClock.uptimeMillis() - mElapsedContainerMillis)
+ .setElapsedSessionMillis(SystemClock.uptimeMillis() - mElapsedSessionMillis).build();
+ if (!IS_VERBOSE) {
+ return;
+ }
+ Log.d(TAG, launcherEvent.toString());
+ }
+
public void logDeepShortcutsOpen(View icon) {
LogContainerProvider provider = StatsLogUtils.getLaunchProviderRecursive(icon);
if (icon == null || !(icon.getTag() instanceof ItemInfo || provider == null)) {
diff --git a/tests/Android.mk b/tests/Android.mk
index d1a6c06..a9fff8e 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -51,7 +51,7 @@
androidx.test.rules \
androidx.test.uiautomator_uiautomator \
mockito-target-minus-junit4 \
- launcher-log-protos-lite
+ launcher_log_protos_lite
ifneq (,$(wildcard frameworks/base))
LOCAL_PRIVATE_PLATFORM_APIS := true