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