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

Commit 2b9353ff authored by Mady Mellor's avatar Mady Mellor Committed by Android (Google) Code Review
Browse files

Merge "Support for a floating task window (behind a sysui flag)" into tm-qpr-dev

parents 591f54ba 38edb219
Loading
Loading
Loading
Loading
+24 −0
Original line number Diff line number Diff line
@@ -297,4 +297,28 @@
      when the pinned stack size is overridden by app via minWidth/minHeight.
    -->
    <dimen name="overridable_minimal_size_pip_resizable_task">48dp</dimen>

    <!-- The size of the drag handle / menu shown along with a floating task. -->
    <dimen name="floating_task_menu_size">32dp</dimen>

    <!-- The size of menu items in the floating task menu. -->
    <dimen name="floating_task_menu_item_size">24dp</dimen>

    <!-- The horizontal margin of menu items in the floating task menu. -->
    <dimen name="floating_task_menu_item_padding">5dp</dimen>

    <!-- The width of visible floating view region when stashed. -->
    <dimen name="floating_task_stash_offset">32dp</dimen>

    <!-- The amount of elevation for a floating task. -->
    <dimen name="floating_task_elevation">8dp</dimen>

    <!-- The amount of padding around the bottom and top of the task. -->
    <dimen name="floating_task_vertical_padding">8dp</dimen>

    <!-- The normal size of the dismiss target. -->
    <dimen name="floating_task_dismiss_circle_size">150dp</dimen>

    <!-- The smaller size of the dismiss target (shrinks when something is in the target). -->
    <dimen name="floating_dismiss_circle_small">120dp</dimen>
</resources>
+2 −0
Original line number Diff line number Diff line
@@ -49,6 +49,8 @@ public class DismissCircleView extends FrameLayout {
    @Override
    protected void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        final Resources res = getResources();
        setBackground(res.getDrawable(R.drawable.dismiss_circle_background));
        setViewSizes();
    }

+42 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.SystemProperties;
import android.view.IWindowManager;
import android.view.WindowManager;

import com.android.internal.logging.UiEventLogger;
import com.android.launcher3.icons.IconProvider;
@@ -60,6 +61,8 @@ import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
import com.android.wm.shell.displayareahelper.DisplayAreaHelperController;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.floating.FloatingTasks;
import com.android.wm.shell.floating.FloatingTasksController;
import com.android.wm.shell.freeform.FreeformComponents;
import com.android.wm.shell.fullscreen.FullscreenTaskListener;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController;
@@ -571,6 +574,45 @@ public abstract class WMShellBaseModule {
        return Optional.empty();
    }

    //
    // Floating tasks
    //

    @WMSingleton
    @Provides
    static Optional<FloatingTasks> provideFloatingTasks(
            Optional<FloatingTasksController> floatingTaskController) {
        return floatingTaskController.map((controller) -> controller.asFloatingTasks());
    }

    @WMSingleton
    @Provides
    static Optional<FloatingTasksController> provideFloatingTasksController(Context context,
            ShellInit shellInit,
            ShellController shellController,
            ShellCommandHandler shellCommandHandler,
            WindowManager windowManager,
            ShellTaskOrganizer organizer,
            TaskViewTransitions taskViewTransitions,
            @ShellMainThread ShellExecutor mainExecutor,
            @ShellBackgroundThread ShellExecutor bgExecutor,
            SyncTransactionQueue syncQueue) {
        if (FloatingTasksController.FLOATING_TASKS_ENABLED) {
            return Optional.of(new FloatingTasksController(context,
                    shellInit,
                    shellController,
                    shellCommandHandler,
                    windowManager,
                    organizer,
                    taskViewTransitions,
                    mainExecutor,
                    bgExecutor,
                    syncQueue));
        } else {
            return Optional.empty();
        }
    }

    //
    // Starting window
    //
+259 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.wm.shell.floating;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.view.MotionEvent;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.dynamicanimation.animation.DynamicAnimation;

import com.android.wm.shell.R;
import com.android.wm.shell.bubbles.DismissView;
import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
import com.android.wm.shell.floating.views.FloatingTaskLayer;
import com.android.wm.shell.floating.views.FloatingTaskView;

import java.util.Objects;

/**
 * Controls a floating dismiss circle that has a 'magnetic' field around it, causing views moved
 * close to the target to be stuck to it unless moved out again.
 */
public class FloatingDismissController {

    /** Velocity required to dismiss the view without dragging it into the dismiss target. */
    private static final float FLING_TO_DISMISS_MIN_VELOCITY = 4000f;
    /**
     * Max velocity that the view can be moving through the target with to stick (i.e. if it's
     * more than this velocity, it will pass through the target.
     */
    private static final float STICK_TO_TARGET_MAX_X_VELOCITY = 2000f;
    /**
     * Percentage of the target width to use to determine if an object flung towards the target
     * should dismiss (e.g. if target is 100px and this is set ot 2f, anything flung within a
     * 200px-wide area around the target will be considered 'near' enough get dismissed).
     */
    private static final float FLING_TO_TARGET_WIDTH_PERCENT = 2f;
    /** Minimum alpha to apply to the view being dismissed when it is in the target. */
    private static final float DISMISS_VIEW_MIN_ALPHA = 0.6f;
    /** Amount to scale down the view being dismissed when it is in the target. */
    private static final float DISMISS_VIEW_SCALE_DOWN_PERCENT = 0.15f;

    private Context mContext;
    private FloatingTasksController mController;
    private FloatingTaskLayer mParent;

    private DismissView mDismissView;
    private ValueAnimator mDismissAnimator;
    private View mViewBeingDismissed;
    private float mDismissSizePercent;
    private float mDismissSize;

    /**
     * The currently magnetized object, which is being dragged and will be attracted to the magnetic
     * dismiss target.
     */
    private MagnetizedObject<View> mMagnetizedObject;
    /**
     * The MagneticTarget instance for our circular dismiss view. This is added to the
     * MagnetizedObject instances for the view being dragged.
     */
    private MagnetizedObject.MagneticTarget mMagneticTarget;
    /** Magnet listener that handles animating and dismissing the view. */
    private MagnetizedObject.MagnetListener mFloatingViewMagnetListener;

    public FloatingDismissController(Context context, FloatingTasksController controller,
            FloatingTaskLayer parent) {
        mContext = context;
        mController = controller;
        mParent = parent;
        updateSizes();
        createAndAddDismissView();

        mDismissAnimator = ValueAnimator.ofFloat(1f, 0f);
        mDismissAnimator.addUpdateListener(animation -> {
            final float value = (float) animation.getAnimatedValue();
            if (mDismissView != null) {
                mDismissView.setPivotX((mDismissView.getRight() - mDismissView.getLeft()) / 2f);
                mDismissView.setPivotY((mDismissView.getBottom() - mDismissView.getTop()) / 2f);
                final float scaleValue = Math.max(value, mDismissSizePercent);
                mDismissView.getCircle().setScaleX(scaleValue);
                mDismissView.getCircle().setScaleY(scaleValue);
            }
            if (mViewBeingDismissed != null) {
                // TODO: alpha doesn't actually apply to taskView currently.
                mViewBeingDismissed.setAlpha(Math.max(value, DISMISS_VIEW_MIN_ALPHA));
                mViewBeingDismissed.setScaleX(Math.max(value, DISMISS_VIEW_SCALE_DOWN_PERCENT));
                mViewBeingDismissed.setScaleY(Math.max(value, DISMISS_VIEW_SCALE_DOWN_PERCENT));
            }
        });

        mFloatingViewMagnetListener = new MagnetizedObject.MagnetListener() {
            @Override
            public void onStuckToTarget(
                    @NonNull MagnetizedObject.MagneticTarget target) {
                animateDismissing(/* dismissing= */ true);
            }

            @Override
            public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
                    float velX, float velY, boolean wasFlungOut) {
                animateDismissing(/* dismissing= */ false);
                mParent.onUnstuckFromTarget((FloatingTaskView) mViewBeingDismissed, velX, velY,
                        wasFlungOut);
            }

            @Override
            public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
                doDismiss();
            }
        };
    }

    /** Updates all the sizes used and applies them to the {@link DismissView}. */
    public void updateSizes() {
        Resources res = mContext.getResources();
        mDismissSize = res.getDimensionPixelSize(
                R.dimen.floating_task_dismiss_circle_size);
        final float minDismissSize = res.getDimensionPixelSize(
                R.dimen.floating_dismiss_circle_small);
        mDismissSizePercent = minDismissSize / mDismissSize;

        if (mDismissView != null) {
            mDismissView.updateResources();
        }
    }

    /** Prepares the view being dragged to be magnetic. */
    public void setUpMagneticObject(View viewBeingDragged) {
        mViewBeingDismissed = viewBeingDragged;
        mMagnetizedObject = getMagnetizedView(viewBeingDragged);
        mMagnetizedObject.clearAllTargets();
        mMagnetizedObject.addTarget(mMagneticTarget);
        mMagnetizedObject.setMagnetListener(mFloatingViewMagnetListener);
    }

    /** Shows or hides the dismiss target. */
    public void showDismiss(boolean show) {
        if (show) {
            mDismissView.show();
        } else {
            mDismissView.hide();
        }
    }

    /** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */
    public boolean passEventToMagnetizedObject(MotionEvent event) {
        return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
    }

    private void createAndAddDismissView() {
        if (mDismissView != null) {
            mParent.removeView(mDismissView);
        }
        mDismissView = new DismissView(mContext);
        mDismissView.setTargetSizeResId(R.dimen.floating_task_dismiss_circle_size);
        mDismissView.updateResources();
        mParent.addView(mDismissView);

        final float dismissRadius = mDismissSize;
        // Save the MagneticTarget instance for the newly set up view - we'll add this to the
        // MagnetizedObjects when the dismiss view gets shown.
        mMagneticTarget = new MagnetizedObject.MagneticTarget(
                mDismissView.getCircle(), (int) dismissRadius);
    }

    private MagnetizedObject<View> getMagnetizedView(View v) {
        if (mMagnetizedObject != null
                && Objects.equals(mMagnetizedObject.getUnderlyingObject(), v)) {
            // Same view being dragged, we can reuse the magnetic object.
            return mMagnetizedObject;
        }
        MagnetizedObject<View> magnetizedView = new MagnetizedObject<View>(
                mContext,
                v,
                DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y
        ) {
            @Override
            public float getWidth(@NonNull View underlyingObject) {
                return underlyingObject.getWidth();
            }

            @Override
            public float getHeight(@NonNull View underlyingObject) {
                return underlyingObject.getHeight();
            }

            @Override
            public void getLocationOnScreen(@NonNull View underlyingObject,
                    @NonNull int[] loc) {
                loc[0] = (int) underlyingObject.getTranslationX();
                loc[1] = (int) underlyingObject.getTranslationY();
            }
        };
        magnetizedView.setHapticsEnabled(true);
        magnetizedView.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
        magnetizedView.setStickToTargetMaxXVelocity(STICK_TO_TARGET_MAX_X_VELOCITY);
        magnetizedView.setFlingToTargetWidthPercent(FLING_TO_TARGET_WIDTH_PERCENT);
        return magnetizedView;
    }

    /** Animates the dismiss treatment on the view being dismissed. */
    private void animateDismissing(boolean shouldDismiss) {
        if (mViewBeingDismissed == null) {
            return;
        }
        if (shouldDismiss) {
            mDismissAnimator.removeAllListeners();
            mDismissAnimator.start();
        } else {
            mDismissAnimator.removeAllListeners();
            mDismissAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    resetDismissAnimator();
                }
            });
            mDismissAnimator.reverse();
        }
    }

    /** Actually dismisses the view. */
    private void doDismiss() {
        mDismissView.hide();
        mController.removeTask();
        resetDismissAnimator();
        mViewBeingDismissed = null;
    }

    private void resetDismissAnimator() {
        mDismissAnimator.removeAllListeners();
        mDismissAnimator.cancel();
        if (mDismissView != null) {
            mDismissView.cancelAnimators();
            mDismissView.getCircle().setScaleX(1f);
            mDismissView.getCircle().setScaleY(1f);
        }
    }
}
+41 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.wm.shell.floating;

import android.content.Intent;

import com.android.wm.shell.common.annotations.ExternalThread;

/**
 * Interface to interact with floating tasks.
 */
@ExternalThread
public interface FloatingTasks {

    /**
     * Shows, stashes, or un-stashes the floating task depending on state:
     * - If there is no floating task for this intent, it shows the task for the provided intent.
     * - If there is a floating task for this intent, but it's stashed, this un-stashes it.
     * - If there is a floating task for this intent, and it's not stashed, this stashes it.
     */
    void showOrSetStashed(Intent intent);

    /** Returns a binder that can be passed to an external process to manipulate FloatingTasks. */
    default IFloatingTasks createExternalInterface() {
        return null;
    }
}
Loading