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