• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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 
17 package com.android.launcher3.model.data;
18 
19 import static android.text.TextUtils.isEmpty;
20 
21 import static androidx.core.util.Preconditions.checkNotNull;
22 
23 import static com.android.launcher3.logger.LauncherAtom.Attribute.EMPTY_LABEL;
24 import static com.android.launcher3.logger.LauncherAtom.Attribute.MANUAL_LABEL;
25 import static com.android.launcher3.logger.LauncherAtom.Attribute.SUGGESTED_LABEL;
26 
27 import android.os.Process;
28 
29 import androidx.annotation.Nullable;
30 
31 import com.android.launcher3.LauncherSettings;
32 import com.android.launcher3.Utilities;
33 import com.android.launcher3.config.FeatureFlags;
34 import com.android.launcher3.folder.FolderNameInfos;
35 import com.android.launcher3.logger.LauncherAtom;
36 import com.android.launcher3.logger.LauncherAtom.Attribute;
37 import com.android.launcher3.logger.LauncherAtom.FolderIcon;
38 import com.android.launcher3.logger.LauncherAtom.FromState;
39 import com.android.launcher3.logger.LauncherAtom.ToState;
40 import com.android.launcher3.model.ModelWriter;
41 import com.android.launcher3.util.ContentWriter;
42 
43 import java.util.ArrayList;
44 import java.util.Collections;
45 import java.util.List;
46 import java.util.OptionalInt;
47 import java.util.stream.IntStream;
48 
49 
50 /**
51  * Represents a folder containing shortcuts or apps.
52  */
53 public class FolderInfo extends ItemInfo {
54 
55     public static final int NO_FLAGS = 0x00000000;
56 
57     /**
58      * The folder is locked in sorted mode
59      */
60     public static final int FLAG_ITEMS_SORTED = 0x00000001;
61 
62     /**
63      * It is a work folder
64      */
65     public static final int FLAG_WORK_FOLDER = 0x00000002;
66 
67     /**
68      * The multi-page animation has run for this folder
69      */
70     public static final int FLAG_MULTI_PAGE_ANIMATION = 0x00000004;
71 
72     public static final int FLAG_MANUAL_FOLDER_NAME = 0x00000008;
73 
74     /**
75      * Different states of folder label.
76      */
77     public enum LabelState {
78         // Folder's label is not yet assigned( i.e., title == null). Eligible for auto-labeling.
79         UNLABELED(Attribute.UNLABELED),
80 
81         // Folder's label is empty(i.e., title == ""). Not eligible for auto-labeling.
82         EMPTY(EMPTY_LABEL),
83 
84         // Folder's label is one of the non-empty suggested values.
85         SUGGESTED(SUGGESTED_LABEL),
86 
87         // Folder's label is non-empty, manually entered by the user
88         // and different from any of suggested values.
89         MANUAL(MANUAL_LABEL);
90 
91         private final LauncherAtom.Attribute mLogAttribute;
92 
LabelState(Attribute logAttribute)93         LabelState(Attribute logAttribute) {
94             this.mLogAttribute = logAttribute;
95         }
96     }
97 
98     public static final String EXTRA_FOLDER_SUGGESTIONS = "suggest";
99 
100     public int options;
101 
102     public FolderNameInfos suggestedFolderNames;
103 
104     /**
105      * The apps and shortcuts
106      */
107     public ArrayList<WorkspaceItemInfo> contents = new ArrayList<>();
108 
109     private ArrayList<FolderListener> mListeners = new ArrayList<>();
110 
FolderInfo()111     public FolderInfo() {
112         itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
113         user = Process.myUserHandle();
114     }
115 
116     /**
117      * Add an app or shortcut
118      *
119      * @param item
120      */
add(WorkspaceItemInfo item, boolean animate)121     public void add(WorkspaceItemInfo item, boolean animate) {
122         add(item, contents.size(), animate);
123     }
124 
125     /**
126      * Add an app or shortcut for a specified rank.
127      */
add(WorkspaceItemInfo item, int rank, boolean animate)128     public void add(WorkspaceItemInfo item, int rank, boolean animate) {
129         rank = Utilities.boundToRange(rank, 0, contents.size());
130         contents.add(rank, item);
131         for (int i = 0; i < mListeners.size(); i++) {
132             mListeners.get(i).onAdd(item, rank);
133         }
134         itemsChanged(animate);
135     }
136 
137     /**
138      * Remove an app or shortcut. Does not change the DB.
139      *
140      * @param item
141      */
remove(WorkspaceItemInfo item, boolean animate)142     public void remove(WorkspaceItemInfo item, boolean animate) {
143         removeAll(Collections.singletonList(item), animate);
144     }
145 
146     /**
147      * Remove all matching app or shortcut. Does not change the DB.
148      */
removeAll(List<WorkspaceItemInfo> items, boolean animate)149     public void removeAll(List<WorkspaceItemInfo> items, boolean animate) {
150         contents.removeAll(items);
151         for (int i = 0; i < mListeners.size(); i++) {
152             mListeners.get(i).onRemove(items);
153         }
154         itemsChanged(animate);
155     }
156 
157     @Override
onAddToDatabase(ContentWriter writer)158     public void onAddToDatabase(ContentWriter writer) {
159         super.onAddToDatabase(writer);
160         writer.put(LauncherSettings.Favorites.TITLE, title)
161                 .put(LauncherSettings.Favorites.OPTIONS, options);
162     }
163 
addListener(FolderListener listener)164     public void addListener(FolderListener listener) {
165         mListeners.add(listener);
166     }
167 
removeListener(FolderListener listener)168     public void removeListener(FolderListener listener) {
169         mListeners.remove(listener);
170     }
171 
itemsChanged(boolean animate)172     public void itemsChanged(boolean animate) {
173         for (int i = 0; i < mListeners.size(); i++) {
174             mListeners.get(i).onItemsChanged(animate);
175         }
176     }
177 
178     public interface FolderListener {
onAdd(WorkspaceItemInfo item, int rank)179         void onAdd(WorkspaceItemInfo item, int rank);
onRemove(List<WorkspaceItemInfo> item)180         void onRemove(List<WorkspaceItemInfo> item);
onItemsChanged(boolean animate)181         void onItemsChanged(boolean animate);
182     }
183 
hasOption(int optionFlag)184     public boolean hasOption(int optionFlag) {
185         return (options & optionFlag) != 0;
186     }
187 
188     /**
189      * @param option flag to set or clear
190      * @param isEnabled whether to set or clear the flag
191      * @param writer if not null, save changes to the db.
192      */
setOption(int option, boolean isEnabled, ModelWriter writer)193     public void setOption(int option, boolean isEnabled, ModelWriter writer) {
194         int oldOptions = options;
195         if (isEnabled) {
196             options |= option;
197         } else {
198             options &= ~option;
199         }
200         if (writer != null && oldOptions != options) {
201             writer.updateItemInDatabase(this);
202         }
203     }
204 
205     @Override
dumpProperties()206     protected String dumpProperties() {
207         return String.format("%s; labelState=%s", super.dumpProperties(), getLabelState());
208     }
209 
210     @Override
buildProto(FolderInfo fInfo)211     public LauncherAtom.ItemInfo buildProto(FolderInfo fInfo) {
212         FolderIcon.Builder folderIcon = FolderIcon.newBuilder()
213                 .setCardinality(contents.size());
214         if (LabelState.SUGGESTED.equals(getLabelState())) {
215             folderIcon.setLabelInfo(title.toString());
216         }
217         return getDefaultItemInfoBuilder()
218                 .setFolderIcon(folderIcon)
219                 .setRank(rank)
220                 .setAttribute(getLabelState().mLogAttribute)
221                 .setContainerInfo(getContainerInfo())
222                 .build();
223     }
224 
225     @Override
setTitle(@ullable CharSequence title, ModelWriter modelWriter)226     public void setTitle(@Nullable CharSequence title, ModelWriter modelWriter) {
227         // Updating label from null to empty is considered as false touch.
228         // Retaining null title(ie., UNLABELED state) allows auto-labeling when new items added.
229         if (isEmpty(title) && this.title == null) {
230             return;
231         }
232 
233         // Updating title to same value does not change any states.
234         if (title != null && title.equals(this.title)) {
235             return;
236         }
237 
238         this.title = title;
239         LabelState newLabelState =
240                 title == null ? LabelState.UNLABELED
241                         : title.length() == 0 ? LabelState.EMPTY :
242                                 getAcceptedSuggestionIndex().isPresent() ? LabelState.SUGGESTED
243                                         : LabelState.MANUAL;
244 
245         if (newLabelState.equals(LabelState.MANUAL)) {
246             options |= FLAG_MANUAL_FOLDER_NAME;
247         } else {
248             options &= ~FLAG_MANUAL_FOLDER_NAME;
249         }
250         if (modelWriter != null) {
251             modelWriter.updateItemInDatabase(this);
252         }
253     }
254 
255     /**
256      * Returns current state of the current folder label.
257      */
getLabelState()258     public LabelState getLabelState() {
259         return title == null ? LabelState.UNLABELED
260                 : title.length() == 0 ? LabelState.EMPTY :
261                         hasOption(FLAG_MANUAL_FOLDER_NAME) ? LabelState.MANUAL
262                                 : LabelState.SUGGESTED;
263     }
264 
265     @Override
makeShallowCopy()266     public ItemInfo makeShallowCopy() {
267         FolderInfo folderInfo = new FolderInfo();
268         folderInfo.copyFrom(this);
269         folderInfo.contents = this.contents;
270         return folderInfo;
271     }
272 
273     /**
274      * Returns {@link LauncherAtom.FolderIcon} wrapped as {@link LauncherAtom.ItemInfo} for logging.
275      */
276     @Override
buildProto()277     public LauncherAtom.ItemInfo buildProto() {
278         return buildProto(null);
279     }
280 
281     /**
282      * Returns index of the accepted suggestion.
283      */
getAcceptedSuggestionIndex()284     public OptionalInt getAcceptedSuggestionIndex() {
285         String newLabel = checkNotNull(title,
286                 "Expected valid folder label, but found null").toString();
287         if (suggestedFolderNames == null || !suggestedFolderNames.hasSuggestions()) {
288             return OptionalInt.empty();
289         }
290         CharSequence[] labels = suggestedFolderNames.getLabels();
291         return IntStream.range(0, labels.length)
292                 .filter(index -> !isEmpty(labels[index])
293                         && newLabel.equalsIgnoreCase(
294                         labels[index].toString()))
295                 .sequential()
296                 .findFirst();
297     }
298 
299     /**
300      * Returns {@link FromState} based on current {@link #title}.
301      */
getFromLabelState()302     public LauncherAtom.FromState getFromLabelState() {
303         switch (getLabelState()){
304             case EMPTY:
305                 return LauncherAtom.FromState.FROM_EMPTY;
306             case MANUAL:
307                 return LauncherAtom.FromState.FROM_CUSTOM;
308             case SUGGESTED:
309                 return LauncherAtom.FromState.FROM_SUGGESTED;
310             case UNLABELED:
311             default:
312                 return LauncherAtom.FromState.FROM_STATE_UNSPECIFIED;
313         }
314     }
315 
316     /**
317      * Returns {@link ToState} based on current {@link #title}.
318      */
getToLabelState()319     public LauncherAtom.ToState getToLabelState() {
320         if (title == null) {
321             return LauncherAtom.ToState.TO_STATE_UNSPECIFIED;
322         }
323 
324         if (!FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
325             return title.length() > 0
326                     ? LauncherAtom.ToState.TO_CUSTOM_WITH_SUGGESTIONS_DISABLED
327                     : LauncherAtom.ToState.TO_EMPTY_WITH_SUGGESTIONS_DISABLED;
328         }
329 
330         // TODO: if suggestedFolderNames is null then it infrastructure issue, not
331         // ranking issue. We should log these appropriately.
332         if (suggestedFolderNames == null || !suggestedFolderNames.hasSuggestions()) {
333             return title.length() > 0
334                     ? LauncherAtom.ToState.TO_CUSTOM_WITH_EMPTY_SUGGESTIONS
335                     : LauncherAtom.ToState.TO_EMPTY_WITH_EMPTY_SUGGESTIONS;
336         }
337 
338         boolean hasValidPrimary = suggestedFolderNames != null && suggestedFolderNames.hasPrimary();
339         if (title.length() == 0) {
340             return hasValidPrimary ? LauncherAtom.ToState.TO_EMPTY_WITH_VALID_PRIMARY
341                     : LauncherAtom.ToState.TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
342         }
343 
344         OptionalInt accepted_suggestion_index = getAcceptedSuggestionIndex();
345         if (!accepted_suggestion_index.isPresent()) {
346             return hasValidPrimary ? LauncherAtom.ToState.TO_CUSTOM_WITH_VALID_PRIMARY
347                     : LauncherAtom.ToState.TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
348         }
349 
350         switch (accepted_suggestion_index.getAsInt()) {
351             case 0:
352                 return LauncherAtom.ToState.TO_SUGGESTION0;
353             case 1:
354                 return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION1_WITH_VALID_PRIMARY
355                         : LauncherAtom.ToState.TO_SUGGESTION1_WITH_EMPTY_PRIMARY;
356             case 2:
357                 return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION2_WITH_VALID_PRIMARY
358                         : LauncherAtom.ToState.TO_SUGGESTION2_WITH_EMPTY_PRIMARY;
359             case 3:
360                 return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION3_WITH_VALID_PRIMARY
361                         : LauncherAtom.ToState.TO_SUGGESTION3_WITH_EMPTY_PRIMARY;
362             default:
363                 // fall through
364         }
365         return LauncherAtom.ToState.TO_STATE_UNSPECIFIED;
366     }
367 }
368