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