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.CONTAINER_DESKTOP;
24 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
25 import static com.android.launcher3.logger.LauncherAtom.Attribute.EMPTY_LABEL;
26 import static com.android.launcher3.logger.LauncherAtom.Attribute.MANUAL_LABEL;
27 import static com.android.launcher3.logger.LauncherAtom.Attribute.SUGGESTED_LABEL;
28 import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_CUSTOM;
29 import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_EMPTY;
30 import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_FOLDER_LABEL_STATE_UNSPECIFIED;
31 import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_SUGGESTED;
32 
33 import android.os.Process;
34 
35 import androidx.annotation.Nullable;
36 
37 import com.android.launcher3.LauncherSettings;
38 import com.android.launcher3.Utilities;
39 import com.android.launcher3.config.FeatureFlags;
40 import com.android.launcher3.folder.FolderNameInfos;
41 import com.android.launcher3.logger.LauncherAtom;
42 import com.android.launcher3.logger.LauncherAtom.Attribute;
43 import com.android.launcher3.logger.LauncherAtom.FromState;
44 import com.android.launcher3.logger.LauncherAtom.ToState;
45 import com.android.launcher3.model.ModelWriter;
46 import com.android.launcher3.userevent.LauncherLogProto;
47 import com.android.launcher3.userevent.LauncherLogProto.Target;
48 import com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState;
49 import com.android.launcher3.userevent.LauncherLogProto.Target.ToFolderLabelState;
50 import com.android.launcher3.util.ContentWriter;
51 
52 import java.util.ArrayList;
53 import java.util.OptionalInt;
54 import java.util.stream.IntStream;
55 
56 
57 /**
58  * Represents a folder containing shortcuts or apps.
59  */
60 public class FolderInfo extends ItemInfo {
61 
62     public static final int NO_FLAGS = 0x00000000;
63 
64     /**
65      * The folder is locked in sorted mode
66      */
67     public static final int FLAG_ITEMS_SORTED = 0x00000001;
68 
69     /**
70      * It is a work folder
71      */
72     public static final int FLAG_WORK_FOLDER = 0x00000002;
73 
74     /**
75      * The multi-page animation has run for this folder
76      */
77     public static final int FLAG_MULTI_PAGE_ANIMATION = 0x00000004;
78 
79     public static final int FLAG_MANUAL_FOLDER_NAME = 0x00000008;
80 
81     /**
82      * Different states of folder label.
83      */
84     public enum LabelState {
85         // Folder's label is not yet assigned( i.e., title == null). Eligible for auto-labeling.
86         UNLABELED(Attribute.UNLABELED),
87 
88         // Folder's label is empty(i.e., title == ""). Not eligible for auto-labeling.
89         EMPTY(EMPTY_LABEL),
90 
91         // Folder's label is one of the non-empty suggested values.
92         SUGGESTED(SUGGESTED_LABEL),
93 
94         // Folder's label is non-empty, manually entered by the user
95         // and different from any of suggested values.
96         MANUAL(MANUAL_LABEL);
97 
98         private final LauncherAtom.Attribute mLogAttribute;
99 
LabelState(Attribute logAttribute)100         LabelState(Attribute logAttribute) {
101             this.mLogAttribute = logAttribute;
102         }
103     }
104 
105     public static final String EXTRA_FOLDER_SUGGESTIONS = "suggest";
106 
107     public int options;
108 
109     public FolderNameInfos suggestedFolderNames;
110 
111     /**
112      * The apps and shortcuts
113      */
114     public ArrayList<WorkspaceItemInfo> contents = new ArrayList<>();
115 
116     private ArrayList<FolderListener> mListeners = new ArrayList<>();
117 
FolderInfo()118     public FolderInfo() {
119         itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
120         user = Process.myUserHandle();
121     }
122 
123     /**
124      * Add an app or shortcut
125      *
126      * @param item
127      */
add(WorkspaceItemInfo item, boolean animate)128     public void add(WorkspaceItemInfo item, boolean animate) {
129         add(item, contents.size(), animate);
130     }
131 
132     /**
133      * Add an app or shortcut for a specified rank.
134      */
add(WorkspaceItemInfo item, int rank, boolean animate)135     public void add(WorkspaceItemInfo item, int rank, boolean animate) {
136         rank = Utilities.boundToRange(rank, 0, contents.size());
137         contents.add(rank, item);
138         for (int i = 0; i < mListeners.size(); i++) {
139             mListeners.get(i).onAdd(item, rank);
140         }
141         itemsChanged(animate);
142     }
143 
144     /**
145      * Remove an app or shortcut. Does not change the DB.
146      *
147      * @param item
148      */
remove(WorkspaceItemInfo item, boolean animate)149     public void remove(WorkspaceItemInfo item, boolean animate) {
150         contents.remove(item);
151         for (int i = 0; i < mListeners.size(); i++) {
152             mListeners.get(i).onRemove(item);
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         public void onAdd(WorkspaceItemInfo item, int rank);
onRemove(WorkspaceItemInfo item)180         public void onRemove(WorkspaceItemInfo item);
onItemsChanged(boolean animate)181         public 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         return getDefaultItemInfoBuilder()
213                 .setFolderIcon(LauncherAtom.FolderIcon.newBuilder().setCardinality(contents.size()))
214                 .setRank(rank)
215                 .setAttribute(getLabelState().mLogAttribute)
216                 .setContainerInfo(getContainerInfo())
217                 .build();
218     }
219 
220     @Override
setTitle(@ullable CharSequence title, ModelWriter modelWriter)221     public void setTitle(@Nullable CharSequence title, ModelWriter modelWriter) {
222         // Updating label from null to empty is considered as false touch.
223         // Retaining null title(ie., UNLABELED state) allows auto-labeling when new items added.
224         if (isEmpty(title) && this.title == null) {
225             return;
226         }
227 
228         // Updating title to same value does not change any states.
229         if (title != null && title.equals(this.title)) {
230             return;
231         }
232 
233         this.title = title;
234         LabelState newLabelState =
235                 title == null ? LabelState.UNLABELED
236                         : title.length() == 0 ? LabelState.EMPTY :
237                                 getAcceptedSuggestionIndex().isPresent() ? LabelState.SUGGESTED
238                                         : LabelState.MANUAL;
239 
240         if (newLabelState.equals(LabelState.MANUAL)) {
241             options |= FLAG_MANUAL_FOLDER_NAME;
242         } else {
243             options &= ~FLAG_MANUAL_FOLDER_NAME;
244         }
245         if (modelWriter != null) {
246             modelWriter.updateItemInDatabase(this);
247         }
248     }
249 
250     /**
251      * Returns current state of the current folder label.
252      */
getLabelState()253     public LabelState getLabelState() {
254         return title == null ? LabelState.UNLABELED
255                 : title.length() == 0 ? LabelState.EMPTY :
256                         hasOption(FLAG_MANUAL_FOLDER_NAME) ? LabelState.MANUAL
257                                 : LabelState.SUGGESTED;
258     }
259 
260     @Override
makeShallowCopy()261     public ItemInfo makeShallowCopy() {
262         FolderInfo folderInfo = new FolderInfo();
263         folderInfo.copyFrom(this);
264         folderInfo.contents = this.contents;
265         return folderInfo;
266     }
267 
268     /**
269      * Returns {@link LauncherAtom.FolderIcon} wrapped as {@link LauncherAtom.ItemInfo} for logging.
270      */
271     @Override
buildProto()272     public LauncherAtom.ItemInfo buildProto() {
273         return buildProto(null);
274     }
275 
276     /**
277      * Returns index of the accepted suggestion.
278      */
getAcceptedSuggestionIndex()279     public OptionalInt getAcceptedSuggestionIndex() {
280         String newLabel = checkNotNull(title,
281                 "Expected valid folder label, but found null").toString();
282         if (suggestedFolderNames == null || !suggestedFolderNames.hasSuggestions()) {
283             return OptionalInt.empty();
284         }
285         CharSequence[] labels = suggestedFolderNames.getLabels();
286         return IntStream.range(0, labels.length)
287                 .filter(index -> !isEmpty(labels[index])
288                         && newLabel.equalsIgnoreCase(
289                         labels[index].toString()))
290                 .sequential()
291                 .findFirst();
292     }
293 
294     /**
295      * Returns {@link FromState} based on current {@link #title}.
296      */
getFromLabelState()297     public LauncherAtom.FromState getFromLabelState() {
298         switch (getLabelState()){
299             case EMPTY:
300                 return LauncherAtom.FromState.FROM_EMPTY;
301             case MANUAL:
302                 return LauncherAtom.FromState.FROM_CUSTOM;
303             case SUGGESTED:
304                 return LauncherAtom.FromState.FROM_SUGGESTED;
305             case UNLABELED:
306             default:
307                 return LauncherAtom.FromState.FROM_STATE_UNSPECIFIED;
308         }
309     }
310 
311     /**
312      * Returns {@link ToState} based on current {@link #title}.
313      */
getToLabelState()314     public LauncherAtom.ToState getToLabelState() {
315         if (title == null) {
316             return LauncherAtom.ToState.TO_STATE_UNSPECIFIED;
317         }
318 
319         if (!FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
320             return title.length() > 0
321                     ? LauncherAtom.ToState.TO_CUSTOM_WITH_SUGGESTIONS_DISABLED
322                     : LauncherAtom.ToState.TO_EMPTY_WITH_SUGGESTIONS_DISABLED;
323         }
324 
325         // TODO: if suggestedFolderNames is null then it infrastructure issue, not
326         // ranking issue. We should log these appropriately.
327         if (suggestedFolderNames == null || !suggestedFolderNames.hasSuggestions()) {
328             return title.length() > 0
329                     ? LauncherAtom.ToState.TO_CUSTOM_WITH_EMPTY_SUGGESTIONS
330                     : LauncherAtom.ToState.TO_EMPTY_WITH_EMPTY_SUGGESTIONS;
331         }
332 
333         boolean hasValidPrimary = suggestedFolderNames != null && suggestedFolderNames.hasPrimary();
334         if (title.length() == 0) {
335             return hasValidPrimary ? LauncherAtom.ToState.TO_EMPTY_WITH_VALID_PRIMARY
336                     : LauncherAtom.ToState.TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
337         }
338 
339         OptionalInt accepted_suggestion_index = getAcceptedSuggestionIndex();
340         if (!accepted_suggestion_index.isPresent()) {
341             return hasValidPrimary ? LauncherAtom.ToState.TO_CUSTOM_WITH_VALID_PRIMARY
342                     : LauncherAtom.ToState.TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
343         }
344 
345         switch (accepted_suggestion_index.getAsInt()) {
346             case 0:
347                 return LauncherAtom.ToState.TO_SUGGESTION0;
348             case 1:
349                 return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION1_WITH_VALID_PRIMARY
350                         : LauncherAtom.ToState.TO_SUGGESTION1_WITH_EMPTY_PRIMARY;
351             case 2:
352                 return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION2_WITH_VALID_PRIMARY
353                         : LauncherAtom.ToState.TO_SUGGESTION2_WITH_EMPTY_PRIMARY;
354             case 3:
355                 return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION3_WITH_VALID_PRIMARY
356                         : LauncherAtom.ToState.TO_SUGGESTION3_WITH_EMPTY_PRIMARY;
357             default:
358                 // fall through
359         }
360         return LauncherAtom.ToState.TO_STATE_UNSPECIFIED;
361     }
362 
363     /**
364      * Returns {@link LauncherLogProto.LauncherEvent} to log current folder label info.
365      *
366      * @deprecated This method is used only for validation purpose and soon will be removed.
367      */
368     @Deprecated
getFolderLabelStateLauncherEvent(FromState fromState, ToState toState)369     public LauncherLogProto.LauncherEvent getFolderLabelStateLauncherEvent(FromState fromState,
370             ToState toState) {
371         return LauncherLogProto.LauncherEvent.newBuilder()
372                 .setAction(LauncherLogProto.Action
373                         .newBuilder()
374                         .setType(LauncherLogProto.Action.Type.SOFT_KEYBOARD))
375                 .addSrcTarget(Target
376                         .newBuilder()
377                         .setType(Target.Type.ITEM)
378                         .setItemType(LauncherLogProto.ItemType.EDITTEXT)
379                         .setFromFolderLabelState(convertFolderLabelState(fromState))
380                         .setToFolderLabelState(convertFolderLabelState(toState)))
381                 .addSrcTarget(Target.newBuilder()
382                         .setType(Target.Type.CONTAINER)
383                         .setContainerType(LauncherLogProto.ContainerType.FOLDER)
384                         .setPageIndex(screenId)
385                         .setGridX(cellX)
386                         .setGridY(cellY)
387                         .setCardinality(contents.size()))
388                 .addSrcTarget(newParentContainerTarget())
389                 .build();
390     }
391 
392     /**
393      * @deprecated This method is used only for validation purpose and soon will be removed.
394      */
395     @Deprecated
newParentContainerTarget()396     private Target.Builder newParentContainerTarget() {
397         Target.Builder builder = Target.newBuilder().setType(Target.Type.CONTAINER);
398         switch (container) {
399             case CONTAINER_HOTSEAT:
400                 return builder.setContainerType(LauncherLogProto.ContainerType.HOTSEAT);
401             case CONTAINER_DESKTOP:
402                 return builder.setContainerType(LauncherLogProto.ContainerType.WORKSPACE);
403             default:
404                 throw new AssertionError(String
405                         .format("Expected container to be either %s or %s but found %s.",
406                                 CONTAINER_HOTSEAT,
407                                 CONTAINER_DESKTOP,
408                                 container));
409         }
410     }
411 
412     /**
413      * @deprecated This method is used only for validation purpose and soon will be removed.
414      */
415     @Deprecated
convertFolderLabelState(FromState fromState)416     private static FromFolderLabelState convertFolderLabelState(FromState fromState) {
417         switch (fromState) {
418             case FROM_EMPTY:
419                 return FROM_EMPTY;
420             case FROM_SUGGESTED:
421                 return FROM_SUGGESTED;
422             case FROM_CUSTOM:
423                 return FROM_CUSTOM;
424             default:
425                 return FROM_FOLDER_LABEL_STATE_UNSPECIFIED;
426         }
427     }
428 
429     /**
430      * @deprecated This method is used only for validation purpose and soon will be removed.
431      */
432     @Deprecated
convertFolderLabelState(ToState toState)433     private static ToFolderLabelState convertFolderLabelState(ToState toState) {
434         switch (toState) {
435             case UNCHANGED:
436                 return ToFolderLabelState.UNCHANGED;
437             case TO_SUGGESTION0:
438                 return ToFolderLabelState.TO_SUGGESTION0_WITH_VALID_PRIMARY;
439             case TO_SUGGESTION1_WITH_VALID_PRIMARY:
440                 return ToFolderLabelState.TO_SUGGESTION1_WITH_VALID_PRIMARY;
441             case TO_SUGGESTION1_WITH_EMPTY_PRIMARY:
442                 return ToFolderLabelState.TO_SUGGESTION1_WITH_EMPTY_PRIMARY;
443             case TO_SUGGESTION2_WITH_VALID_PRIMARY:
444                 return ToFolderLabelState.TO_SUGGESTION2_WITH_VALID_PRIMARY;
445             case TO_SUGGESTION2_WITH_EMPTY_PRIMARY:
446                 return ToFolderLabelState.TO_SUGGESTION2_WITH_EMPTY_PRIMARY;
447             case TO_SUGGESTION3_WITH_VALID_PRIMARY:
448                 return ToFolderLabelState.TO_SUGGESTION3_WITH_VALID_PRIMARY;
449             case TO_SUGGESTION3_WITH_EMPTY_PRIMARY:
450                 return ToFolderLabelState.TO_SUGGESTION3_WITH_EMPTY_PRIMARY;
451             case TO_EMPTY_WITH_VALID_PRIMARY:
452                 return ToFolderLabelState.TO_EMPTY_WITH_VALID_PRIMARY;
453             case TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY:
454                 return ToFolderLabelState.TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
455             case TO_EMPTY_WITH_EMPTY_SUGGESTIONS:
456                 return ToFolderLabelState.TO_EMPTY_WITH_EMPTY_SUGGESTIONS;
457             case TO_EMPTY_WITH_SUGGESTIONS_DISABLED:
458                 return ToFolderLabelState.TO_EMPTY_WITH_SUGGESTIONS_DISABLED;
459             case TO_CUSTOM_WITH_VALID_PRIMARY:
460                 return ToFolderLabelState.TO_CUSTOM_WITH_VALID_PRIMARY;
461             case TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY:
462                 return ToFolderLabelState.TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
463             case TO_CUSTOM_WITH_EMPTY_SUGGESTIONS:
464                 return ToFolderLabelState.TO_CUSTOM_WITH_EMPTY_SUGGESTIONS;
465             case TO_CUSTOM_WITH_SUGGESTIONS_DISABLED:
466                 return ToFolderLabelState.TO_CUSTOM_WITH_SUGGESTIONS_DISABLED;
467             default:
468                 return ToFolderLabelState.TO_FOLDER_LABEL_STATE_UNSPECIFIED;
469         }
470     }
471 }
472