Loading protos/launcher_atom.proto +78 −0 Original line number Diff line number Diff line Loading @@ -95,7 +95,17 @@ message Task { // Represents folder in a closed state. message FolderIcon { // Number of items inside folder. optional int32 cardinality = 1; // State of the folder label before the event. optional FromState from_state = 2; // State of the folder label after the event. optional ToState to_state = 3; // Populated only when folder label was suggested. optional string label = 4; } ////////////////////////////////////////////// Loading @@ -120,3 +130,71 @@ message FolderContainer { HotseatContainer hotseat = 5; } } // Represents state of FolderLabel before editing. enum FromState { // Default value. FROM_STATE_UNSPECIFIED = 0; // FolderLabel was empty. FROM_EMPTY = 1; // FolderLabel was non-empty and manually entered by the user. FROM_CUSTOM = 2; // FolderLabel was non-empty and one of the suggestions. FROM_SUGGESTED = 3; } // Represents state of FolderLabel after editing. enum ToState { // Default value. TO_STATE_UNSPECIFIED = 0; // User attempted to change the folder label, but was not changed. UNCHANGED = 1; // New label matches with primary(aka top) suggestion. TO_SUGGESTION0 = 2; // New label matches with second top suggestion even though the top suggestion was non-empty. TO_SUGGESTION1_WITH_VALID_PRIMARY = 3; // New label matches with second top suggestion given that top suggestion was empty. TO_SUGGESTION1_WITH_EMPTY_PRIMARY = 4; // New label matches with third top suggestion even though the top suggestion was non-empty. TO_SUGGESTION2_WITH_VALID_PRIMARY = 5; // New label matches with third top suggestion given that top suggestion was empty. TO_SUGGESTION2_WITH_EMPTY_PRIMARY = 6; // New label matches with 4th top suggestion even though the top suggestion was non-empty. TO_SUGGESTION3_WITH_VALID_PRIMARY = 7; // New label matches with 4th top suggestion given that top suggestion was empty. TO_SUGGESTION3_WITH_EMPTY_PRIMARY = 8; // New label is empty even though the top suggestion was non-empty. TO_EMPTY_WITH_VALID_PRIMARY = 9; // New label is empty given that top suggestion was empty. TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY = 10; // New label is empty given that no suggestions were provided. TO_EMPTY_WITH_EMPTY_SUGGESTIONS = 11; // New label is empty given that suggestions feature was disabled. TO_EMPTY_WITH_SUGGESTIONS_DISABLED = 12; // New label is non-empty and does not match with any of the suggestions even though the top suggestion was non-empty. TO_CUSTOM_WITH_VALID_PRIMARY = 13; // New label is non-empty and not match with any suggestions given that top suggestion was empty. TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY = 14; // New label is non-empty and also no suggestions were provided. TO_CUSTOM_WITH_EMPTY_SUGGESTIONS = 15; // New label is non-empty and also suggestions feature was disable. TO_CUSTOM_WITH_SUGGESTIONS_DISABLED = 16; } src/com/android/launcher3/folder/Folder.java +2 −168 Original line number Diff line number Diff line Loading @@ -18,23 +18,15 @@ package com.android.launcher3.folder; import static android.text.TextUtils.isEmpty; import static androidx.core.util.Preconditions.checkNotNull; 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.config.FeatureFlags.ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS; import static com.android.launcher3.logging.LoggerUtils.newContainerTarget; import static com.android.launcher3.model.data.FolderInfo.FLAG_MANUAL_FOLDER_NAME; 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 static java.util.Optional.ofNullable; import android.animation.Animator; Loading Loading @@ -94,12 +86,6 @@ import com.android.launcher3.model.data.FolderInfo.FolderListener; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pageindicators.PageIndicatorDots; 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.LauncherLogProto.Target.ToFolderLabelState; import com.android.launcher3.userevent.nano.LauncherLogProto; import com.android.launcher3.util.Executors; import com.android.launcher3.util.Thunk; Loading @@ -111,10 +97,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; import java.util.stream.Collectors; import java.util.stream.IntStream; /** * Represents a set of icons chosen by the user or generated by the system. Loading Loading @@ -213,8 +196,6 @@ 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. Loading Loading @@ -348,9 +329,9 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo if (DEBUG) { Log.d(TAG, "onBackKey newTitle=" + newTitle); } mInfo.previousTitle = mInfo.title; mInfo.title = newTitle; mInfo.setOption(FLAG_MANUAL_FOLDER_NAME, !getAcceptedSuggestionIndex().isPresent(), mInfo.setOption(FLAG_MANUAL_FOLDER_NAME, !mInfo.getAcceptedSuggestionIndex().isPresent(), mLauncher.getModelWriter()); mFolderIcon.onTitleChanged(newTitle); mLauncher.getModelWriter().updateItemInDatabase(mInfo); Loading Loading @@ -441,8 +422,6 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } mItemsInvalidated = true; mInfo.addListener(this); Optional.ofNullable(mInfo.title).ifPresent(title -> mPreviousLabel = title.toString()); mIsPreviousLabelSuggested = !mInfo.hasOption(FLAG_MANUAL_FOLDER_NAME); if (!isEmpty(mInfo.title)) { mFolderName.setText(mInfo.title); Loading Loading @@ -1455,7 +1434,6 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo if (hasFocus) { startEditingFolderName(); } else { logCurrentFolderLabelState(); mFolderName.dispatchBackKey(); } } Loading Loading @@ -1653,148 +1631,4 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo public FolderPagedView getContent() { return mContent; } protected void logCurrentFolderLabelState() { 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"); if (newLabel.equals(mPreviousLabel)) { return Target.ToFolderLabelState.UNCHANGED; } if (!FeatureFlags.FOLDER_NAME_SUGGEST.get()) { return newLabel.isEmpty() ? ToFolderLabelState.TO_EMPTY_WITH_SUGGESTIONS_DISABLED : ToFolderLabelState.TO_CUSTOM_WITH_SUGGESTIONS_DISABLED; } Optional<String[]> suggestedLabels = getSuggestedLabels(); boolean isEmptySuggestions = suggestedLabels .map(labels -> stream(labels).allMatch(TextUtils::isEmpty)) .orElse(true); if (isEmptySuggestions) { return newLabel.isEmpty() ? ToFolderLabelState.TO_EMPTY_WITH_EMPTY_SUGGESTIONS : ToFolderLabelState.TO_CUSTOM_WITH_EMPTY_SUGGESTIONS; } boolean hasValidPrimary = suggestedLabels .map(labels -> !isEmpty(labels[0])) .orElse(false); if (newLabel.isEmpty()) { return hasValidPrimary ? ToFolderLabelState.TO_EMPTY_WITH_VALID_PRIMARY : ToFolderLabelState.TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY; } OptionalInt accepted_suggestion_index = getAcceptedSuggestionIndex(); if (!accepted_suggestion_index.isPresent()) { return hasValidPrimary ? ToFolderLabelState.TO_CUSTOM_WITH_VALID_PRIMARY : ToFolderLabelState.TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY; } switch (accepted_suggestion_index.getAsInt()) { case 0: return ToFolderLabelState.TO_SUGGESTION0_WITH_VALID_PRIMARY; case 1: return hasValidPrimary ? ToFolderLabelState.TO_SUGGESTION1_WITH_VALID_PRIMARY : ToFolderLabelState.TO_SUGGESTION1_WITH_EMPTY_PRIMARY; case 2: return hasValidPrimary ? ToFolderLabelState.TO_SUGGESTION2_WITH_VALID_PRIMARY : ToFolderLabelState.TO_SUGGESTION2_WITH_EMPTY_PRIMARY; case 3: return hasValidPrimary ? ToFolderLabelState.TO_SUGGESTION3_WITH_VALID_PRIMARY : ToFolderLabelState.TO_SUGGESTION3_WITH_EMPTY_PRIMARY; default: // fall through } return ToFolderLabelState.TO_FOLDER_LABEL_STATE_UNSPECIFIED; } private Optional<String[]> getSuggestedLabels() { return ofNullable(mInfo) .map(info -> info.suggestedFolderNames) .map( folderNames -> (FolderNameInfo[]) folderNames.getParcelableArrayExtra(FolderInfo.EXTRA_FOLDER_SUGGESTIONS)) .map( folderNameInfoArray -> stream(folderNameInfoArray) .filter(Objects::nonNull) .map(FolderNameInfo::getLabel) .filter(Objects::nonNull) .map(CharSequence::toString) .toArray(String[]::new)); } private OptionalInt getAcceptedSuggestionIndex() { String newLabel = checkNotNull(mFolderName.getText().toString(), "Expected valid folder label, but found null"); return getSuggestedLabels() .map(suggestionsArray -> IntStream.range(0, suggestionsArray.length) .filter( index -> !isEmpty(suggestionsArray[index]) && newLabel.equalsIgnoreCase(suggestionsArray[index])) .sequential() .findFirst() ).orElse(OptionalInt.empty()); } 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)); } } } src/com/android/launcher3/folder/FolderIcon.java +11 −6 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import static android.text.TextUtils.isEmpty; import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW; import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_LABEL_CHANGED; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; Loading Loading @@ -62,6 +63,8 @@ import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.dragndrop.DragView; import com.android.launcher3.dragndrop.DraggableView; import com.android.launcher3.icons.DotRenderer; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.FolderInfo.FolderListener; Loading Loading @@ -410,10 +413,10 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel Executors.UI_HELPER_EXECUTOR.post(() -> { d.folderNameProvider.getSuggestedFolderName( getContext(), mInfo.contents, nameInfos); showFinalView(finalIndex, item, nameInfos); showFinalView(finalIndex, item, nameInfos, d.logInstanceId); }); } else { showFinalView(finalIndex, item, nameInfos); showFinalView(finalIndex, item, nameInfos, d.logInstanceId); } } else { addItem(item); Loading @@ -421,12 +424,11 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel } private void showFinalView(int finalIndex, final WorkspaceItemInfo item, FolderNameInfo[] nameInfos) { FolderNameInfo[] nameInfos, InstanceId instanceId) { postDelayed(() -> { mPreviewItemManager.hidePreviewItem(finalIndex, false); mFolder.showItem(item); setLabelSuggestion(nameInfos); mFolder.logCurrentFolderLabelState(); setLabelSuggestion(nameInfos, instanceId); invalidate(); }, DROP_IN_ANIMATION_DURATION); } Loading @@ -434,7 +436,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel /** * Set the suggested folder name. */ public void setLabelSuggestion(FolderNameInfo[] nameInfos) { public void setLabelSuggestion(FolderNameInfo[] nameInfos, InstanceId instanceId) { if (!FeatureFlags.FOLDER_NAME_SUGGEST.get()) { return; } Loading @@ -445,7 +447,10 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel if (nameInfos == null || nameInfos[0] == null || isEmpty(nameInfos[0].getLabel())) { return; } mInfo.previousTitle = mInfo.title; mInfo.title = nameInfos[0].getLabel(); StatsLogManager.newInstance(getContext()) .log(LAUNCHER_FOLDER_LABEL_CHANGED, instanceId, mInfo.getFolderIconAtom()); onTitleChanged(mInfo.title); mFolder.mFolderName.setText(mInfo.title); mFolder.mLauncher.getModelWriter().updateItemInDatabase(mInfo); Loading src/com/android/launcher3/logging/StatsLogManager.java +4 −0 Original line number Diff line number Diff line Loading @@ -57,6 +57,10 @@ public class StatsLogManager implements ResourceBasedOverride { + "resulting in a new folder creation") LAUNCHER_ITEM_DROP_FOLDER_CREATED(386), @LauncherUiEvent(doc = "A dragged launcher item is successfully dropped on another item " + "resulting in new folder creation") LAUNCHER_FOLDER_LABEL_CHANGED(460), @LauncherUiEvent(doc = "A dragged item is dropped on 'Remove' button in the target bar") LAUNCHER_ITEM_DROPPED_ON_REMOVE(465), Loading src/com/android/launcher3/model/data/FolderInfo.java +140 −0 Original line number Diff line number Diff line Loading @@ -16,16 +16,31 @@ package com.android.launcher3.model.data; import static android.text.TextUtils.isEmpty; import static androidx.core.util.Preconditions.checkNotNull; import static java.util.Arrays.stream; import static java.util.Optional.ofNullable; import android.content.Intent; import android.os.Process; import android.text.TextUtils; import com.android.launcher3.LauncherSettings; import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.folder.FolderNameInfo; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.model.ModelWriter; import com.android.launcher3.util.ContentWriter; import java.util.ArrayList; import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; import java.util.stream.IntStream; /** * Represents a folder containing shortcuts or apps. Loading Loading @@ -57,6 +72,10 @@ public class FolderInfo extends ItemInfo { public Intent suggestedFolderNames; // When title changes, previous title is stored. // Primarily used for logging purpose. public CharSequence previousTitle; /** * The apps and shortcuts */ Loading Loading @@ -172,4 +191,125 @@ public class FolderInfo extends ItemInfo { folderInfo.contents = this.contents; return folderInfo; } /** * Returns {@link LauncherAtom.FolderIcon} wrapped as {@link LauncherAtom.ItemInfo} for logging * into Westworld. * */ public LauncherAtom.ItemInfo getFolderIconAtom() { LauncherAtom.ToState toFolderLabelState = getToFolderLabelState(); LauncherAtom.FolderIcon.Builder folderIconBuilder = LauncherAtom.FolderIcon.newBuilder() .setCardinality(contents.size()) .setFromState(getFromFolderLabelState()) .setToState(toFolderLabelState); if (toFolderLabelState.toString().startsWith("TO_SUGGESTION")) { folderIconBuilder.setLabel(title.toString()); } return getDefaultItemInfoBuilder() .setFolderIcon(folderIconBuilder) .setContainerInfo(getContainerInfo()) .build(); } /** * Returns index of the accepted suggestion. */ public OptionalInt getAcceptedSuggestionIndex() { String newLabel = checkNotNull(title, "Expected valid folder label, but found null").toString(); return getSuggestedLabels() .map(suggestionsArray -> IntStream.range(0, suggestionsArray.length) .filter( index -> !isEmpty(suggestionsArray[index]) && newLabel.equalsIgnoreCase( suggestionsArray[index])) .sequential() .findFirst() ).orElse(OptionalInt.empty()); } private LauncherAtom.ToState getToFolderLabelState() { if (title == null) { return LauncherAtom.ToState.TO_STATE_UNSPECIFIED; } if (title.equals(previousTitle)) { return LauncherAtom.ToState.UNCHANGED; } if (!FeatureFlags.FOLDER_NAME_SUGGEST.get()) { return title.length() > 0 ? LauncherAtom.ToState.TO_CUSTOM_WITH_SUGGESTIONS_DISABLED : LauncherAtom.ToState.TO_EMPTY_WITH_SUGGESTIONS_DISABLED; } Optional<String[]> suggestedLabels = getSuggestedLabels(); boolean isEmptySuggestions = suggestedLabels .map(labels -> stream(labels).allMatch(TextUtils::isEmpty)) .orElse(true); if (isEmptySuggestions) { return title.length() > 0 ? LauncherAtom.ToState.TO_CUSTOM_WITH_EMPTY_SUGGESTIONS : LauncherAtom.ToState.TO_EMPTY_WITH_EMPTY_SUGGESTIONS; } boolean hasValidPrimary = suggestedLabels .map(labels -> !isEmpty(labels[0])) .orElse(false); if (title.length() == 0) { return hasValidPrimary ? LauncherAtom.ToState.TO_EMPTY_WITH_VALID_PRIMARY : LauncherAtom.ToState.TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY; } OptionalInt accepted_suggestion_index = getAcceptedSuggestionIndex(); if (!accepted_suggestion_index.isPresent()) { return hasValidPrimary ? LauncherAtom.ToState.TO_CUSTOM_WITH_VALID_PRIMARY : LauncherAtom.ToState.TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY; } switch (accepted_suggestion_index.getAsInt()) { case 0: return LauncherAtom.ToState.TO_SUGGESTION0; case 1: return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION1_WITH_VALID_PRIMARY : LauncherAtom.ToState.TO_SUGGESTION1_WITH_EMPTY_PRIMARY; case 2: return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION2_WITH_VALID_PRIMARY : LauncherAtom.ToState.TO_SUGGESTION2_WITH_EMPTY_PRIMARY; case 3: return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION3_WITH_VALID_PRIMARY : LauncherAtom.ToState.TO_SUGGESTION3_WITH_EMPTY_PRIMARY; default: // fall through } return LauncherAtom.ToState.TO_STATE_UNSPECIFIED; } private LauncherAtom.FromState getFromFolderLabelState() { return previousTitle == null ? LauncherAtom.FromState.FROM_STATE_UNSPECIFIED : previousTitle.toString().isEmpty() ? LauncherAtom.FromState.FROM_EMPTY : hasOption(FLAG_MANUAL_FOLDER_NAME) ? LauncherAtom.FromState.FROM_CUSTOM : LauncherAtom.FromState.FROM_SUGGESTED; } private Optional<String[]> getSuggestedLabels() { return ofNullable(suggestedFolderNames) .map(folderNames -> (FolderNameInfo[]) folderNames.getParcelableArrayExtra(EXTRA_FOLDER_SUGGESTIONS)) .map(folderNameInfoArray -> stream(folderNameInfoArray) .filter(Objects::nonNull) .map(FolderNameInfo::getLabel) .filter(Objects::nonNull) .map(CharSequence::toString) .toArray(String[]::new)); } } Loading
protos/launcher_atom.proto +78 −0 Original line number Diff line number Diff line Loading @@ -95,7 +95,17 @@ message Task { // Represents folder in a closed state. message FolderIcon { // Number of items inside folder. optional int32 cardinality = 1; // State of the folder label before the event. optional FromState from_state = 2; // State of the folder label after the event. optional ToState to_state = 3; // Populated only when folder label was suggested. optional string label = 4; } ////////////////////////////////////////////// Loading @@ -120,3 +130,71 @@ message FolderContainer { HotseatContainer hotseat = 5; } } // Represents state of FolderLabel before editing. enum FromState { // Default value. FROM_STATE_UNSPECIFIED = 0; // FolderLabel was empty. FROM_EMPTY = 1; // FolderLabel was non-empty and manually entered by the user. FROM_CUSTOM = 2; // FolderLabel was non-empty and one of the suggestions. FROM_SUGGESTED = 3; } // Represents state of FolderLabel after editing. enum ToState { // Default value. TO_STATE_UNSPECIFIED = 0; // User attempted to change the folder label, but was not changed. UNCHANGED = 1; // New label matches with primary(aka top) suggestion. TO_SUGGESTION0 = 2; // New label matches with second top suggestion even though the top suggestion was non-empty. TO_SUGGESTION1_WITH_VALID_PRIMARY = 3; // New label matches with second top suggestion given that top suggestion was empty. TO_SUGGESTION1_WITH_EMPTY_PRIMARY = 4; // New label matches with third top suggestion even though the top suggestion was non-empty. TO_SUGGESTION2_WITH_VALID_PRIMARY = 5; // New label matches with third top suggestion given that top suggestion was empty. TO_SUGGESTION2_WITH_EMPTY_PRIMARY = 6; // New label matches with 4th top suggestion even though the top suggestion was non-empty. TO_SUGGESTION3_WITH_VALID_PRIMARY = 7; // New label matches with 4th top suggestion given that top suggestion was empty. TO_SUGGESTION3_WITH_EMPTY_PRIMARY = 8; // New label is empty even though the top suggestion was non-empty. TO_EMPTY_WITH_VALID_PRIMARY = 9; // New label is empty given that top suggestion was empty. TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY = 10; // New label is empty given that no suggestions were provided. TO_EMPTY_WITH_EMPTY_SUGGESTIONS = 11; // New label is empty given that suggestions feature was disabled. TO_EMPTY_WITH_SUGGESTIONS_DISABLED = 12; // New label is non-empty and does not match with any of the suggestions even though the top suggestion was non-empty. TO_CUSTOM_WITH_VALID_PRIMARY = 13; // New label is non-empty and not match with any suggestions given that top suggestion was empty. TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY = 14; // New label is non-empty and also no suggestions were provided. TO_CUSTOM_WITH_EMPTY_SUGGESTIONS = 15; // New label is non-empty and also suggestions feature was disable. TO_CUSTOM_WITH_SUGGESTIONS_DISABLED = 16; }
src/com/android/launcher3/folder/Folder.java +2 −168 Original line number Diff line number Diff line Loading @@ -18,23 +18,15 @@ package com.android.launcher3.folder; import static android.text.TextUtils.isEmpty; import static androidx.core.util.Preconditions.checkNotNull; 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.config.FeatureFlags.ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS; import static com.android.launcher3.logging.LoggerUtils.newContainerTarget; import static com.android.launcher3.model.data.FolderInfo.FLAG_MANUAL_FOLDER_NAME; 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 static java.util.Optional.ofNullable; import android.animation.Animator; Loading Loading @@ -94,12 +86,6 @@ import com.android.launcher3.model.data.FolderInfo.FolderListener; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pageindicators.PageIndicatorDots; 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.LauncherLogProto.Target.ToFolderLabelState; import com.android.launcher3.userevent.nano.LauncherLogProto; import com.android.launcher3.util.Executors; import com.android.launcher3.util.Thunk; Loading @@ -111,10 +97,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; import java.util.stream.Collectors; import java.util.stream.IntStream; /** * Represents a set of icons chosen by the user or generated by the system. Loading Loading @@ -213,8 +196,6 @@ 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. Loading Loading @@ -348,9 +329,9 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo if (DEBUG) { Log.d(TAG, "onBackKey newTitle=" + newTitle); } mInfo.previousTitle = mInfo.title; mInfo.title = newTitle; mInfo.setOption(FLAG_MANUAL_FOLDER_NAME, !getAcceptedSuggestionIndex().isPresent(), mInfo.setOption(FLAG_MANUAL_FOLDER_NAME, !mInfo.getAcceptedSuggestionIndex().isPresent(), mLauncher.getModelWriter()); mFolderIcon.onTitleChanged(newTitle); mLauncher.getModelWriter().updateItemInDatabase(mInfo); Loading Loading @@ -441,8 +422,6 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } mItemsInvalidated = true; mInfo.addListener(this); Optional.ofNullable(mInfo.title).ifPresent(title -> mPreviousLabel = title.toString()); mIsPreviousLabelSuggested = !mInfo.hasOption(FLAG_MANUAL_FOLDER_NAME); if (!isEmpty(mInfo.title)) { mFolderName.setText(mInfo.title); Loading Loading @@ -1455,7 +1434,6 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo if (hasFocus) { startEditingFolderName(); } else { logCurrentFolderLabelState(); mFolderName.dispatchBackKey(); } } Loading Loading @@ -1653,148 +1631,4 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo public FolderPagedView getContent() { return mContent; } protected void logCurrentFolderLabelState() { 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"); if (newLabel.equals(mPreviousLabel)) { return Target.ToFolderLabelState.UNCHANGED; } if (!FeatureFlags.FOLDER_NAME_SUGGEST.get()) { return newLabel.isEmpty() ? ToFolderLabelState.TO_EMPTY_WITH_SUGGESTIONS_DISABLED : ToFolderLabelState.TO_CUSTOM_WITH_SUGGESTIONS_DISABLED; } Optional<String[]> suggestedLabels = getSuggestedLabels(); boolean isEmptySuggestions = suggestedLabels .map(labels -> stream(labels).allMatch(TextUtils::isEmpty)) .orElse(true); if (isEmptySuggestions) { return newLabel.isEmpty() ? ToFolderLabelState.TO_EMPTY_WITH_EMPTY_SUGGESTIONS : ToFolderLabelState.TO_CUSTOM_WITH_EMPTY_SUGGESTIONS; } boolean hasValidPrimary = suggestedLabels .map(labels -> !isEmpty(labels[0])) .orElse(false); if (newLabel.isEmpty()) { return hasValidPrimary ? ToFolderLabelState.TO_EMPTY_WITH_VALID_PRIMARY : ToFolderLabelState.TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY; } OptionalInt accepted_suggestion_index = getAcceptedSuggestionIndex(); if (!accepted_suggestion_index.isPresent()) { return hasValidPrimary ? ToFolderLabelState.TO_CUSTOM_WITH_VALID_PRIMARY : ToFolderLabelState.TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY; } switch (accepted_suggestion_index.getAsInt()) { case 0: return ToFolderLabelState.TO_SUGGESTION0_WITH_VALID_PRIMARY; case 1: return hasValidPrimary ? ToFolderLabelState.TO_SUGGESTION1_WITH_VALID_PRIMARY : ToFolderLabelState.TO_SUGGESTION1_WITH_EMPTY_PRIMARY; case 2: return hasValidPrimary ? ToFolderLabelState.TO_SUGGESTION2_WITH_VALID_PRIMARY : ToFolderLabelState.TO_SUGGESTION2_WITH_EMPTY_PRIMARY; case 3: return hasValidPrimary ? ToFolderLabelState.TO_SUGGESTION3_WITH_VALID_PRIMARY : ToFolderLabelState.TO_SUGGESTION3_WITH_EMPTY_PRIMARY; default: // fall through } return ToFolderLabelState.TO_FOLDER_LABEL_STATE_UNSPECIFIED; } private Optional<String[]> getSuggestedLabels() { return ofNullable(mInfo) .map(info -> info.suggestedFolderNames) .map( folderNames -> (FolderNameInfo[]) folderNames.getParcelableArrayExtra(FolderInfo.EXTRA_FOLDER_SUGGESTIONS)) .map( folderNameInfoArray -> stream(folderNameInfoArray) .filter(Objects::nonNull) .map(FolderNameInfo::getLabel) .filter(Objects::nonNull) .map(CharSequence::toString) .toArray(String[]::new)); } private OptionalInt getAcceptedSuggestionIndex() { String newLabel = checkNotNull(mFolderName.getText().toString(), "Expected valid folder label, but found null"); return getSuggestedLabels() .map(suggestionsArray -> IntStream.range(0, suggestionsArray.length) .filter( index -> !isEmpty(suggestionsArray[index]) && newLabel.equalsIgnoreCase(suggestionsArray[index])) .sequential() .findFirst() ).orElse(OptionalInt.empty()); } 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)); } } }
src/com/android/launcher3/folder/FolderIcon.java +11 −6 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import static android.text.TextUtils.isEmpty; import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW; import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_LABEL_CHANGED; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; Loading Loading @@ -62,6 +63,8 @@ import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.dragndrop.DragView; import com.android.launcher3.dragndrop.DraggableView; import com.android.launcher3.icons.DotRenderer; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.FolderInfo.FolderListener; Loading Loading @@ -410,10 +413,10 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel Executors.UI_HELPER_EXECUTOR.post(() -> { d.folderNameProvider.getSuggestedFolderName( getContext(), mInfo.contents, nameInfos); showFinalView(finalIndex, item, nameInfos); showFinalView(finalIndex, item, nameInfos, d.logInstanceId); }); } else { showFinalView(finalIndex, item, nameInfos); showFinalView(finalIndex, item, nameInfos, d.logInstanceId); } } else { addItem(item); Loading @@ -421,12 +424,11 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel } private void showFinalView(int finalIndex, final WorkspaceItemInfo item, FolderNameInfo[] nameInfos) { FolderNameInfo[] nameInfos, InstanceId instanceId) { postDelayed(() -> { mPreviewItemManager.hidePreviewItem(finalIndex, false); mFolder.showItem(item); setLabelSuggestion(nameInfos); mFolder.logCurrentFolderLabelState(); setLabelSuggestion(nameInfos, instanceId); invalidate(); }, DROP_IN_ANIMATION_DURATION); } Loading @@ -434,7 +436,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel /** * Set the suggested folder name. */ public void setLabelSuggestion(FolderNameInfo[] nameInfos) { public void setLabelSuggestion(FolderNameInfo[] nameInfos, InstanceId instanceId) { if (!FeatureFlags.FOLDER_NAME_SUGGEST.get()) { return; } Loading @@ -445,7 +447,10 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel if (nameInfos == null || nameInfos[0] == null || isEmpty(nameInfos[0].getLabel())) { return; } mInfo.previousTitle = mInfo.title; mInfo.title = nameInfos[0].getLabel(); StatsLogManager.newInstance(getContext()) .log(LAUNCHER_FOLDER_LABEL_CHANGED, instanceId, mInfo.getFolderIconAtom()); onTitleChanged(mInfo.title); mFolder.mFolderName.setText(mInfo.title); mFolder.mLauncher.getModelWriter().updateItemInDatabase(mInfo); Loading
src/com/android/launcher3/logging/StatsLogManager.java +4 −0 Original line number Diff line number Diff line Loading @@ -57,6 +57,10 @@ public class StatsLogManager implements ResourceBasedOverride { + "resulting in a new folder creation") LAUNCHER_ITEM_DROP_FOLDER_CREATED(386), @LauncherUiEvent(doc = "A dragged launcher item is successfully dropped on another item " + "resulting in new folder creation") LAUNCHER_FOLDER_LABEL_CHANGED(460), @LauncherUiEvent(doc = "A dragged item is dropped on 'Remove' button in the target bar") LAUNCHER_ITEM_DROPPED_ON_REMOVE(465), Loading
src/com/android/launcher3/model/data/FolderInfo.java +140 −0 Original line number Diff line number Diff line Loading @@ -16,16 +16,31 @@ package com.android.launcher3.model.data; import static android.text.TextUtils.isEmpty; import static androidx.core.util.Preconditions.checkNotNull; import static java.util.Arrays.stream; import static java.util.Optional.ofNullable; import android.content.Intent; import android.os.Process; import android.text.TextUtils; import com.android.launcher3.LauncherSettings; import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.folder.FolderNameInfo; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.model.ModelWriter; import com.android.launcher3.util.ContentWriter; import java.util.ArrayList; import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; import java.util.stream.IntStream; /** * Represents a folder containing shortcuts or apps. Loading Loading @@ -57,6 +72,10 @@ public class FolderInfo extends ItemInfo { public Intent suggestedFolderNames; // When title changes, previous title is stored. // Primarily used for logging purpose. public CharSequence previousTitle; /** * The apps and shortcuts */ Loading Loading @@ -172,4 +191,125 @@ public class FolderInfo extends ItemInfo { folderInfo.contents = this.contents; return folderInfo; } /** * Returns {@link LauncherAtom.FolderIcon} wrapped as {@link LauncherAtom.ItemInfo} for logging * into Westworld. * */ public LauncherAtom.ItemInfo getFolderIconAtom() { LauncherAtom.ToState toFolderLabelState = getToFolderLabelState(); LauncherAtom.FolderIcon.Builder folderIconBuilder = LauncherAtom.FolderIcon.newBuilder() .setCardinality(contents.size()) .setFromState(getFromFolderLabelState()) .setToState(toFolderLabelState); if (toFolderLabelState.toString().startsWith("TO_SUGGESTION")) { folderIconBuilder.setLabel(title.toString()); } return getDefaultItemInfoBuilder() .setFolderIcon(folderIconBuilder) .setContainerInfo(getContainerInfo()) .build(); } /** * Returns index of the accepted suggestion. */ public OptionalInt getAcceptedSuggestionIndex() { String newLabel = checkNotNull(title, "Expected valid folder label, but found null").toString(); return getSuggestedLabels() .map(suggestionsArray -> IntStream.range(0, suggestionsArray.length) .filter( index -> !isEmpty(suggestionsArray[index]) && newLabel.equalsIgnoreCase( suggestionsArray[index])) .sequential() .findFirst() ).orElse(OptionalInt.empty()); } private LauncherAtom.ToState getToFolderLabelState() { if (title == null) { return LauncherAtom.ToState.TO_STATE_UNSPECIFIED; } if (title.equals(previousTitle)) { return LauncherAtom.ToState.UNCHANGED; } if (!FeatureFlags.FOLDER_NAME_SUGGEST.get()) { return title.length() > 0 ? LauncherAtom.ToState.TO_CUSTOM_WITH_SUGGESTIONS_DISABLED : LauncherAtom.ToState.TO_EMPTY_WITH_SUGGESTIONS_DISABLED; } Optional<String[]> suggestedLabels = getSuggestedLabels(); boolean isEmptySuggestions = suggestedLabels .map(labels -> stream(labels).allMatch(TextUtils::isEmpty)) .orElse(true); if (isEmptySuggestions) { return title.length() > 0 ? LauncherAtom.ToState.TO_CUSTOM_WITH_EMPTY_SUGGESTIONS : LauncherAtom.ToState.TO_EMPTY_WITH_EMPTY_SUGGESTIONS; } boolean hasValidPrimary = suggestedLabels .map(labels -> !isEmpty(labels[0])) .orElse(false); if (title.length() == 0) { return hasValidPrimary ? LauncherAtom.ToState.TO_EMPTY_WITH_VALID_PRIMARY : LauncherAtom.ToState.TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY; } OptionalInt accepted_suggestion_index = getAcceptedSuggestionIndex(); if (!accepted_suggestion_index.isPresent()) { return hasValidPrimary ? LauncherAtom.ToState.TO_CUSTOM_WITH_VALID_PRIMARY : LauncherAtom.ToState.TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY; } switch (accepted_suggestion_index.getAsInt()) { case 0: return LauncherAtom.ToState.TO_SUGGESTION0; case 1: return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION1_WITH_VALID_PRIMARY : LauncherAtom.ToState.TO_SUGGESTION1_WITH_EMPTY_PRIMARY; case 2: return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION2_WITH_VALID_PRIMARY : LauncherAtom.ToState.TO_SUGGESTION2_WITH_EMPTY_PRIMARY; case 3: return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION3_WITH_VALID_PRIMARY : LauncherAtom.ToState.TO_SUGGESTION3_WITH_EMPTY_PRIMARY; default: // fall through } return LauncherAtom.ToState.TO_STATE_UNSPECIFIED; } private LauncherAtom.FromState getFromFolderLabelState() { return previousTitle == null ? LauncherAtom.FromState.FROM_STATE_UNSPECIFIED : previousTitle.toString().isEmpty() ? LauncherAtom.FromState.FROM_EMPTY : hasOption(FLAG_MANUAL_FOLDER_NAME) ? LauncherAtom.FromState.FROM_CUSTOM : LauncherAtom.FromState.FROM_SUGGESTED; } private Optional<String[]> getSuggestedLabels() { return ofNullable(suggestedFolderNames) .map(folderNames -> (FolderNameInfo[]) folderNames.getParcelableArrayExtra(EXTRA_FOLDER_SUGGESTIONS)) .map(folderNameInfoArray -> stream(folderNameInfoArray) .filter(Objects::nonNull) .map(FolderNameInfo::getLabel) .filter(Objects::nonNull) .map(CharSequence::toString) .toArray(String[]::new)); } }