Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 20035f18 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Migrate hotseat items into a folder" into ub-launcher3-master

parents 5de624c2 82bbdace
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:layout_gravity="center_vertical"
            android:textColor="@android:color/white"
            android:textSize="16sp"/>
+151 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2008 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;

import android.content.Context;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.graphics.drawable.ShapeDrawable;
import android.os.Handler;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.core.content.ContextCompat;

import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.graphics.TriangleShape;

/**
 * A base class for arrow tip view in launcher
 */
public class ArrowTipView extends AbstractFloatingView {

    private static final long AUTO_CLOSE_TIMEOUT_MILLIS = 10 * 1000;
    private static final long SHOW_DELAY_MS = 200;
    private static final long SHOW_DURATION_MS = 300;
    private static final long HIDE_DURATION_MS = 100;

    protected final Launcher mLauncher;
    private final Handler mHandler = new Handler();
    private Runnable mOnClosed;

    public ArrowTipView(Context context) {
        super(context, null, 0);
        mLauncher = Launcher.getLauncher(context);
        init(context);
    }

    @Override
    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            close(true);
        }
        return false;
    }

    @Override
    protected void handleClose(boolean animate) {
        if (mIsOpen) {
            if (animate) {
                animate().alpha(0f)
                        .withLayer()
                        .setStartDelay(0)
                        .setDuration(HIDE_DURATION_MS)
                        .setInterpolator(Interpolators.ACCEL)
                        .withEndAction(() -> mLauncher.getDragLayer().removeView(this))
                        .start();
            } else {
                animate().cancel();
                mLauncher.getDragLayer().removeView(this);
            }
            if (mOnClosed != null) mOnClosed.run();
            mIsOpen = false;
        }
    }

    @Override
    public void logActionCommand(int command) {
    }

    @Override
    protected boolean isOfType(int type) {
        return (type & TYPE_ON_BOARD_POPUP) != 0;
    }

    private void init(Context context) {
        inflate(context, R.layout.arrow_toast, this);
        setOrientation(LinearLayout.VERTICAL);
        View dismissButton = findViewById(R.id.dismiss);
        dismissButton.setOnClickListener(view -> {
            handleClose(true);
        });

        View arrowView = findViewById(R.id.arrow);
        ViewGroup.LayoutParams arrowLp = arrowView.getLayoutParams();
        ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
                arrowLp.width, arrowLp.height, false));
        Paint arrowPaint = arrowDrawable.getPaint();
        TypedValue typedValue = new TypedValue();
        context.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true);
        arrowPaint.setColor(ContextCompat.getColor(getContext(), typedValue.resourceId));
        // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
        arrowPaint.setPathEffect(new CornerPathEffect(
                context.getResources().getDimension(R.dimen.arrow_toast_corner_radius)));
        arrowView.setBackground(arrowDrawable);

        mIsOpen = true;

        mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS);
    }

    /**
     * Show Tip with specified string and Y location
     */
    public ArrowTipView show(String text, int top) {
        ((TextView) findViewById(R.id.text)).setText(text);
        mLauncher.getDragLayer().addView(this);

        DragLayer.LayoutParams params = (DragLayer.LayoutParams) getLayoutParams();
        params.gravity = Gravity.CENTER_HORIZONTAL;
        params.leftMargin = mLauncher.getDeviceProfile().workspacePadding.left;
        params.rightMargin = mLauncher.getDeviceProfile().workspacePadding.right;
        post(() -> setY(top - getHeight()));
        setAlpha(0);
        animate()
                .alpha(1f)
                .withLayer()
                .setStartDelay(SHOW_DELAY_MS)
                .setDuration(SHOW_DURATION_MS)
                .setInterpolator(Interpolators.DEACCEL)
                .start();
        return this;
    }

    /**
     * Register a callback fired when toast is hidden
     */
    public ArrowTipView setOnClosedCallback(Runnable runnable) {
        mOnClosed = runnable;
        return this;
    }
}
+13 −129
Original line number Diff line number Diff line
@@ -16,133 +16,30 @@

package com.android.launcher3.appprediction;

import static com.android.launcher3.AbstractFloatingView.TYPE_DISCOVERY_BOUNCE;
import static com.android.launcher3.AbstractFloatingView.TYPE_ON_BOARD_POPUP;
import static com.android.launcher3.LauncherState.ALL_APPS;
import static com.android.quickstep.logging.UserEventDispatcherExtension.ALL_APPS_PREDICTION_TIPS;

import android.content.Context;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.graphics.drawable.ShapeDrawable;
import android.os.Handler;
import android.os.UserManager;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.core.content.ContextCompat;

import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.ArrowTipView;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.LauncherStateManager;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.allapps.FloatingHeaderView;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.graphics.TriangleShape;
import com.android.systemui.shared.system.LauncherEventUtil;

/**
 * All apps tip view aligned just above prediction apps, shown to users that enter all apps for the
 * ArrowTip helper aligned just above prediction apps, shown to users that enter all apps for the
 * first time.
 */
public class AllAppsTipView extends AbstractFloatingView {
public class AllAppsTipView {

    private static final String ALL_APPS_TIP_SEEN = "launcher.all_apps_tip_seen";
    private static final long AUTO_CLOSE_TIMEOUT_MILLIS = 10 * 1000;
    private static final long SHOW_DELAY_MS = 200;
    private static final long SHOW_DURATION_MS = 300;
    private static final long HIDE_DURATION_MS = 100;

    private final Launcher mLauncher;
    private final Handler mHandler = new Handler();

    private AllAppsTipView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    private AllAppsTipView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setOrientation(LinearLayout.VERTICAL);

        mLauncher = Launcher.getLauncher(context);

        init(context);
    }

    @Override
    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            close(true);
        }
        return false;
    }

    @Override
    protected void handleClose(boolean animate) {
        if (mIsOpen) {
            if (animate) {
                animate().alpha(0f)
                        .withLayer()
                        .setStartDelay(0)
                        .setDuration(HIDE_DURATION_MS)
                        .setInterpolator(Interpolators.ACCEL)
                        .withEndAction(() -> mLauncher.getDragLayer().removeView(this))
                        .start();
            } else {
                animate().cancel();
                mLauncher.getDragLayer().removeView(this);
            }
            mLauncher.getSharedPrefs().edit().putBoolean(ALL_APPS_TIP_SEEN, true).apply();
            mIsOpen = false;
        }
    }

    @Override
    public void logActionCommand(int command) {
    }

    @Override
    protected boolean isOfType(int type) {
        return (type & TYPE_ON_BOARD_POPUP) != 0;
    }

    private void init(Context context) {
        inflate(context, R.layout.arrow_toast, this);

        TextView textView = findViewById(R.id.text);
        textView.setText(R.string.all_apps_prediction_tip);

        View dismissButton = findViewById(R.id.dismiss);
        dismissButton.setOnClickListener(view -> {
            mLauncher.getUserEventDispatcher().logActionTip(
                    LauncherEventUtil.DISMISS, ALL_APPS_PREDICTION_TIPS);
            handleClose(true);
        });

        View arrowView = findViewById(R.id.arrow);
        ViewGroup.LayoutParams arrowLp = arrowView.getLayoutParams();
        ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
                arrowLp.width, arrowLp.height, false));
        Paint arrowPaint = arrowDrawable.getPaint();
        TypedValue typedValue = new TypedValue();
        context.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true);
        arrowPaint.setColor(ContextCompat.getColor(getContext(), typedValue.resourceId));
        // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
        arrowPaint.setPathEffect(new CornerPathEffect(
                context.getResources().getDimension(R.dimen.arrow_toast_corner_radius)));
        arrowView.setBackground(arrowDrawable);

        mIsOpen = true;

        mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS);
    }

    private static boolean showAllAppsTipIfNecessary(Launcher launcher) {
        FloatingHeaderView floatingHeaderView = launcher.getAppsView().getFloatingHeaderView();
@@ -156,28 +53,15 @@ public class AllAppsTipView extends AbstractFloatingView {
            return false;
        }

        AllAppsTipView allAppsTipView = new AllAppsTipView(launcher.getAppsView().getContext(),
            null);
        launcher.getDragLayer().addView(allAppsTipView);

        DragLayer.LayoutParams params = (DragLayer.LayoutParams) allAppsTipView.getLayoutParams();
        params.gravity = Gravity.CENTER_HORIZONTAL;

        int top = floatingHeaderView.findFixedRowByType(PredictionRowView.class).getTop();
        allAppsTipView.setY(top - launcher.getResources().getDimensionPixelSize(
                R.dimen.all_apps_tip_bottom_margin));

        allAppsTipView.setAlpha(0);
        allAppsTipView.animate()
                .alpha(1f)
                .withLayer()
                .setStartDelay(SHOW_DELAY_MS)
                .setDuration(SHOW_DURATION_MS)
                .setInterpolator(Interpolators.DEACCEL)
                .start();
        int[] coords = new int[2];
        floatingHeaderView.findFixedRowByType(PredictionRowView.class).getLocationOnScreen(coords);
        ArrowTipView arrowTipView = new ArrowTipView(launcher).setOnClosedCallback(() -> {
            launcher.getSharedPrefs().edit().putBoolean(ALL_APPS_TIP_SEEN, true).apply();
            launcher.getUserEventDispatcher().logActionTip(LauncherEventUtil.DISMISS,
                    ALL_APPS_PREDICTION_TIPS);
        });
        arrowTipView.show(launcher.getString(R.string.all_apps_prediction_tip), coords[1]);

        launcher.getUserEventDispatcher().logActionTip(
                LauncherEventUtil.VISIBLE, ALL_APPS_PREDICTION_TIPS);
        return true;
    }

+157 −21
Original line number Diff line number Diff line
@@ -23,23 +23,28 @@ import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.view.View;
import android.view.ViewGroup;

import androidx.core.app.NotificationCompat;

import com.android.launcher3.CellLayout;
import com.android.launcher3.FolderInfo;
import com.android.launcher3.Hotseat;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
import com.android.launcher3.Workspace;
import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.WorkspaceLayoutManager;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.launcher3.util.ActivityTracker;
import com.android.launcher3.util.GridOccupancy;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.Themes;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;

/**
@@ -52,48 +57,179 @@ public class HotseatEduController {
    private static final int ONBOARDING_NOTIFICATION_ID = 7641;

    private final Launcher mLauncher;
    private final NotificationManager mNotificationManager;
    private final Notification mNotification;
    private List<WorkspaceItemInfo> mPredictedApps;
    private HotseatEduDialog mActiveDialog;

    private final NotificationManager mNotificationManager;
    private final Notification mNotification;
    private ArrayList<ItemInfo> mNewItems = new ArrayList<>();
    private IntArray mNewScreens = null;
    private Runnable mOnOnboardingComplete;

    HotseatEduController(Launcher launcher) {
    HotseatEduController(Launcher launcher, Runnable runnable) {
        mLauncher = launcher;
        mOnOnboardingComplete = runnable;
        mNotificationManager = mLauncher.getSystemService(NotificationManager.class);
        createNotificationChannel();
        mNotification = createNotification();
    }

    boolean migrate() {
    /**
     * Checks what type of migration should be used and migrates hotseat
     */
    void migrate() {
        if (FeatureFlags.HOTSEAT_MIGRATE_TO_FOLDER.get()) {
            migrateToFolder();
        } else {
            migrateHotseatWhole();
        }
    }

    /**
     * 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 = mLauncher.getHotseat().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) {
                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();
            folderInfo.title = "";
            mLauncher.getModelWriter().addItemToDatabase(folderInfo, firstItem.container,
                    firstItem.screenId, firstItem.cellX, firstItem.cellY);
            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();
        CellLayout firstScreen = workspace.getScreenWithId(WorkspaceLayoutManager.FIRST_SCREEN_ID);
        int toPage = Workspace.FIRST_SCREEN_ID;
        int toRow = mLauncher.getDeviceProfile().inv.numRows - 1;
        if (FeatureFlags.HOTSEAT_MIGRATE_NEW_PAGE.get()) {
            toPage = workspace.getScreenIdForPageIndex(workspace.getPageCount());
            toRow = 0;
        } else if (!firstScreen.makeSpaceForHotseatMigration(true)) {
            return false;
        }
        ViewGroup hotseatVG = mLauncher.getHotseat().getShortcutsAndWidgets();
        for (int i = 0; i < hotseatVG.getChildCount(); i++) {
            View child = hotseatVG.getChildAt(i);
        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();
        Hotseat hotseatVG = mLauncher.getHotseat();

        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);
        }
        for (int i = 0; i < mLauncher.getDeviceProfile().inv.numHotseatIcons; i++) {
            View child = hotseatVG.getChildAt(i, 0);
            if (child == null || child.getTag() == null) continue;
            ItemInfo tag = (ItemInfo) child.getTag();
            mLauncher.getModelWriter().moveItemInDatabase(tag,
                    LauncherSettings.Favorites.CONTAINER_DESKTOP, toPage, tag.screenId, toRow);
                    LauncherSettings.Favorites.CONTAINER_DESKTOP, pageId, i, toRow);
            mNewItems.add(tag);
        }
        return true;
        return pageId;
    }


    void removeNotification() {
        mNotificationManager.cancel(ONBOARDING_NOTIFICATION_ID);
    }

    void finishOnboarding() {
        mLauncher.getModel().rebindCallbacks();
        mLauncher.getHotseat().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);
        }
        mOnOnboardingComplete.run();
        destroy();
        mLauncher.getSharedPrefs().edit().putBoolean(KEY_HOTSEAT_EDU_SEEN, true).apply();
        removeNotification();
    }

    void setPredictedApps(List<WorkspaceItemInfo> predictedApps) {
+19 −28

File changed.

Preview size limit exceeded, changes collapsed.

Loading