diff --git a/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java index f7690550f3..74ef55eb68 100644 --- a/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java +++ b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java @@ -15,7 +15,7 @@ */ package com.android.launcher3.folder; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; import android.content.ComponentName; import android.content.Context; @@ -61,15 +61,16 @@ public final class FolderNameProviderTest { ArrayList list = new ArrayList<>(); list.add(mItem1); list.add(mItem2); - String[] suggestedNameOut = new String[FolderNameProvider.SUGGEST_MAX]; - new FolderNameProvider().getSuggestedFolderName(mContext, list, suggestedNameOut); - assertTrue(suggestedNameOut[0].equals("Work")); + FolderNameInfo[] nameInfos = + new FolderNameInfo[FolderNameProvider.SUGGEST_MAX]; + new FolderNameProvider().getSuggestedFolderName(mContext, list, nameInfos); + assertEquals("Work", nameInfos[0].getLabel()); - suggestedNameOut[0] = "candidate1"; - suggestedNameOut[1] = "candidate2"; - suggestedNameOut[2] = "candidate3"; - new FolderNameProvider().getSuggestedFolderName(mContext, list, suggestedNameOut); - assertTrue(suggestedNameOut[3].equals("Work")); + nameInfos[0] = new FolderNameInfo("candidate1", 0.9); + nameInfos[1] = new FolderNameInfo("candidate2", 0.8); + nameInfos[2] = new FolderNameInfo("candidate3", 0.7); + new FolderNameProvider().getSuggestedFolderName(mContext, list, nameInfos); + assertEquals("Work", nameInfos[3].getLabel()); } } diff --git a/src/com/android/launcher3/FolderInfo.java b/src/com/android/launcher3/FolderInfo.java index 787eee1483..336e423373 100644 --- a/src/com/android/launcher3/FolderInfo.java +++ b/src/com/android/launcher3/FolderInfo.java @@ -48,6 +48,8 @@ public class FolderInfo extends ItemInfo { public static final int FLAG_MANUAL_FOLDER_NAME = 0x00000008; + public static final String EXTRA_FOLDER_SUGGESTIONS = "suggest"; + public int options; public Intent suggestedFolderNames; diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java index 024c7dd47b..0222b57fec 100644 --- a/src/com/android/launcher3/folder/Folder.java +++ b/src/com/android/launcher3/folder/Folder.java @@ -89,6 +89,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; /** * Represents a set of icons chosen by the user or generated by the system. @@ -301,12 +303,12 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo post(() -> { if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) { if (TextUtils.isEmpty(mFolderName.getText())) { - String[] suggestedNames = - mInfo.suggestedFolderNames.getStringArrayExtra("suggest"); - mFolderName.setText(suggestedNames[0]); - mFolderName.displayCompletions(Arrays.asList(suggestedNames).subList(1, - suggestedNames.length)); - mFolderName.setEnteredCompose(false); + FolderNameInfo[] nameInfos = + (FolderNameInfo[]) mInfo.suggestedFolderNames.getParcelableArrayExtra( + FolderInfo.EXTRA_FOLDER_SUGGESTIONS); + if (nameInfos != null) { + showLabelSuggestion(nameInfos); + } } } mFolderName.setHint(""); @@ -443,26 +445,52 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } /** - * Show suggested folder title. + * Show suggested folder title in FolderEditText, push InputMethodManager suggestions and save + * the suggestedFolderNames. */ - public void showSuggestedTitle(String[] suggestName) { + public void showSuggestedTitle(FolderNameInfo[] nameInfos) { if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) { - mInfo.suggestedFolderNames = new Intent().putExtra("suggest", suggestName); + mInfo.suggestedFolderNames = new Intent().putExtra(FolderInfo.EXTRA_FOLDER_SUGGESTIONS, + nameInfos); if (TextUtils.isEmpty(mFolderName.getText().toString()) && !mInfo.hasOption(FolderInfo.FLAG_MANUAL_FOLDER_NAME)) { - if (suggestName.length > 0 && !TextUtils.isEmpty(suggestName[0])) { - mFolderName.setHint(""); - mFolderName.setText(suggestName[0]); - mInfo.title = suggestName[0]; - animateOpen(mInfo.contents, 0, true); - mFolderName.showKeyboard(); - mFolderName.displayCompletions( - Arrays.asList(suggestName).subList(1, suggestName.length)); - } + showLabelSuggestion(nameInfos); } } } + /** + * Show suggested folder title in FolderEditText if the first suggestion is non-empty, push + * InputMethodManager suggestions. + */ + private void showLabelSuggestion(FolderNameInfo[] nameInfos) { + if (nameInfos == null) { + return; + } + // 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( + nameInfos[0].getLabel()) + || nameInfos.length > 1 && nameInfos[1] != null && !TextUtils.isEmpty( + nameInfos[1].getLabel()); + CharSequence firstLabel = nameInfos[0].getLabel(); + + if (shouldOpen) { + if (!TextUtils.isEmpty(firstLabel)) { + mFolderName.setHint(""); + mFolderName.setText(firstLabel); + mInfo.title = firstLabel; + } + animateOpen(mInfo.contents, 0, true); + mFolderName.showKeyboard(); + mFolderName.displayCompletions( + Arrays.asList(nameInfos).subList(1, nameInfos.length).stream() + .filter(Objects::nonNull) + .map(s -> s.getLabel().toString()) + .collect(Collectors.toList())); + } + } + /** * Creates a new UserFolder, inflated from R.layout.user_folder. * diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java index 6a47b98af9..ab1ff102c6 100644 --- a/src/com/android/launcher3/folder/FolderIcon.java +++ b/src/com/android/launcher3/folder/FolderIcon.java @@ -393,15 +393,16 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel if (!itemAdded) mPreviewItemManager.hidePreviewItem(index, true); final int finalIndex = index; - String[] suggestedNameOut = new String[FolderNameProvider.SUGGEST_MAX]; + FolderNameInfo[] nameInfos = + new FolderNameInfo[FolderNameProvider.SUGGEST_MAX]; if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) { Executors.UI_HELPER_EXECUTOR.post(() -> { d.folderNameProvider.getSuggestedFolderName( - getContext(), mInfo.contents, suggestedNameOut); - showFinalView(finalIndex, item, suggestedNameOut); + getContext(), mInfo.contents, nameInfos); + showFinalView(finalIndex, item, nameInfos); }); } else { - showFinalView(finalIndex, item, suggestedNameOut); + showFinalView(finalIndex, item, nameInfos); } } else { addItem(item); @@ -409,12 +410,12 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel } private void showFinalView(int finalIndex, final WorkspaceItemInfo item, - String[] suggestedNameOut) { + FolderNameInfo[] nameInfos) { postDelayed(() -> { mPreviewItemManager.hidePreviewItem(finalIndex, false); mFolder.showItem(item); invalidate(); - mFolder.showSuggestedTitle(suggestedNameOut); + mFolder.showSuggestedTitle(nameInfos); }, DROP_IN_ANIMATION_DURATION); } diff --git a/src/com/android/launcher3/folder/FolderNameInfo.java b/src/com/android/launcher3/folder/FolderNameInfo.java new file mode 100644 index 0000000000..eb9da90ac4 --- /dev/null +++ b/src/com/android/launcher3/folder/FolderNameInfo.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.folder; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import androidx.annotation.NonNull; + +/** + * Information about a single label suggestions of the Folder. + */ + +public final class FolderNameInfo implements Parcelable { + private final double mScore; + private final CharSequence mLabel; + + /** + * Create a simple completion with label. + * + * @param label The text that should be inserted into the editor and pushed to + * InputMethodManager suggestions. + * @param score The score for the label between 0.0 and 1.0. + */ + public FolderNameInfo(CharSequence label, double score) { + mScore = score; + mLabel = label; + } + + private FolderNameInfo(Parcel source) { + mScore = source.readDouble(); + mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + } + + public CharSequence getLabel() { + return mLabel; + } + + /** + * Used to package this object into a {@link Parcel}. + * + * @param dest The {@link Parcel} to be written. + * @param flags The flags used for parceling. + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeDouble(mScore); + TextUtils.writeToParcel(mLabel, dest, flags); + } + + /** + * Used to make this class parcelable. + */ + @NonNull + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public FolderNameInfo createFromParcel(Parcel source) { + return new FolderNameInfo(source); + } + + public FolderNameInfo[] newArray(int size) { + return new FolderNameInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } +} diff --git a/src/com/android/launcher3/folder/FolderNameProvider.java b/src/com/android/launcher3/folder/FolderNameProvider.java index 957e6361fe..d5990fa4e2 100644 --- a/src/com/android/launcher3/folder/FolderNameProvider.java +++ b/src/com/android/launcher3/folder/FolderNameProvider.java @@ -30,6 +30,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; @@ -60,29 +61,33 @@ public class FolderNameProvider implements ResourceBasedOverride { return fnp; } - public CharSequence getSuggestedFolderName(Context context, - ArrayList workspaceItemInfos, CharSequence[] candidates) { + /** + * Generate and rank the suggested Folder names. + */ + public void getSuggestedFolderName(Context context, + ArrayList workspaceItemInfos, + FolderNameInfo[] nameInfos) { if (DEBUG) { - Log.d(TAG, "getSuggestedFolderName:" + Arrays.toString(candidates)); + Log.d(TAG, "getSuggestedFolderName:" + Arrays.toString(nameInfos)); } // If all the icons are from work profile, // Then, suggest "Work" as the folder name List distinctItemInfos = workspaceItemInfos.stream() - .filter(distinctByKey(p-> p.user)) + .filter(distinctByKey(p -> p.user)) .collect(Collectors.toList()); if (distinctItemInfos.size() == 1 && !distinctItemInfos.get(0).user.equals(Process.myUserHandle())) { // Place it as last viable suggestion - setAsLastSuggestion(candidates, + setAsLastSuggestion(nameInfos, context.getResources().getString(R.string.work_folder_name)); } // If all the icons are from same package (e.g., main icon, shortcut, shortcut) // Then, suggest the package's title as the folder name distinctItemInfos = workspaceItemInfos.stream() - .filter(distinctByKey(p-> p.getTargetComponent() != null + .filter(distinctByKey(p -> p.getTargetComponent() != null ? p.getTargetComponent().getPackageName() : "")) .collect(Collectors.toList()); @@ -91,44 +96,46 @@ public class FolderNameProvider implements ResourceBasedOverride { .getAppInfoByPackageName(distinctItemInfos.get(0).getTargetComponent() .getPackageName()); // Place it as first viable suggestion and shift everything else - info.ifPresent(i -> setAsFirstSuggestion(candidates, i.title.toString())); + info.ifPresent(i -> setAsFirstSuggestion(nameInfos, i.title.toString())); } if (DEBUG) { - Log.d(TAG, "getSuggestedFolderName:" + Arrays.toString(candidates)); + Log.d(TAG, "getSuggestedFolderName:" + Arrays.toString(nameInfos)); } - return candidates[0]; } - private void setAsFirstSuggestion(CharSequence[] candidatesOut, CharSequence candidate) { - if (contains(candidatesOut, candidate)) { + private void setAsFirstSuggestion(FolderNameInfo[] nameInfos, CharSequence label) { + if (nameInfos.length == 0 || contains(nameInfos, label)) { return; } - for (int i = candidatesOut.length - 1; i > 0; i--) { - if (!TextUtils.isEmpty(candidatesOut[i - 1])) { - candidatesOut[i] = candidatesOut[i - 1]; + for (int i = nameInfos.length - 1; i > 0; i--) { + if (nameInfos[i - 1] != null && !TextUtils.isEmpty(nameInfos[i - 1].getLabel())) { + nameInfos[i] = nameInfos[i - 1]; } } - candidatesOut[0] = candidate; + nameInfos[0] = new FolderNameInfo(label, 1.0); } - private void setAsLastSuggestion(CharSequence[] candidatesOut, CharSequence candidate) { - if (contains(candidatesOut, candidate)) { + private void setAsLastSuggestion(FolderNameInfo[] nameInfos, CharSequence label) { + if (nameInfos.length == 0 || contains(nameInfos, label)) { return; } - for (int i = 0; i < candidate.length(); i++) { - if (TextUtils.isEmpty(candidatesOut[i])) { - candidatesOut[i] = candidate; + for (int i = 0; i < nameInfos.length; i++) { + if (nameInfos[i] == null || TextUtils.isEmpty(nameInfos[i].getLabel())) { + nameInfos[i] = new FolderNameInfo(label, 1.0); return; } } - candidatesOut[candidate.length() - 1] = candidate; + // Overwrite the last suggestion. + int lastIndex = nameInfos.length - 1; + nameInfos[lastIndex] = new FolderNameInfo(label, 1.0); } - private boolean contains(CharSequence[] list, CharSequence key) { - return Arrays.asList(list).stream() - .filter(s -> s != null) - .anyMatch(s -> s.toString().equalsIgnoreCase(key.toString())); + private boolean contains(FolderNameInfo[] nameInfos, CharSequence label) { + return Arrays.stream(nameInfos) + .filter(Objects::nonNull) + .anyMatch(nameInfo -> nameInfo.getLabel().toString().equalsIgnoreCase( + label.toString())); } // This method can be moved to some Utility class location. diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index 8dc7a3a308..8717c23257 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -59,6 +59,7 @@ import com.android.launcher3.WorkspaceItemInfo; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.folder.Folder; import com.android.launcher3.folder.FolderGridOrganizer; +import com.android.launcher3.folder.FolderNameInfo; import com.android.launcher3.folder.FolderNameProvider; import com.android.launcher3.icons.ComponentWithLabelAndIcon; import com.android.launcher3.icons.ComponentWithLabelAndIcon.ComponentWithIconCachingLogic; @@ -925,11 +926,13 @@ public class LoaderTask implements Runnable { synchronized (mBgDataModel) { for (int i = 0; i < mBgDataModel.folders.size(); i++) { - String[] suggestedOut = new String[FolderNameProvider.SUGGEST_MAX]; + FolderNameInfo[] suggestionInfos = + new FolderNameInfo[FolderNameProvider.SUGGEST_MAX]; FolderInfo info = mBgDataModel.folders.valueAt(i); if (info.suggestedFolderNames == null) { - provider.getSuggestedFolderName(mApp.getContext(), info.contents, suggestedOut); - info.suggestedFolderNames = new Intent().putExtra("suggest", suggestedOut); + provider.getSuggestedFolderName(mApp.getContext(), info.contents, + suggestionInfos); + info.suggestedFolderNames = new Intent().putExtra("suggest", suggestionInfos); } } }