• 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      * Create an app pair, a type of app collection that launches multiple apps into split screen
118      */
createAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2)119     public static FolderInfo createAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
120         FolderInfo newAppPair = new FolderInfo();
121         newAppPair.itemType = LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
122         newAppPair.add(app1, /* animate */ false);
123         newAppPair.add(app2, /* animate */ false);
124         return newAppPair;
125     }
126 
127     /**
128      * Add an app or shortcut
129      *
130      * @param item
131      */
add(WorkspaceItemInfo item, boolean animate)132     public void add(WorkspaceItemInfo item, boolean animate) {
133         add(item, contents.size(), animate);
134     }
135 
136     /**
137      * Add an app or shortcut for a specified rank.
138      */
add(WorkspaceItemInfo item, int rank, boolean animate)139     public void add(WorkspaceItemInfo item, int rank, boolean animate) {
140         rank = Utilities.boundToRange(rank, 0, contents.size());
141         contents.add(rank, item);
142         for (int i = 0; i < mListeners.size(); i++) {
143             mListeners.get(i).onAdd(item, rank);
144         }
145         itemsChanged(animate);
146     }
147 
148     /**
149      * Remove an app or shortcut. Does not change the DB.
150      *
151      * @param item
152      */
remove(WorkspaceItemInfo item, boolean animate)153     public void remove(WorkspaceItemInfo item, boolean animate) {
154         removeAll(Collections.singletonList(item), animate);
155     }
156 
157     /**
158      * Remove all matching app or shortcut. Does not change the DB.
159      */
removeAll(List<WorkspaceItemInfo> items, boolean animate)160     public void removeAll(List<WorkspaceItemInfo> items, boolean animate) {
161         contents.removeAll(items);
162         for (int i = 0; i < mListeners.size(); i++) {
163             mListeners.get(i).onRemove(items);
164         }
165         itemsChanged(animate);
166     }
167 
168     @Override
onAddToDatabase(@onNull ContentWriter writer)169     public void onAddToDatabase(@NonNull ContentWriter writer) {
170         super.onAddToDatabase(writer);
171         writer.put(LauncherSettings.Favorites.TITLE, title)
172                 .put(LauncherSettings.Favorites.OPTIONS, options);
173     }
174 
addListener(FolderListener listener)175     public void addListener(FolderListener listener) {
176         mListeners.add(listener);
177     }
178 
removeListener(FolderListener listener)179     public void removeListener(FolderListener listener) {
180         mListeners.remove(listener);
181     }
182 
itemsChanged(boolean animate)183     public void itemsChanged(boolean animate) {
184         for (int i = 0; i < mListeners.size(); i++) {
185             mListeners.get(i).onItemsChanged(animate);
186         }
187     }
188 
189     public interface FolderListener {
onAdd(WorkspaceItemInfo item, int rank)190         void onAdd(WorkspaceItemInfo item, int rank);
onRemove(List<WorkspaceItemInfo> item)191         void onRemove(List<WorkspaceItemInfo> item);
onItemsChanged(boolean animate)192         void onItemsChanged(boolean animate);
193     }
194 
hasOption(int optionFlag)195     public boolean hasOption(int optionFlag) {
196         return (options & optionFlag) != 0;
197     }
198 
199     /**
200      * @param option flag to set or clear
201      * @param isEnabled whether to set or clear the flag
202      * @param writer if not null, save changes to the db.
203      */
setOption(int option, boolean isEnabled, ModelWriter writer)204     public void setOption(int option, boolean isEnabled, ModelWriter writer) {
205         int oldOptions = options;
206         if (isEnabled) {
207             options |= option;
208         } else {
209             options &= ~option;
210         }
211         if (writer != null && oldOptions != options) {
212             writer.updateItemInDatabase(this);
213         }
214     }
215 
216     @Override
dumpProperties()217     protected String dumpProperties() {
218         return String.format("%s; labelState=%s", super.dumpProperties(), getLabelState());
219     }
220 
221     @NonNull
222     @Override
buildProto(@ullable FolderInfo fInfo)223     public LauncherAtom.ItemInfo buildProto(@Nullable FolderInfo fInfo) {
224         FolderIcon.Builder folderIcon = FolderIcon.newBuilder()
225                 .setCardinality(contents.size());
226         if (LabelState.SUGGESTED.equals(getLabelState())) {
227             folderIcon.setLabelInfo(title.toString());
228         }
229         return getDefaultItemInfoBuilder()
230                 .setFolderIcon(folderIcon)
231                 .setRank(rank)
232                 .addItemAttributes(getLabelState().mLogAttribute)
233                 .setContainerInfo(getContainerInfo())
234                 .build();
235     }
236 
237     @Override
setTitle(@ullable CharSequence title, ModelWriter modelWriter)238     public void setTitle(@Nullable CharSequence title, ModelWriter modelWriter) {
239         // Updating label from null to empty is considered as false touch.
240         // Retaining null title(ie., UNLABELED state) allows auto-labeling when new items added.
241         if (isEmpty(title) && this.title == null) {
242             return;
243         }
244 
245         // Updating title to same value does not change any states.
246         if (title != null && title.equals(this.title)) {
247             return;
248         }
249 
250         this.title = title;
251         LabelState newLabelState =
252                 title == null ? LabelState.UNLABELED
253                         : title.length() == 0 ? LabelState.EMPTY :
254                                 getAcceptedSuggestionIndex().isPresent() ? LabelState.SUGGESTED
255                                         : LabelState.MANUAL;
256 
257         if (newLabelState.equals(LabelState.MANUAL)) {
258             options |= FLAG_MANUAL_FOLDER_NAME;
259         } else {
260             options &= ~FLAG_MANUAL_FOLDER_NAME;
261         }
262         if (modelWriter != null) {
263             modelWriter.updateItemInDatabase(this);
264         }
265     }
266 
267     /**
268      * Returns current state of the current folder label.
269      */
getLabelState()270     public LabelState getLabelState() {
271         return title == null ? LabelState.UNLABELED
272                 : title.length() == 0 ? LabelState.EMPTY :
273                         hasOption(FLAG_MANUAL_FOLDER_NAME) ? LabelState.MANUAL
274                                 : LabelState.SUGGESTED;
275     }
276 
277     @NonNull
278     @Override
makeShallowCopy()279     public ItemInfo makeShallowCopy() {
280         FolderInfo folderInfo = new FolderInfo();
281         folderInfo.copyFrom(this);
282         folderInfo.contents = this.contents;
283         return folderInfo;
284     }
285 
286     /**
287      * Returns {@link LauncherAtom.FolderIcon} wrapped as {@link LauncherAtom.ItemInfo} for logging.
288      */
289     @NonNull
290     @Override
buildProto()291     public LauncherAtom.ItemInfo buildProto() {
292         return buildProto(null);
293     }
294 
295     /**
296      * Returns index of the accepted suggestion.
297      */
getAcceptedSuggestionIndex()298     public OptionalInt getAcceptedSuggestionIndex() {
299         String newLabel = checkNotNull(title,
300                 "Expected valid folder label, but found null").toString();
301         if (suggestedFolderNames == null || !suggestedFolderNames.hasSuggestions()) {
302             return OptionalInt.empty();
303         }
304         CharSequence[] labels = suggestedFolderNames.getLabels();
305         return IntStream.range(0, labels.length)
306                 .filter(index -> !isEmpty(labels[index])
307                         && newLabel.equalsIgnoreCase(
308                         labels[index].toString()))
309                 .sequential()
310                 .findFirst();
311     }
312 
313     /**
314      * Returns {@link FromState} based on current {@link #title}.
315      */
getFromLabelState()316     public LauncherAtom.FromState getFromLabelState() {
317         switch (getLabelState()){
318             case EMPTY:
319                 return LauncherAtom.FromState.FROM_EMPTY;
320             case MANUAL:
321                 return LauncherAtom.FromState.FROM_CUSTOM;
322             case SUGGESTED:
323                 return LauncherAtom.FromState.FROM_SUGGESTED;
324             case UNLABELED:
325             default:
326                 return LauncherAtom.FromState.FROM_STATE_UNSPECIFIED;
327         }
328     }
329 
330     /**
331      * Returns {@link ToState} based on current {@link #title}.
332      */
getToLabelState()333     public LauncherAtom.ToState getToLabelState() {
334         if (title == null) {
335             return LauncherAtom.ToState.TO_STATE_UNSPECIFIED;
336         }
337 
338         // TODO: if suggestedFolderNames is null then it infrastructure issue, not
339         // ranking issue. We should log these appropriately.
340         if (suggestedFolderNames == null || !suggestedFolderNames.hasSuggestions()) {
341             return title.length() > 0
342                     ? LauncherAtom.ToState.TO_CUSTOM_WITH_EMPTY_SUGGESTIONS
343                     : LauncherAtom.ToState.TO_EMPTY_WITH_EMPTY_SUGGESTIONS;
344         }
345 
346         boolean hasValidPrimary = suggestedFolderNames != null && suggestedFolderNames.hasPrimary();
347         if (title.length() == 0) {
348             return hasValidPrimary ? LauncherAtom.ToState.TO_EMPTY_WITH_VALID_PRIMARY
349                     : LauncherAtom.ToState.TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
350         }
351 
352         OptionalInt accepted_suggestion_index = getAcceptedSuggestionIndex();
353         if (!accepted_suggestion_index.isPresent()) {
354             return hasValidPrimary ? LauncherAtom.ToState.TO_CUSTOM_WITH_VALID_PRIMARY
355                     : LauncherAtom.ToState.TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
356         }
357 
358         switch (accepted_suggestion_index.getAsInt()) {
359             case 0:
360                 return LauncherAtom.ToState.TO_SUGGESTION0;
361             case 1:
362                 return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION1_WITH_VALID_PRIMARY
363                         : LauncherAtom.ToState.TO_SUGGESTION1_WITH_EMPTY_PRIMARY;
364             case 2:
365                 return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION2_WITH_VALID_PRIMARY
366                         : LauncherAtom.ToState.TO_SUGGESTION2_WITH_EMPTY_PRIMARY;
367             case 3:
368                 return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION3_WITH_VALID_PRIMARY
369                         : LauncherAtom.ToState.TO_SUGGESTION3_WITH_EMPTY_PRIMARY;
370             default:
371                 // fall through
372         }
373         return LauncherAtom.ToState.TO_STATE_UNSPECIFIED;
374     }
375 }
376