• 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.NonNull;
30 import androidx.annotation.Nullable;
31 
32 import com.android.launcher3.LauncherSettings;
33 import com.android.launcher3.Utilities;
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(@onNull ContentWriter writer)158     public void onAddToDatabase(@NonNull 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     @NonNull
211     @Override
buildProto(@ullable FolderInfo fInfo)212     public LauncherAtom.ItemInfo buildProto(@Nullable FolderInfo fInfo) {
213         FolderIcon.Builder folderIcon = FolderIcon.newBuilder()
214                 .setCardinality(contents.size());
215         if (LabelState.SUGGESTED.equals(getLabelState())) {
216             folderIcon.setLabelInfo(title.toString());
217         }
218         return getDefaultItemInfoBuilder()
219                 .setFolderIcon(folderIcon)
220                 .setRank(rank)
221                 .addItemAttributes(getLabelState().mLogAttribute)
222                 .setContainerInfo(getContainerInfo())
223                 .build();
224     }
225 
226     @Override
setTitle(@ullable CharSequence title, ModelWriter modelWriter)227     public void setTitle(@Nullable CharSequence title, ModelWriter modelWriter) {
228         // Updating label from null to empty is considered as false touch.
229         // Retaining null title(ie., UNLABELED state) allows auto-labeling when new items added.
230         if (isEmpty(title) && this.title == null) {
231             return;
232         }
233 
234         // Updating title to same value does not change any states.
235         if (title != null && title.equals(this.title)) {
236             return;
237         }
238 
239         this.title = title;
240         LabelState newLabelState =
241                 title == null ? LabelState.UNLABELED
242                         : title.length() == 0 ? LabelState.EMPTY :
243                                 getAcceptedSuggestionIndex().isPresent() ? LabelState.SUGGESTED
244                                         : LabelState.MANUAL;
245 
246         if (newLabelState.equals(LabelState.MANUAL)) {
247             options |= FLAG_MANUAL_FOLDER_NAME;
248         } else {
249             options &= ~FLAG_MANUAL_FOLDER_NAME;
250         }
251         if (modelWriter != null) {
252             modelWriter.updateItemInDatabase(this);
253         }
254     }
255 
256     /**
257      * Returns current state of the current folder label.
258      */
getLabelState()259     public LabelState getLabelState() {
260         return title == null ? LabelState.UNLABELED
261                 : title.length() == 0 ? LabelState.EMPTY :
262                         hasOption(FLAG_MANUAL_FOLDER_NAME) ? LabelState.MANUAL
263                                 : LabelState.SUGGESTED;
264     }
265 
266     @NonNull
267     @Override
makeShallowCopy()268     public ItemInfo makeShallowCopy() {
269         FolderInfo folderInfo = new FolderInfo();
270         folderInfo.copyFrom(this);
271         folderInfo.contents = this.contents;
272         return folderInfo;
273     }
274 
275     /**
276      * Returns {@link LauncherAtom.FolderIcon} wrapped as {@link LauncherAtom.ItemInfo} for logging.
277      */
278     @NonNull
279     @Override
buildProto()280     public LauncherAtom.ItemInfo buildProto() {
281         return buildProto(null);
282     }
283 
284     /**
285      * Returns index of the accepted suggestion.
286      */
getAcceptedSuggestionIndex()287     public OptionalInt getAcceptedSuggestionIndex() {
288         String newLabel = checkNotNull(title,
289                 "Expected valid folder label, but found null").toString();
290         if (suggestedFolderNames == null || !suggestedFolderNames.hasSuggestions()) {
291             return OptionalInt.empty();
292         }
293         CharSequence[] labels = suggestedFolderNames.getLabels();
294         return IntStream.range(0, labels.length)
295                 .filter(index -> !isEmpty(labels[index])
296                         && newLabel.equalsIgnoreCase(
297                         labels[index].toString()))
298                 .sequential()
299                 .findFirst();
300     }
301 
302     /**
303      * Returns {@link FromState} based on current {@link #title}.
304      */
getFromLabelState()305     public LauncherAtom.FromState getFromLabelState() {
306         switch (getLabelState()){
307             case EMPTY:
308                 return LauncherAtom.FromState.FROM_EMPTY;
309             case MANUAL:
310                 return LauncherAtom.FromState.FROM_CUSTOM;
311             case SUGGESTED:
312                 return LauncherAtom.FromState.FROM_SUGGESTED;
313             case UNLABELED:
314             default:
315                 return LauncherAtom.FromState.FROM_STATE_UNSPECIFIED;
316         }
317     }
318 
319     /**
320      * Returns {@link ToState} based on current {@link #title}.
321      */
getToLabelState()322     public LauncherAtom.ToState getToLabelState() {
323         if (title == null) {
324             return LauncherAtom.ToState.TO_STATE_UNSPECIFIED;
325         }
326 
327         // TODO: if suggestedFolderNames is null then it infrastructure issue, not
328         // ranking issue. We should log these appropriately.
329         if (suggestedFolderNames == null || !suggestedFolderNames.hasSuggestions()) {
330             return title.length() > 0
331                     ? LauncherAtom.ToState.TO_CUSTOM_WITH_EMPTY_SUGGESTIONS
332                     : LauncherAtom.ToState.TO_EMPTY_WITH_EMPTY_SUGGESTIONS;
333         }
334 
335         boolean hasValidPrimary = suggestedFolderNames != null && suggestedFolderNames.hasPrimary();
336         if (title.length() == 0) {
337             return hasValidPrimary ? LauncherAtom.ToState.TO_EMPTY_WITH_VALID_PRIMARY
338                     : LauncherAtom.ToState.TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
339         }
340 
341         OptionalInt accepted_suggestion_index = getAcceptedSuggestionIndex();
342         if (!accepted_suggestion_index.isPresent()) {
343             return hasValidPrimary ? LauncherAtom.ToState.TO_CUSTOM_WITH_VALID_PRIMARY
344                     : LauncherAtom.ToState.TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
345         }
346 
347         switch (accepted_suggestion_index.getAsInt()) {
348             case 0:
349                 return LauncherAtom.ToState.TO_SUGGESTION0;
350             case 1:
351                 return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION1_WITH_VALID_PRIMARY
352                         : LauncherAtom.ToState.TO_SUGGESTION1_WITH_EMPTY_PRIMARY;
353             case 2:
354                 return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION2_WITH_VALID_PRIMARY
355                         : LauncherAtom.ToState.TO_SUGGESTION2_WITH_EMPTY_PRIMARY;
356             case 3:
357                 return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION3_WITH_VALID_PRIMARY
358                         : LauncherAtom.ToState.TO_SUGGESTION3_WITH_EMPTY_PRIMARY;
359             default:
360                 // fall through
361         }
362         return LauncherAtom.ToState.TO_STATE_UNSPECIFIED;
363     }
364 }
365