From c1c2bfa7fb20c67cacf70ee42ef7294da962c531 Mon Sep 17 00:00:00 2001 From: thiruram Date: Tue, 4 Feb 2020 18:56:40 -0800 Subject: [PATCH] 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 --- Android.bp | 3 +- Android.mk | 14 +- protos/launcher_log.proto | 32 +++- src/com/android/launcher3/folder/Folder.java | 177 +++++++++++++++--- .../logging/UserEventDispatcher.java | 22 ++- tests/Android.mk | 2 +- 6 files changed, 219 insertions(+), 31 deletions(-) diff --git a/Android.bp b/Android.bp index fc998809e1..cb695df241 100644 --- a/Android.bp +++ b/Android.bp @@ -32,7 +32,7 @@ android_library { } java_library_static { - name: "launcher-log-protos-lite", + name: "launcher_log_protos_lite", srcs: [ "protos/*.proto", "proto_overrides/*.proto", @@ -45,4 +45,5 @@ java_library_static { "proto_overrides", ], }, + static_libs: ["libprotobuf-java-lite"], } diff --git a/Android.mk b/Android.mk index 66ccae038f..c066a12aeb 100644 --- a/Android.mk +++ b/Android.mk @@ -48,7 +48,9 @@ LOCAL_STATIC_ANDROID_LIBRARIES := \ 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_USE_AAPT2 := true 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 @@ include $(CLEAR_VARS) 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 3c7f308f5d..ec1d55b753 100644 --- a/protos/launcher_log.proto +++ b/protos/launcher_log.proto @@ -58,6 +58,35 @@ message Target { 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 @@ message Action { 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 544efd53d7..1d315ddd79 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.DragLayer; 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 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo @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 class Folder extends AbstractFloatingView implements ClipPathView, DragSo 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 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } 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 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo // 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 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo 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 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo 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 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } // 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 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo 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 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo 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 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo if (hasFocus) { startEditingFolderName(); } else { + logEditFolderLabel(); mFolderName.dispatchBackKey(); } } @@ -1433,11 +1455,12 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } @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 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo @Override public int getLogContainerType() { - return ContainerType.FOLDER; + return LauncherLogProto.ContainerType.FOLDER; } /** @@ -1570,7 +1593,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } } else { mLauncher.getUserEventDispatcher().logActionTapOutside( - LoggerUtils.newContainerTarget(ContainerType.FOLDER)); + LoggerUtils.newContainerTarget(LauncherLogProto.ContainerType.FOLDER)); close(true); return true; } @@ -1600,4 +1623,112 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo 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 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 8289da942b..199d13f747 100644 --- a/src/com/android/launcher3/logging/UserEventDispatcher.java +++ b/src/com/android/launcher3/logging/UserEventDispatcher.java @@ -69,8 +69,7 @@ import java.util.UUID; 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 @@ public class UserEventDispatcher implements ResourceBasedOverride { 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 d1a6c063f2..a9fff8ef97 100644 --- a/tests/Android.mk +++ b/tests/Android.mk @@ -51,7 +51,7 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ 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