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