• 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.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
24 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
25 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
26 import static com.android.launcher3.logger.LauncherAtom.Attribute.EMPTY_LABEL;
27 import static com.android.launcher3.logger.LauncherAtom.Attribute.MANUAL_LABEL;
28 import static com.android.launcher3.logger.LauncherAtom.Attribute.SUGGESTED_LABEL;
29 
30 import android.content.Context;
31 
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 
35 import com.android.launcher3.LauncherSettings;
36 import com.android.launcher3.folder.FolderNameInfos;
37 import com.android.launcher3.logger.LauncherAtom;
38 import com.android.launcher3.logger.LauncherAtom.Attribute;
39 import com.android.launcher3.logger.LauncherAtom.FolderIcon;
40 import com.android.launcher3.logger.LauncherAtom.FromState;
41 import com.android.launcher3.logger.LauncherAtom.ToState;
42 import com.android.launcher3.model.ModelWriter;
43 import com.android.launcher3.util.ContentWriter;
44 
45 import java.util.ArrayList;
46 import java.util.OptionalInt;
47 import java.util.stream.IntStream;
48 
49 /**
50  * Represents a folder containing shortcuts or apps.
51  */
52 public class FolderInfo extends CollectionInfo {
53 
54     /**
55      * The multi-page animation has run for this folder
56      */
57     public static final int FLAG_MULTI_PAGE_ANIMATION = 0x00000004;
58 
59     public static final int FLAG_MANUAL_FOLDER_NAME = 0x00000008;
60 
61     /**
62      * Different states of folder label.
63      */
64     public enum LabelState {
65         // Folder's label is not yet assigned( i.e., title == null). Eligible for auto-labeling.
66         UNLABELED(Attribute.UNLABELED),
67 
68         // Folder's label is empty(i.e., title == ""). Not eligible for auto-labeling.
69         EMPTY(EMPTY_LABEL),
70 
71         // Folder's label is one of the non-empty suggested values.
72         SUGGESTED(SUGGESTED_LABEL),
73 
74         // Folder's label is non-empty, manually entered by the user
75         // and different from any of suggested values.
76         MANUAL(MANUAL_LABEL);
77 
78         private final LauncherAtom.Attribute mLogAttribute;
79 
LabelState(Attribute logAttribute)80         LabelState(Attribute logAttribute) {
81             this.mLogAttribute = logAttribute;
82         }
83     }
84 
85     public int options;
86 
87     public FolderNameInfos suggestedFolderNames;
88 
89     /**
90      * The apps and shortcuts
91      */
92     private final ArrayList<ItemInfo> contents = new ArrayList<>();
93 
FolderInfo()94     public FolderInfo() {
95         itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
96     }
97 
98     @Override
add(@onNull ItemInfo item)99     public void add(@NonNull ItemInfo item) {
100         if (!willAcceptItemType(item.itemType)) {
101             throw new RuntimeException("tried to add an illegal type into a folder");
102         }
103         getContents().add(item);
104     }
105 
106     /**
107      * Returns the folder's contents as an unsorted ArrayList of {@link ItemInfo}. Includes
108      * {@link WorkspaceItemInfo} and {@link AppPairInfo}s.
109      */
110     @NonNull
111     @Override
getContents()112     public ArrayList<ItemInfo> getContents() {
113         return contents;
114     }
115 
116     /**
117      * Returns the folder's contents as an ArrayList of {@link WorkspaceItemInfo}. Note: Does not
118      * return any {@link AppPairInfo}s contained in the folder, instead collects *their* contents
119      * and adds them to the ArrayList.
120      */
121     @Override
getAppContents()122     public ArrayList<WorkspaceItemInfo> getAppContents()  {
123         ArrayList<WorkspaceItemInfo> workspaceItemInfos = new ArrayList<>();
124         for (ItemInfo item : contents) {
125             if (item instanceof WorkspaceItemInfo wii) {
126                 workspaceItemInfos.add(wii);
127             } else if (item instanceof AppPairInfo api) {
128                 workspaceItemInfos.addAll(api.getAppContents());
129             }
130         }
131         return workspaceItemInfos;
132     }
133 
134     @Override
onAddToDatabase(@onNull ContentWriter writer)135     public void onAddToDatabase(@NonNull ContentWriter writer) {
136         super.onAddToDatabase(writer);
137         writer.put(LauncherSettings.Favorites.OPTIONS, options);
138     }
139 
hasOption(int optionFlag)140     public boolean hasOption(int optionFlag) {
141         return (options & optionFlag) != 0;
142     }
143 
144     /**
145      * @param option flag to set or clear
146      * @param isEnabled whether to set or clear the flag
147      * @param writer if not null, save changes to the db.
148      */
setOption(int option, boolean isEnabled, ModelWriter writer)149     public void setOption(int option, boolean isEnabled, ModelWriter writer) {
150         int oldOptions = options;
151         if (isEnabled) {
152             options |= option;
153         } else {
154             options &= ~option;
155         }
156         if (writer != null && oldOptions != options) {
157             writer.updateItemInDatabase(this);
158         }
159     }
160 
161     @Override
dumpProperties()162     protected String dumpProperties() {
163         return String.format("%s; labelState=%s", super.dumpProperties(), getLabelState());
164     }
165 
166     @NonNull
167     @Override
buildProto(@ullable CollectionInfo cInfo, Context context)168     public LauncherAtom.ItemInfo buildProto(@Nullable CollectionInfo cInfo, Context context) {
169         FolderIcon.Builder folderIcon = FolderIcon.newBuilder()
170                 .setCardinality(getContents().size());
171         if (LabelState.SUGGESTED.equals(getLabelState())) {
172             folderIcon.setLabelInfo(title.toString());
173         }
174         return getDefaultItemInfoBuilder(context)
175                 .setFolderIcon(folderIcon)
176                 .setRank(rank)
177                 .addItemAttributes(getLabelState().mLogAttribute)
178                 .setContainerInfo(getContainerInfo())
179                 .build();
180     }
181 
setTitle(@ullable CharSequence title, ModelWriter modelWriter)182     public void setTitle(@Nullable CharSequence title, ModelWriter modelWriter) {
183         // Updating label from null to empty is considered as false touch.
184         // Retaining null title(ie., UNLABELED state) allows auto-labeling when new items added.
185         if (isEmpty(title) && this.title == null) {
186             return;
187         }
188 
189         // Updating title to same value does not change any states.
190         if (title != null && title.equals(this.title)) {
191             return;
192         }
193 
194         this.title = title;
195         LabelState newLabelState =
196                 title == null ? LabelState.UNLABELED
197                         : title.length() == 0 ? LabelState.EMPTY :
198                                 getAcceptedSuggestionIndex().isPresent() ? LabelState.SUGGESTED
199                                         : LabelState.MANUAL;
200 
201         if (newLabelState.equals(LabelState.MANUAL)) {
202             options |= FLAG_MANUAL_FOLDER_NAME;
203         } else {
204             options &= ~FLAG_MANUAL_FOLDER_NAME;
205         }
206         if (modelWriter != null) {
207             modelWriter.updateItemInDatabase(this);
208         }
209     }
210 
211     /**
212      * Returns current state of the current folder label.
213      */
getLabelState()214     public LabelState getLabelState() {
215         return title == null ? LabelState.UNLABELED
216                 : title.length() == 0 ? LabelState.EMPTY :
217                         hasOption(FLAG_MANUAL_FOLDER_NAME) ? LabelState.MANUAL
218                                 : LabelState.SUGGESTED;
219     }
220 
221     @NonNull
222     @Override
makeShallowCopy()223     public ItemInfo makeShallowCopy() {
224         FolderInfo folderInfo = new FolderInfo();
225         folderInfo.copyFrom(this);
226         return folderInfo;
227     }
228 
229     @Override
copyFrom(@onNull ItemInfo info)230     public void copyFrom(@NonNull ItemInfo info) {
231         super.copyFrom(info);
232         if (info instanceof FolderInfo fi) {
233             contents.addAll(fi.getContents());
234         }
235     }
236 
237     /**
238      * Returns index of the accepted suggestion.
239      */
getAcceptedSuggestionIndex()240     public OptionalInt getAcceptedSuggestionIndex() {
241         String newLabel = checkNotNull(title,
242                 "Expected valid folder label, but found null").toString();
243         if (suggestedFolderNames == null || !suggestedFolderNames.hasSuggestions()) {
244             return OptionalInt.empty();
245         }
246         CharSequence[] labels = suggestedFolderNames.getLabels();
247         return IntStream.range(0, labels.length)
248                 .filter(index -> !isEmpty(labels[index])
249                         && newLabel.equalsIgnoreCase(
250                         labels[index].toString()))
251                 .sequential()
252                 .findFirst();
253     }
254 
255     /**
256      * Returns {@link FromState} based on current {@link #title}.
257      */
getFromLabelState()258     public LauncherAtom.FromState getFromLabelState() {
259         switch (getLabelState()){
260             case EMPTY:
261                 return LauncherAtom.FromState.FROM_EMPTY;
262             case MANUAL:
263                 return LauncherAtom.FromState.FROM_CUSTOM;
264             case SUGGESTED:
265                 return LauncherAtom.FromState.FROM_SUGGESTED;
266             case UNLABELED:
267             default:
268                 return LauncherAtom.FromState.FROM_STATE_UNSPECIFIED;
269         }
270     }
271 
272     /**
273      * Returns {@link ToState} based on current {@link #title}.
274      */
getToLabelState()275     public LauncherAtom.ToState getToLabelState() {
276         if (title == null) {
277             return LauncherAtom.ToState.TO_STATE_UNSPECIFIED;
278         }
279 
280         // TODO: if suggestedFolderNames is null then it infrastructure issue, not
281         // ranking issue. We should log these appropriately.
282         if (suggestedFolderNames == null || !suggestedFolderNames.hasSuggestions()) {
283             return title.length() > 0
284                     ? LauncherAtom.ToState.TO_CUSTOM_WITH_EMPTY_SUGGESTIONS
285                     : LauncherAtom.ToState.TO_EMPTY_WITH_EMPTY_SUGGESTIONS;
286         }
287 
288         boolean hasValidPrimary = suggestedFolderNames != null && suggestedFolderNames.hasPrimary();
289         if (title.length() == 0) {
290             return hasValidPrimary ? LauncherAtom.ToState.TO_EMPTY_WITH_VALID_PRIMARY
291                     : LauncherAtom.ToState.TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
292         }
293 
294         OptionalInt accepted_suggestion_index = getAcceptedSuggestionIndex();
295         if (!accepted_suggestion_index.isPresent()) {
296             return hasValidPrimary ? LauncherAtom.ToState.TO_CUSTOM_WITH_VALID_PRIMARY
297                     : LauncherAtom.ToState.TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
298         }
299 
300         switch (accepted_suggestion_index.getAsInt()) {
301             case 0:
302                 return LauncherAtom.ToState.TO_SUGGESTION0;
303             case 1:
304                 return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION1_WITH_VALID_PRIMARY
305                         : LauncherAtom.ToState.TO_SUGGESTION1_WITH_EMPTY_PRIMARY;
306             case 2:
307                 return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION2_WITH_VALID_PRIMARY
308                         : LauncherAtom.ToState.TO_SUGGESTION2_WITH_EMPTY_PRIMARY;
309             case 3:
310                 return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION3_WITH_VALID_PRIMARY
311                         : LauncherAtom.ToState.TO_SUGGESTION3_WITH_EMPTY_PRIMARY;
312             default:
313                 // fall through
314         }
315         return LauncherAtom.ToState.TO_STATE_UNSPECIFIED;
316     }
317 
318     /**
319      * Checks if {@code itemType} is a type that can be placed in folders.
320      */
willAcceptItemType(int itemType)321     public static boolean willAcceptItemType(int itemType) {
322         return itemType == ITEM_TYPE_APPLICATION
323                 || itemType == ITEM_TYPE_DEEP_SHORTCUT
324                 || itemType == ITEM_TYPE_APP_PAIR;
325     }
326 }
327