1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.launcher3.folder; 17 18 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER; 19 20 import android.annotation.SuppressLint; 21 import android.app.admin.DevicePolicyManager; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.os.Process; 25 import android.os.UserHandle; 26 import android.text.TextUtils; 27 import android.util.Log; 28 29 import androidx.annotation.NonNull; 30 import androidx.annotation.WorkerThread; 31 32 import com.android.launcher3.LauncherAppState; 33 import com.android.launcher3.LauncherModel.ModelUpdateTask; 34 import com.android.launcher3.R; 35 import com.android.launcher3.Utilities; 36 import com.android.launcher3.model.AllAppsList; 37 import com.android.launcher3.model.BgDataModel; 38 import com.android.launcher3.model.ModelTaskController; 39 import com.android.launcher3.model.StringCache; 40 import com.android.launcher3.model.data.AppInfo; 41 import com.android.launcher3.model.data.CollectionInfo; 42 import com.android.launcher3.model.data.FolderInfo; 43 import com.android.launcher3.model.data.WorkspaceItemInfo; 44 import com.android.launcher3.util.IntSparseArrayMap; 45 import com.android.launcher3.util.Preconditions; 46 import com.android.launcher3.util.ResourceBasedOverride; 47 48 import java.util.ArrayList; 49 import java.util.Arrays; 50 import java.util.List; 51 import java.util.Objects; 52 import java.util.Optional; 53 import java.util.Set; 54 import java.util.stream.Collectors; 55 56 /** 57 * Locates provider for the folder name. 58 */ 59 public class FolderNameProvider implements ResourceBasedOverride { 60 61 private static final String TAG = "FolderNameProvider"; 62 private static final boolean DEBUG = false; 63 64 /** 65 * IME usually has up to 3 suggest slots. In total, there are 4 suggest slots as the folder 66 * name edit box can also be used to provide suggestion. 67 */ 68 public static final int SUGGEST_MAX = 4; 69 protected IntSparseArrayMap<CollectionInfo> mCollectionInfos; 70 protected List<AppInfo> mAppInfos; 71 72 /** 73 * Retrieve instance of this object that can be overridden in runtime based on the build 74 * variant of the application. 75 */ newInstance(Context context)76 public static FolderNameProvider newInstance(Context context) { 77 FolderNameProvider fnp = Overrides.getObject(FolderNameProvider.class, 78 context.getApplicationContext(), R.string.folder_name_provider_class); 79 Preconditions.assertWorkerThread(); 80 fnp.load(context); 81 82 return fnp; 83 } 84 newInstance(Context context, List<AppInfo> appInfos, IntSparseArrayMap<CollectionInfo> folderInfos)85 public static FolderNameProvider newInstance(Context context, List<AppInfo> appInfos, 86 IntSparseArrayMap<CollectionInfo> folderInfos) { 87 Preconditions.assertWorkerThread(); 88 FolderNameProvider fnp = Overrides.getObject(FolderNameProvider.class, 89 context.getApplicationContext(), R.string.folder_name_provider_class); 90 fnp.load(appInfos, folderInfos); 91 92 return fnp; 93 } 94 load(Context context)95 private void load(Context context) { 96 LauncherAppState.getInstance(context).getModel().enqueueModelUpdateTask( 97 new FolderNameWorker()); 98 } 99 load(List<AppInfo> appInfos, IntSparseArrayMap<CollectionInfo> folderInfos)100 private void load(List<AppInfo> appInfos, IntSparseArrayMap<CollectionInfo> folderInfos) { 101 mAppInfos = appInfos; 102 mCollectionInfos = folderInfos; 103 } 104 105 /** 106 * Generate and rank the suggested Folder names. 107 */ 108 @WorkerThread getSuggestedFolderName(Context context, ArrayList<WorkspaceItemInfo> workspaceItemInfos, FolderNameInfos nameInfos)109 public void getSuggestedFolderName(Context context, 110 ArrayList<WorkspaceItemInfo> workspaceItemInfos, 111 FolderNameInfos nameInfos) { 112 Preconditions.assertWorkerThread(); 113 if (DEBUG) { 114 Log.d(TAG, "getSuggestedFolderName:" + nameInfos.toString()); 115 } 116 117 // If all the icons are from work profile, 118 // Then, suggest "Work" as the folder name 119 Set<UserHandle> users = workspaceItemInfos.stream().map(w -> w.user) 120 .collect(Collectors.toSet()); 121 if (users.size() == 1 && !users.contains(Process.myUserHandle())) { 122 setAsLastSuggestion(nameInfos, getWorkFolderName(context)); 123 } 124 125 // If all the icons are from same package (e.g., main icon, shortcut, shortcut) 126 // Then, suggest the package's title as the folder name 127 Set<String> packageNames = workspaceItemInfos.stream() 128 .map(WorkspaceItemInfo::getTargetComponent) 129 .filter(Objects::nonNull) 130 .map(ComponentName::getPackageName) 131 .collect(Collectors.toSet()); 132 133 if (packageNames.size() == 1) { 134 Optional<AppInfo> info = getAppInfoByPackageName(packageNames.iterator().next()); 135 // Place it as first viable suggestion and shift everything else 136 info.ifPresent(i -> setAsFirstSuggestion( 137 nameInfos, i.title == null ? "" : i.title.toString())); 138 } 139 if (DEBUG) { 140 Log.d(TAG, "getSuggestedFolderName:" + nameInfos.toString()); 141 } 142 } 143 144 @WorkerThread 145 @SuppressLint("NewApi") getWorkFolderName(Context context)146 private String getWorkFolderName(Context context) { 147 if (!Utilities.ATLEAST_T) { 148 return context.getString(R.string.work_folder_name); 149 } 150 return context.getSystemService(DevicePolicyManager.class).getResources() 151 .getString(StringCache.WORK_FOLDER_NAME, () -> 152 context.getString(R.string.work_folder_name)); 153 } 154 getAppInfoByPackageName(String packageName)155 private Optional<AppInfo> getAppInfoByPackageName(String packageName) { 156 if (mAppInfos == null || mAppInfos.isEmpty()) { 157 return Optional.empty(); 158 } 159 return mAppInfos.stream() 160 .filter(info -> info.componentName != null) 161 .filter(info -> info.componentName.getPackageName().equals(packageName)) 162 .findAny(); 163 } 164 setAsFirstSuggestion(FolderNameInfos nameInfos, CharSequence label)165 private void setAsFirstSuggestion(FolderNameInfos nameInfos, CharSequence label) { 166 if (nameInfos == null || nameInfos.contains(label)) { 167 return; 168 } 169 nameInfos.setStatus(FolderNameInfos.HAS_PRIMARY); 170 nameInfos.setStatus(FolderNameInfos.HAS_SUGGESTIONS); 171 CharSequence[] labels = nameInfos.getLabels(); 172 Float[] scores = nameInfos.getScores(); 173 for (int i = labels.length - 1; i > 0; i--) { 174 if (labels[i - 1] != null && !TextUtils.isEmpty(labels[i - 1])) { 175 nameInfos.setLabel(i, labels[i - 1], scores[i - 1]); 176 } 177 } 178 nameInfos.setLabel(0, label, 1.0f); 179 } 180 setAsLastSuggestion(FolderNameInfos nameInfos, CharSequence label)181 private void setAsLastSuggestion(FolderNameInfos nameInfos, CharSequence label) { 182 if (nameInfos == null || nameInfos.contains(label)) { 183 return; 184 } 185 nameInfos.setStatus(FolderNameInfos.HAS_PRIMARY); 186 nameInfos.setStatus(FolderNameInfos.HAS_SUGGESTIONS); 187 CharSequence[] labels = nameInfos.getLabels(); 188 for (int i = 0; i < labels.length; i++) { 189 if (labels[i] == null || TextUtils.isEmpty(labels[i])) { 190 nameInfos.setLabel(i, label, 1.0f); 191 return; 192 } 193 } 194 // Overwrite the last suggestion. 195 nameInfos.setLabel(labels.length - 1, label, 1.0f); 196 } 197 198 private class FolderNameWorker implements ModelUpdateTask { 199 200 @Override execute(@onNull ModelTaskController taskController, @NonNull BgDataModel dataModel, @NonNull AllAppsList apps)201 public void execute(@NonNull ModelTaskController taskController, 202 @NonNull BgDataModel dataModel, @NonNull AllAppsList apps) { 203 mCollectionInfos = getCollectionForSuggestions(dataModel); 204 mAppInfos = Arrays.asList(apps.copyData()); 205 } 206 } 207 getCollectionForSuggestions( BgDataModel dataModel)208 public static IntSparseArrayMap<CollectionInfo> getCollectionForSuggestions( 209 BgDataModel dataModel) { 210 IntSparseArrayMap<CollectionInfo> result = new IntSparseArrayMap<>(); 211 dataModel.itemsIdMap.stream() 212 .filter(item -> item.itemType == ITEM_TYPE_FOLDER) 213 .forEach(item -> result.put(item.id, (FolderInfo) item)); 214 return result; 215 } 216 217 } 218