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