From 97253ec3ab581b709f083b32b981d80ca58859fa Mon Sep 17 00:00:00 2001 From: Hyunyoung Song Date: Wed, 25 Dec 2019 13:51:28 -0800 Subject: [PATCH] Change the load logic of FolderNameProvider Work profile apps are suggested as "Work" folder name Bug: 147359653 Bug: 147359733 Change-Id: Idb2438de9c71c85cfeca6a6b0e116174ea2f3b62 --- .../folder/FolderNameProviderTest.java | 75 +++++++++++++++ .../launcher3/shadows/LShadowUserManager.java | 9 ++ src/com/android/launcher3/AppInfo.java | 11 +++ src/com/android/launcher3/LauncherModel.java | 23 +++-- .../launcher3/folder/FolderNameProvider.java | 94 +++++++++++++------ 5 files changed, 177 insertions(+), 35 deletions(-) create mode 100644 robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java diff --git a/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java new file mode 100644 index 0000000000..f7690550f3 --- /dev/null +++ b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java @@ -0,0 +1,75 @@ +/* + * 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 static org.junit.Assert.assertTrue; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.WorkspaceItemInfo; +import com.android.launcher3.shadows.LShadowUserManager; +import com.android.launcher3.util.LauncherRoboTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; + +import java.util.ArrayList; + +@RunWith(LauncherRoboTestRunner.class) +public final class FolderNameProviderTest { + private Context mContext; + private WorkspaceItemInfo mItem1; + private WorkspaceItemInfo mItem2; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mItem1 = new WorkspaceItemInfo(new AppInfo( + new ComponentName("a.b.c", "a.b.c/a.b.c.d"), + "title1", + LShadowUserManager.newUserHandle(10), + new Intent().setComponent(new ComponentName("a.b.c", "a.b.c/a.b.c.d")) + )); + mItem2 = new WorkspaceItemInfo(new AppInfo( + new ComponentName("a.b.c", "a.b.c/a.b.c.d"), + "title2", + LShadowUserManager.newUserHandle(10), + new Intent().setComponent(new ComponentName("a.b.c", "a.b.c/a.b.c.d")) + )); + } + + @Test + public void getSuggestedFolderName_workAssignedToEnd() { + 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")); + + suggestedNameOut[0] = "candidate1"; + suggestedNameOut[1] = "candidate2"; + suggestedNameOut[2] = "candidate3"; + new FolderNameProvider().getSuggestedFolderName(mContext, list, suggestedNameOut); + assertTrue(suggestedNameOut[3].equals("Work")); + + } +} diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java index edf8edbb43..576ddbd407 100644 --- a/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java +++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java @@ -16,6 +16,7 @@ package com.android.launcher3.shadows; +import android.os.Parcel; import android.os.UserHandle; import android.os.UserManager; import android.util.SparseBooleanArray; @@ -50,4 +51,12 @@ public class LShadowUserManager extends ShadowUserManager { public void setUserLocked(UserHandle userHandle, boolean enabled) { mLockedUsers.put(userHandle.hashCode(), enabled); } + + // Create user handle from parcel since UserHandle.of() was only added in later APIs. + public static UserHandle newUserHandle(int uid) { + Parcel userParcel = Parcel.obtain(); + userParcel.writeInt(uid); + userParcel.setDataPosition(0); + return new UserHandle(userParcel); + } } diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java index af219bad13..f76ca502e8 100644 --- a/src/com/android/launcher3/AppInfo.java +++ b/src/com/android/launcher3/AppInfo.java @@ -26,6 +26,8 @@ import android.os.Process; import android.os.UserHandle; import android.os.UserManager; +import androidx.annotation.VisibleForTesting; + import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.PackageManagerHelper; @@ -89,6 +91,15 @@ public class AppInfo extends ItemInfoWithIcon { runtimeStatusFlags = info.runtimeStatusFlags; } + @VisibleForTesting + public AppInfo(ComponentName componentName, CharSequence title, + UserHandle user, Intent intent) { + this.componentName = componentName; + this.title = title; + this.user = user; + this.intent = intent; + } + @Override protected String dumpProperties() { return super.dumpProperties() + " componentName=" + componentName; diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index e00532003f..63b0e1eab5 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -16,12 +16,6 @@ package com.android.launcher3; -import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD; -import static com.android.launcher3.config.FeatureFlags.IS_DOGFOOD_BUILD; -import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; -import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; -import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission; - import android.content.Context; import android.content.Intent; import android.content.pm.LauncherApps; @@ -68,10 +62,17 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.concurrent.CancellationException; import java.util.concurrent.Executor; import java.util.function.Supplier; +import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD; +import static com.android.launcher3.config.FeatureFlags.IS_DOGFOOD_BUILD; +import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; +import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission; + /** * Maintains in-memory state of the Launcher. It is expected that there should be only one * LauncherModel object held in a static. Also provide APIs for updating the database state @@ -126,6 +127,16 @@ public class LauncherModel extends LauncherApps.Callback implements InstallSessi mBgAllAppsList = new AllAppsList(iconCache, appFilter); } + /** + * Returns AppInfo with corresponding package name. + * TODO: move to enqueueModelTask + */ + public Optional getAppInfoByPackageName(String pkg) { + return mBgAllAppsList.data.stream() + .filter(info -> info.componentName.getPackageName().equals(pkg)) + .findAny(); + } + /** * Adds the provided items to the workspace. */ diff --git a/src/com/android/launcher3/folder/FolderNameProvider.java b/src/com/android/launcher3/folder/FolderNameProvider.java index 37aa815771..e58d4848a5 100644 --- a/src/com/android/launcher3/folder/FolderNameProvider.java +++ b/src/com/android/launcher3/folder/FolderNameProvider.java @@ -15,13 +15,23 @@ */ package com.android.launcher3.folder; -import android.content.ComponentName; import android.content.Context; +import android.os.Process; +import android.text.TextUtils; -import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.AppInfo; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.R; import com.android.launcher3.WorkspaceItemInfo; import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; /** * Locates provider for the folder name. @@ -29,39 +39,65 @@ import java.util.ArrayList; public class FolderNameProvider { /** - * IME usually has up to 3 suggest slots. Adding one as in Launcher, there are folder - * name edit box that we can also provide suggestion. + * IME usually has up to 3 suggest slots. In total, there are 4 suggest slots as the folder + * name edit box can also be used to provide suggestion. */ public static final int SUGGEST_MAX = 4; - /** - * Returns suggested folder name. - */ public CharSequence getSuggestedFolderName(Context context, - ArrayList workspaceItemInfos, CharSequence[] suggestName) { - // Currently only run the algorithm on initial folder creation. - // For more than 2 items in the folder, the ranking algorithm for finding - // candidate folder name should be rewritten. - if (workspaceItemInfos.size() == 2) { - ComponentName cmp1 = workspaceItemInfos.get(0).getTargetComponent(); - ComponentName cmp2 = workspaceItemInfos.get(1).getTargetComponent(); + ArrayList workspaceItemInfos, CharSequence[] candidates) { - String pkgName0 = cmp1 == null ? "" : cmp1.getPackageName(); - String pkgName1 = cmp2 == null ? "" : cmp2.getPackageName(); - // If the two icons are from the same package, - // then assign the main icon's name - if (pkgName0.equals(pkgName1)) { - WorkspaceItemInfo wInfo0 = workspaceItemInfos.get(0); - WorkspaceItemInfo wInfo1 = workspaceItemInfos.get(1); - if (workspaceItemInfos.get(0).itemType == Favorites.ITEM_TYPE_APPLICATION) { - suggestName[0] = wInfo0.title; - } else if (wInfo1.itemType == Favorites.ITEM_TYPE_APPLICATION) { - suggestName[0] = wInfo1.title; - } - return suggestName[0]; - // two icons are all shortcuts. Don't assign title + CharSequence suggest; + // If all the icons are from work profile, + // Then, suggest "Work" as the folder name + List distinctItemInfos = workspaceItemInfos.stream() + .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, + 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 + ? p.getTargetComponent().getPackageName() : "")) + .collect(Collectors.toList()); + + if (distinctItemInfos.size() == 1) { + Optional info = LauncherAppState.getInstance(context).getModel() + .getAppInfoByPackageName(distinctItemInfos.get(0).getTargetComponent() + .getPackageName()); + // Place it as first viable suggestion and shift everything else + info.ifPresent(i -> setAsFirstSuggestion(candidates, i.title.toString())); + } + return candidates[0]; + } + + private void setAsFirstSuggestion(CharSequence[] candidatesOut, CharSequence candidate) { + for (int i = candidatesOut.length - 1; i > 0; i--) { + if (TextUtils.isEmpty(candidatesOut[i])) { + candidatesOut[i - 1] = candidatesOut[i]; + } + candidatesOut[0] = candidate; + } + } + + private void setAsLastSuggestion(CharSequence[] candidatesOut, CharSequence candidate) { + for (int i = 0; i < candidate.length(); i++) { + if (TextUtils.isEmpty(candidatesOut[i])) { + candidatesOut[i] = candidate; } } - return suggestName[0]; + } + + // This method can be moved to some Utility class location. + private static Predicate distinctByKey(Function keyExtractor) { + Map map = new ConcurrentHashMap<>(); + return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; } }