diff options
15 files changed, 392 insertions, 68 deletions
diff --git a/libs/WindowManager/Shell/res/drawable/ic_floating_landscape.xml b/libs/WindowManager/Shell/res/drawable/ic_floating_landscape.xml new file mode 100644 index 000000000000..8ef3307ee875 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/ic_floating_landscape.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 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 + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M4,18H20V6H4V18ZM22,18C22,19.1 21.1,20 20,20H4C2.9,20 2,19.1 2,18V6C2,4.9 2.9,4 4,4H20C21.1,4 22,4.9 22,6V18ZM13,8H18V14H13V8Z" + android:fillColor="#455A64" + android:fillType="evenOdd"/> +</vector> diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml new file mode 100644 index 000000000000..b489a5c1acd0 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 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 + --> +<com.android.wm.shell.common.bubbles.BubblePopupView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="bottom|end" + android:layout_margin="@dimen/bubble_popup_margin_horizontal" + android:layout_marginBottom="120dp" + android:elevation="@dimen/bubble_manage_menu_elevation" + android:gravity="center_horizontal" + android:orientation="vertical"> + + <ImageView + android:layout_width="32dp" + android:layout_height="32dp" + android:tint="?android:attr/colorAccent" + android:contentDescription="@null" + android:src="@drawable/ic_floating_landscape"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:maxWidth="@dimen/bubble_popup_content_max_width" + android:maxLines="1" + android:ellipsize="end" + android:textAppearance="@android:style/TextAppearance.DeviceDefault.Headline" + android:textColor="?android:attr/textColorPrimary" + android:text="@string/bubble_bar_education_stack_title"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:maxWidth="@dimen/bubble_popup_content_max_width" + android:textAppearance="@android:style/TextAppearance.DeviceDefault" + android:textColor="?android:attr/textColorSecondary" + android:textAlignment="center" + android:text="@string/bubble_bar_education_stack_text"/> + +</com.android.wm.shell.common.bubbles.BubblePopupView>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 00c63d70d3a0..eabe3a4eca0b 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -163,6 +163,10 @@ <!-- [CHAR LIMIT=NONE] Empty overflow subtitle --> <string name="bubble_overflow_empty_subtitle">Recent bubbles and dismissed bubbles will appear here</string> + <!-- Title text for the bubble bar feature education cling shown when a bubble is on screen for the first time. [CHAR LIMIT=60]--> + <string name="bubble_bar_education_stack_title">Chat using bubbles</string> + <!-- Descriptive text for the bubble bar feature education cling shown when a bubble is on screen for the first time. [CHAR LIMIT=NONE] --> + <string name="bubble_bar_education_stack_text">New conversations appear as icons in a bottom corner of your screen. Tap to expand them or drag to dismiss them.</string> <!-- Title text for the bubble bar "manage" button tool tip highlighting where users can go to control bubble settings. [CHAR LIMIT=60]--> <string name="bubble_bar_education_manage_title">Control bubbles anytime</string> <!-- Descriptive text for the bubble bar "manage" button tool tip highlighting where users can go to control bubble settings. [CHAR LIMIT=80]--> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index dfdc79ea7afa..f259902e9565 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -56,6 +56,7 @@ import android.content.pm.ShortcutInfo; import android.content.pm.UserInfo; import android.content.res.Configuration; import android.graphics.PixelFormat; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Icon; import android.os.Binder; @@ -1063,6 +1064,15 @@ public class BubbleController implements ConfigurationChangeListener, } } + /** + * Show bubble bar user education relative to the reference position. + * @param position the reference position in Screen coordinates. + */ + public void showUserEducation(Point position) { + if (mLayerView == null) return; + mLayerView.showUserEducation(position); + } + @VisibleForTesting public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) { boolean isSuppressedBubble = (mBubbleData.hasAnyBubbleWithKey(key) @@ -1115,6 +1125,16 @@ public class BubbleController implements ConfigurationChangeListener, } /** + * Expands the stack if the selected bubble is present. This is currently used when user + * education view is clicked to expand the selected bubble. + */ + public void expandStackWithSelectedBubble() { + if (mBubbleData.getSelectedBubble() != null) { + mBubbleData.setExpanded(true); + } + } + + /** * Expands and selects the provided bubble as long as it already exists in the stack or the * overflow. This is currently used when opening a bubble via clicking on a conversation widget. */ @@ -1730,7 +1750,8 @@ public class BubbleController implements ConfigurationChangeListener, + " expandedChanged=" + update.expandedChanged + " selectionChanged=" + update.selectionChanged + " suppressed=" + (update.suppressedBubble != null) - + " unsuppressed=" + (update.unsuppressedBubble != null)); + + " unsuppressed=" + (update.unsuppressedBubble != null) + + " shouldShowEducation=" + update.shouldShowEducation); } ensureBubbleViewsAndWindowCreated(); @@ -2155,6 +2176,12 @@ public class BubbleController implements ConfigurationChangeListener, public void onBubbleDrag(String bubbleKey, boolean isBeingDragged) { mMainExecutor.execute(() -> mController.onBubbleDrag(bubbleKey, isBeingDragged)); } + + @Override + public void showUserEducation(int positionX, int positionY) { + mMainExecutor.execute(() -> + mController.showUserEducation(new Point(positionX, positionY))); + } } private class BubblesImpl implements Bubbles { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index c6f74af0284b..595a4afbfc86 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -77,6 +77,7 @@ public class BubbleData { boolean orderChanged; boolean suppressedSummaryChanged; boolean expanded; + boolean shouldShowEducation; @Nullable BubbleViewProvider selectedBubble; @Nullable Bubble addedBubble; @Nullable Bubble updatedBubble; @@ -126,6 +127,7 @@ public class BubbleData { bubbleBarUpdate.expandedChanged = expandedChanged; bubbleBarUpdate.expanded = expanded; + bubbleBarUpdate.shouldShowEducation = shouldShowEducation; if (selectionChanged) { bubbleBarUpdate.selectedBubbleKey = selectedBubble != null ? selectedBubble.getKey() @@ -165,6 +167,7 @@ public class BubbleData { */ BubbleBarUpdate getInitialState() { BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); + bubbleBarUpdate.shouldShowEducation = shouldShowEducation; for (int i = 0; i < bubbles.size(); i++) { bubbleBarUpdate.currentBubbleList.add(bubbles.get(i).asBubbleBarBubble()); } @@ -187,6 +190,7 @@ public class BubbleData { private final Context mContext; private final BubblePositioner mPositioner; + private final BubbleEducationController mEducationController; private final Executor mMainExecutor; /** Bubbles that are actively in the stack. */ private final List<Bubble> mBubbles; @@ -233,10 +237,11 @@ public class BubbleData { private HashMap<String, String> mSuppressedGroupKeys = new HashMap<>(); public BubbleData(Context context, BubbleLogger bubbleLogger, BubblePositioner positioner, - Executor mainExecutor) { + BubbleEducationController educationController, Executor mainExecutor) { mContext = context; mLogger = bubbleLogger; mPositioner = positioner; + mEducationController = educationController; mMainExecutor = mainExecutor; mOverflow = new BubbleOverflow(context, positioner); mBubbles = new ArrayList<>(); @@ -447,6 +452,7 @@ public class BubbleData { if (bubble.shouldAutoExpand()) { bubble.setShouldAutoExpand(false); setSelectedBubbleInternal(bubble); + if (!mExpanded) { setExpandedInternal(true); } @@ -877,6 +883,9 @@ public class BubbleData { private void dispatchPendingChanges() { if (mListener != null && mStateChange.anythingChanged()) { + mStateChange.shouldShowEducation = mSelectedBubble != null + && mEducationController.shouldShowStackEducation(mSelectedBubble) + && !mExpanded; mListener.applyUpdate(mStateChange); } mStateChange = new Update(mBubbles, mOverflowBubbles); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java index 1c0e0522d359..f56b1712c5c1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java @@ -44,7 +44,7 @@ public class BubbleDebugConfig { static final boolean DEBUG_BUBBLE_EXPANDED_VIEW = false; static final boolean DEBUG_EXPERIMENTS = true; static final boolean DEBUG_OVERFLOW = false; - static final boolean DEBUG_USER_EDUCATION = false; + public static final boolean DEBUG_USER_EDUCATION = false; static final boolean DEBUG_POSITIONER = false; public static final boolean DEBUG_COLLAPSE_ANIMATOR = false; public static boolean DEBUG_EXPANDED_VIEW_DRAGGING = false; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl index 4dda0688b790..5776ad109d19 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl @@ -39,4 +39,6 @@ interface IBubbles { oneway void onBubbleDrag(in String key, in boolean isBeingDragged) = 7; + oneway void showUserEducation(in int positionX, in int positionY) = 8; + }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index 8f11253290ea..e788341df5f8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -21,6 +21,7 @@ import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT; import android.annotation.Nullable; import android.content.Context; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.ColorDrawable; @@ -36,6 +37,8 @@ import com.android.wm.shell.bubbles.BubbleViewProvider; import java.util.function.Consumer; +import kotlin.Unit; + /** * Similar to {@link com.android.wm.shell.bubbles.BubbleStackView}, this view is added to window * manager to display bubbles. However, it is only used when bubbles are being displayed in @@ -111,7 +114,7 @@ public class BubbleBarLayerView extends FrameLayout getViewTreeObserver().removeOnComputeInternalInsetsListener(this); if (mExpandedView != null) { - mEducationViewController.hideManageEducation(/* animated = */ false); + mEducationViewController.hideEducation(/* animated = */ false); removeView(mExpandedView); mExpandedView = null; } @@ -171,7 +174,9 @@ public class BubbleBarLayerView extends FrameLayout mExpandedView.setListener(new BubbleBarExpandedView.Listener() { @Override public void onTaskCreated() { - mEducationViewController.maybeShowManageEducation(b, mExpandedView); + if (mEducationViewController != null && mExpandedView != null) { + mEducationViewController.maybeShowManageEducation(b, mExpandedView); + } } @Override @@ -190,6 +195,10 @@ public class BubbleBarLayerView extends FrameLayout addView(mExpandedView, new FrameLayout.LayoutParams(width, height)); } + if (mEducationViewController.isEducationVisible()) { + mEducationViewController.hideEducation(/* animated = */ true); + } + mIsExpanded = true; mBubbleController.getSysuiProxy().onStackExpandChanged(true); mAnimationHelper.animateExpansion(mExpandedBubble, () -> { @@ -210,7 +219,7 @@ public class BubbleBarLayerView extends FrameLayout public void collapse() { mIsExpanded = false; final BubbleBarExpandedView viewToRemove = mExpandedView; - mEducationViewController.hideManageEducation(/* animated = */ true); + mEducationViewController.hideEducation(/* animated = */ true); mAnimationHelper.animateCollapse(() -> removeView(viewToRemove)); mBubbleController.getSysuiProxy().onStackExpandChanged(false); mExpandedView = null; @@ -218,6 +227,21 @@ public class BubbleBarLayerView extends FrameLayout showScrim(false); } + /** + * Show bubble bar user education relative to the reference position. + * @param position the reference position in Screen coordinates. + */ + public void showUserEducation(Point position) { + mEducationViewController.showStackEducation(position, /* root = */ this, () -> { + // When the user education is clicked hide it and expand the selected bubble + mEducationViewController.hideEducation(/* animated = */ true, () -> { + mBubbleController.expandStackWithSelectedBubble(); + return Unit.INSTANCE; + }); + return Unit.INSTANCE; + }); + } + /** Sets the function to call to un-bubble the given conversation. */ public void setUnBubbleConversationCallback( @Nullable Consumer<String> unBubbleConversationCallback) { @@ -226,8 +250,8 @@ public class BubbleBarLayerView extends FrameLayout /** Hides the current modal education/menu view, expanded view or collapses the bubble stack */ private void hideMenuOrCollapse() { - if (mEducationViewController.isManageEducationVisible()) { - mEducationViewController.hideManageEducation(/* animated = */ true); + if (mEducationViewController.isEducationVisible()) { + mEducationViewController.hideEducation(/* animated = */ true); } else if (isExpanded() && mExpandedView != null) { mExpandedView.hideMenuOrCollapse(); } else { @@ -275,7 +299,7 @@ public class BubbleBarLayerView extends FrameLayout */ private void getTouchableRegion(Region outRegion) { mTempRect.setEmpty(); - if (mIsExpanded) { + if (mIsExpanded || mEducationViewController.isEducationVisible()) { getBoundsOnScreen(mTempRect); outRegion.op(mTempRect, Region.Op.UNION); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt index 7b39c6fd4059..ee552ae204b8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt @@ -15,23 +15,34 @@ */ package com.android.wm.shell.bubbles.bar +import android.annotation.LayoutRes import android.content.Context +import android.graphics.Point +import android.graphics.Rect +import android.util.Log import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout import androidx.core.view.doOnLayout import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.SpringForce import com.android.wm.shell.R import com.android.wm.shell.animation.PhysicsAnimator +import com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_USER_EDUCATION +import com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES +import com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME import com.android.wm.shell.bubbles.BubbleEducationController import com.android.wm.shell.bubbles.BubbleViewProvider import com.android.wm.shell.bubbles.setup +import com.android.wm.shell.common.bubbles.BubblePopupDrawable import com.android.wm.shell.common.bubbles.BubblePopupView +import kotlin.math.roundToInt /** Manages bubble education presentation and animation */ class BubbleEducationViewController(private val context: Context, private val listener: Listener) { interface Listener { - fun onManageEducationVisibilityChanged(isVisible: Boolean) + fun onEducationVisibilityChanged(isVisible: Boolean) } private var rootView: ViewGroup? = null @@ -45,61 +56,112 @@ class BubbleEducationViewController(private val context: Context, private val li ) } + private val scrimView by lazy { + View(context).apply { + importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + setOnClickListener { hideEducation(animated = true) } + } + } + private val controller by lazy { BubbleEducationController(context) } /** Whether the education view is visible or being animated */ - val isManageEducationVisible: Boolean + val isEducationVisible: Boolean get() = educationView != null && rootView != null /** - * Show manage bubble education if hasn't been shown before + * Hide the current education view if visible * - * @param bubble the bubble used for the manage education check - * @param root the view to show manage education in + * @param animated whether should hide with animation */ - fun maybeShowManageEducation(bubble: BubbleViewProvider, root: ViewGroup) { - if (!controller.shouldShowManageEducation(bubble)) return - showManageEducation(root) + @JvmOverloads + fun hideEducation(animated: Boolean, endActions: () -> Unit = {}) { + log { "hideEducation animated: $animated" } + + if (animated) { + animateTransition(show = false) { + cleanUp() + endActions() + listener.onEducationVisibilityChanged(isVisible = false) + } + } else { + cleanUp() + endActions() + listener.onEducationVisibilityChanged(isVisible = false) + } } /** - * Hide the manage education view if visible + * Show bubble bar stack user education. * - * @param animated whether should hide with animation + * @param position the reference position for the user education in Screen coordinates. + * @param root the view to show user education in. + * @param educationClickHandler the on click handler for the user education view */ - fun hideManageEducation(animated: Boolean) { - rootView?.let { - fun cleanUp() { - it.removeView(educationView) - rootView = null - listener.onManageEducationVisibilityChanged(isVisible = false) - } + fun showStackEducation(position: Point, root: ViewGroup, educationClickHandler: () -> Unit) { + hideEducation(animated = false) + log { "showStackEducation at: $position" } - if (animated) { - animateTransition(show = false, ::cleanUp) - } else { - cleanUp() + educationView = + createEducationView(R.layout.bubble_bar_stack_education, root).apply { + setArrowDirection(BubblePopupDrawable.ArrowDirection.DOWN) + setArrowPosition(BubblePopupDrawable.ArrowPosition.End) + updateEducationPosition(view = this, position, root) + val arrowToEdgeOffset = popupDrawable?.config?.cornerRadius ?: 0f + doOnLayout { + it.pivotX = it.width - arrowToEdgeOffset + it.pivotY = it.height.toFloat() + } + setOnClickListener { educationClickHandler() } } + + rootView = root + animator = createAnimator() + + root.addView(scrimView) + root.addView(educationView) + animateTransition(show = true) { + controller.hasSeenStackEducation = true + listener.onEducationVisibilityChanged(isVisible = true) } } /** + * Show manage bubble education if hasn't been shown before + * + * @param bubble the bubble used for the manage education check + * @param root the view to show manage education in + */ + fun maybeShowManageEducation(bubble: BubbleViewProvider, root: ViewGroup) { + log { "maybeShowManageEducation bubble: $bubble" } + if (!controller.shouldShowManageEducation(bubble)) return + showManageEducation(root) + } + + /** * Show manage education with animation * * @param root the view to show manage education in */ private fun showManageEducation(root: ViewGroup) { - hideManageEducation(animated = false) - if (educationView == null) { - val eduView = createEducationView(root) - educationView = eduView - animator = createAnimation(eduView) - } - root.addView(educationView) + hideEducation(animated = false) + log { "showManageEducation" } + + educationView = + createEducationView(R.layout.bubble_bar_manage_education, root).apply { + pivotY = 0f + doOnLayout { it.pivotX = it.width / 2f } + setOnClickListener { hideEducation(animated = true) } + } + rootView = root + animator = createAnimator() + + root.addView(scrimView) + root.addView(educationView) animateTransition(show = true) { controller.hasSeenManageEducation = true - listener.onManageEducationVisibilityChanged(isVisible = true) + listener.onEducationVisibilityChanged(isVisible = true) } } @@ -110,39 +172,75 @@ class BubbleEducationViewController(private val context: Context, private val li * @param endActions a closure to be called when the animation completes */ private fun animateTransition(show: Boolean, endActions: () -> Unit) { - animator?.let { animator -> - animator - .spring(DynamicAnimation.ALPHA, if (show) 1f else 0f) - .spring(DynamicAnimation.SCALE_X, if (show) 1f else EDU_SCALE_HIDDEN) - .spring(DynamicAnimation.SCALE_Y, if (show) 1f else EDU_SCALE_HIDDEN) - .withEndActions(endActions) - .start() - } ?: endActions() + animator + ?.spring(DynamicAnimation.ALPHA, if (show) 1f else 0f) + ?.spring(DynamicAnimation.SCALE_X, if (show) 1f else EDU_SCALE_HIDDEN) + ?.spring(DynamicAnimation.SCALE_Y, if (show) 1f else EDU_SCALE_HIDDEN) + ?.withEndActions(endActions) + ?.start() + ?: endActions() + } + + /** Remove education view from the root and clean up all relative properties */ + private fun cleanUp() { + log { "cleanUp" } + rootView?.removeView(educationView) + rootView?.removeView(scrimView) + educationView = null + rootView = null + animator = null } - private fun createEducationView(root: ViewGroup): BubblePopupView { - val view = - LayoutInflater.from(context).inflate(R.layout.bubble_bar_manage_education, root, false) - as BubblePopupView - - return view.apply { - setup() - alpha = 0f - pivotY = 0f - scaleX = EDU_SCALE_HIDDEN - scaleY = EDU_SCALE_HIDDEN - doOnLayout { it.pivotX = it.width / 2f } - setOnClickListener { hideManageEducation(animated = true) } + /** + * Create education view by inflating layout provided. + * + * @param layout layout resource id to inflate. The root view should be [BubblePopupView] + * @param root view group to use as root for inflation, is not attached to root + */ + private fun createEducationView(@LayoutRes layout: Int, root: ViewGroup): BubblePopupView { + val view = LayoutInflater.from(context).inflate(layout, root, false) as BubblePopupView + view.setup() + view.alpha = 0f + view.scaleX = EDU_SCALE_HIDDEN + view.scaleY = EDU_SCALE_HIDDEN + return view + } + + /** Create animator for the user education transitions */ + private fun createAnimator(): PhysicsAnimator<BubblePopupView>? { + return educationView?.let { + PhysicsAnimator.getInstance(it).apply { setDefaultSpringConfig(springConfig) } } } - private fun createAnimation(view: BubblePopupView): PhysicsAnimator<BubblePopupView> { - val animator = PhysicsAnimator.getInstance(view) - animator.setDefaultSpringConfig(springConfig) - return animator + /** + * Update user education view position relative to the reference position + * + * @param view the user education view to layout + * @param position the reference position in Screen coordinates + * @param root the root view to use for the layout + */ + private fun updateEducationPosition(view: BubblePopupView, position: Point, root: ViewGroup) { + val rootBounds = Rect() + // Get root bounds on screen as position is in screen coordinates + root.getBoundsOnScreen(rootBounds) + // Get the offset to the arrow from the edge of the education view + val arrowToEdgeOffset = + view.popupDrawable?.config?.let { it.cornerRadius + it.arrowWidth / 2f }?.roundToInt() + ?: 0 + // Calculate education view margins + val params = view.layoutParams as FrameLayout.LayoutParams + params.bottomMargin = rootBounds.bottom - position.y + params.rightMargin = rootBounds.right - position.x - arrowToEdgeOffset + view.layoutParams = params + } + + private fun log(msg: () -> String) { + if (DEBUG_USER_EDUCATION) Log.d(TAG, msg()) } companion object { + private val TAG = if (TAG_WITH_CLASS_NAME) "BubbleEducationViewController" else TAG_BUBBLES private const val EDU_SCALE_HIDDEN = 0.5f } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java index 81423473171d..fc627a8dcb36 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java @@ -35,6 +35,7 @@ public class BubbleBarUpdate implements Parcelable { public boolean expandedChanged; public boolean expanded; + public boolean shouldShowEducation; @Nullable public String selectedBubbleKey; @Nullable @@ -61,6 +62,7 @@ public class BubbleBarUpdate implements Parcelable { public BubbleBarUpdate(Parcel parcel) { expandedChanged = parcel.readBoolean(); expanded = parcel.readBoolean(); + shouldShowEducation = parcel.readBoolean(); selectedBubbleKey = parcel.readString(); addedBubble = parcel.readParcelable(BubbleInfo.class.getClassLoader(), BubbleInfo.class); @@ -95,6 +97,7 @@ public class BubbleBarUpdate implements Parcelable { return "BubbleBarUpdate{ expandedChanged=" + expandedChanged + " expanded=" + expanded + " selectedBubbleKey=" + selectedBubbleKey + + " shouldShowEducation=" + shouldShowEducation + " addedBubble=" + addedBubble + " updatedBubble=" + updatedBubble + " suppressedBubbleKey=" + suppressedBubbleKey @@ -114,6 +117,7 @@ public class BubbleBarUpdate implements Parcelable { public void writeToParcel(Parcel parcel, int flags) { parcel.writeBoolean(expandedChanged); parcel.writeBoolean(expanded); + parcel.writeBoolean(shouldShowEducation); parcel.writeString(selectedBubbleKey); parcel.writeParcelable(addedBubble, flags); parcel.writeParcelable(updatedBubble, flags); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupDrawable.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupDrawable.kt index 1fd22d0a3505..887af17c9653 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupDrawable.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupDrawable.kt @@ -31,7 +31,7 @@ import kotlin.math.sin import kotlin.properties.Delegates /** A drawable for the [BubblePopupView] that draws a popup background with a directional arrow */ -class BubblePopupDrawable(private val config: Config) : Drawable() { +class BubblePopupDrawable(val config: Config) : Drawable() { /** The direction of the arrow in the popup drawable */ enum class ArrowDirection { UP, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupView.kt index f8a4946bb5c5..444fbf7884be 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupView.kt @@ -29,7 +29,8 @@ constructor( defStyleAttr: Int = 0, defStyleRes: Int = 0 ) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) { - private var popupDrawable: BubblePopupDrawable? = null + var popupDrawable: BubblePopupDrawable? = null + private set /** * Sets up the popup drawable with the config provided. Required to remove dependency on local diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index e9f3e1a2647c..fd23d147b1b7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -35,6 +35,7 @@ import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleData; import com.android.wm.shell.bubbles.BubbleDataRepository; +import com.android.wm.shell.bubbles.BubbleEducationController; import com.android.wm.shell.bubbles.BubbleLogger; import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.properties.ProdBubbleProperties; @@ -134,11 +135,18 @@ public abstract class WMShellModule { @WMSingleton @Provides + static BubbleEducationController provideBubbleEducationProvider(Context context) { + return new BubbleEducationController(context); + } + + @WMSingleton + @Provides static BubbleData provideBubbleData(Context context, BubbleLogger logger, BubblePositioner positioner, + BubbleEducationController educationController, @ShellMainThread ShellExecutor mainExecutor) { - return new BubbleData(context, logger, positioner, mainExecutor); + return new BubbleData(context, logger, positioner, educationController, mainExecutor); } // Note: Handler needed for LauncherApps.register diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index 4a55429eacb6..26c73946c1c6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -111,6 +111,8 @@ public class BubbleDataTest extends ShellTestCase { @Mock private BubbleLogger mBubbleLogger; @Mock + private BubbleEducationController mEducationController; + @Mock private ShellExecutor mMainExecutor; @Captor @@ -191,7 +193,7 @@ public class BubbleDataTest extends ShellTestCase { mPositioner = new TestableBubblePositioner(mContext, mock(WindowManager.class)); - mBubbleData = new BubbleData(getContext(), mBubbleLogger, mPositioner, + mBubbleData = new BubbleData(getContext(), mBubbleLogger, mPositioner, mEducationController, mMainExecutor); // Used by BubbleData to set lastAccessedTime @@ -385,6 +387,65 @@ public class BubbleDataTest extends ShellTestCase { assertOverflowChangedTo(ImmutableList.of()); } + /** + * Verifies that the update shouldn't show the user education, if the education is not required + */ + @Test + public void test_shouldNotShowEducation() { + // Setup + when(mEducationController.shouldShowStackEducation(any())).thenReturn(false); + mBubbleData.setListener(mListener); + + // Test + mBubbleData.notificationEntryUpdated(mBubbleA1, /* suppressFlyout */ true, /* showInShade */ + true); + + // Verify + verifyUpdateReceived(); + BubbleData.Update update = mUpdateCaptor.getValue(); + assertThat(update.shouldShowEducation).isFalse(); + } + + /** + * Verifies that the update should show the user education, if the education is required + */ + @Test + public void test_shouldShowEducation() { + // Setup + when(mEducationController.shouldShowStackEducation(any())).thenReturn(true); + mBubbleData.setListener(mListener); + + // Test + mBubbleData.notificationEntryUpdated(mBubbleA1, /* suppressFlyout */ true, /* showInShade */ + true); + + // Verify + verifyUpdateReceived(); + BubbleData.Update update = mUpdateCaptor.getValue(); + assertThat(update.shouldShowEducation).isTrue(); + } + + /** + * Verifies that the update shouldn't show the user education, if the education is required but + * the bubble should auto-expand + */ + @Test + public void test_shouldShowEducation_shouldAutoExpand() { + // Setup + when(mEducationController.shouldShowStackEducation(any())).thenReturn(true); + mBubbleData.setListener(mListener); + mBubbleA1.setShouldAutoExpand(true); + + // Test + mBubbleData.notificationEntryUpdated(mBubbleA1, /* suppressFlyout */ true, /* showInShade */ + true); + + // Verify + verifyUpdateReceived(); + BubbleData.Update update = mUpdateCaptor.getValue(); + assertThat(update.shouldShowEducation).isFalse(); + } + // COLLAPSED / ADD /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 1b623a3de7bd..7595a54a18fd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -134,6 +134,7 @@ import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.bubbles.BubbleData; import com.android.wm.shell.bubbles.BubbleDataRepository; +import com.android.wm.shell.bubbles.BubbleEducationController; import com.android.wm.shell.bubbles.BubbleEntry; import com.android.wm.shell.bubbles.BubbleLogger; import com.android.wm.shell.bubbles.BubbleOverflow; @@ -277,6 +278,8 @@ public class BubblesTest extends SysuiTestCase { @Mock private BubbleLogger mBubbleLogger; @Mock + private BubbleEducationController mEducationController; + @Mock private TaskStackListenerImpl mTaskStackListener; @Mock private KeyguardStateController mKeyguardStateController; @@ -369,7 +372,8 @@ public class BubblesTest extends SysuiTestCase { mPositioner = new TestableBubblePositioner(mContext, mWindowManager); mPositioner.setMaxBubbles(5); - mBubbleData = new BubbleData(mContext, mBubbleLogger, mPositioner, syncExecutor); + mBubbleData = new BubbleData(mContext, mBubbleLogger, mPositioner, mEducationController, + syncExecutor); when(mUserManager.getProfiles(ActivityManager.getCurrentUser())).thenReturn( Collections.singletonList(mock(UserInfo.class))); |