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

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

Merge changes from topic "initial-bubbs"

* changes:
  Update scrim controller to bubble state when bubbles are expanded
  Introduce scrim state for bubbles
  Update PIP dismiss to work a little better for bubbles
  Make status bar full screen when bubbles are present
  Auto bubble some notifications (behind debug flag)
  Introduce bubble controller & friends
parents 9734fa53 cd9b1308
Loading
Loading
Loading
Loading
+13 −0
Original line number Original line Diff line number Diff line
@@ -970,4 +970,17 @@
    <dimen name="ongoing_appops_dialog_title_size">24sp</dimen>
    <dimen name="ongoing_appops_dialog_title_size">24sp</dimen>
    <!-- Text size for Ongoing App Ops dialog items -->
    <!-- Text size for Ongoing App Ops dialog items -->
    <dimen name="ongoing_appops_dialog_item_size">20sp</dimen>
    <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>
</resources>
+2 −0
Original line number Original line 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.internal.widget.LockPatternUtils;
import com.android.keyguard.ViewMediatorCallback;
import com.android.keyguard.ViewMediatorCallback;
import com.android.systemui.Dependency.DependencyProvider;
import com.android.systemui.Dependency.DependencyProvider;
import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.classifier.FalsingManager;
import com.android.systemui.classifier.FalsingManager;
import com.android.systemui.keyguard.DismissCallbackRegistry;
import com.android.systemui.keyguard.DismissCallbackRegistry;
import com.android.systemui.qs.QSTileHost;
import com.android.systemui.qs.QSTileHost;
@@ -155,5 +156,6 @@ public class SystemUIFactory {
        providers.put(SmartReplyController.class, () -> new SmartReplyController());
        providers.put(SmartReplyController.class, () -> new SmartReplyController());
        providers.put(RemoteInputQuickSettingsDisabler.class,
        providers.put(RemoteInputQuickSettingsDisabler.class,
                () -> new RemoteInputQuickSettingsDisabler(context));
                () -> new RemoteInputQuickSettingsDisabler(context));
        providers.put(BubbleController.class, () -> new BubbleController(context));
    }
    }
}
}
+344 −0
Original line number Original line 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.INVISIBLE;
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.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.service.notification.StatusBarNotification;
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.ArrayList;
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";

    // Enables some subset of notifs to automatically become bubbles
    public static final boolean DEBUG_ENABLE_AUTO_BUBBLE = false;
    // When a bubble is dismissed, recreate it as a notification
    public static final boolean DEBUG_DEMOTE_TO_NOTIF = false;

    private Context mContext;
    private BubbleDismissListener mDismissListener;
    private BubbleStateChangeListener mStateChangeListener;
    private BubbleExpandListener mExpandListener;

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

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

    // Used for determining view rect for touch interaction
    private Rect mTempRect = new Rect();

    /**
     * Listener to find out about bubble / bubble stack dismissal events.
     */
    public interface BubbleDismissListener {
        /**
         * Called when the entire stack of bubbles is dismissed by the user.
         */
        void onStackDismissed();

        /**
         * Called when a specific bubble is dismissed by the user.
         */
        void onBubbleDismissed(String key);
    }

    /**
     * Listener to be notified when some states of the bubbles change.
     */
    public interface BubbleStateChangeListener {
        /**
         * Called when the stack has bubbles or no longer has bubbles.
         */
        void onHasBubblesChanged(boolean hasBubbles);
    }

    /**
     * Listener to find out about stack expansion / collapse events.
     */
    public interface BubbleExpandListener {
        /**
         * Called when the expansion state of the bubble stack changes.
         *
         * @param isExpanding whether it's expanding or collapsing
         * @param amount fraction of how expanded or collapsed it is, 1 being fully, 0 at the start
         */
        void onBubbleExpandChanged(boolean isExpanding, float amount);
    }

    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);
    }

    /**
     * Set a listener to be notified of bubble dismissal events.
     */
    public void setDismissListener(BubbleDismissListener listener) {
        mDismissListener = listener;
    }

    /**
     * Set a listener to be notified when some states of the bubbles change.
     */
    public void setBubbleStateChangeListener(BubbleStateChangeListener listener) {
        mStateChangeListener = listener;
    }

    /**
     * Set a listener to be notified of bubble expand events.
     */
    public void setExpandListener(BubbleExpandListener listener) {
        mExpandListener = listener;
        if (mStackView != null) {
            mStackView.setExpandListener(mExpandListener);
        }
    }

    /**
     * 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() {
        if (mStackView == null) {
            return;
        }
        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);
        }
        if (mDismissListener != null) {
            mDismissListener.onStackDismissed();
        }
        updateBubblesShowing();
    }

    /**
     * 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 = mStackView != null && mStackView.getVisibility() != VISIBLE;
            if (mStackView == null) {
                setPosition = true;
                mStackView = new BubbleStackView(mContext);
                ViewGroup sbv = 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));
                if (mExpandListener != null) {
                    mStackView.setExpandListener(mExpandListener);
                }
            }
            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);
                mStackView.setVisibility(VISIBLE);
            }
            updateBubblesShowing();
        }
    }

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

    private void updateBubblesShowing() {
        boolean hasBubblesShowing = false;
        for (BubbleView bv : mBubbles.values()) {
            if (!bv.getEntry().isBubbleDismissed()) {
                hasBubblesShowing = true;
                break;
            }
        }
        boolean hadBubbles = mStatusBarWindowController.getBubblesShowing();
        mStatusBarWindowController.setBubblesShowing(hasBubblesShowing);
        if (mStackView != null && !hasBubblesShowing) {
            mStackView.setVisibility(INVISIBLE);
        }
        if (mStateChangeListener != null && hadBubbles != hasBubblesShowing) {
            mStateChangeListener.onHasBubblesChanged(hasBubblesShowing);
        }
    }

    /**
     * Sets the visibility of the bubbles, doesn't un-bubble them, just changes visibility.
     */
    public void updateVisibility(boolean visible) {
        if (mStackView == null) {
            return;
        }
        ArrayList<BubbleView> viewsToRemove = new ArrayList<>();
        for (BubbleView bv : mBubbles.values()) {
            NotificationData.Entry entry = bv.getEntry();
            if (entry != null) {
                if (entry.row.isRemoved() || entry.isBubbleDismissed() || entry.row.isDismissed()) {
                    viewsToRemove.add(bv);
                }
            }
        }
        for (BubbleView view : viewsToRemove) {
            mBubbles.remove(view.getKey());
            mStackView.removeBubble(view);
        }
        if (mStackView != null) {
            mStackView.setVisibility(visible ? VISIBLE : INVISIBLE);
            if (!visible) {
                collapseStack();
            }
        }
        updateBubblesShowing();
    }

    /**
     * Rect indicating the touchable region for the bubble stack / expanded stack.
     */
    public Rect getTouchableRegion() {
        if (mStackView == null || mStackView.getVisibility() != VISIBLE) {
            return null;
        }
        mStackView.getBoundsOnScreen(mTempRect);
        return mTempRect;
    }

    // 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);
    }

    /**
     * Whether the notification should bubble or not.
     */
    public static boolean shouldAutoBubble(NotificationData.Entry entry, int priority,
            boolean canAppOverlay) {
        if (!DEBUG_ENABLE_AUTO_BUBBLE || entry.isBubbleDismissed()) {
            return false;
        }
        StatusBarNotification n = entry.notification;
        boolean hasRemoteInput = false;
        if (n.getNotification().actions != null) {
            for (Notification.Action action : n.getNotification().actions) {
                if (action.getRemoteInputs() != null) {
                    hasRemoteInput = true;
                    break;
                }
            }
        }
        Class<? extends Notification.Style> style = n.getNotification().getNotificationStyle();
        boolean shouldBubble = priority >= NotificationManager.IMPORTANCE_HIGH
                || Notification.MessagingStyle.class.equals(style)
                || Notification.CATEGORY_MESSAGE.equals(n.getNotification().category)
                || hasRemoteInput
                || canAppOverlay;
        return shouldBubble && !entry.isBubbleDismissed();
    }
}
+326 −0

File added.

Preview size limit exceeded, changes collapsed.

+495 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading