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