Reparent folders and app pairs
Previously, app pairs and folders shared a common data model, FolderInfo. Now we need to separate them, so a new type, CollectionInfo, will serve as the parent of both types. Bug: 315731527 Fixes: 326664798 Flag: ACONFIG com.android.wm.shell.enable_app_pairs TRUNKFOOD Test: Manual, unit tests to follow Change-Id: Ia8c429cf6e6a376f2554ae1866549ef0bcab2a22
This commit is contained in:
@@ -18,8 +18,8 @@ package com.android.launcher3.model;
|
||||
import static android.text.format.DateUtils.DAY_IN_MILLIS;
|
||||
import static android.text.format.DateUtils.formatElapsedTime;
|
||||
|
||||
import static com.android.launcher3.LauncherPrefs.nonRestorableItem;
|
||||
import static com.android.launcher3.EncryptionType.ENCRYPTED;
|
||||
import static com.android.launcher3.LauncherPrefs.nonRestorableItem;
|
||||
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
|
||||
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION;
|
||||
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
|
||||
@@ -65,7 +65,7 @@ import com.android.launcher3.logging.InstanceId;
|
||||
import com.android.launcher3.logging.InstanceIdSequence;
|
||||
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
|
||||
import com.android.launcher3.model.data.AppInfo;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.CollectionInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.WorkspaceItemInfo;
|
||||
import com.android.launcher3.pm.UserCache;
|
||||
@@ -233,7 +233,7 @@ public class QuickstepModelDelegate extends ModelDelegate {
|
||||
}
|
||||
InstanceId instanceId = new InstanceIdSequence().newInstanceId();
|
||||
for (ItemInfo info : itemsIdMap) {
|
||||
FolderInfo parent = getContainer(info, itemsIdMap);
|
||||
CollectionInfo parent = getContainer(info, itemsIdMap);
|
||||
StatsLogCompatManager.writeSnapshot(info.buildProto(parent), instanceId);
|
||||
}
|
||||
additionalSnapshotEvents(instanceId);
|
||||
@@ -270,7 +270,7 @@ public class QuickstepModelDelegate extends ModelDelegate {
|
||||
}
|
||||
|
||||
for (ItemInfo info : itemsIdMap) {
|
||||
FolderInfo parent = getContainer(info, itemsIdMap);
|
||||
CollectionInfo parent = getContainer(info, itemsIdMap);
|
||||
LauncherAtom.ItemInfo itemInfo = info.buildProto(parent);
|
||||
Log.d(TAG, itemInfo.toString());
|
||||
StatsEvent statsEvent = StatsLogCompatManager.buildStatsEvent(itemInfo,
|
||||
@@ -293,18 +293,19 @@ public class QuickstepModelDelegate extends ModelDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private static FolderInfo getContainer(ItemInfo info, IntSparseArrayMap<ItemInfo> itemsIdMap) {
|
||||
private static CollectionInfo getContainer(
|
||||
ItemInfo info, IntSparseArrayMap<ItemInfo> itemsIdMap) {
|
||||
if (info.container > 0) {
|
||||
ItemInfo containerInfo = itemsIdMap.get(info.container);
|
||||
|
||||
if (!(containerInfo instanceof FolderInfo)) {
|
||||
if (!(containerInfo instanceof CollectionInfo)) {
|
||||
Log.e(TAG, String.format(
|
||||
"Item info: %s found with invalid container: %s",
|
||||
info,
|
||||
containerInfo));
|
||||
}
|
||||
// Allow crash to help debug b/173838775
|
||||
return (FolderInfo) containerInfo;
|
||||
return (CollectionInfo) containerInfo;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ import com.android.launcher3.folder.FolderIcon;
|
||||
import com.android.launcher3.logger.LauncherAtom;
|
||||
import com.android.launcher3.logging.StatsLogManager;
|
||||
import com.android.launcher3.model.data.AppInfo;
|
||||
import com.android.launcher3.model.data.AppPairInfo;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.WorkspaceItemInfo;
|
||||
@@ -1082,19 +1083,19 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
|
||||
ActivityManagerWrapper.getInstance().startActivityFromRecents(task.key,
|
||||
ActivityOptions.makeBasic());
|
||||
mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
|
||||
} else if (tag instanceof FolderInfo fi && fi.itemType == Favorites.ITEM_TYPE_FOLDER) {
|
||||
} else if (tag instanceof FolderInfo) {
|
||||
// Tapping an expandable folder icon on Taskbar
|
||||
shouldCloseAllOpenViews = false;
|
||||
expandFolder((FolderIcon) view);
|
||||
} else if (tag instanceof FolderInfo fi && fi.itemType == Favorites.ITEM_TYPE_APP_PAIR) {
|
||||
} else if (tag instanceof AppPairInfo api) {
|
||||
// Tapping an app pair icon on Taskbar
|
||||
if (recents != null && recents.isSplitSelectionActive()) {
|
||||
Toast.makeText(this, "Unable to split with an app pair. Select another app.",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
// Else launch the selected app pair
|
||||
launchFromTaskbar(recents, view, fi.contents);
|
||||
mControllers.uiController.onTaskbarIconLaunched(fi);
|
||||
launchFromTaskbar(recents, view, api.getContents());
|
||||
mControllers.uiController.onTaskbarIconLaunched(api);
|
||||
mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
|
||||
}
|
||||
} else if (tag instanceof WorkspaceItemInfo) {
|
||||
|
||||
@@ -116,9 +116,9 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba
|
||||
}
|
||||
} else if (info instanceof FolderInfo && v instanceof FolderIcon) {
|
||||
FolderInfo fi = (FolderInfo) info;
|
||||
if (fi.contents.stream().anyMatch(matcher)) {
|
||||
if (fi.anyMatch(matcher)) {
|
||||
FolderDotInfo folderDotInfo = new FolderDotInfo();
|
||||
for (WorkspaceItemInfo si : fi.contents) {
|
||||
for (WorkspaceItemInfo si : fi.getContents()) {
|
||||
folderDotInfo.addDotInfo(mPopupDataProvider.getDotInfoForItem(si));
|
||||
}
|
||||
((FolderIcon) v).setDotInfo(folderDotInfo);
|
||||
|
||||
@@ -18,6 +18,7 @@ package com.android.launcher3.taskbar;
|
||||
import static android.content.pm.PackageManager.FEATURE_PC;
|
||||
import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
|
||||
|
||||
import static com.android.launcher3.BubbleTextView.DISPLAY_TASKBAR;
|
||||
import static com.android.launcher3.Flags.enableCursorHoverStates;
|
||||
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
|
||||
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
|
||||
@@ -52,6 +53,8 @@ import com.android.launcher3.Utilities;
|
||||
import com.android.launcher3.apppairs.AppPairIcon;
|
||||
import com.android.launcher3.folder.FolderIcon;
|
||||
import com.android.launcher3.folder.PreviewBackground;
|
||||
import com.android.launcher3.model.data.AppPairInfo;
|
||||
import com.android.launcher3.model.data.CollectionInfo;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.WorkspaceItemInfo;
|
||||
@@ -282,7 +285,7 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar
|
||||
removeView(view);
|
||||
view.setOnClickListener(null);
|
||||
view.setOnLongClickListener(null);
|
||||
if (!(view.getTag() instanceof FolderInfo)) {
|
||||
if (!(view.getTag() instanceof CollectionInfo)) {
|
||||
mActivityContext.getViewCache().recycleView(view.getSourceLayoutResId(), view);
|
||||
}
|
||||
view.setTag(null);
|
||||
@@ -316,8 +319,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar
|
||||
boolean isCollection = false;
|
||||
if (hotseatItemInfo.isPredictedItem()) {
|
||||
expectedLayoutResId = R.layout.taskbar_predicted_app_icon;
|
||||
} else if (hotseatItemInfo instanceof FolderInfo fi) {
|
||||
expectedLayoutResId = fi.itemType == ITEM_TYPE_APP_PAIR
|
||||
} else if (hotseatItemInfo instanceof CollectionInfo ci) {
|
||||
expectedLayoutResId = ci.itemType == ITEM_TYPE_APP_PAIR
|
||||
? R.layout.app_pair_icon
|
||||
: R.layout.folder_icon;
|
||||
isCollection = true;
|
||||
@@ -345,17 +348,18 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar
|
||||
|
||||
if (hotseatView == null) {
|
||||
if (isCollection) {
|
||||
FolderInfo folderInfo = (FolderInfo) hotseatItemInfo;
|
||||
CollectionInfo collectionInfo = (CollectionInfo) hotseatItemInfo;
|
||||
switch (hotseatItemInfo.itemType) {
|
||||
case ITEM_TYPE_FOLDER:
|
||||
hotseatView = FolderIcon.inflateFolderAndIcon(
|
||||
expectedLayoutResId, mActivityContext, this, folderInfo);
|
||||
expectedLayoutResId, mActivityContext, this,
|
||||
(FolderInfo) collectionInfo);
|
||||
((FolderIcon) hotseatView).setTextVisible(false);
|
||||
break;
|
||||
case ITEM_TYPE_APP_PAIR:
|
||||
hotseatView = AppPairIcon.inflateIcon(
|
||||
expectedLayoutResId, mActivityContext, this, folderInfo,
|
||||
BubbleTextView.DISPLAY_TASKBAR);
|
||||
expectedLayoutResId, mActivityContext, this,
|
||||
(AppPairInfo) collectionInfo, DISPLAY_TASKBAR);
|
||||
((AppPairIcon) hotseatView).setTextVisible(false);
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -813,8 +813,8 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar
|
||||
* 3) All Apps button
|
||||
*/
|
||||
public View getFirstIconMatch(Predicate<ItemInfo> matcher) {
|
||||
Predicate<ItemInfo> folderMatcher = ItemInfoMatcher.forFolderMatch(matcher);
|
||||
return mTaskbarView.getFirstMatch(matcher, folderMatcher);
|
||||
Predicate<ItemInfo> collectionMatcher = ItemInfoMatcher.forFolderMatch(matcher);
|
||||
return mTaskbarView.getFirstMatch(matcher, collectionMatcher);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -61,7 +61,7 @@ import com.android.launcher3.logging.StatsLogManager;
|
||||
import com.android.launcher3.model.AllAppsList;
|
||||
import com.android.launcher3.model.BaseModelUpdateTask;
|
||||
import com.android.launcher3.model.BgDataModel;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.CollectionInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.util.Executors;
|
||||
import com.android.launcher3.util.LogConfig;
|
||||
@@ -375,7 +375,7 @@ public class StatsLogCompatManager extends StatsLogManager {
|
||||
Executors.MODEL_EXECUTOR.execute(
|
||||
() -> write(event, applyOverwrites(mItemInfo.buildProto())));
|
||||
} else {
|
||||
// Item is inside the folder, fetch folder info in a BG thread
|
||||
// Item is inside a collection, fetch collection info in a BG thread
|
||||
// and then write to StatsLog.
|
||||
appState.getModel().enqueueModelUpdateTask(
|
||||
new BaseModelUpdateTask() {
|
||||
@@ -383,8 +383,9 @@ public class StatsLogCompatManager extends StatsLogManager {
|
||||
public void execute(@NonNull final LauncherAppState app,
|
||||
@NonNull final BgDataModel dataModel,
|
||||
@NonNull final AllAppsList apps) {
|
||||
FolderInfo folderInfo = dataModel.folders.get(mItemInfo.container);
|
||||
write(event, applyOverwrites(mItemInfo.buildProto(folderInfo)));
|
||||
CollectionInfo collectionInfo =
|
||||
dataModel.collections.get(mItemInfo.container);
|
||||
write(event, applyOverwrites(mItemInfo.buildProto(collectionInfo)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ import com.android.launcher3.icons.IconCache;
|
||||
import com.android.launcher3.logging.InstanceId;
|
||||
import com.android.launcher3.logging.StatsLogManager;
|
||||
import com.android.launcher3.model.data.AppInfo;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.AppPairInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.WorkspaceItemInfo;
|
||||
import com.android.launcher3.taskbar.TaskbarActivityContext;
|
||||
@@ -149,25 +149,17 @@ public class AppPairsController {
|
||||
|
||||
app1.rank = encodeRank(SPLIT_POSITION_TOP_OR_LEFT, snapPosition);
|
||||
app2.rank = encodeRank(SPLIT_POSITION_BOTTOM_OR_RIGHT, snapPosition);
|
||||
FolderInfo newAppPair = FolderInfo.createAppPair(app1, app2);
|
||||
|
||||
if (newAppPair.contents.size() != 2) {
|
||||
// if app pair doesn't have exactly 2 members, log an error and do not create the app
|
||||
// pair.
|
||||
Log.wtf(TAG,
|
||||
"tried to save an app pair with " + newAppPair.contents.size() + " members");
|
||||
return;
|
||||
}
|
||||
AppPairInfo newAppPair = new AppPairInfo(app1, app2);
|
||||
|
||||
IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
|
||||
MODEL_EXECUTOR.execute(() -> {
|
||||
newAppPair.contents.forEach(member -> {
|
||||
newAppPair.getContents().forEach(member -> {
|
||||
member.title = "";
|
||||
member.bitmap = iconCache.getDefaultIcon(newAppPair.user);
|
||||
iconCache.getTitleAndIcon(member, member.usingLowResIcon());
|
||||
});
|
||||
newAppPair.title = getDefaultTitle(newAppPair.contents.get(0).title,
|
||||
newAppPair.contents.get(1).title);
|
||||
newAppPair.title = getDefaultTitle(newAppPair.getFirstApp().title,
|
||||
newAppPair.getSecondApp().title);
|
||||
MAIN_EXECUTOR.execute(() -> {
|
||||
LauncherAccessibilityDelegate delegate =
|
||||
Launcher.getLauncher(mContext).getAccessibilityDelegate();
|
||||
@@ -194,8 +186,8 @@ public class AppPairsController {
|
||||
* monitoring
|
||||
*/
|
||||
public void launchAppPair(AppPairIcon appPairIcon, int cuj) {
|
||||
WorkspaceItemInfo app1 = appPairIcon.getInfo().contents.get(0);
|
||||
WorkspaceItemInfo app2 = appPairIcon.getInfo().contents.get(1);
|
||||
WorkspaceItemInfo app1 = appPairIcon.getInfo().getFirstApp();
|
||||
WorkspaceItemInfo app2 = appPairIcon.getInfo().getSecondApp();
|
||||
ComponentKey app1Key = new ComponentKey(app1.getTargetComponent(), app1.user);
|
||||
ComponentKey app2Key = new ComponentKey(app2.getTargetComponent(), app2.user);
|
||||
mSplitSelectStateController.setLaunchingCuj(cuj);
|
||||
|
||||
@@ -659,8 +659,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC
|
||||
|
||||
// Create a new floating view in Launcher, positioned above the launching icon
|
||||
val drawableArea = launchingIconView.iconDrawableArea
|
||||
val appIcon1 = launchingIconView.info.contents[0].newIcon(launchingIconView.context)
|
||||
val appIcon2 = launchingIconView.info.contents[1].newIcon(launchingIconView.context)
|
||||
val appIcon1 = launchingIconView.info.getFirstApp().newIcon(launchingIconView.context)
|
||||
val appIcon2 = launchingIconView.info.getSecondApp().newIcon(launchingIconView.context)
|
||||
appIcon1.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx)
|
||||
appIcon2.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx)
|
||||
val floatingView =
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package com.android.quickstep.util;
|
||||
|
||||
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
|
||||
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
|
||||
import static com.android.window.flags.Flags.enableDesktopWindowingMode;
|
||||
|
||||
@@ -44,7 +43,7 @@ import com.android.launcher3.anim.PendingAnimation;
|
||||
import com.android.launcher3.config.FeatureFlags;
|
||||
import com.android.launcher3.icons.BitmapInfo;
|
||||
import com.android.launcher3.icons.IconCache;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.AppPairInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.PackageItemInfo;
|
||||
import com.android.launcher3.model.data.WorkspaceItemInfo;
|
||||
@@ -126,7 +125,7 @@ public class SplitToWorkspaceController {
|
||||
intent = appInfo.intent;
|
||||
user = appInfo.user;
|
||||
bitmapInfo = appInfo.bitmap;
|
||||
} else if (tag instanceof FolderInfo fi && fi.itemType == ITEM_TYPE_APP_PAIR) {
|
||||
} else if (tag instanceof AppPairInfo) {
|
||||
// Prompt the user to select something else by wiggling the instructions view
|
||||
mController.getSplitInstructionsView().goBoing();
|
||||
return true;
|
||||
|
||||
@@ -435,8 +435,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@VisibleForTesting
|
||||
public void applyLabel(ItemInfoWithIcon info) {
|
||||
public void applyLabel(ItemInfo info) {
|
||||
CharSequence label = info.title;
|
||||
if (label != null) {
|
||||
mLastOriginalText = label;
|
||||
|
||||
@@ -27,7 +27,7 @@ import android.view.View;
|
||||
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
|
||||
import com.android.launcher3.dragndrop.DragOptions;
|
||||
import com.android.launcher3.logging.StatsLogManager;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.CollectionInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
|
||||
import com.android.launcher3.model.data.WorkspaceItemInfo;
|
||||
@@ -75,7 +75,7 @@ public class DeleteDropTarget extends ButtonDropTarget {
|
||||
}
|
||||
|
||||
return (info instanceof LauncherAppWidgetInfo)
|
||||
|| (info instanceof FolderInfo);
|
||||
|| (info instanceof CollectionInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -202,6 +202,8 @@ import com.android.launcher3.model.ItemInstallQueue;
|
||||
import com.android.launcher3.model.ModelWriter;
|
||||
import com.android.launcher3.model.StringCache;
|
||||
import com.android.launcher3.model.data.AppInfo;
|
||||
import com.android.launcher3.model.data.AppPairInfo;
|
||||
import com.android.launcher3.model.data.CollectionInfo;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
|
||||
@@ -798,13 +800,19 @@ public class Launcher extends StatefulActivity<LauncherState>
|
||||
@Override
|
||||
public void invalidateParent(ItemInfo info) {
|
||||
if (info.container >= 0) {
|
||||
View folderIcon = getWorkspace().getHomescreenIconByItemId(info.container);
|
||||
if (folderIcon instanceof FolderIcon && folderIcon.getTag() instanceof FolderInfo) {
|
||||
View collectionIcon = getWorkspace().getHomescreenIconByItemId(info.container);
|
||||
if (collectionIcon instanceof FolderIcon folderIcon
|
||||
&& collectionIcon.getTag() instanceof FolderInfo) {
|
||||
if (new FolderGridOrganizer(getDeviceProfile())
|
||||
.setFolderInfo((FolderInfo) folderIcon.getTag())
|
||||
.isItemInPreview(info.rank)) {
|
||||
folderIcon.invalidate();
|
||||
}
|
||||
} else if (collectionIcon instanceof AppPairIcon appPairIcon
|
||||
&& collectionIcon.getTag() instanceof AppPairInfo appPairInfo) {
|
||||
if (appPairInfo.getContents().contains(info)) {
|
||||
appPairIcon.getIconDrawableArea().redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2003,24 +2011,26 @@ public class Launcher extends StatefulActivity<LauncherState>
|
||||
public boolean removeItem(View v, final ItemInfo itemInfo, boolean deleteFromDb,
|
||||
@Nullable final String reason) {
|
||||
if (itemInfo instanceof WorkspaceItemInfo) {
|
||||
// Remove the shortcut from the folder before removing it from launcher
|
||||
View folderIcon = mWorkspace.getHomescreenIconByItemId(itemInfo.container);
|
||||
if (folderIcon instanceof FolderIcon) {
|
||||
((FolderInfo) folderIcon.getTag()).remove((WorkspaceItemInfo) itemInfo, true);
|
||||
View collectionIcon = mWorkspace.getHomescreenIconByItemId(itemInfo.container);
|
||||
if (collectionIcon instanceof FolderIcon) {
|
||||
// Remove the shortcut from the folder before removing it from launcher
|
||||
((FolderInfo) collectionIcon.getTag()).remove((WorkspaceItemInfo) itemInfo, true);
|
||||
} else if (collectionIcon instanceof AppPairIcon appPairIcon) {
|
||||
removeItem(appPairIcon, appPairIcon.getInfo(), deleteFromDb,
|
||||
"removing app pair because one of its member apps was removed");
|
||||
} else {
|
||||
mWorkspace.removeWorkspaceItem(v);
|
||||
}
|
||||
if (deleteFromDb) {
|
||||
getModelWriter().deleteItemFromDatabase(itemInfo, reason);
|
||||
}
|
||||
} else if (itemInfo instanceof FolderInfo) {
|
||||
final FolderInfo folderInfo = (FolderInfo) itemInfo;
|
||||
} else if (itemInfo instanceof CollectionInfo ci) {
|
||||
if (v instanceof FolderIcon) {
|
||||
((FolderIcon) v).removeListeners();
|
||||
}
|
||||
mWorkspace.removeWorkspaceItem(v);
|
||||
if (deleteFromDb) {
|
||||
getModelWriter().deleteFolderAndContentsFromDatabase(folderInfo);
|
||||
getModelWriter().deleteCollectionAndContentsFromDatabase(ci);
|
||||
}
|
||||
} else if (itemInfo instanceof LauncherAppWidgetInfo) {
|
||||
final LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo) itemInfo;
|
||||
|
||||
@@ -95,6 +95,7 @@ import com.android.launcher3.logger.LauncherAtom;
|
||||
import com.android.launcher3.logging.InstanceId;
|
||||
import com.android.launcher3.logging.StatsLogManager;
|
||||
import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
|
||||
import com.android.launcher3.model.data.AppPairInfo;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
|
||||
@@ -3313,7 +3314,7 @@ public class Workspace<T extends View & PageIndicator> extends PagedView<T>
|
||||
}
|
||||
} else if (child instanceof FolderIcon) {
|
||||
FolderInfo folderInfo = (FolderInfo) info;
|
||||
List<WorkspaceItemInfo> matches = folderInfo.contents.stream()
|
||||
List<WorkspaceItemInfo> matches = folderInfo.getContents().stream()
|
||||
.filter(matcher)
|
||||
.collect(Collectors.toList());
|
||||
if (!matches.isEmpty()) {
|
||||
@@ -3322,6 +3323,11 @@ public class Workspace<T extends View & PageIndicator> extends PagedView<T>
|
||||
((FolderIcon) child).getFolder().close(false /* animate */);
|
||||
}
|
||||
}
|
||||
} else if (info instanceof AppPairInfo api) {
|
||||
// If an app pair's member apps are being removed, delete the whole app pair.
|
||||
if (api.anyMatch(matcher)) {
|
||||
mLauncher.removeItem(child, info, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3373,9 +3379,9 @@ public class Workspace<T extends View & PageIndicator> extends PagedView<T>
|
||||
}
|
||||
} else if (info instanceof FolderInfo && v instanceof FolderIcon) {
|
||||
FolderInfo fi = (FolderInfo) info;
|
||||
if (fi.contents.stream().anyMatch(matcher)) {
|
||||
if (fi.anyMatch(matcher)) {
|
||||
FolderDotInfo folderDotInfo = new FolderDotInfo();
|
||||
for (WorkspaceItemInfo si : fi.contents) {
|
||||
for (WorkspaceItemInfo si : fi.getContents()) {
|
||||
folderDotInfo.addDotInfo(mLauncher.getDotInfoForItem(si));
|
||||
}
|
||||
((FolderIcon) v).setDotInfo(folderDotInfo);
|
||||
|
||||
@@ -28,7 +28,7 @@ import com.android.launcher3.DropTarget;
|
||||
import com.android.launcher3.LauncherSettings;
|
||||
import com.android.launcher3.dragndrop.DragController;
|
||||
import com.android.launcher3.dragndrop.DragOptions;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.CollectionInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
|
||||
import com.android.launcher3.model.data.WorkspaceItemInfo;
|
||||
@@ -45,6 +45,7 @@ public abstract class BaseAccessibilityDelegate<T extends Context & ActivityCont
|
||||
public enum DragType {
|
||||
ICON,
|
||||
FOLDER,
|
||||
APP_PAIR,
|
||||
WIDGET
|
||||
}
|
||||
|
||||
@@ -103,7 +104,7 @@ public abstract class BaseAccessibilityDelegate<T extends Context & ActivityCont
|
||||
&& item.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
|
||||
}
|
||||
return (item instanceof LauncherAppWidgetInfo)
|
||||
|| (item instanceof FolderInfo);
|
||||
|| (item instanceof CollectionInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -39,6 +39,8 @@ import com.android.launcher3.dragndrop.DragView;
|
||||
import com.android.launcher3.folder.Folder;
|
||||
import com.android.launcher3.keyboard.KeyboardDragAndDropView;
|
||||
import com.android.launcher3.model.data.AppInfo;
|
||||
import com.android.launcher3.model.data.AppPairInfo;
|
||||
import com.android.launcher3.model.data.CollectionInfo;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
|
||||
@@ -317,6 +319,8 @@ public class LauncherAccessibilityDelegate extends BaseAccessibilityDelegate<Lau
|
||||
mDragInfo.dragType = DragType.ICON;
|
||||
if (info instanceof FolderInfo) {
|
||||
mDragInfo.dragType = DragType.FOLDER;
|
||||
} else if (info instanceof AppPairInfo) {
|
||||
mDragInfo.dragType = DragType.APP_PAIR;
|
||||
} else if (info instanceof LauncherAppWidgetInfo) {
|
||||
mDragInfo.dragType = DragType.WIDGET;
|
||||
}
|
||||
@@ -430,16 +434,16 @@ public class LauncherAccessibilityDelegate extends BaseAccessibilityDelegate<Lau
|
||||
LauncherSettings.Favorites.CONTAINER_DESKTOP,
|
||||
screenId, coordinates[0], coordinates[1]);
|
||||
bindItem(info, accessibility, finishCallback);
|
||||
} else if (item instanceof FolderInfo fi) {
|
||||
} else if (item instanceof CollectionInfo ci) {
|
||||
Workspace<?> workspace = mContext.getWorkspace();
|
||||
workspace.snapToPage(workspace.getPageIndexForScreenId(screenId));
|
||||
mContext.getModelWriter().addItemToDatabase(fi,
|
||||
mContext.getModelWriter().addItemToDatabase(ci,
|
||||
LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, coordinates[0],
|
||||
coordinates[1]);
|
||||
fi.contents.forEach(member -> {
|
||||
mContext.getModelWriter().addItemToDatabase(member, fi.id, -1, -1, -1);
|
||||
});
|
||||
bindItem(fi, accessibility, finishCallback);
|
||||
ci.getContents().forEach(member ->
|
||||
mContext.getModelWriter()
|
||||
.addItemToDatabase(member, ci.id, -1, -1, -1));
|
||||
bindItem(ci, accessibility, finishCallback);
|
||||
}
|
||||
}));
|
||||
return true;
|
||||
|
||||
@@ -149,7 +149,7 @@ public class WorkspaceAccessibilityHelper extends DragAndDropAccessibilityDelega
|
||||
// Find the first item in the folder.
|
||||
FolderInfo folder = (FolderInfo) info;
|
||||
WorkspaceItemInfo firstItem = null;
|
||||
for (WorkspaceItemInfo shortcut : folder.contents) {
|
||||
for (WorkspaceItemInfo shortcut : folder.getContents()) {
|
||||
if (firstItem == null || firstItem.rank > shortcut.rank) {
|
||||
firstItem = shortcut;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ import com.android.launcher3.DeviceProfile;
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.Reorderable;
|
||||
import com.android.launcher3.dragndrop.DraggableView;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.AppPairInfo;
|
||||
import com.android.launcher3.model.data.WorkspaceItemInfo;
|
||||
import com.android.launcher3.util.MultiTranslateDelegate;
|
||||
import com.android.launcher3.views.ActivityContext;
|
||||
@@ -50,17 +50,12 @@ import java.util.function.Predicate;
|
||||
public class AppPairIcon extends FrameLayout implements DraggableView, Reorderable {
|
||||
private static final String TAG = "AppPairIcon";
|
||||
|
||||
/**
|
||||
* Indicates that the app pair is currently launchable on the current screen.
|
||||
*/
|
||||
private boolean mIsLaunchableAtScreenSize = true;
|
||||
|
||||
// A view that holds the app pair icon graphic.
|
||||
private AppPairIconGraphic mIconGraphic;
|
||||
// A view that holds the app pair's title.
|
||||
private BubbleTextView mAppPairName;
|
||||
// The underlying ItemInfo that stores info about the app pair members, etc.
|
||||
private FolderInfo mInfo;
|
||||
private AppPairInfo mInfo;
|
||||
// The containing element that holds this icon: workspace, taskbar, folder, etc. Affects certain
|
||||
// aspects of how the icon is drawn.
|
||||
private int mContainer;
|
||||
@@ -81,7 +76,7 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab
|
||||
* Builds an AppPairIcon to be added to the Launcher.
|
||||
*/
|
||||
public static AppPairIcon inflateIcon(int resId, ActivityContext activity,
|
||||
@Nullable ViewGroup group, FolderInfo appPairInfo, int container) {
|
||||
@Nullable ViewGroup group, AppPairInfo appPairInfo, int container) {
|
||||
DeviceProfile grid = activity.getDeviceProfile();
|
||||
LayoutInflater inflater = (group != null)
|
||||
? LayoutInflater.from(group.getContext())
|
||||
@@ -89,7 +84,7 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab
|
||||
AppPairIcon icon = (AppPairIcon) inflater.inflate(resId, group, false);
|
||||
|
||||
// Sort contents, so that left-hand app comes first
|
||||
appPairInfo.contents.sort(Comparator.comparingInt(a -> a.rank));
|
||||
appPairInfo.getContents().sort(Comparator.comparingInt(a -> a.rank));
|
||||
|
||||
icon.setTag(appPairInfo);
|
||||
icon.setOnClickListener(activity.getItemOnClickListener());
|
||||
@@ -100,8 +95,6 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab
|
||||
icon.mIconGraphic = icon.findViewById(R.id.app_pair_icon_graphic);
|
||||
icon.mIconGraphic.init(icon, container);
|
||||
|
||||
icon.checkDisabledState();
|
||||
|
||||
// Set up app pair title
|
||||
icon.mAppPairName = icon.findViewById(R.id.app_pair_icon_name);
|
||||
FrameLayout.LayoutParams lp =
|
||||
@@ -115,7 +108,7 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab
|
||||
// For some reason, app icons have setIncludeFontPadding(false) inside folders, so we set it
|
||||
// here to match that.
|
||||
icon.mAppPairName.setIncludeFontPadding(container != DISPLAY_FOLDER);
|
||||
icon.mAppPairName.setText(appPairInfo.title);
|
||||
icon.mAppPairName.applyLabel(appPairInfo);
|
||||
|
||||
// Set up accessibility
|
||||
icon.setContentDescription(icon.getAccessibilityTitle(appPairInfo));
|
||||
@@ -127,9 +120,9 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab
|
||||
/**
|
||||
* Returns a formatted accessibility title for app pairs.
|
||||
*/
|
||||
public String getAccessibilityTitle(FolderInfo appPairInfo) {
|
||||
CharSequence app1 = appPairInfo.contents.get(0).title;
|
||||
CharSequence app2 = appPairInfo.contents.get(1).title;
|
||||
public String getAccessibilityTitle(AppPairInfo appPairInfo) {
|
||||
CharSequence app1 = appPairInfo.getFirstApp().title;
|
||||
CharSequence app2 = appPairInfo.getSecondApp().title;
|
||||
return getContext().getString(R.string.app_pair_name_format, app1, app2);
|
||||
}
|
||||
|
||||
@@ -174,7 +167,7 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab
|
||||
return mScaleForReorderBounce;
|
||||
}
|
||||
|
||||
public FolderInfo getInfo() {
|
||||
public AppPairInfo getInfo() {
|
||||
return mInfo;
|
||||
}
|
||||
|
||||
@@ -186,41 +179,20 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab
|
||||
return mIconGraphic;
|
||||
}
|
||||
|
||||
public boolean isLaunchableAtScreenSize() {
|
||||
return mIsLaunchableAtScreenSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the "disabled" state of the app pair in the current device configuration.
|
||||
* App pairs can be "disabled" in two ways:
|
||||
* 1) One of the member WorkspaceItemInfos is disabled (i.e. the app software itself is paused
|
||||
* by the user or can't be launched for some other reason).
|
||||
* 2) This specific instance of an app pair can't be launched due to screen size requirements.
|
||||
*/
|
||||
public void checkDisabledState() {
|
||||
DeviceProfile dp = ActivityContext.lookupContext(getContext()).getDeviceProfile();
|
||||
// If user is on a small screen, we can't launch if either of the apps is non-resizeable
|
||||
mIsLaunchableAtScreenSize =
|
||||
dp.isTablet || getInfo().contents.stream().noneMatch(
|
||||
wii -> wii.hasStatusFlag(WorkspaceItemInfo.FLAG_NON_RESIZEABLE));
|
||||
// Invalidate to update icons
|
||||
mIconGraphic.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when WorkspaceItemInfos get updated, and the app pair icon may need to be redrawn.
|
||||
*/
|
||||
public void maybeRedrawForWorkspaceUpdate(Predicate<WorkspaceItemInfo> itemCheck) {
|
||||
// If either of the app pair icons return true on the predicate (i.e. in the list of
|
||||
// updated apps), redraw the icon graphic (icon background and both icons).
|
||||
if (getInfo().contents.stream().anyMatch(itemCheck)) {
|
||||
checkDisabledState();
|
||||
if (getInfo().anyMatch(itemCheck)) {
|
||||
mIconGraphic.redraw();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inside folders, icons are vertically centered in their rows. See
|
||||
* {@link BubbleTextView#onMeasure(int, int)} for comparison.
|
||||
* {@link BubbleTextView} for comparison.
|
||||
*/
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
|
||||
@@ -23,12 +23,12 @@ import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.OpenForTesting
|
||||
import com.android.launcher3.DeviceProfile
|
||||
import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener
|
||||
import com.android.launcher3.icons.BitmapInfo
|
||||
import com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter
|
||||
import com.android.launcher3.model.data.FolderInfo
|
||||
import com.android.launcher3.model.data.WorkspaceItemInfo
|
||||
import com.android.launcher3.model.data.AppPairInfo
|
||||
import com.android.launcher3.util.Themes
|
||||
import com.android.launcher3.views.ActivityContext
|
||||
|
||||
@@ -36,29 +36,32 @@ import com.android.launcher3.views.ActivityContext
|
||||
* A FrameLayout marking the area on an [AppPairIcon] where the visual icon will be drawn. One of
|
||||
* two child UI elements on an [AppPairIcon], along with a BubbleTextView holding the text title.
|
||||
*/
|
||||
class AppPairIconGraphic @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
@OpenForTesting
|
||||
open class AppPairIconGraphic
|
||||
@JvmOverloads
|
||||
constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
FrameLayout(context, attrs), OnDeviceProfileChangeListener {
|
||||
private val TAG = "AppPairIconGraphic"
|
||||
|
||||
companion object {
|
||||
/** Composes a drawable for this icon, consisting of a background and 2 app icons. */
|
||||
/**
|
||||
* Composes a drawable for this icon, consisting of a background and 2 app icons. The app
|
||||
* pair will draw as "disabled" if either of the following is true:
|
||||
* 1) One of the member WorkspaceItemInfos is disabled (i.e. the app software itself is
|
||||
* paused or can't be launched for some other reason).
|
||||
* 2) One of the member apps can't be launched due to screen size requirements.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun composeDrawable(appPairInfo: FolderInfo, p: AppPairIconDrawingParams): Drawable {
|
||||
fun composeDrawable(appPairInfo: AppPairInfo, p: AppPairIconDrawingParams): Drawable {
|
||||
// Generate new icons, using themed flag if needed.
|
||||
val flags = if (Themes.isThemedIconEnabled(p.context)) BitmapInfo.FLAG_THEMED else 0
|
||||
val appIcon1 = appPairInfo.contents[0].newIcon(p.context, flags)
|
||||
val appIcon2 = appPairInfo.contents[1].newIcon(p.context, flags)
|
||||
val appIcon1 = appPairInfo.getFirstApp().newIcon(p.context, flags)
|
||||
val appIcon2 = appPairInfo.getSecondApp().newIcon(p.context, flags)
|
||||
appIcon1.setBounds(0, 0, p.memberIconSize.toInt(), p.memberIconSize.toInt())
|
||||
appIcon2.setBounds(0, 0, p.memberIconSize.toInt(), p.memberIconSize.toInt())
|
||||
|
||||
// Check disabled status.
|
||||
val activity: ActivityContext = ActivityContext.lookupContext(p.context)
|
||||
val isLaunchableAtScreenSize =
|
||||
activity.deviceProfile.isTablet ||
|
||||
appPairInfo.contents.stream().noneMatch { wii: WorkspaceItemInfo ->
|
||||
wii.hasStatusFlag(WorkspaceItemInfo.FLAG_NON_RESIZEABLE)
|
||||
}
|
||||
val shouldDrawAsDisabled = appPairInfo.isDisabled || !isLaunchableAtScreenSize
|
||||
val shouldDrawAsDisabled =
|
||||
appPairInfo.isDisabled || !appPairInfo.isLaunchable(p.context)
|
||||
|
||||
// Set disabled status on icons.
|
||||
appIcon1.setIsDisabled(shouldDrawAsDisabled)
|
||||
@@ -124,7 +127,6 @@ class AppPairIconGraphic @JvmOverloads constructor(context: Context, attrs: Attr
|
||||
*/
|
||||
fun getIconBounds(outBounds: Rect) {
|
||||
outBounds.set(0, 0, drawParams.backgroundSize.toInt(), drawParams.backgroundSize.toInt())
|
||||
|
||||
outBounds.offset(
|
||||
// x-coordinate in parent's coordinate system
|
||||
((parentIcon.width - drawParams.backgroundSize) / 2).toInt(),
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.android.launcher3.DragSource;
|
||||
import com.android.launcher3.DropTarget;
|
||||
import com.android.launcher3.Flags;
|
||||
import com.android.launcher3.logging.InstanceId;
|
||||
import com.android.launcher3.model.data.AppPairInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.ItemInfoWithIcon;
|
||||
import com.android.launcher3.model.data.WorkspaceItemInfo;
|
||||
@@ -289,7 +290,8 @@ public abstract class DragController<T extends ActivityContext>
|
||||
// Cancel the current drag if we are removing an app that we are dragging
|
||||
if (mDragObject != null) {
|
||||
ItemInfo dragInfo = mDragObject.dragInfo;
|
||||
if (dragInfo instanceof WorkspaceItemInfo && matcher.test(dragInfo)) {
|
||||
if ((dragInfo instanceof WorkspaceItemInfo && matcher.test(dragInfo))
|
||||
|| (dragInfo instanceof AppPairInfo api && api.anyMatch(matcher))) {
|
||||
cancelDrag();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,7 +493,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
|
||||
mInfo = info;
|
||||
mFromTitle = info.title;
|
||||
mFromLabelState = info.getFromLabelState();
|
||||
ArrayList<WorkspaceItemInfo> children = info.contents;
|
||||
ArrayList<WorkspaceItemInfo> children = info.getContents();
|
||||
Collections.sort(children, ITEM_POS_COMPARATOR);
|
||||
updateItemLocationsInDatabaseBatch(true);
|
||||
|
||||
@@ -626,7 +626,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
|
||||
// onDropComplete. Perform cleanup once drag-n-drop ends.
|
||||
mDragController.addDragListener(this);
|
||||
|
||||
ArrayList<WorkspaceItemInfo> items = new ArrayList<>(mInfo.contents);
|
||||
ArrayList<WorkspaceItemInfo> items = new ArrayList<>(mInfo.getContents());
|
||||
mEmptyCellRank = items.size();
|
||||
items.add(null); // Add an empty spot at the end
|
||||
|
||||
@@ -639,7 +639,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
|
||||
* is played.
|
||||
*/
|
||||
public void animateOpen() {
|
||||
animateOpen(mInfo.contents, 0);
|
||||
animateOpen(mInfo.getContents(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1097,9 +1097,9 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
|
||||
mActivityContext.getDeviceProfile()).setFolderInfo(mInfo);
|
||||
|
||||
ArrayList<ItemInfo> items = new ArrayList<>();
|
||||
int total = mInfo.contents.size();
|
||||
int total = mInfo.getContents().size();
|
||||
for (int i = 0; i < total; i++) {
|
||||
WorkspaceItemInfo itemInfo = mInfo.contents.get(i);
|
||||
WorkspaceItemInfo itemInfo = mInfo.getContents().get(i);
|
||||
if (verifier.updateRankAndPos(itemInfo, i)) {
|
||||
items.add(itemInfo);
|
||||
}
|
||||
@@ -1113,7 +1113,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
|
||||
FolderNameInfos nameInfos = new FolderNameInfos();
|
||||
FolderNameProvider fnp = FolderNameProvider.newInstance(getContext());
|
||||
fnp.getSuggestedFolderName(
|
||||
getContext(), mInfo.contents, nameInfos);
|
||||
getContext(), mInfo.getContents(), nameInfos);
|
||||
mInfo.suggestedFolderNames = nameInfos;
|
||||
});
|
||||
}
|
||||
@@ -1217,7 +1217,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
|
||||
}
|
||||
|
||||
public int getItemCount() {
|
||||
return mInfo.contents.size();
|
||||
return mInfo.getContents().size();
|
||||
}
|
||||
|
||||
void replaceFolderWithFinalItem() {
|
||||
|
||||
@@ -57,7 +57,7 @@ public class FolderGridOrganizer {
|
||||
* Updates the organizer with the provided folder info
|
||||
*/
|
||||
public FolderGridOrganizer setFolderInfo(FolderInfo info) {
|
||||
return setContentSize(info.contents.size());
|
||||
return setContentSize(info.getContents().size());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -215,7 +215,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel
|
||||
|
||||
// Keep the notification dot up to date with the sum of all the content's dots.
|
||||
FolderDotInfo folderDotInfo = new FolderDotInfo();
|
||||
for (WorkspaceItemInfo si : folderInfo.contents) {
|
||||
for (WorkspaceItemInfo si : folderInfo.getContents()) {
|
||||
folderDotInfo.addDotInfo(activity.getDotInfoForItem(si));
|
||||
}
|
||||
icon.setDotInfo(folderDotInfo);
|
||||
@@ -422,7 +422,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel
|
||||
FolderNameInfos nameInfos = new FolderNameInfos();
|
||||
Executors.MODEL_EXECUTOR.post(() -> {
|
||||
d.folderNameProvider.getSuggestedFolderName(
|
||||
getContext(), mInfo.contents, nameInfos);
|
||||
getContext(), mInfo.getContents(), nameInfos);
|
||||
postDelayed(() -> {
|
||||
setLabelSuggestion(nameInfos, d.logInstanceId);
|
||||
invalidate();
|
||||
@@ -487,7 +487,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel
|
||||
}
|
||||
mFolder.notifyDrop();
|
||||
onDrop(item, d, null, 1.0f,
|
||||
itemReturnedOnFailedDrop ? item.rank : mInfo.contents.size(),
|
||||
itemReturnedOnFailedDrop ? item.rank : mInfo.getContents().size(),
|
||||
itemReturnedOnFailedDrop
|
||||
);
|
||||
}
|
||||
@@ -666,7 +666,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel
|
||||
* Returns the list of items which should be visible in the preview
|
||||
*/
|
||||
public List<WorkspaceItemInfo> getPreviewItemsOnPage(int page) {
|
||||
return mPreviewVerifier.setFolderInfo(mInfo).previewItemsForPage(page, mInfo.contents);
|
||||
return mPreviewVerifier.setFolderInfo(mInfo).previewItemsForPage(page, mInfo.getContents());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -809,7 +809,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel
|
||||
* Returns a formatted accessibility title for folder
|
||||
*/
|
||||
public String getAccessiblityTitle(CharSequence title) {
|
||||
int size = mInfo.contents.size();
|
||||
int size = mInfo.getContents().size();
|
||||
if (size < MAX_NUM_ITEMS_IN_PREVIEW) {
|
||||
return getContext().getString(R.string.folder_name_format_exact, title, size);
|
||||
} else {
|
||||
|
||||
@@ -35,7 +35,7 @@ import com.android.launcher3.model.BaseModelUpdateTask;
|
||||
import com.android.launcher3.model.BgDataModel;
|
||||
import com.android.launcher3.model.StringCache;
|
||||
import com.android.launcher3.model.data.AppInfo;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.CollectionInfo;
|
||||
import com.android.launcher3.model.data.WorkspaceItemInfo;
|
||||
import com.android.launcher3.util.IntSparseArrayMap;
|
||||
import com.android.launcher3.util.Preconditions;
|
||||
@@ -62,7 +62,7 @@ public class FolderNameProvider implements ResourceBasedOverride {
|
||||
* name edit box can also be used to provide suggestion.
|
||||
*/
|
||||
public static final int SUGGEST_MAX = 4;
|
||||
protected IntSparseArrayMap<FolderInfo> mFolderInfos;
|
||||
protected IntSparseArrayMap<CollectionInfo> mCollectionInfos;
|
||||
protected List<AppInfo> mAppInfos;
|
||||
|
||||
/**
|
||||
@@ -79,7 +79,7 @@ public class FolderNameProvider implements ResourceBasedOverride {
|
||||
}
|
||||
|
||||
public static FolderNameProvider newInstance(Context context, List<AppInfo> appInfos,
|
||||
IntSparseArrayMap<FolderInfo> folderInfos) {
|
||||
IntSparseArrayMap<CollectionInfo> folderInfos) {
|
||||
Preconditions.assertWorkerThread();
|
||||
FolderNameProvider fnp = Overrides.getObject(FolderNameProvider.class,
|
||||
context.getApplicationContext(), R.string.folder_name_provider_class);
|
||||
@@ -93,9 +93,9 @@ public class FolderNameProvider implements ResourceBasedOverride {
|
||||
new FolderNameWorker());
|
||||
}
|
||||
|
||||
private void load(List<AppInfo> appInfos, IntSparseArrayMap<FolderInfo> folderInfos) {
|
||||
private void load(List<AppInfo> appInfos, IntSparseArrayMap<CollectionInfo> folderInfos) {
|
||||
mAppInfos = appInfos;
|
||||
mFolderInfos = folderInfos;
|
||||
mCollectionInfos = folderInfos;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,7 +195,7 @@ public class FolderNameProvider implements ResourceBasedOverride {
|
||||
@Override
|
||||
public void execute(@NonNull final LauncherAppState app,
|
||||
@NonNull final BgDataModel dataModel, @NonNull final AllAppsList apps) {
|
||||
mFolderInfos = dataModel.folders.clone();
|
||||
mCollectionInfos = dataModel.collections.clone();
|
||||
mAppInfos = Arrays.asList(apps.copyData());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ public class LauncherDelegate {
|
||||
// folder
|
||||
CellLayout cellLayout = mLauncher.getCellLayout(info.container,
|
||||
mLauncher.getCellPosMapper().mapModelToPresenter(info).screenId);
|
||||
finalItem = info.contents.remove(0);
|
||||
finalItem = info.getContents().remove(0);
|
||||
newIcon = mLauncher.getItemInflater().inflateItem(
|
||||
finalItem, mLauncher.getModelWriter(), cellLayout);
|
||||
mLauncher.getModelWriter().addOrMoveItemInDatabase(finalItem,
|
||||
|
||||
@@ -82,6 +82,8 @@ import com.android.launcher3.model.BgDataModel;
|
||||
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
|
||||
import com.android.launcher3.model.WidgetItem;
|
||||
import com.android.launcher3.model.WidgetsModel;
|
||||
import com.android.launcher3.model.data.AppPairInfo;
|
||||
import com.android.launcher3.model.data.CollectionInfo;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
|
||||
@@ -388,16 +390,16 @@ public class LauncherPreviewRenderer extends ContextWrapper
|
||||
addInScreenFromBind(icon, info);
|
||||
}
|
||||
|
||||
private void inflateAndAddCollectionIcon(FolderInfo info) {
|
||||
private void inflateAndAddCollectionIcon(CollectionInfo info) {
|
||||
boolean isOnDesktop = info.container == Favorites.CONTAINER_DESKTOP;
|
||||
CellLayout screen = isOnDesktop
|
||||
? mWorkspaceScreens.get(info.screenId)
|
||||
: mHotseat;
|
||||
FrameLayout folderIcon = info.itemType == Favorites.ITEM_TYPE_FOLDER
|
||||
? FolderIcon.inflateIcon(R.layout.folder_icon, this, screen, info)
|
||||
: AppPairIcon.inflateIcon(R.layout.app_pair_icon, this, screen, info,
|
||||
FrameLayout collectionIcon = info.itemType == Favorites.ITEM_TYPE_FOLDER
|
||||
? FolderIcon.inflateIcon(R.layout.folder_icon, this, screen, (FolderInfo) info)
|
||||
: AppPairIcon.inflateIcon(R.layout.app_pair_icon, this, screen, (AppPairInfo) info,
|
||||
isOnDesktop ? DISPLAY_WORKSPACE : DISPLAY_TASKBAR);
|
||||
addInScreenFromBind(folderIcon, info);
|
||||
addInScreenFromBind(collectionIcon, info);
|
||||
}
|
||||
|
||||
private void inflateAndAddWidgets(
|
||||
@@ -501,7 +503,7 @@ public class LauncherPreviewRenderer extends ContextWrapper
|
||||
break;
|
||||
case Favorites.ITEM_TYPE_FOLDER:
|
||||
case Favorites.ITEM_TYPE_APP_PAIR:
|
||||
inflateAndAddCollectionIcon((FolderInfo) itemInfo);
|
||||
inflateAndAddCollectionIcon((CollectionInfo) itemInfo);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -31,7 +31,7 @@ import com.android.launcher3.LauncherSettings;
|
||||
import com.android.launcher3.logging.FileLog;
|
||||
import com.android.launcher3.model.BgDataModel.Callbacks;
|
||||
import com.android.launcher3.model.data.AppInfo;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.CollectionInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.ItemInfoWithIcon;
|
||||
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
|
||||
@@ -131,8 +131,8 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask {
|
||||
int screenId = coords[0];
|
||||
|
||||
ItemInfo itemInfo;
|
||||
if (item instanceof WorkspaceItemInfo || item instanceof FolderInfo ||
|
||||
item instanceof LauncherAppWidgetInfo) {
|
||||
if (item instanceof WorkspaceItemInfo || item instanceof CollectionInfo
|
||||
|| item instanceof LauncherAppWidgetInfo) {
|
||||
itemInfo = item;
|
||||
} else if (item instanceof WorkspaceItemFactory) {
|
||||
itemInfo = ((WorkspaceItemFactory) item).makeWorkspaceItem(app.getContext());
|
||||
|
||||
@@ -44,6 +44,7 @@ import com.android.launcher3.LauncherSettings.Favorites;
|
||||
import com.android.launcher3.Workspace;
|
||||
import com.android.launcher3.config.FeatureFlags;
|
||||
import com.android.launcher3.model.data.AppInfo;
|
||||
import com.android.launcher3.model.data.CollectionInfo;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
|
||||
@@ -102,9 +103,9 @@ public class BgDataModel {
|
||||
public final ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Map of id to FolderInfos of all the folders created by LauncherModel
|
||||
* Map of id to CollectionInfos of all the folders or app pairs created by LauncherModel
|
||||
*/
|
||||
public final IntSparseArrayMap<FolderInfo> folders = new IntSparseArrayMap<>();
|
||||
public final IntSparseArrayMap<CollectionInfo> collections = new IntSparseArrayMap<>();
|
||||
|
||||
/**
|
||||
* Extra container based items
|
||||
@@ -144,7 +145,7 @@ public class BgDataModel {
|
||||
public synchronized void clear() {
|
||||
workspaceItems.clear();
|
||||
appWidgets.clear();
|
||||
folders.clear();
|
||||
collections.clear();
|
||||
itemsIdMap.clear();
|
||||
deepShortcutMap.clear();
|
||||
extraItems.clear();
|
||||
@@ -179,9 +180,9 @@ public class BgDataModel {
|
||||
for (int i = 0; i < appWidgets.size(); i++) {
|
||||
writer.println(prefix + '\t' + appWidgets.get(i).toString());
|
||||
}
|
||||
writer.println(prefix + " ---- folder items ");
|
||||
for (int i = 0; i < folders.size(); i++) {
|
||||
writer.println(prefix + '\t' + folders.valueAt(i).toString());
|
||||
writer.println(prefix + " ---- collection items ");
|
||||
for (int i = 0; i < collections.size(); i++) {
|
||||
writer.println(prefix + '\t' + collections.valueAt(i).toString());
|
||||
}
|
||||
writer.println(prefix + " ---- extra items ");
|
||||
for (int i = 0; i < extraItems.size(); i++) {
|
||||
@@ -211,12 +212,12 @@ public class BgDataModel {
|
||||
switch (item.itemType) {
|
||||
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
|
||||
case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR:
|
||||
folders.remove(item.id);
|
||||
collections.remove(item.id);
|
||||
if (FeatureFlags.IS_STUDIO_BUILD) {
|
||||
for (ItemInfo info : itemsIdMap) {
|
||||
if (info.container == item.id) {
|
||||
// We are deleting a folder which still contains items that
|
||||
// think they are contained by that folder.
|
||||
// We are deleting a collection which still contains items that
|
||||
// think they are contained by that collection.
|
||||
String msg = "deleting a collection (" + item + ") which still "
|
||||
+ "contains items (" + info + ")";
|
||||
Log.e(TAG, msg);
|
||||
@@ -259,7 +260,7 @@ public class BgDataModel {
|
||||
switch (item.itemType) {
|
||||
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
|
||||
case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR:
|
||||
folders.put(item.id, (FolderInfo) item);
|
||||
collections.put(item.id, (CollectionInfo) item);
|
||||
workspaceItems.add(item);
|
||||
break;
|
||||
case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
|
||||
@@ -269,14 +270,14 @@ public class BgDataModel {
|
||||
workspaceItems.add(item);
|
||||
} else {
|
||||
if (newItem) {
|
||||
if (!folders.containsKey(item.container)) {
|
||||
if (!collections.containsKey(item.container)) {
|
||||
// Adding an item to a nonexistent collection.
|
||||
String msg = "attempted to add item: " + item + " to a nonexistent app"
|
||||
+ " collection";
|
||||
Log.e(TAG, msg);
|
||||
}
|
||||
} else {
|
||||
findOrMakeFolder(item.container).add((WorkspaceItemInfo) item, false);
|
||||
findOrMakeFolder(item.container).add((WorkspaceItemInfo) item);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -371,15 +372,18 @@ public class BgDataModel {
|
||||
* Return an existing FolderInfo object if we have encountered this ID previously,
|
||||
* or make a new one.
|
||||
*/
|
||||
public synchronized FolderInfo findOrMakeFolder(int id) {
|
||||
public synchronized CollectionInfo findOrMakeFolder(int id) {
|
||||
// See if a placeholder was created for us already
|
||||
FolderInfo folderInfo = folders.get(id);
|
||||
if (folderInfo == null) {
|
||||
// No placeholder -- create a new instance
|
||||
folderInfo = new FolderInfo();
|
||||
folders.put(id, folderInfo);
|
||||
CollectionInfo collectionInfo = collections.get(id);
|
||||
if (collectionInfo == null) {
|
||||
// No placeholder -- create a new blank folder instance. At this point, we don't know
|
||||
// if the desired container is supposed to be a folder or an app pair. In the case that
|
||||
// it is an app pair, the blank folder will be replaced by a blank app pair when the app
|
||||
// pair is getting processed, in WorkspaceItemProcessor.processFolderOrAppPair().
|
||||
collectionInfo = new FolderInfo();
|
||||
collections.put(id, collectionInfo);
|
||||
}
|
||||
return folderInfo;
|
||||
return collectionInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,6 +36,7 @@ import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.android.launcher3.LauncherSettings;
|
||||
import com.android.launcher3.model.data.CollectionInfo;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
|
||||
@@ -67,7 +68,8 @@ public class FirstScreenBroadcast {
|
||||
private static final String ACTION_FIRST_SCREEN_ACTIVE_INSTALLS
|
||||
= "com.android.launcher3.action.FIRST_SCREEN_ACTIVE_INSTALLS";
|
||||
|
||||
private static final String FOLDER_ITEM_EXTRA = "folderItem";
|
||||
// String retained as "folderItem" for back-compatibility reasons.
|
||||
private static final String COLLECTION_ITEM_EXTRA = "folderItem";
|
||||
private static final String WORKSPACE_ITEM_EXTRA = "workspaceItem";
|
||||
private static final String HOTSEAT_ITEM_EXTRA = "hotseatItem";
|
||||
private static final String WIDGET_ITEM_EXTRA = "widgetItem";
|
||||
@@ -105,20 +107,19 @@ public class FirstScreenBroadcast {
|
||||
@WorkerThread
|
||||
private void sendBroadcastToInstaller(Context context, String installerPackageName,
|
||||
Set<String> packages, List<ItemInfo> firstScreenItems) {
|
||||
Set<String> folderItems = new HashSet<>();
|
||||
Set<String> collectionItems = new HashSet<>();
|
||||
Set<String> workspaceItems = new HashSet<>();
|
||||
Set<String> hotseatItems = new HashSet<>();
|
||||
Set<String> widgetItems = new HashSet<>();
|
||||
|
||||
for (ItemInfo info : firstScreenItems) {
|
||||
if (info instanceof FolderInfo) {
|
||||
FolderInfo folderInfo = (FolderInfo) info;
|
||||
String folderItemInfoPackage;
|
||||
for (ItemInfo folderItemInfo : cloneOnMainThread(folderInfo.contents)) {
|
||||
folderItemInfoPackage = getPackageName(folderItemInfo);
|
||||
if (folderItemInfoPackage != null
|
||||
&& packages.contains(folderItemInfoPackage)) {
|
||||
folderItems.add(folderItemInfoPackage);
|
||||
if (info instanceof CollectionInfo ci) {
|
||||
String collectionItemInfoPackage;
|
||||
for (ItemInfo collectionItemInfo : cloneOnMainThread(ci.getContents())) {
|
||||
collectionItemInfoPackage = getPackageName(collectionItemInfo);
|
||||
if (collectionItemInfoPackage != null
|
||||
&& packages.contains(collectionItemInfoPackage)) {
|
||||
collectionItems.add(collectionItemInfoPackage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,13 +138,13 @@ public class FirstScreenBroadcast {
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
printList(installerPackageName, "Folder item", folderItems);
|
||||
printList(installerPackageName, "Collection item", collectionItems);
|
||||
printList(installerPackageName, "Workspace item", workspaceItems);
|
||||
printList(installerPackageName, "Hotseat item", hotseatItems);
|
||||
printList(installerPackageName, "Widget item", widgetItems);
|
||||
}
|
||||
|
||||
if (folderItems.isEmpty()
|
||||
if (collectionItems.isEmpty()
|
||||
&& workspaceItems.isEmpty()
|
||||
&& hotseatItems.isEmpty()
|
||||
&& widgetItems.isEmpty()) {
|
||||
@@ -152,7 +153,7 @@ public class FirstScreenBroadcast {
|
||||
}
|
||||
context.sendBroadcast(new Intent(ACTION_FIRST_SCREEN_ACTIVE_INSTALLS)
|
||||
.setPackage(installerPackageName)
|
||||
.putStringArrayListExtra(FOLDER_ITEM_EXTRA, new ArrayList<>(folderItems))
|
||||
.putStringArrayListExtra(COLLECTION_ITEM_EXTRA, new ArrayList<>(collectionItems))
|
||||
.putStringArrayListExtra(WORKSPACE_ITEM_EXTRA, new ArrayList<>(workspaceItems))
|
||||
.putStringArrayListExtra(HOTSEAT_ITEM_EXTRA, new ArrayList<>(hotseatItems))
|
||||
.putStringArrayListExtra(WIDGET_ITEM_EXTRA, new ArrayList<>(widgetItems))
|
||||
@@ -180,7 +181,7 @@ public class FirstScreenBroadcast {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the provided list on UI thread. This is used for {@link FolderInfo#contents} which
|
||||
* Clone the provided list on UI thread. This is used for {@link FolderInfo#getContents()} which
|
||||
* is always modified on UI thread.
|
||||
*/
|
||||
@AnyThread
|
||||
|
||||
@@ -20,7 +20,6 @@ import static com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN;
|
||||
import static com.android.launcher3.Flags.enableLauncherBrMetricsFixed;
|
||||
import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE;
|
||||
import static com.android.launcher3.LauncherPrefs.SHOULD_SHOW_SMARTSPACE;
|
||||
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
|
||||
import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
|
||||
import static com.android.launcher3.config.FeatureFlags.ENABLE_SMARTSPACE_REMOVAL;
|
||||
import static com.android.launcher3.config.FeatureFlags.SMARTSPACE_AS_A_WIDGET;
|
||||
@@ -77,9 +76,12 @@ import com.android.launcher3.icons.ShortcutCachingLogic;
|
||||
import com.android.launcher3.icons.cache.IconCacheUpdateHandler;
|
||||
import com.android.launcher3.logging.FileLog;
|
||||
import com.android.launcher3.model.data.AppInfo;
|
||||
import com.android.launcher3.model.data.AppPairInfo;
|
||||
import com.android.launcher3.model.data.CollectionInfo;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.IconRequestInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.ItemInfoWithIcon;
|
||||
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
|
||||
import com.android.launcher3.model.data.WorkspaceItemInfo;
|
||||
import com.android.launcher3.pm.InstallSessionHelper;
|
||||
@@ -99,7 +101,6 @@ import com.android.launcher3.util.TraceHelper;
|
||||
import com.android.launcher3.widget.WidgetInflater;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -234,6 +235,7 @@ public class LoaderTask implements Runnable {
|
||||
if (Objects.equals(mApp.getInvariantDeviceProfile().dbFile, mDbName)) {
|
||||
verifyNotStopped();
|
||||
sanitizeFolders(mItemsDeleted);
|
||||
sanitizeAppPairs();
|
||||
sanitizeWidgetsShortcutsAndPackages();
|
||||
logASplit("sanitizeData");
|
||||
}
|
||||
@@ -482,14 +484,20 @@ public class LoaderTask implements Runnable {
|
||||
}
|
||||
|
||||
/**
|
||||
* After all items have been processed and added to the BgDataModel, this method requests
|
||||
* high-res icons for the items that are part of an app pair
|
||||
* After all items have been processed and added to the BgDataModel, this method sorts and
|
||||
* requests high-res icons for the items that are part of an app pair.
|
||||
*/
|
||||
private void processAppPairItems() {
|
||||
mBgDataModel.workspaceItems.stream()
|
||||
.filter((itemInfo -> itemInfo.itemType == ITEM_TYPE_APP_PAIR))
|
||||
.forEach(fi -> ((FolderInfo) fi).contents.forEach(item ->
|
||||
mIconCache.getTitleAndIcon(item, false /*useLowResIcon*/)));
|
||||
for (CollectionInfo collection : mBgDataModel.collections) {
|
||||
if (!(collection instanceof AppPairInfo appPair)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
appPair.getContents().sort(Folder.ITEM_POS_COMPARATOR);
|
||||
// Fetch hi-res icons if needed.
|
||||
appPair.getContents().stream().filter(ItemInfoWithIcon::usingLowResIcon)
|
||||
.forEach(member -> mIconCache.getTitleAndIcon(member, false));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -545,20 +553,21 @@ public class LoaderTask implements Runnable {
|
||||
// Sort the folder items, update ranks, and make sure all preview items are high res.
|
||||
List<FolderGridOrganizer> verifiers = mApp.getInvariantDeviceProfile().supportedProfiles
|
||||
.stream().map(FolderGridOrganizer::new).toList();
|
||||
for (FolderInfo folder : mBgDataModel.folders) {
|
||||
Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR);
|
||||
for (CollectionInfo collection : mBgDataModel.collections) {
|
||||
if (!(collection instanceof FolderInfo folder)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
folder.getContents().sort(Folder.ITEM_POS_COMPARATOR);
|
||||
verifiers.forEach(verifier -> verifier.setFolderInfo(folder));
|
||||
int size = folder.contents.size();
|
||||
int size = folder.getContents().size();
|
||||
|
||||
// Update ranks here to ensure there are no gaps caused by removed folder items.
|
||||
// Ranks are the source of truth for folder items, so cellX and cellY can be
|
||||
// ignored for now. Database will be updated once user manually modifies folder.
|
||||
for (int rank = 0; rank < size; ++rank) {
|
||||
WorkspaceItemInfo info = folder.contents.get(rank);
|
||||
// rank is used differently in app pairs, so don't reset
|
||||
if (folder.itemType != ITEM_TYPE_APP_PAIR) {
|
||||
info.rank = rank;
|
||||
}
|
||||
WorkspaceItemInfo info = folder.getContents().get(rank);
|
||||
info.rank = rank;
|
||||
|
||||
if (info.usingLowResIcon() && info.itemType == Favorites.ITEM_TYPE_APPLICATION
|
||||
&& verifiers.stream().anyMatch(it -> it.isItemInPreview(info.rank))) {
|
||||
@@ -611,14 +620,32 @@ public class LoaderTask implements Runnable {
|
||||
IntArray deletedFolderIds = mApp.getModel().getModelDbController().deleteEmptyFolders();
|
||||
synchronized (mBgDataModel) {
|
||||
for (int folderId : deletedFolderIds) {
|
||||
mBgDataModel.workspaceItems.remove(mBgDataModel.folders.get(folderId));
|
||||
mBgDataModel.folders.remove(folderId);
|
||||
mBgDataModel.workspaceItems.remove(mBgDataModel.collections.get(folderId));
|
||||
mBgDataModel.collections.remove(folderId);
|
||||
mBgDataModel.itemsIdMap.remove(folderId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Cleans up app pairs if they don't have the right number of member apps (2). */
|
||||
private void sanitizeAppPairs() {
|
||||
IntArray deletedAppPairIds = mApp.getModel().getModelDbController().deleteBadAppPairs();
|
||||
IntArray deletedAppIds = mApp.getModel().getModelDbController().deleteUnparentedApps();
|
||||
|
||||
IntArray deleted = new IntArray();
|
||||
deleted.addAll(deletedAppPairIds);
|
||||
deleted.addAll(deletedAppIds);
|
||||
|
||||
synchronized (mBgDataModel) {
|
||||
for (int id : deleted) {
|
||||
mBgDataModel.workspaceItems.remove(mBgDataModel.collections.get(id));
|
||||
mBgDataModel.collections.remove(id);
|
||||
mBgDataModel.itemsIdMap.remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sanitizeWidgetsShortcutsAndPackages() {
|
||||
Context context = mApp.getContext();
|
||||
|
||||
@@ -754,16 +781,16 @@ public class LoaderTask implements Runnable {
|
||||
|
||||
private void loadFolderNames() {
|
||||
FolderNameProvider provider = FolderNameProvider.newInstance(mApp.getContext(),
|
||||
mBgAllAppsList.data, mBgDataModel.folders);
|
||||
mBgAllAppsList.data, mBgDataModel.collections);
|
||||
|
||||
synchronized (mBgDataModel) {
|
||||
for (int i = 0; i < mBgDataModel.folders.size(); i++) {
|
||||
for (int i = 0; i < mBgDataModel.collections.size(); i++) {
|
||||
FolderNameInfos suggestionInfos = new FolderNameInfos();
|
||||
FolderInfo info = mBgDataModel.folders.valueAt(i);
|
||||
if (info.suggestedFolderNames == null) {
|
||||
provider.getSuggestedFolderName(mApp.getContext(), info.contents,
|
||||
CollectionInfo info = mBgDataModel.collections.valueAt(i);
|
||||
if (info instanceof FolderInfo fi && fi.suggestedFolderNames == null) {
|
||||
provider.getSuggestedFolderName(mApp.getContext(), fi.getContents(),
|
||||
suggestionInfos);
|
||||
info.suggestedFolderNames = suggestionInfos;
|
||||
fi.suggestedFolderNames = suggestionInfos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,15 @@
|
||||
*/
|
||||
package com.android.launcher3.model;
|
||||
|
||||
import static android.provider.BaseColumns._ID;
|
||||
import static android.util.Base64.NO_PADDING;
|
||||
import static android.util.Base64.NO_WRAP;
|
||||
|
||||
import static com.android.launcher3.DefaultLayoutParser.RES_PARTNER_DEFAULT_LAYOUT;
|
||||
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER;
|
||||
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE;
|
||||
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
|
||||
import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
|
||||
import static com.android.launcher3.LauncherSettings.Favorites.addTableToDb;
|
||||
import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_KEY;
|
||||
import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL;
|
||||
@@ -391,6 +395,68 @@ public class ModelDbController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes any app pair that doesn't contain 2 member apps from the DB.
|
||||
* @return Ids of deleted app pairs.
|
||||
*/
|
||||
@WorkerThread
|
||||
public IntArray deleteBadAppPairs() {
|
||||
createDbIfNotExists();
|
||||
|
||||
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
||||
try (SQLiteTransaction t = new SQLiteTransaction(db)) {
|
||||
// Select all entries with ITEM_TYPE = ITEM_TYPE_APP_PAIR whose id does not appear
|
||||
// exactly twice in the CONTAINER column.
|
||||
String selection =
|
||||
ITEM_TYPE + " = " + ITEM_TYPE_APP_PAIR
|
||||
+ " AND " + _ID + " NOT IN"
|
||||
+ " (SELECT " + CONTAINER + " FROM " + TABLE_NAME
|
||||
+ " GROUP BY " + CONTAINER + " HAVING COUNT(*) = 2)";
|
||||
|
||||
IntArray appPairIds = LauncherDbUtils.queryIntArray(false, db, TABLE_NAME,
|
||||
_ID, selection, null, null);
|
||||
if (!appPairIds.isEmpty()) {
|
||||
db.delete(TABLE_NAME, Utilities.createDbSelectionQuery(
|
||||
_ID, appPairIds), null);
|
||||
}
|
||||
t.commit();
|
||||
return appPairIds;
|
||||
} catch (SQLException ex) {
|
||||
Log.e(TAG, ex.getMessage(), ex);
|
||||
return new IntArray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes any app with a container id that doesn't exist.
|
||||
* @return Ids of deleted apps.
|
||||
*/
|
||||
@WorkerThread
|
||||
public IntArray deleteUnparentedApps() {
|
||||
createDbIfNotExists();
|
||||
|
||||
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
||||
try (SQLiteTransaction t = new SQLiteTransaction(db)) {
|
||||
// Select all entries whose container id does not appear in the database.
|
||||
String selection =
|
||||
CONTAINER + " >= 0"
|
||||
+ " AND " + CONTAINER + " NOT IN"
|
||||
+ " (SELECT " + _ID + " FROM " + TABLE_NAME + ")";
|
||||
|
||||
IntArray appIds = LauncherDbUtils.queryIntArray(false, db, TABLE_NAME,
|
||||
_ID, selection, null, null);
|
||||
if (!appIds.isEmpty()) {
|
||||
db.delete(TABLE_NAME, Utilities.createDbSelectionQuery(
|
||||
_ID, appIds), null);
|
||||
}
|
||||
t.commit();
|
||||
return appIds;
|
||||
} catch (SQLException ex) {
|
||||
Log.e(TAG, ex.getMessage(), ex);
|
||||
return new IntArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static void addModifiedTime(ContentValues values) {
|
||||
values.put(LauncherSettings.Favorites.MODIFIED, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ import com.android.launcher3.celllayout.CellPosMapper.CellPos;
|
||||
import com.android.launcher3.config.FeatureFlags;
|
||||
import com.android.launcher3.logging.FileLog;
|
||||
import com.android.launcher3.model.BgDataModel.Callbacks;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.CollectionInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
|
||||
import com.android.launcher3.model.data.WorkspaceItemInfo;
|
||||
@@ -275,7 +275,7 @@ public class ModelWriter {
|
||||
public void deleteItemsFromDatabase(@NonNull final Predicate<ItemInfo> matcher,
|
||||
@Nullable final String reason) {
|
||||
deleteItemsFromDatabase(StreamSupport.stream(mBgDataModel.itemsIdMap.spliterator(), false)
|
||||
.filter(matcher).collect(Collectors.toList()), reason);
|
||||
.filter(matcher).collect(Collectors.toList()), reason);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -302,15 +302,15 @@ public class ModelWriter {
|
||||
/**
|
||||
* Remove the specified folder and all its contents from the database.
|
||||
*/
|
||||
public void deleteFolderAndContentsFromDatabase(final FolderInfo info) {
|
||||
public void deleteCollectionAndContentsFromDatabase(final CollectionInfo info) {
|
||||
ModelVerifier verifier = new ModelVerifier();
|
||||
notifyDelete(Collections.singleton(info));
|
||||
|
||||
enqueueDeleteRunnable(newModelTask(() -> {
|
||||
mModel.getModelDbController().delete(Favorites.TABLE_NAME,
|
||||
Favorites.CONTAINER + "=" + info.id, null);
|
||||
mBgDataModel.removeItem(mContext, info.contents);
|
||||
info.contents.clear();
|
||||
mBgDataModel.removeItem(mContext, info.getContents());
|
||||
info.getContents().clear();
|
||||
|
||||
mModel.getModelDbController().delete(Favorites.TABLE_NAME,
|
||||
Favorites._ID + "=" + info.id, null);
|
||||
@@ -458,12 +458,12 @@ public class ModelWriter {
|
||||
|
||||
if (item.container != Favorites.CONTAINER_DESKTOP &&
|
||||
item.container != Favorites.CONTAINER_HOTSEAT) {
|
||||
// Item is in a folder, make sure this folder exists
|
||||
if (!mBgDataModel.folders.containsKey(item.container)) {
|
||||
// Item is in a collection, make sure this collection exists
|
||||
if (!mBgDataModel.collections.containsKey(item.container)) {
|
||||
// An items container is being set to a that of an item which is not in
|
||||
// the list of Folders.
|
||||
String msg = "item: " + item + " container being set to: " +
|
||||
item.container + ", not in the list of folders";
|
||||
item.container + ", not in the list of collections";
|
||||
Log.e(TAG, msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,17 +361,10 @@ public class PackageUpdatedTask extends BaseModelUpdateTask {
|
||||
}
|
||||
|
||||
if (!removedPackages.isEmpty() || !removedComponents.isEmpty()) {
|
||||
// This predicate is used to mark an ItemInfo for removal if its package or component
|
||||
// is marked for removal.
|
||||
Predicate<ItemInfo> removeAppMatch =
|
||||
Predicate<ItemInfo> removeMatch =
|
||||
ItemInfoMatcher.ofPackages(removedPackages, mUser)
|
||||
.or(ItemInfoMatcher.ofComponents(removedComponents, mUser))
|
||||
.and(ItemInfoMatcher.ofItemIds(forceKeepShortcuts).negate());
|
||||
// This predicate is used to mark an app pair for removal if it contains an app marked
|
||||
// for removal.
|
||||
Predicate<ItemInfo> removeAppPairMatch =
|
||||
ItemInfoMatcher.forAppPairMatch(removeAppMatch);
|
||||
Predicate<ItemInfo> removeMatch = removeAppMatch.or(removeAppPairMatch);
|
||||
deleteAndBindComponentsRemoved(removeMatch,
|
||||
"removed because the corresponding package or component is removed. "
|
||||
+ "mOp=" + mOp + " removedPackages=" + removedPackages.stream().collect(
|
||||
|
||||
@@ -32,6 +32,8 @@ import com.android.launcher3.LauncherSettings.Favorites
|
||||
import com.android.launcher3.Utilities
|
||||
import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError
|
||||
import com.android.launcher3.logging.FileLog
|
||||
import com.android.launcher3.model.data.AppPairInfo
|
||||
import com.android.launcher3.model.data.FolderInfo
|
||||
import com.android.launcher3.model.data.IconRequestInfo
|
||||
import com.android.launcher3.model.data.ItemInfoWithIcon
|
||||
import com.android.launcher3.model.data.LauncherAppWidgetInfo
|
||||
@@ -360,25 +362,40 @@ class WorkspaceItemProcessor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the folder information from the database and formats it into a FolderInfo. Some of the
|
||||
* processing for folder content items is done in LoaderTask after all the items in the
|
||||
* workspace have been loaded. The loaded FolderInfos are stored in the BgDataModel.
|
||||
* Loads CollectionInfo information from the database and formats it. This function runs while
|
||||
* LoaderTask is still active; some of the processing for folder content items is done after all
|
||||
* the items in the workspace have been loaded. The loaded and formatted CollectionInfo is then
|
||||
* stored in the BgDataModel.
|
||||
*/
|
||||
private fun processFolderOrAppPair() {
|
||||
val folderInfo =
|
||||
bgDataModel.findOrMakeFolder(c.id).apply {
|
||||
c.applyCommonProperties(this)
|
||||
itemType = c.itemType
|
||||
// Do not trim the folder label, as is was set by the user.
|
||||
title = c.getString(c.mTitleIndex)
|
||||
spanX = 1
|
||||
spanY = 1
|
||||
options = c.options
|
||||
}
|
||||
var collection = bgDataModel.findOrMakeFolder(c.id)
|
||||
// If we generated a placeholder Folder before this point, it may need to be replaced with
|
||||
// an app pair.
|
||||
if (c.itemType == Favorites.ITEM_TYPE_APP_PAIR && collection is FolderInfo) {
|
||||
val folderInfo: FolderInfo = collection
|
||||
val newAppPair = AppPairInfo()
|
||||
// Move the placeholder's contents over to the new app pair.
|
||||
folderInfo.contents.forEach(newAppPair::add)
|
||||
collection = newAppPair
|
||||
// Remove the placeholder and add the app pair into the data model.
|
||||
bgDataModel.collections.remove(c.id)
|
||||
bgDataModel.collections.put(c.id, collection)
|
||||
}
|
||||
|
||||
c.applyCommonProperties(collection)
|
||||
// Do not trim the folder label, as is was set by the user.
|
||||
collection.title = c.getString(c.mTitleIndex)
|
||||
collection.spanX = 1
|
||||
collection.spanY = 1
|
||||
if (collection is FolderInfo) {
|
||||
collection.options = c.options
|
||||
} else {
|
||||
// An app pair may be inside another folder, so it needs to preserve rank information.
|
||||
collection.rank = c.rank
|
||||
}
|
||||
|
||||
// no special handling required for restored folders
|
||||
c.markRestored()
|
||||
c.checkAndAddItem(folderInfo, bgDataModel, memoryLogger)
|
||||
c.checkAndAddItem(collection, bgDataModel, memoryLogger)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.launcher3.model.data
|
||||
|
||||
import android.content.Context
|
||||
import com.android.launcher3.LauncherSettings
|
||||
import com.android.launcher3.logger.LauncherAtom
|
||||
import com.android.launcher3.views.ActivityContext
|
||||
|
||||
/** A type of app collection that launches multiple apps into split screen. */
|
||||
class AppPairInfo() : CollectionInfo() {
|
||||
init {
|
||||
itemType = LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR
|
||||
}
|
||||
|
||||
/** Convenience constructor, calls primary constructor and init block */
|
||||
constructor(app1: WorkspaceItemInfo, app2: WorkspaceItemInfo) : this() {
|
||||
add(app1)
|
||||
add(app2)
|
||||
}
|
||||
|
||||
/** Adds an element to the contents array. */
|
||||
override fun add(item: WorkspaceItemInfo) {
|
||||
contents.add(item)
|
||||
}
|
||||
|
||||
/** Returns the first app in the pair. */
|
||||
fun getFirstApp() = contents[0]
|
||||
|
||||
/** Returns the second app in the pair. */
|
||||
fun getSecondApp() = contents[1]
|
||||
|
||||
/** Returns if either of the app pair members is currently disabled. */
|
||||
override fun isDisabled() = anyMatch { it.isDisabled }
|
||||
|
||||
/** Checks if the app pair is launchable at the current screen size. */
|
||||
fun isLaunchable(context: Context) =
|
||||
(ActivityContext.lookupContext(context) as ActivityContext).getDeviceProfile().isTablet ||
|
||||
noneMatch { it.hasStatusFlag(WorkspaceItemInfo.FLAG_NON_RESIZEABLE) }
|
||||
|
||||
/** Generates an ItemInfo for logging. */
|
||||
override fun buildProto(cInfo: CollectionInfo?): LauncherAtom.ItemInfo {
|
||||
val appPairIcon = LauncherAtom.FolderIcon.newBuilder().setCardinality(contents.size)
|
||||
appPairIcon.setLabelInfo(title.toString())
|
||||
return getDefaultItemInfoBuilder()
|
||||
.setFolderIcon(appPairIcon)
|
||||
.setRank(rank)
|
||||
.setContainerInfo(getContainerInfo())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.launcher3.model.data
|
||||
|
||||
import com.android.launcher3.LauncherSettings
|
||||
import com.android.launcher3.logger.LauncherAtom
|
||||
import com.android.launcher3.util.ContentWriter
|
||||
import java.util.function.Predicate
|
||||
|
||||
abstract class CollectionInfo : ItemInfo() {
|
||||
var contents: ArrayList<WorkspaceItemInfo> = ArrayList()
|
||||
|
||||
abstract fun add(item: WorkspaceItemInfo)
|
||||
|
||||
/** Convenience function. Checks contents to see if any match a given predicate. */
|
||||
fun anyMatch(matcher: Predicate<in WorkspaceItemInfo>): Boolean {
|
||||
return contents.stream().anyMatch(matcher)
|
||||
}
|
||||
|
||||
/** Convenience function. Returns true if none of the contents match a given predicate. */
|
||||
fun noneMatch(matcher: Predicate<in WorkspaceItemInfo>): Boolean {
|
||||
return contents.stream().noneMatch(matcher)
|
||||
}
|
||||
|
||||
override fun onAddToDatabase(writer: ContentWriter) {
|
||||
super.onAddToDatabase(writer)
|
||||
writer.put(LauncherSettings.Favorites.TITLE, title)
|
||||
}
|
||||
|
||||
/** Returns the collection wrapped as {@link LauncherAtom.ItemInfo} for logging. */
|
||||
override fun buildProto(): LauncherAtom.ItemInfo {
|
||||
return buildProto(null)
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,6 @@ import static com.android.launcher3.logger.LauncherAtom.Attribute.EMPTY_LABEL;
|
||||
import static com.android.launcher3.logger.LauncherAtom.Attribute.MANUAL_LABEL;
|
||||
import static com.android.launcher3.logger.LauncherAtom.Attribute.SUGGESTED_LABEL;
|
||||
|
||||
import android.os.Process;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -49,7 +47,7 @@ import java.util.stream.IntStream;
|
||||
/**
|
||||
* Represents a folder containing shortcuts or apps.
|
||||
*/
|
||||
public class FolderInfo extends ItemInfo {
|
||||
public class FolderInfo extends CollectionInfo {
|
||||
|
||||
public static final int NO_FLAGS = 0x00000000;
|
||||
|
||||
@@ -100,27 +98,15 @@ public class FolderInfo extends ItemInfo {
|
||||
|
||||
public FolderNameInfos suggestedFolderNames;
|
||||
|
||||
/**
|
||||
* The apps and shortcuts
|
||||
*/
|
||||
public ArrayList<WorkspaceItemInfo> contents = new ArrayList<>();
|
||||
|
||||
private ArrayList<FolderListener> mListeners = new ArrayList<>();
|
||||
|
||||
public FolderInfo() {
|
||||
itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
|
||||
user = Process.myUserHandle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an app pair, a type of app collection that launches multiple apps into split screen
|
||||
*/
|
||||
public static FolderInfo createAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
|
||||
FolderInfo newAppPair = new FolderInfo();
|
||||
newAppPair.itemType = LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
|
||||
newAppPair.add(app1, /* animate */ false);
|
||||
newAppPair.add(app2, /* animate */ false);
|
||||
return newAppPair;
|
||||
/** Adds a app or shortcut to the contents array without animation. */
|
||||
public void add(@NonNull WorkspaceItemInfo item) {
|
||||
add(item, false /* animate */);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,15 +115,15 @@ public class FolderInfo extends ItemInfo {
|
||||
* @param item
|
||||
*/
|
||||
public void add(WorkspaceItemInfo item, boolean animate) {
|
||||
add(item, contents.size(), animate);
|
||||
add(item, getContents().size(), animate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an app or shortcut for a specified rank.
|
||||
*/
|
||||
public void add(WorkspaceItemInfo item, int rank, boolean animate) {
|
||||
rank = Utilities.boundToRange(rank, 0, contents.size());
|
||||
contents.add(rank, item);
|
||||
rank = Utilities.boundToRange(rank, 0, getContents().size());
|
||||
getContents().add(rank, item);
|
||||
for (int i = 0; i < mListeners.size(); i++) {
|
||||
mListeners.get(i).onAdd(item, rank);
|
||||
}
|
||||
@@ -157,7 +143,7 @@ public class FolderInfo extends ItemInfo {
|
||||
* Remove all matching app or shortcut. Does not change the DB.
|
||||
*/
|
||||
public void removeAll(List<WorkspaceItemInfo> items, boolean animate) {
|
||||
contents.removeAll(items);
|
||||
getContents().removeAll(items);
|
||||
for (int i = 0; i < mListeners.size(); i++) {
|
||||
mListeners.get(i).onRemove(items);
|
||||
}
|
||||
@@ -167,8 +153,7 @@ public class FolderInfo extends ItemInfo {
|
||||
@Override
|
||||
public void onAddToDatabase(@NonNull ContentWriter writer) {
|
||||
super.onAddToDatabase(writer);
|
||||
writer.put(LauncherSettings.Favorites.TITLE, title)
|
||||
.put(LauncherSettings.Favorites.OPTIONS, options);
|
||||
writer.put(LauncherSettings.Favorites.OPTIONS, options);
|
||||
}
|
||||
|
||||
public void addListener(FolderListener listener) {
|
||||
@@ -219,9 +204,9 @@ public class FolderInfo extends ItemInfo {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public LauncherAtom.ItemInfo buildProto(@Nullable FolderInfo fInfo) {
|
||||
public LauncherAtom.ItemInfo buildProto(@Nullable CollectionInfo cInfo) {
|
||||
FolderIcon.Builder folderIcon = FolderIcon.newBuilder()
|
||||
.setCardinality(contents.size());
|
||||
.setCardinality(getContents().size());
|
||||
if (LabelState.SUGGESTED.equals(getLabelState())) {
|
||||
folderIcon.setLabelInfo(title.toString());
|
||||
}
|
||||
@@ -278,19 +263,10 @@ public class FolderInfo extends ItemInfo {
|
||||
public ItemInfo makeShallowCopy() {
|
||||
FolderInfo folderInfo = new FolderInfo();
|
||||
folderInfo.copyFrom(this);
|
||||
folderInfo.contents = this.contents;
|
||||
folderInfo.setContents(this.getContents());
|
||||
return folderInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@link LauncherAtom.FolderIcon} wrapped as {@link LauncherAtom.ItemInfo} for logging.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public LauncherAtom.ItemInfo buildProto() {
|
||||
return buildProto(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns index of the accepted suggestion.
|
||||
*/
|
||||
@@ -371,13 +347,4 @@ public class FolderInfo extends ItemInfo {
|
||||
}
|
||||
return LauncherAtom.ToState.TO_STATE_UNSPECIFIED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDisabled() {
|
||||
if (itemType == LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR) {
|
||||
return contents.stream().anyMatch((WorkspaceItemInfo::isDisabled));
|
||||
}
|
||||
|
||||
return super.isDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,10 +349,9 @@ public class ItemInfo {
|
||||
|
||||
/**
|
||||
* Creates {@link LauncherAtom.ItemInfo} with important fields and parent container info.
|
||||
* @param fInfo
|
||||
*/
|
||||
@NonNull
|
||||
public LauncherAtom.ItemInfo buildProto(@Nullable final FolderInfo fInfo) {
|
||||
public LauncherAtom.ItemInfo buildProto(@Nullable final CollectionInfo cInfo) {
|
||||
LauncherAtom.ItemInfo.Builder itemBuilder = getDefaultItemInfoBuilder();
|
||||
Optional<ComponentName> nullableComponent = Optional.ofNullable(getTargetComponent());
|
||||
switch (itemType) {
|
||||
@@ -398,21 +397,21 @@ public class ItemInfo {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (fInfo != null) {
|
||||
if (cInfo != null) {
|
||||
LauncherAtom.FolderContainer.Builder folderBuilder =
|
||||
LauncherAtom.FolderContainer.newBuilder();
|
||||
folderBuilder.setGridX(cellX).setGridY(cellY).setPageIndex(screenId);
|
||||
|
||||
switch (fInfo.container) {
|
||||
switch (cInfo.container) {
|
||||
case CONTAINER_HOTSEAT:
|
||||
case CONTAINER_HOTSEAT_PREDICTION:
|
||||
folderBuilder.setHotseat(LauncherAtom.HotseatContainer.newBuilder()
|
||||
.setIndex(fInfo.screenId));
|
||||
.setIndex(cInfo.screenId));
|
||||
break;
|
||||
case CONTAINER_DESKTOP:
|
||||
folderBuilder.setWorkspace(LauncherAtom.WorkspaceContainer.newBuilder()
|
||||
.setPageIndex(fInfo.screenId)
|
||||
.setGridX(fInfo.cellX).setGridY(fInfo.cellY));
|
||||
.setPageIndex(cInfo.screenId)
|
||||
.setGridX(cInfo.cellX).setGridY(cInfo.cellY));
|
||||
break;
|
||||
}
|
||||
itemBuilder.setContainerInfo(ContainerInfo.newBuilder().setFolder(folderBuilder));
|
||||
|
||||
@@ -271,8 +271,8 @@ public class LauncherAppWidgetInfo extends ItemInfo {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public LauncherAtom.ItemInfo buildProto(@Nullable FolderInfo folderInfo) {
|
||||
LauncherAtom.ItemInfo info = super.buildProto(folderInfo);
|
||||
public LauncherAtom.ItemInfo buildProto(@Nullable CollectionInfo collectionInfo) {
|
||||
LauncherAtom.ItemInfo info = super.buildProto(collectionInfo);
|
||||
return info.toBuilder()
|
||||
.setWidget(info.getWidget().toBuilder().setWidgetFeatures(widgetFeatures))
|
||||
.addItemAttributes(getAttribute(sourceContainer))
|
||||
|
||||
@@ -53,6 +53,7 @@ import com.android.launcher3.logging.InstanceId;
|
||||
import com.android.launcher3.logging.InstanceIdSequence;
|
||||
import com.android.launcher3.logging.StatsLogManager;
|
||||
import com.android.launcher3.model.data.AppInfo;
|
||||
import com.android.launcher3.model.data.AppPairInfo;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.ItemInfoWithIcon;
|
||||
@@ -101,11 +102,9 @@ public class ItemClickHandler {
|
||||
if (tag instanceof WorkspaceItemInfo) {
|
||||
onClickAppShortcut(v, (WorkspaceItemInfo) tag, launcher);
|
||||
} else if (tag instanceof FolderInfo) {
|
||||
if (v instanceof FolderIcon) {
|
||||
onClickFolderIcon(v);
|
||||
} else if (v instanceof AppPairIcon) {
|
||||
onClickAppPairIcon(v);
|
||||
}
|
||||
onClickFolderIcon(v);
|
||||
} else if (tag instanceof AppPairInfo) {
|
||||
onClickAppPairIcon(v);
|
||||
} else if (tag instanceof AppInfo) {
|
||||
startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher);
|
||||
} else if (tag instanceof LauncherAppWidgetInfo) {
|
||||
@@ -150,7 +149,7 @@ public class ItemClickHandler {
|
||||
private static void onClickAppPairIcon(View v) {
|
||||
Launcher launcher = Launcher.getLauncher(v.getContext());
|
||||
AppPairIcon appPairIcon = (AppPairIcon) v;
|
||||
if (!appPairIcon.isLaunchableAtScreenSize()) {
|
||||
if (!appPairIcon.getInfo().isLaunchable(launcher)) {
|
||||
// Display a message for app pairs that are disabled due to screen size
|
||||
boolean isFoldable = InvariantDeviceProfile.INSTANCE.get(launcher)
|
||||
.supportedProfiles.stream().anyMatch(dp -> dp.isTwoPanels);
|
||||
@@ -159,8 +158,8 @@ public class ItemClickHandler {
|
||||
: R.string.app_pair_unlaunchable_at_screen_size,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
} else if (appPairIcon.getInfo().isDisabled()) {
|
||||
WorkspaceItemInfo app1 = appPairIcon.getInfo().contents.get(0);
|
||||
WorkspaceItemInfo app2 = appPairIcon.getInfo().contents.get(1);
|
||||
WorkspaceItemInfo app1 = appPairIcon.getInfo().getFirstApp();
|
||||
WorkspaceItemInfo app2 = appPairIcon.getInfo().getSecondApp();
|
||||
// Show the user why the app pair is disabled.
|
||||
if (app1.isDisabled() && !handleDisabledItemClicked(app1, launcher)) {
|
||||
// If handleDisabledItemClicked() did not handle the error message, we initiate an
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.android.launcher3.R
|
||||
import com.android.launcher3.apppairs.AppPairIcon
|
||||
import com.android.launcher3.folder.FolderIcon
|
||||
import com.android.launcher3.model.ModelWriter
|
||||
import com.android.launcher3.model.data.AppPairInfo
|
||||
import com.android.launcher3.model.data.FolderInfo
|
||||
import com.android.launcher3.model.data.ItemInfo
|
||||
import com.android.launcher3.model.data.LauncherAppWidgetInfo
|
||||
@@ -81,7 +82,7 @@ class ItemInflater<T>(
|
||||
R.layout.app_pair_icon,
|
||||
context,
|
||||
parent,
|
||||
item as FolderInfo,
|
||||
item as AppPairInfo,
|
||||
BubbleTextView.DISPLAY_WORKSPACE
|
||||
)
|
||||
Favorites.ITEM_TYPE_APPWIDGET,
|
||||
|
||||
@@ -65,19 +65,10 @@ public abstract class ItemInfoMatcher {
|
||||
* Returns a matcher for items within folders.
|
||||
*/
|
||||
public static Predicate<ItemInfo> forFolderMatch(Predicate<ItemInfo> childOperator) {
|
||||
return info -> info instanceof FolderInfo && ((FolderInfo) info).contents.stream()
|
||||
return info -> info instanceof FolderInfo && ((FolderInfo) info).getContents().stream()
|
||||
.anyMatch(childOperator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a matcher for items within app pairs.
|
||||
*/
|
||||
public static Predicate<ItemInfo> forAppPairMatch(Predicate<ItemInfo> childOperator) {
|
||||
Predicate<ItemInfo> isAppPair = info ->
|
||||
info instanceof FolderInfo fi && fi.itemType == Favorites.ITEM_TYPE_APP_PAIR;
|
||||
return isAppPair.and(forFolderMatch(childOperator));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a matcher for items with provided ids
|
||||
*/
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.android.launcher3.apppairs.AppPairIcon;
|
||||
import com.android.launcher3.folder.Folder;
|
||||
import com.android.launcher3.folder.FolderIcon;
|
||||
import com.android.launcher3.graphics.PreloadIconDrawable;
|
||||
import com.android.launcher3.model.data.AppPairInfo;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
|
||||
@@ -59,7 +60,7 @@ public interface LauncherBindableItemsContainer {
|
||||
: null);
|
||||
} else if (info instanceof FolderInfo && v instanceof FolderIcon) {
|
||||
((FolderIcon) v).updatePreviewItems(updates::contains);
|
||||
} else if (info instanceof FolderInfo && v instanceof AppPairIcon appPairIcon) {
|
||||
} else if (info instanceof AppPairInfo && v instanceof AppPairIcon appPairIcon) {
|
||||
appPairIcon.maybeRedrawForWorkspaceUpdate(updates::contains);
|
||||
}
|
||||
|
||||
@@ -89,7 +90,7 @@ public interface LauncherBindableItemsContainer {
|
||||
((PendingAppWidgetHostView) v).applyState();
|
||||
} else if (v instanceof FolderIcon && info instanceof FolderInfo) {
|
||||
((FolderIcon) v).updatePreviewItems(updates::contains);
|
||||
} else if (info instanceof FolderInfo && v instanceof AppPairIcon appPairIcon) {
|
||||
} else if (info instanceof AppPairInfo && v instanceof AppPairIcon appPairIcon) {
|
||||
appPairIcon.maybeRedrawForWorkspaceUpdate(updates::contains);
|
||||
}
|
||||
// process all the shortcuts
|
||||
|
||||
@@ -25,7 +25,7 @@ import androidx.annotation.Nullable;
|
||||
import com.android.launcher3.LauncherSettings;
|
||||
import com.android.launcher3.PendingAddItemInfo;
|
||||
import com.android.launcher3.logger.LauncherAtom;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.CollectionInfo;
|
||||
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
|
||||
import com.android.launcher3.widget.picker.WidgetRecommendationCategory;
|
||||
import com.android.launcher3.widget.util.WidgetSizes;
|
||||
@@ -82,8 +82,8 @@ public class PendingAddWidgetInfo extends PendingAddItemInfo {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public LauncherAtom.ItemInfo buildProto(@Nullable FolderInfo folderInfo) {
|
||||
LauncherAtom.ItemInfo info = super.buildProto(folderInfo);
|
||||
public LauncherAtom.ItemInfo buildProto(@Nullable CollectionInfo collectionInfo) {
|
||||
LauncherAtom.ItemInfo info = super.buildProto(collectionInfo);
|
||||
return info.toBuilder()
|
||||
.addItemAttributes(LauncherAppWidgetInfo.getAttribute(sourceContainer))
|
||||
.build();
|
||||
|
||||
+3
-4
@@ -26,7 +26,7 @@ import com.android.launcher3.LauncherAppState;
|
||||
import com.android.launcher3.LauncherModel;
|
||||
import com.android.launcher3.LauncherSettings;
|
||||
import com.android.launcher3.model.ModelDbController;
|
||||
import com.android.launcher3.model.data.FolderInfo;
|
||||
import com.android.launcher3.model.data.CollectionInfo;
|
||||
import com.android.launcher3.model.data.ItemInfo;
|
||||
import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
|
||||
import com.android.launcher3.util.ContentWriter;
|
||||
@@ -73,9 +73,8 @@ public class FavoriteItemsTransaction {
|
||||
ContentWriter writer = new ContentWriter(mContext);
|
||||
ItemInfo item = mItemsToSubmit.get(i).get();
|
||||
|
||||
if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
|
||||
FolderInfo folderInfo = (FolderInfo) item;
|
||||
for (ItemInfo itemInfo : folderInfo.contents) {
|
||||
if (item instanceof CollectionInfo ci) {
|
||||
for (ItemInfo itemInfo : ci.getContents()) {
|
||||
itemInfo.container = i;
|
||||
containerItems.add(itemInfo);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.android.launcher3.LauncherPrefs.Companion.get
|
||||
import com.android.launcher3.icons.BaseIconFactory
|
||||
import com.android.launcher3.icons.FastBitmapDrawable
|
||||
import com.android.launcher3.icons.UserBadgeDrawable
|
||||
import com.android.launcher3.model.data.FolderInfo
|
||||
import com.android.launcher3.model.data.WorkspaceItemInfo
|
||||
import com.android.launcher3.util.ActivityContextWrapper
|
||||
import com.android.launcher3.util.FlagOp
|
||||
@@ -71,8 +72,8 @@ class PreviewItemManagerTest {
|
||||
.build()
|
||||
)
|
||||
.loadModelSync()
|
||||
folderItems = modelHelper.bgDataModel.folders.valueAt(0).contents
|
||||
folderIcon.mInfo = modelHelper.bgDataModel.folders.valueAt(0)
|
||||
folderItems = modelHelper.bgDataModel.collections.valueAt(0).contents
|
||||
folderIcon.mInfo = modelHelper.bgDataModel.collections.valueAt(0) as FolderInfo
|
||||
folderIcon.mInfo.contents = folderItems
|
||||
|
||||
// Set first icon to be themed.
|
||||
|
||||
@@ -160,6 +160,6 @@ public class CacheDataUpdatedTaskTest {
|
||||
}
|
||||
|
||||
private List<WorkspaceItemInfo> allItems() {
|
||||
return ((FolderInfo) mModelHelper.getBgDataModel().itemsIdMap.get(1)).contents;
|
||||
return ((FolderInfo) mModelHelper.getBgDataModel().itemsIdMap.get(1)).getContents();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ public class DefaultLayoutProviderTest {
|
||||
assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
|
||||
ItemInfo info = mModelHelper.getBgDataModel().workspaceItems.get(0);
|
||||
assertEquals(LauncherSettings.Favorites.ITEM_TYPE_FOLDER, info.itemType);
|
||||
assertEquals(3, ((FolderInfo) info).contents.size());
|
||||
assertEquals(3, ((FolderInfo) info).getContents().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -102,7 +102,7 @@ public class DefaultLayoutProviderTest {
|
||||
assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
|
||||
ItemInfo info = mModelHelper.getBgDataModel().workspaceItems.get(0);
|
||||
assertEquals(LauncherSettings.Favorites.ITEM_TYPE_FOLDER, info.itemType);
|
||||
assertEquals(3, ((FolderInfo) info).contents.size());
|
||||
assertEquals(3, ((FolderInfo) info).getContents().size());
|
||||
assertEquals("CustomFolder", info.title.toString());
|
||||
}
|
||||
|
||||
@@ -154,11 +154,11 @@ public class DefaultLayoutProviderTest {
|
||||
// Verify folder
|
||||
assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
|
||||
FolderInfo info = (FolderInfo) mModelHelper.getBgDataModel().workspaceItems.get(0);
|
||||
assertEquals(3, info.contents.size());
|
||||
assertEquals(3, info.getContents().size());
|
||||
|
||||
// Verify last icon
|
||||
assertEquals(LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT,
|
||||
info.contents.get(info.contents.size() - 1).itemType);
|
||||
info.getContents().get(info.getContents().size() - 1).itemType);
|
||||
}
|
||||
|
||||
private void writeLayoutAndLoad(LauncherLayoutBuilder builder) throws Exception {
|
||||
|
||||
@@ -165,11 +165,11 @@ class FolderIconLoadTest {
|
||||
// Reload again with correct icon state
|
||||
app.model.forceReload()
|
||||
modelHelper.loadModelSync()
|
||||
val folders = modelHelper.getBgDataModel().folders
|
||||
val collections = modelHelper.getBgDataModel().collections
|
||||
|
||||
assertThat(folders.size()).isEqualTo(1)
|
||||
assertThat(folders.valueAt(0).contents.size).isEqualTo(itemCount)
|
||||
return folders.valueAt(0).contents
|
||||
assertThat(collections.size()).isEqualTo(1)
|
||||
assertThat(collections.valueAt(0).contents.size).isEqualTo(itemCount)
|
||||
return collections.valueAt(0).contents
|
||||
}
|
||||
|
||||
private fun verifyHighRes(items: ArrayList<WorkspaceItemInfo>, vararg indices: Int) {
|
||||
|
||||
@@ -103,7 +103,7 @@ class LoaderTaskTest {
|
||||
.runSyncOnBackgroundThread()
|
||||
Truth.assertThat(workspaceItems.size).isAtLeast(25)
|
||||
Truth.assertThat(appWidgets.size).isAtLeast(7)
|
||||
Truth.assertThat(folders.size()).isAtLeast(8)
|
||||
Truth.assertThat(collections.size()).isAtLeast(8)
|
||||
Truth.assertThat(itemsIdMap.size()).isAtLeast(40)
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.android.launcher3.apppairs.AppPairIcon
|
||||
import com.android.launcher3.folder.FolderIcon
|
||||
import com.android.launcher3.model.ModelWriter
|
||||
import com.android.launcher3.model.data.AppInfo
|
||||
import com.android.launcher3.model.data.AppPairInfo
|
||||
import com.android.launcher3.model.data.FolderInfo
|
||||
import com.android.launcher3.model.data.LauncherAppWidgetInfo
|
||||
import com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
|
||||
@@ -170,7 +171,7 @@ class ItemInflaterTest {
|
||||
|
||||
@Test
|
||||
fun test_app_pair_inflated_on_UI() {
|
||||
val itemInfo = FolderInfo()
|
||||
val itemInfo = AppPairInfo()
|
||||
itemInfo.itemType = ITEM_TYPE_APP_PAIR
|
||||
itemInfo.contents.add(workspaceItemInfo())
|
||||
itemInfo.contents.add(workspaceItemInfo())
|
||||
@@ -186,7 +187,7 @@ class ItemInflaterTest {
|
||||
fun test_app_pair_inflated_on_BG() {
|
||||
setFlagsRule.enableFlags(Flags.FLAG_ENABLE_WORKSPACE_INFLATION)
|
||||
|
||||
val itemInfo = FolderInfo()
|
||||
val itemInfo = AppPairInfo()
|
||||
itemInfo.itemType = ITEM_TYPE_APP_PAIR
|
||||
itemInfo.contents.add(workspaceItemInfo())
|
||||
itemInfo.contents.add(workspaceItemInfo())
|
||||
|
||||
Reference in New Issue
Block a user