diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 3754dc147b..192b8a7c6f 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -871,11 +871,11 @@ public class Launcher extends StatefulActivity implements Launche if (dropLayout == null) { // it's possible that the add screen was removed because it was // empty and a re-bind occurred - mWorkspace.addExtraEmptyScreen(); - return mWorkspace.commitExtraEmptyScreen(); - } else { - return screenId; + mWorkspace.addExtraEmptyScreens(); + IntSet emptyPagesAdded = mWorkspace.commitExtraEmptyScreens(); + return emptyPagesAdded.isEmpty() ? -1 : emptyPagesAdded.getArray().get(0); } + return screenId; } @Thunk @@ -2175,7 +2175,7 @@ public class Launcher extends StatefulActivity implements Launche orderedScreenIds.add(firstScreenPosition, Workspace.FIRST_SCREEN_ID); } else if (!FeatureFlags.QSB_ON_FIRST_SCREEN && orderedScreenIds.isEmpty()) { // If there are no screens, we need to have an empty screen - mWorkspace.addExtraEmptyScreen(); + mWorkspace.addExtraEmptyScreens(); } bindAddScreens(orderedScreenIds); @@ -2190,17 +2190,7 @@ public class Launcher extends StatefulActivity implements Launche // Some empty pages might have been removed while the phone was in a single panel // mode, so we want to add those empty pages back. IntSet screenIds = IntSet.wrap(orderedScreenIds); - for (int i = 0; i < orderedScreenIds.size(); i++) { - int screenId = orderedScreenIds.get(i); - // Don't add the page pair if the page is the last one and if the pair is on the - // right, because that would cause a bug when adding new pages. - // TODO: (b/196376162) remove this when the new screen id logic is fixed for two - // panel in Workspace::commitExtraEmptyScreen - if (i == orderedScreenIds.size() - 1 && screenId % 2 == 0) { - continue; - } - screenIds.add(mWorkspace.getPagePair(screenId)); - } + orderedScreenIds.forEach(screenId -> screenIds.add(mWorkspace.getPagePair(screenId))); orderedScreenIds = screenIds.getArray(); } diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 94ec903b86..c404d1a386 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -125,6 +125,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -642,18 +643,35 @@ public class Workspace extends PagedView boolean childOnFinalScreen = false; if (mDragSourceInternal != null) { + int dragSourceChildCount = mDragSourceInternal.getChildCount(); + + if (isTwoPanelEnabled()) { + int pagePairScreenId = getPagePair(dragObject.dragInfo.screenId); + CellLayout pagePair = mWorkspaceScreens.get(pagePairScreenId); + if (pagePair == null) { + // TODO: after http://b/198820019 is fixed, remove this + throw new IllegalStateException("Page pair is null, " + + "dragScreenId: " + dragObject.dragInfo.screenId + + ", pagePairScreenId: " + pagePairScreenId + + ", mScreenOrder: " + mScreenOrder.toConcatString() + ); + } + dragSourceChildCount += pagePair.getShortcutsAndWidgets().getChildCount(); + } + // When the drag view content is a LauncherAppWidgetHostView, we should increment the // drag source child count by 1 because the widget in drag has been detached from its // original parent, ShortcutAndWidgetContainer, and reattached to the DragView. - int dragSourceChildCount = - dragObject.dragView.getContentView() instanceof LauncherAppWidgetHostView - ? mDragSourceInternal.getChildCount() + 1 - : mDragSourceInternal.getChildCount(); + if (dragObject.dragView.getContentView() instanceof LauncherAppWidgetHostView) { + dragSourceChildCount++; + } + if (dragSourceChildCount == 1) { lastChildOnScreen = true; } CellLayout cl = (CellLayout) mDragSourceInternal.getParent(); - if (indexOfChild(cl) == getChildCount() - 1) { + if (getLeftmostVisiblePageForIndex(indexOfChild(cl)) + == getLeftmostVisiblePageForIndex(getPageCount() - 1)) { childOnFinalScreen = true; } } @@ -662,40 +680,83 @@ public class Workspace extends PagedView if (lastChildOnScreen && childOnFinalScreen) { return; } - if (!mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID)) { - insertNewWorkspaceScreen(EXTRA_EMPTY_SCREEN_ID); + + forEachExtraEmptyPageId(extraEmptyPageId -> { + if (!mWorkspaceScreens.containsKey(extraEmptyPageId)) { + insertNewWorkspaceScreen(extraEmptyPageId); + } + }); + } + + /** + * Inserts extra empty pages to the end of the existing workspaces. + * Usually we add one extra empty screen, but when two panel home is enabled we add + * two extra screens. + **/ + public void addExtraEmptyScreens() { + forEachExtraEmptyPageId(extraEmptyPageId -> { + if (!mWorkspaceScreens.containsKey(extraEmptyPageId)) { + insertNewWorkspaceScreen(extraEmptyPageId); + } + }); + } + + /** + * Calls the consumer with all the necessary extra empty page IDs. + * On a normal one panel Workspace that means only EXTRA_EMPTY_SCREEN_ID, + * but in a two panel scenario this also includes EXTRA_EMPTY_SCREEN_SECOND_ID. + */ + private void forEachExtraEmptyPageId(Consumer callback) { + callback.accept(EXTRA_EMPTY_SCREEN_ID); + if (isTwoPanelEnabled()) { + callback.accept(EXTRA_EMPTY_SCREEN_SECOND_ID); } } - public boolean addExtraEmptyScreen() { - if (!mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID)) { - insertNewWorkspaceScreen(EXTRA_EMPTY_SCREEN_ID); - return true; - } - return false; - } - + /** + * If two panel home is enabled we convert the last two screens that are visible at the same + * time. In other cases we only convert the last page. + */ private void convertFinalScreenToEmptyScreenIfNecessary() { if (mLauncher.isWorkspaceLoading()) { // Invalid and dangerous operation if workspace is loading return; } - if (hasExtraEmptyScreen() || mScreenOrder.size() == 0) return; - int finalScreenId = mScreenOrder.get(mScreenOrder.size() - 1); + int panelCount = getPanelCount(); + if (hasExtraEmptyScreens() || mScreenOrder.size() < panelCount) { + return; + } - CellLayout finalScreen = mWorkspaceScreens.get(finalScreenId); + SparseArray finalScreens = new SparseArray<>(); - // If the final screen is empty, convert it to the extra empty screen - if (finalScreen != null - && finalScreen.getShortcutsAndWidgets().getChildCount() == 0 - && !finalScreen.isDropPending()) { - mWorkspaceScreens.remove(finalScreenId); - mScreenOrder.removeValue(finalScreenId); + int pageCount = mScreenOrder.size(); + // First we add the last page(s) to the finalScreens collection. The number of final pages + // depends on the panel count. + for (int pageIndex = pageCount - panelCount; pageIndex < pageCount; pageIndex++) { + int screenId = mScreenOrder.get(pageIndex); + CellLayout screen = mWorkspaceScreens.get(screenId); + if (screen == null || screen.getShortcutsAndWidgets().getChildCount() != 0 + || screen.isDropPending()) { + // Final screen doesn't exist or it isn't empty or there's a pending drop + return; + } + finalScreens.append(screenId, screen); + } - // if this is the last screen, convert it to the empty screen - mWorkspaceScreens.put(EXTRA_EMPTY_SCREEN_ID, finalScreen); - mScreenOrder.add(EXTRA_EMPTY_SCREEN_ID); + // Then we remove the final screens from the collections (but not from the view hierarchy) + // and we store them as extra empty screens. + for (int i = 0; i < finalScreens.size(); i++) { + int screenId = finalScreens.keyAt(i); + CellLayout screen = finalScreens.get(screenId); + + mWorkspaceScreens.remove(screenId); + mScreenOrder.removeValue(screenId); + + int newScreenId = mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID) + ? EXTRA_EMPTY_SCREEN_SECOND_ID : EXTRA_EMPTY_SCREEN_ID; + mWorkspaceScreens.put(newScreenId, screen); + mScreenOrder.add(newScreenId); } } @@ -703,6 +764,23 @@ public class Workspace extends PagedView removeExtraEmptyScreenDelayed(0, stripEmptyScreens, null); } + /** + * The purpose of this method is to remove empty pages from Workspace. + * Empty page(s) from the end of mWorkspaceScreens will always be removed. The pages with + * ID = Workspace.EXTRA_EMPTY_SCREEN_IDS will be removed if there are other non-empty pages. + * If there are no more non-empty pages left, extra empty page(s) will either stay or get added. + * + * If stripEmptyScreens is true, all empty pages (not just the ones on the end) will be removed + * from the Workspace, and if there are no more pages left then extra empty page(s) will be + * added. + * + * The number of extra empty pages is equal to what getPanelCount() returns. + * + * After the method returns the possible pages are: + * stripEmptyScreens = true : [non-empty pages, extra empty page(s) alone] + * stripEmptyScreens = false : [non-empty pages, empty pages (not in the end), + * extra empty page(s) alone] + */ public void removeExtraEmptyScreenDelayed( int delay, boolean stripEmptyScreens, Runnable onComplete) { if (mLauncher.isWorkspaceLoading()) { @@ -716,18 +794,26 @@ public class Workspace extends PagedView return; } + // First we convert the last page to an extra page if the last page is empty + // and we don't already have an extra page. convertFinalScreenToEmptyScreenIfNecessary(); - if (hasExtraEmptyScreen()) { - removeView(mWorkspaceScreens.get(EXTRA_EMPTY_SCREEN_ID)); + // Then we remove the extra page(s) if they are not the only pages left in Workspace. + if (hasExtraEmptyScreens()) { + forEachExtraEmptyPageId(extraEmptyPageId -> { + removeView(mWorkspaceScreens.get(extraEmptyPageId)); + mWorkspaceScreens.remove(extraEmptyPageId); + mScreenOrder.removeValue(extraEmptyPageId); + }); + setCurrentPage(getNextPage()); - mWorkspaceScreens.remove(EXTRA_EMPTY_SCREEN_ID); - mScreenOrder.removeValue(EXTRA_EMPTY_SCREEN_ID); // Update the page indicator to reflect the removed page. showPageIndicatorAtCurrentScroll(); } if (stripEmptyScreens) { + // This will remove all empty pages from the Workspace. If there are no more pages left, + // it will add extra page(s) so that users can put items on at least one page. stripEmptyScreens(); } @@ -736,27 +822,56 @@ public class Workspace extends PagedView } } - public boolean hasExtraEmptyScreen() { - return mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID) && getChildCount() > 1; + public boolean hasExtraEmptyScreens() { + return mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID) + && getChildCount() > getPanelCount() + && (!isTwoPanelEnabled() + || mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_SECOND_ID)); } - public int commitExtraEmptyScreen() { + /** + * Commits the extra empty pages then returns the screen ids of those new screens. + * Usually there's only one extra empty screen, but when two panel home is enabled we commit + * two extra screens. + * + * Returns an empty IntSet in case we cannot commit any new screens. + */ + public IntSet commitExtraEmptyScreens() { if (mLauncher.isWorkspaceLoading()) { // Invalid and dangerous operation if workspace is loading - return -1; + return new IntSet(); } - CellLayout cl = mWorkspaceScreens.get(EXTRA_EMPTY_SCREEN_ID); - mWorkspaceScreens.remove(EXTRA_EMPTY_SCREEN_ID); - mScreenOrder.removeValue(EXTRA_EMPTY_SCREEN_ID); + IntSet extraEmptyPageIds = new IntSet(); + forEachExtraEmptyPageId(extraEmptyPageId -> + extraEmptyPageIds.add(commitExtraEmptyScreen(extraEmptyPageId))); - int newId = LauncherSettings.Settings.call(getContext().getContentResolver(), + return extraEmptyPageIds; + } + + private int commitExtraEmptyScreen(int emptyScreenId) { + CellLayout cl = mWorkspaceScreens.get(emptyScreenId); + mWorkspaceScreens.remove(emptyScreenId); + mScreenOrder.removeValue(emptyScreenId); + + int newScreenId = LauncherSettings.Settings.call(getContext().getContentResolver(), LauncherSettings.Settings.METHOD_NEW_SCREEN_ID) .getInt(LauncherSettings.Settings.EXTRA_VALUE); - mWorkspaceScreens.put(newId, cl); - mScreenOrder.add(newId); - return newId; + + // When two panel home is enabled and the last page (the page on the right) doesn't + // have any items, then Launcher database doesn't know about this page because it was added + // by Launcher::bindAddScreens but wasn't inserted into the database. LauncherSettings's + // generate new screen ID method will return the ID for the left page, + // so we need to increment it. + if (isTwoPanelEnabled() && emptyScreenId == EXTRA_EMPTY_SCREEN_ID && newScreenId % 2 == 1) { + newScreenId++; + } + + mWorkspaceScreens.put(newScreenId, cl); + mScreenOrder.add(newScreenId); + + return newScreenId; } @Override @@ -857,9 +972,10 @@ public class Workspace extends PagedView } } - // We enforce at least one page to add new items to. In the case that we remove the last - // such screen, we convert the last screen to the empty screen - int minScreens = 1; + // We enforce at least one page (two pages on two panel home) to add new items to. + // In the case that we remove the last such screen(s), we convert the last screen(s) + // to the empty screen(s) + int minScreens = getPanelCount(); int pageShift = 0; for (int i = 0; i < removeScreens.size(); i++) { @@ -869,14 +985,21 @@ public class Workspace extends PagedView mScreenOrder.removeValue(id); if (getChildCount() > minScreens) { + // If this isn't the last page, just remove it if (indexOfChild(cl) < currentPage) { pageShift++; } removeView(cl); } else { - // if this is the last screen, convert it to the empty screen - mWorkspaceScreens.put(EXTRA_EMPTY_SCREEN_ID, cl); - mScreenOrder.add(EXTRA_EMPTY_SCREEN_ID); + // The last page(s) should be converted into extra empty page(s) + int extraScreenId = isTwoPanelEnabled() && id % 2 == 1 + // This is the right panel in a two panel scenario + ? EXTRA_EMPTY_SCREEN_SECOND_ID + // This is either the last screen in a one panel scenario, or the left panel + // in a two panel scenario when there are only two empty pages left + : EXTRA_EMPTY_SCREEN_ID; + mWorkspaceScreens.put(extraScreenId, cl); + mScreenOrder.add(extraScreenId); } } @@ -1646,8 +1769,8 @@ public class Workspace extends PagedView } int screenId = getIdForScreen(dropTargetLayout); - if (screenId == EXTRA_EMPTY_SCREEN_ID) { - commitExtraEmptyScreen(); + if (Workspace.EXTRA_EMPTY_SCREEN_IDS.contains(screenId)) { + commitExtraEmptyScreens(); } return true; diff --git a/src/com/android/launcher3/WorkspaceLayoutManager.java b/src/com/android/launcher3/WorkspaceLayoutManager.java index 44a5536c92..7e6e1b60de 100644 --- a/src/com/android/launcher3/WorkspaceLayoutManager.java +++ b/src/com/android/launcher3/WorkspaceLayoutManager.java @@ -23,6 +23,7 @@ import com.android.launcher3.folder.Folder; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.touch.ItemLongClickListener; +import com.android.launcher3.util.IntSet; public interface WorkspaceLayoutManager { @@ -30,6 +31,12 @@ public interface WorkspaceLayoutManager { // The screen id used for the empty screen always present at the end. int EXTRA_EMPTY_SCREEN_ID = -201; + // The screen id used for the second empty screen always present at the end for two panel home. + int EXTRA_EMPTY_SCREEN_SECOND_ID = -200; + // The screen ids used for the empty screens at the end of the workspaces. + IntSet EXTRA_EMPTY_SCREEN_IDS = + IntSet.wrap(EXTRA_EMPTY_SCREEN_ID, EXTRA_EMPTY_SCREEN_SECOND_ID); + // The is the first screen. It is always present, even if its empty. int FIRST_SCREEN_ID = 0; // This is the second page. On two panel home it is always present, even if its empty. @@ -81,9 +88,9 @@ public interface WorkspaceLayoutManager { return; } } - if (screenId == EXTRA_EMPTY_SCREEN_ID) { + if (EXTRA_EMPTY_SCREEN_IDS.contains(screenId)) { // This should never happen - throw new RuntimeException("Screen id should not be EXTRA_EMPTY_SCREEN_ID"); + throw new RuntimeException("Screen id should not be extra empty screen: " + screenId); } final CellLayout layout; diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java index 9faac5b7ef..2032b26992 100644 --- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java +++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java @@ -45,6 +45,7 @@ import com.android.launcher3.popup.ArrowPopup; import com.android.launcher3.popup.PopupContainerWithArrow; import com.android.launcher3.touch.ItemLongClickListener; import com.android.launcher3.util.IntArray; +import com.android.launcher3.util.IntSet; import com.android.launcher3.util.ShortcutUtil; import com.android.launcher3.util.Thunk; import com.android.launcher3.views.OptionsPopupView; @@ -221,6 +222,9 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme } else if (action == ADD_TO_WORKSPACE) { final int[] coordinates = new int[2]; final int screenId = findSpaceOnWorkspace(item, coordinates); + if (screenId == -1) { + return false; + } mLauncher.getStateManager().goToState(NORMAL, true, forSuccessCallback(() -> { if (item instanceof AppInfo) { WorkspaceItemInfo info = ((AppInfo) item).makeWorkspaceItem(); @@ -250,6 +254,9 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme final int[] coordinates = new int[2]; final int screenId = findSpaceOnWorkspace(item, coordinates); + if (screenId == -1) { + return false; + } mLauncher.getModelWriter().moveItemInDatabase(info, Favorites.CONTAINER_DESKTOP, screenId, coordinates[0], coordinates[1]); @@ -489,8 +496,14 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme return screenId; } - workspace.addExtraEmptyScreen(); - screenId = workspace.commitExtraEmptyScreen(); + workspace.addExtraEmptyScreens(); + IntSet emptyScreenIds = workspace.commitExtraEmptyScreens(); + if (emptyScreenIds.isEmpty()) { + // Couldn't create extra empty screens for some reason (e.g. Workspace is loading) + return -1; + } + + screenId = emptyScreenIds.getArray().get(0); layout = workspace.getScreenWithId(screenId); found = layout.findCellForSpan(outCoordinates, info.spanX, info.spanY); diff --git a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java index f96afa856b..bf5a24b65b 100644 --- a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java +++ b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java @@ -68,6 +68,9 @@ public class ShortcutMenuAccessibilityDelegate extends LauncherAccessibilityDele final WorkspaceItemInfo info = ((DeepShortcutView) host.getParent()).getFinalInfo(); final int[] coordinates = new int[2]; final int screenId = findSpaceOnWorkspace(item, coordinates); + if (screenId == -1) { + return false; + } mLauncher.getStateManager().goToState(NORMAL, true, forSuccessCallback(() -> { mLauncher.getModelWriter().addItemToDatabase(info, LauncherSettings.Favorites.CONTAINER_DESKTOP, diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java index 7091d2b143..4fdc412459 100644 --- a/src/com/android/launcher3/model/data/ItemInfo.java +++ b/src/com/android/launcher3/model/data/ItemInfo.java @@ -232,9 +232,9 @@ public class ItemInfo { * Write the fields of this item to the DB */ public void onAddToDatabase(ContentWriter writer) { - if (screenId == Workspace.EXTRA_EMPTY_SCREEN_ID) { + if (Workspace.EXTRA_EMPTY_SCREEN_IDS.contains(screenId)) { // We should never persist an item on the extra empty screen. - throw new RuntimeException("Screen id should not be EXTRA_EMPTY_SCREEN_ID"); + throw new RuntimeException("Screen id should not be extra empty screen: " + screenId); } writeToValues(writer); diff --git a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java index c51f66f407..8a7cae90bc 100644 --- a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java +++ b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java @@ -150,8 +150,8 @@ public class WallpaperOffsetInterpolator extends BroadcastReceiver { */ private int getNumPagesExcludingEmpty() { int numOfPages = mWorkspace.getChildCount(); - if (numOfPages >= MIN_PARALLAX_PAGE_SPAN && mWorkspace.hasExtraEmptyScreen()) { - return numOfPages - 1; + if (numOfPages >= MIN_PARALLAX_PAGE_SPAN && mWorkspace.hasExtraEmptyScreens()) { + return numOfPages - mWorkspace.getPanelCount(); } else { return numOfPages; }