From 5a3e94b7028e4f53bbcd3500156ab0d66600c8ca Mon Sep 17 00:00:00 2001 From: Mady Mellor Date: Fri, 7 Feb 2020 12:16:21 -0800 Subject: [PATCH] Bubbles user education for stack & manage Two types of education 1) highlighting the bubble when it first appears on screen (after user tap) 2) highlighting manage button when a bubble is expanded * Both of these are added in BubbleStackView & respond to theme changes * Prefs to save whether user has seen either education * Simple animations in / out; we can tune them later * Adjusts the starting position of the stack to work with user education positioning * Adds a secure setting string to force user education to show for demo purposes. Test: manual: 1) get bubble for first time 2) tap on flyout => flyout disappears & user education shows 3) get another update to that bubble => note that it doesn't show bc user education is displaying 1) get bubble for first time 2) tap on bubble => flyout disappears & user education shows 3) tap outside of the user education => user education hides 1) get bubble for first time 2) wait => after flyout disappears, user education shows 3) tap on the bubble => user education hides & bubble expands, after expansion "manage" user education appears 4a) interact with content in the activity view 4b) collapse the stack (via back or tapping outside) 4c) tap on the manage button 4d) select a different bubble => each of 4x) should dismiss the manage button user education Check that switching themes (and dark mode) gets the right colour for user education backgrounds & text. Bug: 148105621 Change-Id: I8e6401573c1ac21dfe636aa1132461fe04ce67ac Merged-In: I8e6401573c1ac21dfe636aa1132461fe04ce67ac --- .../bubble_manage_user_education_bg.xml | 21 ++ .../bubble_stack_user_education_bg.xml | 22 ++ .../layout/bubble_stack_user_education.xml | 48 +++ .../bubbles_manage_button_education.xml | 46 +++ packages/SystemUI/res/values/dimens.xml | 14 +- packages/SystemUI/res/values/strings.xml | 6 + .../src/com/android/systemui/Prefs.java | 6 +- .../systemui/bubbles/BubbleDebugConfig.java | 17 ++ .../systemui/bubbles/BubbleExpandedView.java | 12 +- .../bubbles/BubbleManageEducationView.java | 109 +++++++ .../systemui/bubbles/BubbleStackView.java | 285 +++++++++++++++--- .../systemui/bubbles/BubbleTouchHandler.java | 8 +- .../animation/StackAnimationController.java | 9 +- 13 files changed, 544 insertions(+), 59 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/drawable/bubble_manage_user_education_bg.xml create mode 100644 packages/SystemUI/res/drawable/bubble_stack_user_education_bg.xml create mode 100644 packages/SystemUI/res/layout/bubble_stack_user_education.xml create mode 100644 packages/SystemUI/res/layout/bubbles_manage_button_education.xml create mode 100644 packages/SystemUI/src/com/android/systemui/bubbles/BubbleManageEducationView.java diff --git a/packages/SystemUI/res-keyguard/drawable/bubble_manage_user_education_bg.xml b/packages/SystemUI/res-keyguard/drawable/bubble_manage_user_education_bg.xml new file mode 100644 index 0000000000000..64db25b65a0b1 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/bubble_manage_user_education_bg.xml @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/bubble_stack_user_education_bg.xml b/packages/SystemUI/res/drawable/bubble_stack_user_education_bg.xml new file mode 100644 index 0000000000000..4b9219cd6194b --- /dev/null +++ b/packages/SystemUI/res/drawable/bubble_stack_user_education_bg.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/layout/bubble_stack_user_education.xml b/packages/SystemUI/res/layout/bubble_stack_user_education.xml new file mode 100644 index 0000000000000..81b28e612993c --- /dev/null +++ b/packages/SystemUI/res/layout/bubble_stack_user_education.xml @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/packages/SystemUI/res/layout/bubbles_manage_button_education.xml b/packages/SystemUI/res/layout/bubbles_manage_button_education.xml new file mode 100644 index 0000000000000..0cabc3245152c --- /dev/null +++ b/packages/SystemUI/res/layout/bubbles_manage_button_education.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 12b925406864e..aefe4a20a4962 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1159,7 +1159,7 @@ 16dp - 48dp + 48dp 100dp @@ -1175,16 +1175,22 @@ 9dp - 96dp + 120dp 8dp - - 120dp 40dp 20dp + + 160dp + + 10dp + + 72dp + 10sp diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index ff28b4d289e86..496ab439bdc02 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2586,6 +2586,12 @@ Move bottom right Dismiss + + Keep chats up front + + New chats from %1$s will appear as bubbles. Tap a bubble to open it. Drag to move it.\n\nTap the bubble + + Tap Manage to turn off bubbles from this app System navigation updated. To make changes, go to Settings. diff --git a/packages/SystemUI/src/com/android/systemui/Prefs.java b/packages/SystemUI/src/com/android/systemui/Prefs.java index f0a82c519d48d..5e6589f76c132 100644 --- a/packages/SystemUI/src/com/android/systemui/Prefs.java +++ b/packages/SystemUI/src/com/android/systemui/Prefs.java @@ -57,7 +57,9 @@ public final class Prefs { Key.SEEN_RINGER_GUIDANCE_COUNT, Key.QS_HAS_TURNED_OFF_MOBILE_DATA, Key.TOUCHED_RINGER_TOGGLE, - Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP + Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP, + Key.HAS_SEEN_BUBBLES_EDUCATION, + Key.HAS_SEEN_BUBBLES_MANAGE_EDUCATION }) public @interface Key { @Deprecated @@ -103,6 +105,8 @@ public final class Prefs { String QS_HAS_TURNED_OFF_MOBILE_DATA = "QsHasTurnedOffMobileData"; String TOUCHED_RINGER_TOGGLE = "TouchedRingerToggle"; String HAS_SEEN_ODI_CAPTIONS_TOOLTIP = "HasSeenODICaptionsTooltip"; + String HAS_SEEN_BUBBLES_EDUCATION = "HasSeenBubblesOnboarding"; + String HAS_SEEN_BUBBLES_MANAGE_EDUCATION = "HasSeenBubblesManageOnboarding"; } public static boolean getBoolean(Context context, @Key String key, boolean defaultValue) { diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDebugConfig.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDebugConfig.java index 3190662216009..e800011981a96 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDebugConfig.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDebugConfig.java @@ -16,6 +16,9 @@ package com.android.systemui.bubbles; +import android.content.Context; +import android.provider.Settings; + import java.util.List; /** @@ -41,6 +44,20 @@ 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; + + private static final boolean FORCE_SHOW_USER_EDUCATION = false; + private static final String FORCE_SHOW_USER_EDUCATION_SETTING = + "force_show_bubbles_user_education"; + + /** + * @return whether we should force show user education for bubbles. Used for debugging & demos. + */ + static boolean forceShowUserEducation(Context context) { + boolean forceShow = Settings.Secure.getInt(context.getContentResolver(), + FORCE_SHOW_USER_EDUCATION_SETTING, 0) != 0; + return FORCE_SHOW_USER_EDUCATION || forceShow; + } static String formatBubblesString(List bubbles, Bubble selected) { StringBuilder sb = new StringBuilder(); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java index e3983c5b2d92d..a6f759f3e0b90 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java @@ -243,7 +243,7 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList mPointerView.setVisibility(INVISIBLE); mSettingsIconHeight = getContext().getResources().getDimensionPixelSize( - R.dimen.bubble_settings_size); + R.dimen.bubble_manage_button_height); mSettingsIcon = findViewById(R.id.settings_button); mSettingsIcon.setOnClickListener(this); @@ -530,6 +530,16 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList mPointerView.setVisibility(VISIBLE); } + /** + * Position of the manage button displayed in the expanded view. Used for placing user + * education about the manage button. + */ + public Rect getManageButtonLocationOnScreen() { + mTempLoc = mSettingsIcon.getLocationOnScreen(); + return new Rect(mTempLoc[0], mTempLoc[1], mTempLoc[0] + mSettingsIcon.getWidth(), + mTempLoc[1] + mSettingsIcon.getHeight()); + } + /** * Removes and releases an ActivityView if one was previously created for this bubble. */ diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleManageEducationView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleManageEducationView.java new file mode 100644 index 0000000000000..f4d64322c7ff6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleManageEducationView.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2020 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 android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.ShapeDrawable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.internal.util.ContrastColorUtil; +import com.android.systemui.R; +import com.android.systemui.recents.TriangleShape; + +/** + * Educational view to highlight the manage button that allows a user to configure the settings + * for the bubble. Shown only the first time a user expands a bubble. + */ +public class BubbleManageEducationView extends LinearLayout { + + private View mPointerView; + private View mManageView; + + public BubbleManageEducationView(Context context) { + this(context, null); + } + + public BubbleManageEducationView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public BubbleManageEducationView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public BubbleManageEducationView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mManageView = findViewById(R.id.manage_education_view); + + final TypedArray ta = mContext.obtainStyledAttributes( + new int[] {android.R.attr.colorAccent, + android.R.attr.textColorPrimaryInverse}); + final int bgColor = ta.getColor(0, Color.BLACK); + int textColor = ta.getColor(1, Color.WHITE); + ta.recycle(); + + textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true); + ((TextView) findViewById(R.id.user_education_description)).setTextColor(textColor); + + final Resources res = getResources(); + final int pointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width); + final int pointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); + + ShapeDrawable triangleShape = + new ShapeDrawable(TriangleShape.create( + pointerWidth, pointerHeight, false /* isPointingUp */)); + triangleShape.setTint(bgColor); + + mPointerView = findViewById(R.id.user_education_pointer); + mPointerView.setBackground(triangleShape); + } + + /** + * Specifies the x value this pointer should point to. + */ + public void setPointerPosition(int x) { + mPointerView.setTranslationX(x - (mPointerView.getWidth() / 2)); + } + + /** + * Specifies the position for the manage view. + */ + public void setManageViewPosition(int x, int y) { + mManageView.setTranslationX(x); + mManageView.setTranslationY(y); + } + + /** + * @return the height of the view that shows the educational text and pointer. + */ + public int getManageViewHeight() { + return mManageView.getHeight(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index df8e3941178ef..1ce04df02381b 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -19,9 +19,13 @@ package com.android.systemui.bubbles; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN; +import static com.android.systemui.Prefs.Key.HAS_SEEN_BUBBLES_EDUCATION; +import static com.android.systemui.Prefs.Key.HAS_SEEN_BUBBLES_MANAGE_EDUCATION; import static com.android.systemui.bubbles.BadgedImageView.DOT_STATE_DEFAULT; import static com.android.systemui.bubbles.BadgedImageView.DOT_STATE_SUPPRESSED_FOR_FLYOUT; import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW; +import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_USER_EDUCATION; import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; @@ -33,6 +37,8 @@ import android.app.Notification; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Paint; @@ -57,6 +63,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.FrameLayout; +import android.widget.TextView; import androidx.annotation.MainThread; import androidx.annotation.Nullable; @@ -66,7 +73,9 @@ import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ContrastColorUtil; import com.android.internal.widget.ViewClippingUtil; +import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.bubbles.animation.ExpandedAnimationController; import com.android.systemui.bubbles.animation.PhysicsAnimationLayout; @@ -88,6 +97,10 @@ import java.util.List; public class BubbleStackView extends FrameLayout { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES; + /** Animation durations for bubble stack user education views. **/ + private static final int ANIMATE_STACK_USER_EDUCATION_DURATION = 200; + private static final int ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT = 40; + /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */ static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f; @@ -171,6 +184,12 @@ public class BubbleStackView extends FrameLayout { * previous one animates out. */ private Runnable mAfterFlyoutHidden; + /** + * Set when the flyout is tapped, so that we can expand the bubble associated with the flyout + * once it collapses. + */ + @Nullable + private Bubble mBubbleToExpandAfterFlyoutCollapse = null; /** Layout change listener that moves the stack to the nearest valid position on rotation. */ private OnLayoutChangeListener mOrientationChangedListener; @@ -319,6 +338,14 @@ public class BubbleStackView extends FrameLayout { private BubbleOverflow mBubbleOverflow; + private boolean mShouldShowUserEducation; + private boolean mAnimatingEducationAway; + private View mUserEducationView; + + private boolean mShouldShowManageEducation; + private BubbleManageEducationView mManageEducationView; + private boolean mAnimatingManageEducationAway; + public BubbleStackView(Context context, BubbleData data, @Nullable SurfaceSynchronizer synchronizer, FloatingContentCoordinator floatingContentCoordinator) { @@ -361,6 +388,8 @@ public class BubbleStackView extends FrameLayout { mDisplaySize, mExpandedViewPadding, res.getConfiguration().orientation); mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER; + setUpUserEducation(); + mBubbleContainer = new PhysicsAnimationLayout(context); mBubbleContainer.setActiveController(mStackAnimationController); mBubbleContainer.setElevation(elevation); @@ -500,10 +529,50 @@ public class BubbleStackView extends FrameLayout { }); } - void showExpandedViewContents(int displayId) { - if (mExpandedBubble != null - && mExpandedBubble.getExpandedView().getVirtualDisplayId() == displayId) { - mExpandedBubble.setContentVisibility(true); + private void setUpUserEducation() { + if (mUserEducationView != null) { + removeView(mUserEducationView); + } + mShouldShowUserEducation = shouldShowBubblesEducation(); + if (DEBUG_USER_EDUCATION) { + Log.d(TAG, "shouldShowUserEducation: " + mShouldShowUserEducation); + } + if (mShouldShowUserEducation) { + mUserEducationView = mInflater.inflate(R.layout.bubble_stack_user_education, this, + false /* attachToRoot */); + mUserEducationView.setVisibility(GONE); + + final TypedArray ta = mContext.obtainStyledAttributes( + new int[] {android.R.attr.colorAccent, + android.R.attr.textColorPrimaryInverse}); + final int bgColor = ta.getColor(0, Color.BLACK); + int textColor = ta.getColor(1, Color.WHITE); + ta.recycle(); + textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true); + + TextView title = mUserEducationView.findViewById(R.id.user_education_title); + TextView description = mUserEducationView.findViewById(R.id.user_education_description); + title.setTextColor(textColor); + description.setTextColor(textColor); + + addView(mUserEducationView); + } + + if (mManageEducationView != null) { + removeView(mManageEducationView); + } + mShouldShowManageEducation = shouldShowManageEducation(); + if (DEBUG_USER_EDUCATION) { + Log.d(TAG, "shouldShowManageEducation: " + mShouldShowManageEducation); + } + if (mShouldShowManageEducation) { + mManageEducationView = (BubbleManageEducationView) + mInflater.inflate(R.layout.bubbles_manage_button_education, this, + false /* attachToRoot */); + mManageEducationView.setVisibility(GONE); + mManageEducationView.setElevation(mBubbleElevation); + + addView(mManageEducationView); } } @@ -539,6 +608,7 @@ public class BubbleStackView extends FrameLayout { public void onThemeChanged() { setUpFlyout(); setUpOverflow(); + setUpUserEducation(); } /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */ @@ -731,7 +801,7 @@ public class BubbleStackView extends FrameLayout { Bubble getExpandedBubble() { if (mExpandedBubble == null || (mExpandedBubble.getIconView() == mBubbleOverflow.getBtn() - && mExpandedBubble.getKey() == BubbleOverflow.KEY)) { + && BubbleOverflow.KEY.equals(mExpandedBubble.getKey()))) { return null; } return (Bubble) mExpandedBubble; @@ -743,6 +813,12 @@ public class BubbleStackView extends FrameLayout { Log.d(TAG, "addBubble: " + bubble); } + if (getBubbleCount() == 0 && mShouldShowUserEducation) { + // Override the default stack position if we're showing user education. + mStackAnimationController.setStackPosition( + mStackAnimationController.getDefaultStartPosition()); + } + if (getBubbleCount() == 0) { mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); } @@ -880,6 +956,109 @@ public class BubbleStackView extends FrameLayout { notifyExpansionChanged(mExpandedBubble, mIsExpanded); } + /** + * If necessary, shows the user education view for the bubble stack. This appears the first + * time a user taps on a bubble. + * + * @return true if user education was shown, false otherwise. + */ + private boolean maybeShowStackUserEducation() { + if (mShouldShowUserEducation && mUserEducationView.getVisibility() != VISIBLE) { + Bubble b = mBubbleData.getSelectedBubble(); + TextView description = mUserEducationView.findViewById(R.id.user_education_description); + description.setText(mContext.getString( + R.string.bubbles_user_education_description, b.getAppName())); + + mUserEducationView.setAlpha(0); + mUserEducationView.setVisibility(VISIBLE); + // Post so we have height of mUserEducationView + mUserEducationView.post(() -> { + final int viewHeight = mUserEducationView.getHeight(); + PointF stackPosition = mStackAnimationController.getDefaultStartPosition(); + final float translationY = stackPosition.y + (mBubbleSize / 2) - (viewHeight / 2); + mUserEducationView.setTranslationY(translationY); + mUserEducationView.animate() + .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION) + .setInterpolator(FAST_OUT_SLOW_IN) + .alpha(1); + }); + Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, true); + return true; + } + return false; + } + + /** + * If necessary, hides the user education view for the bubble stack. + * + * @param fromExpansion if true this indicates the hide is happening due to the bubble being + * expanded, false if due to a touch outside of the bubble stack. + */ + void hideStackUserEducation(boolean fromExpansion) { + if (mShouldShowUserEducation + && mUserEducationView.getVisibility() == VISIBLE + && !mAnimatingEducationAway) { + mAnimatingEducationAway = true; + mUserEducationView.animate() + .alpha(0) + .setDuration(fromExpansion + ? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT + : ANIMATE_STACK_USER_EDUCATION_DURATION) + .withEndAction(() -> { + mAnimatingEducationAway = false; + mShouldShowUserEducation = shouldShowBubblesEducation(); + mUserEducationView.setVisibility(GONE); + }); + } + } + + /** + * If necessary, toggles the user education view for the manage button. This is shown when the + * bubble stack is expanded for the first time. + * + * @param show whether the user education view should show or not. + */ + void maybeShowManageEducation(boolean show) { + if (mManageEducationView == null) { + return; + } + if (show + && mShouldShowManageEducation + && mManageEducationView.getVisibility() != VISIBLE + && mIsExpanded) { + mManageEducationView.setAlpha(0); + mManageEducationView.setVisibility(VISIBLE); + mManageEducationView.post(() -> { + final Rect position = + mExpandedBubble.getExpandedView().getManageButtonLocationOnScreen(); + final int viewHeight = mManageEducationView.getManageViewHeight(); + final int inset = getResources().getDimensionPixelSize( + R.dimen.bubbles_manage_education_top_inset); + mManageEducationView.bringToFront(); + mManageEducationView.setManageViewPosition(position.left, + position.top - viewHeight + inset); + mManageEducationView.setPointerPosition(position.centerX() - position.left); + mManageEducationView.animate() + .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION) + .setInterpolator(FAST_OUT_SLOW_IN).alpha(1); + }); + Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, true); + } else if (!show + && mManageEducationView.getVisibility() == VISIBLE + && !mAnimatingManageEducationAway) { + mManageEducationView.animate() + .alpha(0) + .setDuration(mIsExpansionAnimating + ? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT + : ANIMATE_STACK_USER_EDUCATION_DURATION) + .withEndAction(() -> { + mAnimatingManageEducationAway = false; + mShouldShowManageEducation = shouldShowManageEducation(); + mManageEducationView.setVisibility(GONE); + }); + } + } + /** * Dismiss the stack of bubbles. * @@ -923,7 +1102,17 @@ public class BubbleStackView extends FrameLayout { return null; } else if (mFlyout.getVisibility() == VISIBLE && isIntersecting(mFlyout, x, y)) { return mFlyout; + } else if (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE) { + View bubbleChild = mBubbleContainer.getChildAt(0); + if (isIntersecting(bubbleChild, x, y)) { + return this; + } else if (isIntersecting(mUserEducationView, x, y)) { + return mUserEducationView; + } else { + return null; + } } + // If it wasn't an individual bubble in the expanded state, or the flyout, it's the stack. return this; } @@ -932,22 +1121,6 @@ public class BubbleStackView extends FrameLayout { return mFlyout; } - /** - * Collapses the stack of bubbles. - *

- * Must be called from the main thread. - * - * @deprecated use {@link #setExpanded(boolean)} and {@link #setSelectedBubble(Bubble)} - */ - @Deprecated - @MainThread - void collapseStack() { - if (DEBUG_BUBBLE_STACK_VIEW) { - Log.d(TAG, "collapseStack()"); - } - mBubbleData.setExpanded(false); - } - /** * @deprecated use {@link #setExpanded(boolean)} and {@link #setSelectedBubble(Bubble)} */ @@ -957,25 +1130,16 @@ public class BubbleStackView extends FrameLayout { if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "collapseStack(endRunnable)"); } - collapseStack(); + mBubbleData.setExpanded(false); // TODO - use the runnable at end of animation endRunnable.run(); } - /** - * Expands the stack of bubbles. - *

- * Must be called from the main thread. - * - * @deprecated use {@link #setExpanded(boolean)} and {@link #setSelectedBubble(Bubble)} - */ - @Deprecated - @MainThread - void expandStack() { - if (DEBUG_BUBBLE_STACK_VIEW) { - Log.d(TAG, "expandStack()"); + void showExpandedViewContents(int displayId) { + if (mExpandedBubble != null + && mExpandedBubble.getExpandedView().getVirtualDisplayId() == displayId) { + mExpandedBubble.setContentVisibility(true); } - mBubbleData.setExpanded(true); } private void beforeExpandedViewAnimation() { @@ -995,11 +1159,12 @@ public class BubbleStackView extends FrameLayout { mIsExpanded = false; final BubbleViewProvider previouslySelected = mExpandedBubble; beforeExpandedViewAnimation(); + maybeShowManageEducation(false); if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "animateCollapse"); - Log.d(TAG, BubbleDebugConfig.formatBubblesString(this.getBubblesOnScreen(), - this.getExpandedBubble())); + Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(), + getExpandedBubble())); } updateOverflowBtnVisibility(/* apply */ false); mBubbleContainer.cancelAllAnimations(); @@ -1021,6 +1186,7 @@ public class BubbleStackView extends FrameLayout { private void animateExpansion() { mIsExpanded = true; + hideStackUserEducation(true /* fromExpansion */); beforeExpandedViewAnimation(); mBubbleContainer.setActiveController(mExpandedAnimationController); @@ -1028,6 +1194,7 @@ public class BubbleStackView extends FrameLayout { mExpandedAnimationController.expandFromStack(() -> { updatePointerPosition(); afterExpandedViewAnimation(); + maybeShowManageEducation(true); } /* after */); mExpandedViewContainer.setTranslationX(getCollapsedX()); @@ -1074,11 +1241,19 @@ public class BubbleStackView extends FrameLayout { } } + /** Called when the collapsed stack is tapped on. */ + void onStackTapped() { + if (!maybeShowStackUserEducation()) { + mBubbleData.setExpanded(true); + } + } + /** Called when a drag operation on an individual bubble has started. */ public void onBubbleDragStart(View bubble) { if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "onBubbleDragStart: bubble=" + bubble); } + maybeShowManageEducation(false); mExpandedAnimationController.prepareForBubbleDrag(bubble); } @@ -1129,6 +1304,7 @@ public class BubbleStackView extends FrameLayout { return; } + hideStackUserEducation(false /* fromExpansion */); springInDismissTarget(); mStackAnimationController.moveStackFromTouch(x, y); } @@ -1191,14 +1367,13 @@ public class BubbleStackView extends FrameLayout { mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation); } - /** - * Set when the flyout is tapped, so that we can expand the bubble associated with the flyout - * once it collapses. - */ - @Nullable private Bubble mBubbleToExpandAfterFlyoutCollapse = null; - void onFlyoutTapped() { - mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble(); + if (maybeShowStackUserEducation()) { + // If we're showing user education, don't open the bubble show the education first + mBubbleToExpandAfterFlyoutCollapse = null; + } else { + mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble(); + } mFlyout.removeCallbacks(mHideFlyout); mHideFlyout.run(); @@ -1221,6 +1396,8 @@ public class BubbleStackView extends FrameLayout { mFlyout.removeCallbacks(mHideFlyout); animateFlyoutCollapsed(shouldDismiss, velX); + + maybeShowStackUserEducation(); } /** @@ -1432,6 +1609,7 @@ public class BubbleStackView extends FrameLayout { if (flyoutMessage == null || flyoutMessage.message == null || !bubble.showFlyout() + || (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE) || isExpanded() || mIsExpansionAnimating || mIsGestureInProgress @@ -1517,7 +1695,12 @@ public class BubbleStackView extends FrameLayout { @Override public void getBoundsOnScreen(Rect outRect) { - // If the bubble menu is open, the entire screen should capture touch events. + if (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE) { + // When user education shows then capture all touches + outRect.set(0, 0, getWidth(), getHeight()); + return; + } + if (!mIsExpanded) { if (getBubbleCount() > 0) { mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect); @@ -1704,6 +1887,18 @@ public class BubbleStackView extends FrameLayout { return mExpandedBubble.getExpandedView().performBackPressIfNeeded(); } + /** Whether the educational view should appear for bubbles. **/ + private boolean shouldShowBubblesEducation() { + return BubbleDebugConfig.forceShowUserEducation(getContext()) + || !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, false); + } + + /** Whether the educational view should appear for the expanded view "manage" button. **/ + private boolean shouldShowManageEducation() { + return BubbleDebugConfig.forceShowUserEducation(getContext()) + || !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, false); + } + /** For debugging only */ List getBubblesOnScreen() { List bubbles = new ArrayList<>(); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java index 5e3e747ad2c08..46d1e0dfbab75 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java @@ -24,7 +24,6 @@ import android.view.View; import android.view.ViewConfiguration; import com.android.systemui.Dependency; -import com.android.systemui.R; /** * Handles interpreting touches on a {@link BubbleStackView}. This includes expanding, collapsing, @@ -92,6 +91,7 @@ class BubbleTouchHandler implements View.OnTouchListener { // anything, collapse the stack. if (action == MotionEvent.ACTION_OUTSIDE || mTouchedView == null) { mBubbleData.setExpanded(false); + mStack.hideStackUserEducation(false /* fromExpansion */); resetForNextGesture(); return false; } @@ -102,6 +102,7 @@ class BubbleTouchHandler implements View.OnTouchListener { // Not touching anything touchable, but we shouldn't collapse (e.g. touching edge // of expanded view). + mStack.maybeShowManageEducation(false); resetForNextGesture(); return false; } @@ -217,9 +218,8 @@ class BubbleTouchHandler implements View.OnTouchListener { } } else if (mTouchedView == mStack.getExpandedBubbleView()) { mBubbleData.setExpanded(false); - } else if (isStack || isFlyout) { - // Toggle expansion - mBubbleData.setExpanded(!mBubbleData.isExpanded()); + } else if (isStack) { + mStack.onStackTapped(); } else { final String key = ((BadgedImageView) mTouchedView).getKey(); if (key == BubbleOverflow.KEY) { diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java index 245d4afbf015a..f22c8faad95c1 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java @@ -879,9 +879,10 @@ public class StackAnimationController extends } /** Moves the stack to a position instantly, with no animation. */ - private void setStackPosition(PointF pos) { + public void setStackPosition(PointF pos) { Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y)); mStackPosition.set(pos.x, pos.y); + mRestingStackPosition = mStackPosition; // If we're not the active controller, we don't want to physically move the bubble views. if (isActiveController()) { @@ -902,10 +903,10 @@ public class StackAnimationController extends } } - /** Returns the default stack position, which is on the top right. */ - private PointF getDefaultStartPosition() { + /** Returns the default stack position, which is on the top left. */ + public PointF getDefaultStartPosition() { return new PointF( - getAllowableStackPositionRegion().right, + getAllowableStackPositionRegion().left, getAllowableStackPositionRegion().top + mStackStartingVerticalOffset); }