/* * Copyright (C) 2020 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.hybridhotseat; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_EDU_ONLY_TIP; import android.content.Intent; import android.graphics.Rect; import android.util.Log; import android.view.Gravity; import android.view.View; import com.android.launcher3.BubbleTextView; import com.android.launcher3.CellLayout; import com.android.launcher3.Hotseat; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherSettings; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.util.GridOccupancy; import com.android.launcher3.util.IntArray; import com.android.launcher3.views.ArrowTipView; import com.android.launcher3.views.Snackbar; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; import java.util.stream.IntStream; /** * Controller class for managing user onboaridng flow for hybrid hotseat */ public class HotseatEduController { private static final String TAG = "HotseatEduController"; public static final String SETTINGS_ACTION = "android.settings.ACTION_CONTENT_SUGGESTIONS_SETTINGS"; private final Launcher mLauncher; private final Hotseat mHotseat; private List mPredictedApps; private HotseatEduDialog mActiveDialog; private ArrayList mNewItems = new ArrayList<>(); private IntArray mNewScreens = null; HotseatEduController(Launcher launcher) { mLauncher = launcher; mHotseat = launcher.getHotseat(); } /** * Checks what type of migration should be used and migrates hotseat */ void migrate() { HotseatRestoreHelper.createBackup(mLauncher); if (FeatureFlags.HOTSEAT_MIGRATE_TO_FOLDER.get()) { migrateToFolder(); } else { migrateHotseatWhole(); } Snackbar.show(mLauncher, R.string.hotsaet_tip_prediction_enabled, R.string.hotseat_prediction_settings, null, () -> mLauncher.startActivity(getSettingsIntent())); } /** * This migration places all non folder items in the hotseat into a folder and then moves * all folders in the hotseat to a workspace page that has enough empty spots. * * @return pageId that has accepted the items. */ private int migrateToFolder() { ArrayDeque folders = new ArrayDeque<>(); ArrayList putIntoFolder = new ArrayList<>(); //separate folders and items that can get in folders for (int i = 0; i < mLauncher.getDeviceProfile().numShownHotseatIcons; i++) { View view = mHotseat.getChildAt(i, 0); if (view == null) continue; ItemInfo info = (ItemInfo) view.getTag(); if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { folders.add((FolderInfo) info); } else if (info instanceof WorkspaceItemInfo && info.container == LauncherSettings .Favorites.CONTAINER_HOTSEAT) { putIntoFolder.add((WorkspaceItemInfo) info); } } // create a temp folder and add non folder items to it if (!putIntoFolder.isEmpty()) { ItemInfo firstItem = putIntoFolder.get(0); FolderInfo folderInfo = new FolderInfo(); mLauncher.getModelWriter().addItemToDatabase(folderInfo, firstItem.container, firstItem.screenId, firstItem.cellX, firstItem.cellY); folderInfo.setTitle("", mLauncher.getModelWriter()); folderInfo.contents.addAll(putIntoFolder); for (int i = 0; i < folderInfo.contents.size(); i++) { ItemInfo item = folderInfo.contents.get(i); item.rank = i; mLauncher.getModelWriter().moveItemInDatabase(item, folderInfo.id, 0, item.cellX, item.cellY); } folders.add(folderInfo); } mNewItems.addAll(folders); return placeFoldersInWorkspace(folders); } private int placeFoldersInWorkspace(ArrayDeque folders) { if (folders.isEmpty()) return 0; Workspace workspace = mLauncher.getWorkspace(); InvariantDeviceProfile idp = mLauncher.getDeviceProfile().inv; GridOccupancy[] occupancyList = new GridOccupancy[workspace.getChildCount()]; for (int i = 0; i < occupancyList.length; i++) { occupancyList[i] = ((CellLayout) workspace.getChildAt(i)).cloneGridOccupancy(); } //scan every screen to find available spots to place folders int occupancyIndex = 0; int[] itemXY = new int[2]; while (occupancyIndex < occupancyList.length && !folders.isEmpty()) { GridOccupancy occupancy = occupancyList[occupancyIndex]; if (occupancy.findVacantCell(itemXY, 1, 1)) { FolderInfo info = folders.poll(); mLauncher.getModelWriter().moveItemInDatabase(info, LauncherSettings.Favorites.CONTAINER_DESKTOP, workspace.getScreenIdForPageIndex(occupancyIndex), itemXY[0], itemXY[1]); occupancy.markCells(info, true); } else { occupancyIndex++; } } if (folders.isEmpty()) return workspace.getScreenIdForPageIndex(occupancyIndex); int screenId = LauncherSettings.Settings.call(mLauncher.getContentResolver(), LauncherSettings.Settings.METHOD_NEW_SCREEN_ID) .getInt(LauncherSettings.Settings.EXTRA_VALUE); // if all screens are full and we still have folders left, put those on a new page FolderInfo folderInfo; int col = 0; while ((folderInfo = folders.poll()) != null) { mLauncher.getModelWriter().moveItemInDatabase(folderInfo, LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, col++, idp.numRows - 1); } mNewScreens = IntArray.wrap(screenId); return workspace.getPageCount(); } /** * This migration option attempts to move the entire hotseat up to the first workspace that * has space to host items. If no such page is found, it moves items to a new page. * * @return pageId where items are migrated */ private int migrateHotseatWhole() { Workspace workspace = mLauncher.getWorkspace(); int pageId = -1; int toRow = 0; for (int i = 0; i < workspace.getPageCount(); i++) { CellLayout target = workspace.getScreenWithId(workspace.getScreenIdForPageIndex(i)); if (target.makeSpaceForHotseatMigration(true)) { toRow = mLauncher.getDeviceProfile().inv.numRows - 1; pageId = i; break; } } if (pageId == -1) { pageId = LauncherSettings.Settings.call(mLauncher.getContentResolver(), LauncherSettings.Settings.METHOD_NEW_SCREEN_ID) .getInt(LauncherSettings.Settings.EXTRA_VALUE); mNewScreens = IntArray.wrap(pageId); } boolean isPortrait = !mLauncher.getDeviceProfile().isVerticalBarLayout(); int hotseatItemsNum = mLauncher.getDeviceProfile().numShownHotseatIcons; for (int i = 0; i < hotseatItemsNum; i++) { int x = isPortrait ? i : 0; int y = isPortrait ? 0 : hotseatItemsNum - i - 1; View child = mHotseat.getChildAt(x, y); if (child == null || child.getTag() == null) continue; ItemInfo tag = (ItemInfo) child.getTag(); if (tag.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) continue; mLauncher.getModelWriter().moveItemInDatabase(tag, LauncherSettings.Favorites.CONTAINER_DESKTOP, pageId, i, toRow); mNewItems.add(tag); } return pageId; } void moveHotseatItems() { mHotseat.removeAllViewsInLayout(); if (!mNewItems.isEmpty()) { int lastPage = mNewItems.get(mNewItems.size() - 1).screenId; ArrayList animated = new ArrayList<>(); ArrayList nonAnimated = new ArrayList<>(); for (ItemInfo info : mNewItems) { if (info.screenId == lastPage) { animated.add(info); } else { nonAnimated.add(info); } } mLauncher.bindAppsAdded(mNewScreens, nonAnimated, animated); } } void finishOnboarding() { mLauncher.getModel().onWorkspaceUiChanged(); } void showDimissTip() { if (mHotseat.getShortcutsAndWidgets().getChildCount() < mLauncher.getDeviceProfile().numShownHotseatIcons) { Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled, R.string.hotseat_prediction_settings, null, () -> mLauncher.startActivity(getSettingsIntent())); } else { showHotseatArrowTip(true, mLauncher.getString(R.string.hotseat_tip_no_empty_slots)); } } void setPredictedApps(List predictedApps) { mPredictedApps = predictedApps; } void showEdu() { int childCount = mHotseat.getShortcutsAndWidgets().getChildCount(); CellLayout cellLayout = mLauncher.getWorkspace().getScreenWithId(Workspace.FIRST_SCREEN_ID); // hotseat is already empty and does not require migration. show edu tip boolean requiresMigration = IntStream.range(0, childCount).anyMatch(i -> { View v = mHotseat.getShortcutsAndWidgets().getChildAt(i); return v != null && v.getTag() != null && ((ItemInfo) v.getTag()).container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; }); boolean canMigrateToFirstPage = cellLayout.makeSpaceForHotseatMigration(false); if (requiresMigration && canMigrateToFirstPage) { showDialog(); } else { if (showHotseatArrowTip(requiresMigration, mLauncher.getString( requiresMigration ? R.string.hotseat_tip_no_empty_slots : R.string.hotseat_auto_enrolled))) { mLauncher.getStatsLogManager().logger().log(LAUNCHER_HOTSEAT_EDU_ONLY_TIP); } finishOnboarding(); } } /** * Finds a child suitable child in hotseat and shows arrow tip pointing at it. * * @param usePinned used to determine target view. If true, will use the first matching pinned * item. Otherwise, will use the first predicted child * @param message String to be shown inside the arrowView * @return whether suitable child was found and tip was shown */ private boolean showHotseatArrowTip(boolean usePinned, String message) { int childCount = mHotseat.getShortcutsAndWidgets().getChildCount(); boolean isPortrait = !mLauncher.getDeviceProfile().isVerticalBarLayout(); BubbleTextView tipTargetView = null; for (int i = childCount - 1; i > -1; i--) { int x = isPortrait ? i : 0; int y = isPortrait ? 0 : i; View v = mHotseat.getShortcutsAndWidgets().getChildAt(x, y); if (v instanceof BubbleTextView && v.getTag() instanceof WorkspaceItemInfo) { ItemInfo info = (ItemInfo) v.getTag(); boolean isPinned = info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT; if (isPinned == usePinned) { tipTargetView = (BubbleTextView) v; break; } } } if (tipTargetView == null) { Log.e(TAG, "Unable to find suitable view for ArrowTip"); return false; } Rect bounds = Utilities.getViewBounds(tipTargetView); new ArrowTipView(mLauncher).show(message, Gravity.END, bounds.centerX(), bounds.top); return true; } void showDialog() { if (mPredictedApps == null || mPredictedApps.isEmpty()) { return; } if (mActiveDialog != null) { mActiveDialog.handleClose(false); } mActiveDialog = HotseatEduDialog.getDialog(mLauncher); mActiveDialog.setHotseatEduController(this); mActiveDialog.show(mPredictedApps); } static Intent getSettingsIntent() { return new Intent(SETTINGS_ACTION).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } }