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

Commit c3d6f7d3 authored by Mady Mellor's avatar Mady Mellor
Browse files

Introduce bubble controller & friends

* BubbleController manages adding / removing / state of the bubbles,
  this is what other things should use to check what the state is.
* BubbleStackView renders the stack and deals with expanding / collapsing
  and any view stuff
* BubbleView creates the circular bubble representation and holds the
  notification entry for the bubble. BubbleStackView manages the
  BubbleViews.
* BubbleTouchHandler is where all the touch logic is, BubbleStackView
  uses and so do BubbleViews if you're dragging out a BubbleView from the
  stack to dismiss

* Adding bubbles to the screen and dismissing them are not included in
  this CL, there are later CLs including this logic

Test: manual / working on proper ones
Bug: 111236845
Change-Id: I07ae1202cc7019fcd7e00151ad3ca4a48e2e1350
parent 2938bf7f
Loading
Loading
Loading
Loading
+13 −0
Original line number Diff line number Diff line
@@ -970,4 +970,17 @@
    <dimen name="ongoing_appops_dialog_title_size">24sp</dimen>
    <!-- Text size for Ongoing App Ops dialog items -->
    <dimen name="ongoing_appops_dialog_item_size">20sp</dimen>

    <!-- How much a bubble is elevated -->
    <dimen name="bubble_elevation">8dp</dimen>
    <!-- Padding between bubbles when displayed in expanded state -->
    <dimen name="bubble_padding">8dp</dimen>
    <!-- Padding around the view displayed when the bubble is expanded -->
    <dimen name="bubble_expanded_view_padding">8dp</dimen>
    <!-- Size of the collapsed bubble -->
    <dimen name="bubble_size">56dp</dimen>
    <!-- Size of an icon displayed within the bubble -->
    <dimen name="bubble_icon_size">24dp</dimen>
    <!-- Default height of the expanded view shown when the bubble is expanded -->
    <dimen name="bubble_expanded_default_height">400dp</dimen>
</resources>
+2 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import com.android.internal.util.function.TriConsumer;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.ViewMediatorCallback;
import com.android.systemui.Dependency.DependencyProvider;
import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.classifier.FalsingManager;
import com.android.systemui.keyguard.DismissCallbackRegistry;
import com.android.systemui.qs.QSTileHost;
@@ -155,5 +156,6 @@ public class SystemUIFactory {
        providers.put(SmartReplyController.class, () -> new SmartReplyController());
        providers.put(RemoteInputQuickSettingsDisabler.class,
                () -> new RemoteInputQuickSettingsDisabler(context));
        providers.put(BubbleController.class, () -> new BubbleController(context));
    }
}
+169 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.systemui.bubbles;

import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;

import static com.android.systemui.bubbles.BubbleMovementHelper.EDGE_OVERLAP;

import android.content.Context;
import android.graphics.Point;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.FrameLayout;

import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.statusbar.notification.NotificationData;
import com.android.systemui.statusbar.phone.StatusBarWindowController;

import java.util.HashMap;
import java.util.Map;

/**
 * Bubbles are a special type of content that can "float" on top of other apps or System UI.
 * Bubbles can be expanded to show more content.
 *
 * The controller manages addition, removal, and visible state of bubbles on screen.
 */
public class BubbleController {
    private static final int MAX_BUBBLES = 5; // TODO: actually enforce this

    private static final String TAG = "BubbleController";

    private Context mContext;

    private Map<String, BubbleView> mBubbles = new HashMap<>();
    private BubbleStackView mStackView;
    private Point mDisplaySize;

    // Bubbles get added to the status bar view
    private StatusBarWindowController mStatusBarWindowController;

    public BubbleController(Context context) {
        mContext = context;
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        mDisplaySize = new Point();
        wm.getDefaultDisplay().getSize(mDisplaySize);
        mStatusBarWindowController = Dependency.get(StatusBarWindowController.class);
    }

    /**
     * Whether or not there are bubbles present, regardless of them being visible on the
     * screen (e.g. if on AOD).
     */
    public boolean hasBubbles() {
        return mBubbles.size() > 0;
    }

    /**
     * Whether the stack of bubbles is expanded or not.
     */
    public boolean isStackExpanded() {
        return mStackView != null && mStackView.isExpanded();
    }

    /**
     * Tell the stack of bubbles to collapse.
     */
    public void collapseStack() {
        if (mStackView != null) {
            mStackView.animateExpansion(false);
        }
    }

    /**
     * Tell the stack of bubbles to be dismissed, this will remove all of the bubbles in the stack.
     */
    public void dismissStack() {
        mStackView.setVisibility(GONE);
        Point startPoint = getStartPoint(mStackView.getStackWidth(), mDisplaySize);
        // Reset the position of the stack (TODO - or should we save / respect last user position?)
        mStackView.setPosition(startPoint.x, startPoint.y);
        for (String key: mBubbles.keySet()) {
            removeBubble(key);
        }
    }

    /**
     * Adds a bubble associated with the provided notification entry or updates it if it exists.
     */
    public void addBubble(NotificationData.Entry notif) {
        if (mBubbles.containsKey(notif.key)) {
            // It's an update
            BubbleView bubble = mBubbles.get(notif.key);
            mStackView.updateBubble(bubble, notif);
        } else {
            // It's new
            BubbleView bubble = new BubbleView(mContext);
            bubble.setNotif(notif);
            mBubbles.put(bubble.getKey(), bubble);

            boolean setPosition = false;
            if (mStackView == null) {
                setPosition = true;
                mStackView = new BubbleStackView(mContext);
                ViewGroup sbv = (ViewGroup) mStatusBarWindowController.getStatusBarView();
                // XXX: Bug when you expand the shade on top of expanded bubble, there is no scrim
                // between bubble and the shade
                int bubblePosition = sbv.indexOfChild(sbv.findViewById(R.id.scrim_behind)) + 1;
                sbv.addView(mStackView, bubblePosition,
                        new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
            }
            mStackView.setVisibility(VISIBLE);
            mStackView.addBubble(bubble);

            if (setPosition) {
                // Need to add the bubble to the stack before we can know the width
                Point startPoint = getStartPoint(mStackView.getStackWidth(), mDisplaySize);
                mStackView.setPosition(startPoint.x, startPoint.y);
            }
        }
    }

    /**
     * Removes the bubble associated with the {@param uri}.
     */
    public void removeBubble(String key) {
        BubbleView bv = mBubbles.get(key);
        if (bv != null) {
            mStackView.removeBubble(bv);
            bv.getEntry().setBubbleDismissed(true);
        }
    }

    // TODO: factor in PIP location / maybe last place user had it
    /**
     * Gets an appropriate starting point to position the bubble stack.
     */
    public static Point getStartPoint(int size, Point displaySize) {
        final int x = displaySize.x - size + EDGE_OVERLAP;
        final int y = displaySize.y / 4;
        return new Point(x, y);
    }

    /**
     * Gets an appropriate position for the bubble when the stack is expanded.
     */
    public static Point getExpandPoint(BubbleStackView view, int size, Point displaySize) {
        // Same place for now..
        return new Point(EDGE_OVERLAP, size);
    }

}
+326 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.systemui.bubbles;

import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN;

import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.view.View;
import android.view.WindowManager;

import com.android.systemui.bubbles.BubbleTouchHandler.FloatingView;

import java.util.Arrays;

/**
 * Math and animators to move bubbles around the screen.
 *
 * TODO: straight up copy paste from old prototype -- consider physics, see if bubble & pip
 * movements can be unified maybe?
 */
public class BubbleMovementHelper {

    private static final int MAGNET_ANIM_TIME = 150;
    public static final int EDGE_OVERLAP = 0;

    private Context mContext;
    private Point mDisplaySize;

    public BubbleMovementHelper(Context context) {
        mContext = context;
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        mDisplaySize = new Point();
        wm.getDefaultDisplay().getSize(mDisplaySize);
    }

    /**
     * @return the distance between the two provided points.
     */
    static double distance(Point p1, Point p2) {
        return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
    }

    /**
     * @return the y value of a line defined by y = mx+b
     */
    static float findY(float m, float b, float x) {
        return (m * x) + b;
    }

    /**
     * @return the x value of a line defined by y = mx+b
     */
    static float findX(float m, float b, float y) {
        return (y - b) / m;
    }

    /**
     * Determines a point on the edge of the screen based on the velocity and position.
     */
    public Point getPointOnEdge(View bv, Point p, float velX, float velY) {
        // Find the slope and the y-intercept
        velX = velX == 0 ? 1 : velX;
        final float m = velY / velX;
        final float b = p.y - m * p.x;

        // There are two lines it can intersect, find the two points
        Point pointHoriz = new Point();
        Point pointVert = new Point();

        if (velX > 0) {
            // right
            pointHoriz.x = mDisplaySize.x;
            pointHoriz.y = (int) findY(m, b, mDisplaySize.x);
        } else {
            // left
            pointHoriz.x = EDGE_OVERLAP;
            pointHoriz.y = (int) findY(m, b, 0);
        }
        if (velY > 0) {
            // bottom
            pointVert.x = (int) findX(m, b, mDisplaySize.y);
            pointVert.y = mDisplaySize.y - getNavBarHeight();
        } else {
            // top
            pointVert.x = (int) findX(m, b, 0);
            pointVert.y = EDGE_OVERLAP;
        }

        // Use the point that's closest to the start position
        final double distanceToVertPoint = distance(p, pointVert);
        final double distanceToHorizPoint = distance(p, pointHoriz);
        boolean useVert = distanceToVertPoint < distanceToHorizPoint;
        // Check if we're being flung along the current edge, use opposite point in this case
        // XXX: on*Edge methods should actually use 'down' position of view and compare 'up' but
        // this works well enough for now
        if (onSideEdge(bv, p) && Math.abs(velY) > Math.abs(velX)) {
            // Flinging along left or right edge, favor vert edge
            useVert = true;

        } else if (onTopBotEdge(bv, p) && Math.abs(velX) > Math.abs(velY)) {
            // Flinging along top or bottom edge
            useVert = false;
        }

        if (useVert) {
            pointVert.x = capX(pointVert.x, bv);
            pointVert.y = capY(pointVert.y, bv);
            return pointVert;

        }
        pointHoriz.x = capX(pointHoriz.x, bv);
        pointHoriz.y = capY(pointHoriz.y, bv);
        return pointHoriz;
    }

    /**
     * @return whether the view is on a side edge of the screen (i.e. left or right).
     */
    public boolean onSideEdge(View fv, Point p) {
        return p.x + fv.getWidth() + EDGE_OVERLAP <= mDisplaySize.x
                - EDGE_OVERLAP
                || p.x >= EDGE_OVERLAP;
    }

    /**
     * @return whether the view is on a top or bottom edge of the screen.
     */
    public boolean onTopBotEdge(View bv, Point p) {
        return p.y >= getStatusBarHeight() + EDGE_OVERLAP
                || p.y + bv.getHeight() + EDGE_OVERLAP <= mDisplaySize.y
                - EDGE_OVERLAP;
    }

    /**
     * @return constrained x value based on screen size and how much a view can overlap with a side
     *         edge.
     */
    public int capX(float x, View bv) {
        // Floating things can't stick to top or bottom edges, so figure out if it's closer to
        // left or right and just use that side + the overlap.
        final float centerX = x + bv.getWidth() / 2;
        if (centerX > mDisplaySize.x / 2) {
            // Right side
            return mDisplaySize.x - bv.getWidth() - EDGE_OVERLAP;
        } else {
            // Left side
            return EDGE_OVERLAP;
        }
    }

    /**
     * @return constrained y value based on screen size and how much a view can overlap with a top
     *         or bottom edge.
     */
    public int capY(float y, View bv) {
        final int height = bv.getHeight();
        if (y < getStatusBarHeight() + EDGE_OVERLAP) {
            return getStatusBarHeight() + EDGE_OVERLAP;
        }
        if (y + height + EDGE_OVERLAP > mDisplaySize.y - EDGE_OVERLAP) {
            return mDisplaySize.y - height - EDGE_OVERLAP;
        }
        return (int) y;
    }

    /**
     * Animation to translate the provided view.
     */
    public AnimatorSet animateMagnetTo(final BubbleStackView bv) {
        Point pos = bv.getPosition();

        // Find the distance to each edge
        final int leftDistance = pos.x;
        final int rightDistance = mDisplaySize.x - leftDistance;
        final int topDistance = pos.y;
        final int botDistance = mDisplaySize.y - topDistance;

        int smallest;
        // Find the closest one
        int[] distances = {
                leftDistance, rightDistance, topDistance, botDistance
        };
        Arrays.sort(distances);
        smallest = distances[0];

        // Animate to the closest edge
        Point p = new Point();
        if (smallest == leftDistance) {
            p.x = capX(EDGE_OVERLAP, bv);
            p.y = capY(topDistance, bv);
        }
        if (smallest == rightDistance) {
            p.x = capX(mDisplaySize.x, bv);
            p.y = capY(topDistance, bv);
        }
        if (smallest == topDistance) {
            p.x = capX(leftDistance, bv);
            p.y = capY(0, bv);
        }
        if (smallest == botDistance) {
            p.x = capX(leftDistance, bv);
            p.y = capY(mDisplaySize.y, bv);
        }
        return getTranslateAnim(bv, p, MAGNET_ANIM_TIME);
    }

    /**
     * Animation to fling the provided view.
     */
    public AnimatorSet animateFlingTo(final BubbleStackView bv, float velX, float velY) {
        Point pos = bv.getPosition();
        Point endPos = getPointOnEdge(bv, pos, velX, velY);
        endPos = new Point(capX(endPos.x, bv), capY(endPos.y, bv));
        final double distance = Math.sqrt(Math.pow(endPos.x - pos.x, 2)
                + Math.pow(endPos.y - pos.y, 2));
        final float sumVel = Math.abs(velX) + Math.abs(velY);
        final int duration = Math.max(Math.min(200, (int) (distance * 1000f / (sumVel / 2))), 50);
        return getTranslateAnim(bv, endPos, duration);
    }

    /**
     * Animation to translate the provided view.
     */
    public AnimatorSet getTranslateAnim(final FloatingView v, Point p, int duration) {
        return getTranslateAnim(v, p, duration, 0);
    }

    /**
     * Animation to translate the provided view.
     */
    public AnimatorSet getTranslateAnim(final FloatingView v, Point p,
            int duration, int startDelay) {
        return getTranslateAnim(v, p, duration, startDelay, null);
    }

    /**
     * Animation to translate the provided view.
     *
     * @param v the view to translate.
     * @param p the point to translate to.
     * @param duration the duration of the animation.
     * @param startDelay the start delay of the animation.
     * @param listener the listener to add to the animation.
     *
     * @return the animation.
     */
    public static AnimatorSet getTranslateAnim(final FloatingView v, Point p, int duration,
            int startDelay, AnimatorListener listener) {
        Point curPos = v.getPosition();
        final ValueAnimator animX = ValueAnimator.ofFloat(curPos.x, p.x);
        animX.setDuration(duration);
        animX.setStartDelay(startDelay);
        animX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (float) animation.getAnimatedValue();
                v.setPositionX((int) value);
            }
        });

        final ValueAnimator animY = ValueAnimator.ofFloat(curPos.y, p.y);
        animY.setDuration(duration);
        animY.setStartDelay(startDelay);
        animY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (float) animation.getAnimatedValue();
                v.setPositionY((int) value);
            }
        });
        if (listener != null) {
            animY.addListener(listener);
        }

        AnimatorSet set = new AnimatorSet();
        set.playTogether(animX, animY);
        set.setInterpolator(FAST_OUT_SLOW_IN);
        return set;
    }


    // TODO -- now that this is in system we should be able to get these better, but ultimately
    // makes more sense to move to movement bounds style a la PIP
    /**
     * Returns the status bar height.
     */
    public int getStatusBarHeight() {
        Resources res = mContext.getResources();
        int resourceId = res.getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            return res.getDimensionPixelSize(resourceId);
        }
        return 0;
    }

    /**
     * Returns the status bar height.
     */
    public int getNavBarHeight() {
        Resources res = mContext.getResources();
        int resourceId = res.getIdentifier("navigation_bar_height", "dimen", "android");
        if (resourceId > 0) {
            return res.getDimensionPixelSize(resourceId);
        }
        return 0;
    }
}
+481 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading