blob: 4f95254a72490201e675611d5b0c871a0e3ade71 [file] [log] [blame]
/*
* 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.view.View;
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.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 {
public static final String HOTSEAT_EDU_ACTION =
"com.android.launcher3.action.SHOW_HYBRID_HOTSEAT_EDU";
public static final String SETTINGS_ACTION =
"android.settings.ACTION_CONTENT_SUGGESTIONS_SETTINGS";
private final Launcher mLauncher;
private final Hotseat mHotseat;
private HotseatRestoreHelper mRestoreHelper;
private List<WorkspaceItemInfo> mPredictedApps;
private HotseatEduDialog mActiveDialog;
private ArrayList<ItemInfo> mNewItems = new ArrayList<>();
private IntArray mNewScreens = null;
private Runnable mOnOnboardingComplete;
HotseatEduController(Launcher launcher, HotseatRestoreHelper restoreHelper, Runnable runnable) {
mLauncher = launcher;
mHotseat = launcher.getHotseat();
mRestoreHelper = restoreHelper;
mOnOnboardingComplete = runnable;
}
/**
* Checks what type of migration should be used and migrates hotseat
*/
void migrate() {
if (mRestoreHelper != null) {
mRestoreHelper.createBackup();
}
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<FolderInfo> folders = new ArrayDeque<>();
ArrayList<WorkspaceItemInfo> putIntoFolder = new ArrayList<>();
//separate folders and items that can get in folders
for (int i = 0; i < mLauncher.getDeviceProfile().inv.numHotseatIcons; 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<FolderInfo> 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);
}
for (int i = 0; i < mLauncher.getDeviceProfile().inv.numHotseatIcons; i++) {
View child = mHotseat.getChildAt(i, 0);
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<ItemInfo> animated = new ArrayList<>();
ArrayList<ItemInfo> 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() {
mOnOnboardingComplete.run();
}
void showDimissTip() {
if (mHotseat.getShortcutsAndWidgets().getChildCount()
< mLauncher.getDeviceProfile().inv.numHotseatIcons) {
Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled,
R.string.hotseat_prediction_settings, null,
() -> mLauncher.startActivity(getSettingsIntent()));
} else {
new ArrowTipView(mLauncher).show(
mLauncher.getString(R.string.hotseat_tip_no_empty_slots), mHotseat.getTop());
}
}
void setPredictedApps(List<WorkspaceItemInfo> 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 {
new ArrowTipView(mLauncher).show(mLauncher.getString(
requiresMigration ? R.string.hotseat_tip_no_empty_slots
: R.string.hotseat_auto_enrolled),
mHotseat.getTop());
mLauncher.getStatsLogManager().logger().log(LAUNCHER_HOTSEAT_EDU_ONLY_TIP);
finishOnboarding();
}
}
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);
}
}