diff options
Diffstat (limited to 'packages/SystemUI/src')
232 files changed, 8744 insertions, 18173 deletions
diff --git a/packages/SystemUI/src/com/android/keyguard/GradientTextClock.java b/packages/SystemUI/src/com/android/keyguard/GradientTextClock.java new file mode 100644 index 000000000000..7cf1bd0b3e79 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/GradientTextClock.java @@ -0,0 +1,102 @@ +/* + * 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.keyguard; + +import android.content.Context; +import android.graphics.LinearGradient; +import android.graphics.Shader; +import android.util.AttributeSet; +import android.widget.TextClock; + +/** + * Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30) + * The time's text color is a gradient that changes its colors based on its controller. + */ +public class GradientTextClock extends TextClock { + private int[] mGradientColors; + private float[] mPositions; + + public GradientTextClock(Context context) { + this(context, null, 0, 0); + } + + public GradientTextClock(Context context, AttributeSet attrs) { + this(context, attrs, 0, 0); + } + + public GradientTextClock(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public GradientTextClock(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + addOnLayoutChangeListener(mOnLayoutChangeListener); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + removeOnLayoutChangeListener(mOnLayoutChangeListener); + } + + @Override + public void refreshTime() { + super.refreshTime(); + } + + @Override + public void setFormat12Hour(CharSequence format) { + super.setFormat12Hour(FORMAT_12); + } + + @Override + public void setFormat24Hour(CharSequence format) { + super.setFormat24Hour(FORMAT_24); + } + + public void setGradientColors(int[] colors) { + mGradientColors = colors; + updatePaint(); + } + + public void setColorPositions(float[] positions) { + mPositions = positions; + } + + private void updatePaint() { + getPaint().setShader( + new LinearGradient( + getX(), getY(), getX(), getMeasuredHeight() + getY(), + mGradientColors, mPositions, Shader.TileMode.REPEAT)); + } + + private final OnLayoutChangeListener mOnLayoutChangeListener = + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + if (bottom != oldBottom || top != oldTop) { + updatePaint(); + } + }; + + public static final CharSequence FORMAT_12 = "hh\nmm"; + public static final CharSequence FORMAT_24 = "HH\nmm"; +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java index 4ffd22c73116..5e452666bece 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java @@ -81,8 +81,7 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey abstract void resetState(); @Override - public void init() { - super.init(); + public void onInit() { mMessageAreaController.init(); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 272954df6dd6..c6ee15fcf4e3 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -15,6 +15,7 @@ import android.transition.TransitionValues; import android.util.AttributeSet; import android.util.Log; import android.util.MathUtils; +import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -72,6 +73,16 @@ public class KeyguardClockSwitch extends RelativeLayout { private TextClock mClockViewBold; /** + * Gradient clock for usage when mode != KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL. + */ + private TimeBasedColorsClockController mNewLockscreenClockViewController; + + /** + * Frame for clock when mode != KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL. + */ + private FrameLayout mNewLockscreenClockFrame; + + /** * Frame for default and custom clock. */ private FrameLayout mSmallClockFrame; @@ -137,23 +148,28 @@ public class KeyguardClockSwitch extends RelativeLayout { mLockScreenMode = mode; RelativeLayout.LayoutParams statusAreaLP = (RelativeLayout.LayoutParams) mKeyguardStatusArea.getLayoutParams(); - RelativeLayout.LayoutParams clockLP = (RelativeLayout.LayoutParams) - mSmallClockFrame.getLayoutParams(); if (mode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) { + final int startEndPadding = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 12, + getResources().getDisplayMetrics()); + setPaddingRelative(startEndPadding, 0, startEndPadding, 0); + mSmallClockFrame.setVisibility(GONE); + mNewLockscreenClockFrame.setVisibility(VISIBLE); + mNewLockscreenClockViewController.init(); + statusAreaLP.removeRule(RelativeLayout.BELOW); - statusAreaLP.addRule(RelativeLayout.LEFT_OF, R.id.clock_view); + statusAreaLP.addRule(RelativeLayout.LEFT_OF, R.id.new_lockscreen_clock_view); statusAreaLP.addRule(RelativeLayout.ALIGN_PARENT_START); - - clockLP.addRule(RelativeLayout.ALIGN_PARENT_END); - clockLP.width = ViewGroup.LayoutParams.WRAP_CONTENT; } else { + setPaddingRelative(0, 0, 0, 0); + mSmallClockFrame.setVisibility(VISIBLE); + mNewLockscreenClockFrame.setVisibility(GONE); + statusAreaLP.removeRule(RelativeLayout.LEFT_OF); statusAreaLP.removeRule(RelativeLayout.ALIGN_PARENT_START); statusAreaLP.addRule(RelativeLayout.BELOW, R.id.clock_view); - - clockLP.removeRule(RelativeLayout.ALIGN_PARENT_END); - clockLP.width = ViewGroup.LayoutParams.MATCH_PARENT; } requestLayout(); @@ -164,6 +180,9 @@ public class KeyguardClockSwitch extends RelativeLayout { super.onFinishInflate(); mClockView = findViewById(R.id.default_clock_view); mClockViewBold = findViewById(R.id.default_clock_view_bold); + mNewLockscreenClockFrame = findViewById(R.id.new_lockscreen_clock_view); + mNewLockscreenClockViewController = + new TimeBasedColorsClockController(findViewById(R.id.gradient_clock_view)); mSmallClockFrame = findViewById(R.id.clock_view); mKeyguardStatusArea = findViewById(R.id.keyguard_status_area); } @@ -286,6 +305,7 @@ public class KeyguardClockSwitch extends RelativeLayout { if (mClockPlugin != null) { mClockPlugin.setDarkAmount(darkAmount); } + mNewLockscreenClockViewController.setDarkAmount(darkAmount); updateBigClockAlpha(); } @@ -336,6 +356,7 @@ public class KeyguardClockSwitch extends RelativeLayout { * Refresh the time of the clock, due to either time tick broadcast or doze time tick alarm. */ public void refresh() { + mNewLockscreenClockViewController.refreshTime(System.currentTimeMillis()); mClockView.refreshTime(); mClockViewBold.refreshTime(); if (mClockPlugin != null) { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index fe5fcc6fd632..5b89f7f46772 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -17,28 +17,47 @@ package com.android.keyguard; import android.app.WallpaperManager; +import android.content.res.Resources; +import android.text.format.DateFormat; +import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; +import android.widget.FrameLayout; import com.android.internal.colorextraction.ColorExtractor; import com.android.keyguard.clock.ClockManager; +import com.android.systemui.R; import com.android.systemui.colorextraction.SysuiColorExtractor; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.ClockPlugin; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.notification.AnimatableProperty; +import com.android.systemui.statusbar.notification.PropertyAnimator; +import com.android.systemui.statusbar.notification.stack.AnimationProperties; +import com.android.systemui.statusbar.phone.NotificationIconAreaController; +import com.android.systemui.statusbar.phone.NotificationIconContainer; +import com.android.systemui.util.ViewController; + +import java.util.Locale; +import java.util.TimeZone; import javax.inject.Inject; /** * Injectable controller for {@link KeyguardClockSwitch}. */ -public class KeyguardClockSwitchController { +public class KeyguardClockSwitchController extends ViewController<KeyguardClockSwitch> { private static final boolean CUSTOM_CLOCKS_ENABLED = true; - private final KeyguardClockSwitch mView; + private final Resources mResources; private final StatusBarStateController mStatusBarStateController; private final SysuiColorExtractor mColorExtractor; private final ClockManager mClockManager; private final KeyguardSliceViewController mKeyguardSliceViewController; + private final NotificationIconAreaController mNotificationIconAreaController; + private FrameLayout mNewLockscreenClockFrame; + + private int mLockScreenMode = KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL; private final StatusBarStateController.StateListener mStateListener = new StatusBarStateController.StateListener() { @@ -65,51 +84,60 @@ public class KeyguardClockSwitchController { private ClockManager.ClockChangedListener mClockChangedListener = this::setClockPlugin; - private final View.OnAttachStateChangeListener mOnAttachStateChangeListener = - new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - if (CUSTOM_CLOCKS_ENABLED) { - mClockManager.addOnClockChangedListener(mClockChangedListener); - } - mStatusBarStateController.addCallback(mStateListener); - mColorExtractor.addOnColorsChangedListener(mColorsListener); - mView.updateColors(getGradientColors()); - } - - @Override - public void onViewDetachedFromWindow(View v) { - if (CUSTOM_CLOCKS_ENABLED) { - mClockManager.removeOnClockChangedListener(mClockChangedListener); - } - mStatusBarStateController.removeCallback(mStateListener); - mColorExtractor.removeOnColorsChangedListener(mColorsListener); - mView.setClockPlugin(null, mStatusBarStateController.getState()); - } - }; - @Inject - public KeyguardClockSwitchController(KeyguardClockSwitch keyguardClockSwitch, + public KeyguardClockSwitchController( + KeyguardClockSwitch keyguardClockSwitch, + @Main Resources resources, StatusBarStateController statusBarStateController, SysuiColorExtractor colorExtractor, ClockManager clockManager, - KeyguardSliceViewController keyguardSliceViewController) { - mView = keyguardClockSwitch; + KeyguardSliceViewController keyguardSliceViewController, + NotificationIconAreaController notificationIconAreaController) { + super(keyguardClockSwitch); + mResources = resources; mStatusBarStateController = statusBarStateController; mColorExtractor = colorExtractor; mClockManager = clockManager; mKeyguardSliceViewController = keyguardSliceViewController; + mNotificationIconAreaController = notificationIconAreaController; } /** * Attach the controller to the view it relates to. */ - public void init() { - if (mView.isAttachedToWindow()) { - mOnAttachStateChangeListener.onViewAttachedToWindow(mView); + @Override + public void onInit() { + mKeyguardSliceViewController.init(); + } + + @Override + protected void onViewAttached() { + if (CUSTOM_CLOCKS_ENABLED) { + mClockManager.addOnClockChangedListener(mClockChangedListener); } - mView.addOnAttachStateChangeListener(mOnAttachStateChangeListener); + refreshFormat(); + mStatusBarStateController.addCallback(mStateListener); + mColorExtractor.addOnColorsChangedListener(mColorsListener); + mView.updateColors(getGradientColors()); + updateAodIcons(); + mNewLockscreenClockFrame = mView.findViewById(R.id.new_lockscreen_clock_view); + } - mKeyguardSliceViewController.init(); + @Override + protected void onViewDetached() { + if (CUSTOM_CLOCKS_ENABLED) { + mClockManager.removeOnClockChangedListener(mClockChangedListener); + } + mStatusBarStateController.removeCallback(mStateListener); + mColorExtractor.removeOnColorsChangedListener(mColorsListener); + mView.setClockPlugin(null, mStatusBarStateController.getState()); + } + + /** + * Updates clock's text + */ + public void onDensityOrFontScaleChanged() { + mView.setTextSize(TypedValue.COMPLEX_UNIT_PX, + mResources.getDimensionPixelSize(R.dimen.widget_big_font_size)); } /** @@ -119,6 +147,91 @@ public class KeyguardClockSwitchController { mView.setBigClockContainer(bigClockContainer, mStatusBarStateController.getState()); } + /** + * Set whether or not the lock screen is showing notifications. + */ + public void setHasVisibleNotifications(boolean hasVisibleNotifications) { + mView.setHasVisibleNotifications(hasVisibleNotifications); + } + + /** + * If we're presenting a custom clock of just the default one. + */ + public boolean hasCustomClock() { + return mView.hasCustomClock(); + } + + /** + * Get the clock text size. + */ + public float getClockTextSize() { + return mView.getTextSize(); + } + + /** + * Returns the preferred Y position of the clock. + * + * @param totalHeight The height available to position the clock. + * @return Y position of clock. + */ + public int getClockPreferredY(int totalHeight) { + return mView.getPreferredY(totalHeight); + } + + /** + * Refresh clock. Called in response to TIME_TICK broadcasts. + */ + void refresh() { + mView.refresh(); + } + + /** + * Update position of the view, with optional animation. Move the slice view and the clock + * slightly towards the center in order to prevent burn-in. Y positioning occurs at the + * view parent level. + */ + void updatePosition(int x, AnimationProperties props, boolean animate) { + x = Math.abs(x); + if (mNewLockscreenClockFrame != null) { + PropertyAnimator.setProperty(mNewLockscreenClockFrame, AnimatableProperty.TRANSLATION_X, + -x, props, animate); + } + mKeyguardSliceViewController.updatePosition(x, props, animate); + mNotificationIconAreaController.updatePosition(x, props, animate); + } + + /** + * Update lockscreen mode that may change clock display. + */ + void updateLockScreenMode(int mode) { + mLockScreenMode = mode; + mView.updateLockScreenMode(mLockScreenMode); + updateAodIcons(); + } + + void updateTimeZone(TimeZone timeZone) { + mView.onTimeZoneChanged(timeZone); + } + + void refreshFormat() { + Patterns.update(mResources); + mView.setFormat12Hour(Patterns.sClockView12); + mView.setFormat24Hour(Patterns.sClockView24); + } + + private void updateAodIcons() { + NotificationIconContainer nic = (NotificationIconContainer) + mView.findViewById( + com.android.systemui.R.id.left_aligned_notification_icon_container); + + if (mLockScreenMode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) { + // alt icon area is set in KeyguardClockSwitchController + mNotificationIconAreaController.setupAodIcons(nic, mLockScreenMode); + } else { + nic.setVisibility(View.GONE); + } + } + private void setClockPlugin(ClockPlugin plugin) { mView.setClockPlugin(plugin, mStatusBarStateController.getState()); } @@ -126,4 +239,35 @@ public class KeyguardClockSwitchController { private ColorExtractor.GradientColors getGradientColors() { return mColorExtractor.getColors(WallpaperManager.FLAG_LOCK); } + + // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often. + // This is an optimization to ensure we only recompute the patterns when the inputs change. + private static final class Patterns { + static String sClockView12; + static String sClockView24; + static String sCacheKey; + + static void update(Resources res) { + final Locale locale = Locale.getDefault(); + final String clockView12Skel = res.getString(R.string.clock_12hr_format); + final String clockView24Skel = res.getString(R.string.clock_24hr_format); + final String key = locale.toString() + clockView12Skel + clockView24Skel; + if (key.equals(sCacheKey)) return; + + sClockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel); + // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton + // format. The following code removes the AM/PM indicator if we didn't want it. + if (!clockView12Skel.contains("a")) { + sClockView12 = sClockView12.replaceAll("a", "").trim(); + } + + sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel); + + // Use fancy colon. + sClockView24 = sClockView24.replace(':', '\uee01'); + sClockView12 = sClockView12.replace(':', '\uee01'); + + sCacheKey = key; + } + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java index 36d5543f1c01..901a7360f311 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java @@ -20,7 +20,7 @@ import static android.view.Display.DEFAULT_DISPLAY; import android.app.Presentation; import android.content.Context; import android.graphics.Color; -import android.graphics.Point; +import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.media.MediaRouter; import android.media.MediaRouter.RouteInfo; @@ -127,7 +127,7 @@ public class KeyguardDisplayManager { Presentation presentation = mPresentations.get(displayId); if (presentation == null) { final Presentation newPresentation = new KeyguardPresentation(mContext, display, - mKeyguardStatusViewComponentFactory, LayoutInflater.from(mContext)); + mKeyguardStatusViewComponentFactory); newPresentation.setOnDismissListener(dialog -> { if (newPresentation.equals(mPresentations.get(displayId))) { mPresentations.remove(displayId); @@ -245,7 +245,6 @@ public class KeyguardDisplayManager { private static final int VIDEO_SAFE_REGION = 80; // Percentage of display width & height private static final int MOVE_CLOCK_TIMEOUT = 10000; // 10s private final KeyguardStatusViewComponent.Factory mKeyguardStatusViewComponentFactory; - private final LayoutInflater mLayoutInflater; private KeyguardClockSwitchController mKeyguardClockSwitchController; private View mClock; private int mUsableWidth; @@ -264,18 +263,16 @@ public class KeyguardDisplayManager { }; KeyguardPresentation(Context context, Display display, - KeyguardStatusViewComponent.Factory keyguardStatusViewComponentFactory, - LayoutInflater layoutInflater) { - super(context, display, R.style.Theme_SystemUI_KeyguardPresentation); + KeyguardStatusViewComponent.Factory keyguardStatusViewComponentFactory) { + super(context, display, R.style.Theme_SystemUI_KeyguardPresentation, + WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); mKeyguardStatusViewComponentFactory = keyguardStatusViewComponentFactory; - mLayoutInflater = layoutInflater; - getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); setCancelable(false); } @Override public void cancel() { - // Do not allow anything to cancel KeyguardPresetation except KeyguardDisplayManager. + // Do not allow anything to cancel KeyguardPresentation except KeyguardDisplayManager. } @Override @@ -287,14 +284,15 @@ public class KeyguardDisplayManager { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Point p = new Point(); - getDisplay().getSize(p); - mUsableWidth = VIDEO_SAFE_REGION * p.x/100; - mUsableHeight = VIDEO_SAFE_REGION * p.y/100; - mMarginLeft = (100 - VIDEO_SAFE_REGION) * p.x / 200; - mMarginTop = (100 - VIDEO_SAFE_REGION) * p.y / 200; + final Rect bounds = getWindow().getWindowManager().getMaximumWindowMetrics() + .getBounds(); + mUsableWidth = VIDEO_SAFE_REGION * bounds.width() / 100; + mUsableHeight = VIDEO_SAFE_REGION * bounds.height() / 100; + mMarginLeft = (100 - VIDEO_SAFE_REGION) * bounds.width() / 200; + mMarginTop = (100 - VIDEO_SAFE_REGION) * bounds.height() / 200; - setContentView(mLayoutInflater.inflate(R.layout.keyguard_presentation, null)); + setContentView(LayoutInflater.from(getContext()) + .inflate(R.layout.keyguard_presentation, null)); // Logic to make the lock screen fullscreen getWindow().getDecorView().setSystemUiVisibility( diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java index 351369c51364..3fafa5c606bd 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java @@ -178,8 +178,7 @@ public class KeyguardHostViewController extends ViewController<KeyguardHostView> } /** Initialize the Controller. */ - public void init() { - super.init(); + public void onInit() { mKeyguardSecurityContainerController.init(); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java index fbc0a3163817..db3d616fbcda 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java @@ -191,8 +191,8 @@ public class KeyguardPatternViewController } @Override - public void init() { - super.init(); + public void onInit() { + super.onInit(); mMessageAreaController.init(); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardRootViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardRootViewController.java index 5c125fcc95cb..4e375c2d1227 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardRootViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardRootViewController.java @@ -19,7 +19,7 @@ package com.android.keyguard; import android.view.ViewGroup; import com.android.keyguard.dagger.KeyguardBouncerScope; -import com.android.keyguard.dagger.RootView; +import com.android.systemui.dagger.qualifiers.RootView; import com.android.systemui.statusbar.phone.KeyguardBouncer; import com.android.systemui.util.ViewController; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 1c23605a8516..9a511502b475 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -169,8 +169,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } @Override - public void init() { - super.init(); + public void onInit() { mSecurityViewFlipperController.init(); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java index a479bca56c2a..a9c06edf46cc 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java @@ -33,9 +33,11 @@ import android.text.TextUtils; import android.text.TextUtils.TruncateAt; import android.util.AttributeSet; import android.util.TypedValue; +import android.view.Gravity; import android.view.View; import android.view.animation.Animation; import android.widget.LinearLayout; +import android.widget.RelativeLayout; import android.widget.TextView; import androidx.slice.SliceItem; @@ -55,8 +57,10 @@ import com.android.systemui.util.wakelock.KeepAwakeAnimationListener; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * View visible under the clock on the lock screen and AoD. @@ -86,6 +90,8 @@ public class KeyguardSliceView extends LinearLayout { private float mRowWithHeaderTextSize; private View.OnClickListener mOnClickListener; + private int mLockScreenMode = KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL; + public KeyguardSliceView(Context context, AttributeSet attrs) { super(context, attrs); @@ -142,6 +148,40 @@ public class KeyguardSliceView extends LinearLayout { } } + /** + * Updates the lockscreen mode which may change the layout of the keyguard slice view. + */ + public void updateLockScreenMode(int mode) { + mLockScreenMode = mode; + if (mLockScreenMode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) { + // add top padding to better align with top of clock + final int topPadding = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 20, + getResources().getDisplayMetrics()); + mTitle.setPaddingRelative(0, topPadding, 0, 0); + mTitle.setGravity(Gravity.START); + setGravity(Gravity.START); + RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) getLayoutParams(); + lp.removeRule(RelativeLayout.CENTER_HORIZONTAL); + setLayoutParams(lp); + } else { + final int horizontalPaddingDpValue = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 44, + getResources().getDisplayMetrics() + ); + mTitle.setPaddingRelative(horizontalPaddingDpValue, 0, horizontalPaddingDpValue, 0); + mTitle.setGravity(Gravity.CENTER_HORIZONTAL); + setGravity(Gravity.CENTER_HORIZONTAL); + RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) getLayoutParams(); + lp.addRule(RelativeLayout.CENTER_HORIZONTAL); + setLayoutParams(lp); + } + mRow.setLockscreenMode(mode); + requestLayout(); + } + Map<View, PendingIntent> showSlice(RowContent header, List<SliceContent> subItems) { Trace.beginSection("KeyguardSliceView#showSlice"); mHasHeader = header != null; @@ -166,6 +206,8 @@ public class KeyguardSliceView extends LinearLayout { final int startIndex = mHasHeader ? 1 : 0; // First item is header; skip it mRow.setVisibility(subItemsCount > 0 ? VISIBLE : GONE); LinearLayout.LayoutParams layoutParams = (LayoutParams) mRow.getLayoutParams(); + layoutParams.gravity = mLockScreenMode != KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL + ? Gravity.START : Gravity.CENTER; layoutParams.topMargin = mHasHeader ? mRowWithHeaderPadding : mRowPadding; mRow.setLayoutParams(layoutParams); @@ -282,6 +324,7 @@ public class KeyguardSliceView extends LinearLayout { pw.println(" mTextColor: " + Integer.toHexString(mTextColor)); pw.println(" mDarkAmount: " + mDarkAmount); pw.println(" mHasHeader: " + mHasHeader); + pw.println(" mLockScreenMode: " + mLockScreenMode); } @Override @@ -291,6 +334,8 @@ public class KeyguardSliceView extends LinearLayout { } public static class Row extends LinearLayout { + private Set<KeyguardSliceTextView> mKeyguardSliceTextViewSet = new HashSet(); + private int mLockScreenModeRow = KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL; /** * This view is visible in AOD, which means that the device will sleep if we @@ -361,12 +406,18 @@ public class KeyguardSliceView extends LinearLayout { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (child instanceof KeyguardSliceTextView) { - ((KeyguardSliceTextView) child).setMaxWidth(width / 3); + if (mLockScreenModeRow == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) { + ((KeyguardSliceTextView) child).setMaxWidth(Integer.MAX_VALUE); + } else { + ((KeyguardSliceTextView) child).setMaxWidth(width / 3); + } } } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @@ -384,6 +435,42 @@ public class KeyguardSliceView extends LinearLayout { public boolean hasOverlappingRendering() { return false; } + + @Override + public void addView(View view, int index) { + super.addView(view, index); + + if (view instanceof KeyguardSliceTextView) { + ((KeyguardSliceTextView) view).setLockScreenMode(mLockScreenModeRow); + mKeyguardSliceTextViewSet.add((KeyguardSliceTextView) view); + } + } + + @Override + public void removeView(View view) { + super.removeView(view); + if (view instanceof KeyguardSliceTextView) { + mKeyguardSliceTextViewSet.remove((KeyguardSliceTextView) view); + } + } + + /** + * Updates the lockscreen mode which may change the layout of this view. + */ + public void setLockscreenMode(int mode) { + mLockScreenModeRow = mode; + if (mLockScreenModeRow == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) { + setOrientation(LinearLayout.VERTICAL); + setGravity(Gravity.START); + } else { + setOrientation(LinearLayout.HORIZONTAL); + setGravity(Gravity.CENTER); + } + + for (KeyguardSliceTextView textView : mKeyguardSliceTextViewSet) { + textView.setLockScreenMode(mLockScreenModeRow); + } + } } /** @@ -392,6 +479,7 @@ public class KeyguardSliceView extends LinearLayout { @VisibleForTesting static class KeyguardSliceTextView extends TextView implements ConfigurationController.ConfigurationListener { + private int mLockScreenMode = KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL; @StyleRes private static int sStyleId = R.style.TextAppearance_Keyguard_Secondary; @@ -432,9 +520,16 @@ public class KeyguardSliceView extends LinearLayout { private void updatePadding() { boolean hasText = !TextUtils.isEmpty(getText()); - int horizontalPadding = (int) getContext().getResources() + int padding = (int) getContext().getResources() .getDimension(R.dimen.widget_horizontal_padding) / 2; - setPadding(horizontalPadding, 0, horizontalPadding * (hasText ? 1 : -1), 0); + if (mLockScreenMode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) { + // orientation is vertical, so add padding to top & bottom + setPadding(0, padding, 0, padding * (hasText ? 1 : -1)); + } else { + // oreintation is horizontal, so add padding to left & right + setPadding(padding, 0, padding * (hasText ? 1 : -1), 0); + } + setCompoundDrawablePadding((int) mContext.getResources() .getDimension(R.dimen.widget_icon_padding)); } @@ -461,5 +556,18 @@ public class KeyguardSliceView extends LinearLayout { } } } + + /** + * Updates the lockscreen mode which may change the layout of this view. + */ + public void setLockScreenMode(int mode) { + mLockScreenMode = mode; + if (mLockScreenMode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) { + setGravity(Gravity.START); + } else { + setGravity(Gravity.CENTER); + } + updatePadding(); + } } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java index 2470b958a85f..02b18b28a5ea 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java @@ -42,8 +42,12 @@ import com.android.systemui.Dumpable; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardSliceProvider; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.statusbar.notification.AnimatableProperty; +import com.android.systemui.statusbar.notification.PropertyAnimator; +import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.tuner.TunerService; +import com.android.systemui.util.ViewController; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -55,11 +59,10 @@ import javax.inject.Inject; /** Controller for a {@link KeyguardSliceView}. */ @KeyguardStatusViewScope -public class KeyguardSliceViewController implements Dumpable { +public class KeyguardSliceViewController extends ViewController<KeyguardSliceView> implements + Dumpable { private static final String TAG = "KeyguardSliceViewCtrl"; - private final KeyguardSliceView mView; - private final KeyguardStatusView mKeyguardStatusView; private final ActivityStarter mActivityStarter; private final ConfigurationController mConfigurationController; private final TunerService mTunerService; @@ -69,41 +72,7 @@ public class KeyguardSliceViewController implements Dumpable { private Uri mKeyguardSliceUri; private Slice mSlice; private Map<View, PendingIntent> mClickActions; - - private final View.OnAttachStateChangeListener mOnAttachStateChangeListener = - new View.OnAttachStateChangeListener() { - - @Override - public void onViewAttachedToWindow(View v) { - - Display display = mView.getDisplay(); - if (display != null) { - mDisplayId = display.getDisplayId(); - } - mTunerService.addTunable(mTunable, Settings.Secure.KEYGUARD_SLICE_URI); - // Make sure we always have the most current slice - if (mDisplayId == DEFAULT_DISPLAY && mLiveData != null) { - mLiveData.observeForever(mObserver); - } - mConfigurationController.addCallback(mConfigurationListener); - mDumpManager.registerDumpable( - TAG + "@" + Integer.toHexString( - KeyguardSliceViewController.this.hashCode()), - KeyguardSliceViewController.this); - } - - @Override - public void onViewDetachedFromWindow(View v) { - - // TODO(b/117344873) Remove below work around after this issue be fixed. - if (mDisplayId == DEFAULT_DISPLAY) { - mLiveData.removeObserver(mObserver); - } - mTunerService.removeTunable(mTunable); - mConfigurationController.removeCallback(mConfigurationListener); - mDumpManager.unregisterDumpable(TAG); - } - }; + private int mLockScreenMode = KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL; TunerService.Tunable mTunable = (key, newValue) -> setupUri(newValue); @@ -134,27 +103,57 @@ public class KeyguardSliceViewController implements Dumpable { }; @Inject - public KeyguardSliceViewController(KeyguardSliceView keyguardSliceView, - KeyguardStatusView keyguardStatusView, ActivityStarter activityStarter, - ConfigurationController configurationController, TunerService tunerService, + public KeyguardSliceViewController( + KeyguardSliceView keyguardSliceView, + ActivityStarter activityStarter, + ConfigurationController configurationController, + TunerService tunerService, DumpManager dumpManager) { - mView = keyguardSliceView; - mKeyguardStatusView = keyguardStatusView; + super(keyguardSliceView); mActivityStarter = activityStarter; mConfigurationController = configurationController; mTunerService = tunerService; mDumpManager = dumpManager; } - /** Initialize the controller. */ - public void init() { - if (mView.isAttachedToWindow()) { - mOnAttachStateChangeListener.onViewAttachedToWindow(mView); + @Override + protected void onViewAttached() { + Display display = mView.getDisplay(); + if (display != null) { + mDisplayId = display.getDisplayId(); + } + mTunerService.addTunable(mTunable, Settings.Secure.KEYGUARD_SLICE_URI); + // Make sure we always have the most current slice + if (mDisplayId == DEFAULT_DISPLAY && mLiveData != null) { + mLiveData.observeForever(mObserver); + } + mConfigurationController.addCallback(mConfigurationListener); + mDumpManager.registerDumpable( + TAG + "@" + Integer.toHexString( + KeyguardSliceViewController.this.hashCode()), + KeyguardSliceViewController.this); + mView.updateLockScreenMode(mLockScreenMode); + } + + @Override + protected void onViewDetached() { + // TODO(b/117344873) Remove below work around after this issue be fixed. + if (mDisplayId == DEFAULT_DISPLAY) { + mLiveData.removeObserver(mObserver); } - mView.addOnAttachStateChangeListener(mOnAttachStateChangeListener); - mView.setOnClickListener(mOnClickListener); - // TODO: remove the line below. - mKeyguardStatusView.setKeyguardSliceViewController(this); + mTunerService.removeTunable(mTunable); + mConfigurationController.removeCallback(mConfigurationListener); + mDumpManager.unregisterDumpable( + TAG + "@" + Integer.toHexString( + KeyguardSliceViewController.this.hashCode())); + } + + /** + * Updates the lockscreen mode which may change the layout of the keyguard slice view. + */ + public void updateLockScreenMode(int mode) { + mLockScreenMode = mode; + mView.updateLockScreenMode(mLockScreenMode); } /** @@ -203,6 +202,13 @@ public class KeyguardSliceViewController implements Dumpable { Trace.endSection(); } + /** + * Update position of the view, with optional animation + */ + void updatePosition(int x, AnimationProperties props, boolean animate) { + PropertyAnimator.setProperty(mView, AnimatableProperty.TRANSLATION_X, x, props, animate); + } + void showSlice(Slice slice) { Trace.beginSection("KeyguardSliceViewController#showSlice"); if (slice == null) { @@ -228,12 +234,10 @@ public class KeyguardSliceViewController implements Dumpable { Trace.endSection(); } - @Override public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { pw.println(" mSlice: " + mSlice); pw.println(" mClickActions: " + mClickActions); - - mKeyguardStatusView.dump(fd, pw, args); + pw.println(" mLockScreenMode: " + mLockScreenMode); } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java index 9ef2def04ec1..2036b3321bda 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java @@ -19,16 +19,13 @@ package com.android.keyguard; import android.app.ActivityManager; import android.app.IActivityManager; import android.content.Context; -import android.content.res.Resources; import android.graphics.Color; import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; import android.text.TextUtils; -import android.text.format.DateFormat; import android.util.AttributeSet; import android.util.Log; -import android.util.Slog; import android.util.TypedValue; import android.view.View; import android.widget.GridLayout; @@ -39,15 +36,18 @@ import androidx.core.graphics.ColorUtils; import com.android.internal.widget.LockPatternUtils; import com.android.systemui.Dependency; import com.android.systemui.R; -import com.android.systemui.statusbar.policy.ConfigurationController; import java.io.FileDescriptor; import java.io.PrintWriter; -import java.util.Locale; -import java.util.TimeZone; -public class KeyguardStatusView extends GridLayout implements - ConfigurationController.ConfigurationListener { +/** + * View consisting of: + * - keyguard clock + * - logout button (on certain managed devices) + * - owner information (if set) + * - notification icons (shown on AOD) + */ +public class KeyguardStatusView extends GridLayout { private static final boolean DEBUG = KeyguardConstants.DEBUG; private static final String TAG = "KeyguardStatusView"; private static final int MARQUEE_DELAY_MS = 2000; @@ -62,9 +62,7 @@ public class KeyguardStatusView extends GridLayout implements private View mNotificationIcons; private Runnable mPendingMarqueeStart; private Handler mHandler; - private KeyguardSliceViewController mKeyguardSliceViewController; - private boolean mPulsing; private float mDarkAmount = 0; private int mTextColor; @@ -76,56 +74,6 @@ public class KeyguardStatusView extends GridLayout implements private int mIconTopMarginWithHeader; private boolean mShowingHeader; - private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { - - @Override - public void onLockScreenModeChanged(int mode) { - updateLockScreenMode(mode); - } - - @Override - public void onTimeChanged() { - refreshTime(); - } - - @Override - public void onTimeZoneChanged(TimeZone timeZone) { - updateTimeZone(timeZone); - } - - @Override - public void onKeyguardVisibilityChanged(boolean showing) { - if (showing) { - if (DEBUG) Slog.v(TAG, "refresh statusview showing:" + showing); - refreshTime(); - updateOwnerInfo(); - updateLogoutView(); - } - } - - @Override - public void onStartedWakingUp() { - setEnableMarquee(true); - } - - @Override - public void onFinishedGoingToSleep(int why) { - setEnableMarquee(false); - } - - @Override - public void onUserSwitchComplete(int userId) { - refreshFormat(); - updateOwnerInfo(); - updateLogoutView(); - } - - @Override - public void onLogoutEnabledChanged() { - updateLogoutView(); - } - }; - public KeyguardStatusView(Context context) { this(context, null, 0); } @@ -142,21 +90,7 @@ public class KeyguardStatusView extends GridLayout implements onDensityOrFontScaleChanged(); } - /** - * If we're presenting a custom clock of just the default one. - */ - public boolean hasCustomClock() { - return mClockView.hasCustomClock(); - } - - /** - * Set whether or not the lock screen is showing notifications. - */ - public void setHasVisibleNotifications(boolean hasVisibleNotifications) { - mClockView.setHasVisibleNotifications(hasVisibleNotifications); - } - - private void setEnableMarquee(boolean enabled) { + void setEnableMarquee(boolean enabled) { if (DEBUG) Log.v(TAG, "Schedule setEnableMarquee: " + (enabled ? "Enable" : "Disable")); if (enabled) { if (mPendingMarqueeStart == null) { @@ -203,7 +137,6 @@ public class KeyguardStatusView extends GridLayout implements boolean shouldMarquee = Dependency.get(KeyguardUpdateMonitor.class).isDeviceInteractive(); setEnableMarquee(shouldMarquee); - refreshFormat(); updateOwnerInfo(); updateLogoutView(); updateDark(); @@ -238,64 +171,14 @@ public class KeyguardStatusView extends GridLayout implements layoutOwnerInfo(); } - @Override - public void onDensityOrFontScaleChanged() { - if (mClockView != null) { - mClockView.setTextSize(TypedValue.COMPLEX_UNIT_PX, - getResources().getDimensionPixelSize(R.dimen.widget_big_font_size)); - } - if (mOwnerInfo != null) { - mOwnerInfo.setTextSize(TypedValue.COMPLEX_UNIT_PX, - getResources().getDimensionPixelSize(R.dimen.widget_label_font_size)); - } - loadBottomMargin(); - } - - public void dozeTimeTick() { - refreshTime(); - mKeyguardSliceViewController.refresh(); - } - - private void refreshTime() { - mClockView.refresh(); - } - - private void updateLockScreenMode(int mode) { - mClockView.updateLockScreenMode(mode); - } - - private void updateTimeZone(TimeZone timeZone) { - mClockView.onTimeZoneChanged(timeZone); - } - - private void refreshFormat() { - Patterns.update(mContext); - mClockView.setFormat12Hour(Patterns.clockView12); - mClockView.setFormat24Hour(Patterns.clockView24); - } - - public int getLogoutButtonHeight() { + int getLogoutButtonHeight() { if (mLogoutView == null) { return 0; } return mLogoutView.getVisibility() == VISIBLE ? mLogoutView.getHeight() : 0; } - public float getClockTextSize() { - return mClockView.getTextSize(); - } - - /** - * Returns the preferred Y position of the clock. - * - * @param totalHeight The height available to position the clock. - * @return Y position of clock. - */ - public int getClockPreferredY(int totalHeight) { - return mClockView.getPreferredY(totalHeight); - } - - private void updateLogoutView() { + void updateLogoutView() { if (mLogoutView == null) { return; } @@ -305,7 +188,16 @@ public class KeyguardStatusView extends GridLayout implements com.android.internal.R.string.global_action_logout)); } - private void updateOwnerInfo() { + void onDensityOrFontScaleChanged() { + if (mOwnerInfo != null) { + mOwnerInfo.setTextSize(TypedValue.COMPLEX_UNIT_PX, + getResources().getDimensionPixelSize( + com.android.systemui.R.dimen.widget_label_font_size)); + loadBottomMargin(); + } + } + + void updateOwnerInfo() { if (mOwnerInfo == null) return; String info = mLockPatternUtils.getDeviceOwnerInfo(); if (info == null) { @@ -320,30 +212,36 @@ public class KeyguardStatusView extends GridLayout implements updateDark(); } - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - Dependency.get(KeyguardUpdateMonitor.class).registerCallback(mInfoCallback); - Dependency.get(ConfigurationController.class).addCallback(this); + void setDarkAmount(float darkAmount) { + if (mDarkAmount == darkAmount) { + return; + } + mDarkAmount = darkAmount; + mClockView.setDarkAmount(darkAmount); + updateDark(); } - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - Dependency.get(KeyguardUpdateMonitor.class).removeCallback(mInfoCallback); - Dependency.get(ConfigurationController.class).removeCallback(this); - } + void updateDark() { + boolean dark = mDarkAmount == 1; + if (mLogoutView != null) { + mLogoutView.setAlpha(dark ? 0 : 1); + } - @Override - public void onLocaleListChanged() { - refreshFormat(); + if (mOwnerInfo != null) { + boolean hasText = !TextUtils.isEmpty(mOwnerInfo.getText()); + mOwnerInfo.setVisibility(hasText ? VISIBLE : GONE); + layoutOwnerInfo(); + } + + final int blendedTextColor = ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount); + mKeyguardSlice.setDarkAmount(mDarkAmount); + mClockView.setTextColor(blendedTextColor); } public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("KeyguardStatusView:"); pw.println(" mOwnerInfo: " + (mOwnerInfo == null ? "null" : mOwnerInfo.getVisibility() == VISIBLE)); - pw.println(" mPulsing: " + mPulsing); pw.println(" mDarkAmount: " + mDarkAmount); pw.println(" mTextColor: " + Integer.toHexString(mTextColor)); if (mLogoutView != null) { @@ -363,64 +261,6 @@ public class KeyguardStatusView extends GridLayout implements R.dimen.widget_vertical_padding_with_header); } - // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often. - // This is an optimization to ensure we only recompute the patterns when the inputs change. - private static final class Patterns { - static String clockView12; - static String clockView24; - static String cacheKey; - - static void update(Context context) { - final Locale locale = Locale.getDefault(); - final Resources res = context.getResources(); - final String clockView12Skel = res.getString(R.string.clock_12hr_format); - final String clockView24Skel = res.getString(R.string.clock_24hr_format); - final String key = locale.toString() + clockView12Skel + clockView24Skel; - if (key.equals(cacheKey)) return; - - clockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel); - // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton - // format. The following code removes the AM/PM indicator if we didn't want it. - if (!clockView12Skel.contains("a")) { - clockView12 = clockView12.replaceAll("a", "").trim(); - } - - clockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel); - - // Use fancy colon. - clockView24 = clockView24.replace(':', '\uee01'); - clockView12 = clockView12.replace(':', '\uee01'); - - cacheKey = key; - } - } - - public void setDarkAmount(float darkAmount) { - if (mDarkAmount == darkAmount) { - return; - } - mDarkAmount = darkAmount; - mClockView.setDarkAmount(darkAmount); - updateDark(); - } - - private void updateDark() { - boolean dark = mDarkAmount == 1; - if (mLogoutView != null) { - mLogoutView.setAlpha(dark ? 0 : 1); - } - - if (mOwnerInfo != null) { - boolean hasText = !TextUtils.isEmpty(mOwnerInfo.getText()); - mOwnerInfo.setVisibility(hasText ? VISIBLE : GONE); - layoutOwnerInfo(); - } - - final int blendedTextColor = ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount); - mKeyguardSlice.setDarkAmount(mDarkAmount); - mClockView.setTextColor(blendedTextColor); - } - private void layoutOwnerInfo() { if (mOwnerInfo != null && mOwnerInfo.getVisibility() != GONE) { // Animate owner info during wake-up transition @@ -442,13 +282,6 @@ public class KeyguardStatusView extends GridLayout implements } } - public void setPulsing(boolean pulsing) { - if (mPulsing == pulsing) { - return; - } - mPulsing = pulsing; - } - private boolean shouldShowLogout() { return Dependency.get(KeyguardUpdateMonitor.class).isLogoutEnabled() && KeyguardUpdateMonitor.getCurrentUser() != UserHandle.USER_SYSTEM; @@ -463,9 +296,4 @@ public class KeyguardStatusView extends GridLayout implements Log.e(TAG, "Failed to logout user", re); } } - - // TODO: remove this method when a controller is available. - void setKeyguardSliceViewController(KeyguardSliceViewController keyguardSliceViewController) { - mKeyguardSliceViewController = keyguardSliceViewController; - } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java new file mode 100644 index 000000000000..cc7b8322d190 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java @@ -0,0 +1,360 @@ +/* + * 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.keyguard; + +import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; + +import android.util.Slog; +import android.view.View; + +import com.android.systemui.Interpolators; +import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.notification.AnimatableProperty; +import com.android.systemui.statusbar.notification.PropertyAnimator; +import com.android.systemui.statusbar.notification.stack.AnimationProperties; +import com.android.systemui.statusbar.notification.stack.StackStateAnimator; +import com.android.systemui.statusbar.phone.NotificationIconAreaController; +import com.android.systemui.statusbar.phone.NotificationIconContainer; +import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.ViewController; + +import java.util.TimeZone; + +import javax.inject.Inject; + +/** + * Injectable controller for {@link KeyguardStatusView}. + */ +public class KeyguardStatusViewController extends ViewController<KeyguardStatusView> { + private static final boolean DEBUG = KeyguardConstants.DEBUG; + private static final String TAG = "KeyguardStatusViewController"; + + private static final AnimationProperties CLOCK_ANIMATION_PROPERTIES = + new AnimationProperties().setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); + + private final KeyguardSliceViewController mKeyguardSliceViewController; + private final KeyguardClockSwitchController mKeyguardClockSwitchController; + private final KeyguardStateController mKeyguardStateController; + private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; + private final ConfigurationController mConfigurationController; + private final NotificationIconAreaController mNotificationIconAreaController; + + private boolean mKeyguardStatusViewAnimating; + private int mLockScreenMode = KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL; + + @Inject + public KeyguardStatusViewController( + KeyguardStatusView keyguardStatusView, + KeyguardSliceViewController keyguardSliceViewController, + KeyguardClockSwitchController keyguardClockSwitchController, + KeyguardStateController keyguardStateController, + KeyguardUpdateMonitor keyguardUpdateMonitor, + ConfigurationController configurationController, + NotificationIconAreaController notificationIconAreaController) { + super(keyguardStatusView); + mKeyguardSliceViewController = keyguardSliceViewController; + mKeyguardClockSwitchController = keyguardClockSwitchController; + mKeyguardStateController = keyguardStateController; + mKeyguardUpdateMonitor = keyguardUpdateMonitor; + mConfigurationController = configurationController; + mNotificationIconAreaController = notificationIconAreaController; + } + + @Override + public void onInit() { + mKeyguardClockSwitchController.init(); + } + + @Override + protected void onViewAttached() { + mKeyguardUpdateMonitor.registerCallback(mInfoCallback); + mConfigurationController.addCallback(mConfigurationListener); + updateAodIcons(); + } + + @Override + protected void onViewDetached() { + mKeyguardUpdateMonitor.removeCallback(mInfoCallback); + mConfigurationController.removeCallback(mConfigurationListener); + } + + /** + * Updates views on doze time tick. + */ + public void dozeTimeTick() { + refreshTime(); + mKeyguardSliceViewController.refresh(); + } + + /** + * The amount we're in doze. + */ + public void setDarkAmount(float darkAmount) { + mView.setDarkAmount(darkAmount); + } + + /** + * Set whether or not the lock screen is showing notifications. + */ + public void setHasVisibleNotifications(boolean hasVisibleNotifications) { + mKeyguardClockSwitchController.setHasVisibleNotifications(hasVisibleNotifications); + } + + /** + * If we're presenting a custom clock of just the default one. + */ + public boolean hasCustomClock() { + return mKeyguardClockSwitchController.hasCustomClock(); + } + + /** + * Get the height of the logout button. + */ + public int getLogoutButtonHeight() { + return mView.getLogoutButtonHeight(); + } + + /** + * Set keyguard status view alpha. + */ + public void setAlpha(float alpha) { + if (!mKeyguardStatusViewAnimating) { + mView.setAlpha(alpha); + } + } + + /** + * Set pivot x. + */ + public void setPivotX(float pivot) { + mView.setPivotX(pivot); + } + + /** + * Set pivot y. + */ + public void setPivotY(float pivot) { + mView.setPivotY(pivot); + } + + /** + * Get the clock text size. + */ + public float getClockTextSize() { + return mKeyguardClockSwitchController.getClockTextSize(); + } + + /** + * Returns the preferred Y position of the clock. + * + * @param totalHeight The height available to position the clock. + * @return Y position of clock. + */ + public int getClockPreferredY(int totalHeight) { + return mKeyguardClockSwitchController.getClockPreferredY(totalHeight); + } + + /** + * Get the height of the keyguard status view. + */ + public int getHeight() { + return mView.getHeight(); + } + + /** + * Set whether the view accessibility importance mode. + */ + public void setStatusAccessibilityImportance(int mode) { + mView.setImportantForAccessibility(mode); + } + + /** + * Update position of the view with an optional animation + */ + public void updatePosition(int x, int y, boolean animate) { + PropertyAnimator.setProperty(mView, AnimatableProperty.Y, y, CLOCK_ANIMATION_PROPERTIES, + animate); + + if (mLockScreenMode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) { + // reset any prior movement + PropertyAnimator.setProperty(mView, AnimatableProperty.X, 0, + CLOCK_ANIMATION_PROPERTIES, animate); + + mKeyguardClockSwitchController.updatePosition(x, CLOCK_ANIMATION_PROPERTIES, animate); + } else { + // reset any prior movement + mKeyguardClockSwitchController.updatePosition(0, CLOCK_ANIMATION_PROPERTIES, animate); + + PropertyAnimator.setProperty(mView, AnimatableProperty.X, x, + CLOCK_ANIMATION_PROPERTIES, animate); + } + } + + /** + * Set the visibility of the keyguard status view based on some new state. + */ + public void setKeyguardStatusViewVisibility( + int statusBarState, + boolean keyguardFadingAway, + boolean goingToFullShade, + int oldStatusBarState) { + mView.animate().cancel(); + mKeyguardStatusViewAnimating = false; + if ((!keyguardFadingAway && oldStatusBarState == KEYGUARD + && statusBarState != KEYGUARD) || goingToFullShade) { + mKeyguardStatusViewAnimating = true; + mView.animate() + .alpha(0f) + .setStartDelay(0) + .setDuration(160) + .setInterpolator(Interpolators.ALPHA_OUT) + .withEndAction( + mAnimateKeyguardStatusViewGoneEndRunnable); + if (keyguardFadingAway) { + mView.animate() + .setStartDelay(mKeyguardStateController.getKeyguardFadingAwayDelay()) + .setDuration(mKeyguardStateController.getShortenedFadingAwayDuration()) + .start(); + } + } else if (oldStatusBarState == StatusBarState.SHADE_LOCKED && statusBarState == KEYGUARD) { + mView.setVisibility(View.VISIBLE); + mKeyguardStatusViewAnimating = true; + mView.setAlpha(0f); + mView.animate() + .alpha(1f) + .setStartDelay(0) + .setDuration(320) + .setInterpolator(Interpolators.ALPHA_IN) + .withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable); + } else if (statusBarState == KEYGUARD) { + if (keyguardFadingAway) { + mKeyguardStatusViewAnimating = true; + mView.animate() + .alpha(0) + .translationYBy(-getHeight() * 0.05f) + .setInterpolator(Interpolators.FAST_OUT_LINEAR_IN) + .setDuration(125) + .setStartDelay(0) + .withEndAction(mAnimateKeyguardStatusViewInvisibleEndRunnable) + .start(); + } else { + mView.setVisibility(View.VISIBLE); + mView.setAlpha(1f); + } + } else { + mView.setVisibility(View.GONE); + mView.setAlpha(1f); + } + } + + private void refreshTime() { + mKeyguardClockSwitchController.refresh(); + } + + private void updateAodIcons() { + NotificationIconContainer nic = (NotificationIconContainer) + mView.findViewById(com.android.systemui.R.id.clock_notification_icon_container); + if (mLockScreenMode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL) { + // alternate icon area is set in KeyguardClockSwitchController + mNotificationIconAreaController.setupAodIcons(nic, mLockScreenMode); + } else { + nic.setVisibility(View.GONE); + } + } + + private final ConfigurationController.ConfigurationListener mConfigurationListener = + new ConfigurationController.ConfigurationListener() { + @Override + public void onLocaleListChanged() { + refreshTime(); + } + + @Override + public void onDensityOrFontScaleChanged() { + mKeyguardClockSwitchController.onDensityOrFontScaleChanged(); + mView.onDensityOrFontScaleChanged(); + } + }; + + private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { + @Override + public void onLockScreenModeChanged(int mode) { + mLockScreenMode = mode; + mKeyguardClockSwitchController.updateLockScreenMode(mode); + mKeyguardSliceViewController.updateLockScreenMode(mode); + updateAodIcons(); + } + + @Override + public void onTimeChanged() { + refreshTime(); + } + + @Override + public void onTimeZoneChanged(TimeZone timeZone) { + mKeyguardClockSwitchController.updateTimeZone(timeZone); + } + + @Override + public void onKeyguardVisibilityChanged(boolean showing) { + if (showing) { + if (DEBUG) Slog.v(TAG, "refresh statusview showing:" + showing); + refreshTime(); + mView.updateOwnerInfo(); + mView.updateLogoutView(); + } + } + + @Override + public void onStartedWakingUp() { + mView.setEnableMarquee(true); + } + + @Override + public void onFinishedGoingToSleep(int why) { + mView.setEnableMarquee(false); + } + + @Override + public void onUserSwitchComplete(int userId) { + mKeyguardClockSwitchController.refreshFormat(); + mView.updateOwnerInfo(); + mView.updateLogoutView(); + } + + @Override + public void onLogoutEnabledChanged() { + mView.updateLogoutView(); + } + }; + + private final Runnable mAnimateKeyguardStatusViewInvisibleEndRunnable = () -> { + mKeyguardStatusViewAnimating = false; + mView.setVisibility(View.INVISIBLE); + }; + + + private final Runnable mAnimateKeyguardStatusViewGoneEndRunnable = () -> { + mKeyguardStatusViewAnimating = false; + mView.setVisibility(View.GONE); + }; + + private final Runnable mAnimateKeyguardStatusViewVisibleEndRunnable = () -> { + mKeyguardStatusViewAnimating = false; + }; +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index bb8a99bb8cd8..fa1762e1e5db 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -94,6 +94,7 @@ import com.android.settingslib.fuelgauge.BatteryStatus; import com.android.systemui.DejankUtils; import com.android.systemui.Dumpable; import com.android.systemui.R; +import com.android.systemui.biometrics.AuthController; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; @@ -236,6 +237,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final Context mContext; private final boolean mIsPrimaryUser; private final boolean mIsAutomotive; + private final AuthController mAuthController; private final StatusBarStateController mStatusBarStateController; HashMap<Integer, SimData> mSimDatas = new HashMap<>(); HashMap<Integer, ServiceState> mServiceStates = new HashMap<Integer, ServiceState>(); @@ -288,6 +290,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final DevicePolicyManager mDevicePolicyManager; private final BroadcastDispatcher mBroadcastDispatcher; private boolean mLogoutEnabled; + // cached value to avoid IPCs + private boolean mIsUdfpsEnrolled; // If the user long pressed the lock icon, disabling face auth for the current session. private boolean mLockIconPressed; private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID; @@ -1582,7 +1586,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab RingerModeTracker ringerModeTracker, @Background Executor backgroundExecutor, StatusBarStateController statusBarStateController, - LockPatternUtils lockPatternUtils) { + LockPatternUtils lockPatternUtils, + AuthController authController) { mContext = context; mSubscriptionManager = SubscriptionManager.from(context); mDeviceProvisioned = isDeviceProvisionedInSettingsDb(); @@ -1592,6 +1597,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mRingerModeTracker = ringerModeTracker; mStatusBarStateController = statusBarStateController; mLockPatternUtils = lockPatternUtils; + mAuthController = authController; dumpManager.registerDumpable(getClass().getName(), this); mHandler = new Handler(mainLooper) { @@ -1718,7 +1724,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } // Take a guess at initial SIM state, battery status and PLMN until we get an update - mBatteryStatus = new BatteryStatus(BATTERY_STATUS_UNKNOWN, 100, 0, 0, 0); + mBatteryStatus = new BatteryStatus(BATTERY_STATUS_UNKNOWN, 100, 0, 0, 0, true); // Watch for interesting updates final IntentFilter filter = new IntentFilter(); @@ -1854,7 +1860,15 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private void updateLockScreenMode() { mLockScreenMode = Settings.Global.getInt(mContext.getContentResolver(), - Settings.Global.SHOW_NEW_LOCKSCREEN, 0); + Settings.Global.SHOW_NEW_LOCKSCREEN, + isUdfpsEnrolled() ? 1 : 0); + } + + private void updateUdfpsEnrolled(int userId) { + mIsUdfpsEnrolled = mAuthController.isUdfpsEnrolled(userId); + } + public boolean isUdfpsEnrolled() { + return mIsUdfpsEnrolled; } private final UserSwitchObserver mUserSwitchObserver = new UserSwitchObserver() { @@ -2095,6 +2109,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } if (DEBUG) Log.v(TAG, "startListeningForFingerprint()"); int userId = getCurrentUser(); + updateUdfpsEnrolled(userId); if (isUnlockWithFingerprintPossible(userId)) { if (mFingerprintCancelSignal != null) { mFingerprintCancelSignal.cancel(); @@ -2103,7 +2118,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab if (isEncryptedOrLockdown(userId)) { mFpm.detectFingerprint(mFingerprintCancelSignal, mFingerprintDetectionCallback, - userId, null /* surface */); + userId); } else { mFpm.authenticate(null /* crypto */, mFingerprintCancelSignal, mFingerprintAuthenticationCallback, null /* handler */, userId); @@ -2632,6 +2647,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab final boolean wasPluggedIn = old.isPluggedIn(); final boolean stateChangedWhilePluggedIn = wasPluggedIn && nowPluggedIn && (old.status != current.status); + final boolean nowPresent = current.present; + final boolean wasPresent = old.present; // change in plug state is always interesting if (wasPluggedIn != nowPluggedIn || stateChangedWhilePluggedIn) { @@ -2648,6 +2665,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return true; } + // Battery either showed up or disappeared + if (wasPresent != nowPresent) { + return true; + } + return false; } @@ -3122,6 +3144,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab + " expected=" + (shouldListenForFingerprint() ? 1 : 0)); pw.println(" strongAuthFlags=" + Integer.toHexString(strongAuthFlags)); pw.println(" trustManaged=" + getUserTrustIsManaged(userId)); + pw.println(" udfpsEnrolled=" + isUdfpsEnrolled()); } if (mFaceManager != null && mFaceManager.isHardwareDetected()) { final int userId = ActivityManager.getCurrentUser(); diff --git a/packages/SystemUI/src/com/android/keyguard/TimeBasedColorsClockController.java b/packages/SystemUI/src/com/android/keyguard/TimeBasedColorsClockController.java new file mode 100644 index 000000000000..3cbae0a18937 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/TimeBasedColorsClockController.java @@ -0,0 +1,178 @@ +/* + * 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.keyguard; + +import android.util.MathUtils; + +import com.android.internal.graphics.ColorUtils; +import com.android.settingslib.Utils; +import com.android.systemui.R; +import com.android.systemui.util.ViewController; + +import java.util.Calendar; +import java.util.GregorianCalendar; + +/** + * Changes the color of the text clock based on the time of day. + */ +public class TimeBasedColorsClockController extends ViewController<GradientTextClock> { + private final int[] mGradientColors = new int[3]; + private final float[] mPositions = new float[3]; + + /** + * 0 = fully awake + * between 0 and 1 = transitioning between awake and doze + * 1 = fully in doze + */ + private float mDarkAmount = 0f; + + public TimeBasedColorsClockController(GradientTextClock view) { + super(view); + } + + @Override + protected void onViewAttached() { + refreshTime(System.currentTimeMillis()); + } + + @Override + protected void onViewDetached() { + + } + + /** + * Updates the time for this view. Also updates any color changes. + */ + public void refreshTime(long timeInMillis) { + updateColors(timeInMillis); + updatePositions(timeInMillis); + mView.refreshTime(); + } + + /** + * Set the amount (ratio) that the device has transitioned to doze. + * + * @param darkAmount Amount of transition to doze: 1f for doze and 0f for awake. + */ + public void setDarkAmount(float darkAmount) { + mDarkAmount = darkAmount; + + // TODO: (b/170228350) currently this relayouts throughout the animation; + // eventually this should use new Text APIs to animate the variable font weight + refreshTime(System.currentTimeMillis()); + + int weight = (int) MathUtils.lerp(200, 400, 1f - darkAmount); + mView.setFontVariationSettings("'wght' " + weight); + } + + private int getTimeIndex(long timeInMillis) { + Calendar now = getCalendar(timeInMillis); + int hour = now.get(Calendar.HOUR_OF_DAY); // 0 - 23 + if (hour < mTimes[0]) { + return mTimes.length - 1; + } + + for (int i = 1; i < mTimes.length; i++) { + if (hour < mTimes[i]) { + return i - 1; + } + } + + return mTimes.length - 1; + } + + private void updateColors(long timeInMillis) { + final int index = getTimeIndex(timeInMillis); + final int wallpaperTextColor = + Utils.getColorAttrDefaultColor(mView.getContext(), R.attr.wallpaperTextColor); + for (int i = 0; i < mGradientColors.length; i++) { + // wallpaperTextColor on LS when mDarkAmount = 0f + // full color on AOD when mDarkAmount = 1f + mGradientColors[i] = + ColorUtils.blendARGB(wallpaperTextColor, COLORS[index][i], mDarkAmount); + } + mView.setGradientColors(mGradientColors); + } + + private void updatePositions(long timeInMillis) { + Calendar now = getCalendar(timeInMillis); + final int index = getTimeIndex(timeInMillis); + + final Calendar startTime = new GregorianCalendar(); + startTime.setTimeInMillis(now.getTimeInMillis()); + startTime.set(Calendar.HOUR_OF_DAY, mTimes[index]); + if (startTime.getTimeInMillis() > now.getTimeInMillis()) { + // start should be earlier than 'now' + startTime.add(Calendar.DATE, -1); + } + + final Calendar endTime = new GregorianCalendar(); + endTime.setTimeInMillis(now.getTimeInMillis()); + if (index == mTimes.length - 1) { + endTime.set(Calendar.HOUR_OF_DAY, mTimes[0]); + endTime.add(Calendar.DATE, 1); // end time is tomorrow + } else { + endTime.set(Calendar.HOUR_OF_DAY, mTimes[index + 1]); + } + + long totalTimeInThisColorGradient = endTime.getTimeInMillis() - startTime.getTimeInMillis(); + long timeIntoThisColorGradient = now.getTimeInMillis() - startTime.getTimeInMillis(); + float percentageWithinGradient = + (float) timeIntoThisColorGradient / (float) totalTimeInThisColorGradient; + + for (int i = 0; i < mPositions.length; i++) { + // currently hard-coded .3 movement of gradient + mPositions[i] = POSITIONS[index][i] - (.3f * percentageWithinGradient); + } + mView.setColorPositions(mPositions); + } + + private Calendar getCalendar(long timeInMillis) { + Calendar now = new GregorianCalendar(); + now.setTimeInMillis(timeInMillis); + return now; + } + + private static final int[] SUNRISE = new int[] {0xFF6F75AA, 0xFFAFF0FF, 0xFFFFDEBF}; + private static final int[] DAY = new int[] {0xFF9BD8FB, 0xFFD7F5FF, 0xFFFFF278}; + private static final int[] NIGHT = new int[] {0xFF333D5E, 0xFFC5A1D6, 0xFF907359}; + + private static final float[] SUNRISE_START_POSITIONS = new float[] {.3f, .5f, .8f}; + private static final float[] DAY_START_POSITIONS = new float[] {.4f, .8f, 1f}; + private static final float[] NIGHT_START_POSITIONS = new float[] {.25f, .5f, .8f}; + + // TODO (b/170228350): use TwilightManager to set sunrise/sunset times + private final int mSunriseTime = 6; // 6am + private final int mDaytime = 9; // 9 am + private final int mNightTime = 19; // 7pm + + private int[] mTimes = new int[] { + mSunriseTime, + mDaytime, + mNightTime + }; + private static final int[][] COLORS = new int[][] { + SUNRISE, + DAY, + NIGHT + }; + private static final float[][] POSITIONS = new float[][] { + SUNRISE_START_POSITIONS, + DAY_START_POSITIONS, + NIGHT_START_POSITIONS + }; +} diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java index 881108858b51..4fad9a916d0d 100644 --- a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java +++ b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java @@ -24,6 +24,7 @@ import com.android.keyguard.KeyguardMessageArea; import com.android.keyguard.KeyguardSecurityContainer; import com.android.keyguard.KeyguardSecurityViewFlipper; import com.android.systemui.R; +import com.android.systemui.dagger.qualifiers.RootView; import com.android.systemui.statusbar.phone.KeyguardBouncer; import dagger.Module; diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusViewComponent.java b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusViewComponent.java index 21ccff707d34..1b6476ce74df 100644 --- a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusViewComponent.java +++ b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusViewComponent.java @@ -18,6 +18,7 @@ package com.android.keyguard.dagger; import com.android.keyguard.KeyguardClockSwitchController; import com.android.keyguard.KeyguardStatusView; +import com.android.keyguard.KeyguardStatusViewController; import dagger.BindsInstance; import dagger.Subcomponent; @@ -36,4 +37,7 @@ public interface KeyguardStatusViewComponent { /** Builds a {@link com.android.keyguard.KeyguardClockSwitchController}. */ KeyguardClockSwitchController getKeyguardClockSwitchController(); + + /** Builds a {@link com.android.keyguard.KeyguardStatusViewController}. */ + KeyguardStatusViewController getKeyguardStatusViewController(); } diff --git a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java index 521bb8d58c2b..caaee5fd3f37 100644 --- a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java +++ b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java @@ -31,6 +31,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.database.ContentObserver; import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Handler; import android.provider.Settings; @@ -91,12 +92,15 @@ public class BatteryMeterView extends LinearLayout implements private int mTextColor; private int mLevel; private int mShowPercentMode = MODE_DEFAULT; - private boolean mForceShowPercent; private boolean mShowPercentAvailable; // Some places may need to show the battery conditionally, and not obey the tuner private boolean mIgnoreTunerUpdates; private boolean mIsSubscribedForTunerUpdates; private boolean mCharging; + // Error state where we know nothing about the current battery state + private boolean mBatteryStateUnknown; + // Lazily-loaded since this is expected to be a rare-if-ever state + private Drawable mUnknownStateDrawable; private DualToneHandler mDualToneHandler; private int mUser; @@ -341,6 +345,11 @@ public class BatteryMeterView extends LinearLayout implements } private void updatePercentText() { + if (mBatteryStateUnknown) { + setContentDescription(getContext().getString(R.string.accessibility_battery_unknown)); + return; + } + if (mBatteryController == null) { return; } @@ -381,9 +390,13 @@ public class BatteryMeterView extends LinearLayout implements final boolean systemSetting = 0 != whitelistIpcs(() -> Settings.System .getIntForUser(getContext().getContentResolver(), SHOW_BATTERY_PERCENT, 0, mUser)); + boolean shouldShow = + (mShowPercentAvailable && systemSetting && mShowPercentMode != MODE_OFF) + || mShowPercentMode == MODE_ON + || mShowPercentMode == MODE_ESTIMATE; + shouldShow = shouldShow && !mBatteryStateUnknown; - if ((mShowPercentAvailable && systemSetting && mShowPercentMode != MODE_OFF) - || mShowPercentMode == MODE_ON || mShowPercentMode == MODE_ESTIMATE) { + if (shouldShow) { if (!showing) { mBatteryPercentView = loadPercentView(); if (mPercentageStyleId != 0) { // Only set if specified as attribute @@ -409,6 +422,32 @@ public class BatteryMeterView extends LinearLayout implements scaleBatteryMeterViews(); } + private Drawable getUnknownStateDrawable() { + if (mUnknownStateDrawable == null) { + mUnknownStateDrawable = mContext.getDrawable(R.drawable.ic_battery_unknown); + mUnknownStateDrawable.setTint(mTextColor); + } + + return mUnknownStateDrawable; + } + + @Override + public void onBatteryUnknownStateChanged(boolean isUnknown) { + if (mBatteryStateUnknown == isUnknown) { + return; + } + + mBatteryStateUnknown = isUnknown; + + if (mBatteryStateUnknown) { + mBatteryIconView.setImageDrawable(getUnknownStateDrawable()); + } else { + mBatteryIconView.setImageDrawable(mDrawable); + } + + updateShowPercent(); + } + /** * Looks up the scale factor for status bar icons and scales the battery view by that amount. */ @@ -449,6 +488,10 @@ public class BatteryMeterView extends LinearLayout implements if (mBatteryPercentView != null) { mBatteryPercentView.setTextColor(singleToneColor); } + + if (mUnknownStateDrawable != null) { + mUnknownStateDrawable.setTint(singleToneColor); + } } public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { @@ -458,8 +501,8 @@ public class BatteryMeterView extends LinearLayout implements pw.println(" mDrawable.getPowerSave: " + powerSave); pw.println(" mBatteryPercentView.getText(): " + percent); pw.println(" mTextColor: #" + Integer.toHexString(mTextColor)); + pw.println(" mBatteryStateUnknown: " + mBatteryStateUnknown); pw.println(" mLevel: " + mLevel); - pw.println(" mForceShowPercent: " + mForceShowPercent); } private final class SettingObserver extends ContentObserver { diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 9f28e0936d7f..cf576dd6b964 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -37,7 +37,6 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.systemui.appops.AppOpsController; import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.bubbles.Bubbles; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; @@ -119,13 +118,11 @@ import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.tracing.ProtoTracer; import com.android.systemui.tuner.TunablePadding.TunablePaddingService; import com.android.systemui.tuner.TunerService; +import com.android.systemui.util.DeviceConfigProxy; import com.android.systemui.util.leak.GarbageMonitor; import com.android.systemui.util.leak.LeakDetector; import com.android.systemui.util.leak.LeakReporter; import com.android.systemui.util.sensors.AsyncSensorManager; -import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.common.DisplayImeController; -import com.android.wm.shell.common.SystemWindows; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -310,7 +307,6 @@ public class Dependency { @Inject Lazy<KeyguardDismissUtil> mKeyguardDismissUtil; @Inject Lazy<SmartReplyController> mSmartReplyController; @Inject Lazy<RemoteInputQuickSettingsDisabler> mRemoteInputQuickSettingsDisabler; - @Inject Lazy<Bubbles> mBubbles; @Inject Lazy<NotificationEntryManager> mNotificationEntryManager; @Inject Lazy<SensorPrivacyManager> mSensorPrivacyManager; @Inject Lazy<AutoHideController> mAutoHideController; @@ -340,12 +336,10 @@ public class Dependency { @Inject Lazy<CommandQueue> mCommandQueue; @Inject Lazy<Recents> mRecents; @Inject Lazy<StatusBar> mStatusBar; - @Inject Lazy<DisplayController> mDisplayController; - @Inject Lazy<SystemWindows> mSystemWindows; - @Inject Lazy<DisplayImeController> mDisplayImeController; @Inject Lazy<RecordingController> mRecordingController; @Inject Lazy<ProtoTracer> mProtoTracer; @Inject Lazy<MediaOutputDialogFactory> mMediaOutputDialogFactory; + @Inject Lazy<DeviceConfigProxy> mDeviceConfigProxy; @Inject public Dependency() { @@ -510,7 +504,6 @@ public class Dependency { mProviders.put(SmartReplyController.class, mSmartReplyController::get); mProviders.put(RemoteInputQuickSettingsDisabler.class, mRemoteInputQuickSettingsDisabler::get); - mProviders.put(Bubbles.class, mBubbles::get); mProviders.put(NotificationEntryManager.class, mNotificationEntryManager::get); mProviders.put(ForegroundServiceNotificationListener.class, mForegroundServiceNotificationListener::get); @@ -530,10 +523,8 @@ public class Dependency { mProviders.put(CommandQueue.class, mCommandQueue::get); mProviders.put(Recents.class, mRecents::get); mProviders.put(StatusBar.class, mStatusBar::get); - mProviders.put(DisplayController.class, mDisplayController::get); - mProviders.put(SystemWindows.class, mSystemWindows::get); - mProviders.put(DisplayImeController.class, mDisplayImeController::get); mProviders.put(ProtoTracer.class, mProtoTracer::get); + mProviders.put(DeviceConfigProxy.class, mDeviceConfigProxy::get); // TODO(b/118592525): to support multi-display , we start to add something which is // per-display, while others may be global. I think it's time to add diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index a4648ee75485..6d057387430c 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -89,6 +89,7 @@ import com.android.systemui.qs.SecureSetting; import com.android.systemui.settings.UserTracker; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; +import com.android.systemui.util.settings.SecureSettings; import java.util.ArrayList; import java.util.List; @@ -124,6 +125,7 @@ public class ScreenDecorations extends SystemUI implements Tunable { private final BroadcastDispatcher mBroadcastDispatcher; private final Handler mMainHandler; private final TunerService mTunerService; + private final SecureSettings mSecureSettings; private DisplayManager.DisplayListener mDisplayListener; private CameraAvailabilityListener mCameraListener; private final UserTracker mUserTracker; @@ -203,11 +205,13 @@ public class ScreenDecorations extends SystemUI implements Tunable { @Inject public ScreenDecorations(Context context, @Main Handler handler, + SecureSettings secureSettings, BroadcastDispatcher broadcastDispatcher, TunerService tunerService, UserTracker userTracker) { super(context); mMainHandler = handler; + mSecureSettings = secureSettings; mBroadcastDispatcher = broadcastDispatcher; mTunerService = tunerService; mUserTracker = userTracker; @@ -313,7 +317,7 @@ public class ScreenDecorations extends SystemUI implements Tunable { // Watch color inversion and invert the overlay as needed. if (mColorInversionSetting == null) { - mColorInversionSetting = new SecureSetting(mContext, mHandler, + mColorInversionSetting = new SecureSetting(mSecureSettings, mHandler, Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, mUserTracker.getUserId()) { @Override @@ -321,10 +325,9 @@ public class ScreenDecorations extends SystemUI implements Tunable { updateColorInversion(value); } }; - - mColorInversionSetting.setListening(true); - mColorInversionSetting.onChange(false); } + mColorInversionSetting.setListening(true); + mColorInversionSetting.onChange(false); IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_USER_SWITCHED); @@ -586,6 +589,10 @@ public class ScreenDecorations extends SystemUI implements Tunable { @Override protected void onConfigurationChanged(Configuration newConfig) { + if (DEBUG_DISABLE_SCREEN_DECORATIONS) { + Log.i(TAG, "ScreenDecorations is disabled"); + return; + } mHandler.post(() -> { int oldRotation = mRotation; mPendingRotationChange = false; @@ -643,8 +650,8 @@ public class ScreenDecorations extends SystemUI implements Tunable { com.android.internal.R.dimen.rounded_corner_radius_bottom); final boolean changed = mRoundedDefault.x != newRoundedDefault - || mRoundedDefaultTop.x != newRoundedDefault - || mRoundedDefaultBottom.x != newRoundedDefault; + || mRoundedDefaultTop.x != newRoundedDefaultTop + || mRoundedDefaultBottom.x != newRoundedDefaultBottom; if (changed) { // If config_roundedCornerMultipleRadius set as true, ScreenDecorations respect the @@ -777,6 +784,10 @@ public class ScreenDecorations extends SystemUI implements Tunable { @Override public void onTuningChanged(String key, String newValue) { + if (DEBUG_DISABLE_SCREEN_DECORATIONS) { + Log.i(TAG, "ScreenDecorations is disabled"); + return; + } mHandler.post(() -> { if (mOverlays == null) return; if (SIZE.equals(key)) { diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index 7dcec3d75367..bf42a60ac033 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -19,22 +19,28 @@ package com.android.systemui; import android.app.ActivityThread; import android.app.Application; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.os.Process; import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; +import android.provider.Settings; import android.util.Log; import android.util.TimingsTraceLog; +import com.android.internal.protolog.common.ProtoLog; import com.android.systemui.dagger.ContextComponentHelper; import com.android.systemui.dagger.GlobalRootComponent; import com.android.systemui.dagger.SysUIComponent; import com.android.systemui.dump.DumpManager; +import com.android.systemui.people.PeopleSpaceActivity; +import com.android.systemui.people.widget.PeopleSpaceWidgetProvider; import com.android.systemui.util.NotificationChannels; import java.lang.reflect.Constructor; @@ -64,6 +70,8 @@ public class SystemUIApplication extends Application implements public SystemUIApplication() { super(); Log.v(TAG, "SystemUIApplication constructed."); + // SysUI may be building without protolog preprocessing in some cases + ProtoLog.REQUIRE_PROTOLOGTOOL = false; } @Override @@ -104,6 +112,35 @@ public class SystemUIApplication extends Application implements mServices[i].onBootCompleted(); } } + // If flag SHOW_PEOPLE_SPACE is true, enable People Space launcher icon. + // TODO(b/170396074): Remove this when we don't need an icon anymore. + try { + int showPeopleSpace = Settings.Global.getInt(context.getContentResolver(), + Settings.Global.SHOW_PEOPLE_SPACE, 0); + context.getPackageManager().setComponentEnabledSetting( + new ComponentName(context, PeopleSpaceActivity.class), + showPeopleSpace == 1 + ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED + : PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + } catch (Exception e) { + Log.w(TAG, "Error enabling People Space launch icon:", e); + } + + // If SHOW_PEOPLE_SPACE is true, enable People Space widget provider. + // TODO(b/170396074): Remove this when we don't need a widget anymore. + try { + int showPeopleSpace = Settings.Global.getInt(context.getContentResolver(), + Settings.Global.SHOW_PEOPLE_SPACE, 0); + context.getPackageManager().setComponentEnabledSetting( + new ComponentName(context, PeopleSpaceWidgetProvider.class), + showPeopleSpace == 1 + ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED + : PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + } catch (Exception e) { + Log.w(TAG, "Error enabling People Space widget:", e); + } } }, bootCompletedFilter); diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java index 80253b424335..17bb40e69e27 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java @@ -16,6 +16,7 @@ package com.android.systemui; +import android.app.ActivityThread; import android.content.Context; import android.content.res.AssetManager; import android.content.res.Resources; @@ -30,6 +31,7 @@ import com.android.systemui.dagger.WMComponent; import com.android.systemui.navigationbar.gestural.BackGestureTfClassifierProvider; import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider; +import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; @@ -49,6 +51,11 @@ public class SystemUIFactory { } public static void createFromConfig(Context context) { + createFromConfig(context, false); + } + + @VisibleForTesting + public static void createFromConfig(Context context, boolean fromTest) { if (mFactory != null) { return; } @@ -62,7 +69,7 @@ public class SystemUIFactory { Class<?> cls = null; cls = context.getClassLoader().loadClass(clsName); mFactory = (SystemUIFactory) cls.newInstance(); - mFactory.init(context); + mFactory.init(context, fromTest); } catch (Throwable t) { Log.w(TAG, "Error creating SystemUIFactory component: " + clsName, t); throw new RuntimeException(t); @@ -76,16 +83,48 @@ public class SystemUIFactory { public SystemUIFactory() {} - private void init(Context context) throws ExecutionException, InterruptedException { + @VisibleForTesting + public void init(Context context, boolean fromTest) + throws ExecutionException, InterruptedException { + // Only initialize components for the main system ui process running as the primary user + final boolean initializeComponents = !fromTest + && android.os.Process.myUserHandle().isSystem() + && ActivityThread.currentProcessName().equals(ActivityThread.currentPackageName()); mRootComponent = buildGlobalRootComponent(context); // Stand up WMComponent mWMComponent = mRootComponent.getWMComponentBuilder().build(); + if (initializeComponents) { + // Only initialize when not starting from tests since this currently initializes some + // components that shouldn't be run in the test environment + mWMComponent.init(); + } // And finally, retrieve whatever SysUI needs from WMShell and build SysUI. - // TODO: StubAPIClass is just a placeholder. - mSysUIComponent = mRootComponent.getSysUIComponent() - .setStubAPIClass(mWMComponent.createStubAPIClass()) + SysUIComponent.Builder builder = mRootComponent.getSysUIComponent(); + if (initializeComponents) { + // Only initialize when not starting from tests since this currently initializes some + // components that shouldn't be run in the test environment + builder = prepareSysUIComponentBuilder(builder, mWMComponent) + .setPip(mWMComponent.getPip()) + .setSplitScreen(mWMComponent.getSplitScreen()) + .setOneHanded(mWMComponent.getOneHanded()) + .setBubbles(mWMComponent.getBubbles()) + .setShellDump(mWMComponent.getShellDump()); + } else { + // TODO: Call on prepareSysUIComponentBuilder but not with real components. + builder = builder.setPip(Optional.ofNullable(null)) + .setSplitScreen(Optional.ofNullable(null)) + .setOneHanded(Optional.ofNullable(null)) + .setBubbles(Optional.ofNullable(null)) + .setShellDump(Optional.ofNullable(null)); + } + mSysUIComponent = builder + .setInputConsumerController(mWMComponent.getInputConsumerController()) + .setShellTaskOrganizer(mWMComponent.getShellTaskOrganizer()) .build(); + if (initializeComponents) { + mSysUIComponent.init(); + } // Every other part of our codebase currently relies on Dependency, so we // really need to ensure the Dependency gets initialized early on. @@ -93,6 +132,16 @@ public class SystemUIFactory { dependency.start(); } + /** + * Prepares the SysUIComponent builder before it is built. + * @param sysUIBuilder the builder provided by the root component's getSysUIComponent() method + * @param wm the built WMComponent from the root component's getWMComponent() method + */ + protected SysUIComponent.Builder prepareSysUIComponentBuilder( + SysUIComponent.Builder sysUIBuilder, WMComponent wm) { + return sysUIBuilder; + } + protected GlobalRootComponent buildGlobalRootComponent(Context context) { return DaggerGlobalRootComponent.builder() .context(context) @@ -141,7 +190,7 @@ public class SystemUIFactory { * This method is overridden in vendor specific implementation of Sys UI. */ public BackGestureTfClassifierProvider createBackGestureTfClassifierProvider( - AssetManager am) { + AssetManager am, String modelName) { return new BackGestureTfClassifierProvider(); } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java index 708002d5b946..1f41038c260f 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java @@ -32,6 +32,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpHandler; import com.android.systemui.dump.LogBufferFreezer; import com.android.systemui.dump.SystemUIAuxiliaryDumpService; +import com.android.systemui.statusbar.policy.BatteryStateNotifier; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -44,18 +45,21 @@ public class SystemUIService extends Service { private final DumpHandler mDumpHandler; private final BroadcastDispatcher mBroadcastDispatcher; private final LogBufferFreezer mLogBufferFreezer; + private final BatteryStateNotifier mBatteryStateNotifier; @Inject public SystemUIService( @Main Handler mainHandler, DumpHandler dumpHandler, BroadcastDispatcher broadcastDispatcher, - LogBufferFreezer logBufferFreezer) { + LogBufferFreezer logBufferFreezer, + BatteryStateNotifier batteryStateNotifier) { super(); mMainHandler = mainHandler; mDumpHandler = dumpHandler; mBroadcastDispatcher = broadcastDispatcher; mLogBufferFreezer = logBufferFreezer; + mBatteryStateNotifier = batteryStateNotifier; } @Override @@ -68,6 +72,11 @@ public class SystemUIService extends Service { // Finish initializing dump logic mLogBufferFreezer.attach(mBroadcastDispatcher); + // If configured, set up a battery notification + if (getResources().getBoolean(R.bool.config_showNotificationForUnknownBatteryState)) { + mBatteryStateNotifier.startListening(); + } + // For debugging RescueParty if (Build.IS_DEBUGGABLE && SystemProperties.getBoolean("debug.crash_sysui", false)) { throw new RuntimeException(); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java index 69a0d65a6963..e40185c279a8 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java @@ -24,6 +24,7 @@ import android.content.pm.ActivityInfo; import android.graphics.PixelFormat; import android.graphics.PointF; import android.os.Bundle; +import android.os.UserHandle; import android.provider.Settings; import android.util.MathUtils; import android.view.Gravity; @@ -31,6 +32,8 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.WindowManager; +import android.view.WindowManager.LayoutParams; +import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.widget.ImageView; @@ -48,15 +51,16 @@ class MagnificationModeSwitch { @VisibleForTesting static final long FADING_ANIMATION_DURATION_MS = 300; - private static final int DEFAULT_FADE_OUT_ANIMATION_DELAY_MS = 3000; - // The button visible duration starting from the last showButton() called. - private int mVisibleDuration = DEFAULT_FADE_OUT_ANIMATION_DELAY_MS; + @VisibleForTesting + static final int DEFAULT_FADE_OUT_ANIMATION_DELAY_MS = 3000; + private int mUiTimeout; private final Runnable mFadeInAnimationTask; private final Runnable mFadeOutAnimationTask; @VisibleForTesting boolean mIsFadeOutAnimating = false; private final Context mContext; + private final AccessibilityManager mAccessibilityManager; private final WindowManager mWindowManager; private final ImageView mImageView; private final PointF mLastDown = new PointF(); @@ -64,7 +68,7 @@ class MagnificationModeSwitch { private final int mTapTimeout = ViewConfiguration.getTapTimeout(); private final int mTouchSlop; private int mMagnificationMode = Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; - private final WindowManager.LayoutParams mParams; + private final LayoutParams mParams; private boolean mIsVisible = false; MagnificationModeSwitch(Context context) { @@ -74,9 +78,10 @@ class MagnificationModeSwitch { @VisibleForTesting MagnificationModeSwitch(Context context, @NonNull ImageView imageView) { mContext = context; + mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); mWindowManager = (WindowManager) mContext.getSystemService( Context.WINDOW_SERVICE); - mParams = createLayoutParams(); + mParams = createLayoutParams(context); mImageView = imageView; mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); applyResourcesValues(); @@ -202,6 +207,10 @@ class MagnificationModeSwitch { mWindowManager.addView(mImageView, mParams); mIsVisible = true; mImageView.postOnAnimation(mFadeInAnimationTask); + mUiTimeout = mAccessibilityManager.getRecommendedTimeoutMillis( + DEFAULT_FADE_OUT_ANIMATION_DELAY_MS, + AccessibilityManager.FLAG_CONTENT_ICONS + | AccessibilityManager.FLAG_CONTENT_CONTROLS); } if (mIsFadeOutAnimating) { mImageView.animate().cancel(); @@ -209,15 +218,26 @@ class MagnificationModeSwitch { } // Refresh the time slot of the fade-out task whenever this method is called. mImageView.removeCallbacks(mFadeOutAnimationTask); - mImageView.postOnAnimationDelayed(mFadeOutAnimationTask, mVisibleDuration); + mImageView.postOnAnimationDelayed(mFadeOutAnimationTask, mUiTimeout); } void onConfigurationChanged(int configDiff) { - if ((configDiff & ActivityInfo.CONFIG_DENSITY) == 0) { + if ((configDiff & ActivityInfo.CONFIG_DENSITY) != 0) { + applyResourcesValues(); + mImageView.setImageResource(getIconResId(mMagnificationMode)); + return; + } + if ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0) { + updateAccessibilityWindowTitle(); return; } - applyResourcesValues(); - mImageView.setImageResource(getIconResId(mMagnificationMode)); + } + + private void updateAccessibilityWindowTitle() { + mParams.accessibilityTitle = getAccessibilityWindowTitle(mContext); + if (mIsVisible) { + mWindowManager.updateViewLayout(mImageView, mParams); + } } private void toggleMagnificationMode() { @@ -225,8 +245,11 @@ class MagnificationModeSwitch { mMagnificationMode ^ Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL; mMagnificationMode = newMode; mImageView.setImageResource(getIconResId(newMode)); - Settings.Secure.putInt(mContext.getContentResolver(), - Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE, newMode); + Settings.Secure.putIntForUser( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE, + newMode, + UserHandle.USER_CURRENT); } private void handleSingleTap() { @@ -250,14 +273,19 @@ class MagnificationModeSwitch { : R.drawable.ic_open_in_new_fullscreen; } - private static WindowManager.LayoutParams createLayoutParams() { - final WindowManager.LayoutParams params = new WindowManager.LayoutParams( - WindowManager.LayoutParams.WRAP_CONTENT, - WindowManager.LayoutParams.WRAP_CONTENT, - WindowManager.LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + private static LayoutParams createLayoutParams(Context context) { + final LayoutParams params = new LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT, + LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, + LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); params.gravity = Gravity.BOTTOM | Gravity.RIGHT; + params.accessibilityTitle = getAccessibilityWindowTitle(context); return params; } + + private static String getAccessibilityWindowTitle(Context context) { + return context.getString(com.android.internal.R.string.android_system_label); + } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java index a705ec784c9a..98424beab14e 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java @@ -53,7 +53,8 @@ public class WindowMagnification extends SystemUI implements WindowMagnifierCall CommandQueue.Callbacks { private static final String TAG = "WindowMagnification"; private static final int CONFIG_MASK = - ActivityInfo.CONFIG_DENSITY | ActivityInfo.CONFIG_ORIENTATION; + ActivityInfo.CONFIG_DENSITY | ActivityInfo.CONFIG_ORIENTATION + | ActivityInfo.CONFIG_LOCALE; @VisibleForTesting protected WindowMagnificationAnimationController mWindowMagnificationAnimationController; @@ -72,8 +73,7 @@ public class WindowMagnification extends SystemUI implements WindowMagnifierCall super(context); mHandler = mainHandler; mLastConfiguration = new Configuration(context.getResources().getConfiguration()); - mAccessibilityManager = (AccessibilityManager) mContext.getSystemService( - Context.ACCESSIBILITY_SERVICE); + mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); mCommandQueue = commandQueue; mModeSwitchesController = modeSwitchesController; final WindowMagnificationController controller = new WindowMagnificationController(mContext, diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java index c3474bb7ca57..fd89baa61657 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java @@ -16,7 +16,7 @@ package com.android.systemui.accessibility; -import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; +import static android.view.WindowManager.LayoutParams; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; @@ -249,14 +249,23 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold if ((configDiff & ActivityInfo.CONFIG_DENSITY) != 0) { updateDimensions(); if (isWindowVisible()) { - mWm.removeView(mMirrorView); - createMirrorWindow(); + deleteWindowMagnification(); + enableWindowMagnification(Float.NaN, Float.NaN, Float.NaN); } } else if ((configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0) { onRotate(); + } else if ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0) { + updateAccessibilityWindowTitleIfNeeded(); } } + private void updateAccessibilityWindowTitleIfNeeded() { + if (!isWindowVisible()) return; + LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams(); + params.accessibilityTitle = getAccessibilityWindowTitle(); + mWm.updateViewLayout(mMirrorView, params); + } + /** Handles MirrorWindow position when the navigation bar mode changed. */ public void onNavigationModeChanged(int mode) { mNavBarMode = mode; @@ -290,8 +299,8 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold return; } // The rect of MirrorView is going to be transformed. - WindowManager.LayoutParams params = - (WindowManager.LayoutParams) mMirrorView.getLayoutParams(); + LayoutParams params = + (LayoutParams) mMirrorView.getLayoutParams(); mTmpRect.set(params.x, params.y, params.x + params.width, params.y + params.height); final RectF transformedRect = new RectF(mTmpRect); matrix.mapRect(transformedRect); @@ -313,17 +322,18 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold int windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin; int windowHeight = mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin; - WindowManager.LayoutParams params = new WindowManager.LayoutParams( + LayoutParams params = new LayoutParams( windowWidth, windowHeight, - WindowManager.LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, - WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL - | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, + LayoutParams.FLAG_NOT_TOUCH_MODAL + | LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); params.gravity = Gravity.TOP | Gravity.LEFT; params.x = mMagnificationFrame.left - mMirrorSurfaceMargin; params.y = mMagnificationFrame.top - mMirrorSurfaceMargin; - params.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + params.layoutInDisplayCutoutMode = LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; params.setTitle(mContext.getString(R.string.magnification_window_title)); + params.accessibilityTitle = getAccessibilityWindowTitle(); mMirrorView = LayoutInflater.from(mContext).inflate(R.layout.window_magnifier_view, null); mMirrorSurfaceView = mMirrorView.findViewById(R.id.surface_view); @@ -369,6 +379,10 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold return regionInsideDragBorder; } + private String getAccessibilityWindowTitle() { + return mResources.getString(com.android.internal.R.string.android_system_label); + } + private void showControls() { if (mMirrorWindowControl != null) { mMirrorWindowControl.showControl(); @@ -432,8 +446,8 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold } final int maxMirrorViewX = mDisplaySize.x - mMirrorView.getWidth(); final int maxMirrorViewY = mDisplaySize.y - mMirrorView.getHeight() - mNavGestureHeight; - WindowManager.LayoutParams params = - (WindowManager.LayoutParams) mMirrorView.getLayoutParams(); + LayoutParams params = + (LayoutParams) mMirrorView.getLayoutParams(); params.x = mMagnificationFrame.left - mMirrorSurfaceMargin; params.y = mMagnificationFrame.top - mMirrorSurfaceMargin; // If nav bar mode supports swipe-up gesture, the Y position of mirror view should not diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index ab4025f7ef9d..24ab6355c2bd 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -24,6 +24,9 @@ import android.graphics.PixelFormat; import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.PromptInfo; +import android.hardware.face.FaceSensorPropertiesInternal; +import android.hardware.fingerprint.FingerprintSensorProperties; +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.os.Binder; import android.os.Bundle; import android.os.Handler; @@ -51,6 +54,7 @@ import com.android.systemui.keyguard.WakefulnessLifecycle; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.List; /** * Top level container/controller for the BiometricPrompt UI. @@ -76,6 +80,8 @@ public class AuthContainerView extends LinearLayout final Config mConfig; final int mEffectiveUserId; + @Nullable private final List<FingerprintSensorPropertiesInternal> mFpProps; + @Nullable private final List<FaceSensorPropertiesInternal> mFaceProps; private final Handler mHandler; private final Injector mInjector; private final IBinder mWindowToken = new Binder(); @@ -111,7 +117,8 @@ public class AuthContainerView extends LinearLayout boolean mRequireConfirmation; int mUserId; String mOpPackageName; - @BiometricAuthenticator.Modality int mModalityMask; + int[] mSensorIds; + boolean mCredentialAllowed; boolean mSkipIntro; long mOperationId; } @@ -159,9 +166,12 @@ public class AuthContainerView extends LinearLayout return this; } - public AuthContainerView build(@BiometricAuthenticator.Modality int modalityMask) { - mConfig.mModalityMask = modalityMask; - return new AuthContainerView(mConfig, new Injector()); + public AuthContainerView build(int[] sensorIds, boolean credentialAllowed, + @Nullable List<FingerprintSensorPropertiesInternal> fpProps, + @Nullable List<FaceSensorPropertiesInternal> faceProps) { + mConfig.mSensorIds = sensorIds; + mConfig.mCredentialAllowed = credentialAllowed; + return new AuthContainerView(mConfig, new Injector(), fpProps, faceProps); } } @@ -242,11 +252,15 @@ public class AuthContainerView extends LinearLayout } @VisibleForTesting - AuthContainerView(Config config, Injector injector) { + AuthContainerView(Config config, Injector injector, + @Nullable List<FingerprintSensorPropertiesInternal> fpProps, + @Nullable List<FaceSensorPropertiesInternal> faceProps) { super(config.mContext); mConfig = config; mInjector = injector; + mFpProps = fpProps; + mFaceProps = faceProps; mEffectiveUserId = mInjector.getUserManager(mContext) .getCredentialOwnerProfile(mConfig.mUserId); @@ -269,24 +283,29 @@ public class AuthContainerView extends LinearLayout // Inflate biometric view only if necessary. if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) { - final @BiometricAuthenticator.Modality int biometricModality = - config.mModalityMask & ~BiometricAuthenticator.TYPE_CREDENTIAL; - - switch (biometricModality) { - case BiometricAuthenticator.TYPE_FINGERPRINT: + if (config.mSensorIds.length == 1) { + final int singleSensorAuthId = config.mSensorIds[0]; + if (Utils.containsSensorId(mFpProps, singleSensorAuthId)) { mBiometricView = (AuthBiometricFingerprintView) factory.inflate(R.layout.auth_biometric_fingerprint_view, null, false); - break; - case BiometricAuthenticator.TYPE_FACE: + } else if (Utils.containsSensorId(mFaceProps, singleSensorAuthId)) { mBiometricView = (AuthBiometricFaceView) factory.inflate(R.layout.auth_biometric_face_view, null, false); - break; - default: - Log.e(TAG, "Unsupported biometric modality: " + biometricModality); + } else { + // Unknown sensorId + Log.e(TAG, "Unknown sensorId: " + singleSensorAuthId); mBiometricView = null; mBackgroundView = null; mBiometricScrollView = null; return; + } + } else { + // The UI currently only supports authentication with a single sensor. + Log.e(TAG, "Unsupported sensor array, length: " + config.mSensorIds.length); + mBiometricView = null; + mBackgroundView = null; + mBiometricScrollView = null; + return; } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index b1ae56a584d2..a6b1b90317f9 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -29,12 +29,13 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; -import android.hardware.biometrics.BiometricAuthenticator; +import android.graphics.RectF; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.IBiometricSysuiReceiver; import android.hardware.biometrics.PromptInfo; import android.hardware.face.FaceManager; +import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.os.Bundle; @@ -42,6 +43,7 @@ import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.util.Log; +import android.view.MotionEvent; import android.view.WindowManager; import com.android.internal.R; @@ -54,6 +56,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.phone.KeyguardBouncer; +import java.util.ArrayList; import java.util.List; import javax.inject.Inject; @@ -74,8 +77,13 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, private final StatusBarStateController mStatusBarStateController; private final IActivityTaskManager mActivityTaskManager; @Nullable private final FingerprintManager mFingerprintManager; + @Nullable private final FaceManager mFaceManager; private final Provider<UdfpsController> mUdfpsControllerFactory; + @Nullable private final List<FingerprintSensorPropertiesInternal> mFpProps; + @Nullable private final List<FaceSensorPropertiesInternal> mFaceProps; + @Nullable private final List<FingerprintSensorPropertiesInternal> mUdfpsProps; + // TODO: These should just be saved from onSaveState private SomeArgs mCurrentDialogArgs; @VisibleForTesting @@ -239,16 +247,25 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, } /** + * @return where the UDFPS exists on the screen in pixels. + */ + public RectF getUdfpsRegion() { + return mUdfpsController == null ? null : mUdfpsController.getSensorLocation(); + } + + /** * Requests fingerprint scan. * * @param screenX X position of long press * @param screenY Y position of long press + * @param major length of the major axis. See {@link MotionEvent#AXIS_TOOL_MAJOR}. + * @param minor length of the minor axis. See {@link MotionEvent#AXIS_TOOL_MINOR}. */ - public void onAodInterrupt(int screenX, int screenY) { + public void onAodInterrupt(int screenX, int screenY, float major, float minor) { if (mUdfpsController == null) { return; } - mUdfpsController.onAodInterrupt(screenX, screenY); + mUdfpsController.onAodInterrupt(screenX, screenY, major, minor); } /** @@ -285,14 +302,30 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, StatusBarStateController statusBarStateController, IActivityTaskManager activityTaskManager, @Nullable FingerprintManager fingerprintManager, + @Nullable FaceManager faceManager, Provider<UdfpsController> udfpsControllerFactory) { super(context); mCommandQueue = commandQueue; mStatusBarStateController = statusBarStateController; mActivityTaskManager = activityTaskManager; mFingerprintManager = fingerprintManager; + mFaceManager = faceManager; mUdfpsControllerFactory = udfpsControllerFactory; + mFpProps = mFingerprintManager != null ? mFingerprintManager.getSensorPropertiesInternal() + : null; + mFaceProps = mFaceManager != null ? mFaceManager.getSensorPropertiesInternal() : null; + + List<FingerprintSensorPropertiesInternal> udfpsProps = new ArrayList<>(); + if (mFpProps != null) { + for (FingerprintSensorPropertiesInternal props : mFpProps) { + if (props.isAnyUdfpsType()) { + udfpsProps.add(props); + } + } + } + mUdfpsProps = !udfpsProps.isEmpty() ? udfpsProps : null; + IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); @@ -305,15 +338,9 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, mCommandQueue.addCallback(this); mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); - if (mFingerprintManager != null && mFingerprintManager.isHardwareDetected()) { - final List<FingerprintSensorPropertiesInternal> fingerprintSensorProperties = - mFingerprintManager.getSensorPropertiesInternal(); - for (FingerprintSensorPropertiesInternal props : fingerprintSensorProperties) { - if (props.isAnyUdfpsType()) { - mUdfpsController = mUdfpsControllerFactory.get(); - break; - } - } + if (mFingerprintManager != null && mFingerprintManager.isHardwareDetected() + && mUdfpsProps != null) { + mUdfpsController = mUdfpsControllerFactory.get(); } try { @@ -326,24 +353,30 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, @Override public void showAuthenticationDialog(PromptInfo promptInfo, IBiometricSysuiReceiver receiver, - @BiometricAuthenticator.Modality int biometricModality, boolean requireConfirmation, + int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation, int userId, String opPackageName, long operationId) { @Authenticators.Types final int authenticators = promptInfo.getAuthenticators(); if (DEBUG) { + StringBuilder ids = new StringBuilder(); + for (int sensorId : sensorIds) { + ids.append(sensorId).append(" "); + } Log.d(TAG, "showAuthenticationDialog, authenticators: " + authenticators - + ", biometricModality: " + biometricModality + + ", sensorIds: " + ids.toString() + + ", credentialAllowed: " + credentialAllowed + ", requireConfirmation: " + requireConfirmation + ", operationId: " + operationId); } SomeArgs args = SomeArgs.obtain(); args.arg1 = promptInfo; args.arg2 = receiver; - args.argi1 = biometricModality; - args.arg3 = requireConfirmation; - args.argi2 = userId; - args.arg4 = opPackageName; - args.arg5 = operationId; + args.arg3 = sensorIds; + args.arg4 = credentialAllowed; + args.arg5 = requireConfirmation; + args.argi1 = userId; + args.arg6 = opPackageName; + args.arg7 = operationId; boolean skipAnimation = false; if (mCurrentDialog != null) { @@ -456,27 +489,41 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, } } + /** + * Whether the passed userId has enrolled UDFPS. + */ + public boolean isUdfpsEnrolled(int userId) { + if (mUdfpsController == null) { + return false; + } + + return mFingerprintManager.hasEnrolledTemplatesForAnySensor(userId, mUdfpsProps); + } + private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) { mCurrentDialogArgs = args; - final @BiometricAuthenticator.Modality int type = args.argi1; + final PromptInfo promptInfo = (PromptInfo) args.arg1; - final boolean requireConfirmation = (boolean) args.arg3; - final int userId = args.argi2; - final String opPackageName = (String) args.arg4; - final long operationId = (long) args.arg5; + final int[] sensorIds = (int[]) args.arg3; + final boolean credentialAllowed = (boolean) args.arg4; + final boolean requireConfirmation = (boolean) args.arg5; + final int userId = args.argi1; + final String opPackageName = (String) args.arg6; + final long operationId = (long) args.arg7; // Create a new dialog but do not replace the current one yet. final AuthDialog newDialog = buildDialog( promptInfo, requireConfirmation, userId, - type, + sensorIds, + credentialAllowed, opPackageName, skipAnimation, operationId); if (newDialog == null) { - Log.e(TAG, "Unsupported type: " + type); + Log.e(TAG, "Unsupported type configuration"); return; } @@ -484,8 +531,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, Log.d(TAG, "userId: " + userId + " savedState: " + savedState + " mCurrentDialog: " + mCurrentDialog - + " newDialog: " + newDialog - + " type: " + type); + + " newDialog: " + newDialog); } if (mCurrentDialog != null) { @@ -541,7 +587,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, } protected AuthDialog buildDialog(PromptInfo promptInfo, boolean requireConfirmation, - int userId, @BiometricAuthenticator.Modality int type, String opPackageName, + int userId, int[] sensorIds, boolean credentialAllowed, String opPackageName, boolean skipIntro, long operationId) { return new AuthContainerView.Builder(mContext) .setCallback(this) @@ -551,6 +597,6 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, .setOpPackageName(opPackageName) .setSkipIntro(skipIntro) .setOperationId(operationId) - .build(type); + .build(sensorIds, credentialAllowed, mFpProps, mFaceProps); } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index e3b00495f3dc..a4b407d2785d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -25,6 +25,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.PixelFormat; import android.graphics.Point; +import android.graphics.RectF; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.IUdfpsOverlayController; @@ -78,7 +79,7 @@ class UdfpsController implements DozeReceiver { // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple // sensors, this, in addition to a lot of the code here, will be updated. @VisibleForTesting - final int mUdfpsSensorId; + final FingerprintSensorPropertiesInternal mSensorProps; private final WindowManager mWindowManager; private final SystemSettings mSystemSettings; private final DelayableExecutor mFgExecutor; @@ -180,19 +181,12 @@ class UdfpsController implements DozeReceiver { mFgExecutor = fgExecutor; mLayoutParams = createLayoutParams(context); - int udfpsSensorId = -1; - for (FingerprintSensorPropertiesInternal props : - mFingerprintManager.getSensorPropertiesInternal()) { - if (props.isAnyUdfpsType()) { - udfpsSensorId = props.sensorId; - break; - } - } + mSensorProps = findFirstUdfps(); // At least one UDFPS sensor exists - checkArgument(udfpsSensorId != -1); - mUdfpsSensorId = udfpsSensorId; + checkArgument(mSensorProps != null); mView = (UdfpsView) inflater.inflate(R.layout.udfps_view, null, false); + mView.setSensorProperties(mSensorProps); mHbmPath = resources.getString(R.string.udfps_hbm_sysfs_path); mHbmEnableCommand = resources.getString(R.string.udfps_hbm_enable_command); @@ -235,11 +229,29 @@ class UdfpsController implements DozeReceiver { mIsOverlayShowing = false; } + @Nullable + private FingerprintSensorPropertiesInternal findFirstUdfps() { + for (FingerprintSensorPropertiesInternal props : + mFingerprintManager.getSensorPropertiesInternal()) { + if (props.isAnyUdfpsType()) { + return props; + } + } + return null; + } + @Override public void dozeTimeTick() { mView.dozeTimeTick(); } + /** + * @return where the UDFPS exists on the screen in pixels. + */ + public RectF getSensorLocation() { + return mView.getSensorRect(); + } + private void setShowOverlay(boolean show) { if (show == mIsOverlayRequested) { return; @@ -333,7 +345,7 @@ class UdfpsController implements DozeReceiver { * This is intented to be called in response to a sensor that triggers an AOD interrupt for the * fingerprint sensor. */ - void onAodInterrupt(int screenX, int screenY) { + void onAodInterrupt(int screenX, int screenY, float major, float minor) { if (mIsAodInterruptActive) { return; } @@ -344,7 +356,7 @@ class UdfpsController implements DozeReceiver { mCancelAodTimeoutAction = mFgExecutor.executeDelayed(this::onCancelAodInterrupt, AOD_INTERRUPT_TIMEOUT_MILLIS); // using a hard-coded value for major and minor until it is available from the sensor - onFingerDown(screenX, screenY, 13.0f, 13.0f); + onFingerDown(screenX, screenY, minor, major); } /** @@ -374,7 +386,7 @@ class UdfpsController implements DozeReceiver { fw.write(mHbmEnableCommand); fw.close(); } - mFingerprintManager.onPointerDown(mUdfpsSensorId, x, y, minor, major); + mFingerprintManager.onPointerDown(mSensorProps.sensorId, x, y, minor, major); } catch (IOException e) { mView.hideScrimAndDot(); Log.e(TAG, "onFingerDown | failed to enable HBM: " + e.getMessage()); @@ -382,7 +394,7 @@ class UdfpsController implements DozeReceiver { } private void onFingerUp() { - mFingerprintManager.onPointerUp(mUdfpsSensorId); + mFingerprintManager.onPointerUp(mSensorProps.sensorId); // Hiding the scrim before disabling HBM results in less noticeable flicker. mView.hideScrimAndDot(); if (mHbmSupported) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java index d7e91384f049..7edcf66196e4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java @@ -18,6 +18,7 @@ package com.android.systemui.biometrics; import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; +import android.annotation.NonNull; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; @@ -25,6 +26,7 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; @@ -55,8 +57,6 @@ public class UdfpsView extends View implements DozeReceiver, private final RectF mSensorRect; private final Paint mSensorPaint; - private final float mSensorRadius; - private final float mSensorCenterY; private final float mSensorTouchAreaCoefficient; private final int mMaxBurnInOffsetX; private final int mMaxBurnInOffsetY; @@ -64,9 +64,7 @@ public class UdfpsView extends View implements DozeReceiver, private final Rect mTouchableRegion; private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener; - // This is calculated from the screen's dimensions at runtime, as opposed to mSensorCenterY, - // which is defined in layout.xml - private float mSensorCenterX; + @NonNull private FingerprintSensorPropertiesInternal mProps; // AOD anti-burn-in offsets private float mInterpolatedDarkAmount; @@ -83,18 +81,10 @@ public class UdfpsView extends View implements DozeReceiver, TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.UdfpsView, 0, 0); try { - if (!a.hasValue(R.styleable.UdfpsView_sensorRadius)) { - throw new IllegalArgumentException("UdfpsView must contain sensorRadius"); - } - if (!a.hasValue(R.styleable.UdfpsView_sensorCenterY)) { - throw new IllegalArgumentException("UdfpsView must contain sensorMarginBottom"); - } if (!a.hasValue(R.styleable.UdfpsView_sensorTouchAreaCoefficient)) { throw new IllegalArgumentException( "UdfpsView must contain sensorTouchAreaCoefficient"); } - mSensorRadius = a.getDimension(R.styleable.UdfpsView_sensorRadius, 0f); - mSensorCenterY = a.getDimension(R.styleable.UdfpsView_sensorCenterY, 0f); mSensorTouchAreaCoefficient = a.getFloat( R.styleable.UdfpsView_sensorTouchAreaCoefficient, 0f); } finally { @@ -134,6 +124,10 @@ public class UdfpsView extends View implements DozeReceiver, mIsScrimShowing = false; } + void setSensorProperties(@NonNull FingerprintSensorPropertiesInternal properties) { + mProps = properties; + } + @Override public void dozeTimeTick() { updateAodPosition(); @@ -165,9 +159,10 @@ public class UdfpsView extends View implements DozeReceiver, final int h = getLayoutParams().height; final int w = getLayoutParams().width; mScrimRect.set(0 /* left */, 0 /* top */, w, h); - mSensorCenterX = w / 2f; - mSensorRect.set(mSensorCenterX - mSensorRadius, mSensorCenterY - mSensorRadius, - mSensorCenterX + mSensorRadius, mSensorCenterY + mSensorRadius); + mSensorRect.set(mProps.sensorLocationX - mProps.sensorRadius, + mProps.sensorLocationY - mProps.sensorRadius, + mProps.sensorLocationX + mProps.sensorRadius, + mProps.sensorLocationY + mProps.sensorRadius); // Sets mTouchableRegion with rounded up values from mSensorRect. mSensorRect.roundOut(mTouchableRegion); @@ -201,6 +196,10 @@ public class UdfpsView extends View implements DozeReceiver, canvas.restore(); } + RectF getSensorRect() { + return new RectF(mSensorRect); + } + void setHbmSupported(boolean hbmSupported) { mHbmSupported = hbmSupported; } @@ -211,10 +210,10 @@ public class UdfpsView extends View implements DozeReceiver, } boolean isValidTouch(float x, float y, float pressure) { - return x > (mSensorCenterX - mSensorRadius * mSensorTouchAreaCoefficient) - && x < (mSensorCenterX + mSensorRadius * mSensorTouchAreaCoefficient) - && y > (mSensorCenterY - mSensorRadius * mSensorTouchAreaCoefficient) - && y < (mSensorCenterY + mSensorRadius * mSensorTouchAreaCoefficient); + return x > (mProps.sensorLocationX - mProps.sensorRadius * mSensorTouchAreaCoefficient) + && x < (mProps.sensorLocationX + mProps.sensorRadius * mSensorTouchAreaCoefficient) + && y > (mProps.sensorLocationY - mProps.sensorRadius * mSensorTouchAreaCoefficient) + && y < (mProps.sensorLocationY + mProps.sensorRadius * mSensorTouchAreaCoefficient); } void setScrimAlpha(int alpha) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java index 2e9afa58987a..fd5e85a953ad 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java @@ -20,9 +20,11 @@ import static android.hardware.biometrics.BiometricManager.Authenticators; import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE; import android.annotation.IntDef; +import android.annotation.Nullable; import android.app.admin.DevicePolicyManager; import android.content.Context; import android.hardware.biometrics.PromptInfo; +import android.hardware.biometrics.SensorPropertiesInternal; import android.os.UserManager; import android.util.DisplayMetrics; import android.view.ViewGroup; @@ -33,6 +35,7 @@ import com.android.internal.widget.LockPatternUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.List; public class Utils { @@ -98,4 +101,19 @@ public class Utils { final UserManager userManager = context.getSystemService(UserManager.class); return userManager.isManagedProfile(userId); } + + static boolean containsSensorId(@Nullable List<? extends SensorPropertiesInternal> properties, + int sensorId) { + if (properties == null) { + return false; + } + + for (SensorPropertiesInternal prop : properties) { + if (prop.sensorId == sensorId) { + return true; + } + } + + return false; + } } diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt index 83de3243602b..eea168ad16b3 100644 --- a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt +++ b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt @@ -112,7 +112,7 @@ open class BroadcastDispatcher constructor ( * @param executor An executor to dispatch [BroadcastReceiver.onReceive]. Pass null to use an * executor in the main thread (default). * @param user A user handle to determine which broadcast should be dispatched to this receiver. - * By default, it is the user of the context (system user in SystemUI). + * Pass `null` to use the user of the context (system user in SystemUI). * @throws IllegalArgumentException if the filter has other constraints that are not actions or * categories or the filter has no actions. */ @@ -120,13 +120,17 @@ open class BroadcastDispatcher constructor ( open fun registerReceiver( receiver: BroadcastReceiver, filter: IntentFilter, - executor: Executor? = context.mainExecutor, - user: UserHandle = context.user + executor: Executor? = null, + user: UserHandle? = null ) { checkFilter(filter) this.handler - .obtainMessage(MSG_ADD_RECEIVER, - ReceiverData(receiver, filter, executor ?: context.mainExecutor, user)) + .obtainMessage(MSG_ADD_RECEIVER, ReceiverData( + receiver, + filter, + executor ?: context.mainExecutor, + user ?: context.user + )) .sendToTarget() } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java deleted file mode 100644 index 9f7358bf94ff..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java +++ /dev/null @@ -1,317 +0,0 @@ -/* - * 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 android.annotation.Nullable; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.PaintFlagsDrawFilter; -import android.graphics.Path; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.util.PathParser; -import android.widget.ImageView; - -import com.android.launcher3.icons.DotRenderer; -import com.android.systemui.Interpolators; -import com.android.systemui.R; - -import java.util.EnumSet; - -import static android.graphics.Paint.DITHER_FLAG; -import static android.graphics.Paint.FILTER_BITMAP_FLAG; - -/** - * View that displays an adaptive icon with an app-badge and a dot. - * - * Dot = a small colored circle that indicates whether this bubble has an unread update. - * Badge = the icon associated with the app that created this bubble, this will show work profile - * badge if appropriate. - */ -public class BadgedImageView extends ImageView { - - /** Same value as Launcher3 dot code */ - public static final float WHITE_SCRIM_ALPHA = 0.54f; - /** Same as value in Launcher3 IconShape */ - public static final int DEFAULT_PATH_SIZE = 100; - /** Same as value in Launcher3 BaseIconFactory */ - private static final float ICON_BADGE_SCALE = 0.444f; - - /** - * Flags that suppress the visibility of the 'new' dot, for one reason or another. If any of - * these flags are set, the dot will not be shown even if {@link Bubble#showDot()} returns true. - */ - enum SuppressionFlag { - // Suppressed because the flyout is visible - it will morph into the dot via animation. - FLYOUT_VISIBLE, - // Suppressed because this bubble is behind others in the collapsed stack. - BEHIND_STACK, - } - - /** - * Start by suppressing the dot because the flyout is visible - most bubbles are added with a - * flyout, so this is a reasonable default. - */ - private final EnumSet<SuppressionFlag> mDotSuppressionFlags = - EnumSet.of(SuppressionFlag.FLYOUT_VISIBLE); - - private float mDotScale = 0f; - private float mAnimatingToDotScale = 0f; - private boolean mDotIsAnimating = false; - - private BubbleViewProvider mBubble; - - private int mBubbleBitmapSize; - private int mBubbleSize; - private DotRenderer mDotRenderer; - private DotRenderer.DrawParams mDrawParams; - private boolean mOnLeft; - - private int mDotColor; - - private Rect mTempBounds = new Rect(); - - public BadgedImageView(Context context) { - this(context, null); - } - - public BadgedImageView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - mBubbleBitmapSize = getResources().getDimensionPixelSize(R.dimen.bubble_bitmap_size); - mBubbleSize = getResources().getDimensionPixelSize(R.dimen.individual_bubble_size); - mDrawParams = new DotRenderer.DrawParams(); - - Path iconPath = PathParser.createPathFromPathData( - getResources().getString(com.android.internal.R.string.config_icon_mask)); - mDotRenderer = new DotRenderer(mBubbleBitmapSize, iconPath, DEFAULT_PATH_SIZE); - - setFocusable(true); - setClickable(true); - } - - /** - * Updates the view with provided info. - */ - public void setRenderedBubble(BubbleViewProvider bubble) { - mBubble = bubble; - showBadge(); - mDotColor = bubble.getDotColor(); - drawDot(bubble.getDotPath()); - } - - @Override - public void onDraw(Canvas canvas) { - super.onDraw(canvas); - - if (!shouldDrawDot()) { - return; - } - - getDrawingRect(mTempBounds); - - mDrawParams.color = mDotColor; - mDrawParams.iconBounds = mTempBounds; - mDrawParams.leftAlign = mOnLeft; - mDrawParams.scale = mDotScale; - - mDotRenderer.draw(canvas, mDrawParams); - } - - /** Adds a dot suppression flag, updating dot visibility if needed. */ - void addDotSuppressionFlag(SuppressionFlag flag) { - if (mDotSuppressionFlags.add(flag)) { - // Update dot visibility, and animate out if we're now behind the stack. - updateDotVisibility(flag == SuppressionFlag.BEHIND_STACK /* animate */); - } - } - - /** Removes a dot suppression flag, updating dot visibility if needed. */ - void removeDotSuppressionFlag(SuppressionFlag flag) { - if (mDotSuppressionFlags.remove(flag)) { - // Update dot visibility, animating if we're no longer behind the stack. - updateDotVisibility(flag == SuppressionFlag.BEHIND_STACK); - } - } - - /** Updates the visibility of the dot, animating if requested. */ - void updateDotVisibility(boolean animate) { - final float targetScale = shouldDrawDot() ? 1f : 0f; - - if (animate) { - animateDotScale(targetScale, null /* after */); - } else { - mDotScale = targetScale; - mAnimatingToDotScale = targetScale; - invalidate(); - } - } - - /** - * @param iconPath The new icon path to use when calculating dot position. - */ - void drawDot(Path iconPath) { - mDotRenderer = new DotRenderer(mBubbleBitmapSize, iconPath, DEFAULT_PATH_SIZE); - invalidate(); - } - - /** - * How big the dot should be, fraction from 0 to 1. - */ - void setDotScale(float fraction) { - mDotScale = fraction; - invalidate(); - } - - /** - * Whether decorations (badges or dots) are on the left. - */ - boolean getDotOnLeft() { - return mOnLeft; - } - - /** - * Return dot position relative to bubble view container bounds. - */ - float[] getDotCenter() { - float[] dotPosition; - if (mOnLeft) { - dotPosition = mDotRenderer.getLeftDotPosition(); - } else { - dotPosition = mDotRenderer.getRightDotPosition(); - } - getDrawingRect(mTempBounds); - float dotCenterX = mTempBounds.width() * dotPosition[0]; - float dotCenterY = mTempBounds.height() * dotPosition[1]; - return new float[]{dotCenterX, dotCenterY}; - } - - /** - * The key for the {@link Bubble} associated with this view, if one exists. - */ - @Nullable - public String getKey() { - return (mBubble != null) ? mBubble.getKey() : null; - } - - int getDotColor() { - return mDotColor; - } - - /** Sets the position of the dot and badge, animating them out and back in if requested. */ - void animateDotBadgePositions(boolean onLeft) { - mOnLeft = onLeft; - - if (onLeft != getDotOnLeft() && shouldDrawDot()) { - animateDotScale(0f /* showDot */, () -> { - invalidate(); - animateDotScale(1.0f, null /* after */); - }); - } - // TODO animate badge - showBadge(); - - } - - /** Sets the position of the dot and badge. */ - void setDotBadgeOnLeft(boolean onLeft) { - mOnLeft = onLeft; - invalidate(); - showBadge(); - } - - - /** Whether to draw the dot in onDraw(). */ - private boolean shouldDrawDot() { - // Always render the dot if it's animating, since it could be animating out. Otherwise, show - // it if the bubble wants to show it, and we aren't suppressing it. - return mDotIsAnimating || (mBubble.showDot() && mDotSuppressionFlags.isEmpty()); - } - - /** - * Animates the dot to the given scale, running the optional callback when the animation ends. - */ - private void animateDotScale(float toScale, @Nullable Runnable after) { - mDotIsAnimating = true; - - // Don't restart the animation if we're already animating to the given value. - if (mAnimatingToDotScale == toScale || !shouldDrawDot()) { - mDotIsAnimating = false; - return; - } - - mAnimatingToDotScale = toScale; - - final boolean showDot = toScale > 0f; - - // Do NOT wait until after animation ends to setShowDot - // to avoid overriding more recent showDot states. - clearAnimation(); - animate() - .setDuration(200) - .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) - .setUpdateListener((valueAnimator) -> { - float fraction = valueAnimator.getAnimatedFraction(); - fraction = showDot ? fraction : 1f - fraction; - setDotScale(fraction); - }).withEndAction(() -> { - setDotScale(showDot ? 1f : 0f); - mDotIsAnimating = false; - if (after != null) { - after.run(); - } - }).start(); - } - - void showBadge() { - Drawable badge = mBubble.getAppBadge(); - if (badge == null) { - setImageBitmap(mBubble.getBubbleIcon()); - return; - } - Canvas bubbleCanvas = new Canvas(); - Bitmap noBadgeBubble = mBubble.getBubbleIcon(); - Bitmap bubble = noBadgeBubble.copy(noBadgeBubble.getConfig(), /* isMutable */ true); - - bubbleCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG)); - bubbleCanvas.setBitmap(bubble); - - final int badgeSize = (int) (ICON_BADGE_SCALE * mBubbleSize); - if (mOnLeft) { - badge.setBounds(0, mBubbleSize - badgeSize, badgeSize, mBubbleSize); - } else { - badge.setBounds(mBubbleSize - badgeSize, mBubbleSize - badgeSize, - mBubbleSize, mBubbleSize); - } - badge.draw(bubbleCanvas); - bubbleCanvas.setBitmap(null); - setImageBitmap(bubble); - } - - void hideBadge() { - setImageBitmap(mBubble.getBubbleIcon()); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java deleted file mode 100644 index 57d8dc70cd88..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java +++ /dev/null @@ -1,812 +0,0 @@ -/* - * Copyright (C) 2019 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.app.ActivityTaskManager.INVALID_TASK_ID; -import static android.os.AsyncTask.Status.FINISHED; - -import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; - -import android.annotation.DimenRes; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.Notification; -import android.app.PendingIntent; -import android.app.Person; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.ShortcutInfo; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Path; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.os.Parcelable; -import android.os.UserHandle; -import android.provider.Settings; -import android.text.TextUtils; -import android.util.Log; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.InstanceId; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.List; -import java.util.Objects; - -/** - * Encapsulates the data and UI elements of a bubble. - */ -class Bubble implements BubbleViewProvider { - private static final String TAG = "Bubble"; - - private final String mKey; - - private long mLastUpdated; - private long mLastAccessed; - - @Nullable - private BubbleController.NotificationSuppressionChangedListener mSuppressionListener; - - /** Whether the bubble should show a dot for the notification indicating updated content. */ - private boolean mShowBubbleUpdateDot = true; - - /** Whether flyout text should be suppressed, regardless of any other flags or state. */ - private boolean mSuppressFlyout; - - // Items that are typically loaded later - private String mAppName; - private ShortcutInfo mShortcutInfo; - private String mMetadataShortcutId; - private BadgedImageView mIconView; - private BubbleExpandedView mExpandedView; - - private BubbleViewInfoTask mInflationTask; - private boolean mInflateSynchronously; - private boolean mPendingIntentCanceled; - private boolean mIsImportantConversation; - - /** - * Presentational info about the flyout. - */ - public static class FlyoutMessage { - @Nullable public Icon senderIcon; - @Nullable public Drawable senderAvatar; - @Nullable public CharSequence senderName; - @Nullable public CharSequence message; - @Nullable public boolean isGroupChat; - } - - private FlyoutMessage mFlyoutMessage; - private Drawable mBadgeDrawable; - // Bitmap with no badge, no dot - private Bitmap mBubbleBitmap; - private int mDotColor; - private Path mDotPath; - private int mFlags; - - @NonNull - private UserHandle mUser; - @NonNull - private String mPackageName; - @Nullable - private String mTitle; - @Nullable - private Icon mIcon; - private boolean mIsBubble; - private boolean mIsVisuallyInterruptive; - private boolean mIsClearable; - private boolean mShouldSuppressNotificationDot; - private boolean mShouldSuppressNotificationList; - private boolean mShouldSuppressPeek; - private int mDesiredHeight; - @DimenRes - private int mDesiredHeightResId; - - /** for logging **/ - @Nullable - private InstanceId mInstanceId; - @Nullable - private String mChannelId; - private int mNotificationId; - private int mAppUid = -1; - - /** - * A bubble is created and can be updated. This intent is updated until the user first - * expands the bubble. Once the user has expanded the contents, we ignore the intent updates - * to prevent restarting the intent & possibly altering UI state in the activity in front of - * the user. - * - * Once the bubble is overflowed, the activity is finished and updates to the - * notification are respected. Typically an update to an overflowed bubble would result in - * that bubble being added back to the stack anyways. - */ - @Nullable - private PendingIntent mIntent; - private boolean mIntentActive; - @Nullable - private PendingIntent.CancelListener mIntentCancelListener; - - /** - * Sent when the bubble & notification are no longer visible to the user (i.e. no - * notification in the shade, no bubble in the stack or overflow). - */ - @Nullable - private PendingIntent mDeleteIntent; - - /** - * Create a bubble with limited information based on given {@link ShortcutInfo}. - * Note: Currently this is only being used when the bubble is persisted to disk. - */ - Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo, - final int desiredHeight, final int desiredHeightResId, @Nullable final String title) { - Objects.requireNonNull(key); - Objects.requireNonNull(shortcutInfo); - mMetadataShortcutId = shortcutInfo.getId(); - mShortcutInfo = shortcutInfo; - mKey = key; - mFlags = 0; - mUser = shortcutInfo.getUserHandle(); - mPackageName = shortcutInfo.getPackage(); - mIcon = shortcutInfo.getIcon(); - mDesiredHeight = desiredHeight; - mDesiredHeightResId = desiredHeightResId; - mTitle = title; - mShowBubbleUpdateDot = false; - } - - @VisibleForTesting(visibility = PRIVATE) - Bubble(@NonNull final BubbleEntry entry, - @Nullable final BubbleController.NotificationSuppressionChangedListener listener, - final BubbleController.PendingIntentCanceledListener intentCancelListener) { - mKey = entry.getKey(); - mSuppressionListener = listener; - mIntentCancelListener = intent -> { - if (mIntent != null) { - mIntent.unregisterCancelListener(mIntentCancelListener); - } - intentCancelListener.onPendingIntentCanceled(this); - }; - setEntry(entry); - } - - @Override - public String getKey() { - return mKey; - } - - public UserHandle getUser() { - return mUser; - } - - @NonNull - public String getPackageName() { - return mPackageName; - } - - @Override - public Bitmap getBubbleIcon() { - return mBubbleBitmap; - } - - @Override - public Drawable getAppBadge() { - return mBadgeDrawable; - } - - @Override - public int getDotColor() { - return mDotColor; - } - - @Override - public Path getDotPath() { - return mDotPath; - } - - @Nullable - public String getAppName() { - return mAppName; - } - - @Nullable - public ShortcutInfo getShortcutInfo() { - return mShortcutInfo; - } - - @Nullable - @Override - public BadgedImageView getIconView() { - return mIconView; - } - - @Override - @Nullable - public BubbleExpandedView getExpandedView() { - return mExpandedView; - } - - @Nullable - public String getTitle() { - return mTitle; - } - - String getMetadataShortcutId() { - return mMetadataShortcutId; - } - - boolean hasMetadataShortcutId() { - return (mMetadataShortcutId != null && !mMetadataShortcutId.isEmpty()); - } - - /** - * Call this to clean up the task for the bubble. Ensure this is always called when done with - * the bubble. - */ - void cleanupExpandedView() { - if (mExpandedView != null) { - mExpandedView.cleanUpExpandedState(); - mExpandedView = null; - } - if (mIntent != null) { - mIntent.unregisterCancelListener(mIntentCancelListener); - } - mIntentActive = false; - } - - /** - * Call when all the views should be removed/cleaned up. - */ - void cleanupViews() { - cleanupExpandedView(); - mIconView = null; - } - - void setPendingIntentCanceled() { - mPendingIntentCanceled = true; - } - - boolean getPendingIntentCanceled() { - return mPendingIntentCanceled; - } - - /** - * Sets whether to perform inflation on the same thread as the caller. This method should only - * be used in tests, not in production. - */ - @VisibleForTesting - void setInflateSynchronously(boolean inflateSynchronously) { - mInflateSynchronously = inflateSynchronously; - } - - /** - * Sets whether this bubble is considered visually interruptive. This method is purely for - * testing. - */ - @VisibleForTesting - void setVisuallyInterruptiveForTest(boolean visuallyInterruptive) { - mIsVisuallyInterruptive = visuallyInterruptive; - } - - /** - * Starts a task to inflate & load any necessary information to display a bubble. - * - * @param callback the callback to notify one the bubble is ready to be displayed. - * @param context the context for the bubble. - * @param stackView the stackView the bubble is eventually added to. - * @param iconFactory the iconfactory use to create badged images for the bubble. - */ - void inflate(BubbleViewInfoTask.Callback callback, - Context context, - BubbleStackView stackView, - BubbleIconFactory iconFactory, - boolean skipInflation) { - if (isBubbleLoading()) { - mInflationTask.cancel(true /* mayInterruptIfRunning */); - } - mInflationTask = new BubbleViewInfoTask(this, - context, - stackView, - iconFactory, - skipInflation, - callback); - if (mInflateSynchronously) { - mInflationTask.onPostExecute(mInflationTask.doInBackground()); - } else { - mInflationTask.execute(); - } - } - - private boolean isBubbleLoading() { - return mInflationTask != null && mInflationTask.getStatus() != FINISHED; - } - - boolean isInflated() { - return mIconView != null && mExpandedView != null; - } - - void stopInflation() { - if (mInflationTask == null) { - return; - } - mInflationTask.cancel(true /* mayInterruptIfRunning */); - } - - void setViewInfo(BubbleViewInfoTask.BubbleViewInfo info) { - if (!isInflated()) { - mIconView = info.imageView; - mExpandedView = info.expandedView; - } - - mShortcutInfo = info.shortcutInfo; - mAppName = info.appName; - mFlyoutMessage = info.flyoutMessage; - - mBadgeDrawable = info.badgeDrawable; - mBubbleBitmap = info.bubbleBitmap; - - mDotColor = info.dotColor; - mDotPath = info.dotPath; - - if (mExpandedView != null) { - mExpandedView.update(this /* bubble */); - } - if (mIconView != null) { - mIconView.setRenderedBubble(this /* bubble */); - } - } - - /** - * Set visibility of bubble in the expanded state. - * - * @param visibility {@code true} if the expanded bubble should be visible on the screen. - * - * Note that this contents visibility doesn't affect visibility at {@link android.view.View}, - * and setting {@code false} actually means rendering the expanded view in transparent. - */ - @Override - public void setContentVisibility(boolean visibility) { - if (mExpandedView != null) { - mExpandedView.setContentVisibility(visibility); - } - } - - /** - * Sets the entry associated with this bubble. - */ - void setEntry(@NonNull final BubbleEntry entry) { - Objects.requireNonNull(entry); - mLastUpdated = entry.getStatusBarNotification().getPostTime(); - mIsBubble = entry.getStatusBarNotification().getNotification().isBubbleNotification(); - mPackageName = entry.getStatusBarNotification().getPackageName(); - mUser = entry.getStatusBarNotification().getUser(); - mTitle = getTitle(entry); - mChannelId = entry.getStatusBarNotification().getNotification().getChannelId(); - mNotificationId = entry.getStatusBarNotification().getId(); - mAppUid = entry.getStatusBarNotification().getUid(); - mInstanceId = entry.getStatusBarNotification().getInstanceId(); - mFlyoutMessage = extractFlyoutMessage(entry); - if (entry.getRanking() != null) { - mShortcutInfo = entry.getRanking().getShortcutInfo(); - mIsVisuallyInterruptive = entry.getRanking().visuallyInterruptive(); - if (entry.getRanking().getChannel() != null) { - mIsImportantConversation = - entry.getRanking().getChannel().isImportantConversation(); - } - } - if (entry.getBubbleMetadata() != null) { - mMetadataShortcutId = entry.getBubbleMetadata().getShortcutId(); - mFlags = entry.getBubbleMetadata().getFlags(); - mDesiredHeight = entry.getBubbleMetadata().getDesiredHeight(); - mDesiredHeightResId = entry.getBubbleMetadata().getDesiredHeightResId(); - mIcon = entry.getBubbleMetadata().getIcon(); - - if (!mIntentActive || mIntent == null) { - if (mIntent != null) { - mIntent.unregisterCancelListener(mIntentCancelListener); - } - mIntent = entry.getBubbleMetadata().getIntent(); - if (mIntent != null) { - mIntent.registerCancelListener(mIntentCancelListener); - } - } else if (mIntent != null && entry.getBubbleMetadata().getIntent() == null) { - // Was an intent bubble now it's a shortcut bubble... still unregister the listener - mIntent.unregisterCancelListener(mIntentCancelListener); - mIntentActive = false; - mIntent = null; - } - mDeleteIntent = entry.getBubbleMetadata().getDeleteIntent(); - } - - mIsClearable = entry.isClearable(); - mShouldSuppressNotificationDot = entry.shouldSuppressNotificationDot(); - mShouldSuppressNotificationList = entry.shouldSuppressNotificationList(); - mShouldSuppressPeek = entry.shouldSuppressPeek(); - } - - @Nullable - Icon getIcon() { - return mIcon; - } - - boolean isVisuallyInterruptive() { - return mIsVisuallyInterruptive; - } - - /** - * @return the last time this bubble was updated or accessed, whichever is most recent. - */ - long getLastActivity() { - return Math.max(mLastUpdated, mLastAccessed); - } - - /** - * Sets if the intent used for this bubble is currently active (i.e. populating an - * expanded view, expanded or not). - */ - void setIntentActive() { - mIntentActive = true; - } - - boolean isIntentActive() { - return mIntentActive; - } - - public InstanceId getInstanceId() { - return mInstanceId; - } - - @Nullable - public String getChannelId() { - return mChannelId; - } - - public int getNotificationId() { - return mNotificationId; - } - - /** - * @return the task id of the task in which bubble contents is drawn. - */ - @Override - public int getTaskId() { - return mExpandedView != null ? mExpandedView.getTaskId() : INVALID_TASK_ID; - } - - /** - * Should be invoked whenever a Bubble is accessed (selected while expanded). - */ - void markAsAccessedAt(long lastAccessedMillis) { - mLastAccessed = lastAccessedMillis; - setSuppressNotification(true); - setShowDot(false /* show */); - } - - /** - * Should be invoked whenever a Bubble is promoted from overflow. - */ - void markUpdatedAt(long lastAccessedMillis) { - mLastUpdated = lastAccessedMillis; - } - - /** - * Whether this notification should be shown in the shade. - */ - boolean showInShade() { - return !shouldSuppressNotification() || !mIsClearable; - } - - /** - * Whether this notification conversation is important. - */ - boolean isImportantConversation() { - return mIsImportantConversation; - } - - /** - * Sets whether this notification should be suppressed in the shade. - */ - void setSuppressNotification(boolean suppressNotification) { - boolean prevShowInShade = showInShade(); - if (suppressNotification) { - mFlags |= Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; - } else { - mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; - } - - if (showInShade() != prevShowInShade && mSuppressionListener != null) { - mSuppressionListener.onBubbleNotificationSuppressionChange(this); - } - } - - /** - * Sets whether the bubble for this notification should show a dot indicating updated content. - */ - void setShowDot(boolean showDot) { - mShowBubbleUpdateDot = showDot; - - if (mIconView != null) { - mIconView.updateDotVisibility(true /* animate */); - } - } - - /** - * Whether the bubble for this notification should show a dot indicating updated content. - */ - @Override - public boolean showDot() { - return mShowBubbleUpdateDot - && !mShouldSuppressNotificationDot - && !shouldSuppressNotification(); - } - - /** - * Whether the flyout for the bubble should be shown. - */ - boolean showFlyout() { - return !mSuppressFlyout && !mShouldSuppressPeek - && !shouldSuppressNotification() - && !mShouldSuppressNotificationList; - } - - /** - * Set whether the flyout text for the bubble should be shown when an update is received. - * - * @param suppressFlyout whether the flyout text is shown - */ - void setSuppressFlyout(boolean suppressFlyout) { - mSuppressFlyout = suppressFlyout; - } - - FlyoutMessage getFlyoutMessage() { - return mFlyoutMessage; - } - - int getRawDesiredHeight() { - return mDesiredHeight; - } - - int getRawDesiredHeightResId() { - return mDesiredHeightResId; - } - - float getDesiredHeight(Context context) { - boolean useRes = mDesiredHeightResId != 0; - if (useRes) { - return getDimenForPackageUser(context, mDesiredHeightResId, mPackageName, - mUser.getIdentifier()); - } else { - return mDesiredHeight * context.getResources().getDisplayMetrics().density; - } - } - - String getDesiredHeightString() { - boolean useRes = mDesiredHeightResId != 0; - if (useRes) { - return String.valueOf(mDesiredHeightResId); - } else { - return String.valueOf(mDesiredHeight); - } - } - - @Nullable - PendingIntent getBubbleIntent() { - return mIntent; - } - - @Nullable - PendingIntent getDeleteIntent() { - return mDeleteIntent; - } - - Intent getSettingsIntent(final Context context) { - final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS); - intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName()); - final int uid = getUid(context); - if (uid != -1) { - intent.putExtra(Settings.EXTRA_APP_UID, uid); - } - intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - return intent; - } - - public int getAppUid() { - return mAppUid; - } - - private int getUid(final Context context) { - if (mAppUid != -1) return mAppUid; - final PackageManager pm = context.getPackageManager(); - if (pm == null) return -1; - try { - final ApplicationInfo info = pm.getApplicationInfo(mShortcutInfo.getPackage(), 0); - return info.uid; - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "cannot find uid", e); - } - return -1; - } - - private int getDimenForPackageUser(Context context, int resId, String pkg, int userId) { - PackageManager pm = context.getPackageManager(); - Resources r; - if (pkg != null) { - try { - if (userId == UserHandle.USER_ALL) { - userId = UserHandle.USER_SYSTEM; - } - r = pm.getResourcesForApplicationAsUser(pkg, userId); - return r.getDimensionPixelSize(resId); - } catch (PackageManager.NameNotFoundException ex) { - // Uninstalled, don't care - } catch (Resources.NotFoundException e) { - // Invalid res id, return 0 and user our default - Log.e(TAG, "Couldn't find desired height res id", e); - } - } - return 0; - } - - private boolean shouldSuppressNotification() { - return isEnabled(Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION); - } - - public boolean shouldAutoExpand() { - return isEnabled(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); - } - - void setShouldAutoExpand(boolean shouldAutoExpand) { - if (shouldAutoExpand) { - enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); - } else { - disable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); - } - } - - public void setIsBubble(final boolean isBubble) { - mIsBubble = isBubble; - } - - public boolean isBubble() { - return mIsBubble; - } - - public void enable(int option) { - mFlags |= option; - } - - public void disable(int option) { - mFlags &= ~option; - } - - public boolean isEnabled(int option) { - return (mFlags & option) != 0; - } - - @Override - public String toString() { - return "Bubble{" + mKey + '}'; - } - - /** - * Description of current bubble state. - */ - public void dump( - @NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { - pw.print("key: "); pw.println(mKey); - pw.print(" showInShade: "); pw.println(showInShade()); - pw.print(" showDot: "); pw.println(showDot()); - pw.print(" showFlyout: "); pw.println(showFlyout()); - pw.print(" lastActivity: "); pw.println(getLastActivity()); - pw.print(" desiredHeight: "); pw.println(getDesiredHeightString()); - pw.print(" suppressNotif: "); pw.println(shouldSuppressNotification()); - pw.print(" autoExpand: "); pw.println(shouldAutoExpand()); - if (mExpandedView != null) { - mExpandedView.dump(fd, pw, args); - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Bubble)) return false; - Bubble bubble = (Bubble) o; - return Objects.equals(mKey, bubble.mKey); - } - - @Override - public int hashCode() { - return Objects.hash(mKey); - } - - @Nullable - private static String getTitle(@NonNull final BubbleEntry e) { - final CharSequence titleCharSeq = e.getStatusBarNotification() - .getNotification().extras.getCharSequence(Notification.EXTRA_TITLE); - return titleCharSeq == null ? null : titleCharSeq.toString(); - } - - /** - * Returns our best guess for the most relevant text summary of the latest update to this - * notification, based on its type. Returns null if there should not be an update message. - */ - @NonNull - static Bubble.FlyoutMessage extractFlyoutMessage(BubbleEntry entry) { - Objects.requireNonNull(entry); - final Notification underlyingNotif = entry.getStatusBarNotification().getNotification(); - final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle(); - - Bubble.FlyoutMessage bubbleMessage = new Bubble.FlyoutMessage(); - bubbleMessage.isGroupChat = underlyingNotif.extras.getBoolean( - Notification.EXTRA_IS_GROUP_CONVERSATION); - try { - if (Notification.BigTextStyle.class.equals(style)) { - // Return the big text, it is big so probably important. If it's not there use the - // normal text. - CharSequence bigText = - underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT); - bubbleMessage.message = !TextUtils.isEmpty(bigText) - ? bigText - : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); - return bubbleMessage; - } else if (Notification.MessagingStyle.class.equals(style)) { - final List<Notification.MessagingStyle.Message> messages = - Notification.MessagingStyle.Message.getMessagesFromBundleArray( - (Parcelable[]) underlyingNotif.extras.get( - Notification.EXTRA_MESSAGES)); - - final Notification.MessagingStyle.Message latestMessage = - Notification.MessagingStyle.findLatestIncomingMessage(messages); - if (latestMessage != null) { - bubbleMessage.message = latestMessage.getText(); - Person sender = latestMessage.getSenderPerson(); - bubbleMessage.senderName = sender != null ? sender.getName() : null; - bubbleMessage.senderAvatar = null; - bubbleMessage.senderIcon = sender != null ? sender.getIcon() : null; - return bubbleMessage; - } - } else if (Notification.InboxStyle.class.equals(style)) { - CharSequence[] lines = - underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES); - - // Return the last line since it should be the most recent. - if (lines != null && lines.length > 0) { - bubbleMessage.message = lines[lines.length - 1]; - return bubbleMessage; - } - } else if (Notification.MediaStyle.class.equals(style)) { - // Return nothing, media updates aren't typically useful as a text update. - return bubbleMessage; - } else { - // Default to text extra. - bubbleMessage.message = - underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); - return bubbleMessage; - } - } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) { - // No use crashing, we'll just return null and the caller will assume there's no update - // message. - e.printStackTrace(); - } - - return bubbleMessage; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java deleted file mode 100644 index 3f94b00d3c60..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +++ /dev/null @@ -1,1713 +0,0 @@ -/* - * 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.app.ActivityTaskManager.INVALID_TASK_ID; -import static android.app.Notification.FLAG_BUBBLE; -import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE; -import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED; -import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; -import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL; -import static android.service.notification.NotificationListenerService.REASON_CANCEL; -import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL; -import static android.service.notification.NotificationListenerService.REASON_CLICK; -import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED; -import static android.service.notification.NotificationStats.DISMISSAL_BUBBLE; -import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL; -import static android.view.View.INVISIBLE; -import static android.view.View.VISIBLE; -import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; - -import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; -import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; -import static com.android.systemui.statusbar.StatusBarState.SHADE; -import static com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.LOCAL_VARIABLE; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import android.annotation.NonNull; -import android.annotation.UserIdInt; -import android.app.ActivityManager.RunningTaskInfo; -import android.app.ActivityTaskManager; -import android.app.INotificationManager; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.pm.ActivityInfo; -import android.content.pm.LauncherApps; -import android.content.pm.PackageManager; -import android.content.pm.ShortcutInfo; -import android.content.res.Configuration; -import android.graphics.PixelFormat; -import android.os.Binder; -import android.os.Handler; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.UserHandle; -import android.service.notification.NotificationListenerService; -import android.service.notification.NotificationListenerService.RankingMap; -import android.service.notification.ZenModeConfig; -import android.util.ArraySet; -import android.util.Log; -import android.util.Pair; -import android.util.SparseSetArray; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; - -import androidx.annotation.IntDef; -import androidx.annotation.MainThread; -import androidx.annotation.Nullable; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.UiEventLogger; -import com.android.internal.statusbar.IStatusBarService; -import com.android.internal.statusbar.NotificationVisibility; -import com.android.systemui.Dumpable; -import com.android.systemui.bubbles.dagger.BubbleModule; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.model.SysUiState; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.shared.system.QuickStepContract; -import com.android.systemui.shared.system.TaskStackChangeListener; -import com.android.systemui.shared.system.TaskStackChangeListeners; -import com.android.systemui.statusbar.FeatureFlags; -import com.android.systemui.statusbar.NotificationLockscreenUserManager; -import com.android.systemui.statusbar.NotificationRemoveInterceptor; -import com.android.systemui.statusbar.NotificationShadeWindowController; -import com.android.systemui.statusbar.ScrimView; -import com.android.systemui.statusbar.notification.NotificationChannelHelper; -import com.android.systemui.statusbar.notification.NotificationEntryListener; -import com.android.systemui.statusbar.notification.NotificationEntryManager; -import com.android.systemui.statusbar.notification.collection.NotifCollection; -import com.android.systemui.statusbar.notification.collection.NotifPipeline; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.coordinator.BubbleCoordinator; -import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy; -import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats; -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; -import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; -import com.android.systemui.statusbar.notification.logging.NotificationLogger; -import com.android.systemui.statusbar.phone.ScrimController; -import com.android.systemui.statusbar.phone.ShadeController; -import com.android.systemui.statusbar.phone.StatusBar; -import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.statusbar.policy.ZenModeController; -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.WindowManagerShellWrapper; -import com.android.wm.shell.common.FloatingContentCoordinator; -import com.android.wm.shell.pip.PinnedStackListenerForwarder; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** - * 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 implements Bubbles, ConfigurationController.ConfigurationListener, - Dumpable { - - private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; - - @Retention(SOURCE) - @IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED, - DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE, - DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT, - DISMISS_OVERFLOW_MAX_REACHED, DISMISS_SHORTCUT_REMOVED, DISMISS_PACKAGE_REMOVED, - DISMISS_NO_BUBBLE_UP}) - @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) - @interface DismissReason {} - - static final int DISMISS_USER_GESTURE = 1; - static final int DISMISS_AGED = 2; - static final int DISMISS_TASK_FINISHED = 3; - static final int DISMISS_BLOCKED = 4; - static final int DISMISS_NOTIF_CANCEL = 5; - static final int DISMISS_ACCESSIBILITY_ACTION = 6; - static final int DISMISS_NO_LONGER_BUBBLE = 7; - static final int DISMISS_USER_CHANGED = 8; - static final int DISMISS_GROUP_CANCELLED = 9; - static final int DISMISS_INVALID_INTENT = 10; - static final int DISMISS_OVERFLOW_MAX_REACHED = 11; - static final int DISMISS_SHORTCUT_REMOVED = 12; - static final int DISMISS_PACKAGE_REMOVED = 13; - static final int DISMISS_NO_BUBBLE_UP = 14; - - private final Context mContext; - private final NotificationEntryManager mNotificationEntryManager; - private final NotifPipeline mNotifPipeline; - private final BubbleTaskStackListener mTaskStackListener; - private BubbleExpandListener mExpandListener; - @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer; - private final NotificationGroupManagerLegacy mNotificationGroupManager; - private final ShadeController mShadeController; - private final FloatingContentCoordinator mFloatingContentCoordinator; - private final BubbleDataRepository mDataRepository; - private BubbleLogger mLogger; - private final Handler mMainHandler; - private BubbleData mBubbleData; - private ScrimView mBubbleScrim; - @Nullable private BubbleStackView mStackView; - private BubbleIconFactory mBubbleIconFactory; - - /** - * The relative position of the stack when we removed it and nulled it out. If the stack is - * re-created, it will re-appear at this position. - */ - @Nullable private BubbleStackView.RelativeStackPosition mPositionFromRemovedStack; - - // Tracks the id of the current (foreground) user. - private int mCurrentUserId; - // Saves notification keys of active bubbles when users are switched. - private final SparseSetArray<String> mSavedBubbleKeysPerUser; - - // Used when ranking updates occur and we check if things should bubble / unbubble - private NotificationListenerService.Ranking mTmpRanking; - - // Bubbles get added to the status bar view - private final NotificationShadeWindowController mNotificationShadeWindowController; - private final ZenModeController mZenModeController; - private StatusBarStateListener mStatusBarStateListener; - private INotificationManager mINotificationManager; - - // Callback that updates BubbleOverflowActivity on data change. - @Nullable private BubbleData.Listener mOverflowListener = null; - - // Only load overflow data from disk once - private boolean mOverflowDataLoaded = false; - - /** - * When the shade status changes to SHADE (from anything but SHADE, like LOCKED) we'll select - * this bubble and expand the stack. - */ - @Nullable private NotificationEntry mNotifEntryToExpandOnShadeUnlock; - - private final NotificationInterruptStateProvider mNotificationInterruptStateProvider; - private IStatusBarService mBarService; - private WindowManager mWindowManager; - private SysUiState mSysUiState; - - // Used to post to main UI thread - private Handler mHandler = new Handler(); - - /** LayoutParams used to add the BubbleStackView to the window manager. */ - private WindowManager.LayoutParams mWmLayoutParams; - /** Whether or not the BubbleStackView has been added to the WindowManager. */ - private boolean mAddedToWindowManager = false; - - // Listens to user switch so bubbles can be saved and restored. - private final NotificationLockscreenUserManager mNotifUserManager; - - /** Last known orientation, used to detect orientation changes in {@link #onConfigChanged}. */ - private int mOrientation = Configuration.ORIENTATION_UNDEFINED; - - /** - * Last known screen density, used to detect display size changes in {@link #onConfigChanged}. - */ - private int mDensityDpi = Configuration.DENSITY_DPI_UNDEFINED; - - /** - * Last known font scale, used to detect font size changes in {@link #onConfigChanged}. - */ - private float mFontScale = 0; - - /** Last known direction, used to detect layout direction changes @link #onConfigChanged}. */ - private int mLayoutDirection = View.LAYOUT_DIRECTION_UNDEFINED; - - private boolean mInflateSynchronously; - - private MultiWindowTaskListener mTaskListener; - - // TODO (b/145659174): allow for multiple callbacks to support the "shadow" new notif pipeline - private final List<NotifCallback> mCallbacks = new ArrayList<>(); - - /** - * Whether the IME is visible, as reported by the BubbleStackView. If it is, we'll make the - * Bubbles window NOT_FOCUSABLE so that touches on the Bubbles UI doesn't steal focus from the - * ActivityView and hide the IME. - */ - private boolean mImeVisible = false; - - /** - * 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 key the notification key associated with bubble being expanded - */ - void onBubbleExpandChanged(boolean isExpanding, String key); - } - - /** - * Listener to be notified when a bubbles' notification suppression state changes. - */ - public interface NotificationSuppressionChangedListener { - /** - * Called when the notification suppression state of a bubble changes. - */ - void onBubbleNotificationSuppressionChange(Bubble bubble); - } - - /** - * Listener to be notified when a pending intent has been canceled for a bubble. - */ - public interface PendingIntentCanceledListener { - /** - * Called when the pending intent for a bubble has been canceled. - */ - void onPendingIntentCanceled(Bubble bubble); - } - - /** - * Callback for when the BubbleController wants to interact with the notification pipeline to: - * - Remove a previously bubbled notification - * - Update the notification shade since bubbled notification should/shouldn't be showing - */ - public interface NotifCallback { - /** - * Called when a bubbled notification that was hidden from the shade is now being removed - * This can happen when an app cancels a bubbled notification or when the user dismisses a - * bubble. - */ - void removeNotification( - @NonNull NotificationEntry entry, - @NonNull DismissedByUserStats stats, - int reason); - - /** - * Called when a bubbled notification has changed whether it should be - * filtered from the shade. - */ - void invalidateNotifications(@NonNull String reason); - - /** - * Called on a bubbled entry that has been removed when there are no longer - * bubbled entries in its group. - * - * Checks whether its group has any other (non-bubbled) children. If it doesn't, - * removes all remnants of the group's summary from the notification pipeline. - * TODO: (b/145659174) Only old pipeline needs this - delete post-migration. - */ - void maybeCancelSummary(@NonNull NotificationEntry entry); - } - - /** - * Listens for the current state of the status bar and updates the visibility state - * of bubbles as needed. - */ - private class StatusBarStateListener implements StatusBarStateController.StateListener { - private int mState; - /** - * Returns the current status bar state. - */ - public int getCurrentState() { - return mState; - } - - @Override - public void onStateChanged(int newState) { - mState = newState; - boolean shouldCollapse = (mState != SHADE); - if (shouldCollapse) { - collapseStack(); - } - - if (mNotifEntryToExpandOnShadeUnlock != null) { - expandStackAndSelectBubble(mNotifEntryToExpandOnShadeUnlock); - mNotifEntryToExpandOnShadeUnlock = null; - } - - updateStack(); - } - } - - /** - * Injected constructor. See {@link BubbleModule}. - */ - public static BubbleController create(Context context, - NotificationShadeWindowController notificationShadeWindowController, - StatusBarStateController statusBarStateController, - ShadeController shadeController, - @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, - ConfigurationController configurationController, - NotificationInterruptStateProvider interruptionStateProvider, - ZenModeController zenModeController, - NotificationLockscreenUserManager notifUserManager, - NotificationGroupManagerLegacy groupManager, - NotificationEntryManager entryManager, - NotifPipeline notifPipeline, - FeatureFlags featureFlags, - DumpManager dumpManager, - FloatingContentCoordinator floatingContentCoordinator, - SysUiState sysUiState, - INotificationManager notificationManager, - @Nullable IStatusBarService statusBarService, - WindowManager windowManager, - WindowManagerShellWrapper windowManagerShellWrapper, - LauncherApps launcherApps, - UiEventLogger uiEventLogger, - @Main Handler mainHandler, - ShellTaskOrganizer organizer) { - BubbleLogger logger = new BubbleLogger(uiEventLogger); - return new BubbleController(context, notificationShadeWindowController, - statusBarStateController, shadeController, new BubbleData(context, logger), - synchronizer, configurationController, interruptionStateProvider, zenModeController, - notifUserManager, groupManager, entryManager, notifPipeline, featureFlags, - dumpManager, floatingContentCoordinator, - new BubbleDataRepository(context, launcherApps), sysUiState, notificationManager, - statusBarService, windowManager, windowManagerShellWrapper, launcherApps, logger, - mainHandler, organizer); - } - - /** - * Testing constructor. - */ - @VisibleForTesting - BubbleController(Context context, - NotificationShadeWindowController notificationShadeWindowController, - StatusBarStateController statusBarStateController, - ShadeController shadeController, - BubbleData data, - @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, - ConfigurationController configurationController, - NotificationInterruptStateProvider interruptionStateProvider, - ZenModeController zenModeController, - NotificationLockscreenUserManager notifUserManager, - NotificationGroupManagerLegacy groupManager, - NotificationEntryManager entryManager, - NotifPipeline notifPipeline, - FeatureFlags featureFlags, - DumpManager dumpManager, - FloatingContentCoordinator floatingContentCoordinator, - BubbleDataRepository dataRepository, - SysUiState sysUiState, - INotificationManager notificationManager, - @Nullable IStatusBarService statusBarService, - WindowManager windowManager, - WindowManagerShellWrapper windowManagerShellWrapper, - LauncherApps launcherApps, - BubbleLogger bubbleLogger, - Handler mainHandler, - ShellTaskOrganizer organizer) { - dumpManager.registerDumpable(TAG, this); - mContext = context; - mShadeController = shadeController; - mNotificationInterruptStateProvider = interruptionStateProvider; - mNotifUserManager = notifUserManager; - mZenModeController = zenModeController; - mFloatingContentCoordinator = floatingContentCoordinator; - mDataRepository = dataRepository; - mINotificationManager = notificationManager; - mLogger = bubbleLogger; - mMainHandler = mainHandler; - mZenModeController.addCallback(new ZenModeController.Callback() { - @Override - public void onZenChanged(int zen) { - for (Bubble b : mBubbleData.getBubbles()) { - b.setShowDot(b.showInShade()); - } - } - - @Override - public void onConfigChanged(ZenModeConfig config) { - for (Bubble b : mBubbleData.getBubbles()) { - b.setShowDot(b.showInShade()); - } - } - }); - - configurationController.addCallback(this /* configurationListener */); - mSysUiState = sysUiState; - - mBubbleData = data; - mBubbleData.setListener(mBubbleDataListener); - mBubbleData.setSuppressionChangedListener(new NotificationSuppressionChangedListener() { - @Override - public void onBubbleNotificationSuppressionChange(Bubble bubble) { - // Make sure NoMan knows it's not showing in the shade anymore so anyone querying it - // can tell. - try { - mBarService.onBubbleNotificationSuppressionChanged(bubble.getKey(), - !bubble.showInShade()); - } catch (RemoteException e) { - // Bad things have happened - } - } - }); - mBubbleData.setPendingIntentCancelledListener(bubble -> { - if (bubble.getBubbleIntent() == null) { - return; - } - if (bubble.isIntentActive() - || mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { - bubble.setPendingIntentCanceled(); - return; - } - mHandler.post( - () -> removeBubble(bubble.getKey(), - BubbleController.DISMISS_INVALID_INTENT)); - }); - - mNotificationEntryManager = entryManager; - mNotificationGroupManager = groupManager; - mNotifPipeline = notifPipeline; - - if (!featureFlags.isNewNotifPipelineRenderingEnabled()) { - setupNEM(); - } else { - setupNotifPipeline(); - } - - mNotificationShadeWindowController = notificationShadeWindowController; - mStatusBarStateListener = new StatusBarStateListener(); - statusBarStateController.addCallback(mStatusBarStateListener); - - mTaskStackListener = new BubbleTaskStackListener(); - TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); - - try { - windowManagerShellWrapper.addPinnedStackListener(new BubblesImeListener()); - } catch (RemoteException e) { - e.printStackTrace(); - } - mSurfaceSynchronizer = synchronizer; - - mWindowManager = windowManager; - mBarService = statusBarService == null - ? IStatusBarService.Stub.asInterface( - ServiceManager.getService(Context.STATUS_BAR_SERVICE)) - : statusBarService; - - mBubbleScrim = new ScrimView(mContext); - mBubbleScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - - mSavedBubbleKeysPerUser = new SparseSetArray<>(); - mCurrentUserId = mNotifUserManager.getCurrentUserId(); - mBubbleData.setCurrentUserId(mCurrentUserId); - - mNotifUserManager.addUserChangedListener( - new NotificationLockscreenUserManager.UserChangedListener() { - @Override - public void onUserChanged(int newUserId) { - BubbleController.this.saveBubbles(mCurrentUserId); - mBubbleData.dismissAll(DISMISS_USER_CHANGED); - BubbleController.this.restoreBubbles(newUserId); - mCurrentUserId = newUserId; - mBubbleData.setCurrentUserId(newUserId); - } - }); - - mBubbleIconFactory = new BubbleIconFactory(context); - mTaskListener = new MultiWindowTaskListener(mMainHandler, organizer); - - launcherApps.registerCallback(new LauncherApps.Callback() { - @Override - public void onPackageAdded(String s, UserHandle userHandle) {} - - @Override - public void onPackageChanged(String s, UserHandle userHandle) {} - - @Override - public void onPackageRemoved(String s, UserHandle userHandle) { - // Remove bubbles with this package name, since it has been uninstalled and attempts - // to open a bubble from an uninstalled app can cause issues. - mBubbleData.removeBubblesWithPackageName(s, DISMISS_PACKAGE_REMOVED); - } - - @Override - public void onPackagesAvailable(String[] strings, UserHandle userHandle, - boolean b) { - - } - - @Override - public void onPackagesUnavailable(String[] packages, UserHandle userHandle, - boolean b) { - for (String packageName : packages) { - // Remove bubbles from unavailable apps. This can occur when the app is on - // external storage that has been removed. - mBubbleData.removeBubblesWithPackageName(packageName, DISMISS_PACKAGE_REMOVED); - } - } - - @Override - public void onShortcutsChanged(String packageName, List<ShortcutInfo> validShortcuts, - UserHandle user) { - super.onShortcutsChanged(packageName, validShortcuts, user); - - // Remove bubbles whose shortcuts aren't in the latest list of valid shortcuts. - mBubbleData.removeBubblesWithInvalidShortcuts( - packageName, validShortcuts, DISMISS_SHORTCUT_REMOVED); - } - }); - } - - /** - * See {@link NotifCallback}. - */ - @Override - public void addNotifCallback(NotifCallback callback) { - mCallbacks.add(callback); - } - - /** - * Hides the current input method, wherever it may be focused, via InputMethodManagerInternal. - */ - public void hideCurrentInputMethod() { - try { - mBarService.hideCurrentInputMethodForBubbles(); - } catch (RemoteException e) { - e.printStackTrace(); - } - } - - private void onBubbleExpandChanged(boolean shouldExpand) { - mSysUiState - .setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED, shouldExpand) - .commitUpdate(mContext.getDisplayId()); - } - - private void setupNEM() { - mNotificationEntryManager.addNotificationEntryListener( - new NotificationEntryListener() { - @Override - public void onPendingEntryAdded(NotificationEntry entry) { - onEntryAdded(entry); - } - - @Override - public void onPreEntryUpdated(NotificationEntry entry) { - onEntryUpdated(entry); - } - - @Override - public void onEntryRemoved( - NotificationEntry entry, - @android.annotation.Nullable NotificationVisibility visibility, - boolean removedByUser, - int reason) { - BubbleController.this.onEntryRemoved(entry); - } - - @Override - public void onNotificationRankingUpdated(RankingMap rankingMap) { - onRankingUpdated(rankingMap); - } - }); - - // The new pipeline takes care of this as a NotifDismissInterceptor BubbleCoordinator - mNotificationEntryManager.addNotificationRemoveInterceptor( - new NotificationRemoveInterceptor() { - @Override - public boolean onNotificationRemoveRequested( - String key, - NotificationEntry entry, - int dismissReason) { - final boolean isClearAll = dismissReason == REASON_CANCEL_ALL; - final boolean isUserDismiss = dismissReason == REASON_CANCEL - || dismissReason == REASON_CLICK; - final boolean isAppCancel = dismissReason == REASON_APP_CANCEL - || dismissReason == REASON_APP_CANCEL_ALL; - final boolean isSummaryCancel = - dismissReason == REASON_GROUP_SUMMARY_CANCELED; - - // Need to check for !appCancel here because the notification may have - // previously been dismissed & entry.isRowDismissed would still be true - boolean userRemovedNotif = - (entry != null && entry.isRowDismissed() && !isAppCancel) - || isClearAll || isUserDismiss || isSummaryCancel; - - if (userRemovedNotif) { - return handleDismissalInterception(entry); - } - return false; - } - }); - - mNotificationGroupManager.registerGroupChangeListener( - new NotificationGroupManagerLegacy.OnGroupChangeListener() { - @Override - public void onGroupSuppressionChanged( - NotificationGroupManagerLegacy.NotificationGroup group, - boolean suppressed) { - // More notifications could be added causing summary to no longer - // be suppressed -- in this case need to remove the key. - final String groupKey = group.summary != null - ? group.summary.getSbn().getGroupKey() - : null; - if (!suppressed && groupKey != null - && mBubbleData.isSummarySuppressed(groupKey)) { - mBubbleData.removeSuppressedSummary(groupKey); - } - } - }); - - addNotifCallback(new NotifCallback() { - @Override - public void removeNotification( - NotificationEntry entry, - DismissedByUserStats dismissedByUserStats, - int reason - ) { - mNotificationEntryManager.performRemoveNotification(entry.getSbn(), - dismissedByUserStats, reason); - } - - @Override - public void invalidateNotifications(String reason) { - mNotificationEntryManager.updateNotifications(reason); - } - - @Override - public void maybeCancelSummary(NotificationEntry entry) { - // Check if removed bubble has an associated suppressed group summary that needs - // to be removed now. - final String groupKey = entry.getSbn().getGroupKey(); - if (mBubbleData.isSummarySuppressed(groupKey)) { - mBubbleData.removeSuppressedSummary(groupKey); - - final NotificationEntry summary = - mNotificationEntryManager.getActiveNotificationUnfiltered( - mBubbleData.getSummaryKey(groupKey)); - if (summary != null) { - mNotificationEntryManager.performRemoveNotification( - summary.getSbn(), - getDismissedByUserStats(summary, false), - UNDEFINED_DISMISS_REASON); - } - } - - // Check if we still need to remove the summary from NoManGroup because the summary - // may not be in the mBubbleData.mSuppressedGroupKeys list and removed above. - // For example: - // 1. Bubbled notifications (group) is posted to shade and are visible bubbles - // 2. User expands bubbles so now their respective notifications in the shade are - // hidden, including the group summary - // 3. User removes all bubbles - // 4. We expect all the removed bubbles AND the summary (note: the summary was - // never added to the suppressedSummary list in BubbleData, so we add this check) - NotificationEntry summary = mNotificationGroupManager.getLogicalGroupSummary(entry); - if (summary != null) { - ArrayList<NotificationEntry> summaryChildren = - mNotificationGroupManager.getLogicalChildren(summary.getSbn()); - boolean isSummaryThisNotif = summary.getKey().equals(entry.getKey()); - if (!isSummaryThisNotif && (summaryChildren == null - || summaryChildren.isEmpty())) { - mNotificationEntryManager.performRemoveNotification( - summary.getSbn(), - getDismissedByUserStats(summary, false), - UNDEFINED_DISMISS_REASON); - } - } - } - }); - } - - private void setupNotifPipeline() { - mNotifPipeline.addCollectionListener(new NotifCollectionListener() { - @Override - public void onEntryAdded(NotificationEntry entry) { - BubbleController.this.onEntryAdded(entry); - } - - @Override - public void onEntryUpdated(NotificationEntry entry) { - BubbleController.this.onEntryUpdated(entry); - } - - @Override - public void onRankingUpdate(RankingMap rankingMap) { - onRankingUpdated(rankingMap); - } - - @Override - public void onEntryRemoved(NotificationEntry entry, - @NotifCollection.CancellationReason int reason) { - BubbleController.this.onEntryRemoved(entry); - } - }); - } - - /** - * Returns the scrim drawn behind the bubble stack. This is managed by {@link ScrimController} - * since we want the scrim's appearance and behavior to be identical to that of the notification - * shade scrim. - */ - @Override - public ScrimView getScrimForBubble() { - return mBubbleScrim; - } - - /** - * Called when the status bar has become visible or invisible (either permanently or - * temporarily). - */ - @Override - public void onStatusBarVisibilityChanged(boolean visible) { - if (mStackView != null) { - // Hide the stack temporarily if the status bar has been made invisible, and the stack - // is collapsed. An expanded stack should remain visible until collapsed. - mStackView.setTemporarilyInvisible(!visible && !isStackExpanded()); - } - } - - /** - * Sets whether to perform inflation on the same thread as the caller. This method should only - * be used in tests, not in production. - */ - @VisibleForTesting - void setInflateSynchronously(boolean inflateSynchronously) { - mInflateSynchronously = inflateSynchronously; - } - - @Override - public void setOverflowListener(BubbleData.Listener listener) { - mOverflowListener = listener; - } - - /** - * @return Bubbles for updating overflow. - */ - @Override - public List<Bubble> getOverflowBubbles() { - return mBubbleData.getOverflowBubbles(); - } - - @Override - public MultiWindowTaskListener getTaskManager() { - return mTaskListener; - } - - /** - * BubbleStackView is lazily created by this method the first time a Bubble is added. This - * method initializes the stack view and adds it to the StatusBar just above the scrim. - */ - private void ensureStackViewCreated() { - if (mStackView == null) { - mStackView = new BubbleStackView( - mContext, mBubbleData, mSurfaceSynchronizer, mFloatingContentCoordinator, - this::onAllBubblesAnimatedOut, this::onImeVisibilityChanged, - this::hideCurrentInputMethod, this::onBubbleExpandChanged); - mStackView.setStackStartPosition(mPositionFromRemovedStack); - mStackView.addView(mBubbleScrim); - if (mExpandListener != null) { - mStackView.setExpandListener(mExpandListener); - } - - mStackView.setUnbubbleConversationCallback(key -> { - final NotificationEntry entry = - mNotificationEntryManager.getPendingOrActiveNotif(key); - if (entry != null) { - onUserChangedBubble(entry, false /* shouldBubble */); - } - }); - } - - addToWindowManagerMaybe(); - } - - /** Adds the BubbleStackView to the WindowManager if it's not already there. */ - private void addToWindowManagerMaybe() { - // If the stack is null, or already added, don't add it. - if (mStackView == null || mAddedToWindowManager) { - return; - } - - mWmLayoutParams = new WindowManager.LayoutParams( - // Fill the screen so we can use translation animations to position the bubble - // stack. We'll use touchable regions to ignore touches that are not on the bubbles - // themselves. - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL - | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, - PixelFormat.TRANSLUCENT); - - mWmLayoutParams.setTrustedOverlay(); - mWmLayoutParams.setFitInsetsTypes(0); - mWmLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; - mWmLayoutParams.token = new Binder(); - mWmLayoutParams.setTitle("Bubbles!"); - mWmLayoutParams.packageName = mContext.getPackageName(); - mWmLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; - - try { - mAddedToWindowManager = true; - mWindowManager.addView(mStackView, mWmLayoutParams); - } catch (IllegalStateException e) { - // This means the stack has already been added. This shouldn't happen... - e.printStackTrace(); - } - } - - private void onImeVisibilityChanged(boolean imeVisible) { - mImeVisible = imeVisible; - } - - /** Removes the BubbleStackView from the WindowManager if it's there. */ - private void removeFromWindowManagerMaybe() { - if (!mAddedToWindowManager) { - return; - } - - try { - mAddedToWindowManager = false; - if (mStackView != null) { - mPositionFromRemovedStack = mStackView.getRelativeStackPosition(); - mWindowManager.removeView(mStackView); - mStackView.removeView(mBubbleScrim); - mStackView = null; - } else { - Log.w(TAG, "StackView added to WindowManager, but was null when removing!"); - } - } catch (IllegalArgumentException e) { - // This means the stack has already been removed - it shouldn't happen, but ignore if it - // does, since we wanted it removed anyway. - e.printStackTrace(); - } - } - - /** - * Called by the BubbleStackView and whenever all bubbles have animated out, and none have been - * added in the meantime. - */ - private void onAllBubblesAnimatedOut() { - if (mStackView != null) { - mStackView.setVisibility(INVISIBLE); - removeFromWindowManagerMaybe(); - } - } - - /** - * Records the notification key for any active bubbles. These are used to restore active - * bubbles when the user returns to the foreground. - * - * @param userId the id of the user - */ - private void saveBubbles(@UserIdInt int userId) { - // First clear any existing keys that might be stored. - mSavedBubbleKeysPerUser.remove(userId); - // Add in all active bubbles for the current user. - for (Bubble bubble: mBubbleData.getBubbles()) { - mSavedBubbleKeysPerUser.add(userId, bubble.getKey()); - } - } - - /** - * Promotes existing notifications to Bubbles if they were previously bubbles. - * - * @param userId the id of the user - */ - private void restoreBubbles(@UserIdInt int userId) { - ArraySet<String> savedBubbleKeys = mSavedBubbleKeysPerUser.get(userId); - if (savedBubbleKeys == null) { - // There were no bubbles saved for this used. - return; - } - for (NotificationEntry e : - mNotificationEntryManager.getActiveNotificationsForCurrentUser()) { - if (savedBubbleKeys.contains(e.getKey()) - && mNotificationInterruptStateProvider.shouldBubbleUp(e) - && e.isBubble() - && canLaunchInActivityView(mContext, e)) { - updateBubble(e, true /* suppressFlyout */, false /* showInShade */); - } - } - // Finally, remove the entries for this user now that bubbles are restored. - mSavedBubbleKeysPerUser.remove(mCurrentUserId); - } - - @Override - public void onUiModeChanged() { - updateForThemeChanges(); - } - - @Override - public void onOverlayChanged() { - updateForThemeChanges(); - } - - private void updateForThemeChanges() { - if (mStackView != null) { - mStackView.onThemeChanged(); - } - mBubbleIconFactory = new BubbleIconFactory(mContext); - // Reload each bubble - for (Bubble b: mBubbleData.getBubbles()) { - b.inflate(null /* callback */, mContext, mStackView, mBubbleIconFactory, - false /* skipInflation */); - } - for (Bubble b: mBubbleData.getOverflowBubbles()) { - b.inflate(null /* callback */, mContext, mStackView, mBubbleIconFactory, - false /* skipInflation */); - } - } - - @Override - public void onConfigChanged(Configuration newConfig) { - if (mStackView != null && newConfig != null) { - if (newConfig.orientation != mOrientation) { - mOrientation = newConfig.orientation; - mStackView.onOrientationChanged(newConfig.orientation); - } - if (newConfig.densityDpi != mDensityDpi) { - mDensityDpi = newConfig.densityDpi; - mBubbleIconFactory = new BubbleIconFactory(mContext); - mStackView.onDisplaySizeChanged(); - } - if (newConfig.fontScale != mFontScale) { - mFontScale = newConfig.fontScale; - mStackView.updateFlyout(mFontScale); - } - if (newConfig.getLayoutDirection() != mLayoutDirection) { - mLayoutDirection = newConfig.getLayoutDirection(); - mStackView.onLayoutDirectionChanged(mLayoutDirection); - } - } - } - - /** - * Set a listener to be notified of bubble expand events. - */ - @Override - public void setExpandListener(BubbleExpandListener listener) { - mExpandListener = ((isExpanding, key) -> { - if (listener != null) { - listener.onBubbleExpandChanged(isExpanding, key); - } - }); - 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). - */ - @VisibleForTesting - boolean hasBubbles() { - if (mStackView == null) { - return false; - } - return mBubbleData.hasBubbles(); - } - - @Override - public boolean isStackExpanded() { - return mBubbleData.isExpanded(); - } - - @Override - public void collapseStack() { - mBubbleData.setExpanded(false /* expanded */); - } - - @Override - public boolean isBubbleNotificationSuppressedFromShade(NotificationEntry entry) { - String key = entry.getKey(); - boolean isSuppressedBubble = (mBubbleData.hasAnyBubbleWithKey(key) - && !mBubbleData.getAnyBubbleWithkey(key).showInShade()); - - String groupKey = entry.getSbn().getGroupKey(); - boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey); - boolean isSummary = key.equals(mBubbleData.getSummaryKey(groupKey)); - return (isSummary && isSuppressedSummary) || isSuppressedBubble; - } - - @Override - public boolean isBubbleExpanded(NotificationEntry entry) { - return isStackExpanded() && mBubbleData != null && mBubbleData.getSelectedBubble() != null - && mBubbleData.getSelectedBubble().getKey().equals(entry.getKey()); - } - - @Override - public void promoteBubbleFromOverflow(Bubble bubble) { - mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK); - bubble.setInflateSynchronously(mInflateSynchronously); - bubble.setShouldAutoExpand(true); - bubble.markAsAccessedAt(System.currentTimeMillis()); - setIsBubble(bubble, true /* isBubble */); - } - - @Override - public void expandStackAndSelectBubble(NotificationEntry entry) { - if (mStatusBarStateListener.getCurrentState() == SHADE) { - mNotifEntryToExpandOnShadeUnlock = null; - - String key = entry.getKey(); - Bubble bubble = mBubbleData.getBubbleInStackWithKey(key); - if (bubble != null) { - mBubbleData.setSelectedBubble(bubble); - mBubbleData.setExpanded(true); - } else { - bubble = mBubbleData.getOverflowBubbleWithKey(key); - if (bubble != null) { - promoteBubbleFromOverflow(bubble); - } else if (entry.canBubble()) { - // It can bubble but it's not -- it got aged out of the overflow before it - // was dismissed or opened, make it a bubble again. - setIsBubble(entry, true /* isBubble */, true /* autoExpand */); - } - } - } else { - // Wait until we're unlocked to expand, so that the user can see the expand animation - // and also to work around bugs with expansion animation + shade unlock happening at the - // same time. - mNotifEntryToExpandOnShadeUnlock = entry; - } - } - - @Override - public void onUserChangedImportance(NotificationEntry entry) { - try { - int flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; - flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE; - mBarService.onNotificationBubbleChanged(entry.getKey(), true, flags); - } catch (RemoteException e) { - Log.e(TAG, e.getMessage()); - } - mShadeController.collapsePanel(true); - if (entry.getRow() != null) { - entry.getRow().updateBubbleButton(); - } - } - - /** - * Adds or updates a bubble associated with the provided notification entry. - * - * @param notif the notification associated with this bubble. - */ - void updateBubble(NotificationEntry notif) { - updateBubble(notif, false /* suppressFlyout */, true /* showInShade */); - } - - /** - * Fills the overflow bubbles by loading them from disk. - */ - void loadOverflowBubblesFromDisk() { - if (!mBubbleData.getOverflowBubbles().isEmpty() || mOverflowDataLoaded) { - // we don't need to load overflow bubbles from disk if it is already in memory - return; - } - mOverflowDataLoaded = true; - mDataRepository.loadBubbles((bubbles) -> { - bubbles.forEach(bubble -> { - if (mBubbleData.hasAnyBubbleWithKey(bubble.getKey())) { - // if the bubble is already active, there's no need to push it to overflow - return; - } - bubble.inflate((b) -> mBubbleData.overflowBubble(DISMISS_AGED, bubble), - mContext, mStackView, mBubbleIconFactory, true /* skipInflation */); - }); - return null; - }); - } - - void updateBubble(NotificationEntry notif, boolean suppressFlyout, boolean showInShade) { - // If this is an interruptive notif, mark that it's interrupted - if (notif.getImportance() >= NotificationManager.IMPORTANCE_HIGH) { - notif.setInterruption(); - } - if (!notif.getRanking().visuallyInterruptive() - && (notif.getBubbleMetadata() != null - && !notif.getBubbleMetadata().getAutoExpandBubble()) - && mBubbleData.hasOverflowBubbleWithKey(notif.getKey())) { - // Update the bubble but don't promote it out of overflow - Bubble b = mBubbleData.getOverflowBubbleWithKey(notif.getKey()); - b.setEntry(notifToBubbleEntry(notif)); - } else { - Bubble bubble = mBubbleData.getOrCreateBubble( - notifToBubbleEntry(notif), null /* persistedBubble */); - inflateAndAdd(bubble, suppressFlyout, showInShade); - } - } - - void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) { - // Lazy init stack view when a bubble is created - ensureStackViewCreated(); - bubble.setInflateSynchronously(mInflateSynchronously); - bubble.inflate(b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade), - mContext, mStackView, mBubbleIconFactory, false /* skipInflation */); - } - - @Override - public void onUserChangedBubble(@NonNull final NotificationEntry entry, boolean shouldBubble) { - NotificationChannel channel = entry.getChannel(); - final String appPkg = entry.getSbn().getPackageName(); - final int appUid = entry.getSbn().getUid(); - if (channel == null || appPkg == null) { - return; - } - - // Update the state in NotificationManagerService - try { - int flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; - flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE; - mBarService.onNotificationBubbleChanged(entry.getKey(), shouldBubble, flags); - } catch (RemoteException e) { - } - - // Change the settings - channel = NotificationChannelHelper.createConversationChannelIfNeeded(mContext, - mINotificationManager, entry, channel); - channel.setAllowBubbles(shouldBubble); - try { - int currentPref = mINotificationManager.getBubblePreferenceForPackage(appPkg, appUid); - if (shouldBubble && currentPref == BUBBLE_PREFERENCE_NONE) { - mINotificationManager.setBubblesAllowed(appPkg, appUid, BUBBLE_PREFERENCE_SELECTED); - } - mINotificationManager.updateNotificationChannelForPackage(appPkg, appUid, channel); - } catch (RemoteException e) { - Log.e(TAG, e.getMessage()); - } - - if (shouldBubble) { - mShadeController.collapsePanel(true); - if (entry.getRow() != null) { - entry.getRow().updateBubbleButton(); - } - } - } - - @MainThread - @Override - public void removeBubble(String key, int reason) { - if (mBubbleData.hasAnyBubbleWithKey(key)) { - mBubbleData.dismissBubbleWithKey(key, reason); - } - } - - private void onEntryAdded(NotificationEntry entry) { - if (mNotificationInterruptStateProvider.shouldBubbleUp(entry) - && entry.isBubble() - && canLaunchInActivityView(mContext, entry)) { - updateBubble(entry); - } - } - - private void onEntryUpdated(NotificationEntry entry) { - // shouldBubbleUp checks canBubble & for bubble metadata - boolean shouldBubble = mNotificationInterruptStateProvider.shouldBubbleUp(entry) - && canLaunchInActivityView(mContext, entry); - if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) { - // It was previously a bubble but no longer a bubble -- lets remove it - removeBubble(entry.getKey(), DISMISS_NO_LONGER_BUBBLE); - } else if (shouldBubble && entry.isBubble()) { - updateBubble(entry); - } - } - - private void onEntryRemoved(NotificationEntry entry) { - if (isSummaryOfBubbles(entry)) { - final String groupKey = entry.getSbn().getGroupKey(); - mBubbleData.removeSuppressedSummary(groupKey); - - // Remove any associated bubble children with the summary - final List<Bubble> bubbleChildren = getBubblesInGroup(groupKey); - for (int i = 0; i < bubbleChildren.size(); i++) { - removeBubble(bubbleChildren.get(i).getKey(), DISMISS_GROUP_CANCELLED); - } - } else { - removeBubble(entry.getKey(), DISMISS_NOTIF_CANCEL); - } - } - - /** - * Called when NotificationListener has received adjusted notification rank and reapplied - * filtering and sorting. This is used to dismiss or create bubbles based on changes in - * permissions on the notification channel or the global setting. - * - * @param rankingMap the updated ranking map from NotificationListenerService - */ - private void onRankingUpdated(RankingMap rankingMap) { - if (mTmpRanking == null) { - mTmpRanking = new NotificationListenerService.Ranking(); - } - String[] orderedKeys = rankingMap.getOrderedKeys(); - for (int i = 0; i < orderedKeys.length; i++) { - String key = orderedKeys[i]; - NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif(key); - rankingMap.getRanking(key, mTmpRanking); - boolean isActiveBubble = mBubbleData.hasAnyBubbleWithKey(key); - if (isActiveBubble && !mTmpRanking.canBubble()) { - // If this entry is no longer allowed to bubble, dismiss with the BLOCKED reason. - // This means that the app or channel's ability to bubble has been revoked. - mBubbleData.dismissBubbleWithKey( - key, BubbleController.DISMISS_BLOCKED); - } else if (isActiveBubble - && !mNotificationInterruptStateProvider.shouldBubbleUp(entry)) { - // If this entry is allowed to bubble, but cannot currently bubble up, dismiss it. - // This happens when DND is enabled and configured to hide bubbles. Dismissing with - // the reason DISMISS_NO_BUBBLE_UP will retain the underlying notification, so that - // the bubble will be re-created if shouldBubbleUp returns true. - mBubbleData.dismissBubbleWithKey( - key, BubbleController.DISMISS_NO_BUBBLE_UP); - } else if (entry != null && mTmpRanking.isBubble() && !isActiveBubble) { - entry.setFlagBubble(true); - onEntryUpdated(entry); - } - } - } - - /** - * Retrieves any bubbles that are part of the notification group represented by the provided - * group key. - */ - private ArrayList<Bubble> getBubblesInGroup(@Nullable String groupKey) { - ArrayList<Bubble> bubbleChildren = new ArrayList<>(); - if (groupKey == null) { - return bubbleChildren; - } - for (Bubble bubble : mBubbleData.getActiveBubbles()) { - final NotificationEntry entry = - mNotificationEntryManager.getPendingOrActiveNotif(bubble.getKey()); - if (entry != null && groupKey.equals(entry.getSbn().getGroupKey())) { - bubbleChildren.add(bubble); - } - } - return bubbleChildren; - } - - private void setIsBubble(@NonNull final NotificationEntry entry, final boolean isBubble, - final boolean autoExpand) { - Objects.requireNonNull(entry); - if (isBubble) { - entry.getSbn().getNotification().flags |= FLAG_BUBBLE; - } else { - entry.getSbn().getNotification().flags &= ~FLAG_BUBBLE; - } - try { - int flags = 0; - if (autoExpand) { - flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; - flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE; - } - mBarService.onNotificationBubbleChanged(entry.getKey(), isBubble, flags); - } catch (RemoteException e) { - // Bad things have happened - } - } - - private void setIsBubble(@NonNull final Bubble b, final boolean isBubble) { - Objects.requireNonNull(b); - b.setIsBubble(isBubble); - final NotificationEntry entry = mNotificationEntryManager - .getPendingOrActiveNotif(b.getKey()); - if (entry != null) { - // Updating the entry to be a bubble will trigger our normal update flow - setIsBubble(entry, isBubble, b.shouldAutoExpand()); - } else if (isBubble) { - // If bubble doesn't exist, it's a persisted bubble so we need to add it to the - // stack ourselves - Bubble bubble = mBubbleData.getOrCreateBubble(null, b /* persistedBubble */); - inflateAndAdd(bubble, bubble.shouldAutoExpand() /* suppressFlyout */, - !bubble.shouldAutoExpand() /* showInShade */); - } - } - - @SuppressWarnings("FieldCanBeLocal") - private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() { - - @Override - public void applyUpdate(BubbleData.Update update) { - ensureStackViewCreated(); - - // Lazy load overflow bubbles from disk - loadOverflowBubblesFromDisk(); - - mStackView.updateOverflowButtonDot(); - - // Update bubbles in overflow. - if (mOverflowListener != null) { - mOverflowListener.applyUpdate(update); - } - - // Collapsing? Do this first before remaining steps. - if (update.expandedChanged && !update.expanded) { - mStackView.setExpanded(false); - mNotificationShadeWindowController.setRequestTopUi(false, TAG); - } - - // Do removals, if any. - ArrayList<Pair<Bubble, Integer>> removedBubbles = - new ArrayList<>(update.removedBubbles); - ArrayList<Bubble> bubblesToBeRemovedFromRepository = new ArrayList<>(); - for (Pair<Bubble, Integer> removed : removedBubbles) { - final Bubble bubble = removed.first; - @DismissReason final int reason = removed.second; - - if (mStackView != null) { - mStackView.removeBubble(bubble); - } - - // Leave the notification in place if we're dismissing due to user switching, or - // because DND is suppressing the bubble. In both of those cases, we need to be able - // to restore the bubble from the notification later. - if (reason == DISMISS_USER_CHANGED || reason == DISMISS_NO_BUBBLE_UP) { - continue; - } - if (reason == DISMISS_NOTIF_CANCEL) { - bubblesToBeRemovedFromRepository.add(bubble); - } - final NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif( - bubble.getKey()); - if (!mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { - if (!mBubbleData.hasOverflowBubbleWithKey(bubble.getKey()) - && (!bubble.showInShade() - || reason == DISMISS_NOTIF_CANCEL - || reason == DISMISS_GROUP_CANCELLED)) { - // The bubble is now gone & the notification is hidden from the shade, so - // time to actually remove it - for (NotifCallback cb : mCallbacks) { - if (entry != null) { - cb.removeNotification( - entry, - getDismissedByUserStats(entry, true), - REASON_CANCEL); - } - } - } else { - if (bubble.isBubble()) { - setIsBubble(bubble, false /* isBubble */); - } - if (entry != null && entry.getRow() != null) { - entry.getRow().updateBubbleButton(); - } - } - - } - if (entry != null) { - final String groupKey = entry.getSbn().getGroupKey(); - if (getBubblesInGroup(groupKey).isEmpty()) { - // Time to potentially remove the summary - for (NotifCallback cb : mCallbacks) { - cb.maybeCancelSummary(entry); - } - } - } - } - mDataRepository.removeBubbles(mCurrentUserId, bubblesToBeRemovedFromRepository); - - if (update.addedBubble != null && mStackView != null) { - mDataRepository.addBubble(mCurrentUserId, update.addedBubble); - mStackView.addBubble(update.addedBubble); - } - - if (update.updatedBubble != null && mStackView != null) { - mStackView.updateBubble(update.updatedBubble); - } - - // At this point, the correct bubbles are inflated in the stack. - // Make sure the order in bubble data is reflected in bubble row. - if (update.orderChanged && mStackView != null) { - mDataRepository.addBubbles(mCurrentUserId, update.bubbles); - mStackView.updateBubbleOrder(update.bubbles); - } - - if (update.selectionChanged && mStackView != null) { - mStackView.setSelectedBubble(update.selectedBubble); - if (update.selectedBubble != null) { - final NotificationEntry entry = mNotificationEntryManager - .getPendingOrActiveNotif(update.selectedBubble.getKey()); - if (entry != null) { - mNotificationGroupManager.updateSuppression(entry); - } - } - } - - // Expanding? Apply this last. - if (update.expandedChanged && update.expanded) { - if (mStackView != null) { - mStackView.setExpanded(true); - mNotificationShadeWindowController.setRequestTopUi(true, TAG); - } - } - - for (NotifCallback cb : mCallbacks) { - cb.invalidateNotifications("BubbleData.Listener.applyUpdate"); - } - updateStack(); - } - }; - - @Override - public boolean handleDismissalInterception(NotificationEntry entry) { - if (entry == null) { - return false; - } - if (isSummaryOfBubbles(entry)) { - handleSummaryDismissalInterception(entry); - } else { - Bubble bubble = mBubbleData.getBubbleInStackWithKey(entry.getKey()); - if (bubble == null || !entry.isBubble()) { - bubble = mBubbleData.getOverflowBubbleWithKey(entry.getKey()); - } - if (bubble == null) { - return false; - } - bubble.setSuppressNotification(true); - bubble.setShowDot(false /* show */); - } - // Update the shade - for (NotifCallback cb : mCallbacks) { - cb.invalidateNotifications("BubbleController.handleDismissalInterception"); - } - return true; - } - - private boolean isSummaryOfBubbles(NotificationEntry entry) { - if (entry == null) { - return false; - } - - String groupKey = entry.getSbn().getGroupKey(); - ArrayList<Bubble> bubbleChildren = getBubblesInGroup(groupKey); - boolean isSuppressedSummary = (mBubbleData.isSummarySuppressed(groupKey) - && mBubbleData.getSummaryKey(groupKey).equals(entry.getKey())); - boolean isSummary = entry.getSbn().getNotification().isGroupSummary(); - return (isSuppressedSummary || isSummary) - && bubbleChildren != null - && !bubbleChildren.isEmpty(); - } - - private void handleSummaryDismissalInterception(NotificationEntry summary) { - // current children in the row: - final List<NotificationEntry> children = summary.getAttachedNotifChildren(); - if (children != null) { - for (int i = 0; i < children.size(); i++) { - NotificationEntry child = children.get(i); - if (mBubbleData.hasAnyBubbleWithKey(child.getKey())) { - // Suppress the bubbled child - // As far as group manager is concerned, once a child is no longer shown - // in the shade, it is essentially removed. - Bubble bubbleChild = mBubbleData.getAnyBubbleWithkey(child.getKey()); - if (bubbleChild != null) { - final NotificationEntry entry = mNotificationEntryManager - .getPendingOrActiveNotif(bubbleChild.getKey()); - if (entry != null) { - mNotificationGroupManager.onEntryRemoved(entry); - } - bubbleChild.setSuppressNotification(true); - bubbleChild.setShowDot(false /* show */); - } - } else { - // non-bubbled children can be removed - for (NotifCallback cb : mCallbacks) { - cb.removeNotification( - child, - getDismissedByUserStats(child, true), - REASON_GROUP_SUMMARY_CANCELED); - } - } - } - } - - // And since all children are removed, remove the summary. - mNotificationGroupManager.onEntryRemoved(summary); - - // TODO: (b/145659174) remove references to mSuppressedGroupKeys once fully migrated - mBubbleData.addSummaryToSuppress(summary.getSbn().getGroupKey(), - summary.getKey()); - } - - /** - * Gets the DismissedByUserStats used by {@link NotificationEntryManager}. - * Will not be necessary when using the new notification pipeline's {@link NotifCollection}. - * Instead, this is taken care of by {@link BubbleCoordinator}. - */ - private DismissedByUserStats getDismissedByUserStats( - NotificationEntry entry, - boolean isVisible) { - return new DismissedByUserStats( - DISMISSAL_BUBBLE, - DISMISS_SENTIMENT_NEUTRAL, - NotificationVisibility.obtain( - entry.getKey(), - entry.getRanking().getRank(), - mNotificationEntryManager.getActiveNotificationsCount(), - isVisible, - NotificationLogger.getNotificationLocation(entry))); - } - - /** - * Updates the visibility of the bubbles based on current state. - * Does not un-bubble, just hides or un-hides. - * Updates stack description for TalkBack focus. - */ - public void updateStack() { - if (mStackView == null) { - return; - } - - if (mStatusBarStateListener.getCurrentState() != SHADE) { - // Bubbles don't appear over the locked shade. - mStackView.setVisibility(INVISIBLE); - } else if (hasBubbles()) { - // If we're unlocked, show the stack if we have bubbles. If we don't have bubbles, the - // stack will be set to INVISIBLE in onAllBubblesAnimatedOut after the bubbles animate - // out. - mStackView.setVisibility(VISIBLE); - } - - mStackView.updateContentDescription(); - } - - /** - * The task id of the expanded view, if the stack is expanded and not occluded by the - * status bar, otherwise returns {@link ActivityTaskManager#INVALID_TASK_ID}. - */ - private int getExpandedTaskId() { - if (mStackView == null) { - return INVALID_TASK_ID; - } - final BubbleViewProvider expandedViewProvider = mStackView.getExpandedBubble(); - if (expandedViewProvider != null && isStackExpanded() - && !mStackView.isExpansionAnimating() - && !mNotificationShadeWindowController.getPanelExpanded()) { - return expandedViewProvider.getTaskId(); - } - return INVALID_TASK_ID; - } - - @VisibleForTesting - BubbleStackView getStackView() { - return mStackView; - } - - /** - * Description of current bubble state. - */ - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println("BubbleController state:"); - mBubbleData.dump(fd, pw, args); - pw.println(); - if (mStackView != null) { - mStackView.dump(fd, pw, args); - } - pw.println(); - } - - /** - * This task stack listener is responsible for responding to tasks moved to the front - * which are on the default (main) display. When this happens, expanded bubbles must be - * collapsed so the user may interact with the app which was just moved to the front. - * <p> - * This listener is registered with SystemUI's ActivityManagerWrapper which dispatches - * these calls via a main thread Handler. - */ - @MainThread - private class BubbleTaskStackListener extends TaskStackChangeListener { - - @Override - public void onTaskMovedToFront(RunningTaskInfo taskInfo) { - int expandedId = getExpandedTaskId(); - if (expandedId != INVALID_TASK_ID && expandedId != taskInfo.taskId) { - mBubbleData.setExpanded(false); - } - } - - @Override - public void onActivityRestartAttempt(RunningTaskInfo taskInfo, boolean homeTaskVisible, - boolean clearedTask, boolean wasVisible) { - for (Bubble b : mBubbleData.getBubbles()) { - if (taskInfo.taskId == b.getTaskId()) { - mBubbleData.setSelectedBubble(b); - mBubbleData.setExpanded(true); - return; - } - } - } - - } - - /** - * Whether an intent is properly configured to display in an {@link android.app.ActivityView}. - * - * Keep checks in sync with NotificationManagerService#canLaunchInActivityView. Typically - * that should filter out any invalid bubbles, but should protect SysUI side just in case. - * - * @param context the context to use. - * @param entry the entry to bubble. - */ - static boolean canLaunchInActivityView(Context context, NotificationEntry entry) { - PendingIntent intent = entry.getBubbleMetadata() != null - ? entry.getBubbleMetadata().getIntent() - : null; - if (entry.getBubbleMetadata() != null - && entry.getBubbleMetadata().getShortcutId() != null) { - return true; - } - if (intent == null) { - Log.w(TAG, "Unable to create bubble -- no intent: " + entry.getKey()); - return false; - } - PackageManager packageManager = StatusBar.getPackageManagerForUser( - context, entry.getSbn().getUser().getIdentifier()); - ActivityInfo info = - intent.getIntent().resolveActivityInfo(packageManager, 0); - if (info == null) { - Log.w(TAG, "Unable to send as bubble, " - + entry.getKey() + " couldn't find activity info for intent: " - + intent); - return false; - } - if (!ActivityInfo.isResizeableMode(info.resizeMode)) { - Log.w(TAG, "Unable to send as bubble, " - + entry.getKey() + " activity is not resizable for intent: " - + intent); - return false; - } - return true; - } - - /** PinnedStackListener that dispatches IME visibility updates to the stack. */ - //TODO(b/170442945): Better way to do this / insets listener? - private class BubblesImeListener extends PinnedStackListenerForwarder.PinnedStackListener { - @Override - public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { - if (mStackView != null) { - mStackView.post(() -> mStackView.onImeVisibilityChanged(imeVisible, imeHeight)); - } - } - } - - static BubbleEntry notifToBubbleEntry(NotificationEntry e) { - return new BubbleEntry(e.getSbn(), e.getRanking(), e.isClearable(), - e.shouldSuppressNotificationDot(), e.shouldSuppressNotificationList(), - e.shouldSuppressPeek()); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java deleted file mode 100644 index b4626f27d370..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java +++ /dev/null @@ -1,824 +0,0 @@ -/* - * Copyright (C) 2019 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.internal.annotations.VisibleForTesting.Visibility.PACKAGE; -import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; -import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_DATA; -import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; -import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; - -import android.annotation.NonNull; -import android.app.PendingIntent; -import android.content.Context; -import android.content.pm.ShortcutInfo; -import android.util.Log; -import android.util.Pair; -import android.view.View; - -import androidx.annotation.Nullable; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.util.FrameworkStatsLog; -import com.android.systemui.R; -import com.android.systemui.bubbles.BubbleController.DismissReason; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Predicate; - -/** - * Keeps track of active bubbles. - */ -public class BubbleData { - - private BubbleLogger mLogger; - - private int mCurrentUserId; - - private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleData" : TAG_BUBBLES; - - private static final Comparator<Bubble> BUBBLES_BY_SORT_KEY_DESCENDING = - Comparator.comparing(BubbleData::sortKey).reversed(); - - /** Contains information about changes that have been made to the state of bubbles. */ - static final class Update { - boolean expandedChanged; - boolean selectionChanged; - boolean orderChanged; - boolean expanded; - @Nullable Bubble selectedBubble; - @Nullable Bubble addedBubble; - @Nullable Bubble updatedBubble; - @Nullable Bubble addedOverflowBubble; - @Nullable Bubble removedOverflowBubble; - // Pair with Bubble and @DismissReason Integer - final List<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>(); - - // A read-only view of the bubbles list, changes there will be reflected here. - final List<Bubble> bubbles; - final List<Bubble> overflowBubbles; - - private Update(List<Bubble> row, List<Bubble> overflow) { - bubbles = Collections.unmodifiableList(row); - overflowBubbles = Collections.unmodifiableList(overflow); - } - - boolean anythingChanged() { - return expandedChanged - || selectionChanged - || addedBubble != null - || updatedBubble != null - || !removedBubbles.isEmpty() - || addedOverflowBubble != null - || removedOverflowBubble != null - || orderChanged; - } - - void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) { - removedBubbles.add(new Pair<>(bubbleToRemove, reason)); - } - } - - /** - * This interface reports changes to the state and appearance of bubbles which should be applied - * as necessary to the UI. - */ - interface Listener { - /** Reports changes have have occurred as a result of the most recent operation. */ - void applyUpdate(Update update); - } - - interface TimeSource { - long currentTimeMillis(); - } - - private final Context mContext; - /** Bubbles that are actively in the stack. */ - private final List<Bubble> mBubbles; - /** Bubbles that aged out to overflow. */ - private final List<Bubble> mOverflowBubbles; - /** Bubbles that are being loaded but haven't been added to the stack just yet. */ - private final HashMap<String, Bubble> mPendingBubbles; - private Bubble mSelectedBubble; - private boolean mShowingOverflow; - private boolean mExpanded; - private final int mMaxBubbles; - private int mMaxOverflowBubbles; - - // State tracked during an operation -- keeps track of what listener events to dispatch. - private Update mStateChange; - - private TimeSource mTimeSource = System::currentTimeMillis; - - @Nullable - private Listener mListener; - - @Nullable - private BubbleController.NotificationSuppressionChangedListener mSuppressionListener; - private BubbleController.PendingIntentCanceledListener mCancelledListener; - - /** - * We track groups with summaries that aren't visibly displayed but still kept around because - * the bubble(s) associated with the summary still exist. - * - * The summary must be kept around so that developers can cancel it (and hence the bubbles - * associated with it). This list is used to check if the summary should be hidden from the - * shade. - * - * Key: group key of the notification - * Value: key of the notification - */ - private HashMap<String, String> mSuppressedGroupKeys = new HashMap<>(); - - public BubbleData(Context context, BubbleLogger bubbleLogger) { - mContext = context; - mLogger = bubbleLogger; - mBubbles = new ArrayList<>(); - mOverflowBubbles = new ArrayList<>(); - mPendingBubbles = new HashMap<>(); - mStateChange = new Update(mBubbles, mOverflowBubbles); - mMaxBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_rendered); - mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow); - } - - public void setSuppressionChangedListener( - BubbleController.NotificationSuppressionChangedListener listener) { - mSuppressionListener = listener; - } - - public void setPendingIntentCancelledListener( - BubbleController.PendingIntentCanceledListener listener) { - mCancelledListener = listener; - } - - public boolean hasBubbles() { - return !mBubbles.isEmpty(); - } - - public boolean isExpanded() { - return mExpanded; - } - - public boolean hasAnyBubbleWithKey(String key) { - return hasBubbleInStackWithKey(key) || hasOverflowBubbleWithKey(key); - } - - public boolean hasBubbleInStackWithKey(String key) { - return getBubbleInStackWithKey(key) != null; - } - - public boolean hasOverflowBubbleWithKey(String key) { - return getOverflowBubbleWithKey(key) != null; - } - - @Nullable - public Bubble getSelectedBubble() { - return mSelectedBubble; - } - - /** Return a read-only current active bubble lists. */ - public List<Bubble> getActiveBubbles() { - return Collections.unmodifiableList(mBubbles); - } - - public void setExpanded(boolean expanded) { - if (DEBUG_BUBBLE_DATA) { - Log.d(TAG, "setExpanded: " + expanded); - } - setExpandedInternal(expanded); - dispatchPendingChanges(); - } - - public void setSelectedBubble(Bubble bubble) { - if (DEBUG_BUBBLE_DATA) { - Log.d(TAG, "setSelectedBubble: " + bubble); - } - setSelectedBubbleInternal(bubble); - dispatchPendingChanges(); - } - - void setShowingOverflow(boolean showingOverflow) { - mShowingOverflow = showingOverflow; - } - - /** - * Constructs a new bubble or returns an existing one. Does not add new bubbles to - * bubble data, must go through {@link #notificationEntryUpdated(Bubble, boolean, boolean)} - * for that. - * - * @param entry The notification entry to use, only null if it's a bubble being promoted from - * the overflow that was persisted over reboot. - * @param persistedBubble The bubble to use, only non-null if it's a bubble being promoted from - * the overflow that was persisted over reboot. - */ - public Bubble getOrCreateBubble(BubbleEntry entry, Bubble persistedBubble) { - String key = persistedBubble != null ? persistedBubble.getKey() : entry.getKey(); - Bubble bubbleToReturn = getBubbleInStackWithKey(key); - - if (bubbleToReturn == null) { - bubbleToReturn = getOverflowBubbleWithKey(key); - if (bubbleToReturn != null) { - // Promoting from overflow - mOverflowBubbles.remove(bubbleToReturn); - } else if (mPendingBubbles.containsKey(key)) { - // Update while it was pending - bubbleToReturn = mPendingBubbles.get(key); - } else if (entry != null) { - // New bubble - bubbleToReturn = new Bubble(entry, mSuppressionListener, mCancelledListener); - } else { - // Persisted bubble being promoted - bubbleToReturn = persistedBubble; - } - } - - if (entry != null) { - bubbleToReturn.setEntry(entry); - } - mPendingBubbles.put(key, bubbleToReturn); - return bubbleToReturn; - } - - /** - * When this method is called it is expected that all info in the bubble has completed loading. - * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, - * BubbleStackView, BubbleIconFactory, boolean). - */ - void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) { - if (DEBUG_BUBBLE_DATA) { - Log.d(TAG, "notificationEntryUpdated: " + bubble); - } - mPendingBubbles.remove(bubble.getKey()); // No longer pending once we're here - Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey()); - suppressFlyout |= !bubble.isVisuallyInterruptive(); - - if (prevBubble == null) { - // Create a new bubble - bubble.setSuppressFlyout(suppressFlyout); - doAdd(bubble); - trim(); - } else { - // Updates an existing bubble - bubble.setSuppressFlyout(suppressFlyout); - // If there is no flyout, we probably shouldn't show the bubble at the top - doUpdate(bubble, !suppressFlyout /* reorder */); - } - - if (bubble.shouldAutoExpand()) { - bubble.setShouldAutoExpand(false); - setSelectedBubbleInternal(bubble); - if (!mExpanded) { - setExpandedInternal(true); - } - } - - boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble; - boolean suppress = isBubbleExpandedAndSelected || !showInShade || !bubble.showInShade(); - bubble.setSuppressNotification(suppress); - bubble.setShowDot(!isBubbleExpandedAndSelected /* show */); - - dispatchPendingChanges(); - } - - /** - * Dismisses the bubble with the matching key, if it exists. - */ - public void dismissBubbleWithKey(String key, @DismissReason int reason) { - if (DEBUG_BUBBLE_DATA) { - Log.d(TAG, "notificationEntryRemoved: key=" + key + " reason=" + reason); - } - doRemove(key, reason); - dispatchPendingChanges(); - } - - /** - * Adds a group key indicating that the summary for this group should be suppressed. - * - * @param groupKey the group key of the group whose summary should be suppressed. - * @param notifKey the notification entry key of that summary. - */ - void addSummaryToSuppress(String groupKey, String notifKey) { - mSuppressedGroupKeys.put(groupKey, notifKey); - } - - /** - * Retrieves the notif entry key of the summary associated with the provided group key. - * - * @param groupKey the group to look up - * @return the key for the notification that is the summary of this group. - */ - String getSummaryKey(String groupKey) { - return mSuppressedGroupKeys.get(groupKey); - } - - /** - * Removes a group key indicating that summary for this group should no longer be suppressed. - */ - void removeSuppressedSummary(String groupKey) { - mSuppressedGroupKeys.remove(groupKey); - } - - /** - * Whether the summary for the provided group key is suppressed. - */ - boolean isSummarySuppressed(String groupKey) { - return mSuppressedGroupKeys.containsKey(groupKey); - } - - /** - * Removes bubbles from the given package whose shortcut are not in the provided list of valid - * shortcuts. - */ - public void removeBubblesWithInvalidShortcuts( - String packageName, List<ShortcutInfo> validShortcuts, int reason) { - - final Set<String> validShortcutIds = new HashSet<String>(); - for (ShortcutInfo info : validShortcuts) { - validShortcutIds.add(info.getId()); - } - - final Predicate<Bubble> invalidBubblesFromPackage = bubble -> { - final boolean bubbleIsFromPackage = packageName.equals(bubble.getPackageName()); - final boolean isShortcutBubble = bubble.hasMetadataShortcutId(); - if (!bubbleIsFromPackage || !isShortcutBubble) { - return false; - } - final boolean hasShortcutIdAndValidShortcut = - bubble.hasMetadataShortcutId() - && bubble.getShortcutInfo() != null - && bubble.getShortcutInfo().isEnabled() - && validShortcutIds.contains(bubble.getShortcutInfo().getId()); - return bubbleIsFromPackage && !hasShortcutIdAndValidShortcut; - }; - - final Consumer<Bubble> removeBubble = bubble -> - dismissBubbleWithKey(bubble.getKey(), reason); - - performActionOnBubblesMatching(getBubbles(), invalidBubblesFromPackage, removeBubble); - performActionOnBubblesMatching( - getOverflowBubbles(), invalidBubblesFromPackage, removeBubble); - } - - /** Dismisses all bubbles from the given package. */ - public void removeBubblesWithPackageName(String packageName, int reason) { - final Predicate<Bubble> bubbleMatchesPackage = bubble -> - bubble.getPackageName().equals(packageName); - - final Consumer<Bubble> removeBubble = bubble -> - dismissBubbleWithKey(bubble.getKey(), reason); - - performActionOnBubblesMatching(getBubbles(), bubbleMatchesPackage, removeBubble); - performActionOnBubblesMatching(getOverflowBubbles(), bubbleMatchesPackage, removeBubble); - } - - private void doAdd(Bubble bubble) { - if (DEBUG_BUBBLE_DATA) { - Log.d(TAG, "doAdd: " + bubble); - } - mBubbles.add(0, bubble); - mStateChange.addedBubble = bubble; - // Adding the first bubble doesn't change the order - mStateChange.orderChanged = mBubbles.size() > 1; - if (!isExpanded()) { - setSelectedBubbleInternal(mBubbles.get(0)); - } - } - - private void trim() { - if (mBubbles.size() > mMaxBubbles) { - mBubbles.stream() - // sort oldest first (ascending lastActivity) - .sorted(Comparator.comparingLong(Bubble::getLastActivity)) - // skip the selected bubble - .filter((b) -> !b.equals(mSelectedBubble)) - .findFirst() - .ifPresent((b) -> doRemove(b.getKey(), BubbleController.DISMISS_AGED)); - } - } - - private void doUpdate(Bubble bubble, boolean reorder) { - if (DEBUG_BUBBLE_DATA) { - Log.d(TAG, "doUpdate: " + bubble); - } - mStateChange.updatedBubble = bubble; - if (!isExpanded() && reorder) { - int prevPos = mBubbles.indexOf(bubble); - mBubbles.remove(bubble); - mBubbles.add(0, bubble); - mStateChange.orderChanged = prevPos != 0; - setSelectedBubbleInternal(mBubbles.get(0)); - } - } - - /** Runs the given action on Bubbles that match the given predicate. */ - private void performActionOnBubblesMatching( - List<Bubble> bubbles, Predicate<Bubble> predicate, Consumer<Bubble> action) { - final List<Bubble> matchingBubbles = new ArrayList<>(); - for (Bubble bubble : bubbles) { - if (predicate.test(bubble)) { - matchingBubbles.add(bubble); - } - } - - for (Bubble matchingBubble : matchingBubbles) { - action.accept(matchingBubble); - } - } - - private void doRemove(String key, @DismissReason int reason) { - if (DEBUG_BUBBLE_DATA) { - Log.d(TAG, "doRemove: " + key); - } - // If it was pending remove it - if (mPendingBubbles.containsKey(key)) { - mPendingBubbles.remove(key); - } - int indexToRemove = indexForKey(key); - if (indexToRemove == -1) { - if (hasOverflowBubbleWithKey(key) - && (reason == BubbleController.DISMISS_NOTIF_CANCEL - || reason == BubbleController.DISMISS_GROUP_CANCELLED - || reason == BubbleController.DISMISS_NO_LONGER_BUBBLE - || reason == BubbleController.DISMISS_BLOCKED - || reason == BubbleController.DISMISS_SHORTCUT_REMOVED - || reason == BubbleController.DISMISS_PACKAGE_REMOVED)) { - - Bubble b = getOverflowBubbleWithKey(key); - if (DEBUG_BUBBLE_DATA) { - Log.d(TAG, "Cancel overflow bubble: " + b); - } - if (b != null) { - b.stopInflation(); - } - mLogger.logOverflowRemove(b, reason); - mOverflowBubbles.remove(b); - mStateChange.bubbleRemoved(b, reason); - mStateChange.removedOverflowBubble = b; - } - return; - } - Bubble bubbleToRemove = mBubbles.get(indexToRemove); - bubbleToRemove.stopInflation(); - if (mBubbles.size() == 1) { - // Going to become empty, handle specially. - setExpandedInternal(false); - // Don't use setSelectedBubbleInternal because we don't want to trigger an applyUpdate - mSelectedBubble = null; - } - if (indexToRemove < mBubbles.size() - 1) { - // Removing anything but the last bubble means positions will change. - mStateChange.orderChanged = true; - } - mBubbles.remove(indexToRemove); - mStateChange.bubbleRemoved(bubbleToRemove, reason); - if (!isExpanded()) { - mStateChange.orderChanged |= repackAll(); - } - - overflowBubble(reason, bubbleToRemove); - - // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null. - if (Objects.equals(mSelectedBubble, bubbleToRemove)) { - // Move selection to the new bubble at the same position. - int newIndex = Math.min(indexToRemove, mBubbles.size() - 1); - Bubble newSelected = mBubbles.get(newIndex); - setSelectedBubbleInternal(newSelected); - } - maybeSendDeleteIntent(reason, bubbleToRemove); - } - - void overflowBubble(@DismissReason int reason, Bubble bubble) { - if (bubble.getPendingIntentCanceled() - || !(reason == BubbleController.DISMISS_AGED - || reason == BubbleController.DISMISS_USER_GESTURE)) { - return; - } - if (DEBUG_BUBBLE_DATA) { - Log.d(TAG, "Overflowing: " + bubble); - } - mLogger.logOverflowAdd(bubble, reason); - mOverflowBubbles.add(0, bubble); - mStateChange.addedOverflowBubble = bubble; - bubble.stopInflation(); - if (mOverflowBubbles.size() == mMaxOverflowBubbles + 1) { - // Remove oldest bubble. - Bubble oldest = mOverflowBubbles.get(mOverflowBubbles.size() - 1); - if (DEBUG_BUBBLE_DATA) { - Log.d(TAG, "Overflow full. Remove: " + oldest); - } - mStateChange.bubbleRemoved(oldest, BubbleController.DISMISS_OVERFLOW_MAX_REACHED); - mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_MAX_REACHED); - mOverflowBubbles.remove(oldest); - mStateChange.removedOverflowBubble = oldest; - } - } - - public void dismissAll(@DismissReason int reason) { - if (DEBUG_BUBBLE_DATA) { - Log.d(TAG, "dismissAll: reason=" + reason); - } - if (mBubbles.isEmpty()) { - return; - } - setExpandedInternal(false); - setSelectedBubbleInternal(null); - while (!mBubbles.isEmpty()) { - doRemove(mBubbles.get(0).getKey(), reason); - } - dispatchPendingChanges(); - } - - private void dispatchPendingChanges() { - if (mListener != null && mStateChange.anythingChanged()) { - mListener.applyUpdate(mStateChange); - } - mStateChange = new Update(mBubbles, mOverflowBubbles); - } - - /** - * Requests a change to the selected bubble. - * - * @param bubble the new selected bubble - */ - private void setSelectedBubbleInternal(@Nullable Bubble bubble) { - if (DEBUG_BUBBLE_DATA) { - Log.d(TAG, "setSelectedBubbleInternal: " + bubble); - } - if (!mShowingOverflow && Objects.equals(bubble, mSelectedBubble)) { - return; - } - // Otherwise, if we are showing the overflow menu, return to the previously selected bubble. - - if (bubble != null && !mBubbles.contains(bubble) && !mOverflowBubbles.contains(bubble)) { - Log.e(TAG, "Cannot select bubble which doesn't exist!" - + " (" + bubble + ") bubbles=" + mBubbles); - return; - } - if (mExpanded && bubble != null) { - bubble.markAsAccessedAt(mTimeSource.currentTimeMillis()); - } - mSelectedBubble = bubble; - mStateChange.selectedBubble = bubble; - mStateChange.selectionChanged = true; - } - - void setCurrentUserId(int uid) { - mCurrentUserId = uid; - } - - /** - * Logs the bubble UI event. - * - * @param provider The bubble view provider that is being interacted on. Null value indicates - * that the user interaction is not specific to one bubble. - * @param action The user interaction enum - * @param packageName SystemUI package - * @param bubbleCount Number of bubbles in the stack - * @param bubbleIndex Index of bubble in the stack - * @param normalX Normalized x position of the stack - * @param normalY Normalized y position of the stack - */ - void logBubbleEvent(@Nullable BubbleViewProvider provider, int action, String packageName, - int bubbleCount, int bubbleIndex, float normalX, float normalY) { - if (provider == null) { - mLogger.logStackUiChanged(packageName, action, bubbleCount, normalX, normalY); - } else if (provider.getKey().equals(BubbleOverflow.KEY)) { - if (action == FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED) { - mLogger.logShowOverflow(packageName, mCurrentUserId); - } - } else { - mLogger.logBubbleUiChanged((Bubble) provider, packageName, action, bubbleCount, normalX, - normalY, bubbleIndex); - } - } - - /** - * Requests a change to the expanded state. - * - * @param shouldExpand the new requested state - */ - private void setExpandedInternal(boolean shouldExpand) { - if (DEBUG_BUBBLE_DATA) { - Log.d(TAG, "setExpandedInternal: shouldExpand=" + shouldExpand); - } - if (mExpanded == shouldExpand) { - return; - } - if (shouldExpand) { - if (mBubbles.isEmpty()) { - Log.e(TAG, "Attempt to expand stack when empty!"); - return; - } - if (mSelectedBubble == null) { - Log.e(TAG, "Attempt to expand stack without selected bubble!"); - return; - } - mSelectedBubble.markAsAccessedAt(mTimeSource.currentTimeMillis()); - mStateChange.orderChanged |= repackAll(); - } else if (!mBubbles.isEmpty()) { - // Apply ordering and grouping rules from expanded -> collapsed, then save - // the result. - mStateChange.orderChanged |= repackAll(); - // Save the state which should be returned to when expanded (with no other changes) - - if (mShowingOverflow) { - // Show previously selected bubble instead of overflow menu on next expansion. - setSelectedBubbleInternal(mSelectedBubble); - } - if (mBubbles.indexOf(mSelectedBubble) > 0) { - // Move the selected bubble to the top while collapsed. - int index = mBubbles.indexOf(mSelectedBubble); - if (index != 0) { - mBubbles.remove(mSelectedBubble); - mBubbles.add(0, mSelectedBubble); - mStateChange.orderChanged = true; - } - } - } - mExpanded = shouldExpand; - mStateChange.expanded = shouldExpand; - mStateChange.expandedChanged = true; - } - - private static long sortKey(Bubble bubble) { - return bubble.getLastActivity(); - } - - /** - * This applies a full sort and group pass to all existing bubbles. - * Bubbles are sorted by lastUpdated descending. - * - * @return true if the position of any bubbles changed as a result - */ - private boolean repackAll() { - if (DEBUG_BUBBLE_DATA) { - Log.d(TAG, "repackAll()"); - } - if (mBubbles.isEmpty()) { - return false; - } - List<Bubble> repacked = new ArrayList<>(mBubbles.size()); - // Add bubbles, freshest to oldest - mBubbles.stream() - .sorted(BUBBLES_BY_SORT_KEY_DESCENDING) - .forEachOrdered(repacked::add); - if (repacked.equals(mBubbles)) { - return false; - } - mBubbles.clear(); - mBubbles.addAll(repacked); - return true; - } - - private void maybeSendDeleteIntent(@DismissReason int reason, @NonNull final Bubble bubble) { - if (reason != BubbleController.DISMISS_USER_GESTURE) return; - PendingIntent deleteIntent = bubble.getDeleteIntent(); - if (deleteIntent == null) return; - try { - deleteIntent.send(); - } catch (PendingIntent.CanceledException e) { - Log.w(TAG, "Failed to send delete intent for bubble with key: " + bubble.getKey()); - } - } - - private int indexForKey(String key) { - for (int i = 0; i < mBubbles.size(); i++) { - Bubble bubble = mBubbles.get(i); - if (bubble.getKey().equals(key)) { - return i; - } - } - return -1; - } - - /** - * The set of bubbles in row. - */ - @VisibleForTesting(visibility = PACKAGE) - public List<Bubble> getBubbles() { - return Collections.unmodifiableList(mBubbles); - } - - /** - * The set of bubbles in overflow. - */ - @VisibleForTesting(visibility = PRIVATE) - List<Bubble> getOverflowBubbles() { - return Collections.unmodifiableList(mOverflowBubbles); - } - - @VisibleForTesting(visibility = PRIVATE) - @Nullable - Bubble getAnyBubbleWithkey(String key) { - Bubble b = getBubbleInStackWithKey(key); - if (b == null) { - b = getOverflowBubbleWithKey(key); - } - return b; - } - - @VisibleForTesting(visibility = PRIVATE) - @Nullable - Bubble getBubbleInStackWithKey(String key) { - for (int i = 0; i < mBubbles.size(); i++) { - Bubble bubble = mBubbles.get(i); - if (bubble.getKey().equals(key)) { - return bubble; - } - } - return null; - } - - @Nullable - Bubble getBubbleWithView(View view) { - for (int i = 0; i < mBubbles.size(); i++) { - Bubble bubble = mBubbles.get(i); - if (bubble.getIconView() != null && bubble.getIconView().equals(view)) { - return bubble; - } - } - return null; - } - - @VisibleForTesting(visibility = PRIVATE) - Bubble getOverflowBubbleWithKey(String key) { - for (int i = 0; i < mOverflowBubbles.size(); i++) { - Bubble bubble = mOverflowBubbles.get(i); - if (bubble.getKey().equals(key)) { - return bubble; - } - } - return null; - } - - @VisibleForTesting(visibility = PRIVATE) - void setTimeSource(TimeSource timeSource) { - mTimeSource = timeSource; - } - - public void setListener(Listener listener) { - mListener = listener; - } - - /** - * Set maximum number of bubbles allowed in overflow. - * This method should only be used in tests, not in production. - */ - @VisibleForTesting - void setMaxOverflowBubbles(int maxOverflowBubbles) { - mMaxOverflowBubbles = maxOverflowBubbles; - } - - /** - * Description of current bubble data state. - */ - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.print("selected: "); - pw.println(mSelectedBubble != null - ? mSelectedBubble.getKey() - : "null"); - pw.print("expanded: "); - pw.println(mExpanded); - - pw.print("stack bubble count: "); - pw.println(mBubbles.size()); - for (Bubble bubble : mBubbles) { - bubble.dump(fd, pw, args); - } - - pw.print("overflow bubble count: "); - pw.println(mOverflowBubbles.size()); - for (Bubble bubble : mOverflowBubbles) { - bubble.dump(fd, pw, args); - } - - pw.print("summaryKeys: "); - pw.println(mSuppressedGroupKeys.size()); - for (String key : mSuppressedGroupKeys.keySet()) { - pw.println(" suppressing: " + key); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt deleted file mode 100644 index 2ab9e8734bef..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt +++ /dev/null @@ -1,178 +0,0 @@ -/* - * 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.annotation.SuppressLint -import android.annotation.UserIdInt -import android.content.Context -import android.content.pm.LauncherApps -import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED -import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC -import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER -import android.os.UserHandle -import android.util.Log -import com.android.systemui.bubbles.storage.BubbleEntity -import com.android.systemui.bubbles.storage.BubblePersistentRepository -import com.android.systemui.bubbles.storage.BubbleVolatileRepository -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.launch -import kotlinx.coroutines.yield - -internal class BubbleDataRepository(context: Context, private val launcherApps: LauncherApps) { - private val volatileRepository = BubbleVolatileRepository(launcherApps) - private val persistentRepository = BubblePersistentRepository(context) - - private val ioScope = CoroutineScope(Dispatchers.IO) - private val uiScope = CoroutineScope(Dispatchers.Main) - private var job: Job? = null - - /** - * Adds the bubble in memory, then persists the snapshot after adding the bubble to disk - * asynchronously. - */ - fun addBubble(@UserIdInt userId: Int, bubble: Bubble) = addBubbles(userId, listOf(bubble)) - - /** - * Adds the bubble in memory, then persists the snapshot after adding the bubble to disk - * asynchronously. - */ - fun addBubbles(@UserIdInt userId: Int, bubbles: List<Bubble>) { - if (DEBUG) Log.d(TAG, "adding ${bubbles.size} bubbles") - val entities = transform(userId, bubbles).also(volatileRepository::addBubbles) - if (entities.isNotEmpty()) persistToDisk() - } - - /** - * Removes the bubbles from memory, then persists the snapshot to disk asynchronously. - */ - fun removeBubbles(@UserIdInt userId: Int, bubbles: List<Bubble>) { - if (DEBUG) Log.d(TAG, "removing ${bubbles.size} bubbles") - val entities = transform(userId, bubbles).also(volatileRepository::removeBubbles) - if (entities.isNotEmpty()) persistToDisk() - } - - private fun transform(userId: Int, bubbles: List<Bubble>): List<BubbleEntity> { - return bubbles.mapNotNull { b -> - BubbleEntity( - userId, - b.packageName, - b.metadataShortcutId ?: return@mapNotNull null, - b.key, - b.rawDesiredHeight, - b.rawDesiredHeightResId, - b.title - ) - } - } - - /** - * Persists the bubbles to disk. When being called multiple times, it waits for first ongoing - * write operation to finish then run another write operation exactly once. - * - * e.g. - * Job A started -> blocking I/O - * Job B started, cancels A, wait for blocking I/O in A finishes - * Job C started, cancels B, wait for job B to finish - * Job D started, cancels C, wait for job C to finish - * Job A completed - * Job B resumes and reaches yield() and is then cancelled - * Job C resumes and reaches yield() and is then cancelled - * Job D resumes and performs another blocking I/O - */ - private fun persistToDisk() { - val prev = job - job = ioScope.launch { - // if there was an ongoing disk I/O operation, they can be cancelled - prev?.cancelAndJoin() - // check for cancellation before disk I/O - yield() - // save to disk - persistentRepository.persistsToDisk(volatileRepository.bubbles) - } - } - - /** - * Load bubbles from disk. - */ - @SuppressLint("WrongConstant") - fun loadBubbles(cb: (List<Bubble>) -> Unit) = ioScope.launch { - /** - * Load BubbleEntity from disk. - * e.g. - * [ - * BubbleEntity(0, "com.example.messenger", "id-2"), - * BubbleEntity(10, "com.example.chat", "my-id1") - * BubbleEntity(0, "com.example.messenger", "id-1") - * ] - */ - val entities = persistentRepository.readFromDisk() - volatileRepository.addBubbles(entities) - /** - * Extract userId/packageName from these entities. - * e.g. - * [ - * ShortcutKey(0, "com.example.messenger"), ShortcutKey(0, "com.example.chat") - * ] - */ - val shortcutKeys = entities.map { ShortcutKey(it.userId, it.packageName) }.toSet() - /** - * Retrieve shortcuts with given userId/packageName combination, then construct a mapping - * from the userId/packageName pair to a list of associated ShortcutInfo. - * e.g. - * { - * ShortcutKey(0, "com.example.messenger") -> [ - * ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-0"), - * ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-2") - * ] - * ShortcutKey(10, "com.example.chat") -> [ - * ShortcutInfo(userId=10, pkg="com.example.chat", id="id-1"), - * ShortcutInfo(userId=10, pkg="com.example.chat", id="id-3") - * ] - * } - */ - val shortcutMap = shortcutKeys.flatMap { key -> - launcherApps.getShortcuts( - LauncherApps.ShortcutQuery() - .setPackage(key.pkg) - .setQueryFlags(SHORTCUT_QUERY_FLAG), UserHandle.of(key.userId)) - ?: emptyList() - }.groupBy { ShortcutKey(it.userId, it.`package`) } - // For each entity loaded from xml, find the corresponding ShortcutInfo then convert them - // into Bubble. - val bubbles = entities.mapNotNull { entity -> - shortcutMap[ShortcutKey(entity.userId, entity.packageName)] - ?.firstOrNull { shortcutInfo -> entity.shortcutId == shortcutInfo.id } - ?.let { shortcutInfo -> Bubble( - entity.key, - shortcutInfo, - entity.desiredHeight, - entity.desiredHeightResId, - entity.title - ) } - } - uiScope.launch { cb(bubbles) } - } -} - -data class ShortcutKey(val userId: Int, val pkg: String) - -private const val TAG = "BubbleDataRepository" -private const val DEBUG = false -private const val SHORTCUT_QUERY_FLAG = - FLAG_MATCH_DYNAMIC or FLAG_MATCH_PINNED_BY_ANY_LAUNCHER or FLAG_MATCH_CACHED
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDebugConfig.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDebugConfig.java deleted file mode 100644 index d98fee399470..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDebugConfig.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2019 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.provider.Settings; - -import java.util.List; - -/** - * Common class for the various debug {@link android.util.Log} output configuration in the Bubbles - * package. - */ -public class BubbleDebugConfig { - - // All output logs in the Bubbles package use the {@link #TAG_BUBBLES} string for tagging their - // log output. This makes it easy to identify the origin of the log message when sifting - // through a large amount of log output from multiple sources. However, it also makes trying - // to figure-out the origin of a log message while debugging the Bubbles a little painful. By - // setting this constant to true, log messages from the Bubbles package will be tagged with - // their class names instead fot the generic tag. - static final boolean TAG_WITH_CLASS_NAME = false; - - // Default log tag for the Bubbles package. - static final String TAG_BUBBLES = "Bubbles"; - - static final boolean DEBUG_BUBBLE_CONTROLLER = false; - static final boolean DEBUG_BUBBLE_DATA = false; - static final boolean DEBUG_BUBBLE_STACK_VIEW = false; - 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<Bubble> bubbles, BubbleViewProvider selected) { - StringBuilder sb = new StringBuilder(); - for (Bubble bubble : bubbles) { - if (bubble == null) { - sb.append(" <null> !!!!!\n"); - } else { - boolean isSelected = (selected != null - && selected.getKey() != BubbleOverflow.KEY - && bubble == selected); - String arrow = isSelected ? "=>" : " "; - sb.append(String.format("%s Bubble{act=%12d, showInShade=%d, key=%s}\n", - arrow, - bubble.getLastActivity(), - (bubble.showInShade() ? 1 : 0), - bubble.getKey())); - } - } - return sb.toString(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleEntry.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleEntry.java deleted file mode 100644 index 6a1302518699..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleEntry.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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.app.Notification.BubbleMetadata; -import android.app.NotificationManager.Policy; -import android.service.notification.NotificationListenerService.Ranking; -import android.service.notification.StatusBarNotification; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** - * Represents a notification with needed data and flag for bubbles. - * - * @see Bubble - */ -public class BubbleEntry { - - private StatusBarNotification mSbn; - private Ranking mRanking; - - private boolean mIsClearable; - private boolean mShouldSuppressNotificationDot; - private boolean mShouldSuppressNotificationList; - private boolean mShouldSuppressPeek; - - public BubbleEntry(@NonNull StatusBarNotification sbn, - Ranking ranking, boolean isClearable, boolean shouldSuppressNotificationDot, - boolean shouldSuppressNotificationList, boolean shouldSuppressPeek) { - mSbn = sbn; - mRanking = ranking; - - mIsClearable = isClearable; - mShouldSuppressNotificationDot = shouldSuppressNotificationDot; - mShouldSuppressNotificationList = shouldSuppressNotificationList; - mShouldSuppressPeek = shouldSuppressPeek; - } - - /** @return the {@link StatusBarNotification} for this entry. */ - @NonNull - public StatusBarNotification getStatusBarNotification() { - return mSbn; - } - - /** @return the {@link Ranking} for this entry. */ - public Ranking getRanking() { - return mRanking; - } - - /** @return the key in the {@link StatusBarNotification}. */ - public String getKey() { - return mSbn.getKey(); - } - - /** @return the {@link BubbleMetadata} in the {@link StatusBarNotification}. */ - @Nullable - public BubbleMetadata getBubbleMetadata() { - return getStatusBarNotification().getNotification().getBubbleMetadata(); - } - - /** @return true if this notification is clearable. */ - public boolean isClearable() { - return mIsClearable; - } - - /** @return true if {@link Policy#SUPPRESSED_EFFECT_BADGE} set for this notification. */ - public boolean shouldSuppressNotificationDot() { - return mShouldSuppressNotificationDot; - } - - /** - * @return true if {@link Policy#SUPPRESSED_EFFECT_NOTIFICATION_LIST} - * set for this notification. - */ - public boolean shouldSuppressNotificationList() { - return mShouldSuppressNotificationList; - } - - /** @return true if {@link Policy#SUPPRESSED_EFFECT_PEEK} set for this notification. */ - public boolean shouldSuppressPeek() { - return mShouldSuppressPeek; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java deleted file mode 100644 index 98a2257d2daa..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java +++ /dev/null @@ -1,630 +0,0 @@ -/* - * 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.app.ActivityTaskManager.INVALID_TASK_ID; -import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; -import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; -import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; - -import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW; -import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; -import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; -import static com.android.systemui.bubbles.BubbleOverflowActivity.EXTRA_BUBBLE_CONTROLLER; - -import android.annotation.NonNull; -import android.annotation.SuppressLint; -import android.app.ActivityOptions; -import android.app.PendingIntent; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Color; -import android.graphics.Outline; -import android.graphics.Point; -import android.graphics.Rect; -import android.graphics.drawable.ShapeDrawable; -import android.os.Bundle; -import android.util.AttributeSet; -import android.util.Log; -import android.view.SurfaceControl; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewOutlineProvider; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityNodeInfo; -import android.widget.FrameLayout; -import android.widget.LinearLayout; - -import androidx.annotation.Nullable; - -import com.android.internal.policy.ScreenDecorationsUtils; -import com.android.systemui.Dependency; -import com.android.systemui.R; -import com.android.systemui.recents.TriangleShape; -import com.android.systemui.statusbar.AlphaOptimizedButton; - -import java.io.FileDescriptor; -import java.io.PrintWriter; - -/** - * Container for the expanded bubble view, handles rendering the caret and settings icon. - */ -public class BubbleExpandedView extends LinearLayout { - private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES; - - // The triangle pointing to the expanded view - private View mPointerView; - private int mPointerMargin; - @Nullable private int[] mExpandedViewContainerLocation; - - private AlphaOptimizedButton mSettingsIcon; - private TaskView mTaskView; - - private int mTaskId = INVALID_TASK_ID; - - private boolean mImeVisible; - private boolean mNeedsNewHeight; - - private Point mDisplaySize; - private int mMinHeight; - private int mOverflowHeight; - private int mSettingsIconHeight; - private int mPointerWidth; - private int mPointerHeight; - private ShapeDrawable mPointerDrawable; - private int mExpandedViewPadding; - private float mCornerRadius = 0f; - - @Nullable private Bubble mBubble; - private PendingIntent mPendingIntent; - - private boolean mIsOverflow; - - private Bubbles mBubbles = Dependency.get(Bubbles.class); - private WindowManager mWindowManager; - private BubbleStackView mStackView; - - /** - * Container for the ActivityView that has a solid, round-rect background that shows if the - * ActivityView hasn't loaded. - */ - private final FrameLayout mExpandedViewContainer = new FrameLayout(getContext()); - - private final TaskView.Listener mTaskViewListener = new TaskView.Listener() { - private boolean mInitialized = false; - private boolean mDestroyed = false; - - @Override - public void onInitialized() { - if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "onActivityViewReady: destroyed=" + mDestroyed - + " initialized=" + mInitialized - + " bubble=" + getBubbleKey()); - } - - if (mDestroyed || mInitialized) { - return; - } - // Custom options so there is no activity transition animation - ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(), - 0 /* enterResId */, 0 /* exitResId */); - - // TODO: I notice inconsistencies in lifecycle - // Post to keep the lifecycle normal - post(() -> { - if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "onActivityViewReady: calling startActivity, bubble=" - + getBubbleKey()); - } - try { - if (!mIsOverflow && mBubble.hasMetadataShortcutId()) { - mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), - options, null /* sourceBounds */); - } else { - Intent fillInIntent = new Intent(); - // Apply flags to make behaviour match documentLaunchMode=always. - fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); - fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); - if (mBubble != null) { - mBubble.setIntentActive(); - } - mTaskView.startActivity(mPendingIntent, fillInIntent, options); - } - } catch (RuntimeException e) { - // If there's a runtime exception here then there's something - // wrong with the intent, we can't really recover / try to populate - // the bubble again so we'll just remove it. - Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey() - + ", " + e.getMessage() + "; removing bubble"); - mBubbles.removeBubble(getBubbleKey(), - BubbleController.DISMISS_INVALID_INTENT); - } - }); - mInitialized = true; - } - - @Override - public void onReleased() { - mDestroyed = true; - } - - @Override - public void onTaskCreated(int taskId, ComponentName name) { - if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "onTaskCreated: taskId=" + taskId - + " bubble=" + getBubbleKey()); - } - // The taskId is saved to use for removeTask, preventing appearance in recent tasks. - mTaskId = taskId; - - // With the task org, the taskAppeared callback will only happen once the task has - // already drawn - setContentVisibility(true); - } - - @Override - public void onTaskVisibilityChanged(int taskId, boolean visible) { - setContentVisibility(visible); - } - - @Override - public void onTaskRemovalStarted(int taskId) { - if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId - + " bubble=" + getBubbleKey()); - } - if (mBubble != null) { - // Must post because this is called from a binder thread. - post(() -> mBubbles.removeBubble(mBubble.getKey(), - BubbleController.DISMISS_TASK_FINISHED)); - } - } - - @Override - public void onBackPressedOnTaskRoot(int taskId) { - if (mTaskId == taskId && mStackView.isExpanded()) { - mBubbles.collapseStack(); - } - } - }; - - public BubbleExpandedView(Context context) { - this(context, null); - } - - public BubbleExpandedView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - updateDimensions(); - } - - void updateDimensions() { - mDisplaySize = new Point(); - mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); - // Get the real size -- this includes screen decorations (notches, statusbar, navbar). - mWindowManager.getDefaultDisplay().getRealSize(mDisplaySize); - Resources res = getResources(); - mMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height); - mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height); - mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin); - } - - @SuppressLint("ClickableViewAccessibility") - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - Resources res = getResources(); - mPointerView = findViewById(R.id.pointer_view); - mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width); - mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); - - mPointerDrawable = new ShapeDrawable(TriangleShape.create( - mPointerWidth, mPointerHeight, true /* pointUp */)); - mPointerView.setVisibility(INVISIBLE); - - mSettingsIconHeight = getContext().getResources().getDimensionPixelSize( - R.dimen.bubble_manage_button_height); - mSettingsIcon = findViewById(R.id.settings_button); - - mTaskView = new TaskView(mContext, mBubbles.getTaskManager()); - // Set ActivityView's alpha value as zero, since there is no view content to be shown. - setContentVisibility(false); - - mExpandedViewContainer.setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius); - } - }); - mExpandedViewContainer.setClipToOutline(true); - mExpandedViewContainer.addView(mTaskView); - mExpandedViewContainer.setLayoutParams( - new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); - addView(mExpandedViewContainer); - - // Expanded stack layout, top to bottom: - // Expanded view container - // ==> bubble row - // ==> expanded view - // ==> activity view - // ==> manage button - bringChildToFront(mTaskView); - bringChildToFront(mSettingsIcon); - mTaskView.setListener(mTaskViewListener); - - applyThemeAttrs(); - - mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); - setPadding(mExpandedViewPadding, mExpandedViewPadding, mExpandedViewPadding, - mExpandedViewPadding); - setOnTouchListener((view, motionEvent) -> { - if (mTaskView == null) { - return false; - } - - final Rect avBounds = new Rect(); - mTaskView.getBoundsOnScreen(avBounds); - - // Consume and ignore events on the expanded view padding that are within the - // ActivityView's vertical bounds. These events are part of a back gesture, and so they - // should not collapse the stack (which all other touches on areas around the AV would - // do). - if (motionEvent.getRawY() >= avBounds.top - && motionEvent.getRawY() <= avBounds.bottom - && (motionEvent.getRawX() < avBounds.left - || motionEvent.getRawX() > avBounds.right)) { - return true; - } - - return false; - }); - - // BubbleStackView is forced LTR, but we want to respect the locale for expanded view layout - // so the Manage button appears on the right. - setLayoutDirection(LAYOUT_DIRECTION_LOCALE); - } - - private String getBubbleKey() { - return mBubble != null ? mBubble.getKey() : "null"; - } - - /** - * Sets whether the surface displaying app content should sit on top. This is useful for - * ordering surfaces during animations. When content is drawn on top of the app (e.g. bubble - * being dragged out, the manage menu) this is set to false, otherwise it should be true. - */ - void setSurfaceZOrderedOnTop(boolean onTop) { - if (mTaskView == null) { - return; - } - mTaskView.setZOrderedOnTop(onTop, true /* allowDynamicChange */); - } - - void setImeVisible(boolean visible) { - mImeVisible = visible; - if (!mImeVisible && mNeedsNewHeight) { - updateHeight(); - } - } - - /** Return a GraphicBuffer with the contents of the task view surface. */ - @Nullable - SurfaceControl.ScreenshotHardwareBuffer snapshotActivitySurface() { - if (mTaskView == null) { - return null; - } - return SurfaceControl.captureLayers( - mTaskView.getSurfaceControl(), - new Rect(0, 0, mTaskView.getWidth(), mTaskView.getHeight()), - 1 /* scale */); - } - - int[] getTaskViewLocationOnScreen() { - if (mTaskView != null) { - return mTaskView.getLocationOnScreen(); - } else { - return new int[]{0, 0}; - } - } - - // TODO: Could listener be passed when we pass StackView / can we avoid setting this like this - void setManageClickListener(OnClickListener manageClickListener) { - mSettingsIcon.setOnClickListener(manageClickListener); - } - - /** - * Updates the obscured touchable region for the task surface. This calls onLocationChanged, - * which results in a call to {@link BubbleStackView#subtractObscuredTouchableRegion}. This is - * useful if a view has been added or removed from on top of the ActivityView, such as the - * manage menu. - */ - void updateObscuredTouchableRegion() { - if (mTaskView != null) { - mTaskView.onLocationChanged(); - } - } - - void applyThemeAttrs() { - final TypedArray ta = mContext.obtainStyledAttributes(new int[] { - android.R.attr.dialogCornerRadius, - android.R.attr.colorBackgroundFloating}); - mCornerRadius = ta.getDimensionPixelSize(0, 0); - mExpandedViewContainer.setBackgroundColor(ta.getColor(1, Color.WHITE)); - ta.recycle(); - - if (mTaskView != null && ScreenDecorationsUtils.supportsRoundedCornersOnWindows( - mContext.getResources())) { - mTaskView.setCornerRadius(mCornerRadius); - } - - final int mode = - getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - switch (mode) { - case Configuration.UI_MODE_NIGHT_NO: - mPointerDrawable.setTint(getResources().getColor(R.color.bubbles_light)); - break; - case Configuration.UI_MODE_NIGHT_YES: - mPointerDrawable.setTint(getResources().getColor(R.color.bubbles_dark)); - break; - } - mPointerView.setBackground(mPointerDrawable); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - mImeVisible = false; - mNeedsNewHeight = false; - if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "onDetachedFromWindow: bubble=" + getBubbleKey()); - } - } - - /** - * Set visibility of contents in the expanded state. - * - * @param visibility {@code true} if the contents should be visible on the screen. - * - * Note that this contents visibility doesn't affect visibility at {@link android.view.View}, - * and setting {@code false} actually means rendering the contents in transparent. - */ - void setContentVisibility(boolean visibility) { - if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "setContentVisibility: visibility=" + visibility - + " bubble=" + getBubbleKey()); - } - final float alpha = visibility ? 1f : 0f; - - mPointerView.setAlpha(alpha); - if (mTaskView == null) { - return; - } - if (alpha != mTaskView.getAlpha()) { - mTaskView.setAlpha(alpha); - } - } - - @Nullable - View getTaskView() { - return mTaskView; - } - - int getTaskId() { - return mTaskId; - } - - void setStackView(BubbleStackView stackView) { - mStackView = stackView; - } - - public void setOverflow(boolean overflow) { - mIsOverflow = overflow; - - Intent target = new Intent(mContext, BubbleOverflowActivity.class); - Bundle extras = new Bundle(); - extras.putBinder(EXTRA_BUBBLE_CONTROLLER, ObjectWrapper.wrap(mBubbles)); - target.putExtras(extras); - mPendingIntent = PendingIntent.getActivity(mContext, 0 /* requestCode */, - target, PendingIntent.FLAG_UPDATE_CURRENT); - mSettingsIcon.setVisibility(GONE); - } - - /** - * Sets the bubble used to populate this view. - */ - void update(Bubble bubble) { - if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "update: bubble=" + bubble); - } - if (mStackView == null) { - Log.w(TAG, "Stack is null for bubble: " + bubble); - return; - } - boolean isNew = mBubble == null || didBackingContentChange(bubble); - if (isNew || bubble != null && bubble.getKey().equals(mBubble.getKey())) { - mBubble = bubble; - mSettingsIcon.setContentDescription(getResources().getString( - R.string.bubbles_settings_button_description, bubble.getAppName())); - mSettingsIcon.setAccessibilityDelegate( - new AccessibilityDelegate() { - @Override - public void onInitializeAccessibilityNodeInfo(View host, - AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(host, info); - // On focus, have TalkBack say - // "Actions available. Use swipe up then right to view." - // in addition to the default "double tap to activate". - mStackView.setupLocalMenu(info); - } - }); - - if (isNew) { - mPendingIntent = mBubble.getBubbleIntent(); - if (mPendingIntent != null || mBubble.hasMetadataShortcutId()) { - setContentVisibility(false); - mTaskView.setVisibility(VISIBLE); - } - } - applyThemeAttrs(); - } else { - Log.w(TAG, "Trying to update entry with different key, new bubble: " - + bubble.getKey() + " old bubble: " + bubble.getKey()); - } - } - - /** - * Bubbles are backed by a pending intent or a shortcut, once the activity is - * started we never change it / restart it on notification updates -- unless the bubbles' - * backing data switches. - * - * This indicates if the new bubble is backed by a different data source than what was - * previously shown here (e.g. previously a pending intent & now a shortcut). - * - * @param newBubble the bubble this view is being updated with. - * @return true if the backing content has changed. - */ - private boolean didBackingContentChange(Bubble newBubble) { - boolean prevWasIntentBased = mBubble != null && mPendingIntent != null; - boolean newIsIntentBased = newBubble.getBubbleIntent() != null; - return prevWasIntentBased != newIsIntentBased; - } - - void updateHeight() { - if (mExpandedViewContainerLocation == null) { - return; - } - - if (mBubble != null || mIsOverflow) { - float desiredHeight = mOverflowHeight; - if (!mIsOverflow) { - desiredHeight = Math.max(mBubble.getDesiredHeight(mContext), mMinHeight); - } - float height = Math.min(desiredHeight, getMaxExpandedHeight()); - height = Math.max(height, mIsOverflow ? mOverflowHeight : mMinHeight); - FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mTaskView.getLayoutParams(); - mNeedsNewHeight = lp.height != height; - if (!mImeVisible) { - // If the ime is visible... don't adjust the height because that will cause - // a configuration change and the ime will be lost. - lp.height = (int) height; - mTaskView.setLayoutParams(lp); - mNeedsNewHeight = false; - } - if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "updateHeight: bubble=" + getBubbleKey() - + " height=" + height - + " mNeedsNewHeight=" + mNeedsNewHeight); - } - } - } - - private int getMaxExpandedHeight() { - mWindowManager.getDefaultDisplay().getRealSize(mDisplaySize); - int expandedContainerY = mExpandedViewContainerLocation != null - ? mExpandedViewContainerLocation[1] - : 0; - int bottomInset = getRootWindowInsets() != null - ? getRootWindowInsets().getStableInsetBottom() - : 0; - - return mDisplaySize.y - - expandedContainerY - - getPaddingTop() - - getPaddingBottom() - - mSettingsIconHeight - - mPointerHeight - - mPointerMargin - bottomInset; - } - - /** - * Update appearance of the expanded view being displayed. - * - * @param containerLocationOnScreen The location on-screen of the container the expanded view is - * added to. This allows us to calculate max height without - * waiting for layout. - */ - public void updateView(int[] containerLocationOnScreen) { - if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "updateView: bubble=" - + getBubbleKey()); - } - mExpandedViewContainerLocation = containerLocationOnScreen; - if (mTaskView != null - && mTaskView.getVisibility() == VISIBLE - && mTaskView.isAttachedToWindow()) { - updateHeight(); - mTaskView.onLocationChanged(); - } - } - - /** - * Set the x position that the tip of the triangle should point to. - */ - public void setPointerPosition(float x) { - float halfPointerWidth = mPointerWidth / 2f; - float pointerLeft = x - halfPointerWidth - mExpandedViewPadding; - mPointerView.setTranslationX(pointerLeft); - mPointerView.setVisibility(VISIBLE); - } - - /** - * Position of the manage button displayed in the expanded view. Used for placing user - * education about the manage button. - */ - public void getManageButtonBoundsOnScreen(Rect rect) { - mSettingsIcon.getBoundsOnScreen(rect); - } - - /** - * Cleans up anything related to the task and TaskView. - */ - public void cleanUpExpandedState() { - if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId); - } - if (mTaskView != null) { - mTaskView.release(); - } - if (mTaskView != null) { - removeView(mTaskView); - mTaskView = null; - } - } - - /** - * Description of current expanded view state. - */ - public void dump( - @NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { - pw.print("BubbleExpandedView"); - pw.print(" taskId: "); pw.println(mTaskId); - pw.print(" stackView: "); pw.println(mStackView); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java deleted file mode 100644 index ffb650d62064..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Copyright (C) 2019 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.app.Notification.EXTRA_MESSAGES; -import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC; -import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST; -import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED; - -import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_EXPERIMENTS; -import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; -import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; - -import android.app.Notification; -import android.app.PendingIntent; -import android.app.Person; -import android.content.Context; -import android.content.pm.LauncherApps; -import android.content.pm.ShortcutInfo; -import android.graphics.Color; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.os.Bundle; -import android.os.Parcelable; -import android.os.UserHandle; -import android.provider.Settings; -import android.util.Log; - -import com.android.internal.graphics.ColorUtils; -import com.android.internal.util.ArrayUtils; -import com.android.internal.util.ContrastColorUtil; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.people.PeopleHubNotificationListenerKt; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** - * Common class for experiments controlled via secure settings. - */ -public class BubbleExperimentConfig { - private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; - - private static final int BUBBLE_HEIGHT = 10000; - - private static final String ALLOW_ANY_NOTIF_TO_BUBBLE = "allow_any_notif_to_bubble"; - private static final boolean ALLOW_ANY_NOTIF_TO_BUBBLE_DEFAULT = false; - - private static final String ALLOW_MESSAGE_NOTIFS_TO_BUBBLE = "allow_message_notifs_to_bubble"; - private static final boolean ALLOW_MESSAGE_NOTIFS_TO_BUBBLE_DEFAULT = false; - - private static final String ALLOW_SHORTCUTS_TO_BUBBLE = "allow_shortcuts_to_bubble"; - private static final boolean ALLOW_SHORTCUT_TO_BUBBLE_DEFAULT = false; - - private static final String WHITELISTED_AUTO_BUBBLE_APPS = "whitelisted_auto_bubble_apps"; - - /** - * When true, if a notification has the information necessary to bubble (i.e. valid - * contentIntent and an icon or image), then a {@link android.app.Notification.BubbleMetadata} - * object will be created by the system and added to the notification. - * <p> - * This does not produce a bubble, only adds the metadata based on the notification info. - */ - static boolean allowAnyNotifToBubble(Context context) { - return Settings.Secure.getInt(context.getContentResolver(), - ALLOW_ANY_NOTIF_TO_BUBBLE, - ALLOW_ANY_NOTIF_TO_BUBBLE_DEFAULT ? 1 : 0) != 0; - } - - /** - * Same as {@link #allowAnyNotifToBubble(Context)} except it filters for notifications that - * are using {@link Notification.MessagingStyle} and have remote input. - */ - static boolean allowMessageNotifsToBubble(Context context) { - return Settings.Secure.getInt(context.getContentResolver(), - ALLOW_MESSAGE_NOTIFS_TO_BUBBLE, - ALLOW_MESSAGE_NOTIFS_TO_BUBBLE_DEFAULT ? 1 : 0) != 0; - } - - /** - * When true, if the notification is able to bubble via {@link #allowAnyNotifToBubble(Context)} - * or {@link #allowMessageNotifsToBubble(Context)} or via normal BubbleMetadata, then a new - * BubbleMetadata object is constructed based on the shortcut info. - * <p> - * This does not produce a bubble, only adds the metadata based on shortcut info. - */ - static boolean useShortcutInfoToBubble(Context context) { - return Settings.Secure.getInt(context.getContentResolver(), - ALLOW_SHORTCUTS_TO_BUBBLE, - ALLOW_SHORTCUT_TO_BUBBLE_DEFAULT ? 1 : 0) != 0; - } - - /** - * Returns whether the provided package is whitelisted to bubble. - */ - static boolean isPackageWhitelistedToAutoBubble(Context context, String packageName) { - String unsplitList = Settings.Secure.getString(context.getContentResolver(), - WHITELISTED_AUTO_BUBBLE_APPS); - if (unsplitList != null) { - // We expect the list to be separated by commas and no white space (but we trim in case) - String[] packageList = unsplitList.split(","); - for (int i = 0; i < packageList.length; i++) { - if (packageList[i].trim().equals(packageName)) { - return true; - } - } - } - return false; - } - - /** - * If {@link #allowAnyNotifToBubble(Context)} is true, this method creates and adds - * {@link android.app.Notification.BubbleMetadata} to the notification entry as long as - * the notification has necessary info for BubbleMetadata. - * - * @return whether an adjustment was made. - */ - static boolean adjustForExperiments(Context context, NotificationEntry entry, - boolean previouslyUserCreated, boolean userBlocked) { - Notification.BubbleMetadata metadata = null; - boolean addedMetadata = false; - boolean whiteListedToAutoBubble = - isPackageWhitelistedToAutoBubble(context, entry.getSbn().getPackageName()); - - Notification notification = entry.getSbn().getNotification(); - boolean isMessage = Notification.MessagingStyle.class.equals( - notification.getNotificationStyle()); - boolean bubbleNotifForExperiment = (isMessage && allowMessageNotifsToBubble(context)) - || allowAnyNotifToBubble(context); - - boolean useShortcutInfo = useShortcutInfoToBubble(context); - String shortcutId = entry.getSbn().getNotification().getShortcutId(); - - boolean hasMetadata = entry.getBubbleMetadata() != null; - if ((!hasMetadata && (previouslyUserCreated || bubbleNotifForExperiment)) - || useShortcutInfo) { - if (DEBUG_EXPERIMENTS) { - Log.d(TAG, "Adjusting " + entry.getKey() + " for bubble experiment." - + " allowMessages=" + allowMessageNotifsToBubble(context) - + " isMessage=" + isMessage - + " allowNotifs=" + allowAnyNotifToBubble(context) - + " useShortcutInfo=" + useShortcutInfo - + " previouslyUserCreated=" + previouslyUserCreated); - } - } - - if (useShortcutInfo && shortcutId != null) { - // We don't actually get anything useful from ShortcutInfo so just check existence - ShortcutInfo info = getShortcutInfo(context, entry.getSbn().getPackageName(), - entry.getSbn().getUser(), shortcutId); - if (info != null) { - metadata = createForShortcut(shortcutId); - } - - // Replace existing metadata with shortcut, or we're bubbling for experiment - boolean shouldBubble = entry.getBubbleMetadata() != null - || bubbleNotifForExperiment - || previouslyUserCreated; - if (shouldBubble && metadata != null) { - if (DEBUG_EXPERIMENTS) { - Log.d(TAG, "Adding experimental shortcut bubble for: " + entry.getKey()); - } - entry.setBubbleMetadata(metadata); - addedMetadata = true; - } - } - - // Didn't get metadata from a shortcut & we're bubbling for experiment - if (entry.getBubbleMetadata() == null - && (bubbleNotifForExperiment || previouslyUserCreated)) { - metadata = createFromNotif(context, entry); - if (metadata != null) { - if (DEBUG_EXPERIMENTS) { - Log.d(TAG, "Adding experimental notification bubble for: " + entry.getKey()); - } - entry.setBubbleMetadata(metadata); - addedMetadata = true; - } - } - - boolean bubbleForWhitelist = !userBlocked - && whiteListedToAutoBubble - && (addedMetadata || hasMetadata); - if ((previouslyUserCreated && addedMetadata) || bubbleForWhitelist) { - // Update to a previous bubble (or new autobubble), set its flag now. - if (DEBUG_EXPERIMENTS) { - Log.d(TAG, "Setting FLAG_BUBBLE for: " + entry.getKey()); - } - entry.setFlagBubble(true); - return true; - } - return addedMetadata; - } - - static Notification.BubbleMetadata createFromNotif(Context context, NotificationEntry entry) { - Notification notification = entry.getSbn().getNotification(); - final PendingIntent intent = notification.contentIntent; - Icon icon = null; - // Use the icon of the person if available - List<Person> personList = getPeopleFromNotification(entry); - if (personList.size() > 0) { - final Person person = personList.get(0); - if (person != null) { - icon = person.getIcon(); - if (icon == null) { - // Lets try and grab the icon constructed by the layout - Drawable d = PeopleHubNotificationListenerKt.extractAvatarFromRow(entry); - if (d instanceof BitmapDrawable) { - icon = Icon.createWithBitmap(((BitmapDrawable) d).getBitmap()); - } - } - } - } - if (icon == null) { - boolean shouldTint = notification.getLargeIcon() == null; - icon = shouldTint - ? notification.getSmallIcon() - : notification.getLargeIcon(); - if (shouldTint) { - int notifColor = entry.getSbn().getNotification().color; - notifColor = ColorUtils.setAlphaComponent(notifColor, 255); - notifColor = ContrastColorUtil.findContrastColor(notifColor, Color.WHITE, - true /* findFg */, 3f); - icon.setTint(notifColor); - } - } - if (intent != null) { - return new Notification.BubbleMetadata.Builder(intent, icon) - .setDesiredHeight(BUBBLE_HEIGHT) - .build(); - } - return null; - } - - static Notification.BubbleMetadata createForShortcut(String shortcutId) { - return new Notification.BubbleMetadata.Builder(shortcutId) - .setDesiredHeight(BUBBLE_HEIGHT) - .build(); - } - - static ShortcutInfo getShortcutInfo(Context context, String packageName, UserHandle user, - String shortcutId) { - LauncherApps launcherAppService = - (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE); - LauncherApps.ShortcutQuery query = new LauncherApps.ShortcutQuery(); - if (packageName != null) { - query.setPackage(packageName); - } - if (shortcutId != null) { - query.setShortcutIds(Arrays.asList(shortcutId)); - } - query.setQueryFlags(FLAG_MATCH_DYNAMIC | FLAG_MATCH_PINNED | FLAG_MATCH_MANIFEST); - List<ShortcutInfo> shortcuts = launcherAppService.getShortcuts(query, user); - return shortcuts != null && shortcuts.size() > 0 - ? shortcuts.get(0) - : null; - } - - static List<Person> getPeopleFromNotification(NotificationEntry entry) { - Bundle extras = entry.getSbn().getNotification().extras; - ArrayList<Person> personList = new ArrayList<>(); - if (extras == null) { - return personList; - } - - List<Person> p = extras.getParcelableArrayList(Notification.EXTRA_PEOPLE_LIST); - - if (p != null) { - personList.addAll(p); - } - - if (Notification.MessagingStyle.class.equals( - entry.getSbn().getNotification().getNotificationStyle())) { - final Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES); - if (!ArrayUtils.isEmpty(messages)) { - for (Notification.MessagingStyle.Message message : - Notification.MessagingStyle.Message - .getMessagesFromBundleArray(messages)) { - personList.add(message.getSenderPerson()); - } - } - } - return personList; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java deleted file mode 100644 index 009114ffa0be..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java +++ /dev/null @@ -1,519 +0,0 @@ -/* - * Copyright (C) 2019 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.graphics.Paint.ANTI_ALIAS_FLAG; -import static android.graphics.Paint.FILTER_BITMAP_FLAG; -import static com.android.systemui.Interpolators.ALPHA_IN; -import static com.android.systemui.Interpolators.ALPHA_OUT; - -import android.animation.ArgbEvaluator; -import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Matrix; -import android.graphics.Outline; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.PointF; -import android.graphics.RectF; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.ShapeDrawable; -import android.text.TextUtils; -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewOutlineProvider; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.Nullable; - -import com.android.systemui.R; -import com.android.systemui.recents.TriangleShape; - -/** - * Flyout view that appears as a 'chat bubble' alongside the bubble stack. The flyout can visually - * transform into the 'new' dot, which is used during flyout dismiss animations/gestures. - */ -public class BubbleFlyoutView extends FrameLayout { - /** Max width of the flyout, in terms of percent of the screen width. */ - private static final float FLYOUT_MAX_WIDTH_PERCENT = .6f; - - /** Translation Y of fade animation. */ - private static final float FLYOUT_FADE_Y = 40f; - - private static final long FLYOUT_FADE_DURATION = 200L; - - private final int mFlyoutPadding; - private final int mFlyoutSpaceFromBubble; - private final int mPointerSize; - private final int mBubbleSize; - private final int mBubbleBitmapSize; - private final float mBubbleIconTopPadding; - - private final int mFlyoutElevation; - private final int mBubbleElevation; - private final int mFloatingBackgroundColor; - private final float mCornerRadius; - - private final ViewGroup mFlyoutTextContainer; - private final ImageView mSenderAvatar; - private final TextView mSenderText; - private final TextView mMessageText; - - /** Values related to the 'new' dot which we use to figure out where to collapse the flyout. */ - private final float mNewDotRadius; - private final float mNewDotSize; - private final float mOriginalDotSize; - - /** - * The paint used to draw the background, whose color changes as the flyout transitions to the - * tinted 'new' dot. - */ - private final Paint mBgPaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG); - private final ArgbEvaluator mArgbEvaluator = new ArgbEvaluator(); - - /** - * Triangular ShapeDrawables used for the triangle that points from the flyout to the bubble - * stack (a chat-bubble effect). - */ - private final ShapeDrawable mLeftTriangleShape; - private final ShapeDrawable mRightTriangleShape; - - /** Whether the flyout arrow is on the left (pointing left) or right (pointing right). */ - private boolean mArrowPointingLeft = true; - - /** Color of the 'new' dot that the flyout will transform into. */ - private int mDotColor; - - /** The outline of the triangle, used for elevation shadows. */ - private final Outline mTriangleOutline = new Outline(); - - /** The bounds of the flyout background, kept up to date as it transitions to the 'new' dot. */ - private final RectF mBgRect = new RectF(); - - /** The y position of the flyout, relative to the top of the screen. */ - private float mFlyoutY = 0f; - - /** - * Percent progress in the transition from flyout to 'new' dot. These two values are the inverse - * of each other (if we're 40% transitioned to the dot, we're 60% flyout), but it makes the code - * much more readable. - */ - private float mPercentTransitionedToDot = 1f; - private float mPercentStillFlyout = 0f; - - /** - * The difference in values between the flyout and the dot. These differences are gradually - * added over the course of the animation to transform the flyout into the 'new' dot. - */ - private float mFlyoutToDotWidthDelta = 0f; - private float mFlyoutToDotHeightDelta = 0f; - - /** The translation values when the flyout is completely transitioned into the dot. */ - private float mTranslationXWhenDot = 0f; - private float mTranslationYWhenDot = 0f; - - /** - * The current translation values applied to the flyout background as it transitions into the - * 'new' dot. - */ - private float mBgTranslationX; - private float mBgTranslationY; - - private float[] mDotCenter; - - /** The flyout's X translation when at rest (not animating or dragging). */ - private float mRestingTranslationX = 0f; - - /** The badge sizes are defined as percentages of the app icon size. Same value as Launcher3. */ - private static final float SIZE_PERCENTAGE = 0.228f; - - private static final float DOT_SCALE = 1f; - - /** Callback to run when the flyout is hidden. */ - @Nullable private Runnable mOnHide; - - public BubbleFlyoutView(Context context) { - super(context); - LayoutInflater.from(context).inflate(R.layout.bubble_flyout, this, true); - - mFlyoutTextContainer = findViewById(R.id.bubble_flyout_text_container); - mSenderText = findViewById(R.id.bubble_flyout_name); - mSenderAvatar = findViewById(R.id.bubble_flyout_avatar); - mMessageText = mFlyoutTextContainer.findViewById(R.id.bubble_flyout_text); - - final Resources res = getResources(); - mFlyoutPadding = res.getDimensionPixelSize(R.dimen.bubble_flyout_padding_x); - mFlyoutSpaceFromBubble = res.getDimensionPixelSize(R.dimen.bubble_flyout_space_from_bubble); - mPointerSize = res.getDimensionPixelSize(R.dimen.bubble_flyout_pointer_size); - - mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size); - mBubbleBitmapSize = res.getDimensionPixelSize(R.dimen.bubble_bitmap_size); - mBubbleIconTopPadding = (mBubbleSize - mBubbleBitmapSize) / 2f; - - mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); - mFlyoutElevation = res.getDimensionPixelSize(R.dimen.bubble_flyout_elevation); - - mOriginalDotSize = SIZE_PERCENTAGE * mBubbleBitmapSize; - mNewDotRadius = (DOT_SCALE * mOriginalDotSize) / 2f; - mNewDotSize = mNewDotRadius * 2f; - - final TypedArray ta = mContext.obtainStyledAttributes( - new int[] { - android.R.attr.colorBackgroundFloating, - android.R.attr.dialogCornerRadius}); - mFloatingBackgroundColor = ta.getColor(0, Color.WHITE); - mCornerRadius = ta.getDimensionPixelSize(1, 0); - ta.recycle(); - - // Add padding for the pointer on either side, onDraw will draw it in this space. - setPadding(mPointerSize, 0, mPointerSize, 0); - setWillNotDraw(false); - setClipChildren(false); - setTranslationZ(mFlyoutElevation); - setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - BubbleFlyoutView.this.getOutline(outline); - } - }); - - // Use locale direction so the text is aligned correctly. - setLayoutDirection(LAYOUT_DIRECTION_LOCALE); - - mBgPaint.setColor(mFloatingBackgroundColor); - - mLeftTriangleShape = - new ShapeDrawable(TriangleShape.createHorizontal( - mPointerSize, mPointerSize, true /* isPointingLeft */)); - mLeftTriangleShape.setBounds(0, 0, mPointerSize, mPointerSize); - mLeftTriangleShape.getPaint().setColor(mFloatingBackgroundColor); - - mRightTriangleShape = - new ShapeDrawable(TriangleShape.createHorizontal( - mPointerSize, mPointerSize, false /* isPointingLeft */)); - mRightTriangleShape.setBounds(0, 0, mPointerSize, mPointerSize); - mRightTriangleShape.getPaint().setColor(mFloatingBackgroundColor); - } - - @Override - protected void onDraw(Canvas canvas) { - renderBackground(canvas); - invalidateOutline(); - super.onDraw(canvas); - } - - void updateFontSize(float fontScale) { - final float fontSize = mContext.getResources() - .getDimensionPixelSize(com.android.internal.R.dimen.text_size_body_2_material); - final float newFontSize = fontSize * fontScale; - mMessageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, newFontSize); - mSenderText.setTextSize(TypedValue.COMPLEX_UNIT_PX, newFontSize); - } - - /* - * Fade animation for consecutive flyouts. - */ - void animateUpdate(Bubble.FlyoutMessage flyoutMessage, float parentWidth, float stackY) { - fade(false /* in */); - updateFlyoutMessage(flyoutMessage, parentWidth); - // Wait for TextViews to layout with updated height. - post(() -> { - mFlyoutY = stackY + (mBubbleSize - mFlyoutTextContainer.getHeight()) / 2f; - fade(true /* in */); - }); - } - - private void fade(boolean in) { - setAlpha(in ? 0f : 1f); - setTranslationY(in ? mFlyoutY : mFlyoutY + FLYOUT_FADE_Y); - animate() - .alpha(in ? 1f : 0f) - .setDuration(FLYOUT_FADE_DURATION) - .setInterpolator(in ? ALPHA_IN : ALPHA_OUT); - animate() - .translationY(in ? mFlyoutY : mFlyoutY - FLYOUT_FADE_Y) - .setDuration(FLYOUT_FADE_DURATION) - .setInterpolator(in ? ALPHA_IN : ALPHA_OUT); - } - - private void updateFlyoutMessage(Bubble.FlyoutMessage flyoutMessage, float parentWidth) { - final Drawable senderAvatar = flyoutMessage.senderAvatar; - if (senderAvatar != null && flyoutMessage.isGroupChat) { - mSenderAvatar.setVisibility(VISIBLE); - mSenderAvatar.setImageDrawable(senderAvatar); - } else { - mSenderAvatar.setVisibility(GONE); - mSenderAvatar.setTranslationX(0); - mMessageText.setTranslationX(0); - mSenderText.setTranslationX(0); - } - - final int maxTextViewWidth = - (int) (parentWidth * FLYOUT_MAX_WIDTH_PERCENT) - mFlyoutPadding * 2; - - // Name visibility - if (!TextUtils.isEmpty(flyoutMessage.senderName)) { - mSenderText.setMaxWidth(maxTextViewWidth); - mSenderText.setText(flyoutMessage.senderName); - mSenderText.setVisibility(VISIBLE); - } else { - mSenderText.setVisibility(GONE); - } - - // Set the flyout TextView's max width in terms of percent, and then subtract out the - // padding so that the entire flyout view will be the desired width (rather than the - // TextView being the desired width + extra padding). - mMessageText.setMaxWidth(maxTextViewWidth); - mMessageText.setText(flyoutMessage.message); - } - - /** Configures the flyout, collapsed into dot form. */ - void setupFlyoutStartingAsDot( - Bubble.FlyoutMessage flyoutMessage, - PointF stackPos, - float parentWidth, - boolean arrowPointingLeft, - int dotColor, - @Nullable Runnable onLayoutComplete, - @Nullable Runnable onHide, - float[] dotCenter, - boolean hideDot) { - - updateFlyoutMessage(flyoutMessage, parentWidth); - - mArrowPointingLeft = arrowPointingLeft; - mDotColor = dotColor; - mOnHide = onHide; - mDotCenter = dotCenter; - - setCollapsePercent(1f); - - // Wait for TextViews to layout with updated height. - post(() -> { - // Flyout is vertically centered with respect to the bubble. - mFlyoutY = - stackPos.y + (mBubbleSize - mFlyoutTextContainer.getHeight()) / 2f; - setTranslationY(mFlyoutY); - - // Calculate the translation required to position the flyout next to the bubble stack, - // with the desired padding. - mRestingTranslationX = mArrowPointingLeft - ? stackPos.x + mBubbleSize + mFlyoutSpaceFromBubble - : stackPos.x - getWidth() - mFlyoutSpaceFromBubble; - - // Calculate the difference in size between the flyout and the 'dot' so that we can - // transform into the dot later. - final float newDotSize = hideDot ? 0f : mNewDotSize; - mFlyoutToDotWidthDelta = getWidth() - newDotSize; - mFlyoutToDotHeightDelta = getHeight() - newDotSize; - - // Calculate the translation values needed to be in the correct 'new dot' position. - final float adjustmentForScaleAway = hideDot ? 0f : (mOriginalDotSize / 2f); - final float dotPositionX = stackPos.x + mDotCenter[0] - adjustmentForScaleAway; - final float dotPositionY = stackPos.y + mDotCenter[1] - adjustmentForScaleAway; - - final float distanceFromFlyoutLeftToDotCenterX = mRestingTranslationX - dotPositionX; - final float distanceFromLayoutTopToDotCenterY = mFlyoutY - dotPositionY; - - mTranslationXWhenDot = -distanceFromFlyoutLeftToDotCenterX; - mTranslationYWhenDot = -distanceFromLayoutTopToDotCenterY; - if (onLayoutComplete != null) { - onLayoutComplete.run(); - } - }); - } - - /** - * Hides the flyout and runs the optional callback passed into setupFlyoutStartingAsDot. - * The flyout has been animated into the 'new' dot by the time we call this, so no animations - * are needed. - */ - void hideFlyout() { - if (mOnHide != null) { - mOnHide.run(); - mOnHide = null; - } - - setVisibility(GONE); - } - - /** Sets the percentage that the flyout should be collapsed into dot form. */ - void setCollapsePercent(float percentCollapsed) { - // This is unlikely, but can happen in a race condition where the flyout view hasn't been - // laid out and returns 0 for getWidth(). We check for this condition at the sites where - // this method is called, but better safe than sorry. - if (Float.isNaN(percentCollapsed)) { - return; - } - - mPercentTransitionedToDot = Math.max(0f, Math.min(percentCollapsed, 1f)); - mPercentStillFlyout = (1f - mPercentTransitionedToDot); - - // Move and fade out the text. - final float translationX = mPercentTransitionedToDot - * (mArrowPointingLeft ? -getWidth() : getWidth()); - final float alpha = clampPercentage( - (mPercentStillFlyout - (1f - BubbleStackView.FLYOUT_DRAG_PERCENT_DISMISS)) - / BubbleStackView.FLYOUT_DRAG_PERCENT_DISMISS); - - mMessageText.setTranslationX(translationX); - mMessageText.setAlpha(alpha); - - mSenderText.setTranslationX(translationX); - mSenderText.setAlpha(alpha); - - mSenderAvatar.setTranslationX(translationX); - mSenderAvatar.setAlpha(alpha); - - // Reduce the elevation towards that of the topmost bubble. - setTranslationZ( - mFlyoutElevation - - (mFlyoutElevation - mBubbleElevation) * mPercentTransitionedToDot); - invalidate(); - } - - /** Return the flyout's resting X translation (translation when not dragging or animating). */ - float getRestingTranslationX() { - return mRestingTranslationX; - } - - /** Clamps a float to between 0 and 1. */ - private float clampPercentage(float percent) { - return Math.min(1f, Math.max(0f, percent)); - } - - /** - * Renders the background, which is either the rounded 'chat bubble' flyout, or some state - * between that and the 'new' dot over the bubbles. - */ - private void renderBackground(Canvas canvas) { - // Calculate the width, height, and corner radius of the flyout given the current collapsed - // percentage. - final float width = getWidth() - (mFlyoutToDotWidthDelta * mPercentTransitionedToDot); - final float height = getHeight() - (mFlyoutToDotHeightDelta * mPercentTransitionedToDot); - final float interpolatedRadius = getInterpolatedRadius(); - - // Translate the flyout background towards the collapsed 'dot' state. - mBgTranslationX = mTranslationXWhenDot * mPercentTransitionedToDot; - mBgTranslationY = mTranslationYWhenDot * mPercentTransitionedToDot; - - // Set the bounds of the rounded rectangle that serves as either the flyout background or - // the collapsed 'dot'. These bounds will also be used to provide the outline for elevation - // shadows. In the expanded flyout state, the left and right bounds leave space for the - // pointer triangle - as the flyout collapses, this space is reduced since the triangle - // retracts into the flyout. - mBgRect.set( - mPointerSize * mPercentStillFlyout /* left */, - 0 /* top */, - width - mPointerSize * mPercentStillFlyout /* right */, - height /* bottom */); - - mBgPaint.setColor( - (int) mArgbEvaluator.evaluate( - mPercentTransitionedToDot, mFloatingBackgroundColor, mDotColor)); - - canvas.save(); - canvas.translate(mBgTranslationX, mBgTranslationY); - renderPointerTriangle(canvas, width, height); - canvas.drawRoundRect(mBgRect, interpolatedRadius, interpolatedRadius, mBgPaint); - canvas.restore(); - } - - /** Renders the 'pointer' triangle that points from the flyout to the bubble stack. */ - private void renderPointerTriangle( - Canvas canvas, float currentFlyoutWidth, float currentFlyoutHeight) { - canvas.save(); - - // Translation to apply for the 'retraction' effect as the flyout collapses. - final float retractionTranslationX = - (mArrowPointingLeft ? 1 : -1) * (mPercentTransitionedToDot * mPointerSize * 2f); - - // Place the arrow either at the left side, or the far right, depending on whether the - // flyout is on the left or right side. - final float arrowTranslationX = - mArrowPointingLeft - ? retractionTranslationX - : currentFlyoutWidth - mPointerSize + retractionTranslationX; - - // Vertically center the arrow at all times. - final float arrowTranslationY = currentFlyoutHeight / 2f - mPointerSize / 2f; - - // Draw the appropriate direction of arrow. - final ShapeDrawable relevantTriangle = - mArrowPointingLeft ? mLeftTriangleShape : mRightTriangleShape; - canvas.translate(arrowTranslationX, arrowTranslationY); - relevantTriangle.setAlpha((int) (255f * mPercentStillFlyout)); - relevantTriangle.draw(canvas); - - // Save the triangle's outline for use in the outline provider, offsetting it to reflect its - // current position. - relevantTriangle.getOutline(mTriangleOutline); - mTriangleOutline.offset((int) arrowTranslationX, (int) arrowTranslationY); - - canvas.restore(); - } - - /** Builds an outline that includes the transformed flyout background and triangle. */ - private void getOutline(Outline outline) { - if (!mTriangleOutline.isEmpty()) { - // Draw the rect into the outline as a path so we can merge the triangle path into it. - final Path rectPath = new Path(); - final float interpolatedRadius = getInterpolatedRadius(); - rectPath.addRoundRect(mBgRect, interpolatedRadius, - interpolatedRadius, Path.Direction.CW); - outline.setPath(rectPath); - - // Get rid of the triangle path once it has disappeared behind the flyout. - if (mPercentStillFlyout > 0.5f) { - outline.mPath.addPath(mTriangleOutline.mPath); - } - - // Translate the outline to match the background's position. - final Matrix outlineMatrix = new Matrix(); - outlineMatrix.postTranslate(getLeft() + mBgTranslationX, getTop() + mBgTranslationY); - - // At the very end, retract the outline into the bubble so the shadow will be pulled - // into the flyout-dot as it (visually) becomes part of the bubble. We can't do this by - // animating translationZ to zero since then it'll go under the bubbles, which have - // elevation. - if (mPercentTransitionedToDot > 0.98f) { - final float percentBetween99and100 = (mPercentTransitionedToDot - 0.98f) / .02f; - final float percentShadowVisible = 1f - percentBetween99and100; - - // Keep it centered. - outlineMatrix.postTranslate( - mNewDotRadius * percentBetween99and100, - mNewDotRadius * percentBetween99and100); - outlineMatrix.preScale(percentShadowVisible, percentShadowVisible); - } - - outline.mPath.transform(outlineMatrix); - } - } - - private float getInterpolatedRadius() { - return mNewDotRadius * mPercentTransitionedToDot - + mCornerRadius * (1 - mPercentTransitionedToDot); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java deleted file mode 100644 index 371e8490d235..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * 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 android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Context; -import android.content.Intent; -import android.content.pm.LauncherApps; -import android.content.pm.ShortcutInfo; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.drawable.AdaptiveIconDrawable; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; - -import com.android.launcher3.icons.BaseIconFactory; -import com.android.launcher3.icons.BitmapInfo; -import com.android.launcher3.icons.ShadowGenerator; -import com.android.systemui.R; - -/** - * Factory for creating normalized bubble icons. - * We are not using Launcher's IconFactory because bubbles only runs on the UI thread, - * so there is no need to manage a pool across multiple threads. - */ -public class BubbleIconFactory extends BaseIconFactory { - - private int mBadgeSize; - - protected BubbleIconFactory(Context context) { - super(context, context.getResources().getConfiguration().densityDpi, - context.getResources().getDimensionPixelSize(R.dimen.individual_bubble_size)); - mBadgeSize = mContext.getResources().getDimensionPixelSize( - com.android.launcher3.icons.R.dimen.profile_badge_size); - } - - /** - * Returns the drawable that the developer has provided to display in the bubble. - */ - Drawable getBubbleDrawable(@NonNull final Context context, - @Nullable final ShortcutInfo shortcutInfo, @Nullable final Icon ic) { - if (shortcutInfo != null) { - LauncherApps launcherApps = - (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE); - int density = context.getResources().getConfiguration().densityDpi; - return launcherApps.getShortcutIconDrawable(shortcutInfo, density); - } else { - if (ic != null) { - if (ic.getType() == Icon.TYPE_URI - || ic.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) { - context.grantUriPermission(context.getPackageName(), - ic.getUri(), - Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - return ic.loadDrawable(context); - } - return null; - } - } - - /** - * Returns a {@link BitmapInfo} for the app-badge that is shown on top of each bubble. This - * will include the workprofile indicator on the badge if appropriate. - */ - BitmapInfo getBadgeBitmap(Drawable userBadgedAppIcon, boolean isImportantConversation) { - ShadowGenerator shadowGenerator = new ShadowGenerator(mBadgeSize); - Bitmap userBadgedBitmap = createIconBitmap(userBadgedAppIcon, 1f, mBadgeSize); - - if (userBadgedAppIcon instanceof AdaptiveIconDrawable) { - userBadgedBitmap = Bitmap.createScaledBitmap( - getCircleBitmap((AdaptiveIconDrawable) userBadgedAppIcon, /* size */ - userBadgedAppIcon.getIntrinsicWidth()), - mBadgeSize, mBadgeSize, /* filter */ true); - } - - if (isImportantConversation) { - final float ringStrokeWidth = mContext.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.importance_ring_stroke_width); - final int importantConversationColor = mContext.getResources().getColor( - com.android.settingslib.R.color.important_conversation, null); - Bitmap badgeAndRing = Bitmap.createBitmap(userBadgedBitmap.getWidth(), - userBadgedBitmap.getHeight(), userBadgedBitmap.getConfig()); - Canvas c = new Canvas(badgeAndRing); - - final int bitmapTop = (int) ringStrokeWidth; - final int bitmapLeft = (int) ringStrokeWidth; - final int bitmapWidth = c.getWidth() - 2 * (int) ringStrokeWidth; - final int bitmapHeight = c.getHeight() - 2 * (int) ringStrokeWidth; - - Bitmap scaledBitmap = Bitmap.createScaledBitmap(userBadgedBitmap, bitmapWidth, - bitmapHeight, /* filter */ true); - c.drawBitmap(scaledBitmap, bitmapTop, bitmapLeft, /* paint */null); - - Paint ringPaint = new Paint(); - ringPaint.setStyle(Paint.Style.STROKE); - ringPaint.setColor(importantConversationColor); - ringPaint.setAntiAlias(true); - ringPaint.setStrokeWidth(ringStrokeWidth); - c.drawCircle(c.getWidth() / 2, c.getHeight() / 2, c.getWidth() / 2 - ringStrokeWidth, - ringPaint); - - shadowGenerator.recreateIcon(Bitmap.createBitmap(badgeAndRing), c); - return createIconBitmap(badgeAndRing); - } else { - Canvas c = new Canvas(); - c.setBitmap(userBadgedBitmap); - shadowGenerator.recreateIcon(Bitmap.createBitmap(userBadgedBitmap), c); - return createIconBitmap(userBadgedBitmap); - } - } - - public Bitmap getCircleBitmap(AdaptiveIconDrawable icon, int size) { - Drawable foreground = icon.getForeground(); - Drawable background = icon.getBackground(); - Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(); - canvas.setBitmap(bitmap); - - // Clip canvas to circle. - Path circlePath = new Path(); - circlePath.addCircle(/* x */ size / 2f, - /* y */ size / 2f, - /* radius */ size / 2f, - Path.Direction.CW); - canvas.clipPath(circlePath); - - // Draw background. - background.setBounds(0, 0, size, size); - background.draw(canvas); - - // Draw foreground. The foreground and background drawables are derived from adaptive icons - // Some icon shapes fill more space than others, so adaptive icons are normalized to about - // the same size. This size is smaller than the original bounds, so we estimate - // the difference in this offset. - int offset = size / 5; - foreground.setBounds(-offset, -offset, size + offset, size + offset); - foreground.draw(canvas); - - canvas.setBitmap(null); - return bitmap; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLogger.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLogger.java deleted file mode 100644 index 48c809d1b0a7..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLogger.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * 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 com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.UiEvent; -import com.android.internal.logging.UiEventLogger; -import com.android.internal.util.FrameworkStatsLog; - -/** - * Implementation of UiEventLogger for logging bubble UI events. - * - * See UiEventReported atom in atoms.proto for more context. - */ -public class BubbleLogger { - - private final UiEventLogger mUiEventLogger; - - /** - * Bubble UI event. - */ - @VisibleForTesting - public enum Event implements UiEventLogger.UiEventEnum { - - @UiEvent(doc = "User dismissed the bubble via gesture, add bubble to overflow.") - BUBBLE_OVERFLOW_ADD_USER_GESTURE(483), - - @UiEvent(doc = "No more space in top row, add bubble to overflow.") - BUBBLE_OVERFLOW_ADD_AGED(484), - - @UiEvent(doc = "No more space in overflow, remove bubble from overflow") - BUBBLE_OVERFLOW_REMOVE_MAX_REACHED(485), - - @UiEvent(doc = "Notification canceled, remove bubble from overflow.") - BUBBLE_OVERFLOW_REMOVE_CANCEL(486), - - @UiEvent(doc = "Notification group canceled, remove bubble for child notif from overflow.") - BUBBLE_OVERFLOW_REMOVE_GROUP_CANCEL(487), - - @UiEvent(doc = "Notification no longer bubble, remove bubble from overflow.") - BUBBLE_OVERFLOW_REMOVE_NO_LONGER_BUBBLE(488), - - @UiEvent(doc = "User tapped overflow bubble. Promote bubble back to top row.") - BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK(489), - - @UiEvent(doc = "User blocked notification from bubbling, remove bubble from overflow.") - BUBBLE_OVERFLOW_REMOVE_BLOCKED(490), - - @UiEvent(doc = "User selected the overflow.") - BUBBLE_OVERFLOW_SELECTED(600); - - private final int mId; - - Event(int id) { - mId = id; - } - - @Override - public int getId() { - return mId; - } - } - - public BubbleLogger(UiEventLogger uiEventLogger) { - mUiEventLogger = uiEventLogger; - } - - /** - * @param b Bubble involved in this UI event - * @param e UI event - */ - public void log(Bubble b, UiEventLogger.UiEventEnum e) { - mUiEventLogger.logWithInstanceId(e, b.getAppUid(), b.getPackageName(), b.getInstanceId()); - } - - /** - * @param b Bubble removed from overflow - * @param r Reason that bubble was removed - */ - public void logOverflowRemove(Bubble b, @BubbleController.DismissReason int r) { - if (r == BubbleController.DISMISS_NOTIF_CANCEL) { - log(b, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_CANCEL); - } else if (r == BubbleController.DISMISS_GROUP_CANCELLED) { - log(b, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_GROUP_CANCEL); - } else if (r == BubbleController.DISMISS_NO_LONGER_BUBBLE) { - log(b, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_NO_LONGER_BUBBLE); - } else if (r == BubbleController.DISMISS_BLOCKED) { - log(b, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BLOCKED); - } - } - - /** - * @param b Bubble added to overflow - * @param r Reason that bubble was added to overflow - */ - public void logOverflowAdd(Bubble b, @BubbleController.DismissReason int r) { - if (r == BubbleController.DISMISS_AGED) { - log(b, Event.BUBBLE_OVERFLOW_ADD_AGED); - } else if (r == BubbleController.DISMISS_USER_GESTURE) { - log(b, Event.BUBBLE_OVERFLOW_ADD_USER_GESTURE); - } - } - - void logStackUiChanged(String packageName, int action, int bubbleCount, float normalX, - float normalY) { - FrameworkStatsLog.write(FrameworkStatsLog.BUBBLE_UI_CHANGED, - packageName, - null /* notification channel */, - 0 /* notification ID */, - 0 /* bubble position */, - bubbleCount, - action, - normalX, - normalY, - false /* unread bubble */, - false /* on-going bubble */, - false /* isAppForeground (unused) */); - } - - void logShowOverflow(String packageName, int currentUserId) { - mUiEventLogger.log(BubbleLogger.Event.BUBBLE_OVERFLOW_SELECTED, currentUserId, - packageName); - } - - void logBubbleUiChanged(Bubble bubble, String packageName, int action, int bubbleCount, - float normalX, float normalY, int index) { - FrameworkStatsLog.write(FrameworkStatsLog.BUBBLE_UI_CHANGED, - packageName, - bubble.getChannelId() /* notification channel */, - bubble.getNotificationId() /* notification ID */, - index, - bubbleCount, - action, - normalX, - normalY, - bubble.showInShade() /* isUnread */, - false /* isOngoing (unused) */, - false /* isAppForeground (unused) */); - } -}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflow.kt b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflow.kt deleted file mode 100644 index 102055de2bea..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflow.kt +++ /dev/null @@ -1,178 +0,0 @@ -/* - * 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.app.ActivityTaskManager.INVALID_TASK_ID -import android.content.Context -import android.content.res.Configuration -import android.graphics.Bitmap -import android.graphics.Matrix -import android.graphics.Path -import android.graphics.drawable.AdaptiveIconDrawable -import android.graphics.drawable.ColorDrawable -import android.graphics.drawable.Drawable -import android.graphics.drawable.InsetDrawable -import android.util.PathParser -import android.util.TypedValue -import android.view.LayoutInflater -import android.view.View -import android.widget.FrameLayout -import com.android.systemui.R - -class BubbleOverflow( - private val context: Context, - private val stack: BubbleStackView -) : BubbleViewProvider { - - private lateinit var bitmap: Bitmap - private lateinit var dotPath: Path - - private var bitmapSize = 0 - private var iconBitmapSize = 0 - private var dotColor = 0 - private var showDot = false - - private val inflater: LayoutInflater = LayoutInflater.from(context) - private val expandedView: BubbleExpandedView = inflater - .inflate(R.layout.bubble_expanded_view, null /* root */, false /* attachToRoot */) - as BubbleExpandedView - private val overflowBtn: BadgedImageView = inflater - .inflate(R.layout.bubble_overflow_button, null /* root */, false /* attachToRoot */) - as BadgedImageView - init { - updateResources() - with(expandedView) { - setOverflow(true) - setStackView(stack) - applyThemeAttrs() - } - with(overflowBtn) { - setContentDescription(context.resources.getString( - R.string.bubble_overflow_button_content_description)) - updateBtnTheme() - } - } - - fun update() { - updateResources() - expandedView.applyThemeAttrs() - // Apply inset and new style to fresh icon drawable. - overflowBtn.setImageResource(R.drawable.ic_bubble_overflow_button) - updateBtnTheme() - } - - fun updateResources() { - bitmapSize = context.resources.getDimensionPixelSize(R.dimen.bubble_bitmap_size) - iconBitmapSize = context.resources.getDimensionPixelSize( - R.dimen.bubble_overflow_icon_bitmap_size) - val bubbleSize = context.resources.getDimensionPixelSize(R.dimen.individual_bubble_size) - overflowBtn.setLayoutParams(FrameLayout.LayoutParams(bubbleSize, bubbleSize)) - expandedView.updateDimensions() - } - - private fun updateBtnTheme() { - val res = context.resources - - // Set overflow button accent color, dot color - val typedValue = TypedValue() - context.theme.resolveAttribute(android.R.attr.colorAccent, typedValue, true) - val colorAccent = res.getColor(typedValue.resourceId) - overflowBtn.drawable?.setTint(colorAccent) - dotColor = colorAccent - - val iconFactory = BubbleIconFactory(context) - - // Update bitmap - val nightMode = (res.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK - == Configuration.UI_MODE_NIGHT_YES) - val bg = ColorDrawable(res.getColor( - if (nightMode) R.color.bubbles_dark else R.color.bubbles_light)) - - val fg = InsetDrawable(overflowBtn.drawable, - bitmapSize - iconBitmapSize /* inset */) - bitmap = iconFactory.createBadgedIconBitmap(AdaptiveIconDrawable(bg, fg), - null /* user */, true /* shrinkNonAdaptiveIcons */).icon - - // Update dot path - dotPath = PathParser.createPathFromPathData( - res.getString(com.android.internal.R.string.config_icon_mask)) - val scale = iconFactory.normalizer.getScale(overflowBtn.getDrawable(), - null /* outBounds */, null /* path */, null /* outMaskShape */) - val radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f - val matrix = Matrix() - matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, - radius /* pivot y */) - dotPath.transform(matrix) - - // Attach BubbleOverflow to BadgedImageView - overflowBtn.setRenderedBubble(this) - overflowBtn.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE) - } - - fun setVisible(visible: Int) { - overflowBtn.visibility = visible - } - - fun setShowDot(show: Boolean) { - showDot = show - overflowBtn.updateDotVisibility(true /* animate */) - } - - override fun getExpandedView(): BubbleExpandedView? { - return expandedView - } - - override fun getDotColor(): Int { - return dotColor - } - - override fun getAppBadge(): Drawable? { - return null - } - - override fun getBubbleIcon(): Bitmap { - return bitmap - } - - override fun showDot(): Boolean { - return showDot - } - - override fun getDotPath(): Path? { - return dotPath - } - - override fun setContentVisibility(visible: Boolean) { - expandedView.setContentVisibility(visible) - } - - override fun getIconView(): View? { - return overflowBtn - } - - override fun getKey(): String { - return KEY - } - - override fun getTaskId(): Int { - return if (expandedView != null) expandedView.getTaskId() else INVALID_TASK_ID - } - - companion object { - @JvmField val KEY = "Overflow" - } -}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java deleted file mode 100644 index fc3f5b6cbf5e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java +++ /dev/null @@ -1,358 +0,0 @@ -/* - * 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 static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_OVERFLOW; -import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; -import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Color; -import android.os.Bundle; -import android.os.IBinder; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.accessibility.AccessibilityNodeInfo; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.android.internal.util.ContrastColorUtil; -import com.android.systemui.R; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - - -/** - * Activity for showing aged out bubbles. - * Must be public to be accessible to androidx...AppComponentFactory - */ -public class BubbleOverflowActivity extends Activity { - static final String EXTRA_BUBBLE_CONTROLLER = "bubble_controller"; - - private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleOverflowActivity" : TAG_BUBBLES; - - private LinearLayout mEmptyState; - private TextView mEmptyStateTitle; - private TextView mEmptyStateSubtitle; - private ImageView mEmptyStateImage; - private Bubbles mBubbles; - private BubbleOverflowAdapter mAdapter; - private RecyclerView mRecyclerView; - private List<Bubble> mOverflowBubbles = new ArrayList<>(); - - private class NoScrollGridLayoutManager extends GridLayoutManager { - NoScrollGridLayoutManager(Context context, int columns) { - super(context, columns); - } - @Override - public boolean canScrollVertically() { - if (getResources().getConfiguration().orientation - == Configuration.ORIENTATION_LANDSCAPE) { - return super.canScrollVertically(); - } - return false; - } - - @Override - public int getColumnCountForAccessibility(RecyclerView.Recycler recycler, - RecyclerView.State state) { - int bubbleCount = state.getItemCount(); - int columnCount = super.getColumnCountForAccessibility(recycler, state); - if (bubbleCount < columnCount) { - // If there are 4 columns and bubbles <= 3, - // TalkBack says "AppName 1 of 4 in list 4 items" - // This is a workaround until TalkBack bug is fixed for GridLayoutManager - return bubbleCount; - } - return columnCount; - } - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.bubble_overflow_activity); - - mRecyclerView = findViewById(R.id.bubble_overflow_recycler); - mEmptyState = findViewById(R.id.bubble_overflow_empty_state); - mEmptyStateTitle = findViewById(R.id.bubble_overflow_empty_title); - mEmptyStateSubtitle = findViewById(R.id.bubble_overflow_empty_subtitle); - mEmptyStateImage = findViewById(R.id.bubble_overflow_empty_state_image); - - Intent intent = getIntent(); - if (intent != null && intent.getExtras() != null) { - IBinder binder = intent.getExtras().getBinder(EXTRA_BUBBLE_CONTROLLER); - if (binder instanceof ObjectWrapper) { - mBubbles = ((ObjectWrapper<Bubbles>) binder).get(); - } - } else { - Log.w(TAG, "Bubble overflow activity created without bubble controller!"); - } - updateOverflow(); - } - - void updateOverflow() { - Resources res = getResources(); - final int columns = res.getInteger(R.integer.bubbles_overflow_columns); - mRecyclerView.setLayoutManager( - new NoScrollGridLayoutManager(getApplicationContext(), columns)); - - DisplayMetrics displayMetrics = new DisplayMetrics(); - getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); - - final int overflowPadding = res.getDimensionPixelSize(R.dimen.bubble_overflow_padding); - final int recyclerViewWidth = displayMetrics.widthPixels - (overflowPadding * 2); - final int viewWidth = recyclerViewWidth / columns; - - final int maxOverflowBubbles = res.getInteger(R.integer.bubbles_max_overflow); - final int rows = (int) Math.ceil((double) maxOverflowBubbles / columns); - final int recyclerViewHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height) - - res.getDimensionPixelSize(R.dimen.bubble_overflow_padding); - final int viewHeight = recyclerViewHeight / rows; - - mAdapter = new BubbleOverflowAdapter(getApplicationContext(), mOverflowBubbles, - mBubbles::promoteBubbleFromOverflow, viewWidth, viewHeight); - mRecyclerView.setAdapter(mAdapter); - - mOverflowBubbles.clear(); - mOverflowBubbles.addAll(mBubbles.getOverflowBubbles()); - mAdapter.notifyDataSetChanged(); - updateEmptyStateVisibility(); - - mBubbles.setOverflowListener(mDataListener); - updateTheme(); - } - - void updateEmptyStateVisibility() { - if (mOverflowBubbles.isEmpty()) { - mEmptyState.setVisibility(View.VISIBLE); - } else { - mEmptyState.setVisibility(View.GONE); - } - } - - /** - * Handle theme changes. - */ - void updateTheme() { - Resources res = getResources(); - final int mode = res.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - final boolean isNightMode = (mode == Configuration.UI_MODE_NIGHT_YES); - - mEmptyStateImage.setImageDrawable(isNightMode - ? res.getDrawable(R.drawable.ic_empty_bubble_overflow_dark) - : res.getDrawable(R.drawable.ic_empty_bubble_overflow_light)); - - findViewById(android.R.id.content) - .setBackgroundColor(isNightMode - ? res.getColor(R.color.bubbles_dark) - : res.getColor(R.color.bubbles_light)); - - final TypedArray typedArray = getApplicationContext().obtainStyledAttributes( - new int[]{android.R.attr.colorBackgroundFloating, - android.R.attr.textColorSecondary}); - int bgColor = typedArray.getColor(0, isNightMode ? Color.BLACK : Color.WHITE); - int textColor = typedArray.getColor(1, isNightMode ? Color.WHITE : Color.BLACK); - textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, isNightMode); - typedArray.recycle(); - - mEmptyStateTitle.setTextColor(textColor); - mEmptyStateSubtitle.setTextColor(textColor); - } - - private final BubbleData.Listener mDataListener = new BubbleData.Listener() { - - @Override - public void applyUpdate(BubbleData.Update update) { - - Bubble toRemove = update.removedOverflowBubble; - if (toRemove != null) { - if (DEBUG_OVERFLOW) { - Log.d(TAG, "remove: " + toRemove); - } - toRemove.cleanupViews(); - final int i = mOverflowBubbles.indexOf(toRemove); - mOverflowBubbles.remove(toRemove); - mAdapter.notifyItemRemoved(i); - } - - Bubble toAdd = update.addedOverflowBubble; - if (toAdd != null) { - if (DEBUG_OVERFLOW) { - Log.d(TAG, "add: " + toAdd); - } - mOverflowBubbles.add(0, toAdd); - mAdapter.notifyItemInserted(0); - } - - updateEmptyStateVisibility(); - - if (DEBUG_OVERFLOW) { - Log.d(TAG, BubbleDebugConfig.formatBubblesString( - mBubbles.getOverflowBubbles(), null)); - } - } - }; - - @Override - public void onStart() { - super.onStart(); - } - - @Override - public void onRestart() { - super.onRestart(); - } - - @Override - public void onResume() { - super.onResume(); - updateOverflow(); - } - - @Override - public void onPause() { - super.onPause(); - } - - @Override - public void onStop() { - super.onStop(); - } - - public void onDestroy() { - super.onDestroy(); - } -} - -class BubbleOverflowAdapter extends RecyclerView.Adapter<BubbleOverflowAdapter.ViewHolder> { - private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleOverflowAdapter" : TAG_BUBBLES; - - private Context mContext; - private Consumer<Bubble> mPromoteBubbleFromOverflow; - private List<Bubble> mBubbles; - private int mWidth; - private int mHeight; - - public BubbleOverflowAdapter(Context context, List<Bubble> list, Consumer<Bubble> promoteBubble, - int width, int height) { - mContext = context; - mBubbles = list; - mPromoteBubbleFromOverflow = promoteBubble; - mWidth = width; - mHeight = height; - } - - @Override - public BubbleOverflowAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, - int viewType) { - - // Set layout for overflow bubble view. - LinearLayout overflowView = (LinearLayout) LayoutInflater.from(parent.getContext()) - .inflate(R.layout.bubble_overflow_view, parent, false); - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT); - params.width = mWidth; - params.height = mHeight; - overflowView.setLayoutParams(params); - - // Ensure name has enough contrast. - final TypedArray ta = mContext.obtainStyledAttributes( - new int[]{android.R.attr.colorBackgroundFloating, android.R.attr.textColorPrimary}); - final int bgColor = ta.getColor(0, Color.WHITE); - int textColor = ta.getColor(1, Color.BLACK); - textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true); - ta.recycle(); - - TextView viewName = overflowView.findViewById(R.id.bubble_view_name); - viewName.setTextColor(textColor); - - return new ViewHolder(overflowView); - } - - @Override - public void onBindViewHolder(ViewHolder vh, int index) { - Bubble b = mBubbles.get(index); - - vh.iconView.setRenderedBubble(b); - vh.iconView.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); - vh.iconView.setOnClickListener(view -> { - mBubbles.remove(b); - notifyDataSetChanged(); - mPromoteBubbleFromOverflow.accept(b); - }); - - String titleStr = b.getTitle(); - if (titleStr == null) { - titleStr = mContext.getResources().getString(R.string.notification_bubble_title); - } - vh.iconView.setContentDescription(mContext.getResources().getString( - R.string.bubble_content_description_single, titleStr, b.getAppName())); - - vh.iconView.setAccessibilityDelegate( - new View.AccessibilityDelegate() { - @Override - public void onInitializeAccessibilityNodeInfo(View host, - AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(host, info); - // Talkback prompts "Double tap to add back to stack" - // instead of the default "Double tap to activate" - info.addAction( - new AccessibilityNodeInfo.AccessibilityAction( - AccessibilityNodeInfo.ACTION_CLICK, - mContext.getResources().getString( - R.string.bubble_accessibility_action_add_back))); - } - }); - - CharSequence label = b.getShortcutInfo() != null - ? b.getShortcutInfo().getLabel() - : b.getAppName(); - vh.textView.setText(label); - } - - @Override - public int getItemCount() { - return mBubbles.size(); - } - - public static class ViewHolder extends RecyclerView.ViewHolder { - public BadgedImageView iconView; - public TextView textView; - - public ViewHolder(LinearLayout v) { - super(v); - iconView = v.findViewById(R.id.bubble_view); - textView = v.findViewById(R.id.bubble_view_name); - } - } -}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java deleted file mode 100644 index 431719f98ad9..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ /dev/null @@ -1,2723 +0,0 @@ -/* - * Copyright (C) 2012 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.ViewGroup.LayoutParams.MATCH_PARENT; -import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; - -import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW; -import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; -import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.annotation.SuppressLint; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.ColorMatrix; -import android.graphics.ColorMatrixColorFilter; -import android.graphics.Outline; -import android.graphics.Paint; -import android.graphics.Point; -import android.graphics.PointF; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.Region; -import android.os.Bundle; -import android.os.Handler; -import android.provider.Settings; -import android.util.Log; -import android.view.Choreographer; -import android.view.DisplayCutout; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.SurfaceControl; -import android.view.SurfaceView; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewOutlineProvider; -import android.view.ViewTreeObserver; -import android.view.WindowInsets; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityNodeInfo; -import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; -import android.view.animation.AccelerateDecelerateInterpolator; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.dynamicanimation.animation.DynamicAnimation; -import androidx.dynamicanimation.animation.FloatPropertyCompat; -import androidx.dynamicanimation.animation.SpringAnimation; -import androidx.dynamicanimation.animation.SpringForce; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.util.FrameworkStatsLog; -import com.android.systemui.Interpolators; -import com.android.systemui.R; -import com.android.systemui.bubbles.animation.AnimatableScaleMatrix; -import com.android.systemui.bubbles.animation.ExpandedAnimationController; -import com.android.systemui.bubbles.animation.PhysicsAnimationLayout; -import com.android.systemui.bubbles.animation.StackAnimationController; -import com.android.wm.shell.animation.PhysicsAnimator; -import com.android.wm.shell.common.FloatingContentCoordinator; -import com.android.wm.shell.common.magnetictarget.MagnetizedObject; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.function.Consumer; - -/** - * Renders bubbles in a stack and handles animating expanded and collapsed states. - */ -public class BubbleStackView extends FrameLayout - implements ViewTreeObserver.OnComputeInternalInsetsListener { - private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES; - - /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */ - static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f; - - /** Velocity required to dismiss the flyout via drag. */ - private static final float FLYOUT_DISMISS_VELOCITY = 2000f; - - /** - * Factor for attenuating translation when the flyout is overscrolled (8f = flyout moves 1 pixel - * for every 8 pixels overscrolled). - */ - private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f; - - /** Duration of the flyout alpha animations. */ - private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100; - - private static final int FADE_IN_DURATION = 320; - - /** Percent to darken the bubbles when they're in the dismiss target. */ - private static final float DARKEN_PERCENT = 0.3f; - - /** How long to wait, in milliseconds, before hiding the flyout. */ - @VisibleForTesting - static final int FLYOUT_HIDE_AFTER = 5000; - - /** - * How long to wait to animate the stack temporarily invisible after a drag/flyout hide - * animation ends, if we are in fact temporarily invisible. - */ - private static final int ANIMATE_TEMPORARILY_INVISIBLE_DELAY = 1000; - - private static final PhysicsAnimator.SpringConfig FLYOUT_IME_ANIMATION_SPRING_CONFIG = - new PhysicsAnimator.SpringConfig( - StackAnimationController.IME_ANIMATION_STIFFNESS, - StackAnimationController.DEFAULT_BOUNCINESS); - - private final PhysicsAnimator.SpringConfig mScaleInSpringConfig = - new PhysicsAnimator.SpringConfig(300f, 0.9f); - - private final PhysicsAnimator.SpringConfig mScaleOutSpringConfig = - new PhysicsAnimator.SpringConfig(900f, 1f); - - private final PhysicsAnimator.SpringConfig mTranslateSpringConfig = - new PhysicsAnimator.SpringConfig( - SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_NO_BOUNCY); - - /** - * Handler to use for all delayed animations - this way, we can easily cancel them before - * starting a new animation. - */ - private final Handler mDelayedAnimationHandler = new Handler(); - - /** - * Interface to synchronize {@link View} state and the screen. - * - * {@hide} - */ - interface SurfaceSynchronizer { - /** - * Wait until requested change on a {@link View} is reflected on the screen. - * - * @param callback callback to run after the change is reflected on the screen. - */ - void syncSurfaceAndRun(Runnable callback); - } - - private static final SurfaceSynchronizer DEFAULT_SURFACE_SYNCHRONIZER = - new SurfaceSynchronizer() { - @Override - public void syncSurfaceAndRun(Runnable callback) { - Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { - // Just wait 2 frames. There is no guarantee, but this is usually enough time that - // the requested change is reflected on the screen. - // TODO: Once SurfaceFlinger provide APIs to sync the state of {@code View} and - // surfaces, rewrite this logic with them. - private int mFrameWait = 2; - - @Override - public void doFrame(long frameTimeNanos) { - if (--mFrameWait > 0) { - Choreographer.getInstance().postFrameCallback(this); - } else { - callback.run(); - } - } - }); - } - }; - - private Point mDisplaySize; - - private final BubbleData mBubbleData; - - private final ValueAnimator mDesaturateAndDarkenAnimator; - private final Paint mDesaturateAndDarkenPaint = new Paint(); - - private PhysicsAnimationLayout mBubbleContainer; - private StackAnimationController mStackAnimationController; - private ExpandedAnimationController mExpandedAnimationController; - - private FrameLayout mExpandedViewContainer; - - /** Matrix used to scale the expanded view container with a given pivot point. */ - private final AnimatableScaleMatrix mExpandedViewContainerMatrix = new AnimatableScaleMatrix(); - - /** - * SurfaceView that we draw screenshots of animating-out bubbles into. This allows us to animate - * between bubble activities without needing both to be alive at the same time. - */ - private SurfaceView mAnimatingOutSurfaceView; - - /** Container for the animating-out SurfaceView. */ - private FrameLayout mAnimatingOutSurfaceContainer; - - /** - * Buffer containing a screenshot of the animating-out bubble. This is drawn into the - * SurfaceView during animations. - */ - private SurfaceControl.ScreenshotHardwareBuffer mAnimatingOutBubbleBuffer; - - private BubbleFlyoutView mFlyout; - /** Runnable that fades out the flyout and then sets it to GONE. */ - private Runnable mHideFlyout = () -> animateFlyoutCollapsed(true, 0 /* velX */); - /** - * Callback to run after the flyout hides. Also called if a new flyout is shown before the - * 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; - - @Nullable private RelativeStackPosition mRelativeStackPositionBeforeRotation; - - private int mMaxBubbles; - private int mBubbleSize; - private int mBubbleElevation; - private int mBubblePaddingTop; - private int mBubbleTouchPadding; - private int mExpandedViewPadding; - private int mCornerRadius; - private int mStatusBarHeight; - private int mImeOffset; - @Nullable private BubbleViewProvider mExpandedBubble; - private boolean mIsExpanded; - - /** Whether the stack is currently on the left side of the screen, or animating there. */ - private boolean mStackOnLeftOrWillBe = true; - - /** Whether a touch gesture, such as a stack/bubble drag or flyout drag, is in progress. */ - private boolean mIsGestureInProgress = false; - - /** Whether or not the stack is temporarily invisible off the side of the screen. */ - private boolean mTemporarilyInvisible = false; - - /** Whether we're in the middle of dragging the stack around by touch. */ - private boolean mIsDraggingStack = false; - - /** - * The pointer index of the ACTION_DOWN event we received prior to an ACTION_UP. We'll ignore - * touches from other pointer indices. - */ - private int mPointerIndexDown = -1; - - /** Description of current animation controller state. */ - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println("Stack view state:"); - - String bubblesOnScreen = BubbleDebugConfig.formatBubblesString( - getBubblesOnScreen(), getExpandedBubble()); - pw.print(" bubbles on screen: "); pw.println(bubblesOnScreen); - pw.print(" gestureInProgress: "); pw.println(mIsGestureInProgress); - pw.print(" showingDismiss: "); pw.println(mDismissView.isShowing()); - pw.print(" isExpansionAnimating: "); pw.println(mIsExpansionAnimating); - pw.print(" expandedContainerVis: "); pw.println(mExpandedViewContainer.getVisibility()); - pw.print(" expandedContainerAlpha: "); pw.println(mExpandedViewContainer.getAlpha()); - pw.print(" expandedContainerMatrix: "); - pw.println(mExpandedViewContainer.getAnimationMatrix()); - - mStackAnimationController.dump(fd, pw, args); - mExpandedAnimationController.dump(fd, pw, args); - - if (mExpandedBubble != null) { - pw.println("Expanded bubble state:"); - pw.println(" expandedBubbleKey: " + mExpandedBubble.getKey()); - - final BubbleExpandedView expandedView = mExpandedBubble.getExpandedView(); - - if (expandedView != null) { - pw.println(" expandedViewVis: " + expandedView.getVisibility()); - pw.println(" expandedViewAlpha: " + expandedView.getAlpha()); - pw.println(" expandedViewTaskId: " + expandedView.getTaskId()); - - final View av = expandedView.getTaskView(); - - if (av != null) { - pw.println(" activityViewVis: " + av.getVisibility()); - pw.println(" activityViewAlpha: " + av.getAlpha()); - } else { - pw.println(" activityView is null"); - } - } else { - pw.println("Expanded bubble view state: expanded bubble view is null"); - } - } else { - pw.println("Expanded bubble state: expanded bubble is null"); - } - } - - private BubbleController.BubbleExpandListener mExpandListener; - - /** Callback to run when we want to unbubble the given notification's conversation. */ - private Consumer<String> mUnbubbleConversationCallback; - - private boolean mViewUpdatedRequested = false; - private boolean mIsExpansionAnimating = false; - private boolean mIsBubbleSwitchAnimating = false; - - /** The view to desaturate/darken when magneted to the dismiss target. */ - @Nullable private View mDesaturateAndDarkenTargetView; - - private Rect mTempRect = new Rect(); - - private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect()); - - private ViewTreeObserver.OnPreDrawListener mViewUpdater = - new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - getViewTreeObserver().removeOnPreDrawListener(mViewUpdater); - updateExpandedView(); - mViewUpdatedRequested = false; - return true; - } - }; - - private ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater = - this::updateSystemGestureExcludeRects; - - /** Float property that 'drags' the flyout. */ - private final FloatPropertyCompat mFlyoutCollapseProperty = - new FloatPropertyCompat("FlyoutCollapseSpring") { - @Override - public float getValue(Object o) { - return mFlyoutDragDeltaX; - } - - @Override - public void setValue(Object o, float v) { - setFlyoutStateForDragLength(v); - } - }; - - /** SpringAnimation that springs the flyout collapsed via onFlyoutDragged. */ - private final SpringAnimation mFlyoutTransitionSpring = - new SpringAnimation(this, mFlyoutCollapseProperty); - - /** Distance the flyout has been dragged in the X axis. */ - private float mFlyoutDragDeltaX = 0f; - - /** - * Runnable that animates in the flyout. This reference is needed to cancel delayed postings. - */ - private Runnable mAnimateInFlyout; - - /** - * End listener for the flyout spring that either posts a runnable to hide the flyout, or hides - * it immediately. - */ - private final DynamicAnimation.OnAnimationEndListener mAfterFlyoutTransitionSpring = - (dynamicAnimation, b, v, v1) -> { - if (mFlyoutDragDeltaX == 0) { - mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); - } else { - mFlyout.hideFlyout(); - } - }; - - @NonNull - private final SurfaceSynchronizer mSurfaceSynchronizer; - - /** - * Callback to run when the IME visibility changes - BubbleController uses this to update the - * Bubbles window focusability flags with the WindowManager. - */ - public final Consumer<Boolean> mOnImeVisibilityChanged; - - /** - * Callback to run when the bubble expand status changes. - */ - private final Consumer<Boolean> mOnBubbleExpandChanged; - - /** - * Callback to run to ask BubbleController to hide the current IME. - */ - private final Runnable mHideCurrentInputMethodCallback; - - /** - * The currently magnetized object, which is being dragged and will be attracted to the magnetic - * dismiss target. - * - * This is either the stack itself, or an individual bubble. - */ - private MagnetizedObject<?> mMagnetizedObject; - - /** - * The MagneticTarget instance for our circular dismiss view. This is added to the - * MagnetizedObject instances for the stack and any dragged-out bubbles. - */ - private MagnetizedObject.MagneticTarget mMagneticTarget; - - /** Magnet listener that handles animating and dismissing individual dragged-out bubbles. */ - private final MagnetizedObject.MagnetListener mIndividualBubbleMagnetListener = - new MagnetizedObject.MagnetListener() { - @Override - public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { - if (mExpandedAnimationController.getDraggedOutBubble() == null) { - return; - } - - animateDesaturateAndDarken( - mExpandedAnimationController.getDraggedOutBubble(), true); - } - - @Override - public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, - float velX, float velY, boolean wasFlungOut) { - if (mExpandedAnimationController.getDraggedOutBubble() == null) { - return; - } - - animateDesaturateAndDarken( - mExpandedAnimationController.getDraggedOutBubble(), false); - - if (wasFlungOut) { - mExpandedAnimationController.snapBubbleBack( - mExpandedAnimationController.getDraggedOutBubble(), velX, velY); - mDismissView.hide(); - } else { - mExpandedAnimationController.onUnstuckFromTarget(); - } - } - - @Override - public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { - if (mExpandedAnimationController.getDraggedOutBubble() == null) { - return; - } - - mExpandedAnimationController.dismissDraggedOutBubble( - mExpandedAnimationController.getDraggedOutBubble() /* bubble */, - mDismissView.getHeight() /* translationYBy */, - BubbleStackView.this::dismissMagnetizedObject /* after */); - mDismissView.hide(); - } - }; - - /** Magnet listener that handles animating and dismissing the entire stack. */ - private final MagnetizedObject.MagnetListener mStackMagnetListener = - new MagnetizedObject.MagnetListener() { - @Override - public void onStuckToTarget( - @NonNull MagnetizedObject.MagneticTarget target) { - animateDesaturateAndDarken(mBubbleContainer, true); - } - - @Override - public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, - float velX, float velY, boolean wasFlungOut) { - animateDesaturateAndDarken(mBubbleContainer, false); - - if (wasFlungOut) { - mStackAnimationController.flingStackThenSpringToEdge( - mStackAnimationController.getStackPosition().x, velX, velY); - mDismissView.hide(); - } else { - mStackAnimationController.onUnstuckFromTarget(); - } - } - - @Override - public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { - mStackAnimationController.animateStackDismissal( - mDismissView.getHeight() /* translationYBy */, - () -> { - resetDesaturationAndDarken(); - dismissMagnetizedObject(); - } - ); - - mDismissView.hide(); - } - }; - - /** - * Click listener set on each bubble view. When collapsed, clicking a bubble expands the stack. - * When expanded, clicking a bubble either expands that bubble, or collapses the stack. - */ - private OnClickListener mBubbleClickListener = new OnClickListener() { - @Override - public void onClick(View view) { - mIsDraggingStack = false; // If the touch ended in a click, we're no longer dragging. - - // Bubble clicks either trigger expansion/collapse or a bubble switch, both of which we - // shouldn't interrupt. These are quick transitions, so it's not worth trying to adjust - // the animations inflight. - if (mIsExpansionAnimating || mIsBubbleSwitchAnimating) { - return; - } - - final Bubble clickedBubble = mBubbleData.getBubbleWithView(view); - - // If the bubble has since left us, ignore the click. - if (clickedBubble == null) { - return; - } - - final boolean clickedBubbleIsCurrentlyExpandedBubble = - clickedBubble.getKey().equals(mExpandedBubble.getKey()); - - if (isExpanded()) { - mExpandedAnimationController.onGestureFinished(); - } - - if (isExpanded() && !clickedBubbleIsCurrentlyExpandedBubble) { - if (clickedBubble != mBubbleData.getSelectedBubble()) { - // Select the clicked bubble. - mBubbleData.setSelectedBubble(clickedBubble); - } else { - // If the clicked bubble is the selected bubble (but not the expanded bubble), - // that means overflow was previously expanded. Set the selected bubble - // internally without going through BubbleData (which would ignore it since it's - // already selected). - setSelectedBubble(clickedBubble); - } - } else { - // Otherwise, we either tapped the stack (which means we're collapsed - // and should expand) or the currently selected bubble (we're expanded - // and should collapse). - if (!maybeShowStackEdu()) { - mBubbleData.setExpanded(!mBubbleData.isExpanded()); - } - } - } - }; - - /** - * Touch listener set on each bubble view. This enables dragging and dismissing the stack (when - * collapsed), or individual bubbles (when expanded). - */ - private RelativeTouchListener mBubbleTouchListener = new RelativeTouchListener() { - - @Override - public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) { - // If we're expanding or collapsing, consume but ignore all touch events. - if (mIsExpansionAnimating) { - return true; - } - - // If the manage menu is visible, just hide it. - if (mShowingManage) { - showManageMenu(false /* show */); - } - - if (mBubbleData.isExpanded()) { - if (mManageEduView != null) { - mManageEduView.hide(false /* show */); - } - - // If we're expanded, tell the animation controller to prepare to drag this bubble, - // dispatching to the individual bubble magnet listener. - mExpandedAnimationController.prepareForBubbleDrag( - v /* bubble */, - mMagneticTarget, - mIndividualBubbleMagnetListener); - - hideCurrentInputMethod(); - - // Save the magnetized individual bubble so we can dispatch touch events to it. - mMagnetizedObject = mExpandedAnimationController.getMagnetizedBubbleDraggingOut(); - } else { - // If we're collapsed, prepare to drag the stack. Cancel active animations, set the - // animation controller, and hide the flyout. - mStackAnimationController.cancelStackPositionAnimations(); - mBubbleContainer.setActiveController(mStackAnimationController); - hideFlyoutImmediate(); - - // Also, save the magnetized stack so we can dispatch touch events to it. - mMagnetizedObject = mStackAnimationController.getMagnetizedStack(mMagneticTarget); - mMagnetizedObject.setMagnetListener(mStackMagnetListener); - - mIsDraggingStack = true; - - // Cancel animations to make the stack temporarily invisible, since we're now - // dragging it. - updateTemporarilyInvisibleAnimation(false /* hideImmediately */); - } - - passEventToMagnetizedObject(ev); - - // Bubbles are always interested in all touch events! - return true; - } - - @Override - public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, - float viewInitialY, float dx, float dy) { - // If we're expanding or collapsing, ignore all touch events. - if (mIsExpansionAnimating) { - return; - } - - // Show the dismiss target, if we haven't already. - mDismissView.show(); - - // First, see if the magnetized object consumes the event - if so, we shouldn't move the - // bubble since it's stuck to the target. - if (!passEventToMagnetizedObject(ev)) { - if (mBubbleData.isExpanded()) { - mExpandedAnimationController.dragBubbleOut( - v, viewInitialX + dx, viewInitialY + dy); - } else { - if (mStackEduView != null) { - mStackEduView.hide(false /* fromExpansion */); - } - mStackAnimationController.moveStackFromTouch( - viewInitialX + dx, viewInitialY + dy); - } - } - } - - @Override - public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, - float viewInitialY, float dx, float dy, float velX, float velY) { - // If we're expanding or collapsing, ignore all touch events. - if (mIsExpansionAnimating) { - return; - } - - // First, see if the magnetized object consumes the event - if so, the bubble was - // released in the target or flung out of it, and we should ignore the event. - if (!passEventToMagnetizedObject(ev)) { - if (mBubbleData.isExpanded()) { - mExpandedAnimationController.snapBubbleBack(v, velX, velY); - } else { - // Fling the stack to the edge, and save whether or not it's going to end up on - // the left side of the screen. - mStackOnLeftOrWillBe = - mStackAnimationController.flingStackThenSpringToEdge( - viewInitialX + dx, velX, velY) <= 0; - updateBubbleIcons(); - logBubbleEvent(null /* no bubble associated with bubble stack move */, - FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED); - } - mDismissView.hide(); - } - - mIsDraggingStack = false; - - // Hide the stack after a delay, if needed. - updateTemporarilyInvisibleAnimation(false /* hideImmediately */); - } - }; - - /** Click listener set on the flyout, which expands the stack when the flyout is tapped. */ - private OnClickListener mFlyoutClickListener = new OnClickListener() { - @Override - public void onClick(View view) { - if (maybeShowStackEdu()) { - // 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(); - } - }; - - /** Touch listener for the flyout. This enables the drag-to-dismiss gesture on the flyout. */ - private RelativeTouchListener mFlyoutTouchListener = new RelativeTouchListener() { - - @Override - public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) { - mFlyout.removeCallbacks(mHideFlyout); - return true; - } - - @Override - public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, - float viewInitialY, float dx, float dy) { - setFlyoutStateForDragLength(dx); - } - - @Override - public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, - float viewInitialY, float dx, float dy, float velX, float velY) { - final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); - final boolean metRequiredVelocity = - onLeft ? velX < -FLYOUT_DISMISS_VELOCITY : velX > FLYOUT_DISMISS_VELOCITY; - final boolean metRequiredDeltaX = - onLeft - ? dx < -mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS - : dx > mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS; - final boolean isCancelFling = onLeft ? velX > 0 : velX < 0; - final boolean shouldDismiss = metRequiredVelocity - || (metRequiredDeltaX && !isCancelFling); - - mFlyout.removeCallbacks(mHideFlyout); - animateFlyoutCollapsed(shouldDismiss, velX); - - maybeShowStackEdu(); - } - }; - - private DismissView mDismissView; - private int mOrientation = Configuration.ORIENTATION_UNDEFINED; - - @Nullable - private BubbleOverflow mBubbleOverflow; - private StackEducationView mStackEduView; - private ManageEducationView mManageEduView; - - private ViewGroup mManageMenu; - private ImageView mManageSettingsIcon; - private TextView mManageSettingsText; - private boolean mShowingManage = false; - private PhysicsAnimator.SpringConfig mManageSpringConfig = new PhysicsAnimator.SpringConfig( - SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY); - @SuppressLint("ClickableViewAccessibility") - public BubbleStackView(Context context, BubbleData data, - @Nullable SurfaceSynchronizer synchronizer, - FloatingContentCoordinator floatingContentCoordinator, - Runnable allBubblesAnimatedOutAction, - Consumer<Boolean> onImeVisibilityChanged, - Runnable hideCurrentInputMethodCallback, - Consumer<Boolean> onBubbleExpandChanged) { - super(context); - - mBubbleData = data; - - Resources res = getResources(); - mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered); - mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size); - mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); - mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); - mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding); - - mStatusBarHeight = - res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); - mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset); - - mDisplaySize = new Point(); - WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - // We use the real size & subtract screen decorations / window insets ourselves when needed - wm.getDefaultDisplay().getRealSize(mDisplaySize); - - mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); - int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); - - final TypedArray ta = mContext.obtainStyledAttributes( - new int[] {android.R.attr.dialogCornerRadius}); - mCornerRadius = ta.getDimensionPixelSize(0, 0); - ta.recycle(); - - final Runnable onBubbleAnimatedOut = () -> { - if (getBubbleCount() == 0) { - allBubblesAnimatedOutAction.run(); - } - }; - - mStackAnimationController = new StackAnimationController( - floatingContentCoordinator, this::getBubbleCount, onBubbleAnimatedOut); - - mExpandedAnimationController = new ExpandedAnimationController( - mDisplaySize, mExpandedViewPadding, res.getConfiguration().orientation, - onBubbleAnimatedOut); - mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER; - - // Force LTR by default since most of the Bubbles UI is positioned manually by the user, or - // is centered. It greatly simplifies translation positioning/animations. Views that will - // actually lay out differently in RTL, such as the flyout and expanded view, will set their - // layout direction to LOCALE. - setLayoutDirection(LAYOUT_DIRECTION_LTR); - - mBubbleContainer = new PhysicsAnimationLayout(context); - mBubbleContainer.setActiveController(mStackAnimationController); - mBubbleContainer.setElevation(elevation); - mBubbleContainer.setClipChildren(false); - addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); - - updateUserEdu(); - - mExpandedViewContainer = new FrameLayout(context); - mExpandedViewContainer.setElevation(elevation); - mExpandedViewContainer.setClipChildren(false); - addView(mExpandedViewContainer); - - mAnimatingOutSurfaceContainer = new FrameLayout(getContext()); - mAnimatingOutSurfaceContainer.setLayoutParams( - new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); - addView(mAnimatingOutSurfaceContainer); - - mAnimatingOutSurfaceView = new SurfaceView(getContext()); - mAnimatingOutSurfaceView.setUseAlpha(); - mAnimatingOutSurfaceView.setZOrderOnTop(true); - mAnimatingOutSurfaceView.setCornerRadius(mCornerRadius); - mAnimatingOutSurfaceView.setLayoutParams(new ViewGroup.LayoutParams(0, 0)); - mAnimatingOutSurfaceContainer.addView(mAnimatingOutSurfaceView); - - mAnimatingOutSurfaceContainer.setPadding( - mExpandedViewPadding, - mExpandedViewPadding, - mExpandedViewPadding, - mExpandedViewPadding); - - setUpManageMenu(); - - setUpFlyout(); - mFlyoutTransitionSpring.setSpring(new SpringForce() - .setStiffness(SpringForce.STIFFNESS_LOW) - .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)); - mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring); - - mDismissView = new DismissView(context); - addView(mDismissView); - - final ContentResolver contentResolver = getContext().getContentResolver(); - final int dismissRadius = Settings.Secure.getInt( - contentResolver, "bubble_dismiss_radius", mBubbleSize * 2 /* default */); - - // Save the MagneticTarget instance for the newly set up view - we'll add this to the - // MagnetizedObjects. - mMagneticTarget = new MagnetizedObject.MagneticTarget( - mDismissView.getCircle(), dismissRadius); - - setClipChildren(false); - setFocusable(true); - mBubbleContainer.bringToFront(); - - mBubbleOverflow = new BubbleOverflow(getContext(), this); - mBubbleContainer.addView(mBubbleOverflow.getIconView(), - mBubbleContainer.getChildCount() /* index */, - new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT)); - updateOverflow(); - mBubbleOverflow.getIconView().setOnClickListener((View v) -> { - setSelectedBubble(mBubbleOverflow); - showManageMenu(false); - }); - - mOnImeVisibilityChanged = onImeVisibilityChanged; - mHideCurrentInputMethodCallback = hideCurrentInputMethodCallback; - mOnBubbleExpandChanged = onBubbleExpandChanged; - - setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> { - onImeVisibilityChanged.accept(insets.getInsets(WindowInsets.Type.ime()).bottom > 0); - if (!mIsExpanded || mIsExpansionAnimating) { - return view.onApplyWindowInsets(insets); - } - return view.onApplyWindowInsets(insets); - }); - - mOrientationChangedListener = - (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { - mExpandedAnimationController.updateResources(mOrientation, mDisplaySize); - mStackAnimationController.updateResources(mOrientation); - mBubbleOverflow.updateResources(); - - // Need to update the padding around the view - WindowInsets insets = getRootWindowInsets(); - int leftPadding = mExpandedViewPadding; - int rightPadding = mExpandedViewPadding; - if (insets != null) { - // Can't have the expanded view overlaying notches - int cutoutLeft = 0; - int cutoutRight = 0; - DisplayCutout cutout = insets.getDisplayCutout(); - if (cutout != null) { - cutoutLeft = cutout.getSafeInsetLeft(); - cutoutRight = cutout.getSafeInsetRight(); - } - // Or overlaying nav or status bar - leftPadding += Math.max(cutoutLeft, insets.getStableInsetLeft()); - rightPadding += Math.max(cutoutRight, insets.getStableInsetRight()); - } - mExpandedViewContainer.setPadding(leftPadding, mExpandedViewPadding, - rightPadding, mExpandedViewPadding); - - if (mIsExpanded) { - // Re-draw bubble row and pointer for new orientation. - beforeExpandedViewAnimation(); - updateOverflowVisibility(); - updatePointerPosition(); - mExpandedAnimationController.expandFromStack(() -> { - afterExpandedViewAnimation(); - } /* after */); - mExpandedViewContainer.setTranslationX(0); - mExpandedViewContainer.setTranslationY(getExpandedViewY()); - mExpandedViewContainer.setAlpha(1f); - } - if (mRelativeStackPositionBeforeRotation != null) { - mStackAnimationController.setStackPosition( - mRelativeStackPositionBeforeRotation); - mRelativeStackPositionBeforeRotation = null; - } - removeOnLayoutChangeListener(mOrientationChangedListener); - }; - - // This must be a separate OnDrawListener since it should be called for every draw. - getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater); - - final ColorMatrix animatedMatrix = new ColorMatrix(); - final ColorMatrix darkenMatrix = new ColorMatrix(); - - mDesaturateAndDarkenAnimator = ValueAnimator.ofFloat(1f, 0f); - mDesaturateAndDarkenAnimator.addUpdateListener(animation -> { - final float animatedValue = (float) animation.getAnimatedValue(); - animatedMatrix.setSaturation(animatedValue); - - final float animatedDarkenValue = (1f - animatedValue) * DARKEN_PERCENT; - darkenMatrix.setScale( - 1f - animatedDarkenValue /* red */, - 1f - animatedDarkenValue /* green */, - 1f - animatedDarkenValue /* blue */, - 1f /* alpha */); - - // Concat the matrices so that the animatedMatrix both desaturates and darkens. - animatedMatrix.postConcat(darkenMatrix); - - // Update the paint and apply it to the bubble container. - mDesaturateAndDarkenPaint.setColorFilter(new ColorMatrixColorFilter(animatedMatrix)); - - if (mDesaturateAndDarkenTargetView != null) { - mDesaturateAndDarkenTargetView.setLayerPaint(mDesaturateAndDarkenPaint); - } - }); - - // If the stack itself is touched, it means none of its touchable views (bubbles, flyouts, - // ActivityViews, etc.) were touched. Collapse the stack if it's expanded. - setOnTouchListener((view, ev) -> { - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - if (mShowingManage) { - showManageMenu(false /* show */); - } else if (mBubbleData.isExpanded()) { - mBubbleData.setExpanded(false); - } - } - - return true; - }); - - animate() - .setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED) - .setDuration(FADE_IN_DURATION); - } - - /** - * Sets whether or not the stack should become temporarily invisible by moving off the side of - * the screen. - * - * If a flyout comes in while it's invisible, it will animate back in while the flyout is - * showing but disappear again when the flyout is gone. - */ - public void setTemporarilyInvisible(boolean invisible) { - mTemporarilyInvisible = invisible; - - // If we are animating out, hide immediately if possible so we animate out with the status - // bar. - updateTemporarilyInvisibleAnimation(invisible /* hideImmediately */); - } - - /** - * Animates the stack to be temporarily invisible, if needed. - * - * If we're currently dragging the stack, or a flyout is visible, the stack will remain visible. - * regardless of the value of {@link #mTemporarilyInvisible}. This method is called on ACTION_UP - * as well as whenever a flyout hides, so we will animate invisible at that point if needed. - */ - private void updateTemporarilyInvisibleAnimation(boolean hideImmediately) { - removeCallbacks(mAnimateTemporarilyInvisibleImmediate); - - if (mIsDraggingStack) { - // If we're dragging the stack, don't animate it invisible. - return; - } - - final boolean shouldHide = - mTemporarilyInvisible && mFlyout.getVisibility() != View.VISIBLE; - - postDelayed(mAnimateTemporarilyInvisibleImmediate, - shouldHide && !hideImmediately ? ANIMATE_TEMPORARILY_INVISIBLE_DELAY : 0); - } - - private final Runnable mAnimateTemporarilyInvisibleImmediate = () -> { - if (mTemporarilyInvisible && mFlyout.getVisibility() != View.VISIBLE) { - if (mStackAnimationController.isStackOnLeftSide()) { - animate().translationX(-mBubbleSize).start(); - } else { - animate().translationX(mBubbleSize).start(); - } - } else { - animate().translationX(0).start(); - } - }; - - private void setUpManageMenu() { - if (mManageMenu != null) { - removeView(mManageMenu); - } - - mManageMenu = (ViewGroup) LayoutInflater.from(getContext()).inflate( - R.layout.bubble_manage_menu, this, false); - mManageMenu.setVisibility(View.INVISIBLE); - - PhysicsAnimator.getInstance(mManageMenu).setDefaultSpringConfig(mManageSpringConfig); - - mManageMenu.setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius); - } - }); - mManageMenu.setClipToOutline(true); - - mManageMenu.findViewById(R.id.bubble_manage_menu_dismiss_container).setOnClickListener( - view -> { - showManageMenu(false /* show */); - dismissBubbleIfExists(mBubbleData.getSelectedBubble()); - }); - - mManageMenu.findViewById(R.id.bubble_manage_menu_dont_bubble_container).setOnClickListener( - view -> { - showManageMenu(false /* show */); - mUnbubbleConversationCallback.accept(mBubbleData.getSelectedBubble().getKey()); - }); - - mManageMenu.findViewById(R.id.bubble_manage_menu_settings_container).setOnClickListener( - view -> { - showManageMenu(false /* show */); - final Bubble bubble = mBubbleData.getSelectedBubble(); - if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { - final Intent intent = bubble.getSettingsIntent(mContext); - mBubbleData.setExpanded(false); - mContext.startActivityAsUser(intent, bubble.getUser()); - logBubbleEvent(bubble, - FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS); - } - }); - - mManageSettingsIcon = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_icon); - mManageSettingsText = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_name); - - // The menu itself should respect locale direction so the icons are on the correct side. - mManageMenu.setLayoutDirection(LAYOUT_DIRECTION_LOCALE); - addView(mManageMenu); - } - - /** - * Whether the educational view should show for the expanded view "manage" menu. - */ - private boolean shouldShowManageEdu() { - final boolean seen = getPrefBoolean(ManageEducationViewKt.PREF_MANAGED_EDUCATION); - final boolean shouldShow = (!seen || BubbleDebugConfig.forceShowUserEducation(mContext)) - && mExpandedBubble != null; - if (BubbleDebugConfig.DEBUG_USER_EDUCATION) { - Log.d(TAG, "Show manage edu: " + shouldShow); - } - return shouldShow; - } - - private void maybeShowManageEdu() { - if (!shouldShowManageEdu()) { - return; - } - if (mManageEduView == null) { - mManageEduView = new ManageEducationView(mContext); - addView(mManageEduView); - } - mManageEduView.show(mExpandedBubble.getExpandedView(), mTempRect); - } - - /** - * Whether education view should show for the collapsed stack. - */ - private boolean shouldShowStackEdu() { - final boolean seen = getPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION); - final boolean shouldShow = !seen || BubbleDebugConfig.forceShowUserEducation(mContext); - if (BubbleDebugConfig.DEBUG_USER_EDUCATION) { - Log.d(TAG, "Show stack edu: " + shouldShow); - } - return shouldShow; - } - - private boolean getPrefBoolean(String key) { - return mContext.getSharedPreferences(mContext.getPackageName(), Context.MODE_PRIVATE) - .getBoolean(key, false /* default */); - } - - /** - * @return true if education view for collapsed stack should show and was not showing before. - */ - private boolean maybeShowStackEdu() { - if (!shouldShowStackEdu()) { - return false; - } - if (mStackEduView == null) { - mStackEduView = new StackEducationView(mContext); - addView(mStackEduView); - } - return mStackEduView.show(mStackAnimationController.getStartPosition()); - } - - private void updateUserEdu() { - maybeShowStackEdu(); - if (mManageEduView != null) { - mManageEduView.invalidate(); - } - maybeShowManageEdu(); - if (mStackEduView != null) { - mStackEduView.invalidate(); - } - } - - @SuppressLint("ClickableViewAccessibility") - private void setUpFlyout() { - if (mFlyout != null) { - removeView(mFlyout); - } - mFlyout = new BubbleFlyoutView(getContext()); - mFlyout.setVisibility(GONE); - mFlyout.animate() - .setDuration(FLYOUT_ALPHA_ANIMATION_DURATION) - .setInterpolator(new AccelerateDecelerateInterpolator()); - mFlyout.setOnClickListener(mFlyoutClickListener); - mFlyout.setOnTouchListener(mFlyoutTouchListener); - addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); - } - - void updateFlyout(float fontScale) { - mFlyout.updateFontSize(fontScale); - } - - private void updateOverflow() { - mBubbleOverflow.update(); - mBubbleContainer.reorderView(mBubbleOverflow.getIconView(), - mBubbleContainer.getChildCount() - 1 /* index */); - updateOverflowVisibility(); - } - - void updateOverflowButtonDot() { - for (Bubble b : mBubbleData.getOverflowBubbles()) { - if (b.showDot()) { - mBubbleOverflow.setShowDot(true); - return; - } - } - mBubbleOverflow.setShowDot(false); - } - - /** - * Handle theme changes. - */ - public void onThemeChanged() { - setUpFlyout(); - setUpManageMenu(); - updateOverflow(); - updateUserEdu(); - updateExpandedViewTheme(); - } - - /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */ - public void onOrientationChanged(int orientation) { - mOrientation = orientation; - - // Display size is based on the rotation device was in when requested, we should update it - // We use the real size & subtract screen decorations / window insets ourselves when needed - WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); - wm.getDefaultDisplay().getRealSize(mDisplaySize); - - // Some resources change depending on orientation - Resources res = getContext().getResources(); - mStatusBarHeight = res.getDimensionPixelSize( - com.android.internal.R.dimen.status_bar_height); - mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); - - mRelativeStackPositionBeforeRotation = mStackAnimationController.getRelativeStackPosition(); - addOnLayoutChangeListener(mOrientationChangedListener); - hideFlyoutImmediate(); - - mManageMenu.setVisibility(View.INVISIBLE); - mShowingManage = false; - } - - /** Tells the views with locale-dependent layout direction to resolve the new direction. */ - public void onLayoutDirectionChanged(int direction) { - mManageMenu.setLayoutDirection(direction); - mFlyout.setLayoutDirection(direction); - if (mStackEduView != null) { - mStackEduView.setLayoutDirection(direction); - } - if (mManageEduView != null) { - mManageEduView.setLayoutDirection(direction); - } - updateExpandedViewDirection(direction); - } - - /** Respond to the display size change by recalculating view size and location. */ - public void onDisplaySizeChanged() { - updateOverflow(); - - WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); - wm.getDefaultDisplay().getRealSize(mDisplaySize); - Resources res = getContext().getResources(); - mStatusBarHeight = res.getDimensionPixelSize( - com.android.internal.R.dimen.status_bar_height); - mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); - mBubbleSize = getResources().getDimensionPixelSize(R.dimen.individual_bubble_size); - for (Bubble b : mBubbleData.getBubbles()) { - if (b.getIconView() == null) { - Log.d(TAG, "Display size changed. Icon null: " + b); - continue; - } - b.getIconView().setLayoutParams(new LayoutParams(mBubbleSize, mBubbleSize)); - } - mExpandedAnimationController.updateResources(mOrientation, mDisplaySize); - mStackAnimationController.updateResources(mOrientation); - mDismissView.updateResources(); - mMagneticTarget.setMagneticFieldRadiusPx(mBubbleSize * 2); - } - - @Override - public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { - inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); - - mTempRect.setEmpty(); - getTouchableRegion(mTempRect); - if (mIsExpanded && mExpandedBubble != null - && mExpandedBubble.getExpandedView() != null - && mExpandedBubble.getExpandedView().getTaskView() != null) { - inoutInfo.touchableRegion.set(mTempRect); - mExpandedBubble.getExpandedView().getTaskView().getBoundsOnScreen(mTempRect); - inoutInfo.touchableRegion.op(mTempRect, Region.Op.DIFFERENCE); - } else { - inoutInfo.touchableRegion.set(mTempRect); - } - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - getViewTreeObserver().addOnComputeInternalInsetsListener(this); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - getViewTreeObserver().removeOnPreDrawListener(mViewUpdater); - getViewTreeObserver().removeOnComputeInternalInsetsListener(this); - if (mBubbleOverflow != null && mBubbleOverflow.getExpandedView() != null) { - mBubbleOverflow.getExpandedView().cleanUpExpandedState(); - } - } - - @Override - public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfoInternal(info); - setupLocalMenu(info); - } - - void updateExpandedViewTheme() { - final List<Bubble> bubbles = mBubbleData.getBubbles(); - if (bubbles.isEmpty()) { - return; - } - bubbles.forEach(bubble -> { - if (bubble.getExpandedView() != null) { - bubble.getExpandedView().applyThemeAttrs(); - } - }); - } - - void updateExpandedViewDirection(int direction) { - final List<Bubble> bubbles = mBubbleData.getBubbles(); - if (bubbles.isEmpty()) { - return; - } - bubbles.forEach(bubble -> { - if (bubble.getExpandedView() != null) { - bubble.getExpandedView().setLayoutDirection(direction); - } - }); - } - - void setupLocalMenu(AccessibilityNodeInfo info) { - Resources res = mContext.getResources(); - - // Custom local actions. - AccessibilityAction moveTopLeft = new AccessibilityAction(R.id.action_move_top_left, - res.getString(R.string.bubble_accessibility_action_move_top_left)); - info.addAction(moveTopLeft); - - AccessibilityAction moveTopRight = new AccessibilityAction(R.id.action_move_top_right, - res.getString(R.string.bubble_accessibility_action_move_top_right)); - info.addAction(moveTopRight); - - AccessibilityAction moveBottomLeft = new AccessibilityAction(R.id.action_move_bottom_left, - res.getString(R.string.bubble_accessibility_action_move_bottom_left)); - info.addAction(moveBottomLeft); - - AccessibilityAction moveBottomRight = new AccessibilityAction(R.id.action_move_bottom_right, - res.getString(R.string.bubble_accessibility_action_move_bottom_right)); - info.addAction(moveBottomRight); - - // Default actions. - info.addAction(AccessibilityAction.ACTION_DISMISS); - if (mIsExpanded) { - info.addAction(AccessibilityAction.ACTION_COLLAPSE); - } else { - info.addAction(AccessibilityAction.ACTION_EXPAND); - } - } - - @Override - public boolean performAccessibilityActionInternal(int action, Bundle arguments) { - if (super.performAccessibilityActionInternal(action, arguments)) { - return true; - } - final RectF stackBounds = mStackAnimationController.getAllowableStackPositionRegion(); - - // R constants are not final so we cannot use switch-case here. - if (action == AccessibilityNodeInfo.ACTION_DISMISS) { - mBubbleData.dismissAll(BubbleController.DISMISS_ACCESSIBILITY_ACTION); - announceForAccessibility( - getResources().getString(R.string.accessibility_bubble_dismissed)); - return true; - } else if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) { - mBubbleData.setExpanded(false); - return true; - } else if (action == AccessibilityNodeInfo.ACTION_EXPAND) { - mBubbleData.setExpanded(true); - return true; - } else if (action == R.id.action_move_top_left) { - mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.top); - return true; - } else if (action == R.id.action_move_top_right) { - mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.top); - return true; - } else if (action == R.id.action_move_bottom_left) { - mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.bottom); - return true; - } else if (action == R.id.action_move_bottom_right) { - mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.bottom); - return true; - } - return false; - } - - /** - * Update content description for a11y TalkBack. - */ - public void updateContentDescription() { - if (mBubbleData.getBubbles().isEmpty()) { - return; - } - - for (int i = 0; i < mBubbleData.getBubbles().size(); i++) { - final Bubble bubble = mBubbleData.getBubbles().get(i); - final String appName = bubble.getAppName(); - - String titleStr = bubble.getTitle(); - if (titleStr == null) { - titleStr = getResources().getString(R.string.notification_bubble_title); - } - - if (bubble.getIconView() != null) { - if (mIsExpanded || i > 0) { - bubble.getIconView().setContentDescription(getResources().getString( - R.string.bubble_content_description_single, titleStr, appName)); - } else { - final int moreCount = mBubbleContainer.getChildCount() - 1; - bubble.getIconView().setContentDescription(getResources().getString( - R.string.bubble_content_description_stack, - titleStr, appName, moreCount)); - } - } - } - } - - private void updateSystemGestureExcludeRects() { - // Exclude the region occupied by the first BubbleView in the stack - Rect excludeZone = mSystemGestureExclusionRects.get(0); - if (getBubbleCount() > 0) { - View firstBubble = mBubbleContainer.getChildAt(0); - excludeZone.set(firstBubble.getLeft(), firstBubble.getTop(), firstBubble.getRight(), - firstBubble.getBottom()); - excludeZone.offset((int) (firstBubble.getTranslationX() + 0.5f), - (int) (firstBubble.getTranslationY() + 0.5f)); - mBubbleContainer.setSystemGestureExclusionRects(mSystemGestureExclusionRects); - } else { - excludeZone.setEmpty(); - mBubbleContainer.setSystemGestureExclusionRects(Collections.emptyList()); - } - } - - /** - * Sets the listener to notify when the bubble stack is expanded. - */ - public void setExpandListener(BubbleController.BubbleExpandListener listener) { - mExpandListener = listener; - } - - /** Sets the function to call to un-bubble the given conversation. */ - public void setUnbubbleConversationCallback( - Consumer<String> unbubbleConversationCallback) { - mUnbubbleConversationCallback = unbubbleConversationCallback; - } - - /** - * Whether the stack of bubbles is expanded or not. - */ - public boolean isExpanded() { - return mIsExpanded; - } - - /** - * Whether the stack of bubbles is animating to or from expansion. - */ - public boolean isExpansionAnimating() { - return mIsExpansionAnimating; - } - - /** - * The {@link Bubble} that is expanded, null if one does not exist. - */ - @Nullable - BubbleViewProvider getExpandedBubble() { - return mExpandedBubble; - } - - // via BubbleData.Listener - @SuppressLint("ClickableViewAccessibility") - void addBubble(Bubble bubble) { - if (DEBUG_BUBBLE_STACK_VIEW) { - Log.d(TAG, "addBubble: " + bubble); - } - - if (getBubbleCount() == 0 && shouldShowStackEdu()) { - // Override the default stack position if we're showing user education. - mStackAnimationController.setStackPosition( - mStackAnimationController.getStartPosition()); - } - - if (getBubbleCount() == 0) { - mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); - } - - if (bubble.getIconView() == null) { - return; - } - - // Set the dot position to the opposite of the side the stack is resting on, since the stack - // resting slightly off-screen would result in the dot also being off-screen. - bubble.getIconView().setDotBadgeOnLeft(!mStackOnLeftOrWillBe /* onLeft */); - - bubble.getIconView().setOnClickListener(mBubbleClickListener); - bubble.getIconView().setOnTouchListener(mBubbleTouchListener); - - mBubbleContainer.addView(bubble.getIconView(), 0, - new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); - animateInFlyoutForBubble(bubble); - requestUpdate(); - logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__POSTED); - } - - // via BubbleData.Listener - void removeBubble(Bubble bubble) { - if (DEBUG_BUBBLE_STACK_VIEW) { - Log.d(TAG, "removeBubble: " + bubble); - } - // Remove it from the views - for (int i = 0; i < getBubbleCount(); i++) { - View v = mBubbleContainer.getChildAt(i); - if (v instanceof BadgedImageView - && ((BadgedImageView) v).getKey().equals(bubble.getKey())) { - mBubbleContainer.removeViewAt(i); - if (mBubbleData.hasOverflowBubbleWithKey(bubble.getKey())) { - bubble.cleanupExpandedView(); - } else { - bubble.cleanupViews(); - } - updatePointerPosition(); - logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); - return; - } - } - Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble); - } - - private void updateOverflowVisibility() { - if (mBubbleOverflow == null) { - return; - } - mBubbleOverflow.setVisible(mIsExpanded ? VISIBLE : GONE); - } - - // via BubbleData.Listener - void updateBubble(Bubble bubble) { - animateInFlyoutForBubble(bubble); - requestUpdate(); - logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__UPDATED); - } - - public void updateBubbleOrder(List<Bubble> bubbles) { - for (int i = 0; i < bubbles.size(); i++) { - Bubble bubble = bubbles.get(i); - mBubbleContainer.reorderView(bubble.getIconView(), i); - } - updateBubbleIcons(); - updatePointerPosition(); - } - - /** - * Changes the currently selected bubble. If the stack is already expanded, the newly selected - * bubble will be shown immediately. This does not change the expanded state or change the - * position of any bubble. - */ - // via BubbleData.Listener - public void setSelectedBubble(@Nullable BubbleViewProvider bubbleToSelect) { - if (DEBUG_BUBBLE_STACK_VIEW) { - Log.d(TAG, "setSelectedBubble: " + bubbleToSelect); - } - - if (bubbleToSelect == null) { - mBubbleData.setShowingOverflow(false); - return; - } - - // Ignore this new bubble only if it is the exact same bubble object. Otherwise, we'll want - // to re-render it even if it has the same key (equals() returns true). If the currently - // expanded bubble is removed and instantly re-added, we'll get back a new Bubble instance - // with the same key (with newly inflated expanded views), and we need to render those new - // views. - if (mExpandedBubble == bubbleToSelect) { - return; - } - - if (bubbleToSelect.getKey().equals(BubbleOverflow.KEY)) { - mBubbleData.setShowingOverflow(true); - } else { - mBubbleData.setShowingOverflow(false); - } - - if (mIsExpanded && mIsExpansionAnimating) { - // If the bubble selection changed during the expansion animation, the expanding bubble - // probably crashed or immediately removed itself (or, we just got unlucky with a new - // auto-expanding bubble showing up at just the right time). Cancel the animations so we - // can start fresh. - cancelAllExpandCollapseSwitchAnimations(); - } - - // If we're expanded, screenshot the currently expanded bubble (before expanding the newly - // selected bubble) so we can animate it out. - if (mIsExpanded && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { - if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { - // Before screenshotting, have the real ActivityView show on top of other surfaces - // so that the screenshot doesn't flicker on top of it. - mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true); - } - - try { - screenshotAnimatingOutBubbleIntoSurface((success) -> { - mAnimatingOutSurfaceContainer.setVisibility( - success ? View.VISIBLE : View.INVISIBLE); - showNewlySelectedBubble(bubbleToSelect); - }); - } catch (Exception e) { - showNewlySelectedBubble(bubbleToSelect); - e.printStackTrace(); - } - } else { - showNewlySelectedBubble(bubbleToSelect); - } - } - - private void showNewlySelectedBubble(BubbleViewProvider bubbleToSelect) { - final BubbleViewProvider previouslySelected = mExpandedBubble; - mExpandedBubble = bubbleToSelect; - updatePointerPosition(); - - if (mIsExpanded) { - hideCurrentInputMethod(); - - // Make the container of the expanded view transparent before removing the expanded view - // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the - // expanded view becomes visible on the screen. See b/126856255 - mExpandedViewContainer.setAlpha(0.0f); - mSurfaceSynchronizer.syncSurfaceAndRun(() -> { - if (previouslySelected != null) { - previouslySelected.setContentVisibility(false); - } - - updateExpandedBubble(); - requestUpdate(); - - logBubbleEvent(previouslySelected, - FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); - logBubbleEvent(bubbleToSelect, - FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); - notifyExpansionChanged(previouslySelected, false /* expanded */); - notifyExpansionChanged(bubbleToSelect, true /* expanded */); - }); - } - } - - /** - * Changes the expanded state of the stack. - * - * @param shouldExpand whether the bubble stack should appear expanded - */ - // via BubbleData.Listener - public void setExpanded(boolean shouldExpand) { - if (DEBUG_BUBBLE_STACK_VIEW) { - Log.d(TAG, "setExpanded: " + shouldExpand); - } - - if (!shouldExpand) { - // If we're collapsing, release the animating-out surface immediately since we have no - // need for it, and this ensures it cannot remain visible as we collapse. - releaseAnimatingOutBubbleBuffer(); - } - - if (shouldExpand == mIsExpanded) { - return; - } - - hideCurrentInputMethod(); - - mOnBubbleExpandChanged.accept(shouldExpand); - - if (mIsExpanded) { - animateCollapse(); - logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); - } else { - animateExpansion(); - // TODO: move next line to BubbleData - logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); - logBubbleEvent(mExpandedBubble, - FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED); - } - notifyExpansionChanged(mExpandedBubble, mIsExpanded); - } - - /** - * Asks the BubbleController to hide the IME from anywhere, whether it's focused on Bubbles or - * not. - */ - void hideCurrentInputMethod() { - mHideCurrentInputMethodCallback.run(); - } - - private void beforeExpandedViewAnimation() { - mIsExpansionAnimating = true; - hideFlyoutImmediate(); - updateExpandedBubble(); - updateExpandedView(); - } - - private void afterExpandedViewAnimation() { - mIsExpansionAnimating = false; - updateExpandedView(); - requestUpdate(); - } - - private void animateExpansion() { - cancelDelayedExpandCollapseSwitchAnimations(); - - mIsExpanded = true; - if (mStackEduView != null) { - mStackEduView.hide(true /* fromExpansion */); - } - beforeExpandedViewAnimation(); - - mBubbleContainer.setActiveController(mExpandedAnimationController); - updateOverflowVisibility(); - updatePointerPosition(); - mExpandedAnimationController.expandFromStack(() -> { - if (mIsExpanded && mExpandedBubble.getExpandedView() != null) { - maybeShowManageEdu(); - } - } /* after */); - - mExpandedViewContainer.setTranslationX(0); - mExpandedViewContainer.setTranslationY(getExpandedViewY()); - mExpandedViewContainer.setAlpha(1f); - - // X-value of the bubble we're expanding, once it's settled in its row. - final float bubbleWillBeAtX = - mExpandedAnimationController.getBubbleLeft( - mBubbleData.getBubbles().indexOf(mExpandedBubble)); - - // How far horizontally the bubble will be animating. We'll wait a bit longer for bubbles - // that are animating farther, so that the expanded view doesn't move as much. - final float horizontalDistanceAnimated = - Math.abs(bubbleWillBeAtX - - mStackAnimationController.getStackPosition().x); - - // Wait for the path animation target to reach its end, and add a small amount of extra time - // if the bubble is moving a lot horizontally. - long startDelay = 0L; - - // Should not happen since we lay out before expanding, but just in case... - if (getWidth() > 0) { - startDelay = (long) - (ExpandedAnimationController.EXPAND_COLLAPSE_TARGET_ANIM_DURATION - + (horizontalDistanceAnimated / getWidth()) * 30); - } - - // Set the pivot point for the scale, so the expanded view animates out from the bubble. - mExpandedViewContainerMatrix.setScale( - 0f, 0f, - bubbleWillBeAtX + mBubbleSize / 2f, getExpandedViewY()); - mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); - - if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { - mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false); - } - - mDelayedAnimationHandler.postDelayed(() -> { - PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); - PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) - .spring(AnimatableScaleMatrix.SCALE_X, - AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), - mScaleInSpringConfig) - .spring(AnimatableScaleMatrix.SCALE_Y, - AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), - mScaleInSpringConfig) - .addUpdateListener((target, values) -> { - if (mExpandedBubble == null || mExpandedBubble.getIconView() == null) { - return; - } - mExpandedViewContainerMatrix.postTranslate( - mExpandedBubble.getIconView().getTranslationX() - - bubbleWillBeAtX, - 0); - mExpandedViewContainer.setAnimationMatrix( - mExpandedViewContainerMatrix); - }) - .withEndActions(() -> { - afterExpandedViewAnimation(); - if (mExpandedBubble != null - && mExpandedBubble.getExpandedView() != null) { - mExpandedBubble.getExpandedView() - .setSurfaceZOrderedOnTop(false); - } - }) - .start(); - }, startDelay); - } - - private void animateCollapse() { - cancelDelayedExpandCollapseSwitchAnimations(); - - // Hide the menu if it's visible. - showManageMenu(false); - - mIsExpanded = false; - mIsExpansionAnimating = true; - - mBubbleContainer.cancelAllAnimations(); - - // If we were in the middle of swapping, the animating-out surface would have been scaling - // to zero - finish it off. - PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel(); - mAnimatingOutSurfaceContainer.setScaleX(0f); - mAnimatingOutSurfaceContainer.setScaleY(0f); - - // Let the expanded animation controller know that it shouldn't animate child adds/reorders - // since we're about to animate collapsed. - mExpandedAnimationController.notifyPreparingToCollapse(); - - final long startDelay = - (long) (ExpandedAnimationController.EXPAND_COLLAPSE_TARGET_ANIM_DURATION * 0.6f); - mDelayedAnimationHandler.postDelayed(() -> mExpandedAnimationController.collapseBackToStack( - mStackAnimationController.getStackPositionAlongNearestHorizontalEdge() - /* collapseTo */, - () -> mBubbleContainer.setActiveController(mStackAnimationController)), startDelay); - - // We want to visually collapse into this bubble during the animation. - final View expandingFromBubble = mExpandedBubble.getIconView(); - - // X-value the bubble is animating from (back into the stack). - final float expandingFromBubbleAtX = - mExpandedAnimationController.getBubbleLeft( - mBubbleData.getBubbles().indexOf(mExpandedBubble)); - - // Set the pivot point. - mExpandedViewContainerMatrix.setScale( - 1f, 1f, - expandingFromBubbleAtX + mBubbleSize / 2f, - getExpandedViewY()); - - PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); - PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) - .spring(AnimatableScaleMatrix.SCALE_X, 0f, mScaleOutSpringConfig) - .spring(AnimatableScaleMatrix.SCALE_Y, 0f, mScaleOutSpringConfig) - .addUpdateListener((target, values) -> { - if (expandingFromBubble != null) { - // Follow the bubble as it translates! - mExpandedViewContainerMatrix.postTranslate( - expandingFromBubble.getTranslationX() - - expandingFromBubbleAtX, 0f); - } - - mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); - - // Hide early so we don't have a tiny little expanded view still visible at the - // end of the scale animation. - if (mExpandedViewContainerMatrix.getScaleX() < 0.05f) { - mExpandedViewContainer.setVisibility(View.INVISIBLE); - } - }) - .withEndActions(() -> { - final BubbleViewProvider previouslySelected = mExpandedBubble; - beforeExpandedViewAnimation(); - if (mManageEduView != null) { - mManageEduView.hide(false /* fromExpansion */); - } - - if (DEBUG_BUBBLE_STACK_VIEW) { - Log.d(TAG, "animateCollapse"); - Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(), - mExpandedBubble)); - } - updateOverflowVisibility(); - - afterExpandedViewAnimation(); - if (previouslySelected != null) { - previouslySelected.setContentVisibility(false); - } - }) - .start(); - } - - private void animateSwitchBubbles() { - // If we're no longer expanded, this is meaningless. - if (!mIsExpanded) { - return; - } - - mIsBubbleSwitchAnimating = true; - - // The surface contains a screenshot of the animating out bubble, so we just need to animate - // it out (and then release the GraphicBuffer). - PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel(); - PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer) - .spring(DynamicAnimation.SCALE_X, 0f, mScaleOutSpringConfig) - .spring(DynamicAnimation.SCALE_Y, 0f, mScaleOutSpringConfig) - .spring(DynamicAnimation.TRANSLATION_Y, - mAnimatingOutSurfaceContainer.getTranslationY() - mBubbleSize * 2, - mTranslateSpringConfig) - .withEndActions(this::releaseAnimatingOutBubbleBuffer) - .start(); - - boolean isOverflow = mExpandedBubble != null - && mExpandedBubble.getKey().equals(BubbleOverflow.KEY); - float expandingFromBubbleDestinationX = - mExpandedAnimationController.getBubbleLeft(isOverflow ? getBubbleCount() - : mBubbleData.getBubbles().indexOf(mExpandedBubble)); - - mExpandedViewContainer.setAlpha(1f); - mExpandedViewContainer.setVisibility(View.VISIBLE); - - mExpandedViewContainerMatrix.setScale( - 0f, 0f, expandingFromBubbleDestinationX + mBubbleSize / 2f, getExpandedViewY()); - mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); - - mDelayedAnimationHandler.postDelayed(() -> { - if (!mIsExpanded) { - mIsBubbleSwitchAnimating = false; - return; - } - - PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); - PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) - .spring(AnimatableScaleMatrix.SCALE_X, - AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), - mScaleInSpringConfig) - .spring(AnimatableScaleMatrix.SCALE_Y, - AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), - mScaleInSpringConfig) - .addUpdateListener((target, values) -> { - mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); - }) - .withEndActions(() -> { - if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { - mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false); - } - - mIsBubbleSwitchAnimating = false; - }) - .start(); - }, 25); - } - - /** - * Cancels any delayed steps for expand/collapse and bubble switch animations, and resets the is - * animating flags for those animations. - */ - private void cancelDelayedExpandCollapseSwitchAnimations() { - mDelayedAnimationHandler.removeCallbacksAndMessages(null); - - mIsExpansionAnimating = false; - mIsBubbleSwitchAnimating = false; - } - - private void cancelAllExpandCollapseSwitchAnimations() { - cancelDelayedExpandCollapseSwitchAnimations(); - - PhysicsAnimator.getInstance(mAnimatingOutSurfaceView).cancel(); - PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); - - mExpandedViewContainer.setAnimationMatrix(null); - } - - private void notifyExpansionChanged(BubbleViewProvider bubble, boolean expanded) { - if (mExpandListener != null && bubble != null) { - mExpandListener.onBubbleExpandChanged(expanded, bubble.getKey()); - } - } - - /** Moves the bubbles out of the way if they're going to be over the keyboard. */ - public void onImeVisibilityChanged(boolean visible, int height) { - mStackAnimationController.setImeHeight(visible ? height + mImeOffset : 0); - - if (!mIsExpanded && getBubbleCount() > 0) { - final float stackDestinationY = - mStackAnimationController.animateForImeVisibility(visible); - - // How far the stack is animating due to IME, we'll just animate the flyout by that - // much too. - final float stackDy = - stackDestinationY - mStackAnimationController.getStackPosition().y; - - // If the flyout is visible, translate it along with the bubble stack. - if (mFlyout.getVisibility() == VISIBLE) { - PhysicsAnimator.getInstance(mFlyout) - .spring(DynamicAnimation.TRANSLATION_Y, - mFlyout.getTranslationY() + stackDy, - FLYOUT_IME_ANIMATION_SPRING_CONFIG) - .start(); - } - } else if (mIsExpanded && mExpandedBubble != null - && mExpandedBubble.getExpandedView() != null) { - mExpandedBubble.getExpandedView().setImeVisible(visible); - } - } - - /** - * This method is called by {@link android.app.ActivityView} because the BubbleStackView has a - * higher Z-index than the ActivityView (so that dragged-out bubbles are visible over the AV). - * ActivityView is asking BubbleStackView to subtract the stack's bounds from the provided - * touchable region, so that the ActivityView doesn't consume events meant for the stack. Due to - * the special nature of ActivityView, it does not respect the standard - * {@link #dispatchTouchEvent} and {@link #onInterceptTouchEvent} methods typically used for - * this purpose. - * - * BubbleStackView is MATCH_PARENT, so that bubbles can be positioned via their translation - * properties for performance reasons. This means that the default implementation of this method - * subtracts the entirety of the screen from the ActivityView's touchable region, resulting in - * it not receiving any touch events. This was previously addressed by returning false in the - * stack's {@link View#canReceivePointerEvents()} method, but this precluded the use of any - * touch handlers in the stack or its child views. - * - * To support touch handlers, we're overriding this method to leave the ActivityView's touchable - * region alone. The only touchable part of the stack that can ever overlap the AV is a - * dragged-out bubble that is animating back into the row of bubbles. It's not worth continually - * updating the touchable region to allow users to grab a bubble while it completes its ~50ms - * animation back to the bubble row. - * - * NOTE: Any future additions to the stack that obscure the ActivityView region will need their - * bounds subtracted here in order to receive touch events. - */ - @Override - public void subtractObscuredTouchableRegion(Region touchableRegion, View view) { - // If the notification shade is expanded, or the manage menu is open, or we are showing - // manage bubbles user education, we shouldn't let the ActivityView steal any touch events - // from any location. - if (!mIsExpanded - || mShowingManage - || (mManageEduView != null - && mManageEduView.getVisibility() == VISIBLE)) { - touchableRegion.setEmpty(); - } - } - - /** - * If you're here because you're not receiving touch events on a view that is a descendant of - * BubbleStackView, and you think BSV is intercepting them - it's not! You need to subtract the - * bounds of the view in question in {@link #subtractObscuredTouchableRegion}. The ActivityView - * consumes all touch events within its bounds, even for views like the BubbleStackView that are - * above it. It ignores typical view touch handling methods like this one and - * dispatchTouchEvent. - */ - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - return super.onInterceptTouchEvent(ev); - } - - @Override - public boolean dispatchTouchEvent(MotionEvent ev) { - if (ev.getAction() != MotionEvent.ACTION_DOWN && ev.getActionIndex() != mPointerIndexDown) { - // Ignore touches from additional pointer indices. - return false; - } - - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - mPointerIndexDown = ev.getActionIndex(); - } else if (ev.getAction() == MotionEvent.ACTION_UP - || ev.getAction() == MotionEvent.ACTION_CANCEL) { - mPointerIndexDown = -1; - } - - boolean dispatched = super.dispatchTouchEvent(ev); - - // If a new bubble arrives while the collapsed stack is being dragged, it will be positioned - // at the front of the stack (under the touch position). Subsequent ACTION_MOVE events will - // then be passed to the new bubble, which will not consume them since it hasn't received an - // ACTION_DOWN yet. Work around this by passing MotionEvents directly to the touch handler - // until the current gesture ends with an ACTION_UP event. - if (!dispatched && !mIsExpanded && mIsGestureInProgress) { - dispatched = mBubbleTouchListener.onTouch(this /* view */, ev); - } - - mIsGestureInProgress = - ev.getAction() != MotionEvent.ACTION_UP - && ev.getAction() != MotionEvent.ACTION_CANCEL; - - return dispatched; - } - - void setFlyoutStateForDragLength(float deltaX) { - // This shouldn't happen, but if it does, just wait until the flyout lays out. This method - // is continually called. - if (mFlyout.getWidth() <= 0) { - return; - } - - final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); - mFlyoutDragDeltaX = deltaX; - - final float collapsePercent = - onLeft ? -deltaX / mFlyout.getWidth() : deltaX / mFlyout.getWidth(); - mFlyout.setCollapsePercent(Math.min(1f, Math.max(0f, collapsePercent))); - - // Calculate how to translate the flyout if it has been dragged too far in either direction. - float overscrollTranslation = 0f; - if (collapsePercent < 0f || collapsePercent > 1f) { - // Whether we are more than 100% transitioned to the dot. - final boolean overscrollingPastDot = collapsePercent > 1f; - - // Whether we are overscrolling physically to the left - this can either be pulling the - // flyout away from the stack (if the stack is on the right) or pushing it to the left - // after it has already become the dot. - final boolean overscrollingLeft = - (onLeft && collapsePercent > 1f) || (!onLeft && collapsePercent < 0f); - overscrollTranslation = - (overscrollingPastDot ? collapsePercent - 1f : collapsePercent * -1) - * (overscrollingLeft ? -1 : 1) - * (mFlyout.getWidth() / (FLYOUT_OVERSCROLL_ATTENUATION_FACTOR - // Attenuate the smaller dot less than the larger flyout. - / (overscrollingPastDot ? 2 : 1))); - } - - mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation); - } - - /** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */ - private boolean passEventToMagnetizedObject(MotionEvent event) { - return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event); - } - - /** - * Dismisses the magnetized object - either an individual bubble, if we're expanded, or the - * stack, if we're collapsed. - */ - private void dismissMagnetizedObject() { - if (mIsExpanded) { - final View draggedOutBubbleView = (View) mMagnetizedObject.getUnderlyingObject(); - dismissBubbleIfExists(mBubbleData.getBubbleWithView(draggedOutBubbleView)); - } else { - mBubbleData.dismissAll(BubbleController.DISMISS_USER_GESTURE); - } - } - - private void dismissBubbleIfExists(@Nullable Bubble bubble) { - if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { - mBubbleData.dismissBubbleWithKey( - bubble.getKey(), BubbleController.DISMISS_USER_GESTURE); - } - } - - /** Prepares and starts the desaturate/darken animation on the bubble stack. */ - private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) { - mDesaturateAndDarkenTargetView = targetView; - - if (mDesaturateAndDarkenTargetView == null) { - return; - } - - if (desaturateAndDarken) { - // Use the animated paint for the bubbles. - mDesaturateAndDarkenTargetView.setLayerType( - View.LAYER_TYPE_HARDWARE, mDesaturateAndDarkenPaint); - mDesaturateAndDarkenAnimator.removeAllListeners(); - mDesaturateAndDarkenAnimator.start(); - } else { - mDesaturateAndDarkenAnimator.removeAllListeners(); - mDesaturateAndDarkenAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - // Stop using the animated paint. - resetDesaturationAndDarken(); - } - }); - mDesaturateAndDarkenAnimator.reverse(); - } - } - - private void resetDesaturationAndDarken() { - - mDesaturateAndDarkenAnimator.removeAllListeners(); - mDesaturateAndDarkenAnimator.cancel(); - - if (mDesaturateAndDarkenTargetView != null) { - mDesaturateAndDarkenTargetView.setLayerType(View.LAYER_TYPE_NONE, null); - mDesaturateAndDarkenTargetView = null; - } - } - - /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */ - private void animateFlyoutCollapsed(boolean collapsed, float velX) { - final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); - // If the flyout was tapped, we want a higher stiffness for the collapse animation so it's - // faster. - mFlyoutTransitionSpring.getSpring().setStiffness( - (mBubbleToExpandAfterFlyoutCollapse != null) - ? SpringForce.STIFFNESS_MEDIUM - : SpringForce.STIFFNESS_LOW); - mFlyoutTransitionSpring - .setStartValue(mFlyoutDragDeltaX) - .setStartVelocity(velX) - .animateToFinalPosition(collapsed - ? (onLeft ? -mFlyout.getWidth() : mFlyout.getWidth()) - : 0f); - } - - /** - * Calculates the y position of the expanded view when it is expanded. - */ - float getExpandedViewY() { - return getStatusBarHeight() + mBubbleSize + mBubblePaddingTop; - } - - private boolean shouldShowFlyout(Bubble bubble) { - Bubble.FlyoutMessage flyoutMessage = bubble.getFlyoutMessage(); - final BadgedImageView bubbleView = bubble.getIconView(); - if (flyoutMessage == null - || flyoutMessage.message == null - || !bubble.showFlyout() - || (mStackEduView != null && mStackEduView.getVisibility() == VISIBLE) - || isExpanded() - || mIsExpansionAnimating - || mIsGestureInProgress - || mBubbleToExpandAfterFlyoutCollapse != null - || bubbleView == null) { - if (bubbleView != null && mFlyout.getVisibility() != VISIBLE) { - bubbleView.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); - } - // Skip the message if none exists, we're expanded or animating expansion, or we're - // about to expand a bubble from the previous tapped flyout, or if bubble view is null. - return false; - } - return true; - } - - /** - * Animates in the flyout for the given bubble, if available, and then hides it after some time. - */ - @VisibleForTesting - void animateInFlyoutForBubble(Bubble bubble) { - if (!shouldShowFlyout(bubble)) { - return; - } - - mFlyoutDragDeltaX = 0f; - clearFlyoutOnHide(); - mAfterFlyoutHidden = () -> { - // Null it out to ensure it runs once. - mAfterFlyoutHidden = null; - - if (mBubbleToExpandAfterFlyoutCollapse != null) { - // User tapped on the flyout and we should expand - mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse); - mBubbleData.setExpanded(true); - mBubbleToExpandAfterFlyoutCollapse = null; - } - - // Stop suppressing the dot now that the flyout has morphed into the dot. - bubble.getIconView().removeDotSuppressionFlag( - BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); - - // Hide the stack after a delay, if needed. - updateTemporarilyInvisibleAnimation(false /* hideImmediately */); - }; - - // Suppress the dot when we are animating the flyout. - bubble.getIconView().addDotSuppressionFlag( - BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); - - // Start flyout expansion. Post in case layout isn't complete and getWidth returns 0. - post(() -> { - // An auto-expanding bubble could have been posted during the time it takes to - // layout. - if (isExpanded() || bubble.getIconView() == null) { - return; - } - final Runnable expandFlyoutAfterDelay = () -> { - mAnimateInFlyout = () -> { - mFlyout.setVisibility(VISIBLE); - updateTemporarilyInvisibleAnimation(false /* hideImmediately */); - mFlyoutDragDeltaX = - mStackAnimationController.isStackOnLeftSide() - ? -mFlyout.getWidth() - : mFlyout.getWidth(); - animateFlyoutCollapsed(false /* collapsed */, 0 /* velX */); - mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); - }; - mFlyout.postDelayed(mAnimateInFlyout, 200); - }; - - - if (mFlyout.getVisibility() == View.VISIBLE) { - mFlyout.animateUpdate(bubble.getFlyoutMessage(), getWidth(), - mStackAnimationController.getStackPosition().y); - } else { - mFlyout.setVisibility(INVISIBLE); - mFlyout.setupFlyoutStartingAsDot(bubble.getFlyoutMessage(), - mStackAnimationController.getStackPosition(), getWidth(), - mStackAnimationController.isStackOnLeftSide(), - bubble.getIconView().getDotColor() /* dotColor */, - expandFlyoutAfterDelay /* onLayoutComplete */, - mAfterFlyoutHidden, - bubble.getIconView().getDotCenter(), - !bubble.showDot()); - } - mFlyout.bringToFront(); - }); - mFlyout.removeCallbacks(mHideFlyout); - mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); - logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT); - } - - /** Hide the flyout immediately and cancel any pending hide runnables. */ - private void hideFlyoutImmediate() { - clearFlyoutOnHide(); - mFlyout.removeCallbacks(mAnimateInFlyout); - mFlyout.removeCallbacks(mHideFlyout); - mFlyout.hideFlyout(); - } - - private void clearFlyoutOnHide() { - mFlyout.removeCallbacks(mAnimateInFlyout); - if (mAfterFlyoutHidden == null) { - return; - } - mAfterFlyoutHidden.run(); - mAfterFlyoutHidden = null; - } - - /** - * Fills the Rect with the touchable region of the bubbles. This will be used by WindowManager - * to decide which touch events go to Bubbles. - * - * Bubbles is below the status bar/notification shade but above application windows. If you're - * trying to get touch events from the status bar or another higher-level window layer, you'll - * need to re-order TYPE_BUBBLES in WindowManagerPolicy so that we have the opportunity to steal - * them. - */ - public void getTouchableRegion(Rect outRect) { - if (mStackEduView != null && mStackEduView.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); - // Increase the touch target size of the bubble - outRect.top -= mBubbleTouchPadding; - outRect.left -= mBubbleTouchPadding; - outRect.right += mBubbleTouchPadding; - outRect.bottom += mBubbleTouchPadding; - } - } else { - mBubbleContainer.getBoundsOnScreen(outRect); - } - - if (mFlyout.getVisibility() == View.VISIBLE) { - final Rect flyoutBounds = new Rect(); - mFlyout.getBoundsOnScreen(flyoutBounds); - outRect.union(flyoutBounds); - } - } - - private int getStatusBarHeight() { - if (getRootWindowInsets() != null) { - WindowInsets insets = getRootWindowInsets(); - return Math.max( - mStatusBarHeight, - insets.getDisplayCutout() != null - ? insets.getDisplayCutout().getSafeInsetTop() - : 0); - } - - return 0; - } - - private void requestUpdate() { - if (mViewUpdatedRequested || mIsExpansionAnimating) { - return; - } - mViewUpdatedRequested = true; - getViewTreeObserver().addOnPreDrawListener(mViewUpdater); - invalidate(); - } - - private void showManageMenu(boolean show) { - mShowingManage = show; - - // This should not happen, since the manage menu is only visible when there's an expanded - // bubble. If we end up in this state, just hide the menu immediately. - if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { - mManageMenu.setVisibility(View.INVISIBLE); - return; - } - - // If available, update the manage menu's settings option with the expanded bubble's app - // name and icon. - if (show && mBubbleData.hasBubbleInStackWithKey(mExpandedBubble.getKey())) { - final Bubble bubble = mBubbleData.getBubbleInStackWithKey(mExpandedBubble.getKey()); - mManageSettingsIcon.setImageDrawable(bubble.getAppBadge()); - mManageSettingsText.setText(getResources().getString( - R.string.bubbles_app_settings, bubble.getAppName())); - } - - mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect); - - final boolean isLtr = - getResources().getConfiguration().getLayoutDirection() == LAYOUT_DIRECTION_LTR; - - // When the menu is open, it should be at these coordinates. The menu pops out to the right - // in LTR and to the left in RTL. - final float targetX = isLtr ? mTempRect.left : mTempRect.right - mManageMenu.getWidth(); - final float targetY = mTempRect.bottom - mManageMenu.getHeight(); - - final float xOffsetForAnimation = (isLtr ? 1 : -1) * mManageMenu.getWidth() / 4f; - if (show) { - mManageMenu.setScaleX(0.5f); - mManageMenu.setScaleY(0.5f); - mManageMenu.setTranslationX(targetX - xOffsetForAnimation); - mManageMenu.setTranslationY(targetY + mManageMenu.getHeight() / 4f); - mManageMenu.setAlpha(0f); - - PhysicsAnimator.getInstance(mManageMenu) - .spring(DynamicAnimation.ALPHA, 1f) - .spring(DynamicAnimation.SCALE_X, 1f) - .spring(DynamicAnimation.SCALE_Y, 1f) - .spring(DynamicAnimation.TRANSLATION_X, targetX) - .spring(DynamicAnimation.TRANSLATION_Y, targetY) - .withEndActions(() -> { - View child = mManageMenu.getChildAt(0); - child.requestAccessibilityFocus(); - // Update the AV's obscured touchable region for the new visibility state. - mExpandedBubble.getExpandedView().updateObscuredTouchableRegion(); - }) - .start(); - - mManageMenu.setVisibility(View.VISIBLE); - } else { - PhysicsAnimator.getInstance(mManageMenu) - .spring(DynamicAnimation.ALPHA, 0f) - .spring(DynamicAnimation.SCALE_X, 0.5f) - .spring(DynamicAnimation.SCALE_Y, 0.5f) - .spring(DynamicAnimation.TRANSLATION_X, targetX - xOffsetForAnimation) - .spring(DynamicAnimation.TRANSLATION_Y, targetY + mManageMenu.getHeight() / 4f) - .withEndActions(() -> { - mManageMenu.setVisibility(View.INVISIBLE); - if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { - // Update the AV's obscured touchable region for the new state. - mExpandedBubble.getExpandedView().updateObscuredTouchableRegion(); - } - }) - .start(); - } - } - - private void updateExpandedBubble() { - if (DEBUG_BUBBLE_STACK_VIEW) { - Log.d(TAG, "updateExpandedBubble()"); - } - - mExpandedViewContainer.removeAllViews(); - if (mIsExpanded && mExpandedBubble != null - && mExpandedBubble.getExpandedView() != null) { - BubbleExpandedView bev = mExpandedBubble.getExpandedView(); - bev.setContentVisibility(false); - mExpandedViewContainerMatrix.setScaleX(0f); - mExpandedViewContainerMatrix.setScaleY(0f); - mExpandedViewContainerMatrix.setTranslate(0f, 0f); - mExpandedViewContainer.setVisibility(View.INVISIBLE); - mExpandedViewContainer.setAlpha(0f); - mExpandedViewContainer.addView(bev); - bev.setManageClickListener((view) -> showManageMenu(!mShowingManage)); - - if (!mIsExpansionAnimating) { - mSurfaceSynchronizer.syncSurfaceAndRun(() -> { - post(this::animateSwitchBubbles); - }); - } - } - } - - /** - * Requests a snapshot from the currently expanded bubble's ActivityView and displays it in a - * SurfaceView. This allows us to load a newly expanded bubble's Activity into the ActivityView, - * while animating the (screenshot of the) previously selected bubble's content away. - * - * @param onComplete Callback to run once we're done here - called with 'false' if something - * went wrong, or 'true' if the SurfaceView is now showing a screenshot of the - * expanded bubble. - */ - private void screenshotAnimatingOutBubbleIntoSurface(Consumer<Boolean> onComplete) { - if (!mIsExpanded || mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { - // You can't animate null. - onComplete.accept(false); - return; - } - - final BubbleExpandedView animatingOutExpandedView = mExpandedBubble.getExpandedView(); - - // Release the previous screenshot if it hasn't been released already. - if (mAnimatingOutBubbleBuffer != null) { - releaseAnimatingOutBubbleBuffer(); - } - - try { - mAnimatingOutBubbleBuffer = animatingOutExpandedView.snapshotActivitySurface(); - } catch (Exception e) { - // If we fail for any reason, print the stack trace and then notify the callback of our - // failure. This is not expected to occur, but it's not worth crashing over. - Log.wtf(TAG, e); - onComplete.accept(false); - } - - if (mAnimatingOutBubbleBuffer == null - || mAnimatingOutBubbleBuffer.getHardwareBuffer() == null) { - // While no exception was thrown, we were unable to get a snapshot. - onComplete.accept(false); - return; - } - - // Make sure the surface container's properties have been reset. - PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel(); - mAnimatingOutSurfaceContainer.setScaleX(1f); - mAnimatingOutSurfaceContainer.setScaleY(1f); - mAnimatingOutSurfaceContainer.setTranslationX(0); - mAnimatingOutSurfaceContainer.setTranslationY(0); - - final int[] activityViewLocation = - mExpandedBubble.getExpandedView().getTaskViewLocationOnScreen(); - final int[] surfaceViewLocation = mAnimatingOutSurfaceView.getLocationOnScreen(); - - // Translate the surface to overlap the real ActivityView. - mAnimatingOutSurfaceContainer.setTranslationY( - activityViewLocation[1] - surfaceViewLocation[1]); - - // Set the width/height of the SurfaceView to match the snapshot. - mAnimatingOutSurfaceView.getLayoutParams().width = - mAnimatingOutBubbleBuffer.getHardwareBuffer().getWidth(); - mAnimatingOutSurfaceView.getLayoutParams().height = - mAnimatingOutBubbleBuffer.getHardwareBuffer().getHeight(); - mAnimatingOutSurfaceView.requestLayout(); - - // Post to wait for layout. - post(() -> { - // The buffer might have been destroyed if the user is mashing on bubbles, that's okay. - if (mAnimatingOutBubbleBuffer == null - || mAnimatingOutBubbleBuffer.getHardwareBuffer() == null - || mAnimatingOutBubbleBuffer.getHardwareBuffer().isClosed()) { - onComplete.accept(false); - return; - } - - if (!mIsExpanded) { - onComplete.accept(false); - return; - } - - // Attach the buffer! We're now displaying the snapshot. - mAnimatingOutSurfaceView.getHolder().getSurface().attachAndQueueBufferWithColorSpace( - mAnimatingOutBubbleBuffer.getHardwareBuffer(), - mAnimatingOutBubbleBuffer.getColorSpace()); - - mSurfaceSynchronizer.syncSurfaceAndRun(() -> post(() -> onComplete.accept(true))); - }); - } - - /** - * Releases the buffer containing the screenshot of the animating-out bubble, if it exists and - * isn't yet destroyed. - */ - private void releaseAnimatingOutBubbleBuffer() { - if (mAnimatingOutBubbleBuffer != null - && !mAnimatingOutBubbleBuffer.getHardwareBuffer().isClosed()) { - mAnimatingOutBubbleBuffer.getHardwareBuffer().close(); - } - } - - private void updateExpandedView() { - if (DEBUG_BUBBLE_STACK_VIEW) { - Log.d(TAG, "updateExpandedView: mIsExpanded=" + mIsExpanded); - } - - mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE); - if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { - mExpandedViewContainer.setTranslationY(getExpandedViewY()); - mExpandedBubble.getExpandedView().updateView( - mExpandedViewContainer.getLocationOnScreen()); - } - - mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); - updateBubbleIcons(); - } - - /** - * Sets the appropriate Z-order, badge, and dot position for each bubble in the stack. - * Animate dot and badge changes. - */ - private void updateBubbleIcons() { - int bubbleCount = getBubbleCount(); - for (int i = 0; i < bubbleCount; i++) { - BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i); - bv.setZ((mMaxBubbles * mBubbleElevation) - i); - - if (mIsExpanded) { - bv.removeDotSuppressionFlag( - BadgedImageView.SuppressionFlag.BEHIND_STACK); - bv.animateDotBadgePositions(false /* onLeft */); - } else if (i == 0) { - bv.removeDotSuppressionFlag( - BadgedImageView.SuppressionFlag.BEHIND_STACK); - bv.animateDotBadgePositions(!mStackOnLeftOrWillBe); - } else { - bv.addDotSuppressionFlag( - BadgedImageView.SuppressionFlag.BEHIND_STACK); - bv.hideBadge(); - } - } - } - - private void updatePointerPosition() { - if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { - return; - } - int index = getBubbleIndex(mExpandedBubble); - if (index == -1) { - return; - } - float bubbleLeftFromScreenLeft = mExpandedAnimationController.getBubbleLeft(index); - float halfBubble = mBubbleSize / 2f; - float bubbleCenter = bubbleLeftFromScreenLeft + halfBubble; - // Padding might be adjusted for insets, so get it directly from the view - bubbleCenter -= mExpandedViewContainer.getPaddingLeft(); - mExpandedBubble.getExpandedView().setPointerPosition(bubbleCenter); - } - - /** - * @return the number of bubbles in the stack view. - */ - public int getBubbleCount() { - // Subtract 1 for the overflow button that is always in the bubble container. - return mBubbleContainer.getChildCount() - 1; - } - - /** - * Finds the bubble index within the stack. - * - * @param provider the bubble view provider with the bubble to look up. - * @return the index of the bubble view within the bubble stack. The range of the position - * is between 0 and the bubble count minus 1. - */ - int getBubbleIndex(@Nullable BubbleViewProvider provider) { - if (provider == null) { - return 0; - } - return mBubbleContainer.indexOfChild(provider.getIconView()); - } - - /** - * @return the normalized x-axis position of the bubble stack rounded to 4 decimal places. - */ - public float getNormalizedXPosition() { - return new BigDecimal(getStackPosition().x / mDisplaySize.x) - .setScale(4, RoundingMode.CEILING.HALF_UP) - .floatValue(); - } - - /** - * @return the normalized y-axis position of the bubble stack rounded to 4 decimal places. - */ - public float getNormalizedYPosition() { - return new BigDecimal(getStackPosition().y / mDisplaySize.y) - .setScale(4, RoundingMode.CEILING.HALF_UP) - .floatValue(); - } - - public void setStackStartPosition(RelativeStackPosition position) { - mStackAnimationController.setStackStartPosition(position); - } - - public PointF getStackPosition() { - return mStackAnimationController.getStackPosition(); - } - - public RelativeStackPosition getRelativeStackPosition() { - return mStackAnimationController.getRelativeStackPosition(); - } - - /** - * Logs the bubble UI event. - * - * @param provider the bubble view provider that is being interacted on. Null value indicates - * that the user interaction is not specific to one bubble. - * @param action the user interaction enum. - */ - private void logBubbleEvent(@Nullable BubbleViewProvider provider, int action) { - mBubbleData.logBubbleEvent(provider, - action, - mContext.getApplicationInfo().packageName, - getBubbleCount(), - getBubbleIndex(provider), - getNormalizedXPosition(), - getNormalizedYPosition()); - } - - /** For debugging only */ - List<Bubble> getBubblesOnScreen() { - List<Bubble> bubbles = new ArrayList<>(); - for (int i = 0; i < getBubbleCount(); i++) { - View child = mBubbleContainer.getChildAt(i); - if (child instanceof BadgedImageView) { - String key = ((BadgedImageView) child).getKey(); - Bubble bubble = mBubbleData.getBubbleInStackWithKey(key); - bubbles.add(bubble); - } - } - return bubbles; - } - - /** - * Representation of stack position that uses relative properties rather than absolute - * coordinates. This is used to maintain similar stack positions across configuration changes. - */ - public static class RelativeStackPosition { - /** Whether to place the stack at the leftmost allowed position. */ - private boolean mOnLeft; - - /** - * How far down the vertically allowed region to place the stack. For example, if the stack - * allowed region is between y = 100 and y = 1100 and this is 0.2f, we'll place the stack at - * 100 + (0.2f * 1000) = 300. - */ - private float mVerticalOffsetPercent; - - public RelativeStackPosition(boolean onLeft, float verticalOffsetPercent) { - mOnLeft = onLeft; - mVerticalOffsetPercent = clampVerticalOffsetPercent(verticalOffsetPercent); - } - - /** Constructs a relative position given a region and a point in that region. */ - public RelativeStackPosition(PointF position, RectF region) { - mOnLeft = position.x < region.width() / 2; - mVerticalOffsetPercent = - clampVerticalOffsetPercent((position.y - region.top) / region.height()); - } - - /** Ensures that the offset percent is between 0f and 1f. */ - private float clampVerticalOffsetPercent(float offsetPercent) { - return Math.max(0f, Math.min(1f, offsetPercent)); - } - - /** - * Given an allowable stack position region, returns the point within that region - * represented by this relative position. - */ - public PointF getAbsolutePositionInRegion(RectF region) { - return new PointF( - mOnLeft ? region.left : region.right, - region.top + mVerticalOffsetPercent * region.height()); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java deleted file mode 100644 index 010a29e3560a..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright (C) 2019 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.bubbles.BadgedImageView.DEFAULT_PATH_SIZE; -import static com.android.systemui.bubbles.BadgedImageView.WHITE_SCRIM_ALPHA; -import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; -import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; - -import android.annotation.NonNull; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.ShortcutInfo; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Matrix; -import android.graphics.Path; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.os.AsyncTask; -import android.util.Log; -import android.util.PathParser; -import android.view.LayoutInflater; - -import androidx.annotation.Nullable; - -import com.android.internal.graphics.ColorUtils; -import com.android.launcher3.icons.BitmapInfo; -import com.android.systemui.R; - -import java.lang.ref.WeakReference; -import java.util.Objects; - -/** - * Simple task to inflate views & load necessary info to display a bubble. - */ -public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask.BubbleViewInfo> { - private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleViewInfoTask" : TAG_BUBBLES; - - - /** - * Callback to find out when the bubble has been inflated & necessary data loaded. - */ - public interface Callback { - /** - * Called when data has been loaded for the bubble. - */ - void onBubbleViewsReady(Bubble bubble); - } - - private Bubble mBubble; - private WeakReference<Context> mContext; - private WeakReference<BubbleStackView> mStackView; - private BubbleIconFactory mIconFactory; - private boolean mSkipInflation; - private Callback mCallback; - - /** - * Creates a task to load information for the provided {@link Bubble}. Once all info - * is loaded, {@link Callback} is notified. - */ - BubbleViewInfoTask(Bubble b, - Context context, - BubbleStackView stackView, - BubbleIconFactory factory, - boolean skipInflation, - Callback c) { - mBubble = b; - mContext = new WeakReference<>(context); - mStackView = new WeakReference<>(stackView); - mIconFactory = factory; - mSkipInflation = skipInflation; - mCallback = c; - } - - @Override - protected BubbleViewInfo doInBackground(Void... voids) { - return BubbleViewInfo.populate(mContext.get(), mStackView.get(), mIconFactory, mBubble, - mSkipInflation); - } - - @Override - protected void onPostExecute(BubbleViewInfo viewInfo) { - if (isCancelled() || viewInfo == null) { - return; - } - mBubble.setViewInfo(viewInfo); - if (mCallback != null) { - mCallback.onBubbleViewsReady(mBubble); - } - } - - /** - * Info necessary to render a bubble. - */ - static class BubbleViewInfo { - BadgedImageView imageView; - BubbleExpandedView expandedView; - ShortcutInfo shortcutInfo; - String appName; - Bitmap bubbleBitmap; - Drawable badgeDrawable; - int dotColor; - Path dotPath; - Bubble.FlyoutMessage flyoutMessage; - - @Nullable - static BubbleViewInfo populate(Context c, BubbleStackView stackView, - BubbleIconFactory iconFactory, Bubble b, boolean skipInflation) { - BubbleViewInfo info = new BubbleViewInfo(); - - // View inflation: only should do this once per bubble - if (!skipInflation && !b.isInflated()) { - LayoutInflater inflater = LayoutInflater.from(c); - info.imageView = (BadgedImageView) inflater.inflate( - R.layout.bubble_view, stackView, false /* attachToRoot */); - - info.expandedView = (BubbleExpandedView) inflater.inflate( - R.layout.bubble_expanded_view, stackView, false /* attachToRoot */); - info.expandedView.setStackView(stackView); - } - - if (b.getShortcutInfo() != null) { - info.shortcutInfo = b.getShortcutInfo(); - } - - // App name & app icon - PackageManager pm = c.getPackageManager(); - ApplicationInfo appInfo; - Drawable badgedIcon; - Drawable appIcon; - try { - appInfo = pm.getApplicationInfo( - b.getPackageName(), - PackageManager.MATCH_UNINSTALLED_PACKAGES - | PackageManager.MATCH_DISABLED_COMPONENTS - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE - | PackageManager.MATCH_DIRECT_BOOT_AWARE); - if (appInfo != null) { - info.appName = String.valueOf(pm.getApplicationLabel(appInfo)); - } - appIcon = pm.getApplicationIcon(b.getPackageName()); - badgedIcon = pm.getUserBadgedIcon(appIcon, b.getUser()); - } catch (PackageManager.NameNotFoundException exception) { - // If we can't find package... don't think we should show the bubble. - Log.w(TAG, "Unable to find package: " + b.getPackageName()); - return null; - } - - // Badged bubble image - Drawable bubbleDrawable = iconFactory.getBubbleDrawable(c, info.shortcutInfo, - b.getIcon()); - if (bubbleDrawable == null) { - // Default to app icon - bubbleDrawable = appIcon; - } - - BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon, - b.isImportantConversation()); - info.badgeDrawable = badgedIcon; - info.bubbleBitmap = iconFactory.createBadgedIconBitmap(bubbleDrawable, - null /* user */, - true /* shrinkNonAdaptiveIcons */).icon; - - // Dot color & placement - Path iconPath = PathParser.createPathFromPathData( - c.getResources().getString(com.android.internal.R.string.config_icon_mask)); - Matrix matrix = new Matrix(); - float scale = iconFactory.getNormalizer().getScale(bubbleDrawable, - null /* outBounds */, null /* path */, null /* outMaskShape */); - float radius = DEFAULT_PATH_SIZE / 2f; - matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, - radius /* pivot y */); - iconPath.transform(matrix); - info.dotPath = iconPath; - info.dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, - Color.WHITE, WHITE_SCRIM_ALPHA); - - // Flyout - info.flyoutMessage = b.getFlyoutMessage(); - if (info.flyoutMessage != null) { - info.flyoutMessage.senderAvatar = - loadSenderAvatar(c, info.flyoutMessage.senderIcon); - } - return info; - } - } - - @Nullable - static Drawable loadSenderAvatar(@NonNull final Context context, @Nullable final Icon icon) { - Objects.requireNonNull(context); - if (icon == null) return null; - if (icon.getType() == Icon.TYPE_URI || icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) { - context.grantUriPermission(context.getPackageName(), - icon.getUri(), Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - return icon.loadDrawable(context); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewProvider.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewProvider.java deleted file mode 100644 index 5cc24ce5a775..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewProvider.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.graphics.Bitmap; -import android.graphics.Path; -import android.graphics.drawable.Drawable; -import android.view.View; - -import androidx.annotation.Nullable; - -/** - * Interface to represent actual Bubbles and UI elements that act like bubbles, like BubbleOverflow. - */ -interface BubbleViewProvider { - @Nullable BubbleExpandedView getExpandedView(); - - void setContentVisibility(boolean visible); - - @Nullable View getIconView(); - - String getKey(); - - /** Bubble icon bitmap with no badge and no dot. */ - Bitmap getBubbleIcon(); - - /** App badge drawable to draw above bubble icon. */ - @Nullable Drawable getAppBadge(); - - /** Path of normalized bubble icon to draw dot on. */ - Path getDotPath(); - - int getDotColor(); - - boolean showDot(); - - int getTaskId(); -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubbles.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubbles.java deleted file mode 100644 index 39c750de28ac..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubbles.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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.annotation.NonNull; - -import androidx.annotation.MainThread; - -import com.android.systemui.statusbar.ScrimView; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.phone.ScrimController; - -import java.util.List; - -/** - * Interface to engage bubbles feature. - */ -public interface Bubbles { - - /** - * @return {@code true} if there is a bubble associated with the provided key and if its - * notification is hidden from the shade or there is a group summary associated with the - * provided key that is hidden from the shade because it has been dismissed but still has child - * bubbles active. - */ - boolean isBubbleNotificationSuppressedFromShade(NotificationEntry entry); - - /** - * @return {@code true} if the current notification entry same as selected bubble - * notification entry and the stack is currently expanded. - */ - boolean isBubbleExpanded(NotificationEntry entry); - - /** @return {@code true} if stack of bubbles is expanded or not. */ - boolean isStackExpanded(); - - /** - * @return the {@link ScrimView} drawn behind the bubble stack. This is managed by - * {@link ScrimController} since we want the scrim's appearance and behavior to be identical to - * that of the notification shade scrim. - */ - ScrimView getScrimForBubble(); - - /** @return Bubbles for updating overflow. */ - List<Bubble> getOverflowBubbles(); - - /** Tell the stack of bubbles to collapse. */ - void collapseStack(); - - /** - * Request the stack expand if needed, then select the specified Bubble as current. - * If no bubble exists for this entry, one is created. - * - * @param entry the notification for the bubble to be selected - */ - void expandStackAndSelectBubble(NotificationEntry entry); - - /** Promote the provided bubbles when overflow view. */ - void promoteBubbleFromOverflow(Bubble bubble); - - /** - * We intercept notification entries (including group summaries) dismissed by the user when - * there is an active bubble associated with it. We do this so that developers can still - * cancel it (and hence the bubbles associated with it). However, these intercepted - * notifications should then be hidden from the shade since the user has cancelled them, so we - * {@link Bubble#setSuppressNotification}. For the case of suppressed summaries, we also add - * {@link BubbleData#addSummaryToSuppress}. - * - * @return true if we want to intercept the dismissal of the entry, else false. - */ - boolean handleDismissalInterception(NotificationEntry entry); - - /** - * Removes the bubble with the given key. - * <p> - * Must be called from the main thread. - */ - @MainThread - void removeBubble(String key, int reason); - - - /** - * When a notification is marked Priority, expand the stack if needed, - * then (maybe create and) select the given bubble. - * - * @param entry the notification for the bubble to show - */ - void onUserChangedImportance(NotificationEntry entry); - - /** - * Called when the status bar has become visible or invisible (either permanently or - * temporarily). - */ - void onStatusBarVisibilityChanged(boolean visible); - - /** - * Called when a user has indicated that an active notification should be shown as a bubble. - * <p> - * This method will collapse the shade, create the bubble without a flyout or dot, and suppress - * the notification from appearing in the shade. - * - * @param entry the notification to change bubble state for. - * @param shouldBubble whether the notification should show as a bubble or not. - */ - void onUserChangedBubble(@NonNull NotificationEntry entry, boolean shouldBubble); - - - /** See {@link BubbleController.NotifCallback}. */ - void addNotifCallback(BubbleController.NotifCallback callback); - - /** Set a listener to be notified of bubble expand events. */ - void setExpandListener(BubbleController.BubbleExpandListener listener); - - /** Set a listener to be notified of when overflow view update. */ - void setOverflowListener(BubbleData.Listener listener); - - /** The task listener for events in bubble tasks. **/ - MultiWindowTaskListener getTaskManager(); -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/DismissView.kt b/packages/SystemUI/src/com/android/systemui/bubbles/DismissView.kt deleted file mode 100644 index b3c552d24dcd..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/DismissView.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.android.systemui.bubbles - -import android.content.Context -import android.graphics.drawable.TransitionDrawable -import android.view.Gravity -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import androidx.dynamicanimation.animation.DynamicAnimation -import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY -import androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW -import com.android.systemui.R -import com.android.wm.shell.common.DismissCircleView -import com.android.wm.shell.animation.PhysicsAnimator - -/* - * View that handles interactions between DismissCircleView and BubbleStackView. - */ -class DismissView(context: Context) : FrameLayout(context) { - - var circle = DismissCircleView(context).apply { - val targetSize: Int = context.resources.getDimensionPixelSize(R.dimen.dismiss_circle_size) - val newParams = LayoutParams(targetSize, targetSize) - newParams.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL - setLayoutParams(newParams) - setTranslationY( - resources.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height).toFloat()) - } - - var isShowing = false - private val animator = PhysicsAnimator.getInstance(circle) - private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY) - private val DISMISS_SCRIM_FADE_MS = 200 - init { - setLayoutParams(LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - resources.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height), - Gravity.BOTTOM)) - setPadding(0, 0, 0, resources.getDimensionPixelSize(R.dimen.floating_dismiss_bottom_margin)) - setClipToPadding(false) - setClipChildren(false) - setVisibility(View.INVISIBLE) - setBackgroundResource( - R.drawable.floating_dismiss_gradient_transition) - addView(circle) - } - - /** - * Animates this view in. - */ - fun show() { - if (isShowing) return - isShowing = true - bringToFront() - setZ(Short.MAX_VALUE - 1f) - setVisibility(View.VISIBLE) - (getBackground() as TransitionDrawable).startTransition(DISMISS_SCRIM_FADE_MS) - animator.cancel() - animator - .spring(DynamicAnimation.TRANSLATION_Y, 0f, spring) - .start() - } - - /** - * Animates this view out, as well as the circle that encircles the bubbles, if they - * were dragged into the target and encircled. - */ - fun hide() { - if (!isShowing) return - isShowing = false - (getBackground() as TransitionDrawable).reverseTransition(DISMISS_SCRIM_FADE_MS) - animator - .spring(DynamicAnimation.TRANSLATION_Y, height.toFloat(), - spring) - .withEndActions({ setVisibility(View.INVISIBLE) }) - .start() - } - - fun updateResources() { - val targetSize: Int = context.resources.getDimensionPixelSize(R.dimen.dismiss_circle_size) - circle.layoutParams.width = targetSize - circle.layoutParams.height = targetSize - circle.requestLayout() - } -}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/ManageEducationView.kt b/packages/SystemUI/src/com/android/systemui/bubbles/ManageEducationView.kt deleted file mode 100644 index 3db07c227d02..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/ManageEducationView.kt +++ /dev/null @@ -1,146 +0,0 @@ -/* - * 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.graphics.Color -import android.graphics.Rect -import android.view.LayoutInflater -import android.view.View -import android.widget.Button -import android.widget.LinearLayout -import android.widget.TextView -import com.android.internal.util.ContrastColorUtil -import com.android.systemui.Interpolators -import com.android.systemui.R - -/** - * User education 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. - */ -class ManageEducationView constructor(context: Context) : LinearLayout(context) { - - private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleManageEducationView" - else BubbleDebugConfig.TAG_BUBBLES - - private val ANIMATE_DURATION: Long = 200 - private val ANIMATE_DURATION_SHORT: Long = 40 - - private val manageView by lazy { findViewById<View>(R.id.manage_education_view) } - private val manageButton by lazy { findViewById<Button>(R.id.manage) } - private val gotItButton by lazy { findViewById<Button>(R.id.got_it) } - private val titleTextView by lazy { findViewById<TextView>(R.id.user_education_title) } - private val descTextView by lazy { findViewById<TextView>(R.id.user_education_description) } - - private var isHiding = false - - init { - LayoutInflater.from(context).inflate(R.layout.bubbles_manage_button_education, this) - visibility = View.GONE - elevation = resources.getDimensionPixelSize(R.dimen.bubble_elevation).toFloat() - - // BubbleStackView forces LTR by default - // since most of Bubble UI direction depends on positioning by the user. - // This view actually lays out differently in RTL, so we set layout LOCALE here. - layoutDirection = View.LAYOUT_DIRECTION_LOCALE - } - - override fun setLayoutDirection(layoutDirection: Int) { - super.setLayoutDirection(layoutDirection) - setDrawableDirection() - } - - override fun onFinishInflate() { - super.onFinishInflate() - layoutDirection = resources.configuration.layoutDirection - setTextColor() - } - - private fun setTextColor() { - val typedArray = mContext.obtainStyledAttributes(intArrayOf(android.R.attr.colorAccent, - android.R.attr.textColorPrimaryInverse)) - val bgColor = typedArray.getColor(0 /* index */, Color.BLACK) - var textColor = typedArray.getColor(1 /* index */, Color.WHITE) - typedArray.recycle() - textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true) - titleTextView.setTextColor(textColor) - descTextView.setTextColor(textColor) - } - - private fun setDrawableDirection() { - manageView.setBackgroundResource( - if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL) - R.drawable.bubble_stack_user_education_bg_rtl - else R.drawable.bubble_stack_user_education_bg) - } - - /** - * 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. - */ - fun show(expandedView: BubbleExpandedView, rect: Rect) { - if (visibility == VISIBLE) return - - alpha = 0f - visibility = View.VISIBLE - post { - expandedView.getManageButtonBoundsOnScreen(rect) - - manageButton - .setOnClickListener { - expandedView.findViewById<View>(R.id.settings_button).performClick() - hide(true /* isStackExpanding */) - } - gotItButton.setOnClickListener { hide(true /* isStackExpanding */) } - setOnClickListener { hide(true /* isStackExpanding */) } - - with(manageView) { - translationX = 0f - val inset = resources.getDimensionPixelSize( - R.dimen.bubbles_manage_education_top_inset) - translationY = (rect.top - manageView.height + inset).toFloat() - } - bringToFront() - animate() - .setDuration(ANIMATE_DURATION) - .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) - .alpha(1f) - } - setShouldShow(false) - } - - fun hide(isStackExpanding: Boolean) { - if (visibility != VISIBLE || isHiding) return - - animate() - .withStartAction { isHiding = true } - .alpha(0f) - .setDuration(if (isStackExpanding) ANIMATE_DURATION_SHORT else ANIMATE_DURATION) - .withEndAction { - isHiding = false - visibility = GONE - } - } - - private fun setShouldShow(shouldShow: Boolean) { - context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE) - .edit().putBoolean(PREF_MANAGED_EDUCATION, !shouldShow).apply() - } -} - -const val PREF_MANAGED_EDUCATION: String = "HasSeenBubblesManageOnboarding"
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/MultiWindowTaskListener.java b/packages/SystemUI/src/com/android/systemui/bubbles/MultiWindowTaskListener.java deleted file mode 100644 index dff8becccb86..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/MultiWindowTaskListener.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * 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 static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_MULTI_WINDOW; - -import android.app.ActivityManager.RunningTaskInfo; -import android.app.ActivityTaskManager; -import android.os.Handler; -import android.os.RemoteException; -import android.util.ArrayMap; -import android.util.Log; -import android.view.SurfaceControl; -import android.window.TaskOrganizer; -import android.window.WindowContainerToken; - -import com.android.wm.shell.ShellTaskOrganizer; - -/** - * Manages tasks that are displayed in multi-window (e.g. bubbles). These are displayed in a - * {@link TaskView}. - * - * This class listens on {@link TaskOrganizer} callbacks for events. Once visible, these tasks will - * intercept back press events. - * - * @see android.app.WindowConfiguration#WINDOWING_MODE_MULTI_WINDOW - * @see TaskView - */ -// TODO: Place in com.android.wm.shell vs. com.android.wm.shell.bubbles on shell migration. -public class MultiWindowTaskListener implements ShellTaskOrganizer.TaskListener { - private static final String TAG = MultiWindowTaskListener.class.getSimpleName(); - - private static final boolean DEBUG = false; - - //TODO(b/170153209): Have shell listener allow per task registration and remove this. - public interface Listener { - void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash); - void onTaskVanished(RunningTaskInfo taskInfo); - void onTaskInfoChanged(RunningTaskInfo taskInfo); - void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo); - } - - private static class TaskData { - final RunningTaskInfo taskInfo; - final Listener listener; - - TaskData(RunningTaskInfo info, Listener l) { - taskInfo = info; - listener = l; - } - } - - private final Handler mHandler; - private final ShellTaskOrganizer mTaskOrganizer; - private final ArrayMap<WindowContainerToken, TaskData> mTasks = new ArrayMap<>(); - - private MultiWindowTaskListener.Listener mPendingListener; - - /** - * Create a listener for tasks in multi-window mode. - */ - public MultiWindowTaskListener(Handler handler, ShellTaskOrganizer organizer) { - mHandler = handler; - mTaskOrganizer = organizer; - mTaskOrganizer.addListener(this, TASK_LISTENER_TYPE_MULTI_WINDOW); - } - - /** - * @return the task organizer that is listened to. - */ - public TaskOrganizer getTaskOrganizer() { - return mTaskOrganizer; - } - - // TODO(b/129067201): track launches for bubbles - // Once we have key in ActivityOptions, match listeners via that key - public void setPendingListener(Listener listener) { - mPendingListener = listener; - } - - /** - * Removes a task listener previously registered when starting a new activity. - */ - public void removeListener(Listener listener) { - if (DEBUG) { - Log.d(TAG, "removeListener: listener=" + listener); - } - if (mPendingListener == listener) { - mPendingListener = null; - } - for (int i = 0; i < mTasks.size(); i++) { - if (mTasks.valueAt(i).listener == listener) { - mTasks.removeAt(i); - } - } - } - - @Override - public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { - if (DEBUG) { - Log.d(TAG, "onTaskAppeared: taskInfo=" + taskInfo - + " mPendingListener=" + mPendingListener); - } - if (mPendingListener == null) { - // If there is no pending listener, then we are either receiving this task as a part of - // registering the task org again (ie. after SysUI dies) or the previously started - // task is no longer needed (ie. bubble is closed soon after), for now, just finish the - // associated task - try { - ActivityTaskManager.getService().removeTask(taskInfo.taskId); - } catch (RemoteException e) { - Log.w(TAG, "Failed to remove taskId " + taskInfo.taskId); - } - return; - } - - mTaskOrganizer.setInterceptBackPressedOnTaskRoot(taskInfo.token, true); - - final TaskData data = new TaskData(taskInfo, mPendingListener); - mTasks.put(taskInfo.token, data); - mHandler.post(() -> data.listener.onTaskAppeared(taskInfo, leash)); - mPendingListener = null; - } - - @Override - public void onTaskVanished(RunningTaskInfo taskInfo) { - final TaskData data = mTasks.remove(taskInfo.token); - if (data == null) { - return; - } - - if (DEBUG) { - Log.d(TAG, "onTaskVanished: taskInfo=" + taskInfo + " listener=" + data.listener); - } - mHandler.post(() -> data.listener.onTaskVanished(taskInfo)); - } - - @Override - public void onTaskInfoChanged(RunningTaskInfo taskInfo) { - final TaskData data = mTasks.get(taskInfo.token); - if (data == null) { - return; - } - - if (DEBUG) { - Log.d(TAG, "onTaskInfoChanged: taskInfo=" + taskInfo + " listener=" + data.listener); - } - mHandler.post(() -> data.listener.onTaskInfoChanged(taskInfo)); - } - - @Override - public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) { - final TaskData data = mTasks.get(taskInfo.token); - if (data == null) { - return; - } - - if (DEBUG) { - Log.d(TAG, "onTaskInfoChanged: taskInfo=" + taskInfo + " listener=" + data.listener); - } - mHandler.post(() -> data.listener.onBackPressedOnTaskRoot(taskInfo)); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/ObjectWrapper.java b/packages/SystemUI/src/com/android/systemui/bubbles/ObjectWrapper.java deleted file mode 100644 index f054122eaa47..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/ObjectWrapper.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.os.Binder; -import android.os.IBinder; - -// Copied from Launcher3 -/** - * Utility class to pass non-parcealable objects within same process using parcealable payload. - * - * It wraps the object in a binder as binders are singleton within a process - */ -public class ObjectWrapper<T> extends Binder { - - private T mObject; - - public ObjectWrapper(T object) { - mObject = object; - } - - public T get() { - return mObject; - } - - public void clear() { - mObject = null; - } - - public static IBinder wrap(Object obj) { - return new ObjectWrapper<>(obj); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/RelativeTouchListener.kt b/packages/SystemUI/src/com/android/systemui/bubbles/RelativeTouchListener.kt deleted file mode 100644 index b1291a507b57..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/RelativeTouchListener.kt +++ /dev/null @@ -1,164 +0,0 @@ -/* - * 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.graphics.PointF -import android.os.Handler -import android.view.MotionEvent -import android.view.VelocityTracker -import android.view.View -import android.view.ViewConfiguration -import kotlin.math.hypot - -/** - * Listener which receives [onDown], [onMove], and [onUp] events, with relevant information about - * the coordinates of the touch and the view relative to the initial ACTION_DOWN event and the - * view's initial position. - */ -abstract class RelativeTouchListener : View.OnTouchListener { - - /** - * Called when an ACTION_DOWN event is received for the given view. - * - * @return False if the object is not interested in MotionEvents at this time, or true if we - * should consume this event and subsequent events, and begin calling [onMove]. - */ - abstract fun onDown(v: View, ev: MotionEvent): Boolean - - /** - * Called when an ACTION_MOVE event is received for the given view. This signals that the view - * is being dragged. - * - * @param viewInitialX The view's translationX value when this touch gesture started. - * @param viewInitialY The view's translationY value when this touch gesture started. - * @param dx Horizontal distance covered since the initial ACTION_DOWN event, in pixels. - * @param dy Vertical distance covered since the initial ACTION_DOWN event, in pixels. - */ - abstract fun onMove( - v: View, - ev: MotionEvent, - viewInitialX: Float, - viewInitialY: Float, - dx: Float, - dy: Float - ) - - /** - * Called when an ACTION_UP event is received for the given view. This signals that a drag or - * fling gesture has completed. - * - * @param viewInitialX The view's translationX value when this touch gesture started. - * @param viewInitialY The view's translationY value when this touch gesture started. - * @param dx Horizontal distance covered, in pixels. - * @param dy Vertical distance covered, in pixels. - * @param velX The final horizontal velocity of the gesture, in pixels/second. - * @param velY The final vertical velocity of the gesture, in pixels/second. - */ - abstract fun onUp( - v: View, - ev: MotionEvent, - viewInitialX: Float, - viewInitialY: Float, - dx: Float, - dy: Float, - velX: Float, - velY: Float - ) - - /** The raw coordinates of the last ACTION_DOWN event. */ - private val touchDown = PointF() - - /** The coordinates of the view, at the time of the last ACTION_DOWN event. */ - private val viewPositionOnTouchDown = PointF() - - private val velocityTracker = VelocityTracker.obtain() - - private var touchSlop: Int = -1 - private var movedEnough = false - - private val handler = Handler() - private var performedLongClick = false - - @Suppress("UNCHECKED_CAST") - override fun onTouch(v: View, ev: MotionEvent): Boolean { - addMovement(ev) - - val dx = ev.rawX - touchDown.x - val dy = ev.rawY - touchDown.y - - when (ev.action) { - MotionEvent.ACTION_DOWN -> { - if (!onDown(v, ev)) { - return false - } - - // Grab the touch slop, it might have changed if the config changed since the - // last gesture. - touchSlop = ViewConfiguration.get(v.context).scaledTouchSlop - - touchDown.set(ev.rawX, ev.rawY) - viewPositionOnTouchDown.set(v.translationX, v.translationY) - - performedLongClick = false - handler.postDelayed({ - if (v.isLongClickable) { - performedLongClick = v.performLongClick() - } - }, ViewConfiguration.getLongPressTimeout().toLong()) - } - - MotionEvent.ACTION_MOVE -> { - if (!movedEnough && hypot(dx, dy) > touchSlop && !performedLongClick) { - movedEnough = true - handler.removeCallbacksAndMessages(null) - } - - if (movedEnough) { - onMove(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy) - } - } - - MotionEvent.ACTION_UP -> { - if (movedEnough) { - velocityTracker.computeCurrentVelocity(1000 /* units */) - onUp(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy, - velocityTracker.xVelocity, velocityTracker.yVelocity) - } else if (!performedLongClick) { - v.performClick() - } else { - handler.removeCallbacksAndMessages(null) - } - - velocityTracker.clear() - movedEnough = false - } - } - - return true - } - - /** - * Adds a movement to the velocity tracker using raw screen coordinates. - */ - private fun addMovement(event: MotionEvent) { - val deltaX = event.rawX - event.x - val deltaY = event.rawY - event.y - event.offsetLocation(deltaX, deltaY) - velocityTracker.addMovement(event) - event.offsetLocation(-deltaX, -deltaY) - } -}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/StackEducationView.kt b/packages/SystemUI/src/com/android/systemui/bubbles/StackEducationView.kt deleted file mode 100644 index 216df2e1f402..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/StackEducationView.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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.graphics.Color -import android.graphics.PointF -import android.view.LayoutInflater -import android.view.View -import android.widget.LinearLayout -import android.widget.TextView -import com.android.internal.util.ContrastColorUtil -import com.android.systemui.Interpolators -import com.android.systemui.R - -/** - * User education view to highlight the collapsed stack of bubbles. - * Shown only the first time a user taps the stack. - */ -class StackEducationView constructor(context: Context) : LinearLayout(context) { - - private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleStackEducationView" - else BubbleDebugConfig.TAG_BUBBLES - - private val ANIMATE_DURATION: Long = 200 - private val ANIMATE_DURATION_SHORT: Long = 40 - - private val view by lazy { findViewById<View>(R.id.stack_education_layout) } - private val titleTextView by lazy { findViewById<TextView>(R.id.stack_education_title) } - private val descTextView by lazy { findViewById<TextView>(R.id.stack_education_description) } - - private var isHiding = false - - init { - LayoutInflater.from(context).inflate(R.layout.bubble_stack_user_education, this) - - visibility = View.GONE - elevation = resources.getDimensionPixelSize(R.dimen.bubble_elevation).toFloat() - - // BubbleStackView forces LTR by default - // since most of Bubble UI direction depends on positioning by the user. - // This view actually lays out differently in RTL, so we set layout LOCALE here. - layoutDirection = View.LAYOUT_DIRECTION_LOCALE - } - - override fun setLayoutDirection(layoutDirection: Int) { - super.setLayoutDirection(layoutDirection) - setDrawableDirection() - } - - override fun onFinishInflate() { - super.onFinishInflate() - layoutDirection = resources.configuration.layoutDirection - setTextColor() - } - - private fun setTextColor() { - val ta = mContext.obtainStyledAttributes(intArrayOf(android.R.attr.colorAccent, - android.R.attr.textColorPrimaryInverse)) - val bgColor = ta.getColor(0 /* index */, Color.BLACK) - var textColor = ta.getColor(1 /* index */, Color.WHITE) - ta.recycle() - textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true) - titleTextView.setTextColor(textColor) - descTextView.setTextColor(textColor) - } - - private fun setDrawableDirection() { - view.setBackgroundResource( - if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR) - R.drawable.bubble_stack_user_education_bg - else R.drawable.bubble_stack_user_education_bg_rtl) - } - - /** - * 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. - */ - fun show(stackPosition: PointF): Boolean { - if (visibility == VISIBLE) return false - - setAlpha(0f) - setVisibility(View.VISIBLE) - post { - with(view) { - val bubbleSize = context.resources.getDimensionPixelSize( - R.dimen.individual_bubble_size) - translationY = stackPosition.y + bubbleSize / 2 - getHeight() / 2 - } - animate() - .setDuration(ANIMATE_DURATION) - .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) - .alpha(1f) - } - setShouldShow(false) - return true - } - - /** - * If necessary, hides the stack education view. - * - * @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. - */ - fun hide(fromExpansion: Boolean) { - if (visibility != VISIBLE || isHiding) return - - animate() - .alpha(0f) - .setDuration(if (fromExpansion) ANIMATE_DURATION_SHORT else ANIMATE_DURATION) - .withEndAction { visibility = GONE } - } - - private fun setShouldShow(shouldShow: Boolean) { - context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE) - .edit().putBoolean(PREF_STACK_EDUCATION, !shouldShow).apply() - } -} - -const val PREF_STACK_EDUCATION: String = "HasSeenBubblesOnboarding"
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/TaskView.java b/packages/SystemUI/src/com/android/systemui/bubbles/TaskView.java deleted file mode 100644 index 524fa42af7d5..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/TaskView.java +++ /dev/null @@ -1,308 +0,0 @@ -/* - * 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 static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.ActivityManager; -import android.app.ActivityOptions; -import android.app.PendingIntent; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.LauncherApps; -import android.content.pm.ShortcutInfo; -import android.graphics.Rect; -import android.view.SurfaceControl; -import android.view.SurfaceHolder; -import android.view.SurfaceView; -import android.window.WindowContainerToken; -import android.window.WindowContainerTransaction; - -import dalvik.system.CloseGuard; - -/** - * View that can display a task. - */ -// TODO: Place in com.android.wm.shell vs. com.android.wm.shell.bubbles on shell migration. -public class TaskView extends SurfaceView implements SurfaceHolder.Callback, - MultiWindowTaskListener.Listener { - - public interface Listener { - /** Called when the container is ready for launching activities. */ - default void onInitialized() {} - - /** Called when the container can no longer launch activities. */ - default void onReleased() {} - - /** Called when a task is created inside the container. */ - default void onTaskCreated(int taskId, ComponentName name) {} - - /** Called when a task visibility changes. */ - default void onTaskVisibilityChanged(int taskId, boolean visible) {} - - /** Called when a task is about to be removed from the stack inside the container. */ - default void onTaskRemovalStarted(int taskId) {} - - /** Called when a task is created inside the container. */ - default void onBackPressedOnTaskRoot(int taskId) {} - } - - private final CloseGuard mGuard = CloseGuard.get(); - - private final MultiWindowTaskListener mMultiWindowTaskListener; - - private ActivityManager.RunningTaskInfo mTaskInfo; - private WindowContainerToken mTaskToken; - private SurfaceControl mTaskLeash; - private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); - private boolean mSurfaceCreated; - private boolean mIsInitialized; - private Listener mListener; - - private final Rect mTmpRect = new Rect(); - private final Rect mTmpRootRect = new Rect(); - - public TaskView(Context context, MultiWindowTaskListener taskListener) { - super(context, null, 0, 0, true /* disableBackgroundLayer */); - - mMultiWindowTaskListener = taskListener; - setUseAlpha(); - getHolder().addCallback(this); - mGuard.open("release"); - } - - /** - * Only one listener may be set on the view, throws an exception otherwise. - */ - public void setListener(Listener listener) { - if (mListener != null) { - throw new IllegalStateException( - "Trying to set a listener when one has already been set"); - } - mListener = listener; - } - - /** - * Launch an activity represented by {@link ShortcutInfo}. - * <p>The owner of this container must be allowed to access the shortcut information, - * as defined in {@link LauncherApps#hasShortcutHostPermission()} to use this method. - * - * @param shortcut the shortcut used to launch the activity. - * @param options options for the activity. - * @param sourceBounds the rect containing the source bounds of the clicked icon to open - * this shortcut. - */ - public void startShortcutActivity(@NonNull ShortcutInfo shortcut, - @NonNull ActivityOptions options, @Nullable Rect sourceBounds) { - mMultiWindowTaskListener.setPendingListener(this); - prepareActivityOptions(options); - LauncherApps service = mContext.getSystemService(LauncherApps.class); - try { - service.startShortcut(shortcut, sourceBounds, options.toBundle()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** - * Launch a new activity. - * - * @param pendingIntent Intent used to launch an activity. - * @param fillInIntent Additional Intent data, see {@link Intent#fillIn Intent.fillIn()} - * @param options options for the activity. - */ - public void startActivity(@NonNull PendingIntent pendingIntent, @Nullable Intent fillInIntent, - @NonNull ActivityOptions options) { - mMultiWindowTaskListener.setPendingListener(this); - prepareActivityOptions(options); - try { - pendingIntent.send(mContext, 0 /* code */, fillInIntent, - null /* onFinished */, null /* handler */, null /* requiredPermission */, - options.toBundle()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private void prepareActivityOptions(ActivityOptions options) { - options.setLaunchWindowingMode(WINDOWING_MODE_MULTI_WINDOW); - options.setTaskAlwaysOnTop(true); - } - - /** - * Call when view position or size has changed. Do not call when animating. - */ - public void onLocationChanged() { - if (mTaskToken == null) { - return; - } - // Update based on the screen bounds - getBoundsOnScreen(mTmpRect); - getRootView().getBoundsOnScreen(mTmpRootRect); - if (!mTmpRootRect.contains(mTmpRect)) { - mTmpRect.offsetTo(0, 0); - } - - WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.setBounds(mTaskToken, mTmpRect); - // TODO(b/151449487): Enable synchronization - mMultiWindowTaskListener.getTaskOrganizer().applyTransaction(wct); - } - - /** - * Release this container if it is initialized. - */ - public void release() { - performRelease(); - } - - @Override - protected void finalize() throws Throwable { - try { - if (mGuard != null) { - mGuard.warnIfOpen(); - performRelease(); - } - } finally { - super.finalize(); - } - } - - private void performRelease() { - getHolder().removeCallback(this); - mMultiWindowTaskListener.removeListener(this); - resetTaskInfo(); - mGuard.close(); - if (mListener != null && mIsInitialized) { - mListener.onReleased(); - mIsInitialized = false; - } - } - - private void resetTaskInfo() { - mTaskInfo = null; - mTaskToken = null; - mTaskLeash = null; - } - - private void updateTaskVisibility() { - WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.setHidden(mTaskToken, !mSurfaceCreated /* hidden */); - mMultiWindowTaskListener.getTaskOrganizer().applyTransaction(wct); - // TODO(b/151449487): Only call callback once we enable synchronization - if (mListener != null) { - mListener.onTaskVisibilityChanged(mTaskInfo.taskId, mSurfaceCreated); - } - } - - @Override - public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, - SurfaceControl leash) { - mTaskInfo = taskInfo; - mTaskToken = taskInfo.token; - mTaskLeash = leash; - - if (mSurfaceCreated) { - // Surface is ready, so just reparent the task to this surface control - mTransaction.reparent(mTaskLeash, getSurfaceControl()) - .show(mTaskLeash) - .apply(); - } else { - // The surface has already been destroyed before the task has appeared, so go ahead and - // hide the task entirely - updateTaskVisibility(); - } - - // TODO: Synchronize show with the resize - onLocationChanged(); - setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor()); - - if (mListener != null) { - mListener.onTaskCreated(taskInfo.taskId, taskInfo.baseActivity); - } - } - - @Override - public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { - if (mTaskToken != null && mTaskToken.equals(taskInfo.token)) { - if (mListener != null) { - mListener.onTaskRemovalStarted(taskInfo.taskId); - } - - // Unparent the task when this surface is destroyed - mTransaction.reparent(mTaskLeash, null).apply(); - resetTaskInfo(); - } - } - - @Override - public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { - mTaskInfo.taskDescription = taskInfo.taskDescription; - setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor()); - } - - @Override - public void onBackPressedOnTaskRoot(ActivityManager.RunningTaskInfo taskInfo) { - if (mTaskToken != null && mTaskToken.equals(taskInfo.token)) { - if (mListener != null) { - mListener.onBackPressedOnTaskRoot(taskInfo.taskId); - } - } - } - - @Override - public void surfaceCreated(SurfaceHolder holder) { - mSurfaceCreated = true; - if (mListener != null && !mIsInitialized) { - mIsInitialized = true; - mListener.onInitialized(); - } - if (mTaskToken == null) { - // Nothing to update, task is not yet available - return; - } - // Reparent the task when this surface is created - mTransaction.reparent(mTaskLeash, getSurfaceControl()) - .show(mTaskLeash) - .apply(); - updateTaskVisibility(); - } - - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - if (mTaskToken == null) { - return; - } - onLocationChanged(); - } - - @Override - public void surfaceDestroyed(SurfaceHolder holder) { - mSurfaceCreated = false; - if (mTaskToken == null) { - // Nothing to update, task is not yet available - return; - } - - // Unparent the task when this surface is destroyed - mTransaction.reparent(mTaskLeash, null).apply(); - updateTaskVisibility(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/AnimatableScaleMatrix.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/AnimatableScaleMatrix.java deleted file mode 100644 index 07acb710c6d7..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/AnimatableScaleMatrix.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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.animation; - -import android.graphics.Matrix; - -import androidx.dynamicanimation.animation.DynamicAnimation; -import androidx.dynamicanimation.animation.FloatPropertyCompat; - -/** - * Matrix whose scale properties can be animated using physics animations, via the {@link #SCALE_X} - * and {@link #SCALE_Y} FloatProperties. - * - * This is useful when you need to perform a scale animation with a pivot point, since pivot points - * are not supported by standard View scale operations but are supported by matrices. - * - * NOTE: DynamicAnimation assumes that all custom properties are denominated in pixels, and thus - * considers 1 to be the smallest user-visible change for custom properties. This means that if you - * animate {@link #SCALE_X} and {@link #SCALE_Y} to 3f, for example, the animation would have only - * three frames. - * - * To work around this, whenever animating to a desired scale value, animate to the value returned - * by {@link #getAnimatableValueForScaleFactor} instead. The SCALE_X and SCALE_Y properties will - * convert that (larger) value into the appropriate scale factor when scaling the matrix. - */ -public class AnimatableScaleMatrix extends Matrix { - - /** - * The X value of the scale. - * - * NOTE: This must be set or animated to the value returned by - * {@link #getAnimatableValueForScaleFactor}, not the desired scale factor itself. - */ - public static final FloatPropertyCompat<AnimatableScaleMatrix> SCALE_X = - new FloatPropertyCompat<AnimatableScaleMatrix>("matrixScaleX") { - @Override - public float getValue(AnimatableScaleMatrix object) { - return getAnimatableValueForScaleFactor(object.mScaleX); - } - - @Override - public void setValue(AnimatableScaleMatrix object, float value) { - object.setScaleX(value * DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE); - } - }; - - /** - * The Y value of the scale. - * - * NOTE: This must be set or animated to the value returned by - * {@link #getAnimatableValueForScaleFactor}, not the desired scale factor itself. - */ - public static final FloatPropertyCompat<AnimatableScaleMatrix> SCALE_Y = - new FloatPropertyCompat<AnimatableScaleMatrix>("matrixScaleY") { - @Override - public float getValue(AnimatableScaleMatrix object) { - return getAnimatableValueForScaleFactor(object.mScaleY); - } - - @Override - public void setValue(AnimatableScaleMatrix object, float value) { - object.setScaleY(value * DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE); - } - }; - - private float mScaleX = 1f; - private float mScaleY = 1f; - - private float mPivotX = 0f; - private float mPivotY = 0f; - - /** - * Return the value to animate SCALE_X or SCALE_Y to in order to achieve the desired scale - * factor. - */ - public static float getAnimatableValueForScaleFactor(float scale) { - return scale * (1f / DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE); - } - - @Override - public void setScale(float sx, float sy, float px, float py) { - mScaleX = sx; - mScaleY = sy; - mPivotX = px; - mPivotY = py; - super.setScale(mScaleX, mScaleY, mPivotX, mPivotY); - } - - public void setScaleX(float scaleX) { - mScaleX = scaleX; - super.setScale(mScaleX, mScaleY, mPivotX, mPivotY); - } - - public void setScaleY(float scaleY) { - mScaleY = scaleY; - super.setScale(mScaleX, mScaleY, mPivotX, mPivotY); - } - - public void setPivotX(float pivotX) { - mPivotX = pivotX; - super.setScale(mScaleX, mScaleY, mPivotX, mPivotY); - } - - public void setPivotY(float pivotY) { - mPivotY = pivotY; - super.setScale(mScaleX, mScaleY, mPivotX, mPivotY); - } - - public float getScaleX() { - return mScaleX; - } - - public float getScaleY() { - return mScaleY; - } - - public float getPivotX() { - return mPivotX; - } - - public float getPivotY() { - return mPivotY; - } - - @Override - public boolean equals(Object obj) { - // Use object equality to allow this matrix to be used as a map key (which is required for - // PhysicsAnimator's animator caching). - return obj == this; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java deleted file mode 100644 index 7fdc01961aa5..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java +++ /dev/null @@ -1,668 +0,0 @@ -/* - * Copyright (C) 2019 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.animation; - -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.Path; -import android.graphics.Point; -import android.graphics.PointF; -import android.view.DisplayCutout; -import android.view.View; -import android.view.WindowInsets; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.dynamicanimation.animation.DynamicAnimation; -import androidx.dynamicanimation.animation.SpringForce; - -import com.android.systemui.Interpolators; -import com.android.systemui.R; -import com.android.wm.shell.animation.PhysicsAnimator; -import com.android.wm.shell.common.magnetictarget.MagnetizedObject; - -import com.google.android.collect.Sets; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.Set; - -/** - * Animation controller for bubbles when they're in their expanded state, or animating to/from the - * expanded state. This controls the expansion animation as well as bubbles 'dragging out' to be - * dismissed. - */ -public class ExpandedAnimationController - extends PhysicsAnimationLayout.PhysicsAnimationController { - - /** - * How much to translate the bubbles when they're animating in/out. This value is multiplied by - * the bubble size. - */ - private static final int ANIMATE_TRANSLATION_FACTOR = 4; - - /** Duration of the expand/collapse target path animation. */ - public static final int EXPAND_COLLAPSE_TARGET_ANIM_DURATION = 175; - - /** Damping ratio for expand/collapse spring. */ - private static final float DAMPING_RATIO_MEDIUM_LOW_BOUNCY = 0.65f; - - /** Stiffness for the expand/collapse path-following animation. */ - private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000; - - /** What percentage of the screen to use when centering the bubbles in landscape. */ - private static final float CENTER_BUBBLES_LANDSCAPE_PERCENT = 0.66f; - - /** - * Velocity required to dismiss an individual bubble without dragging it into the dismiss - * target. - */ - private static final float FLING_TO_DISMISS_MIN_VELOCITY = 6000f; - - private final PhysicsAnimator.SpringConfig mAnimateOutSpringConfig = - new PhysicsAnimator.SpringConfig( - EXPAND_COLLAPSE_ANIM_STIFFNESS, SpringForce.DAMPING_RATIO_NO_BOUNCY); - - /** Horizontal offset between bubbles, which we need to know to re-stack them. */ - private float mStackOffsetPx; - /** Space between status bar and bubbles in the expanded state. */ - private float mBubblePaddingTop; - /** Size of each bubble. */ - private float mBubbleSizePx; - /** Space between bubbles in row above expanded view. */ - private float mSpaceBetweenBubbles; - /** Height of the status bar. */ - private float mStatusBarHeight; - /** Size of display. */ - private Point mDisplaySize; - /** Max number of bubbles shown in row above expanded view. */ - private int mBubblesMaxRendered; - /** What the current screen orientation is. */ - private int mScreenOrientation; - - private boolean mAnimatingExpand = false; - - /** - * Whether we are animating other Bubbles UI elements out in preparation for a call to - * {@link #collapseBackToStack}. If true, we won't animate bubbles in response to adds or - * reorders. - */ - private boolean mPreparingToCollapse = false; - - private boolean mAnimatingCollapse = false; - private @Nullable Runnable mAfterExpand; - private Runnable mAfterCollapse; - private PointF mCollapsePoint; - - /** - * Whether the dragged out bubble is springing towards the touch point, rather than using the - * default behavior of moving directly to the touch point. - * - * This happens when the user's finger exits the dismiss area while the bubble is magnetized to - * the center. Since the touch point differs from the bubble location, we need to animate the - * bubble back to the touch point to avoid a jarring instant location change from the center of - * the target to the touch point just outside the target bounds. - */ - private boolean mSpringingBubbleToTouch = false; - - /** - * Whether to spring the bubble to the next touch event coordinates. This is used to animate the - * bubble out of the magnetic dismiss target to the touch location. - * - * Once it 'catches up' and the animation ends, we'll revert to moving it directly. - */ - private boolean mSpringToTouchOnNextMotionEvent = false; - - /** The bubble currently being dragged out of the row (to potentially be dismissed). */ - private MagnetizedObject<View> mMagnetizedBubbleDraggingOut; - - private int mExpandedViewPadding; - - /** - * Callback to run whenever any bubble is animated out. The BubbleStackView will check if the - * end of this animation means we have no bubbles left, and notify the BubbleController. - */ - private Runnable mOnBubbleAnimatedOutAction; - - public ExpandedAnimationController(Point displaySize, int expandedViewPadding, - int orientation, Runnable onBubbleAnimatedOutAction) { - updateResources(orientation, displaySize); - mExpandedViewPadding = expandedViewPadding; - mOnBubbleAnimatedOutAction = onBubbleAnimatedOutAction; - } - - /** - * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause - * the rest of the bubbles to animate to fill the gap. - */ - private boolean mBubbleDraggedOutEnough = false; - - /** End action to run when the lead bubble's expansion animation completes. */ - @Nullable private Runnable mLeadBubbleEndAction; - - /** - * Animates expanding the bubbles into a row along the top of the screen, optionally running an - * end action when the entire animation completes, and an end action when the lead bubble's - * animation ends. - */ - public void expandFromStack( - @Nullable Runnable after, @Nullable Runnable leadBubbleEndAction) { - mPreparingToCollapse = false; - mAnimatingCollapse = false; - mAnimatingExpand = true; - mAfterExpand = after; - mLeadBubbleEndAction = leadBubbleEndAction; - - startOrUpdatePathAnimation(true /* expanding */); - } - - /** - * Animates expanding the bubbles into a row along the top of the screen. - */ - public void expandFromStack(@Nullable Runnable after) { - expandFromStack(after, null /* leadBubbleEndAction */); - } - - /** - * Sets that we're animating the stack collapsed, but haven't yet called - * {@link #collapseBackToStack}. This will temporarily suspend animations for bubbles that are - * added or re-ordered, since the upcoming collapse animation will handle positioning those - * bubbles in the collapsed stack. - */ - public void notifyPreparingToCollapse() { - mPreparingToCollapse = true; - } - - /** Animate collapsing the bubbles back to their stacked position. */ - public void collapseBackToStack(PointF collapsePoint, Runnable after) { - mAnimatingExpand = false; - mPreparingToCollapse = false; - mAnimatingCollapse = true; - mAfterCollapse = after; - mCollapsePoint = collapsePoint; - - startOrUpdatePathAnimation(false /* expanding */); - } - - /** - * Update effective screen width based on current orientation. - * @param orientation Landscape or portrait. - * @param displaySize Updated display size. - */ - public void updateResources(int orientation, Point displaySize) { - mScreenOrientation = orientation; - mDisplaySize = displaySize; - if (mLayout == null) { - return; - } - Resources res = mLayout.getContext().getResources(); - mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); - mStatusBarHeight = res.getDimensionPixelSize( - com.android.internal.R.dimen.status_bar_height); - mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); - mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); - mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size); - mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered); - - // Includes overflow button. - float totalGapWidth = getWidthForDisplayingBubbles() - (mExpandedViewPadding * 2) - - (mBubblesMaxRendered + 1) * mBubbleSizePx; - mSpaceBetweenBubbles = totalGapWidth / mBubblesMaxRendered; - } - - /** - * Animates the bubbles along a curved path, either to expand them along the top or collapse - * them back into a stack. - */ - private void startOrUpdatePathAnimation(boolean expanding) { - Runnable after; - - if (expanding) { - after = () -> { - mAnimatingExpand = false; - - if (mAfterExpand != null) { - mAfterExpand.run(); - } - - mAfterExpand = null; - - // Update bubble positions in case any bubbles were added or removed during the - // expansion animation. - updateBubblePositions(); - }; - } else { - after = () -> { - mAnimatingCollapse = false; - - if (mAfterCollapse != null) { - mAfterCollapse.run(); - } - - mAfterCollapse = null; - }; - } - - // Animate each bubble individually, since each path will end in a different spot. - animationsForChildrenFromIndex(0, (index, animation) -> { - final View bubble = mLayout.getChildAt(index); - - // Start a path at the bubble's current position. - final Path path = new Path(); - path.moveTo(bubble.getTranslationX(), bubble.getTranslationY()); - - final float expandedY = getExpandedY(); - if (expanding) { - // If we're expanding, first draw a line from the bubble's current position to the - // top of the screen. - path.lineTo(bubble.getTranslationX(), expandedY); - - // Then, draw a line across the screen to the bubble's resting position. - path.lineTo(getBubbleLeft(index), expandedY); - } else { - final float stackedX = mCollapsePoint.x; - - // If we're collapsing, draw a line from the bubble's current position to the side - // of the screen where the bubble will be stacked. - path.lineTo(stackedX, expandedY); - - // Then, draw a line down to the stack position. - path.lineTo(stackedX, mCollapsePoint.y + index * mStackOffsetPx); - } - - // The lead bubble should be the bubble with the longest distance to travel when we're - // expanding, and the bubble with the shortest distance to travel when we're collapsing. - // During expansion from the left side, the last bubble has to travel to the far right - // side, so we have it lead and 'pull' the rest of the bubbles into place. From the - // right side, the first bubble is traveling to the top left, so it leads. During - // collapse to the left, the first bubble has the shortest travel time back to the stack - // position, so it leads (and vice versa). - final boolean firstBubbleLeads = - (expanding && !mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX())) - || (!expanding && mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x)); - final int startDelay = firstBubbleLeads - ? (index * 10) - : ((mLayout.getChildCount() - index) * 10); - - final boolean isLeadBubble = - (firstBubbleLeads && index == 0) - || (!firstBubbleLeads && index == mLayout.getChildCount() - 1); - - animation - .followAnimatedTargetAlongPath( - path, - EXPAND_COLLAPSE_TARGET_ANIM_DURATION /* targetAnimDuration */, - Interpolators.LINEAR /* targetAnimInterpolator */, - isLeadBubble ? mLeadBubbleEndAction : null /* endAction */, - () -> mLeadBubbleEndAction = null /* endAction */) - .withStartDelay(startDelay) - .withStiffness(EXPAND_COLLAPSE_ANIM_STIFFNESS); - }).startAll(after); - } - - /** Notifies the controller that the dragged-out bubble was unstuck from the magnetic target. */ - public void onUnstuckFromTarget() { - mSpringToTouchOnNextMotionEvent = true; - } - - /** - * Prepares the given bubble view to be dragged out, using the provided magnetic target and - * listener. - */ - public void prepareForBubbleDrag( - View bubble, - MagnetizedObject.MagneticTarget target, - MagnetizedObject.MagnetListener listener) { - mLayout.cancelAnimationsOnView(bubble); - - bubble.setTranslationZ(Short.MAX_VALUE); - mMagnetizedBubbleDraggingOut = new MagnetizedObject<View>( - mLayout.getContext(), bubble, - DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) { - @Override - public float getWidth(@NonNull View underlyingObject) { - return mBubbleSizePx; - } - - @Override - public float getHeight(@NonNull View underlyingObject) { - return mBubbleSizePx; - } - - @Override - public void getLocationOnScreen(@NonNull View underlyingObject, @NonNull int[] loc) { - loc[0] = (int) bubble.getTranslationX(); - loc[1] = (int) bubble.getTranslationY(); - } - }; - mMagnetizedBubbleDraggingOut.addTarget(target); - mMagnetizedBubbleDraggingOut.setMagnetListener(listener); - mMagnetizedBubbleDraggingOut.setHapticsEnabled(true); - mMagnetizedBubbleDraggingOut.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY); - } - - private void springBubbleTo(View bubble, float x, float y) { - animationForChild(bubble) - .translationX(x) - .translationY(y) - .withStiffness(SpringForce.STIFFNESS_HIGH) - .start(); - } - - /** - * Drags an individual bubble to the given coordinates. Bubbles to the right will animate to - * take its place once it's dragged out of the row of bubbles, and animate out of the way if the - * bubble is dragged back into the row. - */ - public void dragBubbleOut(View bubbleView, float x, float y) { - if (mSpringToTouchOnNextMotionEvent) { - springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y); - mSpringToTouchOnNextMotionEvent = false; - mSpringingBubbleToTouch = true; - } else if (mSpringingBubbleToTouch) { - if (mLayout.arePropertiesAnimatingOnView( - bubbleView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)) { - springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y); - } else { - mSpringingBubbleToTouch = false; - } - } - - if (!mSpringingBubbleToTouch && !mMagnetizedBubbleDraggingOut.getObjectStuckToTarget()) { - bubbleView.setTranslationX(x); - bubbleView.setTranslationY(y); - } - - final boolean draggedOutEnough = - y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx; - if (draggedOutEnough != mBubbleDraggedOutEnough) { - updateBubblePositions(); - mBubbleDraggedOutEnough = draggedOutEnough; - } - } - - /** Plays a dismiss animation on the dragged out bubble. */ - public void dismissDraggedOutBubble(View bubble, float translationYBy, Runnable after) { - if (bubble == null) { - return; - } - animationForChild(bubble) - .withStiffness(SpringForce.STIFFNESS_HIGH) - .scaleX(0f) - .scaleY(0f) - .translationY(bubble.getTranslationY() + translationYBy) - .alpha(0f, after) - .start(); - - updateBubblePositions(); - } - - @Nullable public View getDraggedOutBubble() { - return mMagnetizedBubbleDraggingOut == null - ? null - : mMagnetizedBubbleDraggingOut.getUnderlyingObject(); - } - - /** Returns the MagnetizedObject instance for the dragging-out bubble. */ - public MagnetizedObject<View> getMagnetizedBubbleDraggingOut() { - return mMagnetizedBubbleDraggingOut; - } - - /** - * Snaps a bubble back to its position within the bubble row, and animates the rest of the - * bubbles to accommodate it if it was previously dragged out past the threshold. - */ - public void snapBubbleBack(View bubbleView, float velX, float velY) { - final int index = mLayout.indexOfChild(bubbleView); - - animationForChildAtIndex(index) - .position(getBubbleLeft(index), getExpandedY()) - .withPositionStartVelocities(velX, velY) - .start(() -> bubbleView.setTranslationZ(0f) /* after */); - - mMagnetizedBubbleDraggingOut = null; - - updateBubblePositions(); - } - - /** Resets bubble drag out gesture flags. */ - public void onGestureFinished() { - mBubbleDraggedOutEnough = false; - mMagnetizedBubbleDraggingOut = null; - updateBubblePositions(); - } - - /** - * Animates the bubbles to {@link #getExpandedY()} position. Used in response to IME showing. - */ - public void updateYPosition(Runnable after) { - if (mLayout == null) return; - animationsForChildrenFromIndex( - 0, (i, anim) -> anim.translationY(getExpandedY())).startAll(after); - } - - /** The Y value of the row of expanded bubbles. */ - public float getExpandedY() { - if (mLayout == null || mLayout.getRootWindowInsets() == null) { - return 0; - } - final WindowInsets insets = mLayout.getRootWindowInsets(); - return mBubblePaddingTop + Math.max( - mStatusBarHeight, - insets.getDisplayCutout() != null - ? insets.getDisplayCutout().getSafeInsetTop() - : 0); - } - - /** Description of current animation controller state. */ - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println("ExpandedAnimationController state:"); - pw.print(" isActive: "); pw.println(isActiveController()); - pw.print(" animatingExpand: "); pw.println(mAnimatingExpand); - pw.print(" animatingCollapse: "); pw.println(mAnimatingCollapse); - pw.print(" springingBubble: "); pw.println(mSpringingBubbleToTouch); - } - - @Override - void onActiveControllerForLayout(PhysicsAnimationLayout layout) { - updateResources(mScreenOrientation, mDisplaySize); - - // Ensure that all child views are at 1x scale, and visible, in case they were animating - // in. - mLayout.setVisibility(View.VISIBLE); - animationsForChildrenFromIndex(0 /* startIndex */, (index, animation) -> - animation.scaleX(1f).scaleY(1f).alpha(1f)).startAll(); - } - - @Override - Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { - return Sets.newHashSet( - DynamicAnimation.TRANSLATION_X, - DynamicAnimation.TRANSLATION_Y, - DynamicAnimation.SCALE_X, - DynamicAnimation.SCALE_Y, - DynamicAnimation.ALPHA); - } - - @Override - int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) { - return NONE; - } - - @Override - float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) { - return 0; - } - - @Override - SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) { - return new SpringForce() - .setDampingRatio(DAMPING_RATIO_MEDIUM_LOW_BOUNCY) - .setStiffness(SpringForce.STIFFNESS_LOW); - } - - @Override - void onChildAdded(View child, int index) { - // If a bubble is added while the expand/collapse animations are playing, update the - // animation to include the new bubble. - if (mAnimatingExpand) { - startOrUpdatePathAnimation(true /* expanding */); - } else if (mAnimatingCollapse) { - startOrUpdatePathAnimation(false /* expanding */); - } else { - child.setTranslationX(getBubbleLeft(index)); - - // If we're preparing to collapse, don't start animations since the collapse animation - // will take over and animate the new bubble into the correct (stacked) position. - if (!mPreparingToCollapse) { - animationForChild(child) - .translationY( - getExpandedY() - - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */ - getExpandedY() /* to */) - .start(); - updateBubblePositions(); - } - } - } - - @Override - void onChildRemoved(View child, int index, Runnable finishRemoval) { - // If we're removing the dragged-out bubble, that means it got dismissed. - if (child.equals(getDraggedOutBubble())) { - mMagnetizedBubbleDraggingOut = null; - finishRemoval.run(); - mOnBubbleAnimatedOutAction.run(); - } else { - PhysicsAnimator.getInstance(child) - .spring(DynamicAnimation.ALPHA, 0f) - .spring(DynamicAnimation.SCALE_X, 0f, mAnimateOutSpringConfig) - .spring(DynamicAnimation.SCALE_Y, 0f, mAnimateOutSpringConfig) - .withEndActions(finishRemoval, mOnBubbleAnimatedOutAction) - .start(); - } - - // Animate all the other bubbles to their new positions sans this bubble. - updateBubblePositions(); - } - - @Override - void onChildReordered(View child, int oldIndex, int newIndex) { - if (mPreparingToCollapse) { - // If a re-order is received while we're preparing to collapse, ignore it. Once started, - // the collapse animation will animate all of the bubbles to their correct (stacked) - // position. - return; - } - - if (mAnimatingCollapse) { - // If a re-order is received during collapse, update the animation so that the bubbles - // end up in the correct (stacked) position. - startOrUpdatePathAnimation(false /* expanding */); - } else { - // Otherwise, animate the bubbles around to reflect their new order. - updateBubblePositions(); - } - } - - private void updateBubblePositions() { - if (mAnimatingExpand || mAnimatingCollapse) { - return; - } - - for (int i = 0; i < mLayout.getChildCount(); i++) { - final View bubble = mLayout.getChildAt(i); - - // Don't animate the dragging out bubble, or it'll jump around while being dragged. It - // will be snapped to the correct X value after the drag (if it's not dismissed). - if (bubble.equals(getDraggedOutBubble())) { - return; - } - - animationForChild(bubble) - .translationX(getBubbleLeft(i)) - .start(); - } - } - - /** - * @param index Bubble index in row. - * @return Bubble left x from left edge of screen. - */ - public float getBubbleLeft(int index) { - final float bubbleFromRowLeft = index * (mBubbleSizePx + mSpaceBetweenBubbles); - return getRowLeft() + bubbleFromRowLeft; - } - - /** - * When expanded, the bubbles are centered in the screen. In portrait, all available space is - * used. In landscape we have too much space so the value is restricted. This method accounts - * for window decorations (nav bar, cutouts). - * - * @return the desired width to display the expanded bubbles in. - */ - public float getWidthForDisplayingBubbles() { - final float availableWidth = getAvailableScreenWidth(true /* includeStableInsets */); - if (mScreenOrientation == Configuration.ORIENTATION_LANDSCAPE) { - // display size y in landscape will be the smaller dimension of the screen - return Math.max(mDisplaySize.y, availableWidth * CENTER_BUBBLES_LANDSCAPE_PERCENT); - } else { - return availableWidth; - } - } - - /** - * Determines the available screen width without the cutout. - * - * @param subtractStableInsets Whether or not stable insets should also be removed from the - * returned width. - * @return the total screen width available accounting for cutouts and insets, - * iff {@param includeStableInsets} is true. - */ - private float getAvailableScreenWidth(boolean subtractStableInsets) { - float availableSize = mDisplaySize.x; - WindowInsets insets = mLayout != null ? mLayout.getRootWindowInsets() : null; - if (insets != null) { - int cutoutLeft = 0; - int cutoutRight = 0; - DisplayCutout cutout = insets.getDisplayCutout(); - if (cutout != null) { - cutoutLeft = cutout.getSafeInsetLeft(); - cutoutRight = cutout.getSafeInsetRight(); - } - final int stableLeft = subtractStableInsets ? insets.getStableInsetLeft() : 0; - final int stableRight = subtractStableInsets ? insets.getStableInsetRight() : 0; - availableSize -= Math.max(stableLeft, cutoutLeft); - availableSize -= Math.max(stableRight, cutoutRight); - } - return availableSize; - } - - private float getRowLeft() { - if (mLayout == null) { - return 0; - } - float rowWidth = (mLayout.getChildCount() * mBubbleSizePx) - + ((mLayout.getChildCount() - 1) * mSpaceBetweenBubbles); - - // This display size we're using includes the size of the insets, we want the true - // center of the display minus the notch here, which means we should include the - // stable insets (e.g. status bar, nav bar) in this calculation. - final float trueCenter = getAvailableScreenWidth(false /* subtractStableInsets */) / 2f; - return trueCenter - (rowWidth / 2f); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/OneTimeEndListener.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/OneTimeEndListener.java deleted file mode 100644 index 4e0abc8009b4..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/OneTimeEndListener.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2019 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.animation; - -import androidx.dynamicanimation.animation.DynamicAnimation; - -/** - * End listener that removes itself from its animation when called for the first time. Useful since - * anonymous OnAnimationEndListener instances can't pass themselves to - * {@link DynamicAnimation#removeEndListener}, but can call through to this superclass - * implementation. - */ -public class OneTimeEndListener implements DynamicAnimation.OnAnimationEndListener { - - @Override - public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value, - float velocity) { - animation.removeEndListener(this); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java deleted file mode 100644 index 6e6f82b714ff..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java +++ /dev/null @@ -1,1147 +0,0 @@ -/* - * Copyright (C) 2019 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.animation; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.animation.TimeInterpolator; -import android.content.Context; -import android.graphics.Path; -import android.graphics.PointF; -import android.util.FloatProperty; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; - -import androidx.annotation.Nullable; -import androidx.dynamicanimation.animation.DynamicAnimation; -import androidx.dynamicanimation.animation.SpringAnimation; -import androidx.dynamicanimation.animation.SpringForce; - -import com.android.systemui.R; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Layout that constructs physics-based animations for each of its children, which behave according - * to settings provided by a {@link PhysicsAnimationController} instance. - * - * See physics-animation-layout.md. - */ -public class PhysicsAnimationLayout extends FrameLayout { - private static final String TAG = "Bubbs.PAL"; - - /** - * Controls the construction, configuration, and use of the physics animations supplied by this - * layout. - */ - abstract static class PhysicsAnimationController { - - /** Configures a given {@link PhysicsPropertyAnimator} for a view at the given index. */ - interface ChildAnimationConfigurator { - - /** - * Called to configure the animator for the view at the given index. - * - * This method should make use of methods such as - * {@link PhysicsPropertyAnimator#translationX} and - * {@link PhysicsPropertyAnimator#withStartDelay} to configure the animation. - * - * Implementations should not call {@link PhysicsPropertyAnimator#start}, this will - * happen elsewhere after configuration is complete. - */ - void configureAnimationForChildAtIndex(int index, PhysicsPropertyAnimator animation); - } - - /** - * Returned by {@link #animationsForChildrenFromIndex} to allow starting multiple animations - * on multiple child views at the same time. - */ - interface MultiAnimationStarter { - - /** - * Start all animations and call the given end actions once all animations have - * completed. - */ - void startAll(Runnable... endActions); - } - - /** - * Constant to return from {@link #getNextAnimationInChain} if the animation should not be - * chained at all. - */ - protected static final int NONE = -1; - - /** Set of properties for which the layout should construct physics animations. */ - abstract Set<DynamicAnimation.ViewProperty> getAnimatedProperties(); - - /** - * Returns the index of the next animation after the given index in the animation chain, or - * {@link #NONE} if it should not be chained, or if the chain should end at the given index. - * - * If a next index is returned, an update listener will be added to the animation at the - * given index that dispatches value updates to the animation at the next index. This - * creates a 'following' effect. - * - * Typical implementations of this method will return either index + 1, or index - 1, to - * create forward or backward chains between adjacent child views, but this is not required. - */ - abstract int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index); - - /** - * Offsets to be added to the value that chained animations of the given property dispatch - * to subsequent child animations. - * - * This is used for things like maintaining the 'stack' effect in Bubbles, where bubbles - * stack off to the left or right side slightly. - */ - abstract float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property); - - /** - * Returns the SpringForce to be used for the given child view's property animation. Despite - * these usually being similar or identical across properties and views, {@link SpringForce} - * also contains the SpringAnimation's final position, so we have to construct a new one for - * each animation rather than using a constant. - */ - abstract SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view); - - /** - * Called when a new child is added at the specified index. Controllers can use this - * opportunity to animate in the new view. - */ - abstract void onChildAdded(View child, int index); - - /** - * Called with a child view that has been removed from the layout, from the given index. The - * passed view has been removed from the layout and added back as a transient view, which - * renders normally, but is not part of the normal view hierarchy and will not be considered - * by getChildAt() and getChildCount(). - * - * The controller can perform animations on the child (either manually, or by using - * {@link #animationForChild(View)}), and then call finishRemoval when complete. - * - * finishRemoval must be called by implementations of this method, or transient views will - * never be removed. - */ - abstract void onChildRemoved(View child, int index, Runnable finishRemoval); - - /** Called when a child view has been reordered in the view hierachy. */ - abstract void onChildReordered(View child, int oldIndex, int newIndex); - - /** - * Called when the controller is set as the active animation controller for the given - * layout. Once active, the controller can start animations using the animator instances - * returned by {@link #animationForChild}. - * - * While all animations started by the previous controller will be cancelled, the new - * controller should not make any assumptions about the state of the layout or its children. - * Their translation, alpha, scale, etc. values may have been changed by the previous - * controller and should be reset here if relevant. - */ - abstract void onActiveControllerForLayout(PhysicsAnimationLayout layout); - - protected PhysicsAnimationLayout mLayout; - - PhysicsAnimationController() { } - - /** Whether this controller is the currently active controller for its associated layout. */ - protected boolean isActiveController() { - return mLayout != null && this == mLayout.mController; - } - - protected void setLayout(PhysicsAnimationLayout layout) { - this.mLayout = layout; - onActiveControllerForLayout(layout); - } - - protected PhysicsAnimationLayout getLayout() { - return mLayout; - } - - /** - * Returns a {@link PhysicsPropertyAnimator} instance for the given child view. - */ - protected PhysicsPropertyAnimator animationForChild(View child) { - PhysicsPropertyAnimator animator = - (PhysicsPropertyAnimator) child.getTag(R.id.physics_animator_tag); - - if (animator == null) { - animator = mLayout.new PhysicsPropertyAnimator(child); - child.setTag(R.id.physics_animator_tag, animator); - } - - animator.clearAnimator(); - animator.setAssociatedController(this); - - return animator; - } - - /** Returns a {@link PhysicsPropertyAnimator} instance for child at the given index. */ - protected PhysicsPropertyAnimator animationForChildAtIndex(int index) { - return animationForChild(mLayout.getChildAt(index)); - } - - /** - * Returns a {@link MultiAnimationStarter} whose startAll method will start the physics - * animations for all children from startIndex onward. The provided configurator will be - * called with each child's {@link PhysicsPropertyAnimator}, where it can set up each - * animation appropriately. - */ - protected MultiAnimationStarter animationsForChildrenFromIndex( - int startIndex, ChildAnimationConfigurator configurator) { - final Set<DynamicAnimation.ViewProperty> allAnimatedProperties = new HashSet<>(); - final List<PhysicsPropertyAnimator> allChildAnims = new ArrayList<>(); - - // Retrieve the animator for each child, ask the configurator to configure it, then save - // it and the properties it chose to animate. - for (int i = startIndex; i < mLayout.getChildCount(); i++) { - final PhysicsPropertyAnimator anim = animationForChildAtIndex(i); - configurator.configureAnimationForChildAtIndex(i, anim); - allAnimatedProperties.addAll(anim.getAnimatedProperties()); - allChildAnims.add(anim); - } - - // Return a MultiAnimationStarter that will start all of the child animations, and also - // add a multiple property end listener to the layout that will call the end action - // provided to startAll() once all animations on the animated properties complete. - return (endActions) -> { - final Runnable runAllEndActions = () -> { - for (Runnable action : endActions) { - action.run(); - } - }; - - // If there aren't any children to animate, just run the end actions. - if (mLayout.getChildCount() == 0) { - runAllEndActions.run(); - return; - } - - if (endActions != null) { - setEndActionForMultipleProperties( - runAllEndActions, - allAnimatedProperties.toArray( - new DynamicAnimation.ViewProperty[0])); - } - - for (PhysicsPropertyAnimator childAnim : allChildAnims) { - childAnim.start(); - } - }; - } - - /** - * Sets an end action that will be run when all child animations for a given property have - * stopped running. - */ - protected void setEndActionForProperty( - Runnable action, DynamicAnimation.ViewProperty property) { - mLayout.mEndActionForProperty.put(property, action); - } - - /** - * Sets an end action that will be run when all child animations for all of the given - * properties have stopped running. - */ - protected void setEndActionForMultipleProperties( - Runnable action, DynamicAnimation.ViewProperty... properties) { - final Runnable checkIfAllFinished = () -> { - if (!mLayout.arePropertiesAnimating(properties)) { - action.run(); - - for (DynamicAnimation.ViewProperty property : properties) { - removeEndActionForProperty(property); - } - } - }; - - for (DynamicAnimation.ViewProperty property : properties) { - setEndActionForProperty(checkIfAllFinished, property); - } - } - - /** - * Removes the end listener that would have been called when all child animations for a - * given property stopped running. - */ - protected void removeEndActionForProperty(DynamicAnimation.ViewProperty property) { - mLayout.mEndActionForProperty.remove(property); - } - } - - /** - * End actions that are called when every child's animation of the given property has finished. - */ - protected final HashMap<DynamicAnimation.ViewProperty, Runnable> mEndActionForProperty = - new HashMap<>(); - - /** The currently active animation controller. */ - @Nullable protected PhysicsAnimationController mController; - - public PhysicsAnimationLayout(Context context) { - super(context); - } - - /** - * Sets the animation controller and constructs or reconfigures the layout's physics animations - * to meet the controller's specifications. - */ - public void setActiveController(PhysicsAnimationController controller) { - cancelAllAnimations(); - mEndActionForProperty.clear(); - - this.mController = controller; - mController.setLayout(this); - - // Set up animations for this controller's animated properties. - for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { - setUpAnimationsForProperty(property); - } - } - - @Override - public void addView(View child, int index, ViewGroup.LayoutParams params) { - addViewInternal(child, index, params, false /* isReorder */); - } - - @Override - public void removeView(View view) { - if (mController != null) { - final int index = indexOfChild(view); - - // Remove the view and add it back as a transient view so we can animate it out. - super.removeView(view); - addTransientView(view, index); - - // Tell the controller to animate this view out, and call the callback when it's - // finished. - mController.onChildRemoved(view, index, () -> { - // The controller says it's done with the transient view, cancel animations in case - // any are still running and then remove it. - cancelAnimationsOnView(view); - removeTransientView(view); - }); - } else { - // Without a controller, nobody will animate this view out, so it gets an unceremonious - // departure. - super.removeView(view); - } - } - - @Override - public void removeViewAt(int index) { - removeView(getChildAt(index)); - } - - /** Immediately re-orders the view to the given index. */ - public void reorderView(View view, int index) { - if (view == null) { - return; - } - final int oldIndex = indexOfChild(view); - - super.removeView(view); - addViewInternal(view, index, view.getLayoutParams(), true /* isReorder */); - - if (mController != null) { - mController.onChildReordered(view, oldIndex, index); - } - } - - /** Checks whether any animations of the given properties are still running. */ - public boolean arePropertiesAnimating(DynamicAnimation.ViewProperty... properties) { - for (int i = 0; i < getChildCount(); i++) { - if (arePropertiesAnimatingOnView(getChildAt(i), properties)) { - return true; - } - } - - return false; - } - - /** Checks whether any animations of the given properties are running on the given view. */ - public boolean arePropertiesAnimatingOnView( - View view, DynamicAnimation.ViewProperty... properties) { - final ObjectAnimator targetAnimator = getTargetAnimatorFromView(view); - for (DynamicAnimation.ViewProperty property : properties) { - final SpringAnimation animation = getAnimationFromView(property, view); - if (animation != null && animation.isRunning()) { - return true; - } - - // If the target animator is running, its update listener will trigger the translation - // physics animations at some point. We should consider the translation properties to be - // be animating in this case, even if the physics animations haven't been started yet. - final boolean isTranslation = - property.equals(DynamicAnimation.TRANSLATION_X) - || property.equals(DynamicAnimation.TRANSLATION_Y); - if (isTranslation && targetAnimator != null && targetAnimator.isRunning()) { - return true; - } - } - - return false; - } - - /** Cancels all animations that are running on all child views, for all properties. */ - public void cancelAllAnimations() { - if (mController == null) { - return; - } - - cancelAllAnimationsOfProperties( - mController.getAnimatedProperties().toArray(new DynamicAnimation.ViewProperty[]{})); - } - - /** Cancels all animations that are running on all child views, for the given properties. */ - public void cancelAllAnimationsOfProperties(DynamicAnimation.ViewProperty... properties) { - if (mController == null) { - return; - } - - for (int i = 0; i < getChildCount(); i++) { - for (DynamicAnimation.ViewProperty property : properties) { - final DynamicAnimation anim = getAnimationAtIndex(property, i); - if (anim != null) { - anim.cancel(); - } - } - } - } - - /** Cancels all of the physics animations running on the given view. */ - public void cancelAnimationsOnView(View view) { - // If present, cancel the target animator so it doesn't restart the translation physics - // animations. - final ObjectAnimator targetAnimator = getTargetAnimatorFromView(view); - if (targetAnimator != null) { - targetAnimator.cancel(); - } - - // Cancel physics animations on the view. - for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { - final DynamicAnimation animationFromView = getAnimationFromView(property, view); - if (animationFromView != null) { - animationFromView.cancel(); - } - } - } - - protected boolean isActiveController(PhysicsAnimationController controller) { - return mController == controller; - } - - /** Whether the first child would be left of center if translated to the given x value. */ - protected boolean isFirstChildXLeftOfCenter(float x) { - if (getChildCount() > 0) { - return x + (getChildAt(0).getWidth() / 2) < getWidth() / 2; - } else { - return false; // If there's no first child, really anything is correct, right? - } - } - - /** ViewProperty's toString is useless, this returns a readable name for debug logging. */ - protected static String getReadablePropertyName(DynamicAnimation.ViewProperty property) { - if (property.equals(DynamicAnimation.TRANSLATION_X)) { - return "TRANSLATION_X"; - } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) { - return "TRANSLATION_Y"; - } else if (property.equals(DynamicAnimation.SCALE_X)) { - return "SCALE_X"; - } else if (property.equals(DynamicAnimation.SCALE_Y)) { - return "SCALE_Y"; - } else if (property.equals(DynamicAnimation.ALPHA)) { - return "ALPHA"; - } else { - return "Unknown animation property."; - } - } - - /** - * Adds a view to the layout. If this addition is not the result of a call to - * {@link #reorderView}, this will also notify the controller via - * {@link PhysicsAnimationController#onChildAdded} and set up animations for the view. - */ - private void addViewInternal( - View child, int index, ViewGroup.LayoutParams params, boolean isReorder) { - super.addView(child, index, params); - - // Set up animations for the new view, if the controller is set. If it isn't set, we'll be - // setting up animations for all children when setActiveController is called. - if (mController != null && !isReorder) { - for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { - setUpAnimationForChild(property, child, index); - } - - mController.onChildAdded(child, index); - } - } - - /** - * Retrieves the animation of the given property from the view at the given index via the view - * tag system. - */ - @Nullable private SpringAnimation getAnimationAtIndex( - DynamicAnimation.ViewProperty property, int index) { - return getAnimationFromView(property, getChildAt(index)); - } - - /** Retrieves the animation of the given property from the view via the view tag system. */ - @Nullable private SpringAnimation getAnimationFromView( - DynamicAnimation.ViewProperty property, View view) { - return (SpringAnimation) view.getTag(getTagIdForProperty(property)); - } - - /** Retrieves the target animator from the view via the view tag system. */ - @Nullable private ObjectAnimator getTargetAnimatorFromView(View view) { - return (ObjectAnimator) view.getTag(R.id.target_animator_tag); - } - - /** Sets up SpringAnimations of the given property for each child view in the layout. */ - private void setUpAnimationsForProperty(DynamicAnimation.ViewProperty property) { - for (int i = 0; i < getChildCount(); i++) { - setUpAnimationForChild(property, getChildAt(i), i); - } - } - - /** Constructs a SpringAnimation of the given property for a child view. */ - private void setUpAnimationForChild( - DynamicAnimation.ViewProperty property, View child, int index) { - SpringAnimation newAnim = new SpringAnimation(child, property); - newAnim.addUpdateListener((animation, value, velocity) -> { - final int indexOfChild = indexOfChild(child); - final int nextAnimInChain = mController.getNextAnimationInChain(property, indexOfChild); - - if (nextAnimInChain == PhysicsAnimationController.NONE || indexOfChild < 0) { - return; - } - - final float offset = mController.getOffsetForChainedPropertyAnimation(property); - if (nextAnimInChain < getChildCount()) { - final SpringAnimation nextAnim = getAnimationAtIndex(property, nextAnimInChain); - if (nextAnim != null) { - nextAnim.animateToFinalPosition(value + offset); - } - } - }); - - newAnim.setSpring(mController.getSpringForce(property, child)); - newAnim.addEndListener(new AllAnimationsForPropertyFinishedEndListener(property)); - child.setTag(getTagIdForProperty(property), newAnim); - } - - /** Return a stable ID to use as a tag key for the given property's animations. */ - private int getTagIdForProperty(DynamicAnimation.ViewProperty property) { - if (property.equals(DynamicAnimation.TRANSLATION_X)) { - return R.id.translation_x_dynamicanimation_tag; - } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) { - return R.id.translation_y_dynamicanimation_tag; - } else if (property.equals(DynamicAnimation.SCALE_X)) { - return R.id.scale_x_dynamicanimation_tag; - } else if (property.equals(DynamicAnimation.SCALE_Y)) { - return R.id.scale_y_dynamicanimation_tag; - } else if (property.equals(DynamicAnimation.ALPHA)) { - return R.id.alpha_dynamicanimation_tag; - } - - return -1; - } - - /** - * End listener that is added to each individual DynamicAnimation, which dispatches to a single - * listener when every other animation of the given property is no longer running. - * - * This is required since chained DynamicAnimations can stop and start again due to changes in - * upstream animations. This means that adding an end listener to just the last animation is not - * sufficient. By firing only when every other animation on the property has stopped running, we - * ensure that no animation will be restarted after the single end listener is called. - */ - protected class AllAnimationsForPropertyFinishedEndListener - implements DynamicAnimation.OnAnimationEndListener { - private DynamicAnimation.ViewProperty mProperty; - - AllAnimationsForPropertyFinishedEndListener(DynamicAnimation.ViewProperty property) { - this.mProperty = property; - } - - @Override - public void onAnimationEnd( - DynamicAnimation anim, boolean canceled, float value, float velocity) { - if (!arePropertiesAnimating(mProperty)) { - if (mEndActionForProperty.containsKey(mProperty)) { - final Runnable callback = mEndActionForProperty.get(mProperty); - - if (callback != null) { - callback.run(); - } - } - } - } - } - - /** - * Animator class returned by {@link PhysicsAnimationController#animationForChild}, to allow - * controllers to animate child views using physics animations. - * - * See docs/physics-animation-layout.md for documentation and examples. - */ - protected class PhysicsPropertyAnimator { - /** The view whose properties this animator animates. */ - private View mView; - - /** Start velocity to use for all property animations. */ - private float mDefaultStartVelocity = -Float.MAX_VALUE; - - /** Start delay to use when start is called. */ - private long mStartDelay = 0; - - /** Damping ratio to use for the animations. */ - private float mDampingRatio = -1; - - /** Stiffness to use for the animations. */ - private float mStiffness = -1; - - /** End actions to call when animations for the given property complete. */ - private Map<DynamicAnimation.ViewProperty, Runnable[]> mEndActionsForProperty = - new HashMap<>(); - - /** - * Start velocities to use for TRANSLATION_X and TRANSLATION_Y, since these are often - * provided by VelocityTrackers and differ from each other. - */ - private Map<DynamicAnimation.ViewProperty, Float> mPositionStartVelocities = - new HashMap<>(); - - /** - * End actions to call when both TRANSLATION_X and TRANSLATION_Y animations have completed, - * if {@link #position} was used to animate TRANSLATION_X and TRANSLATION_Y simultaneously. - */ - @Nullable private Runnable[] mPositionEndActions; - - /** - * All of the properties that have been set and will animate when {@link #start} is called. - */ - private Map<DynamicAnimation.ViewProperty, Float> mAnimatedProperties = new HashMap<>(); - - /** - * All of the initial property values that have been set. These values will be instantly set - * when {@link #start} is called, just before the animation begins. - */ - private Map<DynamicAnimation.ViewProperty, Float> mInitialPropertyValues = new HashMap<>(); - - /** The animation controller that last retrieved this animator instance. */ - private PhysicsAnimationController mAssociatedController; - - /** - * Animator used to traverse the path provided to {@link #followAnimatedTargetAlongPath}. As - * the path is traversed, the view's translation spring animation final positions are - * updated such that the view 'follows' the current position on the path. - */ - @Nullable private ObjectAnimator mPathAnimator; - - /** Current position on the path. This is animated by {@link #mPathAnimator}. */ - private PointF mCurrentPointOnPath = new PointF(); - - /** - * FloatProperty instances that can be passed to {@link ObjectAnimator} to animate the value - * of {@link #mCurrentPointOnPath}. - */ - private final FloatProperty<PhysicsPropertyAnimator> mCurrentPointOnPathXProperty = - new FloatProperty<PhysicsPropertyAnimator>("PathX") { - @Override - public void setValue(PhysicsPropertyAnimator object, float value) { - mCurrentPointOnPath.x = value; - } - - @Override - public Float get(PhysicsPropertyAnimator object) { - return mCurrentPointOnPath.x; - } - }; - - private final FloatProperty<PhysicsPropertyAnimator> mCurrentPointOnPathYProperty = - new FloatProperty<PhysicsPropertyAnimator>("PathY") { - @Override - public void setValue(PhysicsPropertyAnimator object, float value) { - mCurrentPointOnPath.y = value; - } - - @Override - public Float get(PhysicsPropertyAnimator object) { - return mCurrentPointOnPath.y; - } - }; - - protected PhysicsPropertyAnimator(View view) { - this.mView = view; - } - - /** Animate a property to the given value, then call the optional end actions. */ - public PhysicsPropertyAnimator property( - DynamicAnimation.ViewProperty property, float value, Runnable... endActions) { - mAnimatedProperties.put(property, value); - mEndActionsForProperty.put(property, endActions); - return this; - } - - /** Animate the view's alpha value to the provided value. */ - public PhysicsPropertyAnimator alpha(float alpha, Runnable... endActions) { - return property(DynamicAnimation.ALPHA, alpha, endActions); - } - - /** Set the view's alpha value to 'from', then animate it to the given value. */ - public PhysicsPropertyAnimator alpha(float from, float to, Runnable... endActions) { - mInitialPropertyValues.put(DynamicAnimation.ALPHA, from); - return alpha(to, endActions); - } - - /** Animate the view's translationX value to the provided value. */ - public PhysicsPropertyAnimator translationX(float translationX, Runnable... endActions) { - mPathAnimator = null; // We aren't using the path anymore if we're translating. - return property(DynamicAnimation.TRANSLATION_X, translationX, endActions); - } - - /** Set the view's translationX value to 'from', then animate it to the given value. */ - public PhysicsPropertyAnimator translationX( - float from, float to, Runnable... endActions) { - mInitialPropertyValues.put(DynamicAnimation.TRANSLATION_X, from); - return translationX(to, endActions); - } - - /** Animate the view's translationY value to the provided value. */ - public PhysicsPropertyAnimator translationY(float translationY, Runnable... endActions) { - mPathAnimator = null; // We aren't using the path anymore if we're translating. - return property(DynamicAnimation.TRANSLATION_Y, translationY, endActions); - } - - /** Set the view's translationY value to 'from', then animate it to the given value. */ - public PhysicsPropertyAnimator translationY( - float from, float to, Runnable... endActions) { - mInitialPropertyValues.put(DynamicAnimation.TRANSLATION_Y, from); - return translationY(to, endActions); - } - - /** - * Animate the view's translationX and translationY values, and call the end actions only - * once both TRANSLATION_X and TRANSLATION_Y animations have completed. - */ - public PhysicsPropertyAnimator position( - float translationX, float translationY, Runnable... endActions) { - mPositionEndActions = endActions; - translationX(translationX); - return translationY(translationY); - } - - /** - * Animates a 'target' point that moves along the given path, using the provided duration - * and interpolator to animate the target. The view itself is animated using physics-based - * animations, whose final positions are updated to the target position as it animates. This - * results in the view 'following' the target in a realistic way. - * - * This method will override earlier calls to {@link #translationX}, {@link #translationY}, - * or {@link #position}, ultimately animating the view's position to the final point on the - * given path. - * - * @param pathAnimEndActions End actions to run after the animator that moves the target - * along the path ends. The views following the target may still - * be moving. - */ - public PhysicsPropertyAnimator followAnimatedTargetAlongPath( - Path path, - int targetAnimDuration, - TimeInterpolator targetAnimInterpolator, - Runnable... pathAnimEndActions) { - if (mPathAnimator != null) { - mPathAnimator.cancel(); - } - - mPathAnimator = ObjectAnimator.ofFloat( - this, mCurrentPointOnPathXProperty, mCurrentPointOnPathYProperty, path); - - if (pathAnimEndActions != null) { - mPathAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - for (Runnable action : pathAnimEndActions) { - if (action != null) { - action.run(); - } - } - } - }); - } - - mPathAnimator.setDuration(targetAnimDuration); - mPathAnimator.setInterpolator(targetAnimInterpolator); - - // Remove translation related values since we're going to ignore them and follow the - // path instead. - clearTranslationValues(); - return this; - } - - private void clearTranslationValues() { - mAnimatedProperties.remove(DynamicAnimation.TRANSLATION_X); - mAnimatedProperties.remove(DynamicAnimation.TRANSLATION_Y); - mInitialPropertyValues.remove(DynamicAnimation.TRANSLATION_X); - mInitialPropertyValues.remove(DynamicAnimation.TRANSLATION_Y); - mEndActionForProperty.remove(DynamicAnimation.TRANSLATION_X); - mEndActionForProperty.remove(DynamicAnimation.TRANSLATION_Y); - } - - /** Animate the view's scaleX value to the provided value. */ - public PhysicsPropertyAnimator scaleX(float scaleX, Runnable... endActions) { - return property(DynamicAnimation.SCALE_X, scaleX, endActions); - } - - /** Set the view's scaleX value to 'from', then animate it to the given value. */ - public PhysicsPropertyAnimator scaleX(float from, float to, Runnable... endActions) { - mInitialPropertyValues.put(DynamicAnimation.SCALE_X, from); - return scaleX(to, endActions); - } - - /** Animate the view's scaleY value to the provided value. */ - public PhysicsPropertyAnimator scaleY(float scaleY, Runnable... endActions) { - return property(DynamicAnimation.SCALE_Y, scaleY, endActions); - } - - /** Set the view's scaleY value to 'from', then animate it to the given value. */ - public PhysicsPropertyAnimator scaleY(float from, float to, Runnable... endActions) { - mInitialPropertyValues.put(DynamicAnimation.SCALE_Y, from); - return scaleY(to, endActions); - } - - /** Set the start velocity to use for all property animations. */ - public PhysicsPropertyAnimator withStartVelocity(float startVel) { - mDefaultStartVelocity = startVel; - return this; - } - - /** - * Set the damping ratio to use for this animation. If not supplied, will default to the - * value from {@link PhysicsAnimationController#getSpringForce}. - */ - public PhysicsPropertyAnimator withDampingRatio(float dampingRatio) { - mDampingRatio = dampingRatio; - return this; - } - - /** - * Set the stiffness to use for this animation. If not supplied, will default to the - * value from {@link PhysicsAnimationController#getSpringForce}. - */ - public PhysicsPropertyAnimator withStiffness(float stiffness) { - mStiffness = stiffness; - return this; - } - - /** - * Set the start velocities to use for TRANSLATION_X and TRANSLATION_Y animations. This - * overrides any value set via {@link #withStartVelocity(float)} for those properties. - */ - public PhysicsPropertyAnimator withPositionStartVelocities(float velX, float velY) { - mPositionStartVelocities.put(DynamicAnimation.TRANSLATION_X, velX); - mPositionStartVelocities.put(DynamicAnimation.TRANSLATION_Y, velY); - return this; - } - - /** Set a delay, in milliseconds, before kicking off the animations. */ - public PhysicsPropertyAnimator withStartDelay(long startDelay) { - mStartDelay = startDelay; - return this; - } - - /** - * Start the animations, and call the optional end actions once all animations for every - * animated property on every child (including chained animations) have ended. - */ - public void start(Runnable... after) { - if (!isActiveController(mAssociatedController)) { - Log.w(TAG, "Only the active animation controller is allowed to start animations. " - + "Use PhysicsAnimationLayout#setActiveController to set the active " - + "animation controller."); - return; - } - - final Set<DynamicAnimation.ViewProperty> properties = getAnimatedProperties(); - - // If there are end actions, set an end listener on the layout for all the properties - // we're about to animate. - if (after != null && after.length > 0) { - final DynamicAnimation.ViewProperty[] propertiesArray = - properties.toArray(new DynamicAnimation.ViewProperty[0]); - mAssociatedController.setEndActionForMultipleProperties(() -> { - for (Runnable callback : after) { - callback.run(); - } - }, propertiesArray); - } - - // If we used position-specific end actions, we'll need to listen for both TRANSLATION_X - // and TRANSLATION_Y animations ending, and call them once both have finished. - if (mPositionEndActions != null) { - final SpringAnimation translationXAnim = - getAnimationFromView(DynamicAnimation.TRANSLATION_X, mView); - final SpringAnimation translationYAnim = - getAnimationFromView(DynamicAnimation.TRANSLATION_Y, mView); - final Runnable waitForBothXAndY = () -> { - if (!translationXAnim.isRunning() && !translationYAnim.isRunning()) { - if (mPositionEndActions != null) { - for (Runnable callback : mPositionEndActions) { - callback.run(); - } - } - - mPositionEndActions = null; - } - }; - - mEndActionsForProperty.put(DynamicAnimation.TRANSLATION_X, - new Runnable[]{waitForBothXAndY}); - mEndActionsForProperty.put(DynamicAnimation.TRANSLATION_Y, - new Runnable[]{waitForBothXAndY}); - } - - if (mPathAnimator != null) { - startPathAnimation(); - } - - // Actually start the animations. - for (DynamicAnimation.ViewProperty property : properties) { - // Don't start translation animations if we're using a path animator, the update - // listeners added to that animator will take care of that. - if (mPathAnimator != null - && (property.equals(DynamicAnimation.TRANSLATION_X) - || property.equals(DynamicAnimation.TRANSLATION_Y))) { - return; - } - - if (mInitialPropertyValues.containsKey(property)) { - property.setValue(mView, mInitialPropertyValues.get(property)); - } - - final SpringForce defaultSpringForce = mController.getSpringForce(property, mView); - animateValueForChild( - property, - mView, - mAnimatedProperties.get(property), - mPositionStartVelocities.getOrDefault(property, mDefaultStartVelocity), - mStartDelay, - mStiffness >= 0 ? mStiffness : defaultSpringForce.getStiffness(), - mDampingRatio >= 0 ? mDampingRatio : defaultSpringForce.getDampingRatio(), - mEndActionsForProperty.get(property)); - } - - clearAnimator(); - } - - /** Returns the set of properties that will animate once {@link #start} is called. */ - protected Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { - final HashSet<DynamicAnimation.ViewProperty> animatedProperties = new HashSet<>( - mAnimatedProperties.keySet()); - - // If we're using a path animator, it'll kick off translation animations. - if (mPathAnimator != null) { - animatedProperties.add(DynamicAnimation.TRANSLATION_X); - animatedProperties.add(DynamicAnimation.TRANSLATION_Y); - } - - return animatedProperties; - } - - /** - * Animates the property of the given child view, then runs the callback provided when the - * animation ends. - */ - protected void animateValueForChild( - DynamicAnimation.ViewProperty property, - View view, - float value, - float startVel, - long startDelay, - float stiffness, - float dampingRatio, - Runnable... afterCallbacks) { - if (view != null) { - final SpringAnimation animation = - (SpringAnimation) view.getTag(getTagIdForProperty(property)); - - // If the animation is null, the view was probably removed from the layout before - // the animation started. - if (animation == null) { - return; - } - - if (afterCallbacks != null) { - animation.addEndListener(new OneTimeEndListener() { - @Override - public void onAnimationEnd(DynamicAnimation animation, boolean canceled, - float value, float velocity) { - super.onAnimationEnd(animation, canceled, value, velocity); - for (Runnable runnable : afterCallbacks) { - runnable.run(); - } - } - }); - } - - final SpringForce animationSpring = animation.getSpring(); - - if (animationSpring == null) { - return; - } - - final Runnable configureAndStartAnimation = () -> { - animationSpring.setStiffness(stiffness); - animationSpring.setDampingRatio(dampingRatio); - - if (startVel > -Float.MAX_VALUE) { - animation.setStartVelocity(startVel); - } - - animationSpring.setFinalPosition(value); - animation.start(); - }; - - if (startDelay > 0) { - postDelayed(configureAndStartAnimation, startDelay); - } else { - configureAndStartAnimation.run(); - } - } - } - - /** - * Updates the final position of a view's animation, without changing any of the animation's - * other settings. Calling this before an initial call to {@link #animateValueForChild} will - * work, but result in unknown values for stiffness, etc. and is not recommended. - */ - private void updateValueForChild( - DynamicAnimation.ViewProperty property, View view, float position) { - if (view != null) { - final SpringAnimation animation = - (SpringAnimation) view.getTag(getTagIdForProperty(property)); - - if (animation == null) { - return; - } - - final SpringForce animationSpring = animation.getSpring(); - - if (animationSpring == null) { - return; - } - - animationSpring.setFinalPosition(position); - animation.start(); - } - } - - /** - * Configures the path animator to respect the settings passed into the animation builder - * and adds update listeners that update the translation physics animations. Then, starts - * the path animation. - */ - protected void startPathAnimation() { - final SpringForce defaultSpringForceX = mController.getSpringForce( - DynamicAnimation.TRANSLATION_X, mView); - final SpringForce defaultSpringForceY = mController.getSpringForce( - DynamicAnimation.TRANSLATION_Y, mView); - - if (mStartDelay > 0) { - mPathAnimator.setStartDelay(mStartDelay); - } - - final Runnable updatePhysicsAnims = () -> { - updateValueForChild( - DynamicAnimation.TRANSLATION_X, mView, mCurrentPointOnPath.x); - updateValueForChild( - DynamicAnimation.TRANSLATION_Y, mView, mCurrentPointOnPath.y); - }; - - mPathAnimator.addUpdateListener(pathAnim -> updatePhysicsAnims.run()); - mPathAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - animateValueForChild( - DynamicAnimation.TRANSLATION_X, - mView, - mCurrentPointOnPath.x, - mDefaultStartVelocity, - 0 /* startDelay */, - mStiffness >= 0 ? mStiffness : defaultSpringForceX.getStiffness(), - mDampingRatio >= 0 - ? mDampingRatio - : defaultSpringForceX.getDampingRatio()); - - animateValueForChild( - DynamicAnimation.TRANSLATION_Y, - mView, - mCurrentPointOnPath.y, - mDefaultStartVelocity, - 0 /* startDelay */, - mStiffness >= 0 ? mStiffness : defaultSpringForceY.getStiffness(), - mDampingRatio >= 0 - ? mDampingRatio - : defaultSpringForceY.getDampingRatio()); - } - - @Override - public void onAnimationEnd(Animator animation) { - updatePhysicsAnims.run(); - } - }); - - // If there's a target animator saved for the view, make sure it's not running. - final ObjectAnimator targetAnimator = getTargetAnimatorFromView(mView); - if (targetAnimator != null) { - targetAnimator.cancel(); - } - - mView.setTag(R.id.target_animator_tag, mPathAnimator); - mPathAnimator.start(); - } - - private void clearAnimator() { - mInitialPropertyValues.clear(); - mAnimatedProperties.clear(); - mPositionStartVelocities.clear(); - mDefaultStartVelocity = -Float.MAX_VALUE; - mStartDelay = 0; - mStiffness = -1; - mDampingRatio = -1; - mEndActionsForProperty.clear(); - mPathAnimator = null; - mPositionEndActions = null; - } - - /** - * Sets the controller that last retrieved this animator instance, so that we can prevent - * {@link #start} from actually starting animations if called by a non-active controller. - */ - private void setAssociatedController(PhysicsAnimationController controller) { - mAssociatedController = controller; - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java deleted file mode 100644 index 12051241f049..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java +++ /dev/null @@ -1,1106 +0,0 @@ -/* - * Copyright (C) 2019 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.animation; - -import android.content.ContentResolver; -import android.content.res.Resources; -import android.graphics.PointF; -import android.graphics.Rect; -import android.graphics.RectF; -import android.provider.Settings; -import android.util.Log; -import android.view.View; -import android.view.WindowInsets; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.dynamicanimation.animation.DynamicAnimation; -import androidx.dynamicanimation.animation.FlingAnimation; -import androidx.dynamicanimation.animation.FloatPropertyCompat; -import androidx.dynamicanimation.animation.SpringAnimation; -import androidx.dynamicanimation.animation.SpringForce; - -import com.android.systemui.R; -import com.android.systemui.bubbles.BubbleStackView; -import com.android.wm.shell.animation.PhysicsAnimator; -import com.android.wm.shell.common.FloatingContentCoordinator; -import com.android.wm.shell.common.magnetictarget.MagnetizedObject; - -import com.google.android.collect.Sets; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.HashMap; -import java.util.Set; -import java.util.function.IntSupplier; - -/** - * Animation controller for bubbles when they're in their stacked state. Stacked bubbles sit atop - * each other with a slight offset to the left or right (depending on which side of the screen they - * are on). Bubbles 'follow' each other when dragged, and can be flung to the left or right sides of - * the screen. - */ -public class StackAnimationController extends - PhysicsAnimationLayout.PhysicsAnimationController { - - private static final String TAG = "Bubbs.StackCtrl"; - - /** Scale factor to use initially for new bubbles being animated in. */ - private static final float ANIMATE_IN_STARTING_SCALE = 1.15f; - - /** Translation factor (multiplied by stack offset) to use for bubbles being animated in/out. */ - private static final int ANIMATE_TRANSLATION_FACTOR = 4; - - /** Values to use for animating bubbles in. */ - private static final float ANIMATE_IN_STIFFNESS = 1000f; - private static final int ANIMATE_IN_START_DELAY = 25; - - /** - * Values to use for the default {@link SpringForce} provided to the physics animation layout. - */ - public static final int SPRING_TO_TOUCH_STIFFNESS = 12000; - public static final float IME_ANIMATION_STIFFNESS = SpringForce.STIFFNESS_LOW; - private static final int CHAIN_STIFFNESS = 600; - public static final float DEFAULT_BOUNCINESS = 0.9f; - - private final PhysicsAnimator.SpringConfig mAnimateOutSpringConfig = - new PhysicsAnimator.SpringConfig( - ANIMATE_IN_STIFFNESS, SpringForce.DAMPING_RATIO_NO_BOUNCY); - - /** - * Friction applied to fling animations. Since the stack must land on one of the sides of the - * screen, we want less friction horizontally so that the stack has a better chance of making it - * to the side without needing a spring. - */ - private static final float FLING_FRICTION = 2.2f; - - /** - * Values to use for the stack spring animation used to spring the stack to its final position - * after a fling. - */ - private static final int SPRING_AFTER_FLING_STIFFNESS = 750; - private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f; - - /** Sentinel value for unset position value. */ - private static final float UNSET = -Float.MIN_VALUE; - - /** - * Minimum fling velocity required to trigger moving the stack from one side of the screen to - * the other. - */ - private static final float ESCAPE_VELOCITY = 750f; - - /** Velocity required to dismiss the stack without dragging it into the dismiss target. */ - private static final float FLING_TO_DISMISS_MIN_VELOCITY = 4000f; - - /** - * The canonical position of the stack. This is typically the position of the first bubble, but - * we need to keep track of it separately from the first bubble's translation in case there are - * no bubbles, or the first bubble was just added and being animated to its new position. - */ - private PointF mStackPosition = new PointF(-1, -1); - - /** - * MagnetizedObject instance for the stack, which is used by the touch handler for the magnetic - * dismiss target. - */ - private MagnetizedObject<StackAnimationController> mMagnetizedStack; - - /** - * The area that Bubbles will occupy after all animations end. This is used to move other - * floating content out of the way proactively. - */ - private Rect mAnimatingToBounds = new Rect(); - - /** Initial starting location for the stack. */ - @Nullable private BubbleStackView.RelativeStackPosition mStackStartPosition; - - /** Whether or not the stack's start position has been set. */ - private boolean mStackMovedToStartPosition = false; - - /** - * The stack's most recent position along the edge of the screen. This is saved when the last - * bubble is removed, so that the stack can be restored in its previous position. - */ - private PointF mRestingStackPosition; - - /** The height of the most recently visible IME. */ - private float mImeHeight = 0f; - - /** - * The Y position of the stack before the IME became visible, or {@link Float#MIN_VALUE} if the - * IME is not visible or the user moved the stack since the IME became visible. - */ - private float mPreImeY = UNSET; - - /** - * Animations on the stack position itself, which would have been started in - * {@link #flingThenSpringFirstBubbleWithStackFollowing}. These animations dispatch to - * {@link #moveFirstBubbleWithStackFollowing} to move the entire stack (with 'following' effect) - * to a legal position on the side of the screen. - */ - private HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mStackPositionAnimations = - new HashMap<>(); - - /** - * Whether the current motion of the stack is due to a fling animation (vs. being dragged - * manually). - */ - private boolean mIsMovingFromFlinging = false; - - /** - * Whether the first bubble is springing towards the touch point, rather than using the default - * behavior of moving directly to the touch point with the rest of the stack following it. - * - * This happens when the user's finger exits the dismiss area while the stack is magnetized to - * the center. Since the touch point differs from the stack location, we need to animate the - * stack back to the touch point to avoid a jarring instant location change from the center of - * the target to the touch point just outside the target bounds. - * - * This is reset once the spring animations end, since that means the first bubble has - * successfully 'caught up' to the touch. - */ - private boolean mFirstBubbleSpringingToTouch = false; - - /** - * Whether to spring the stack to the next touch event coordinates. This is used to animate the - * stack (including the first bubble) out of the magnetic dismiss target to the touch location. - * Once it 'catches up' and the animation ends, we'll revert to moving the first bubble directly - * and only animating the following bubbles. - */ - private boolean mSpringToTouchOnNextMotionEvent = false; - - /** Horizontal offset of bubbles in the stack. */ - private float mStackOffset; - /** Diameter of the bubble icon. */ - private int mBubbleBitmapSize; - /** Width of the bubble (icon and padding). */ - private int mBubbleSize; - /** - * The amount of space to add between the bubbles and certain UI elements, such as the top of - * the screen or the IME. This does not apply to the left/right sides of the screen since the - * stack goes offscreen intentionally. - */ - private int mBubblePaddingTop; - /** How far offscreen the stack rests. */ - private int mBubbleOffscreen; - /** How far down the screen the stack starts, when there is no pre-existing location. */ - private int mStackStartingVerticalOffset; - /** Height of the status bar. */ - private float mStatusBarHeight; - - /** FloatingContentCoordinator instance for resolving floating content conflicts. */ - private FloatingContentCoordinator mFloatingContentCoordinator; - - /** - * FloatingContent instance that returns the stack's location on the screen, and moves it when - * requested. - */ - private final FloatingContentCoordinator.FloatingContent mStackFloatingContent = - new FloatingContentCoordinator.FloatingContent() { - - private final Rect mFloatingBoundsOnScreen = new Rect(); - - @Override - public void moveToBounds(@NonNull Rect bounds) { - springStack(bounds.left, bounds.top, SpringForce.STIFFNESS_LOW); - } - - @NonNull - @Override - public Rect getAllowedFloatingBoundsRegion() { - final Rect floatingBounds = getFloatingBoundsOnScreen(); - final Rect allowableStackArea = new Rect(); - getAllowableStackPositionRegion().roundOut(allowableStackArea); - allowableStackArea.right += floatingBounds.width(); - allowableStackArea.bottom += floatingBounds.height(); - return allowableStackArea; - } - - @NonNull - @Override - public Rect getFloatingBoundsOnScreen() { - if (!mAnimatingToBounds.isEmpty()) { - return mAnimatingToBounds; - } - - if (mLayout.getChildCount() > 0) { - // Calculate the bounds using stack position + bubble size so that we don't need to - // wait for the bubble views to lay out. - mFloatingBoundsOnScreen.set( - (int) mStackPosition.x, - (int) mStackPosition.y, - (int) mStackPosition.x + mBubbleSize, - (int) mStackPosition.y + mBubbleSize + mBubblePaddingTop); - } else { - mFloatingBoundsOnScreen.setEmpty(); - } - - return mFloatingBoundsOnScreen; - } - }; - - /** Returns the number of 'real' bubbles (excluding the overflow bubble). */ - private IntSupplier mBubbleCountSupplier; - - /** - * Callback to run whenever any bubble is animated out. The BubbleStackView will check if the - * end of this animation means we have no bubbles left, and notify the BubbleController. - */ - private Runnable mOnBubbleAnimatedOutAction; - - public StackAnimationController( - FloatingContentCoordinator floatingContentCoordinator, - IntSupplier bubbleCountSupplier, - Runnable onBubbleAnimatedOutAction) { - mFloatingContentCoordinator = floatingContentCoordinator; - mBubbleCountSupplier = bubbleCountSupplier; - mOnBubbleAnimatedOutAction = onBubbleAnimatedOutAction; - } - - /** - * Instantly move the first bubble to the given point, and animate the rest of the stack behind - * it with the 'following' effect. - */ - public void moveFirstBubbleWithStackFollowing(float x, float y) { - // If we're moving the bubble around, we're not animating to any bounds. - mAnimatingToBounds.setEmpty(); - - // If we manually move the bubbles with the IME open, clear the return point since we don't - // want the stack to snap away from the new position. - mPreImeY = UNSET; - - moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, x); - moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, y); - - // This method is called when the stack is being dragged manually, so we're clearly no - // longer flinging. - mIsMovingFromFlinging = false; - } - - /** - * The position of the stack - typically the position of the first bubble; if no bubbles have - * been added yet, it will be where the first bubble will go when added. - */ - public PointF getStackPosition() { - return mStackPosition; - } - - /** Whether the stack is on the left side of the screen. */ - public boolean isStackOnLeftSide() { - if (mLayout == null || !isStackPositionSet()) { - return true; // Default to left, which is where it starts by default. - } - - float stackCenter = mStackPosition.x + mBubbleBitmapSize / 2; - float screenCenter = mLayout.getWidth() / 2; - return stackCenter < screenCenter; - } - - /** - * Fling stack to given corner, within allowable screen bounds. - * Note that we need new SpringForce instances per animation despite identical configs because - * SpringAnimation uses SpringForce's internal (changing) velocity while the animation runs. - */ - public void springStack( - float destinationX, float destinationY, float stiffness) { - notifyFloatingCoordinatorStackAnimatingTo(destinationX, destinationY); - - springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, - new SpringForce() - .setStiffness(stiffness) - .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO), - 0 /* startXVelocity */, - destinationX); - - springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, - new SpringForce() - .setStiffness(stiffness) - .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO), - 0 /* startYVelocity */, - destinationY); - } - - /** - * Springs the stack to the specified x/y coordinates, with the stiffness used for springs after - * flings. - */ - public void springStackAfterFling(float destinationX, float destinationY) { - springStack(destinationX, destinationY, SPRING_AFTER_FLING_STIFFNESS); - } - - /** - * Flings the stack starting with the given velocities, springing it to the nearest edge - * afterward. - * - * @return The X value that the stack will end up at after the fling/spring. - */ - public float flingStackThenSpringToEdge(float x, float velX, float velY) { - final boolean stackOnLeftSide = x - mBubbleBitmapSize / 2 < mLayout.getWidth() / 2; - - final boolean stackShouldFlingLeft = stackOnLeftSide - ? velX < ESCAPE_VELOCITY - : velX < -ESCAPE_VELOCITY; - - final RectF stackBounds = getAllowableStackPositionRegion(); - - // Target X translation (either the left or right side of the screen). - final float destinationRelativeX = stackShouldFlingLeft - ? stackBounds.left : stackBounds.right; - - // If all bubbles were removed during a drag event, just return the X we would have animated - // to if there were still bubbles. - if (mLayout == null || mLayout.getChildCount() == 0) { - return destinationRelativeX; - } - - final ContentResolver contentResolver = mLayout.getContext().getContentResolver(); - final float stiffness = Settings.Secure.getFloat(contentResolver, "bubble_stiffness", - SPRING_AFTER_FLING_STIFFNESS /* default */); - final float dampingRatio = Settings.Secure.getFloat(contentResolver, "bubble_damping", - SPRING_AFTER_FLING_DAMPING_RATIO); - final float friction = Settings.Secure.getFloat(contentResolver, "bubble_friction", - FLING_FRICTION); - - // Minimum velocity required for the stack to make it to the targeted side of the screen, - // taking friction into account (4.2f is the number that friction scalars are multiplied by - // in DynamicAnimation.DragForce). This is an estimate - it could possibly be slightly off, - // but the SpringAnimation at the end will ensure that it reaches the destination X - // regardless. - final float minimumVelocityToReachEdge = - (destinationRelativeX - x) * (friction * 4.2f); - - final float estimatedY = PhysicsAnimator.estimateFlingEndValue( - mStackPosition.y, velY, - new PhysicsAnimator.FlingConfig( - friction, stackBounds.top, stackBounds.bottom)); - - notifyFloatingCoordinatorStackAnimatingTo(destinationRelativeX, estimatedY); - - // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity so - // that it'll make it all the way to the side of the screen. - final float startXVelocity = stackShouldFlingLeft - ? Math.min(minimumVelocityToReachEdge, velX) - : Math.max(minimumVelocityToReachEdge, velX); - - - - flingThenSpringFirstBubbleWithStackFollowing( - DynamicAnimation.TRANSLATION_X, - startXVelocity, - friction, - new SpringForce() - .setStiffness(stiffness) - .setDampingRatio(dampingRatio), - destinationRelativeX); - - flingThenSpringFirstBubbleWithStackFollowing( - DynamicAnimation.TRANSLATION_Y, - velY, - friction, - new SpringForce() - .setStiffness(stiffness) - .setDampingRatio(dampingRatio), - /* destination */ null); - - // If we're flinging now, there's no more touch event to catch up to. - mFirstBubbleSpringingToTouch = false; - mIsMovingFromFlinging = true; - return destinationRelativeX; - } - - /** - * Where the stack would be if it were snapped to the nearest horizontal edge (left or right). - */ - public PointF getStackPositionAlongNearestHorizontalEdge() { - final PointF stackPos = getStackPosition(); - final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x); - final RectF bounds = getAllowableStackPositionRegion(); - - stackPos.x = onLeft ? bounds.left : bounds.right; - return stackPos; - } - - /** Description of current animation controller state. */ - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println("StackAnimationController state:"); - pw.print(" isActive: "); pw.println(isActiveController()); - pw.print(" restingStackPos: "); - pw.println(mRestingStackPosition != null ? mRestingStackPosition.toString() : "null"); - pw.print(" currentStackPos: "); pw.println(mStackPosition.toString()); - pw.print(" isMovingFromFlinging: "); pw.println(mIsMovingFromFlinging); - pw.print(" withinDismiss: "); pw.println(isStackStuckToTarget()); - pw.print(" firstBubbleSpringing: "); pw.println(mFirstBubbleSpringingToTouch); - } - - /** - * Flings the first bubble along the given property's axis, using the provided configuration - * values. When the animation ends - either by hitting the min/max, or by friction sufficiently - * reducing momentum - a SpringAnimation takes over to snap the bubble to the given final - * position. - */ - protected void flingThenSpringFirstBubbleWithStackFollowing( - DynamicAnimation.ViewProperty property, - float vel, - float friction, - SpringForce spring, - Float finalPosition) { - if (!isActiveController()) { - return; - } - - Log.d(TAG, String.format("Flinging %s.", - PhysicsAnimationLayout.getReadablePropertyName(property))); - - StackPositionProperty firstBubbleProperty = new StackPositionProperty(property); - final float currentValue = firstBubbleProperty.getValue(this); - final RectF bounds = getAllowableStackPositionRegion(); - final float min = - property.equals(DynamicAnimation.TRANSLATION_X) - ? bounds.left - : bounds.top; - final float max = - property.equals(DynamicAnimation.TRANSLATION_X) - ? bounds.right - : bounds.bottom; - - FlingAnimation flingAnimation = new FlingAnimation(this, firstBubbleProperty); - flingAnimation.setFriction(friction) - .setStartVelocity(vel) - - // If the bubble's property value starts beyond the desired min/max, use that value - // instead so that the animation won't immediately end. If, for example, the user - // drags the bubbles into the navigation bar, but then flings them upward, we want - // the fling to occur despite temporarily having a value outside of the min/max. If - // the bubbles are out of bounds and flung even farther out of bounds, the fling - // animation will halt immediately and the SpringAnimation will take over, springing - // it in reverse to the (legal) final position. - .setMinValue(Math.min(currentValue, min)) - .setMaxValue(Math.max(currentValue, max)) - - .addEndListener((animation, canceled, endValue, endVelocity) -> { - if (!canceled) { - mRestingStackPosition.set(mStackPosition); - - springFirstBubbleWithStackFollowing(property, spring, endVelocity, - finalPosition != null - ? finalPosition - : Math.max(min, Math.min(max, endValue))); - } - }); - - cancelStackPositionAnimation(property); - mStackPositionAnimations.put(property, flingAnimation); - flingAnimation.start(); - } - - /** - * Cancel any stack position animations that were started by calling - * @link #flingThenSpringFirstBubbleWithStackFollowing}, and remove any corresponding end - * listeners. - */ - public void cancelStackPositionAnimations() { - cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_X); - cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_Y); - - removeEndActionForProperty(DynamicAnimation.TRANSLATION_X); - removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y); - } - - /** Save the current IME height so that we know where the stack bounds should be. */ - public void setImeHeight(int imeHeight) { - mImeHeight = imeHeight; - } - - /** - * Animates the stack either away from the newly visible IME, or back to its original position - * due to the IME going away. - * - * @return The destination Y value of the stack due to the IME movement (or the current position - * of the stack if it's not moving). - */ - public float animateForImeVisibility(boolean imeVisible) { - final float maxBubbleY = getAllowableStackPositionRegion().bottom; - float destinationY = UNSET; - - if (imeVisible) { - // Stack is lower than it should be and overlaps the now-visible IME. - if (mStackPosition.y > maxBubbleY && mPreImeY == UNSET) { - mPreImeY = mStackPosition.y; - destinationY = maxBubbleY; - } - } else { - if (mPreImeY != UNSET) { - destinationY = mPreImeY; - mPreImeY = UNSET; - } - } - - if (destinationY != UNSET) { - springFirstBubbleWithStackFollowing( - DynamicAnimation.TRANSLATION_Y, - getSpringForce(DynamicAnimation.TRANSLATION_Y, /* view */ null) - .setStiffness(IME_ANIMATION_STIFFNESS), - /* startVel */ 0f, - destinationY); - - notifyFloatingCoordinatorStackAnimatingTo(mStackPosition.x, destinationY); - } - - return destinationY != UNSET ? destinationY : mStackPosition.y; - } - - /** - * Notifies the floating coordinator that we're moving, and sets {@link #mAnimatingToBounds} so - * we return these bounds from - * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}. - */ - private void notifyFloatingCoordinatorStackAnimatingTo(float x, float y) { - final Rect floatingBounds = mStackFloatingContent.getFloatingBoundsOnScreen(); - floatingBounds.offsetTo((int) x, (int) y); - mAnimatingToBounds = floatingBounds; - mFloatingContentCoordinator.onContentMoved(mStackFloatingContent); - } - - /** - * Returns the region that the stack position must stay within. This goes slightly off the left - * and right sides of the screen, below the status bar/cutout and above the navigation bar. - * While the stack position is not allowed to rest outside of these bounds, it can temporarily - * be animated or dragged beyond them. - */ - public RectF getAllowableStackPositionRegion() { - final WindowInsets insets = mLayout.getRootWindowInsets(); - final RectF allowableRegion = new RectF(); - if (insets != null) { - allowableRegion.left = - -mBubbleOffscreen - + Math.max( - insets.getSystemWindowInsetLeft(), - insets.getDisplayCutout() != null - ? insets.getDisplayCutout().getSafeInsetLeft() - : 0); - allowableRegion.right = - mLayout.getWidth() - - mBubbleSize - + mBubbleOffscreen - - Math.max( - insets.getSystemWindowInsetRight(), - insets.getDisplayCutout() != null - ? insets.getDisplayCutout().getSafeInsetRight() - : 0); - - allowableRegion.top = - mBubblePaddingTop - + Math.max( - mStatusBarHeight, - insets.getDisplayCutout() != null - ? insets.getDisplayCutout().getSafeInsetTop() - : 0); - allowableRegion.bottom = - mLayout.getHeight() - - mBubbleSize - - mBubblePaddingTop - - (mImeHeight != UNSET ? mImeHeight + mBubblePaddingTop : 0f) - - Math.max( - insets.getStableInsetBottom(), - insets.getDisplayCutout() != null - ? insets.getDisplayCutout().getSafeInsetBottom() - : 0); - } - - return allowableRegion; - } - - /** Moves the stack in response to a touch event. */ - public void moveStackFromTouch(float x, float y) { - // Begin the spring-to-touch catch up animation if needed. - if (mSpringToTouchOnNextMotionEvent) { - springStack(x, y, SPRING_TO_TOUCH_STIFFNESS); - mSpringToTouchOnNextMotionEvent = false; - mFirstBubbleSpringingToTouch = true; - } else if (mFirstBubbleSpringingToTouch) { - final SpringAnimation springToTouchX = - (SpringAnimation) mStackPositionAnimations.get( - DynamicAnimation.TRANSLATION_X); - final SpringAnimation springToTouchY = - (SpringAnimation) mStackPositionAnimations.get( - DynamicAnimation.TRANSLATION_Y); - - // If either animation is still running, we haven't caught up. Update the animations. - if (springToTouchX.isRunning() || springToTouchY.isRunning()) { - springToTouchX.animateToFinalPosition(x); - springToTouchY.animateToFinalPosition(y); - } else { - // If the animations have finished, the stack is now at the touch point. We can - // resume moving the bubble directly. - mFirstBubbleSpringingToTouch = false; - } - } - - if (!mFirstBubbleSpringingToTouch && !isStackStuckToTarget()) { - moveFirstBubbleWithStackFollowing(x, y); - } - } - - /** Notify the controller that the stack has been unstuck from the dismiss target. */ - public void onUnstuckFromTarget() { - mSpringToTouchOnNextMotionEvent = true; - } - - /** - * 'Implode' the stack by shrinking the bubbles, fading them out, and translating them down. - */ - public void animateStackDismissal(float translationYBy, Runnable after) { - animationsForChildrenFromIndex(0, (index, animation) -> - animation - .scaleX(0f) - .scaleY(0f) - .alpha(0f) - .translationY( - mLayout.getChildAt(index).getTranslationY() + translationYBy) - .withStiffness(SpringForce.STIFFNESS_HIGH)) - .startAll(after); - } - - /** - * Springs the first bubble to the given final position, with the rest of the stack 'following'. - */ - protected void springFirstBubbleWithStackFollowing( - DynamicAnimation.ViewProperty property, SpringForce spring, - float vel, float finalPosition, @Nullable Runnable... after) { - - if (mLayout.getChildCount() == 0 || !isActiveController()) { - return; - } - - Log.d(TAG, String.format("Springing %s to final position %f.", - PhysicsAnimationLayout.getReadablePropertyName(property), - finalPosition)); - - // Whether we're springing towards the touch location, rather than to a position on the - // sides of the screen. - final boolean isSpringingTowardsTouch = mSpringToTouchOnNextMotionEvent; - - StackPositionProperty firstBubbleProperty = new StackPositionProperty(property); - SpringAnimation springAnimation = - new SpringAnimation(this, firstBubbleProperty) - .setSpring(spring) - .addEndListener((dynamicAnimation, b, v, v1) -> { - if (!isSpringingTowardsTouch) { - // If we're springing towards the touch position, don't save the - // resting position - the touch location is not a valid resting - // position. We'll set this when the stack springs to the left or - // right side of the screen after the touch gesture ends. - mRestingStackPosition.set(mStackPosition); - } - - if (after != null) { - for (Runnable callback : after) { - callback.run(); - } - } - }) - .setStartVelocity(vel); - - cancelStackPositionAnimation(property); - mStackPositionAnimations.put(property, springAnimation); - springAnimation.animateToFinalPosition(finalPosition); - } - - @Override - Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { - return Sets.newHashSet( - DynamicAnimation.TRANSLATION_X, // For positioning. - DynamicAnimation.TRANSLATION_Y, - DynamicAnimation.ALPHA, // For fading in new bubbles. - DynamicAnimation.SCALE_X, // For 'popping in' new bubbles. - DynamicAnimation.SCALE_Y); - } - - @Override - int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) { - if (property.equals(DynamicAnimation.TRANSLATION_X) - || property.equals(DynamicAnimation.TRANSLATION_Y)) { - return index + 1; - } else { - return NONE; - } - } - - - @Override - float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) { - if (property.equals(DynamicAnimation.TRANSLATION_Y)) { - // If we're in the dismiss target, have the bubbles pile on top of each other with no - // offset. - if (isStackStuckToTarget()) { - return 0f; - } else { - return mStackOffset; - } - } else { - return 0f; - } - } - - @Override - SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) { - final ContentResolver contentResolver = mLayout.getContext().getContentResolver(); - final float dampingRatio = Settings.Secure.getFloat(contentResolver, "bubble_damping", - DEFAULT_BOUNCINESS); - - return new SpringForce() - .setDampingRatio(dampingRatio) - .setStiffness(CHAIN_STIFFNESS); - } - - @Override - void onChildAdded(View child, int index) { - // Don't animate additions within the dismiss target. - if (isStackStuckToTarget()) { - return; - } - - if (getBubbleCount() == 1) { - // If this is the first child added, position the stack in its starting position. - moveStackToStartPosition(); - } else if (isStackPositionSet() && mLayout.indexOfChild(child) == 0) { - // Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble - // to the back of the stack, it'll be largely invisible so don't bother animating it in. - animateInBubble(child, index); - } - } - - @Override - void onChildRemoved(View child, int index, Runnable finishRemoval) { - PhysicsAnimator.getInstance(child) - .spring(DynamicAnimation.ALPHA, 0f) - .spring(DynamicAnimation.SCALE_X, 0f, mAnimateOutSpringConfig) - .spring(DynamicAnimation.SCALE_Y, 0f, mAnimateOutSpringConfig) - .withEndActions(finishRemoval, mOnBubbleAnimatedOutAction) - .start(); - - // If there are other bubbles, pull them into the correct position. - if (getBubbleCount() > 0) { - animationForChildAtIndex(0).translationX(mStackPosition.x).start(); - } else { - // When all children are removed ensure stack position is sane - setStackPosition(mRestingStackPosition == null - ? getStartPosition() - : mRestingStackPosition); - - // Remove the stack from the coordinator since we don't have any bubbles and aren't - // visible. - mFloatingContentCoordinator.onContentRemoved(mStackFloatingContent); - } - } - - @Override - void onChildReordered(View child, int oldIndex, int newIndex) { - if (isStackPositionSet()) { - setStackPosition(mStackPosition); - } - } - - @Override - void onActiveControllerForLayout(PhysicsAnimationLayout layout) { - Resources res = layout.getResources(); - mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); - mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size); - mBubbleBitmapSize = res.getDimensionPixelSize(R.dimen.bubble_bitmap_size); - mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); - mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen); - mStackStartingVerticalOffset = - res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y); - mStatusBarHeight = - res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); - } - - /** - * Update effective screen width based on current orientation. - * @param orientation Landscape or portrait. - */ - public void updateResources(int orientation) { - if (mLayout != null) { - Resources res = mLayout.getContext().getResources(); - mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); - mStatusBarHeight = res.getDimensionPixelSize( - com.android.internal.R.dimen.status_bar_height); - } - } - - private boolean isStackStuckToTarget() { - return mMagnetizedStack != null && mMagnetizedStack.getObjectStuckToTarget(); - } - - /** Moves the stack, without any animation, to the starting position. */ - private void moveStackToStartPosition() { - // Post to ensure that the layout's width and height have been calculated. - mLayout.setVisibility(View.INVISIBLE); - mLayout.post(() -> { - setStackPosition(mRestingStackPosition == null - ? getStartPosition() - : mRestingStackPosition); - mStackMovedToStartPosition = true; - mLayout.setVisibility(View.VISIBLE); - - // Animate in the top bubble now that we're visible. - if (mLayout.getChildCount() > 0) { - // Add the stack to the floating content coordinator now that we have a bubble and - // are visible. - mFloatingContentCoordinator.onContentAdded(mStackFloatingContent); - - animateInBubble(mLayout.getChildAt(0), 0 /* index */); - } - }); - } - - /** - * Moves the first bubble instantly to the given X or Y translation, and instructs subsequent - * bubbles to animate 'following' to the new location. - */ - private void moveFirstBubbleWithStackFollowing( - DynamicAnimation.ViewProperty property, float value) { - - // Update the canonical stack position. - if (property.equals(DynamicAnimation.TRANSLATION_X)) { - mStackPosition.x = value; - } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) { - mStackPosition.y = value; - } - - if (mLayout.getChildCount() > 0) { - property.setValue(mLayout.getChildAt(0), value); - if (mLayout.getChildCount() > 1) { - animationForChildAtIndex(1) - .property(property, value + getOffsetForChainedPropertyAnimation(property)) - .start(); - } - } - } - - /** Moves the stack to a position instantly, with no animation. */ - 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); - - if (mRestingStackPosition == null) { - mRestingStackPosition = new PointF(); - } - - mRestingStackPosition.set(mStackPosition); - - // If we're not the active controller, we don't want to physically move the bubble views. - if (isActiveController()) { - // Cancel animations that could be moving the views. - mLayout.cancelAllAnimationsOfProperties( - DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); - cancelStackPositionAnimations(); - - // Since we're not using the chained animations, apply the offsets manually. - final float xOffset = getOffsetForChainedPropertyAnimation( - DynamicAnimation.TRANSLATION_X); - final float yOffset = getOffsetForChainedPropertyAnimation( - DynamicAnimation.TRANSLATION_Y); - for (int i = 0; i < mLayout.getChildCount(); i++) { - mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset)); - mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset)); - } - } - } - - public void setStackPosition(BubbleStackView.RelativeStackPosition position) { - setStackPosition(position.getAbsolutePositionInRegion(getAllowableStackPositionRegion())); - } - - public BubbleStackView.RelativeStackPosition getRelativeStackPosition() { - return new BubbleStackView.RelativeStackPosition( - mStackPosition, getAllowableStackPositionRegion()); - } - - /** - * Sets the starting position for the stack, where it will be located when the first bubble is - * added. - */ - public void setStackStartPosition(BubbleStackView.RelativeStackPosition position) { - mStackStartPosition = position; - } - - /** - * Returns the starting stack position. If {@link #setStackStartPosition} was called, this will - * return that position - otherwise, a reasonable default will be returned. - */ - @Nullable public PointF getStartPosition() { - if (mLayout == null) { - return null; - } - - if (mStackStartPosition == null) { - // Start on the left if we're in LTR, right otherwise. - final boolean startOnLeft = - mLayout.getResources().getConfiguration().getLayoutDirection() - != View.LAYOUT_DIRECTION_RTL; - - final float startingVerticalOffset = mLayout.getResources().getDimensionPixelOffset( - R.dimen.bubble_stack_starting_offset_y); - - mStackStartPosition = new BubbleStackView.RelativeStackPosition( - startOnLeft, - startingVerticalOffset / getAllowableStackPositionRegion().height()); - } - - return mStackStartPosition.getAbsolutePositionInRegion(getAllowableStackPositionRegion()); - } - - private boolean isStackPositionSet() { - return mStackMovedToStartPosition; - } - - /** Animates in the given bubble. */ - private void animateInBubble(View child, int index) { - if (!isActiveController()) { - return; - } - - final float xOffset = - getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X); - - // Position the new bubble in the correct position, scaled down completely. - child.setTranslationX(mStackPosition.x + xOffset * index); - child.setTranslationY(mStackPosition.y); - child.setScaleX(0f); - child.setScaleY(0f); - - // Push the subsequent views out of the way, if there are subsequent views. - if (index + 1 < mLayout.getChildCount()) { - animationForChildAtIndex(index + 1) - .translationX(mStackPosition.x + xOffset * (index + 1)) - .withStiffness(SpringForce.STIFFNESS_LOW) - .start(); - } - - // Scale in the new bubble, slightly delayed. - animationForChild(child) - .scaleX(1f) - .scaleY(1f) - .withStiffness(ANIMATE_IN_STIFFNESS) - .withStartDelay(mLayout.getChildCount() > 1 ? ANIMATE_IN_START_DELAY : 0) - .start(); - } - - /** - * Cancels any outstanding first bubble property animations that are running. This does not - * affect the SpringAnimations controlling the individual bubbles' 'following' effect - it only - * cancels animations started from {@link #springFirstBubbleWithStackFollowing} and - * {@link #flingThenSpringFirstBubbleWithStackFollowing}. - */ - private void cancelStackPositionAnimation(DynamicAnimation.ViewProperty property) { - if (mStackPositionAnimations.containsKey(property)) { - mStackPositionAnimations.get(property).cancel(); - } - } - - /** - * Returns the {@link MagnetizedObject} instance for the bubble stack, with the provided - * {@link MagnetizedObject.MagneticTarget} added as a target. - */ - public MagnetizedObject<StackAnimationController> getMagnetizedStack( - MagnetizedObject.MagneticTarget target) { - if (mMagnetizedStack == null) { - mMagnetizedStack = new MagnetizedObject<StackAnimationController>( - mLayout.getContext(), - this, - new StackPositionProperty(DynamicAnimation.TRANSLATION_X), - new StackPositionProperty(DynamicAnimation.TRANSLATION_Y) - ) { - @Override - public float getWidth(@NonNull StackAnimationController underlyingObject) { - return mBubbleSize; - } - - @Override - public float getHeight(@NonNull StackAnimationController underlyingObject) { - return mBubbleSize; - } - - @Override - public void getLocationOnScreen(@NonNull StackAnimationController underlyingObject, - @NonNull int[] loc) { - loc[0] = (int) mStackPosition.x; - loc[1] = (int) mStackPosition.y; - } - }; - mMagnetizedStack.addTarget(target); - mMagnetizedStack.setHapticsEnabled(true); - mMagnetizedStack.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY); - } - - final ContentResolver contentResolver = mLayout.getContext().getContentResolver(); - final float minVelocity = Settings.Secure.getFloat(contentResolver, - "bubble_dismiss_fling_min_velocity", - mMagnetizedStack.getFlingToTargetMinVelocity() /* default */); - final float maxVelocity = Settings.Secure.getFloat(contentResolver, - "bubble_dismiss_stick_max_velocity", - mMagnetizedStack.getStickToTargetMaxXVelocity() /* default */); - final float targetWidth = Settings.Secure.getFloat(contentResolver, - "bubble_dismiss_target_width_percent", - mMagnetizedStack.getFlingToTargetWidthPercent() /* default */); - - mMagnetizedStack.setFlingToTargetMinVelocity(minVelocity); - mMagnetizedStack.setStickToTargetMaxXVelocity(maxVelocity); - mMagnetizedStack.setFlingToTargetWidthPercent(targetWidth); - - return mMagnetizedStack; - } - - /** Returns the number of 'real' bubbles (excluding overflow). */ - private int getBubbleCount() { - return mBubbleCountSupplier.getAsInt(); - } - - /** - * FloatProperty that uses {@link #moveFirstBubbleWithStackFollowing} to set the first bubble's - * translation and animate the rest of the stack with it. A DynamicAnimation can animate this - * property directly to move the first bubble and cause the stack to 'follow' to the new - * location. - * - * This could also be achieved by simply animating the first bubble view and adding an update - * listener to dispatch movement to the rest of the stack. However, this would require - * duplication of logic in that update handler - it's simpler to keep all logic contained in the - * {@link #moveFirstBubbleWithStackFollowing} method. - */ - private class StackPositionProperty - extends FloatPropertyCompat<StackAnimationController> { - private final DynamicAnimation.ViewProperty mProperty; - - private StackPositionProperty(DynamicAnimation.ViewProperty property) { - super(property.toString()); - mProperty = property; - } - - @Override - public float getValue(StackAnimationController controller) { - return mLayout.getChildCount() > 0 ? mProperty.getValue(mLayout.getChildAt(0)) : 0; - } - - @Override - public void setValue(StackAnimationController controller, float value) { - moveFirstBubbleWithStackFollowing(mProperty, value); - } - } -} - diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java b/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java deleted file mode 100644 index 6b5f237ac76f..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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.dagger; - -import android.app.INotificationManager; -import android.content.Context; -import android.content.pm.LauncherApps; -import android.os.Handler; -import android.view.WindowManager; - -import com.android.internal.logging.UiEventLogger; -import com.android.internal.statusbar.IStatusBarService; -import com.android.systemui.bubbles.BubbleController; -import com.android.systemui.bubbles.Bubbles; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.model.SysUiState; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.statusbar.FeatureFlags; -import com.android.systemui.statusbar.NotificationLockscreenUserManager; -import com.android.systemui.statusbar.NotificationShadeWindowController; -import com.android.systemui.statusbar.notification.NotificationEntryManager; -import com.android.systemui.statusbar.notification.collection.NotifPipeline; -import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy; -import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; -import com.android.systemui.statusbar.phone.ShadeController; -import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.statusbar.policy.ZenModeController; -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.WindowManagerShellWrapper; -import com.android.wm.shell.common.FloatingContentCoordinator; - -import dagger.Module; -import dagger.Provides; - -/** */ -@Module -public interface BubbleModule { - - /** - */ - @SysUISingleton - @Provides - static Bubbles newBubbleController( - Context context, - NotificationShadeWindowController notificationShadeWindowController, - StatusBarStateController statusBarStateController, - ShadeController shadeController, - ConfigurationController configurationController, - NotificationInterruptStateProvider interruptionStateProvider, - ZenModeController zenModeController, - NotificationLockscreenUserManager notifUserManager, - NotificationGroupManagerLegacy groupManager, - NotificationEntryManager entryManager, - NotifPipeline notifPipeline, - FeatureFlags featureFlags, - DumpManager dumpManager, - FloatingContentCoordinator floatingContentCoordinator, - SysUiState sysUiState, - INotificationManager notifManager, - IStatusBarService statusBarService, - WindowManager windowManager, - WindowManagerShellWrapper windowManagerShellWrapper, - LauncherApps launcherApps, - UiEventLogger uiEventLogger, - @Main Handler mainHandler, - ShellTaskOrganizer organizer) { - return BubbleController.create( - context, - notificationShadeWindowController, - statusBarStateController, - shadeController, - null /* synchronizer */, - configurationController, - interruptionStateProvider, - zenModeController, - notifUserManager, - groupManager, - entryManager, - notifPipeline, - featureFlags, - dumpManager, - floatingContentCoordinator, - sysUiState, - notifManager, - statusBarService, - windowManager, - windowManagerShellWrapper, - launcherApps, - uiEventLogger, - mainHandler, - organizer); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubblePersistentRepository.kt b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubblePersistentRepository.kt deleted file mode 100644 index ce0786d86460..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubblePersistentRepository.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.storage - -import android.content.Context -import android.util.AtomicFile -import android.util.Log -import java.io.File -import java.io.FileOutputStream -import java.io.IOException - -class BubblePersistentRepository(context: Context) { - - private val bubbleFile: AtomicFile = AtomicFile(File(context.filesDir, - "overflow_bubbles.xml"), "overflow-bubbles") - - fun persistsToDisk(bubbles: List<BubbleEntity>): Boolean { - if (DEBUG) Log.d(TAG, "persisting ${bubbles.size} bubbles") - synchronized(bubbleFile) { - val stream: FileOutputStream = try { bubbleFile.startWrite() } catch (e: IOException) { - Log.e(TAG, "Failed to save bubble file", e) - return false - } - try { - writeXml(stream, bubbles) - bubbleFile.finishWrite(stream) - if (DEBUG) Log.d(TAG, "persisted ${bubbles.size} bubbles") - return true - } catch (e: Exception) { - Log.e(TAG, "Failed to save bubble file, restoring backup", e) - bubbleFile.failWrite(stream) - } - } - return false - } - - fun readFromDisk(): List<BubbleEntity> { - synchronized(bubbleFile) { - if (!bubbleFile.exists()) return emptyList() - try { return bubbleFile.openRead().use(::readXml) } catch (e: Throwable) { - Log.e(TAG, "Failed to open bubble file", e) - } - return emptyList() - } - } -} - -private const val TAG = "BubblePersistentRepository" -private const val DEBUG = false diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleVolatileRepository.kt b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleVolatileRepository.kt deleted file mode 100644 index e0a7c7879f43..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleVolatileRepository.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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.storage - -import android.content.pm.LauncherApps -import android.os.UserHandle -import com.android.internal.annotations.VisibleForTesting -import com.android.systemui.bubbles.ShortcutKey - -private const val CAPACITY = 16 - -/** - * BubbleVolatileRepository holds the most updated snapshot of list of bubbles for in-memory - * manipulation. - */ -class BubbleVolatileRepository(private val launcherApps: LauncherApps) { - /** - * An ordered set of bubbles based on their natural ordering. - */ - private var entities = mutableSetOf<BubbleEntity>() - - /** - * The capacity of the cache. - */ - @VisibleForTesting - var capacity = CAPACITY - - /** - * Returns a snapshot of all the bubbles. - */ - val bubbles: List<BubbleEntity> - @Synchronized - get() = entities.toList() - - /** - * Add the bubbles to memory and perform a de-duplication. In case a bubble already exists, - * it will be moved to the last. - */ - @Synchronized - fun addBubbles(bubbles: List<BubbleEntity>) { - if (bubbles.isEmpty()) return - // Verify the size of given bubbles is within capacity, otherwise trim down to capacity - val bubblesInRange = bubbles.takeLast(capacity) - // To ensure natural ordering of the bubbles, removes bubbles which already exist - val uniqueBubbles = bubblesInRange.filterNot { b: BubbleEntity -> - entities.removeIf { e: BubbleEntity -> b.key == e.key } } - val overflowCount = entities.size + bubblesInRange.size - capacity - if (overflowCount > 0) { - // Uncache ShortcutInfo of bubbles that will be removed due to capacity - uncache(entities.take(overflowCount)) - entities = entities.drop(overflowCount).toMutableSet() - } - entities.addAll(bubblesInRange) - cache(uniqueBubbles) - } - - @Synchronized - fun removeBubbles(bubbles: List<BubbleEntity>) = - uncache(bubbles.filter { b: BubbleEntity -> - entities.removeIf { e: BubbleEntity -> b.key == e.key } }) - - private fun cache(bubbles: List<BubbleEntity>) { - bubbles.groupBy { ShortcutKey(it.userId, it.packageName) }.forEach { (key, bubbles) -> - launcherApps.cacheShortcuts(key.pkg, bubbles.map { it.shortcutId }, - UserHandle.of(key.userId), LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS) - } - } - - private fun uncache(bubbles: List<BubbleEntity>) { - bubbles.groupBy { ShortcutKey(it.userId, it.packageName) }.forEach { (key, bubbles) -> - launcherApps.uncacheShortcuts(key.pkg, bubbles.map { it.shortcutId }, - UserHandle.of(key.userId), LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleXmlHelper.kt b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleXmlHelper.kt deleted file mode 100644 index bf163a230aff..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleXmlHelper.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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.storage - -import com.android.internal.util.FastXmlSerializer -import org.xmlpull.v1.XmlSerializer -import java.io.IOException -import android.util.Xml -import com.android.internal.util.XmlUtils -import org.xmlpull.v1.XmlPullParser -import java.io.InputStream -import java.io.OutputStream -import java.nio.charset.StandardCharsets - -// TODO: handle version changes gracefully -private const val CURRENT_VERSION = 1 - -private const val TAG_BUBBLES = "bs" -private const val ATTR_VERSION = "v" -private const val TAG_BUBBLE = "bb" -private const val ATTR_USER_ID = "uid" -private const val ATTR_PACKAGE = "pkg" -private const val ATTR_SHORTCUT_ID = "sid" -private const val ATTR_KEY = "key" -private const val ATTR_DESIRED_HEIGHT = "h" -private const val ATTR_DESIRED_HEIGHT_RES_ID = "hid" -private const val ATTR_TITLE = "t" - -/** - * Writes the bubbles in xml format into given output stream. - */ -@Throws(IOException::class) -fun writeXml(stream: OutputStream, bubbles: List<BubbleEntity>) { - val serializer: XmlSerializer = FastXmlSerializer() - serializer.setOutput(stream, StandardCharsets.UTF_8.name()) - serializer.startDocument(null, true) - serializer.startTag(null, TAG_BUBBLES) - serializer.attribute(null, ATTR_VERSION, CURRENT_VERSION.toString()) - bubbles.forEach { b -> writeXmlEntry(serializer, b) } - serializer.endTag(null, TAG_BUBBLES) - serializer.endDocument() -} - -/** - * Creates a xml entry for given bubble in following format: - * ``` - * <bb uid="0" pkg="com.example.messenger" sid="my-shortcut" key="my-key" /> - * ``` - */ -private fun writeXmlEntry(serializer: XmlSerializer, bubble: BubbleEntity) { - try { - serializer.startTag(null, TAG_BUBBLE) - serializer.attribute(null, ATTR_USER_ID, bubble.userId.toString()) - serializer.attribute(null, ATTR_PACKAGE, bubble.packageName) - serializer.attribute(null, ATTR_SHORTCUT_ID, bubble.shortcutId) - serializer.attribute(null, ATTR_KEY, bubble.key) - serializer.attribute(null, ATTR_DESIRED_HEIGHT, bubble.desiredHeight.toString()) - serializer.attribute(null, ATTR_DESIRED_HEIGHT_RES_ID, bubble.desiredHeightResId.toString()) - bubble.title?.let { serializer.attribute(null, ATTR_TITLE, it) } - serializer.endTag(null, TAG_BUBBLE) - } catch (e: IOException) { - throw RuntimeException(e) - } -} - -/** - * Reads the bubbles from xml file. - */ -fun readXml(stream: InputStream): List<BubbleEntity> { - val bubbles = mutableListOf<BubbleEntity>() - val parser: XmlPullParser = Xml.newPullParser() - parser.setInput(stream, StandardCharsets.UTF_8.name()) - XmlUtils.beginDocument(parser, TAG_BUBBLES) - val version = parser.getAttributeWithName(ATTR_VERSION)?.toInt() - if (version != null && version == CURRENT_VERSION) { - val outerDepth = parser.depth - while (XmlUtils.nextElementWithin(parser, outerDepth)) { - bubbles.add(readXmlEntry(parser) ?: continue) - } - } - return bubbles -} - -private fun readXmlEntry(parser: XmlPullParser): BubbleEntity? { - while (parser.eventType != XmlPullParser.START_TAG) { parser.next() } - return BubbleEntity( - parser.getAttributeWithName(ATTR_USER_ID)?.toInt() ?: return null, - parser.getAttributeWithName(ATTR_PACKAGE) ?: return null, - parser.getAttributeWithName(ATTR_SHORTCUT_ID) ?: return null, - parser.getAttributeWithName(ATTR_KEY) ?: return null, - parser.getAttributeWithName(ATTR_DESIRED_HEIGHT)?.toInt() ?: return null, - parser.getAttributeWithName(ATTR_DESIRED_HEIGHT_RES_ID)?.toInt() ?: return null, - parser.getAttributeWithName(ATTR_TITLE) - ) -} - -private fun XmlPullParser.getAttributeWithName(name: String): String? { - for (i in 0 until attributeCount) { - if (getAttributeName(i) == name) return getAttributeValue(i) - } - return null -}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java b/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java index cb90b6114396..3aa462657637 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java @@ -38,7 +38,6 @@ import android.view.accessibility.AccessibilityManager; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; -import com.android.internal.logging.UiEventLoggerImpl; import com.android.internal.util.NotificationMessagingUtil; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.ViewMediatorCallback; @@ -340,13 +339,6 @@ public class DependencyProvider { return Choreographer.getInstance(); } - /** Provides an instance of {@link com.android.internal.logging.UiEventLogger} */ - @Provides - @SysUISingleton - static UiEventLogger provideUiEventLogger() { - return new UiEventLoggerImpl(); - } - /** */ @Provides @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/dagger/GlobalModule.java b/packages/SystemUI/src/com/android/systemui/dagger/GlobalModule.java index c5dc8cccfdf4..53383d65e379 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/GlobalModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/GlobalModule.java @@ -16,9 +16,18 @@ package com.android.systemui.dagger; +import android.content.Context; +import android.util.DisplayMetrics; + +import com.android.internal.logging.UiEventLogger; +import com.android.internal.logging.UiEventLoggerImpl; import com.android.systemui.util.concurrency.GlobalConcurrencyModule; +import com.android.wm.shell.animation.FlingAnimationUtils; + +import javax.inject.Singleton; import dagger.Module; +import dagger.Provides; /** * Supplies globally scoped instances that should be available in all versions of SystemUI @@ -39,4 +48,22 @@ import dagger.Module; FrameworkServicesModule.class, GlobalConcurrencyModule.class}) public class GlobalModule { + + // TODO(b/162923491): This should not be a singleton at all, the display metrics can change and + // callers should be creating a new builder on demand + @Singleton + @Provides + static FlingAnimationUtils.Builder provideFlingAnimationUtilsBuilder( + Context context) { + DisplayMetrics displayMetrics = new DisplayMetrics(); + context.getDisplay().getMetrics(displayMetrics); + return new FlingAnimationUtils.Builder(displayMetrics); + } + + /** Provides an instance of {@link com.android.internal.logging.UiEventLogger} */ + @Provides + @Singleton + static UiEventLogger provideUiEventLogger() { + return new UiEventLoggerImpl(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java index 00fdf55b28e0..d648c949ffc5 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java @@ -52,7 +52,7 @@ public interface GlobalRootComponent { WMComponent.Builder getWMComponentBuilder(); /** - * Builder for a SysuiComponent. + * Builder for a SysUIComponent. */ SysUIComponent.Builder getSysUIComponent(); diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index 4bea0674e8bf..b94a68b2441d 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -22,8 +22,17 @@ import com.android.systemui.InitController; import com.android.systemui.SystemUIAppComponentFactory; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardSliceProvider; +import com.android.systemui.shared.system.InputConsumerController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.util.InjectionInflationController; +import com.android.wm.shell.ShellDump; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.bubbles.Bubbles; +import com.android.wm.shell.onehanded.OneHanded; +import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.splitscreen.SplitScreen; + +import java.util.Optional; import dagger.BindsInstance; import dagger.Subcomponent; @@ -43,15 +52,41 @@ public interface SysUIComponent { /** * Builder for a SysUIComponent. */ + @SysUISingleton @Subcomponent.Builder interface Builder { @BindsInstance - Builder setStubAPIClass(WMComponent.StubAPIClass stubAPIClass); + Builder setPip(Optional<Pip> p); + + @BindsInstance + Builder setSplitScreen(Optional<SplitScreen> s); + + @BindsInstance + Builder setOneHanded(Optional<OneHanded> o); + + @BindsInstance + Builder setBubbles(Optional<Bubbles> b); + + @BindsInstance + Builder setInputConsumerController(InputConsumerController i); + + @BindsInstance + Builder setShellTaskOrganizer(ShellTaskOrganizer s); + + @BindsInstance + Builder setShellDump(Optional<ShellDump> shellDump); SysUIComponent build(); } /** + * Initializes all the SysUI components. + */ + default void init() { + // Do nothing + } + + /** * Provides a BootCompleteCache. */ @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java index 1f6288a94ad4..c0013d8cb981 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java @@ -24,7 +24,6 @@ import com.android.systemui.SystemUI; import com.android.systemui.accessibility.SystemActions; import com.android.systemui.accessibility.WindowMagnification; import com.android.systemui.biometrics.AuthController; -import com.android.systemui.bubbles.dagger.BubbleModule; import com.android.systemui.globalactions.GlobalActionsComponent; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.dagger.KeyguardModule; @@ -51,8 +50,7 @@ import dagger.multibindings.IntoMap; /** * SystemUI objects that are injectable should go here. */ -@Module(includes = {RecentsModule.class, StatusBarModule.class, BubbleModule.class, - KeyguardModule.class}) +@Module(includes = {RecentsModule.class, StatusBarModule.class, KeyguardModule.class}) public abstract class SystemUIBinder { /** Inject into AuthController. */ @Binds diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java index 2c0b04fed810..7ca8e63bfae1 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java @@ -33,6 +33,7 @@ import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dock.DockManager; import com.android.systemui.dock.DockManagerImpl; import com.android.systemui.doze.DozeHost; +import com.android.systemui.media.dagger.MediaModule; import com.android.systemui.plugins.qs.QSFactory; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.power.EnhancedEstimates; @@ -61,7 +62,6 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.DeviceProvisionedControllerImpl; import com.android.systemui.statusbar.policy.HeadsUpManager; -import com.android.systemui.wmshell.WMShellModule; import javax.inject.Named; @@ -74,9 +74,9 @@ import dagger.Provides; * overridden by the System UI implementation. */ @Module(includes = { - QSModule.class, - WMShellModule.class - }) + MediaModule.class, + QSModule.class +}) public abstract class SystemUIDefaultModule { @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 63d9a831b33f..780bb5b01103 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -16,6 +16,12 @@ package com.android.systemui.dagger; +import android.app.INotificationManager; +import android.content.Context; + +import androidx.annotation.Nullable; + +import com.android.internal.statusbar.IStatusBarService; import com.android.keyguard.dagger.KeyguardBouncerComponent; import com.android.systemui.BootCompleteCache; import com.android.systemui.BootCompleteCacheImpl; @@ -24,23 +30,35 @@ import com.android.systemui.assist.AssistModule; import com.android.systemui.controls.dagger.ControlsModule; import com.android.systemui.demomode.dagger.DemoModeModule; import com.android.systemui.doze.dagger.DozeComponent; +import com.android.systemui.dump.DumpManager; import com.android.systemui.fragments.FragmentService; import com.android.systemui.log.dagger.LogModule; import com.android.systemui.model.SysUiState; +import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.power.dagger.PowerModule; import com.android.systemui.recents.Recents; import com.android.systemui.screenshot.dagger.ScreenshotModule; import com.android.systemui.settings.dagger.SettingsModule; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.FeatureFlags; +import com.android.systemui.statusbar.NotificationLockscreenUserManager; +import com.android.systemui.statusbar.NotificationShadeWindowController; +import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder; import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl; +import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy; +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; import com.android.systemui.statusbar.notification.people.PeopleHubModule; import com.android.systemui.statusbar.notification.row.dagger.ExpandableNotificationRowComponent; import com.android.systemui.statusbar.notification.row.dagger.NotificationRowComponent; import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent; +import com.android.systemui.statusbar.phone.ShadeController; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.dagger.StatusBarComponent; +import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.HeadsUpManager; +import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.policy.dagger.SmartRepliesInflationModule; import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule; import com.android.systemui.tuner.dagger.TunerModule; @@ -52,6 +70,10 @@ import com.android.systemui.util.settings.SettingsUtilModule; import com.android.systemui.util.time.SystemClock; import com.android.systemui.util.time.SystemClockImpl; import com.android.systemui.volume.dagger.VolumeModule; +import com.android.systemui.wmshell.BubblesManager; +import com.android.wm.shell.bubbles.Bubbles; + +import java.util.Optional; import dagger.Binds; import dagger.BindsOptionalOf; @@ -128,4 +150,25 @@ public abstract class SystemUIModule { @SysUISingleton @Binds abstract SystemClock bindSystemClock(SystemClockImpl systemClock); + + /** Provides Optional of BubbleManager */ + @SysUISingleton + @Provides + static Optional<BubblesManager> provideBubblesManager(Context context, + Optional<Bubbles> bubblesOptional, + NotificationShadeWindowController notificationShadeWindowController, + StatusBarStateController statusBarStateController, ShadeController shadeController, + ConfigurationController configurationController, + @Nullable IStatusBarService statusBarService, INotificationManager notificationManager, + NotificationInterruptStateProvider interruptionStateProvider, + ZenModeController zenModeController, NotificationLockscreenUserManager notifUserManager, + NotificationGroupManagerLegacy groupManager, NotificationEntryManager entryManager, + NotifPipeline notifPipeline, SysUiState sysUiState, FeatureFlags featureFlags, + DumpManager dumpManager) { + return Optional.ofNullable(BubblesManager.create(context, bubblesOptional, + notificationShadeWindowController, statusBarStateController, shadeController, + configurationController, statusBarService, notificationManager, + interruptionStateProvider, zenModeController, notifUserManager, + groupManager, entryManager, notifPipeline, sysUiState, featureFlags, dumpManager)); + } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java index ad90eff3c969..8f3d8eaac2d3 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java @@ -16,7 +16,17 @@ package com.android.systemui.dagger; -import javax.inject.Inject; +import com.android.systemui.shared.system.InputConsumerController; +import com.android.systemui.wmshell.WMShellModule; +import com.android.wm.shell.ShellDump; +import com.android.wm.shell.ShellInit; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.bubbles.Bubbles; +import com.android.wm.shell.onehanded.OneHanded; +import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.splitscreen.SplitScreen; + +import java.util.Optional; import dagger.Subcomponent; @@ -24,7 +34,7 @@ import dagger.Subcomponent; * Dagger Subcomponent for WindowManager. */ @WMSingleton -@Subcomponent(modules = {}) +@Subcomponent(modules = {WMShellModule.class}) public interface WMComponent { /** @@ -35,18 +45,40 @@ public interface WMComponent { WMComponent build(); } - /** - * Example class used for passing an API to SysUI from WMShell. - * - * TODO: Remove this once real WM classes are ready to go. - **/ - @WMSingleton - class StubAPIClass { - @Inject - StubAPIClass() {} + * Initializes all the WMShell components before starting any of the SystemUI components. + */ + default void init() { + getShellInit().init(); } - /** Create a StubAPIClass. */ - StubAPIClass createStubAPIClass(); + // Gets the Shell init instance + @WMSingleton + ShellInit getShellInit(); + + // Gets the Shell dump instance + @WMSingleton + Optional<ShellDump> getShellDump(); + + // TODO(b/162923491): Refactor this out so Pip doesn't need to inject this + @WMSingleton + InputConsumerController getInputConsumerController(); + + // TODO(b/162923491): To be removed once Bubbles migrates over to the Shell + @WMSingleton + ShellTaskOrganizer getShellTaskOrganizer(); + + // TODO(b/162923491): We currently pass the instances through to SysUI, but that may change + // depending on the threading mechanism we go with + @WMSingleton + Optional<OneHanded> getOneHanded(); + + @WMSingleton + Optional<Pip> getPip(); + + @WMSingleton + Optional<SplitScreen> getSplitScreen(); + + @WMSingleton + Optional<Bubbles> getBubbles(); } diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/RootView.java b/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/RootView.java index 5ebff097604b..e6c46c07fff8 100644 --- a/packages/SystemUI/src/com/android/keyguard/dagger/RootView.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/RootView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.keyguard.dagger; +package com.android.systemui.dagger.qualifiers; import static java.lang.annotation.RetentionPolicy.RUNTIME; diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java index 028870f3815e..ebfce661c9af 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java @@ -44,6 +44,8 @@ import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.UiEventLoggerImpl; import com.android.internal.logging.nano.MetricsProto; +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.systemui.biometrics.AuthController; import com.android.systemui.plugins.SensorManagerPlugin; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.util.sensors.AsyncSensorManager; @@ -98,7 +100,8 @@ public class DozeSensors { DozeSensors(Context context, AsyncSensorManager sensorManager, DozeParameters dozeParameters, AmbientDisplayConfiguration config, WakeLock wakeLock, Callback callback, Consumer<Boolean> proxCallback, DozeLog dozeLog, - ProximitySensor proximitySensor, SecureSettings secureSettings) { + ProximitySensor proximitySensor, SecureSettings secureSettings, + AuthController authController) { mContext = context; mSensorManager = sensorManager; mConfig = config; @@ -152,9 +155,9 @@ public class DozeSensors { dozeLog), new TriggerSensor( findSensorWithType(config.udfpsLongPressSensorType()), - Settings.Secure.DOZE_PULSE_ON_LONG_PRESS, - false /* settingDef */, - true /* configured */, + "doze_pulse_on_auth", + true /* settingDef */, + authController.isUdfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser()), DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS, true /* reports touch coordinates */, true /* touchscreen */, diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java index 45e5c614ea58..58e49f896931 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java @@ -131,7 +131,10 @@ public class DozeTriggers implements DozeMachine.Part { DOZING_UPDATE_SENSOR_WAKE_LOCKSCREEN(440), @UiEvent(doc = "Dozing updated because sensor was tapped.") - DOZING_UPDATE_SENSOR_TAP(441); + DOZING_UPDATE_SENSOR_TAP(441), + + @UiEvent(doc = "Dozing updated because on display auth was triggered from AOD.") + DOZING_UPDATE_AUTH_TRIGGERED(442); private final int mId; @@ -155,6 +158,7 @@ public class DozeTriggers implements DozeMachine.Part { case 7: return DOZING_UPDATE_SENSOR_WAKEUP; case 8: return DOZING_UPDATE_SENSOR_WAKE_LOCKSCREEN; case 9: return DOZING_UPDATE_SENSOR_TAP; + case 10: return DOZING_UPDATE_AUTH_TRIGGERED; default: return null; } } @@ -177,7 +181,7 @@ public class DozeTriggers implements DozeMachine.Part { mAllowPulseTriggers = true; mDozeSensors = new DozeSensors(context, mSensorManager, dozeParameters, config, wakeLock, this::onSensor, this::onProximityFar, dozeLog, proximitySensor, - secureSettings); + secureSettings, authController); mUiModeManager = mContext.getSystemService(UiModeManager.class); mDockManager = dockManager; mProxCheck = proxCheck; @@ -286,10 +290,11 @@ public class DozeTriggers implements DozeMachine.Part { } else if (isPickup) { gentleWakeUp(pulseReason); } else if (isUdfpsLongPress) { - gentleWakeUp(pulseReason); + requestPulse(DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS, true, null); // Since the gesture won't be received by the UDFPS view, manually inject an // event. - mAuthController.onAodInterrupt((int) screenX, (int) screenY); + mAuthController.onAodInterrupt((int) screenX, (int) screenY, + rawValues[2] /* major */, rawValues[3] /* minor */); } else { mDozeHost.extendPulse(pulseReason); } diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java index b55b29a80410..a330be6449e2 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java @@ -140,8 +140,14 @@ public class GlobalActionsImpl implements GlobalActions, CommandQueue.Callbacks d.setContentView(R.layout.shutdown_dialog); d.setCancelable(false); - int color = Utils.getColorAttrDefaultColor(mContext, - com.android.systemui.R.attr.wallpaperTextColor); + int color; + if (mBlurUtils.supportsBlursOnWindows()) { + color = Utils.getColorAttrDefaultColor(mContext, + com.android.systemui.R.attr.wallpaperTextColor); + } else { + color = mContext.getResources().getColor( + com.android.systemui.R.color.global_actions_shutdown_ui_text); + } ProgressBar bar = d.findViewById(R.id.progress); bar.getIndeterminateDrawable().setTint(color); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java index 5f726cd1e1f9..37bcb163d6f3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java @@ -316,7 +316,8 @@ public class KeyguardSliceProvider extends SliceProvider implements } mDatePattern = getContext().getString(R.string.system_ui_aod_date_pattern); mPendingIntent = PendingIntent.getActivity(getContext(), 0, - new Intent(getContext(), KeyguardSliceProvider.class), 0); + new Intent(getContext(), KeyguardSliceProvider.class), + PendingIntent.FLAG_IMMUTABLE); try { //TODO(b/168778439): Remove this whole try catch. This is for debugging in dogfood. mMediaManager.addCallback(this); diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index e3ee2a10821b..fff185b99a1e 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -120,6 +120,18 @@ public class LogModule { return buffer; } + /** Provides a logging buffer for all logs related to privacy indicators in SystemUI. */ + @Provides + @SysUISingleton + @PrivacyLog + public static LogBuffer providePrivacyLogBuffer( + LogcatEchoTracker bufferFilter, + DumpManager dumpManager) { + LogBuffer buffer = new LogBuffer(("PrivacyLog"), 100, 10, bufferFilter); + buffer.attach(dumpManager); + return buffer; + } + /** Allows logging buffers to be tweaked via adb on debug builds but not on prod builds. */ @Provides @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java new file mode 100644 index 000000000000..e96e532f94bf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2019 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.log.dagger; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.android.systemui.log.LogBuffer; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +/** A {@link LogBuffer} for privacy indicator-related messages. */ +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface PrivacyLog { +} diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt index 094ece27fc8e..6fb86504ec32 100644 --- a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt @@ -18,6 +18,7 @@ package com.android.systemui.media import android.view.View import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.media.dagger.MediaModule.KEYGUARD import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.StatusBarState @@ -25,6 +26,7 @@ import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.notification.stack.MediaHeaderView import com.android.systemui.statusbar.phone.KeyguardBypassController import javax.inject.Inject +import javax.inject.Named /** * A class that controls the media notifications on the lock screen, handles its visibility and @@ -32,7 +34,7 @@ import javax.inject.Inject */ @SysUISingleton class KeyguardMediaController @Inject constructor( - private val mediaHost: MediaHost, + @param:Named(KEYGUARD) private val mediaHost: MediaHost, private val bypassController: KeyguardBypassController, private val statusBarStateController: SysuiStatusBarStateController, private val notifLockscreenUserManager: NotificationLockscreenUserManager diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt index 1beb875af70c..5eb6687f0b81 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt @@ -143,6 +143,11 @@ class MediaCarouselController @Inject constructor( if (newConfig == null) return isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL } + + override fun onUiModeChanged() { + // Only settings button needs to update for dark theme + inflateSettingsButton() + } } init { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt index d80aafb714d3..cb14f31abd16 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt @@ -282,10 +282,12 @@ class MediaCarouselScrollHandler( scrollXAmount = -1 * relativePos } if (scrollXAmount != 0) { + val dx = if (isRtl) -scrollXAmount else scrollXAmount + val newScrollX = scrollView.relativeScrollX + dx // Delay the scrolling since scrollView calls springback which cancels // the animation again.. mainExecutor.execute { - scrollView.smoothScrollBy(if (isRtl) -scrollXAmount else scrollXAmount, 0) + scrollView.smoothScrollTo(newScrollX, scrollView.scrollY) } } val currentTranslation = scrollView.getContentTranslation() @@ -553,4 +555,4 @@ class MediaCarouselScrollHandler( } } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 5b096ea363b6..c18a6a45e286 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -354,7 +354,15 @@ public class MediaControlPanel { final MediaController controller = getController(); mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller)); + // Guts label + boolean isDismissible = data.isClearable(); + mViewHolder.getSettingsText().setText(isDismissible + ? R.string.controls_media_close_session + : R.string.controls_media_active_session); + // Dismiss + mViewHolder.getDismissLabel().setAlpha(isDismissible ? 1 : DISABLED_ALPHA); + mViewHolder.getDismiss().setEnabled(isDismissible); mViewHolder.getDismiss().setOnClickListener(v -> { if (mKey != null) { closeGuts(); diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index 6f6ee4c8091d..c6ed9c096544 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -641,7 +641,8 @@ class MediaDataManager( // Move to resume key (aka package name) if that key doesn't already exist. val resumeAction = getResumeMediaAction(removed.resumeAction!!) val updated = removed.copy(token = null, actions = listOf(resumeAction), - actionsToShowInCompact = listOf(0), active = false, resumption = true) + actionsToShowInCompact = listOf(0), active = false, resumption = true, + isClearable = true) val pkg = removed.packageName val migrate = mediaEntries.put(pkg, updated) == null // Notify listeners of "new" controls when migrating or removed and update when not diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt index ce184aa23a57..857c50fc8d32 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt @@ -11,7 +11,7 @@ import com.android.systemui.util.animation.UniqueObjectHostView import java.util.Objects import javax.inject.Inject -class MediaHost @Inject constructor( +class MediaHost constructor( private val state: MediaHostStateHolder, private val mediaHierarchyManager: MediaHierarchyManager, private val mediaDataManager: MediaDataManager, diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt index 51dbfa733541..ce72991d01c0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt @@ -46,7 +46,7 @@ class MediaTimeoutListener @Inject constructor( /** * Callback representing that a media object is now expired: * @param token Media session unique identifier - * @param pauseTimeuot True when expired for {@code PAUSED_MEDIA_TIMEOUT} + * @param pauseTimeout True when expired for {@code PAUSED_MEDIA_TIMEOUT} */ lateinit var timeoutCallback: (String, Boolean) -> Unit @@ -57,11 +57,10 @@ class MediaTimeoutListener @Inject constructor( // Having an old key means that we're migrating from/to resumption. We should update // the old listener to make sure that events will be dispatched to the new location. val migrating = oldKey != null && key != oldKey - var wasPlaying = false if (migrating) { val reusedListener = mediaListeners.remove(oldKey) if (reusedListener != null) { - wasPlaying = reusedListener.playing ?: false + val wasPlaying = reusedListener.playing ?: false if (DEBUG) Log.d(TAG, "migrating key $oldKey to $key, for resumption") reusedListener.mediaData = data reusedListener.key = key @@ -159,9 +158,8 @@ class MediaTimeoutListener @Inject constructor( Log.v(TAG, "Execute timeout for $key") } timedOut = true - if (dispatchEvents) { - timeoutCallback(key, timedOut) - } + // this event is async, so it's safe even when `dispatchEvents` is false + timeoutCallback(key, timedOut) }, PAUSED_MEDIA_TIMEOUT) } else { expireMediaTimeout(key, "playback started - $state, $key") diff --git a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt index 666a6038a8b6..16327bd9064a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt @@ -60,8 +60,10 @@ class PlayerViewHolder private constructor(itemView: View) { val action4 = itemView.requireViewById<ImageButton>(R.id.action4) // Settings screen + val settingsText = itemView.requireViewById<TextView>(R.id.remove_text) val cancel = itemView.requireViewById<View>(R.id.cancel) - val dismiss = itemView.requireViewById<View>(R.id.dismiss) + val dismiss = itemView.requireViewById<ViewGroup>(R.id.dismiss) + val dismissLabel = dismiss.getChildAt(0) val settings = itemView.requireViewById<View>(R.id.settings) init { diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt index 9e326aaec3c1..c8244589ce44 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt @@ -91,9 +91,9 @@ class SeekBarViewModel @Inject constructor(@Background private val bgExecutor: R } private var playbackState: PlaybackState? = null private var callback = object : MediaController.Callback() { - override fun onPlaybackStateChanged(state: PlaybackState) { + override fun onPlaybackStateChanged(state: PlaybackState?) { playbackState = state - if (PlaybackState.STATE_NONE.equals(playbackState)) { + if (playbackState == null || PlaybackState.STATE_NONE.equals(playbackState)) { clearController() } else { checkIfPollingNeeded() diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java new file mode 100644 index 000000000000..57ac9dfb52cd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java @@ -0,0 +1,66 @@ +/* + * 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.media.dagger; + +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.media.MediaDataManager; +import com.android.systemui.media.MediaHierarchyManager; +import com.android.systemui.media.MediaHost; +import com.android.systemui.media.MediaHostStatesManager; + +import javax.inject.Named; + +import dagger.Module; +import dagger.Provides; + +/** Dagger module for the media package. */ +@Module +public interface MediaModule { + String QS_PANEL = "media_qs_panel"; + String QUICK_QS_PANEL = "media_quick_qs_panel"; + String KEYGUARD = "media_keyguard"; + + /** */ + @Provides + @SysUISingleton + @Named(QS_PANEL) + static MediaHost providesQSMediaHost(MediaHost.MediaHostStateHolder stateHolder, + MediaHierarchyManager hierarchyManager, MediaDataManager dataManager, + MediaHostStatesManager statesManager) { + return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager); + } + + /** */ + @Provides + @SysUISingleton + @Named(QUICK_QS_PANEL) + static MediaHost providesQuickQSMediaHost(MediaHost.MediaHostStateHolder stateHolder, + MediaHierarchyManager hierarchyManager, MediaDataManager dataManager, + MediaHostStatesManager statesManager) { + return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager); + } + + /** */ + @Provides + @SysUISingleton + @Named(KEYGUARD) + static MediaHost providesKeyguardMediaHost(MediaHost.MediaHostStateHolder stateHolder, + MediaHierarchyManager hierarchyManager, MediaDataManager dataManager, + MediaHostStatesManager statesManager) { + return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java index d1630ebe8dc8..0d5faff65aab 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java @@ -22,6 +22,7 @@ import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.text.SpannableString; +import android.text.TextUtils; import android.text.style.ForegroundColorSpan; import android.util.Log; import android.view.View; @@ -45,6 +46,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private ViewGroup mConnectedItem; + private boolean mInclueDynamicGroup; public MediaOutputAdapter(MediaOutputController controller) { super(controller); @@ -61,9 +63,21 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { @Override public void onBindViewHolder(@NonNull MediaDeviceBaseViewHolder viewHolder, int position) { final int size = mController.getMediaDevices().size(); - if (mController.isZeroMode() && position == size) { + if (position == size && mController.isZeroMode()) { viewHolder.onBind(CUSTOMIZED_ITEM_PAIR_NEW, false /* topMargin */, true /* bottomMargin */); + } else if (mInclueDynamicGroup) { + if (position == 0) { + viewHolder.onBind(CUSTOMIZED_ITEM_DYNAMIC_GROUP, true /* topMargin */, + false /* bottomMargin */); + } else { + // When group item is added at the first(position == 0), devices will be added from + // the second item(position == 1). It means that the index of device list starts + // from "position - 1". + viewHolder.onBind(((List<MediaDevice>) (mController.getMediaDevices())) + .get(position - 1), + false /* topMargin */, position == size /* bottomMargin */); + } } else if (position < size) { viewHolder.onBind(((List<MediaDevice>) (mController.getMediaDevices())).get(position), position == 0 /* topMargin */, position == (size - 1) /* bottomMargin */); @@ -74,8 +88,9 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { @Override public int getItemCount() { - if (mController.isZeroMode()) { - // Add extra one for "pair new" + mInclueDynamicGroup = mController.getSelectedMediaDevice().size() > 1; + if (mController.isZeroMode() || mInclueDynamicGroup) { + // Add extra one for "pair new" or dynamic group return mController.getMediaDevices().size() + 1; } return mController.getMediaDevices().size(); @@ -107,36 +122,47 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { @Override void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin) { super.onBind(device, topMargin, bottomMargin); - final boolean currentlyConnected = isCurrentlyConnected(device); + final boolean currentlyConnected = !mInclueDynamicGroup && isCurrentlyConnected(device); if (currentlyConnected) { - mConnectedItem = mFrameLayout; + mConnectedItem = mContainerLayout; + } + mBottomDivider.setVisibility(View.GONE); + mCheckBox.setVisibility(View.GONE); + if (currentlyConnected && mController.isActiveRemoteDevice(device)) { + // Init active device layout + mDivider.setVisibility(View.VISIBLE); + mDivider.setTransitionAlpha(1); + mAddIcon.setVisibility(View.VISIBLE); + mAddIcon.setTransitionAlpha(1); + mAddIcon.setOnClickListener(v -> onEndItemClick()); + } else { + // Init non-active device layout + mDivider.setVisibility(View.GONE); + mAddIcon.setVisibility(View.GONE); } if (mController.isTransferring()) { if (device.getState() == MediaDeviceState.STATE_CONNECTING && !mController.hasAdjustVolumeUserRestriction()) { - setTwoLineLayout(device, null /* title */, true /* bFocused */, - false /* showSeekBar*/, true /* showProgressBar */, - false /* showSubtitle */); + setTwoLineLayout(device, true /* bFocused */, false /* showSeekBar*/, + true /* showProgressBar */, false /* showSubtitle */); } else { setSingleLineLayout(getItemTitle(device), false /* bFocused */); } } else { // Set different layout for each device if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) { - setTwoLineLayout(device, null /* title */, false /* bFocused */, - false /* showSeekBar*/, false /* showProgressBar */, + setTwoLineLayout(device, false /* bFocused */, + false /* showSeekBar */, false /* showProgressBar */, true /* showSubtitle */); mSubTitleText.setText(R.string.media_output_dialog_connect_failed); - mFrameLayout.setOnClickListener(v -> onItemClick(v, device)); - } else if (!mController.hasAdjustVolumeUserRestriction() - && currentlyConnected) { - setTwoLineLayout(device, null /* title */, true /* bFocused */, - true /* showSeekBar*/, false /* showProgressBar */, - false /* showSubtitle */); + mContainerLayout.setOnClickListener(v -> onItemClick(v, device)); + } else if (!mController.hasAdjustVolumeUserRestriction() && currentlyConnected) { + setTwoLineLayout(device, true /* bFocused */, true /* showSeekBar */, + false /* showProgressBar */, false /* showSubtitle */); initSeekbar(device); } else { setSingleLineLayout(getItemTitle(device), false /* bFocused */); - mFrameLayout.setOnClickListener(v -> onItemClick(v, device)); + mContainerLayout.setOnClickListener(v -> onItemClick(v, device)); } } } @@ -145,13 +171,33 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { void onBind(int customizedItem, boolean topMargin, boolean bottomMargin) { super.onBind(customizedItem, topMargin, bottomMargin); if (customizedItem == CUSTOMIZED_ITEM_PAIR_NEW) { + mCheckBox.setVisibility(View.GONE); + mDivider.setVisibility(View.GONE); + mAddIcon.setVisibility(View.GONE); + mBottomDivider.setVisibility(View.GONE); setSingleLineLayout(mContext.getText(R.string.media_output_dialog_pairing_new), false /* bFocused */); final Drawable d = mContext.getDrawable(R.drawable.ic_add); d.setColorFilter(new PorterDuffColorFilter( Utils.getColorAccentDefaultColor(mContext), PorterDuff.Mode.SRC_IN)); mTitleIcon.setImageDrawable(d); - mFrameLayout.setOnClickListener(v -> onItemClick(CUSTOMIZED_ITEM_PAIR_NEW)); + mContainerLayout.setOnClickListener(v -> onItemClick(CUSTOMIZED_ITEM_PAIR_NEW)); + } else if (customizedItem == CUSTOMIZED_ITEM_DYNAMIC_GROUP) { + mConnectedItem = mContainerLayout; + mBottomDivider.setVisibility(View.GONE); + mCheckBox.setVisibility(View.GONE); + mDivider.setVisibility(View.VISIBLE); + mDivider.setTransitionAlpha(1); + mAddIcon.setVisibility(View.VISIBLE); + mAddIcon.setTransitionAlpha(1); + mAddIcon.setOnClickListener(v -> onEndItemClick()); + mTitleIcon.setImageDrawable(getSpeakerDrawable()); + final CharSequence sessionName = mController.getSessionName(); + final CharSequence title = TextUtils.isEmpty(sessionName) + ? mContext.getString(R.string.media_output_dialog_group) : sessionName; + setTwoLineLayout(title, true /* bFocused */, true /* showSeekBar */, + false /* showProgressBar */, false /* showSubtitle */); + initSessionSeekbar(); } } @@ -173,5 +219,9 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { mController.launchBluetoothPairing(); } } + + private void onEndItemClick() { + mController.launchMediaOutputGroupDialog(); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java index 2d3e77db1ea3..f1d4804aa622 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java @@ -19,13 +19,18 @@ package com.android.systemui.media.dialog; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; import android.graphics.Typeface; +import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.FrameLayout; +import android.widget.CheckBox; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.SeekBar; @@ -34,6 +39,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; +import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.media.MediaDevice; import com.android.systemui.Interpolators; import com.android.systemui.R; @@ -44,19 +50,18 @@ import com.android.systemui.R; public abstract class MediaOutputBaseAdapter extends RecyclerView.Adapter<MediaOutputBaseAdapter.MediaDeviceBaseViewHolder> { - private static final String FONT_SELECTED_TITLE = "sans-serif-medium"; - private static final String FONT_TITLE = "sans-serif"; - static final int CUSTOMIZED_ITEM_PAIR_NEW = 1; + static final int CUSTOMIZED_ITEM_GROUP = 2; + static final int CUSTOMIZED_ITEM_DYNAMIC_GROUP = 3; final MediaOutputController mController; - private boolean mIsDragging; private int mMargin; private boolean mIsAnimating; Context mContext; View mHolderView; + boolean mIsDragging; public MediaOutputBaseAdapter(MediaOutputController controller) { mController = controller; @@ -99,27 +104,33 @@ public abstract class MediaOutputBaseAdapter extends private static final int ANIM_DURATION = 200; - final FrameLayout mFrameLayout; + final LinearLayout mContainerLayout; final TextView mTitleText; final TextView mTwoLineTitleText; final TextView mSubTitleText; final ImageView mTitleIcon; - final ImageView mEndIcon; + final ImageView mAddIcon; final ProgressBar mProgressBar; final SeekBar mSeekBar; final RelativeLayout mTwoLineLayout; + final View mDivider; + final View mBottomDivider; + final CheckBox mCheckBox; MediaDeviceBaseViewHolder(View view) { super(view); - mFrameLayout = view.requireViewById(R.id.device_container); + mContainerLayout = view.requireViewById(R.id.device_container); mTitleText = view.requireViewById(R.id.title); mSubTitleText = view.requireViewById(R.id.subtitle); mTwoLineLayout = view.requireViewById(R.id.two_line_layout); mTwoLineTitleText = view.requireViewById(R.id.two_line_title); mTitleIcon = view.requireViewById(R.id.title_icon); - mEndIcon = view.requireViewById(R.id.end_icon); mProgressBar = view.requireViewById(R.id.volume_indeterminate_progress); mSeekBar = view.requireViewById(R.id.volume_seekbar); + mDivider = view.requireViewById(R.id.end_divider); + mBottomDivider = view.requireViewById(R.id.bottom_divider); + mAddIcon = view.requireViewById(R.id.add_icon); + mCheckBox = view.requireViewById(R.id.check_box); } void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin) { @@ -132,11 +143,11 @@ public abstract class MediaOutputBaseAdapter extends } private void setMargin(boolean topMargin, boolean bottomMargin) { - ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mFrameLayout + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mContainerLayout .getLayoutParams(); params.topMargin = topMargin ? mMargin : 0; params.bottomMargin = bottomMargin ? mMargin : 0; - mFrameLayout.setLayoutParams(params); + mContainerLayout.setLayoutParams(params); } void setSingleLineLayout(CharSequence title, boolean bFocused) { @@ -146,13 +157,26 @@ public abstract class MediaOutputBaseAdapter extends mTitleText.setTranslationY(0); mTitleText.setText(title); if (bFocused) { - mTitleText.setTypeface(Typeface.create(FONT_SELECTED_TITLE, Typeface.NORMAL)); + mTitleText.setTypeface(Typeface.create(mContext.getString( + com.android.internal.R.string.config_headlineFontFamilyMedium), + Typeface.NORMAL)); } else { - mTitleText.setTypeface(Typeface.create(FONT_TITLE, Typeface.NORMAL)); + mTitleText.setTypeface(Typeface.create(mContext.getString( + com.android.internal.R.string.config_headlineFontFamily), Typeface.NORMAL)); } } - void setTwoLineLayout(MediaDevice device, CharSequence title, boolean bFocused, + void setTwoLineLayout(MediaDevice device, boolean bFocused, boolean showSeekBar, + boolean showProgressBar, boolean showSubtitle) { + setTwoLineLayout(device, null, bFocused, showSeekBar, showProgressBar, showSubtitle); + } + + void setTwoLineLayout(CharSequence title, boolean bFocused, boolean showSeekBar, + boolean showProgressBar, boolean showSubtitle) { + setTwoLineLayout(null, title, bFocused, showSeekBar, showProgressBar, showSubtitle); + } + + private void setTwoLineLayout(MediaDevice device, CharSequence title, boolean bFocused, boolean showSeekBar, boolean showProgressBar, boolean showSubtitle) { mTitleText.setVisibility(View.GONE); mTwoLineLayout.setVisibility(View.VISIBLE); @@ -168,18 +192,21 @@ public abstract class MediaOutputBaseAdapter extends } if (bFocused) { - mTwoLineTitleText.setTypeface(Typeface.create(FONT_SELECTED_TITLE, + mTwoLineTitleText.setTypeface(Typeface.create(mContext.getString( + com.android.internal.R.string.config_headlineFontFamilyMedium), Typeface.NORMAL)); } else { - mTwoLineTitleText.setTypeface(Typeface.create(FONT_TITLE, Typeface.NORMAL)); + mTwoLineTitleText.setTypeface(Typeface.create(mContext.getString( + com.android.internal.R.string.config_headlineFontFamily), Typeface.NORMAL)); } } void initSeekbar(MediaDevice device) { mSeekBar.setMax(device.getMaxVolume()); mSeekBar.setMin(0); - if (mSeekBar.getProgress() != device.getCurrentVolume()) { - mSeekBar.setProgress(device.getCurrentVolume()); + final int currentVolume = device.getCurrentVolume(); + if (mSeekBar.getProgress() != currentVolume) { + mSeekBar.setProgress(currentVolume); } mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override @@ -202,6 +229,34 @@ public abstract class MediaOutputBaseAdapter extends }); } + void initSessionSeekbar() { + mSeekBar.setMax(mController.getSessionVolumeMax()); + mSeekBar.setMin(0); + final int currentVolume = mController.getSessionVolume(); + if (mSeekBar.getProgress() != currentVolume) { + mSeekBar.setProgress(currentVolume); + } + mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (!fromUser) { + return; + } + mController.adjustSessionVolume(progress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + mIsDragging = true; + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + mIsDragging = false; + } + }); + } + void playSwitchingAnim(@NonNull View from, @NonNull View to) { final float delta = (float) (mContext.getResources().getDimensionPixelSize( R.dimen.media_output_dialog_title_anim_y_delta)); @@ -213,7 +268,9 @@ public abstract class MediaOutputBaseAdapter extends } mIsAnimating = true; // Animation for title text - toTitleText.setTypeface(Typeface.create(FONT_SELECTED_TITLE, Typeface.NORMAL)); + toTitleText.setTypeface(Typeface.create(mContext.getString( + com.android.internal.R.string.config_headlineFontFamilyMedium), + Typeface.NORMAL)); toTitleText.animate() .setDuration(ANIM_DURATION) .translationY(-delta) @@ -234,7 +291,9 @@ public abstract class MediaOutputBaseAdapter extends public void onAnimationEnd(Animator animation) { final TextView fromTitleText = from.requireViewById( R.id.two_line_title); - fromTitleText.setTypeface(Typeface.create(FONT_TITLE, Typeface.NORMAL)); + fromTitleText.setTypeface(Typeface.create(mContext.getString( + com.android.internal.R.string.config_headlineFontFamily), + Typeface.NORMAL)); fromTitleText.animate() .setDuration(ANIM_DURATION) .translationY(delta) @@ -249,5 +308,15 @@ public abstract class MediaOutputBaseAdapter extends } }); } + + Drawable getSpeakerDrawable() { + final Drawable drawable = mContext.getDrawable(R.drawable.ic_speaker_group_black_24dp) + .mutate(); + final ColorStateList list = mContext.getResources().getColorStateList( + R.color.advanced_icon_color, mContext.getTheme()); + drawable.setColorFilter(new PorterDuffColorFilter(list.getDefaultColor(), + PorterDuff.Mode.SRC_IN)); + return BluetoothUtils.buildAdvancedDrawable(mContext, drawable); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java index caef536961f1..78939dffb4fb 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java @@ -119,6 +119,8 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements // Init device list mDevicesRecyclerView.setLayoutManager(mLayoutManager); mDevicesRecyclerView.setAdapter(mAdapter); + // Init header icon + mHeaderIcon.setOnClickListener(v -> onHeaderIconClick()); // Init bottom buttons mDoneButton.setOnClickListener(v -> dismiss()); mStopButton.setOnClickListener(v -> { @@ -218,4 +220,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements dismiss(); } } + + void onHeaderIconClick() { + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index b1f1bda25961..451bd42bd053 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -16,13 +16,12 @@ package com.android.systemui.media.dialog; +import android.app.Notification; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; import android.media.MediaMetadata; import android.media.MediaRoute2Info; import android.media.RoutingSessionInfo; @@ -49,6 +48,8 @@ import com.android.settingslib.media.MediaOutputSliceConstants; import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.R; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.phone.ShadeController; import java.util.ArrayList; @@ -61,7 +62,7 @@ import javax.inject.Inject; /** * Controller for media output dialog */ -public class MediaOutputController implements LocalMediaManager.DeviceCallback{ +public class MediaOutputController implements LocalMediaManager.DeviceCallback { private static final String TAG = "MediaOutputController"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @@ -71,6 +72,9 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback{ private final MediaSessionManager mMediaSessionManager; private final ShadeController mShadeController; private final ActivityStarter mActivityStarter; + private final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>(); + private final boolean mAboveStatusbar; + private final NotificationEntryManager mNotificationEntryManager; @VisibleForTesting final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>(); @@ -82,13 +86,16 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback{ @Inject public MediaOutputController(@NonNull Context context, String packageName, - MediaSessionManager mediaSessionManager, LocalBluetoothManager - lbm, ShadeController shadeController, ActivityStarter starter) { + boolean aboveStatusbar, MediaSessionManager mediaSessionManager, LocalBluetoothManager + lbm, ShadeController shadeController, ActivityStarter starter, + NotificationEntryManager notificationEntryManager) { mContext = context; mPackageName = packageName; mMediaSessionManager = mediaSessionManager; mShadeController = shadeController; mActivityStarter = starter; + mAboveStatusbar = aboveStatusbar; + mNotificationEntryManager = notificationEntryManager; InfoMediaManager imm = new InfoMediaManager(mContext, packageName, null, lbm); mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName); } @@ -194,7 +201,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback{ if (DEBUG) { Log.d(TAG, "Media meta data does not contain icon information"); } - return getPackageIcon(); + return getNotificationIcon(); } IconCompat getDeviceIconCompat(MediaDevice device) { @@ -210,24 +217,20 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback{ return BluetoothUtils.createIconWithDrawable(drawable); } - private IconCompat getPackageIcon() { + IconCompat getNotificationIcon() { if (TextUtils.isEmpty(mPackageName)) { return null; } - try { - final Drawable drawable = mContext.getPackageManager().getApplicationIcon(mPackageName); - if (drawable instanceof BitmapDrawable) { - return IconCompat.createWithBitmap(((BitmapDrawable) drawable).getBitmap()); - } - final Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), - drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - final Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - drawable.draw(canvas); - return IconCompat.createWithBitmap(bitmap); - } catch (PackageManager.NameNotFoundException e) { - if (DEBUG) { - Log.e(TAG, "Package is not found. Unable to get package icon."); + for (NotificationEntry entry + : mNotificationEntryManager.getActiveNotificationsForCurrentUser()) { + final Notification notification = entry.getSbn().getNotification(); + if (notification.hasMediaSession() + && TextUtils.equals(entry.getSbn().getPackageName(), mPackageName)) { + final Icon icon = notification.getLargeIcon(); + if (icon == null) { + break; + } + return IconCompat.createFromIcon(icon); } } return null; @@ -271,6 +274,42 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback{ mMediaDevices.addAll(targetMediaDevices); } + List<MediaDevice> getGroupMediaDevices() { + final List<MediaDevice> selectedDevices = getSelectedMediaDevice(); + final List<MediaDevice> selectableDevices = getSelectableMediaDevice(); + if (mGroupMediaDevices.isEmpty()) { + mGroupMediaDevices.addAll(selectedDevices); + mGroupMediaDevices.addAll(selectableDevices); + return mGroupMediaDevices; + } + // To keep the same list order + final Collection<MediaDevice> sourceDevices = new ArrayList<>(); + final Collection<MediaDevice> targetMediaDevices = new ArrayList<>(); + sourceDevices.addAll(selectedDevices); + sourceDevices.addAll(selectableDevices); + for (MediaDevice originalDevice : mGroupMediaDevices) { + for (MediaDevice newDevice : sourceDevices) { + if (TextUtils.equals(originalDevice.getId(), newDevice.getId())) { + targetMediaDevices.add(newDevice); + sourceDevices.remove(newDevice); + break; + } + } + } + // Add new devices at the end of list if necessary + if (!sourceDevices.isEmpty()) { + targetMediaDevices.addAll(sourceDevices); + } + mGroupMediaDevices.clear(); + mGroupMediaDevices.addAll(targetMediaDevices); + + return mGroupMediaDevices; + } + + void resetGroupMediaDevices() { + mGroupMediaDevices.clear(); + } + void connectDevice(MediaDevice device) { ThreadUtils.postOnBackgroundThread(() -> { mLocalMediaManager.connectDevice(device); @@ -309,15 +348,6 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback{ return mLocalMediaManager.getDeselectableMediaDevice(); } - boolean isDeviceIncluded(Collection<MediaDevice> deviceCollection, MediaDevice targetDevice) { - for (MediaDevice device : deviceCollection) { - if (TextUtils.equals(device.getId(), targetDevice.getId())) { - return true; - } - } - return false; - } - void adjustSessionVolume(String sessionId, int volume) { mLocalMediaManager.adjustSessionVolume(sessionId, volume); } @@ -407,6 +437,16 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback{ mActivityStarter.dismissKeyguardThenExecute(postKeyguardAction, null, true); } + void launchMediaOutputDialog() { + mCallback.dismissDialog(); + new MediaOutputDialog(mContext, mAboveStatusbar, this); + } + + void launchMediaOutputGroupDialog() { + mCallback.dismissDialog(); + new MediaOutputGroupDialog(mContext, mAboveStatusbar, this); + } + boolean isActiveRemoteDevice(@NonNull MediaDevice device) { final List<String> features = device.getFeatures(); return (features.contains(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK) diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt index 4cdca4cbcf1e..7d1a7ced7472 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt @@ -20,6 +20,7 @@ import android.content.Context import android.media.session.MediaSessionManager import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.notification.NotificationEntryManager import com.android.systemui.statusbar.phone.ShadeController import javax.inject.Inject @@ -31,7 +32,8 @@ class MediaOutputDialogFactory @Inject constructor( private val mediaSessionManager: MediaSessionManager, private val lbm: LocalBluetoothManager?, private val shadeController: ShadeController, - private val starter: ActivityStarter + private val starter: ActivityStarter, + private val notificationEntryManager: NotificationEntryManager ) { companion object { var mediaOutputDialog: MediaOutputDialog? = null @@ -40,9 +42,8 @@ class MediaOutputDialogFactory @Inject constructor( /** Creates a [MediaOutputDialog] for the given package. */ fun create(packageName: String, aboveStatusBar: Boolean) { mediaOutputDialog?.dismiss() - - mediaOutputDialog = MediaOutputController(context, packageName, mediaSessionManager, lbm, - shadeController, starter).run { + mediaOutputDialog = MediaOutputController(context, packageName, aboveStatusBar, + mediaSessionManager, lbm, shadeController, starter, notificationEntryManager).run { MediaOutputDialog(context, aboveStatusBar, this) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java new file mode 100644 index 000000000000..24e076bb22f1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java @@ -0,0 +1,177 @@ +/* + * 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.media.dialog; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.Log; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; + +import com.android.settingslib.media.MediaDevice; +import com.android.systemui.R; + +import java.util.List; + +/** + * Adapter for media output dynamic group dialog. + */ +public class MediaOutputGroupAdapter extends MediaOutputBaseAdapter { + + private static final String TAG = "MediaOutputGroupAdapter"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private final List<MediaDevice> mGroupMediaDevices; + + public MediaOutputGroupAdapter(MediaOutputController controller) { + super(controller); + mGroupMediaDevices = controller.getGroupMediaDevices(); + } + + @Override + public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, + int viewType) { + super.onCreateViewHolder(viewGroup, viewType); + + return new GroupViewHolder(mHolderView); + } + + @Override + public void onBindViewHolder(@NonNull MediaDeviceBaseViewHolder viewHolder, int position) { + // Add "Group" + if (position == 0) { + viewHolder.onBind(CUSTOMIZED_ITEM_GROUP, true /* topMargin */, + false /* bottomMargin */); + return; + } + // Add available devices + final int newPosition = position - 1; + final int size = mGroupMediaDevices.size(); + if (newPosition < size) { + viewHolder.onBind(mGroupMediaDevices.get(newPosition), false /* topMargin */, + newPosition == (size - 1) /* bottomMargin */); + return; + } + if (DEBUG) { + Log.d(TAG, "Incorrect position: " + position); + } + } + + @Override + public int getItemCount() { + // Require extra item for group volume operation + return mGroupMediaDevices.size() + 1; + } + + @Override + CharSequence getItemTitle(MediaDevice device) { + return super.getItemTitle(device); + } + + class GroupViewHolder extends MediaDeviceBaseViewHolder { + + GroupViewHolder(View view) { + super(view); + } + + @Override + void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin) { + super.onBind(device, topMargin, bottomMargin); + mDivider.setVisibility(View.GONE); + mAddIcon.setVisibility(View.GONE); + mBottomDivider.setVisibility(View.GONE); + mCheckBox.setVisibility(View.VISIBLE); + mCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> { + onCheckBoxClicked(isChecked, device); + }); + setTwoLineLayout(device, false /* bFocused */, true /* showSeekBar */, + false /* showProgressBar */, false /* showSubtitle*/); + initSeekbar(device); + final List<MediaDevice> selectedDevices = mController.getSelectedMediaDevice(); + if (isDeviceIncluded(mController.getSelectableMediaDevice(), device)) { + mCheckBox.setButtonDrawable(R.drawable.ic_check_box); + mCheckBox.setChecked(false); + mCheckBox.setEnabled(true); + } else if (isDeviceIncluded(selectedDevices, device)) { + if (selectedDevices.size() == 1 || !isDeviceIncluded( + mController.getDeselectableMediaDevice(), device)) { + mCheckBox.setButtonDrawable(getDisabledCheckboxDrawable()); + mCheckBox.setChecked(true); + mCheckBox.setEnabled(false); + } else { + mCheckBox.setButtonDrawable(R.drawable.ic_check_box); + mCheckBox.setChecked(true); + mCheckBox.setEnabled(true); + } + } + } + + @Override + void onBind(int customizedItem, boolean topMargin, boolean bottomMargin) { + super.onBind(customizedItem, topMargin, bottomMargin); + if (customizedItem == CUSTOMIZED_ITEM_GROUP) { + setTwoLineLayout(mContext.getText(R.string.media_output_dialog_group), + true /* bFocused */, true /* showSeekBar */, false /* showProgressBar */, + false /* showSubtitle*/); + mTitleIcon.setImageDrawable(getSpeakerDrawable()); + mBottomDivider.setVisibility(View.VISIBLE); + mCheckBox.setVisibility(View.GONE); + mDivider.setVisibility(View.GONE); + mAddIcon.setVisibility(View.GONE); + initSessionSeekbar(); + } + } + + private void onCheckBoxClicked(boolean isChecked, MediaDevice device) { + if (isChecked && isDeviceIncluded(mController.getSelectableMediaDevice(), device)) { + mController.addDeviceToPlayMedia(device); + } else if (!isChecked && isDeviceIncluded(mController.getDeselectableMediaDevice(), + device)) { + mController.removeDeviceFromPlayMedia(device); + } + } + + private Drawable getDisabledCheckboxDrawable() { + final Drawable drawable = mContext.getDrawable(R.drawable.ic_check_box_blue_24dp) + .mutate(); + final Bitmap checkbox = Bitmap.createBitmap(drawable.getIntrinsicWidth(), + drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(checkbox); + TypedValue value = new TypedValue(); + mContext.getTheme().resolveAttribute(android.R.attr.disabledAlpha, value, true); + drawable.setAlpha((int) (value.getFloat() * 255)); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + + return drawable; + } + + private boolean isDeviceIncluded(List<MediaDevice> deviceList, MediaDevice targetDevice) { + for (MediaDevice device : deviceList) { + if (TextUtils.equals(device.getId(), targetDevice.getId())) { + return true; + } + } + return false; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java new file mode 100644 index 000000000000..407930492fbe --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java @@ -0,0 +1,88 @@ +/* + * 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.media.dialog; + +import android.content.Context; +import android.os.Bundle; +import android.view.View; +import android.view.WindowManager; + +import androidx.core.graphics.drawable.IconCompat; + +import com.android.systemui.R; + +/** + * Dialog for media output group. + */ +public class MediaOutputGroupDialog extends MediaOutputBaseDialog { + + MediaOutputGroupDialog(Context context, boolean aboveStatusbar, MediaOutputController + mediaOutputController) { + super(context, mediaOutputController); + mMediaOutputController.resetGroupMediaDevices(); + mAdapter = new MediaOutputGroupAdapter(mMediaOutputController); + if (!aboveStatusbar) { + getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); + } + show(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + int getHeaderIconRes() { + return R.drawable.ic_arrow_back; + } + + @Override + IconCompat getHeaderIcon() { + return null; + } + + @Override + int getHeaderIconSize() { + return mContext.getResources().getDimensionPixelSize( + R.dimen.media_output_dialog_header_back_icon_size); + } + + @Override + CharSequence getHeaderText() { + return mContext.getString(R.string.media_output_dialog_add_output); + } + + @Override + CharSequence getHeaderSubtitle() { + final int size = mMediaOutputController.getSelectedMediaDevice().size(); + if (size == 1) { + return mContext.getText(R.string.media_output_dialog_single_device); + } + return mContext.getString(R.string.media_output_dialog_multiple_devices, size); + } + + @Override + int getStopButtonVisibility() { + return View.VISIBLE; + } + + @Override + void onHeaderIconClick() { + mMediaOutputController.launchMediaOutputDialog(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 18cc746666d8..f9982d04e04b 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -137,7 +137,9 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa && (properties.getKeyset().contains( SystemUiDeviceConfigFlags.BACK_GESTURE_ML_MODEL_THRESHOLD) || properties.getKeyset().contains( - SystemUiDeviceConfigFlags.USE_BACK_GESTURE_ML_MODEL))) { + SystemUiDeviceConfigFlags.USE_BACK_GESTURE_ML_MODEL) + || properties.getKeyset().contains( + SystemUiDeviceConfigFlags.BACK_GESTURE_ML_MODEL_NAME))) { updateMLModelState(); } } @@ -483,14 +485,15 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa private void updateMLModelState() { boolean newState = mIsEnabled && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, SystemUiDeviceConfigFlags.USE_BACK_GESTURE_ML_MODEL, false); - if (newState == mUseMLModel) { return; } if (newState) { + String mlModelName = DeviceConfig.getString(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.BACK_GESTURE_ML_MODEL_NAME, "backgesture"); mBackGestureTfClassifierProvider = SystemUIFactory.getInstance() - .createBackGestureTfClassifierProvider(mContext.getAssets()); + .createBackGestureTfClassifierProvider(mContext.getAssets(), mlModelName); mMLModelThreshold = DeviceConfig.getFloat(DeviceConfig.NAMESPACE_SYSTEMUI, SystemUiDeviceConfigFlags.BACK_GESTURE_ML_MODEL_THRESHOLD, 0.9f); if (mBackGestureTfClassifierProvider.isActive()) { @@ -540,21 +543,22 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa boolean withinRange = false; float results = -1; + // Disallow if we are in the bottom gesture area + if (y >= (mDisplaySize.y - mBottomGestureHeight)) { + return false; + } + // If the point is way too far (twice the margin), it is + // not interesting to us for logging purposes, nor we + // should process it. Simply return false and keep + // mLogGesture = false. + if (x > 2 * (mEdgeWidthLeft + mLeftInset) + && x < (mDisplaySize.x - 2 * (mEdgeWidthRight + mRightInset))) { + return false; + } + if (mUseMLModel && (results = getBackGesturePredictionsCategory(x, y)) != -1) { withinRange = results == 1 ? true : false; } else { - // Disallow if we are in the bottom gesture area - if (y >= (mDisplaySize.y - mBottomGestureHeight)) { - return false; - } - // If the point is way too far (twice the margin), it is - // not interesting to us for logging purposes, nor we - // should process it. Simply return false and keep - // mLogGesture = false. - if (x > 2 * (mEdgeWidthLeft + mLeftInset) - && x < (mDisplaySize.x - 2 * (mEdgeWidthRight + mRightInset))) { - return false; - } // Denotes whether we should proceed with the gesture. // Even if it is false, we may want to log it assuming // it is not invalid due to exclusion. diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java index eeb93bb7d766..6a78c64638aa 100644 --- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java @@ -23,22 +23,15 @@ import android.content.Context; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; -import android.icu.text.MeasureFormat; -import android.icu.util.Measure; -import android.icu.util.MeasureUnit; import android.os.Bundle; import android.os.ServiceManager; import android.os.UserHandle; -import android.service.notification.ConversationChannelWrapper; import android.util.Log; import android.view.ViewGroup; import com.android.systemui.R; -import java.time.Duration; import java.util.List; -import java.util.Locale; -import java.util.stream.Collectors; /** * Shows the user their tiles for their priority People (go/live-status). @@ -52,7 +45,6 @@ public class PeopleSpaceActivity extends Activity { private INotificationManager mNotificationManager; private PackageManager mPackageManager; private LauncherApps mLauncherApps; - private List<ConversationChannelWrapper> mConversations; private Context mContext; @Override @@ -77,15 +69,13 @@ public class PeopleSpaceActivity extends Activity { */ private void setTileViewsWithPriorityConversations() { try { - List<ConversationChannelWrapper> conversations = - mNotificationManager.getConversations( - true /* priority only */).getList(); - mConversations = conversations.stream().filter( - c -> shouldKeepConversation(c)).collect(Collectors.toList()); - for (ConversationChannelWrapper conversation : mConversations) { + List<ShortcutInfo> shortcutInfos = + PeopleSpaceUtils.getShortcutInfos( + mContext, mNotificationManager, mPeopleManager); + for (ShortcutInfo conversation : shortcutInfos) { PeopleSpaceTileView tileView = new PeopleSpaceTileView(mContext, mPeopleSpaceLayout, - conversation.getShortcutInfo().getId()); + conversation.getId()); setTileView(tileView, conversation); } } catch (Exception e) { @@ -95,11 +85,10 @@ public class PeopleSpaceActivity extends Activity { /** Sets {@code tileView} with the data in {@code conversation}. */ private void setTileView(PeopleSpaceTileView tileView, - ConversationChannelWrapper conversation) { + ShortcutInfo shortcutInfo) { try { - ShortcutInfo shortcutInfo = conversation.getShortcutInfo(); int userId = UserHandle.getUserHandleForUid( - conversation.getUid()).getIdentifier(); + shortcutInfo.getUserId()).getIdentifier(); String pkg = shortcutInfo.getPackage(); long lastInteraction = mPeopleManager.getLastInteraction( @@ -107,7 +96,7 @@ public class PeopleSpaceActivity extends Activity { shortcutInfo.getId()); String status = lastInteraction != 0l ? mContext.getString( R.string.last_interaction_status, - getLastInteractionString( + PeopleSpaceUtils.getLastInteractionString( lastInteraction)) : mContext.getString(R.string.basic_status); tileView.setStatus(status); @@ -120,46 +109,6 @@ public class PeopleSpaceActivity extends Activity { } } - /** Returns a readable representation of {@code lastInteraction}. */ - private String getLastInteractionString(long lastInteraction) { - long now = System.currentTimeMillis(); - Duration durationSinceLastInteraction = Duration.ofMillis( - now - lastInteraction); - MeasureFormat formatter = MeasureFormat.getInstance(Locale.getDefault(), - MeasureFormat.FormatWidth.WIDE); - if (durationSinceLastInteraction.toDays() >= 1) { - return - formatter - .formatMeasures(new Measure(durationSinceLastInteraction.toDays(), - MeasureUnit.DAY)); - } else if (durationSinceLastInteraction.toHours() >= 1) { - return formatter.formatMeasures(new Measure(durationSinceLastInteraction.toHours(), - MeasureUnit.HOUR)); - } else if (durationSinceLastInteraction.toMinutes() >= 1) { - return formatter.formatMeasures(new Measure(durationSinceLastInteraction.toMinutes(), - MeasureUnit.MINUTE)); - } else { - return formatter.formatMeasures( - new Measure(durationSinceLastInteraction.toMillis() / 1000, - MeasureUnit.SECOND)); - } - } - - /** - * Returns whether the {@code conversation} should be kept for display in the People Space. - * - * <p>A valid {@code conversation} must: - * <ul> - * <li>Have a non-null {@link ShortcutInfo} - * <li>Have an associated label in the {@link ShortcutInfo} - * </ul> - * </li> - */ - private boolean shouldKeepConversation(ConversationChannelWrapper conversation) { - ShortcutInfo shortcutInfo = conversation.getShortcutInfo(); - return shortcutInfo != null && shortcutInfo.getLabel().length() != 0; - } - @Override protected void onResume() { super.onResume(); diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java new file mode 100644 index 000000000000..1a9dd712bd0e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java @@ -0,0 +1,141 @@ +/* + * 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.people; + +import android.app.INotificationManager; +import android.app.people.ConversationChannel; +import android.app.people.IPeopleManager; +import android.content.Context; +import android.content.pm.ShortcutInfo; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.icu.text.MeasureFormat; +import android.icu.util.Measure; +import android.icu.util.MeasureUnit; +import android.provider.Settings; +import android.service.notification.ConversationChannelWrapper; + +import java.time.Duration; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +/** Utils class for People Space. */ +public class PeopleSpaceUtils { + private static final String TAG = "PeopleSpaceUtils"; + + /** Turns on debugging information about People Space. */ + public static final boolean DEBUG = true; + + /** Returns a list of {@link ShortcutInfo} corresponding to user's conversations. */ + public static List<ShortcutInfo> getShortcutInfos( + Context context, + INotificationManager notificationManager, + IPeopleManager peopleManager + ) throws Exception { + boolean showAllConversations = Settings.Global.getInt(context.getContentResolver(), + Settings.Global.PEOPLE_SPACE_CONVERSATION_TYPE, 0) == 0; + List<ConversationChannelWrapper> conversations = + notificationManager.getConversations( + !showAllConversations /* priority only */).getList(); + List<ShortcutInfo> shortcutInfos = conversations.stream().filter( + c -> shouldKeepConversation(c)).map( + c -> c.getShortcutInfo()).collect(Collectors.toList()); + if (showAllConversations) { + List<ConversationChannel> recentConversations = + peopleManager.getRecentConversations().getList(); + List<ShortcutInfo> recentShortcuts = recentConversations.stream().map( + c -> c.getShortcutInfo()).collect(Collectors.toList()); + shortcutInfos.addAll(recentShortcuts); + } + return shortcutInfos; + } + + /** Converts {@code drawable} to a {@link Bitmap}. */ + public static Bitmap convertDrawableToBitmap(Drawable drawable) { + if (drawable == null) { + return null; + } + + if (drawable instanceof BitmapDrawable) { + BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; + if (bitmapDrawable.getBitmap() != null) { + return bitmapDrawable.getBitmap(); + } + } + + Bitmap bitmap; + if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { + bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + // Single color bitmap will be created of 1x1 pixel + } else { + bitmap = Bitmap.createBitmap( + drawable.getIntrinsicWidth(), + drawable.getIntrinsicHeight(), + Bitmap.Config.ARGB_8888 + ); + } + + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } + + /** Returns a readable representation of {@code lastInteraction}. */ + public static String getLastInteractionString(long lastInteraction) { + long now = System.currentTimeMillis(); + Duration durationSinceLastInteraction = Duration.ofMillis( + now - lastInteraction); + MeasureFormat formatter = MeasureFormat.getInstance(Locale.getDefault(), + MeasureFormat.FormatWidth.WIDE); + if (durationSinceLastInteraction.toDays() >= 1) { + return + formatter + .formatMeasures(new Measure(durationSinceLastInteraction.toDays(), + MeasureUnit.DAY)); + } else if (durationSinceLastInteraction.toHours() >= 1) { + return formatter.formatMeasures(new Measure(durationSinceLastInteraction.toHours(), + MeasureUnit.HOUR)); + } else if (durationSinceLastInteraction.toMinutes() >= 1) { + return formatter.formatMeasures(new Measure(durationSinceLastInteraction.toMinutes(), + MeasureUnit.MINUTE)); + } else { + return formatter.formatMeasures( + new Measure(durationSinceLastInteraction.toMillis() / 1000, + MeasureUnit.SECOND)); + } + } + + /** + * Returns whether the {@code conversation} should be kept for display in the People Space. + * + * <p>A valid {@code conversation} must: + * <ul> + * <li>Have a non-null {@link ShortcutInfo} + * <li>Have an associated label in the {@link ShortcutInfo} + * </ul> + * </li> + */ + public static boolean shouldKeepConversation(ConversationChannelWrapper conversation) { + ShortcutInfo shortcutInfo = conversation.getShortcutInfo(); + return shortcutInfo != null && shortcutInfo.getLabel().length() != 0; + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/LaunchConversationActivity.java b/packages/SystemUI/src/com/android/systemui/people/widget/LaunchConversationActivity.java new file mode 100644 index 000000000000..44f173bc5175 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/people/widget/LaunchConversationActivity.java @@ -0,0 +1,59 @@ +/* + * 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.people.widget; + +import android.app.Activity; +import android.content.Intent; +import android.content.pm.LauncherApps; +import android.content.pm.ShortcutInfo; +import android.os.Bundle; +import android.util.Log; + +import com.android.systemui.people.PeopleSpaceUtils; + +/** Proxy activity to launch ShortcutInfo's conversation. */ +public class LaunchConversationActivity extends Activity { + private static final String TAG = "PeopleSpaceLaunchConv"; + private static final boolean DEBUG = PeopleSpaceUtils.DEBUG; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (DEBUG) Log.d(TAG, "onCreate called"); + + Intent intent = getIntent(); + ShortcutInfo shortcutInfo = (ShortcutInfo) intent.getParcelableExtra( + PeopleSpaceWidgetProvider.EXTRA_SHORTCUT_INFO + ); + if (shortcutInfo != null) { + if (DEBUG) { + Log.d(TAG, "Launching conversation with shortcutInfo id " + shortcutInfo.getId()); + } + try { + LauncherApps launcherApps = + getApplicationContext().getSystemService(LauncherApps.class); + launcherApps.startShortcut( + shortcutInfo, null, null); + } catch (Exception e) { + Log.e(TAG, "Exception starting shortcut:" + e); + } + } else { + if (DEBUG) Log.d(TAG, "Trying to launch conversation with null shortcutInfo."); + } + finish(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetProvider.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetProvider.java new file mode 100644 index 000000000000..aa98b61ff947 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetProvider.java @@ -0,0 +1,69 @@ +/* + * 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.people.widget; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.widget.RemoteViews; + +import com.android.systemui.R; +import com.android.systemui.people.PeopleSpaceUtils; + +/** People Space Widget Provider class. */ +public class PeopleSpaceWidgetProvider extends AppWidgetProvider { + private static final String TAG = "PeopleSpaceWidgetPvd"; + private static final boolean DEBUG = PeopleSpaceUtils.DEBUG; + + public static final String EXTRA_SHORTCUT_INFO = "extra_shortcut_info"; + + /** Called when widget updates. */ + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + super.onUpdate(context, appWidgetManager, appWidgetIds); + + if (DEBUG) Log.d(TAG, "onUpdate called"); + // Perform this loop procedure for each App Widget that belongs to this provider + for (int appWidgetId : appWidgetIds) { + RemoteViews views = + new RemoteViews(context.getPackageName(), R.layout.people_space_widget); + + Intent intent = new Intent(context, PeopleSpaceWidgetService.class); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + views.setRemoteAdapter(R.id.widget_list_view, intent); + + Intent activityIntent = new Intent(context, LaunchConversationActivity.class); + activityIntent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TASK + | Intent.FLAG_ACTIVITY_NO_HISTORY + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + PendingIntent pendingIntent = PendingIntent.getActivity( + context, + appWidgetId, + activityIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + views.setPendingIntentTemplate(R.id.widget_list_view, pendingIntent); + + // Tell the AppWidgetManager to perform an update on the current app widget + appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_list_view); + appWidgetManager.updateAppWidget(appWidgetId, views); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetRemoteViewsFactory.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetRemoteViewsFactory.java new file mode 100644 index 000000000000..c68c30632b6c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetRemoteViewsFactory.java @@ -0,0 +1,162 @@ +/* + * 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.people.widget; + +import android.app.INotificationManager; +import android.app.people.IPeopleManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.LauncherApps; +import android.content.pm.PackageManager; +import android.content.pm.ShortcutInfo; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.util.Log; +import android.widget.RemoteViews; +import android.widget.RemoteViewsService; + +import com.android.systemui.R; +import com.android.systemui.people.PeopleSpaceTileView; +import com.android.systemui.people.PeopleSpaceUtils; + +import java.util.ArrayList; +import java.util.List; + +/** People Space Widget RemoteViewsFactory class. */ +public class PeopleSpaceWidgetRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory { + private static final String TAG = "PeopleSpaceWRVFactory"; + private static final boolean DEBUG = PeopleSpaceUtils.DEBUG; + + private IPeopleManager mPeopleManager; + private INotificationManager mNotificationManager; + private PackageManager mPackageManager; + private LauncherApps mLauncherApps; + private List<ShortcutInfo> mShortcutInfos = new ArrayList<>(); + private Context mContext; + + public PeopleSpaceWidgetRemoteViewsFactory(Context context, Intent intent) { + this.mContext = context; + } + + @Override + public void onCreate() { + if (DEBUG) Log.d(TAG, "onCreate called"); + mNotificationManager = + INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE)); + mPackageManager = mContext.getPackageManager(); + mPeopleManager = IPeopleManager.Stub.asInterface( + ServiceManager.getService(Context.PEOPLE_SERVICE)); + mLauncherApps = mContext.getSystemService(LauncherApps.class); + setTileViewsWithPriorityConversations(); + } + + /** + * Retrieves all priority conversations and sets a {@link PeopleSpaceTileView}s for each + * priority conversation. + */ + private void setTileViewsWithPriorityConversations() { + try { + mShortcutInfos = + PeopleSpaceUtils.getShortcutInfos( + mContext, mNotificationManager, mPeopleManager); + } catch (Exception e) { + Log.e(TAG, "Couldn't retrieve conversations", e); + } + } + + @Override + public void onDataSetChanged() { + if (DEBUG) Log.d(TAG, "onDataSetChanged called"); + setTileViewsWithPriorityConversations(); + } + + @Override + public void onDestroy() { + mShortcutInfos.clear(); + } + + @Override + public int getCount() { + return mShortcutInfos.size(); + } + + @Override + public RemoteViews getViewAt(int i) { + if (DEBUG) Log.d(TAG, "getViewAt called, index: " + i); + + RemoteViews personView = + new RemoteViews(mContext.getPackageName(), R.layout.people_space_widget_item); + try { + ShortcutInfo shortcutInfo = mShortcutInfos.get(i); + int userId = UserHandle.getUserHandleForUid( + shortcutInfo.getUserId()).getIdentifier(); + String pkg = shortcutInfo.getPackage(); + long lastInteraction = mPeopleManager.getLastInteraction( + pkg, userId, + shortcutInfo.getId()); + + String status = lastInteraction != 0L ? mContext.getString( + R.string.last_interaction_status, + PeopleSpaceUtils.getLastInteractionString( + lastInteraction)) : mContext.getString(R.string.basic_status); + + personView.setTextViewText(R.id.status, status); + personView.setTextViewText(R.id.name, shortcutInfo.getLabel().toString()); + + personView.setImageViewBitmap( + R.id.package_icon, + PeopleSpaceUtils.convertDrawableToBitmap( + mPackageManager.getApplicationIcon(pkg) + ) + ); + personView.setImageViewBitmap( + R.id.person_icon, + PeopleSpaceUtils.convertDrawableToBitmap( + mLauncherApps.getShortcutIconDrawable(shortcutInfo, 0) + ) + ); + + Intent fillInIntent = new Intent(); + fillInIntent.putExtra(PeopleSpaceWidgetProvider.EXTRA_SHORTCUT_INFO, shortcutInfo); + personView.setOnClickFillInIntent(R.id.item, fillInIntent); + } catch (Exception e) { + Log.e(TAG, "Couldn't retrieve shortcut information", e); + } + return personView; + } + + @Override + public RemoteViews getLoadingView() { + return null; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public long getItemId(int i) { + return i; + } + + @Override + public boolean hasStableIds() { + return true; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetService.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetService.java new file mode 100644 index 000000000000..c0e43473d069 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetService.java @@ -0,0 +1,34 @@ +/* + * 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.people.widget; +import android.content.Intent; +import android.util.Log; +import android.widget.RemoteViewsService; + +import com.android.systemui.people.PeopleSpaceUtils; + +/** People Space Widget Service class. */ +public class PeopleSpaceWidgetService extends RemoteViewsService { + private static final String TAG = "PeopleSpaceWidgetSvc"; + private static final boolean DEBUG = PeopleSpaceUtils.DEBUG; + + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) { + if (DEBUG) Log.d(TAG, "onGetViewFactory called"); + return new PeopleSpaceWidgetRemoteViewsFactory(this.getApplicationContext(), intent); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt index 3da1363f2a56..7359e79b26f5 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt @@ -19,20 +19,31 @@ import com.android.systemui.R typealias Privacy = PrivacyType -enum class PrivacyType(val nameId: Int, val iconId: Int) { +enum class PrivacyType(val nameId: Int, val iconId: Int, val logName: String) { // This is uses the icons used by the corresponding permission groups in the AndroidManifest - TYPE_CAMERA(R.string.privacy_type_camera, - com.android.internal.R.drawable.perm_group_camera), - TYPE_MICROPHONE(R.string.privacy_type_microphone, - com.android.internal.R.drawable.perm_group_microphone), - TYPE_LOCATION(R.string.privacy_type_location, - com.android.internal.R.drawable.perm_group_location); + TYPE_CAMERA( + R.string.privacy_type_camera, + com.android.internal.R.drawable.perm_group_camera, + "camera" + ), + TYPE_MICROPHONE( + R.string.privacy_type_microphone, + com.android.internal.R.drawable.perm_group_microphone, + "microphone" + ), + TYPE_LOCATION( + R.string.privacy_type_location, + com.android.internal.R.drawable.perm_group_location, + "location" + ); fun getName(context: Context) = context.resources.getString(nameId) fun getIcon(context: Context) = context.resources.getDrawable(iconId, context.theme) } -data class PrivacyItem(val privacyType: PrivacyType, val application: PrivacyApplication) +data class PrivacyItem(val privacyType: PrivacyType, val application: PrivacyApplication) { + fun toLog(): String = "(${privacyType.logName}, ${application.packageName}(${application.uid}))" +} data class PrivacyApplication(val packageName: String, val uid: Int) diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt index f56e6cdf5cb7..87ffbd465109 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt @@ -32,6 +32,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.privacy.logging.PrivacyLogger import com.android.systemui.settings.UserTracker import com.android.systemui.util.DeviceConfigProxy import com.android.systemui.util.concurrency.DelayableExecutor @@ -48,6 +49,7 @@ class PrivacyItemController @Inject constructor( @Background private val bgExecutor: Executor, private val deviceConfigProxy: DeviceConfigProxy, private val userTracker: UserTracker, + private val logger: PrivacyLogger, dumpManager: DumpManager ) : Dumpable { @@ -69,8 +71,10 @@ class PrivacyItemController @Inject constructor( private const val ALL_INDICATORS = SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED private const val MIC_CAMERA = SystemUiDeviceConfigFlags.PROPERTY_MIC_CAMERA_ENABLED + private const val LOCATION = SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_ENABLED private const val DEFAULT_ALL_INDICATORS = false private const val DEFAULT_MIC_CAMERA = true + private const val DEFAULT_LOCATION = false } @VisibleForTesting @@ -88,6 +92,11 @@ class PrivacyItemController @Inject constructor( return true } + private fun isLocationEnabled(): Boolean { + return deviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, + LOCATION, DEFAULT_LOCATION) + } + private var currentUserIds = emptyList<Int>() private var listening = false private val callbacks = mutableListOf<WeakReference<Callback>>() @@ -107,13 +116,15 @@ class PrivacyItemController @Inject constructor( private set var micCameraAvailable = isMicCameraEnabled() private set + var locationAvailable = isLocationEnabled() private val devicePropertiesChangedListener = object : DeviceConfig.OnPropertiesChangedListener { override fun onPropertiesChanged(properties: DeviceConfig.Properties) { if (DeviceConfig.NAMESPACE_PRIVACY.equals(properties.getNamespace()) && (properties.keyset.contains(ALL_INDICATORS) || - properties.keyset.contains(MIC_CAMERA))) { + properties.keyset.contains(MIC_CAMERA) || + properties.keyset.contains(LOCATION))) { // Running on the ui executor so can iterate on callbacks if (properties.keyset.contains(ALL_INDICATORS)) { @@ -126,6 +137,10 @@ class PrivacyItemController @Inject constructor( // micCameraAvailable = properties.getBoolean(MIC_CAMERA, DEFAULT_MIC_CAMERA) // callbacks.forEach { it.get()?.onFlagMicCameraChanged(micCameraAvailable) } // } + if (properties.keyset.contains(LOCATION)) { + locationAvailable = properties.getBoolean(LOCATION, DEFAULT_LOCATION) + callbacks.forEach { it.get()?.onFlagLocationChanged(locationAvailable) } + } internalUiExecutor.updateListeningState() } } @@ -139,11 +154,13 @@ class PrivacyItemController @Inject constructor( active: Boolean ) { // Check if we care about this code right now - if (!allIndicatorsAvailable && code in OPS_LOCATION) { + if (!allIndicatorsAvailable && + (code in OPS_LOCATION && !locationAvailable)) { return } val userId = UserHandle.getUserId(uid) if (userId in currentUserIds) { + logger.logUpdatedItemFromAppOps(code, uid, packageName, active) update(false) } } @@ -180,6 +197,7 @@ class PrivacyItemController @Inject constructor( bgExecutor.execute { if (updateUsers) { currentUserIds = userTracker.userProfiles.map { it.id } + logger.logCurrentProfilesChanged(currentUserIds) } updateListAndNotifyChanges.run() } @@ -195,7 +213,8 @@ class PrivacyItemController @Inject constructor( * main thread. */ private fun setListeningState() { - val listen = !callbacks.isEmpty() and (allIndicatorsAvailable || micCameraAvailable) + val listen = !callbacks.isEmpty() and + (allIndicatorsAvailable || micCameraAvailable || locationAvailable) if (listening == listen) return listening = listen if (listening) { @@ -245,6 +264,8 @@ class PrivacyItemController @Inject constructor( } val list = currentUserIds.flatMap { appOpsController.getActiveAppOpsForUser(it) } .mapNotNull { toPrivacyItem(it) }.distinct() + logger.logUpdatedPrivacyItemsList( + list.joinToString(separator = ", ", transform = PrivacyItem::toLog)) privacyList = list } @@ -258,7 +279,9 @@ class PrivacyItemController @Inject constructor( AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE else -> return null } - if (type == PrivacyType.TYPE_LOCATION && !allIndicatorsAvailable) return null + if (type == PrivacyType.TYPE_LOCATION && (!allIndicatorsAvailable && !locationAvailable)) { + return null + } val app = PrivacyApplication(appOpItem.packageName, appOpItem.uid) return PrivacyItem(type, app) } @@ -271,6 +294,9 @@ class PrivacyItemController @Inject constructor( @JvmDefault fun onFlagMicCameraChanged(flag: Boolean) {} + + @JvmDefault + fun onFlagLocationChanged(flag: Boolean) {} } private class NotifyChangesToCallback( diff --git a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt new file mode 100644 index 000000000000..c88676e713b3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt @@ -0,0 +1,87 @@ +/* + * 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.privacy.logging + +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogLevel +import com.android.systemui.log.LogMessage +import com.android.systemui.log.dagger.PrivacyLog +import javax.inject.Inject + +private const val TAG = "PrivacyLog" + +class PrivacyLogger @Inject constructor( + @PrivacyLog private val buffer: LogBuffer +) { + + fun logUpdatedItemFromAppOps(code: Int, uid: Int, packageName: String, active: Boolean) { + log(LogLevel.INFO, { + int1 = code + int2 = uid + str1 = packageName + bool1 = active + }, { + "App Op: $int1 for $str1($int2), active=$bool1" + }) + } + + fun logUpdatedPrivacyItemsList(listAsString: String) { + log(LogLevel.INFO, { + str1 = listAsString + }, { + "Updated list: $str1" + }) + } + + fun logCurrentProfilesChanged(profiles: List<Int>) { + log(LogLevel.INFO, { + str1 = profiles.toString() + }, { + "Profiles changed: $str1" + }) + } + + fun logChipVisible(visible: Boolean) { + log(LogLevel.INFO, { + bool1 = visible + }, { + "Chip visible: $bool1" + }) + } + + fun logStatusBarIconsVisible( + showCamera: Boolean, + showMichrophone: Boolean, + showLocation: Boolean + ) { + log(LogLevel.INFO, { + bool1 = showCamera + bool2 = showMichrophone + bool3 = showLocation + }, { + "Status bar icons visible: camera=$bool1, microphone=$bool2, location=$bool3" + }) + } + + private inline fun log( + logLevel: LogLevel, + initializer: LogMessage.() -> Unit, + noinline printer: LogMessage.() -> String + ) { + buffer.log(TAG, logLevel, initializer, printer) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt index dc157a8dd257..6ac1e7079531 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt @@ -33,7 +33,7 @@ class DoubleLineTileLayout( private const val NUM_LINES = 2 } - protected val mRecords = ArrayList<QSPanel.TileRecord>() + protected val mRecords = ArrayList<QSPanelControllerBase.TileRecord>() private var _listening = false private var smallTileSize = 0 private val twoLineHeight @@ -50,17 +50,17 @@ class DoubleLineTileLayout( updateResources() } - override fun addTile(tile: QSPanel.TileRecord) { + override fun addTile(tile: QSPanelControllerBase.TileRecord) { mRecords.add(tile) tile.tile.setListening(this, _listening) addTileView(tile) } - protected fun addTileView(tile: QSPanel.TileRecord) { + protected fun addTileView(tile: QSPanelControllerBase.TileRecord) { addView(tile.tileView) } - override fun removeTile(tile: QSPanel.TileRecord) { + override fun removeTile(tile: QSPanelControllerBase.TileRecord) { mRecords.remove(tile) tile.tile.setListening(this, false) removeView(tile.tileView) @@ -72,7 +72,7 @@ class DoubleLineTileLayout( super.removeAllViews() } - override fun getOffsetTop(tile: QSPanel.TileRecord?) = top + override fun getOffsetTop(tile: QSPanelControllerBase.TileRecord?) = top override fun updateResources(): Boolean { with(mContext.resources) { @@ -99,7 +99,7 @@ class DoubleLineTileLayout( } } - override fun getNumVisibleTiles() = tilesToShow + override fun getNumVisibleTiles() = Math.min(mRecords.size, tilesToShow) override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java index 04f379ef35ea..3062a77bcbe1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java @@ -26,7 +26,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.qs.QSPanel.QSTileLayout; -import com.android.systemui.qs.QSPanel.TileRecord; +import com.android.systemui.qs.QSPanelControllerBase.TileRecord; import java.util.ArrayList; import java.util.Set; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java index 9dcc924f161e..4d4195063227 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java @@ -28,12 +28,17 @@ import com.android.systemui.qs.QSHost.Callback; import com.android.systemui.qs.QSPanel.QSTileLayout; import com.android.systemui.qs.TouchAnimator.Builder; import com.android.systemui.qs.TouchAnimator.Listener; +import com.android.systemui.qs.dagger.QSScope; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; import java.util.ArrayList; import java.util.Collection; +import javax.inject.Inject; + +/** */ +@QSScope public class QSAnimator implements Callback, PageListener, Listener, OnLayoutChangeListener, OnAttachStateChangeListener, Tunable { @@ -53,6 +58,9 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha private final ArrayList<View> mQuickQsViews = new ArrayList<>(); private final QuickQSPanel mQuickQsPanel; private final QSPanel mQsPanel; + private final QSPanelController mQsPanelController; + private final QuickQSPanelController mQuickQSPanelController; + private final QSSecurityFooter mSecurityFooter; private final QS mQs; private PagedTileLayout mPagedLayout; @@ -78,10 +86,19 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha private QSTileHost mHost; private boolean mShowCollapsedOnKeyguard; - public QSAnimator(QS qs, QuickQSPanel quickPanel, QSPanel panel) { + @Inject + public QSAnimator(QS qs, QuickQSPanel quickPanel, QSPanel panel, + QSPanelController qsPanelController, QuickQSPanelController quickQSPanelController, + QSTileHost qsTileHost, + QSSecurityFooter securityFooter) { mQs = qs; mQuickQsPanel = quickPanel; mQsPanel = panel; + mQsPanelController = qsPanelController; + mQuickQSPanelController = quickQSPanelController; + mSecurityFooter = securityFooter; + mHost = qsTileHost; + mHost.addCallback(this); mQsPanel.addOnAttachStateChangeListener(this); qs.getView().addOnLayoutChangeListener(this); if (mQsPanel.isAttachedToWindow()) { @@ -134,12 +151,6 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha && !mShowCollapsedOnKeyguard ? View.INVISIBLE : View.VISIBLE); } - public void setHost(QSTileHost qsh) { - mHost = qsh; - qsh.addCallback(this); - updateAnimators(); - } - @Override public void onViewAttachedToWindow(View v) { Dependency.get(TunerService.class).addTunable(this, ALLOW_FANCY_ANIMATION, @@ -148,9 +159,7 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha @Override public void onViewDetachedFromWindow(View v) { - if (mHost != null) { - mHost.removeCallback(this); - } + mHost.removeCallback(this); Dependency.get(TunerService.class).removeTunable(this); } @@ -185,8 +194,7 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha TouchAnimator.Builder translationXBuilder = new Builder(); TouchAnimator.Builder translationYBuilder = new Builder(); - if (mQsPanel.getHost() == null) return; - Collection<QSTile> tiles = mQsPanel.getHost().getTiles(); + Collection<QSTile> tiles = mHost.getTiles(); int count = 0; int[] loc1 = new int[2]; int[] loc2 = new int[2]; @@ -206,7 +214,7 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha firstPageBuilder.addFloat(tileLayout, "translationY", heightDiff, 0); for (QSTile tile : tiles) { - QSTileView tileView = mQsPanel.getTileView(tile); + QSTileView tileView = mQsPanelController.getTileView(tile); if (tileView == null) { Log.e(TAG, "tileView is null " + tile.getTileSpec()); continue; @@ -217,7 +225,7 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha // This case: less tiles to animate in small displays. if (count < mQuickQsPanel.getTileLayout().getNumVisibleTiles() && mAllowFancy) { // Quick tiles. - QSTileView quickTileView = mQuickQsPanel.getTileView(tile); + QSTileView quickTileView = mQuickQSPanelController.getTileView(tile); if (quickTileView == null) continue; lastX = loc1[0]; @@ -302,16 +310,12 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha // Fade in the security footer and the divider as we reach the final position builder = new Builder().setStartDelay(EXPANDED_TILE_DELAY); - if (mQsPanel.getSecurityFooter() != null) { - builder.addFloat(mQsPanel.getSecurityFooter().getView(), "alpha", 0, 1); - } + builder.addFloat(mSecurityFooter.getView(), "alpha", 0, 1); if (mQsPanel.getDivider() != null) { builder.addFloat(mQsPanel.getDivider(), "alpha", 0, 1); } mAllPagesDelayedAnimator = builder.build(); - if (mQsPanel.getSecurityFooter() != null) { - mAllViews.add(mQsPanel.getSecurityFooter().getView()); - } + mAllViews.add(mSecurityFooter.getView()); if (mQsPanel.getDivider() != null) { mAllViews.add(mQsPanel.getDivider()); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index 7e2433a1fd33..a35151068bee 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -71,8 +71,7 @@ public class QSContainerImpl extends FrameLayout { private int mSideMargins; private boolean mQsDisabled; - private int mContentPaddingStart = -1; - private int mContentPaddingEnd = -1; + private int mContentPadding = -1; private boolean mAnimateBottomOnNextLayout; public QSContainerImpl(Context context, AttributeSet attrs) { @@ -205,12 +204,10 @@ public class QSContainerImpl extends FrameLayout { mQSPanelContainer.setLayoutParams(layoutParams); mSideMargins = getResources().getDimensionPixelSize(R.dimen.notification_side_paddings); - mContentPaddingStart = getResources().getDimensionPixelSize( - com.android.internal.R.dimen.notification_content_margin_start); - int newPaddingEnd = getResources().getDimensionPixelSize( - com.android.internal.R.dimen.notification_content_margin_end); - boolean marginsChanged = newPaddingEnd != mContentPaddingEnd; - mContentPaddingEnd = newPaddingEnd; + int padding = getResources().getDimensionPixelSize( + R.dimen.notification_shade_content_margin_horizontal); + boolean marginsChanged = padding != mContentPadding; + mContentPadding = padding; if (marginsChanged) { updatePaddingsAndMargins(); } @@ -291,19 +288,19 @@ public class QSContainerImpl extends FrameLayout { lp.leftMargin = mSideMargins; if (view == mQSPanelContainer) { // QS panel lays out some of its content full width - mQSPanel.setContentMargins(mContentPaddingStart, mContentPaddingEnd); + mQSPanel.setContentMargins(mContentPadding, mContentPadding); Pair<Integer, Integer> margins = mQSPanel.getVisualSideMargins(); // Apply paddings based on QSPanel mQSCustomizer.setContentPaddings(margins.first, margins.second); } else if (view == mHeader) { // The header contains the QQS panel which needs to have special padding, to // visually align them. - mHeader.setContentMargins(mContentPaddingStart, mContentPaddingEnd); + mHeader.setContentMargins(mContentPadding, mContentPadding); } else { view.setPaddingRelative( - mContentPaddingStart, + mContentPadding, view.getPaddingTop(), - mContentPaddingEnd, + mContentPadding, view.getPaddingBottom()); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java index 1e239b1e9ec9..4b9f4316f2bf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java @@ -16,24 +16,25 @@ package com.android.systemui.qs; -import com.android.systemui.R; +import com.android.systemui.qs.dagger.QSScope; import com.android.systemui.util.ViewController; import javax.inject.Inject; -class QSContainerImplController extends ViewController<QSContainerImpl> { +/** */ +@QSScope +public class QSContainerImplController extends ViewController<QSContainerImpl> { private final QuickStatusBarHeaderController mQuickStatusBarHeaderController; - private QSContainerImplController(QSContainerImpl view, - QuickStatusBarHeaderController.Builder quickStatusBarHeaderControllerBuilder) { + @Inject + QSContainerImplController(QSContainerImpl view, + QuickStatusBarHeaderController quickStatusBarHeaderController) { super(view); - mQuickStatusBarHeaderController = quickStatusBarHeaderControllerBuilder - .setQuickStatusBarHeader(mView.findViewById(R.id.header)).build(); + mQuickStatusBarHeaderController = quickStatusBarHeaderController; } @Override - public void init() { - super.init(); + public void onInit() { mQuickStatusBarHeaderController.init(); } @@ -49,23 +50,7 @@ class QSContainerImplController extends ViewController<QSContainerImpl> { protected void onViewDetached() { } - static class Builder { - private final QuickStatusBarHeaderController.Builder mQuickStatusBarHeaderControllerBuilder; - private QSContainerImpl mView; - - @Inject - Builder( - QuickStatusBarHeaderController.Builder quickStatusBarHeaderControllerBuilder) { - mQuickStatusBarHeaderControllerBuilder = quickStatusBarHeaderControllerBuilder; - } - - public Builder setQSContainerImpl(QSContainerImpl view) { - mView = view; - return this; - } - - public QSContainerImplController build() { - return new QSContainerImplController(mView, mQuickStatusBarHeaderControllerBuilder); - } + public QSContainerImpl getView() { + return mView; } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java b/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java index 2be8a9704e1c..619729e55314 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java @@ -57,7 +57,7 @@ public class QSDetail extends LinearLayout { protected TextView mDetailDoneButton; private QSDetailClipper mClipper; private DetailAdapter mDetailAdapter; - private QSPanel mQsPanel; + private QSPanelController mQsPanelController; protected View mQsDetailHeader; protected TextView mQsDetailHeaderTitle; @@ -76,7 +76,7 @@ public class QSDetail extends LinearLayout { private int mOpenY; private boolean mAnimatingOpen; private boolean mSwitchState; - private View mFooter; + private QSFooter mFooter; public QSDetail(Context context, @Nullable AttributeSet attrs) { super(context, attrs); @@ -114,18 +114,20 @@ public class QSDetail extends LinearLayout { public void onClick(View v) { announceForAccessibility( mContext.getString(R.string.accessibility_desc_quick_settings)); - mQsPanel.closeDetail(); + mQsPanelController.closeDetail(); } }; mDetailDoneButton.setOnClickListener(doneListener); } - public void setQsPanel(QSPanel panel, QuickStatusBarHeader header, View footer) { - mQsPanel = panel; + /** */ + public void setQsPanel(QSPanelController panelController, QuickStatusBarHeader header, + QSFooter footer) { + mQsPanelController = panelController; mHeader = header; mFooter = footer; mHeader.setCallback(mQsPanelCallback); - mQsPanel.setCallback(mQsPanelCallback); + mQsPanelController.setCallback(mQsPanelCallback); } public void setHost(QSTileHost host) { @@ -220,7 +222,7 @@ public class QSDetail extends LinearLayout { listener = mTeardownDetailWhenDone; mHeader.setVisibility(View.VISIBLE); mFooter.setVisibility(View.VISIBLE); - mQsPanel.setGridContentVisibility(true); + mQsPanelController.setGridContentVisibility(true); mQsPanelCallback.onScanStateChanged(false); } sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); @@ -361,7 +363,7 @@ public class QSDetail extends LinearLayout { public void onAnimationEnd(Animator animation) { // Only hide content if still in detail state. if (mDetailAdapter != null) { - mQsPanel.setGridContentVisibility(false); + mQsPanelController.setGridContentVisibility(false); mHeader.setVisibility(View.INVISIBLE); mFooter.setVisibility(View.INVISIBLE); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java index 84563a078447..8b9dae14c809 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * 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. @@ -11,19 +11,14 @@ * 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 + * limitations under the License. */ package com.android.systemui.qs; import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS; -import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT; - -import android.content.ClipData; -import android.content.ClipboardManager; import android.content.Context; -import android.content.Intent; import android.content.res.Configuration; import android.database.ContentObserver; import android.graphics.PorterDuff.Mode; @@ -36,50 +31,27 @@ import android.os.Handler; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; -import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; -import android.view.View.OnClickListener; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; -import android.widget.Toast; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.nano.MetricsProto; -import com.android.keyguard.KeyguardUpdateMonitor; import com.android.settingslib.Utils; import com.android.settingslib.development.DevelopmentSettingsEnabler; import com.android.settingslib.drawable.UserIconDrawable; -import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.R.dimen; -import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.qs.TouchAnimator.Builder; -import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.MultiUserSwitch; import com.android.systemui.statusbar.phone.SettingsButton; -import com.android.systemui.statusbar.policy.DeviceProvisionedController; -import com.android.systemui.statusbar.policy.UserInfoController; -import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener; -import com.android.systemui.tuner.TunerService; - -import javax.inject.Inject; -import javax.inject.Named; - -public class QSFooterImpl extends FrameLayout implements QSFooter, - OnClickListener, OnUserInfoChangedListener { - private static final String TAG = "QSFooterImpl"; - - private final ActivityStarter mActivityStarter; - private final UserInfoController mUserInfoController; - private final DeviceProvisionedController mDeviceProvisionedController; - private final UserTracker mUserTracker; +/** */ +public class QSFooterView extends FrameLayout { private SettingsButton mSettingsButton; protected View mSettingsContainer; private PageIndicator mPageIndicator; @@ -87,7 +59,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, private boolean mShouldShowBuildText; private boolean mQsDisabled; - private QSPanel mQsPanel; private QuickQSPanel mQuickQsPanel; private boolean mExpanded; @@ -117,39 +88,19 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, } }; - @Inject - public QSFooterImpl(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs, - ActivityStarter activityStarter, UserInfoController userInfoController, - DeviceProvisionedController deviceProvisionedController, UserTracker userTracker) { + public QSFooterView(Context context, AttributeSet attrs) { super(context, attrs); - mActivityStarter = activityStarter; - mUserInfoController = userInfoController; - mDeviceProvisionedController = deviceProvisionedController; - mUserTracker = userTracker; - } - - @VisibleForTesting - public QSFooterImpl(Context context, AttributeSet attrs) { - this(context, attrs, - Dependency.get(ActivityStarter.class), - Dependency.get(UserInfoController.class), - Dependency.get(DeviceProvisionedController.class), - Dependency.get(UserTracker.class)); } @Override protected void onFinishInflate() { super.onFinishInflate(); mEdit = findViewById(android.R.id.edit); - mEdit.setOnClickListener(view -> - mActivityStarter.postQSRunnableDismissingKeyguard(() -> - mQsPanel.showEdit(view))); mPageIndicator = findViewById(R.id.footer_page_indicator); mSettingsButton = findViewById(R.id.settings_button); mSettingsContainer = findViewById(R.id.settings_button_container); - mSettingsButton.setOnClickListener(this); mMultiUserSwitch = findViewById(R.id.multi_user_switch); mMultiUserAvatar = mMultiUserSwitch.findViewById(R.id.multi_user_avatar); @@ -157,19 +108,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, mActionsContainer = findViewById(R.id.qs_footer_actions_container); mEditContainer = findViewById(R.id.qs_footer_actions_edit_container); mBuildText = findViewById(R.id.build); - mBuildText.setOnLongClickListener(view -> { - CharSequence buildText = mBuildText.getText(); - if (!TextUtils.isEmpty(buildText)) { - ClipboardManager service = - mUserTracker.getUserContext().getSystemService(ClipboardManager.class); - String label = mContext.getString(R.string.build_number_clip_data_label); - service.setPrimaryClip(ClipData.newPlainText(label, buildText)); - Toast.makeText(mContext, R.string.build_number_copy_toast, Toast.LENGTH_SHORT) - .show(); - return true; - } - return false; - }); // RenderThread is doing more harm than good when touching the header (to expand quick // settings), so disable it for this view @@ -180,7 +118,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> updateAnimator(right - left)); setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); - updateEverything(); setBuildText(); } @@ -249,24 +186,22 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, .build(); } - @Override - public void setKeyguardShowing(boolean keyguardShowing) { + /** */ + public void setKeyguardShowing() { setExpansion(mExpansionAmount); } - @Override public void setExpandClickListener(OnClickListener onClickListener) { mExpandClickListener = onClickListener; } - @Override - public void setExpanded(boolean expanded) { + void setExpanded(boolean expanded, boolean isTunerEnabled) { if (mExpanded == expanded) return; mExpanded = expanded; - updateEverything(); + updateEverything(isTunerEnabled); } - @Override + /** */ public void setExpansion(float headerExpansionFraction) { mExpansionAmount = headerExpansionFraction; if (mSettingsCogAnimator != null) mSettingsCogAnimator.setPosition(headerExpansionFraction); @@ -287,18 +222,16 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, @Override @VisibleForTesting public void onDetachedFromWindow() { - setListening(false); mContext.getContentResolver().unregisterContentObserver(mDeveloperSettingsObserver); super.onDetachedFromWindow(); } - @Override + /** */ public void setListening(boolean listening) { if (listening == mListening) { return; } mListening = listening; - updateListeners(); } @Override @@ -318,17 +251,16 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND); } - @Override - public void disable(int state1, int state2, boolean animate) { + void disable(int state2, boolean isTunerEnabled) { final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0; if (disabled == mQsDisabled) return; mQsDisabled = disabled; - updateEverything(); + updateEverything(isTunerEnabled); } - public void updateEverything() { + void updateEverything(boolean isTunerEnabled) { post(() -> { - updateVisibilities(); + updateVisibilities(isTunerEnabled); updateClickabilities(); setClickable(false); }); @@ -341,11 +273,10 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, mBuildText.setLongClickable(mBuildText.getVisibility() == View.VISIBLE); } - private void updateVisibilities() { + private void updateVisibilities(boolean isTunerEnabled) { mSettingsContainer.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE); mSettingsContainer.findViewById(R.id.tuner_icon).setVisibility( - TunerService.isTunerEnabled(mContext, mUserTracker.getUserHandle()) ? View.VISIBLE - : View.INVISIBLE); + isTunerEnabled ? View.VISIBLE : View.INVISIBLE); final boolean isDemo = UserManager.isDeviceInDemoMode(mContext); mMultiUserSwitch.setVisibility(showUserSwitcher() ? View.VISIBLE : View.INVISIBLE); mEditContainer.setVisibility(isDemo || !mExpanded ? View.INVISIBLE : View.VISIBLE); @@ -358,78 +289,22 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, return mExpanded && mMultiUserSwitch.isMultiUserEnabled(); } - private void updateListeners() { - if (mListening) { - mUserInfoController.addCallback(this); - } else { - mUserInfoController.removeCallback(this); - } - } - - @Override + /** */ public void setQSPanel(final QSPanel qsPanel) { - mQsPanel = qsPanel; - if (mQsPanel != null) { + if (qsPanel != null) { mMultiUserSwitch.setQsPanel(qsPanel); - mQsPanel.setFooterPageIndicator(mPageIndicator); + qsPanel.setFooterPageIndicator(mPageIndicator); } } - @Override public void setQQSPanel(@Nullable QuickQSPanel panel) { mQuickQsPanel = panel; } - @Override - public void onClick(View v) { - // Don't do anything until view are unhidden - if (!mExpanded) { - return; - } - - if (v == mSettingsButton) { - if (!mDeviceProvisionedController.isCurrentUserSetup()) { - // If user isn't setup just unlock the device and dump them back at SUW. - mActivityStarter.postQSRunnableDismissingKeyguard(() -> { - }); - return; - } - MetricsLogger.action(mContext, - mExpanded ? MetricsProto.MetricsEvent.ACTION_QS_EXPANDED_SETTINGS_LAUNCH - : MetricsProto.MetricsEvent.ACTION_QS_COLLAPSED_SETTINGS_LAUNCH); - if (mSettingsButton.isTunerClick()) { - mActivityStarter.postQSRunnableDismissingKeyguard(() -> { - if (TunerService.isTunerEnabled(mContext, mUserTracker.getUserHandle())) { - TunerService.showResetRequest(mContext, mUserTracker.getUserHandle(), - () -> { - // Relaunch settings so that the tuner disappears. - startSettingsActivity(); - }); - } else { - Toast.makeText(getContext(), R.string.tuner_toast, - Toast.LENGTH_LONG).show(); - TunerService.setTunerEnabled(mContext, mUserTracker.getUserHandle(), true); - } - startSettingsActivity(); - - }); - } else { - startSettingsActivity(); - } - } - } - - private void startSettingsActivity() { - mActivityStarter.startActivity(new Intent(android.provider.Settings.ACTION_SETTINGS), - true /* dismissShade */); - } - @Override - public void onUserInfoChanged(String name, Drawable picture, String userAccount) { - if (picture != null && - UserManager.get(mContext).isGuestUser(KeyguardUpdateMonitor.getCurrentUser()) && - !(picture instanceof UserIconDrawable)) { - picture = picture.getConstantState().newDrawable(mContext.getResources()).mutate(); + void onUserInfoChanged(Drawable picture, boolean isGuestUser) { + if (picture != null && isGuestUser && !(picture instanceof UserIconDrawable)) { + picture = picture.getConstantState().newDrawable(getResources()).mutate(); picture.setColorFilter( Utils.getColorAttrDefaultColor(mContext, android.R.attr.colorForeground), Mode.SRC_IN); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java new file mode 100644 index 000000000000..e3af04bdc31e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java @@ -0,0 +1,241 @@ +/* + * 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.qs; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.UserManager; +import android.text.TextUtils; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.Nullable; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto; +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.systemui.R; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.qs.dagger.QSScope; +import com.android.systemui.settings.UserTracker; +import com.android.systemui.statusbar.phone.SettingsButton; +import com.android.systemui.statusbar.policy.DeviceProvisionedController; +import com.android.systemui.statusbar.policy.UserInfoController; +import com.android.systemui.tuner.TunerService; +import com.android.systemui.util.ViewController; + +import javax.inject.Inject; + +/** + * Controller for {@link QSFooterView}. + */ +@QSScope +public class QSFooterViewController extends ViewController<QSFooterView> implements QSFooter { + + private final UserManager mUserManager; + private final UserInfoController mUserInfoController; + private final ActivityStarter mActivityStarter; + private final DeviceProvisionedController mDeviceProvisionedController; + private final UserTracker mUserTracker; + private final QSPanelController mQsPanelController; + private final TunerService mTunerService; + private final MetricsLogger mMetricsLogger; + private final SettingsButton mSettingsButton; + private final TextView mBuildText; + private final View mEdit; + + private final UserInfoController.OnUserInfoChangedListener mOnUserInfoChangedListener = + new UserInfoController.OnUserInfoChangedListener() { + @Override + public void onUserInfoChanged(String name, Drawable picture, String userAccount) { + boolean isGuestUser = mUserManager.isGuestUser(KeyguardUpdateMonitor.getCurrentUser()); + mView.onUserInfoChanged(picture, isGuestUser); + } + }; + + private final View.OnClickListener mSettingsOnClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + // Don't do anything until view are unhidden + if (!mExpanded) { + return; + } + + if (v == mSettingsButton) { + if (!mDeviceProvisionedController.isCurrentUserSetup()) { + // If user isn't setup just unlock the device and dump them back at SUW. + mActivityStarter.postQSRunnableDismissingKeyguard(() -> { + }); + return; + } + mMetricsLogger.action( + mExpanded ? MetricsProto.MetricsEvent.ACTION_QS_EXPANDED_SETTINGS_LAUNCH + : MetricsProto.MetricsEvent.ACTION_QS_COLLAPSED_SETTINGS_LAUNCH); + if (mSettingsButton.isTunerClick()) { + mActivityStarter.postQSRunnableDismissingKeyguard(() -> { + if (isTunerEnabled()) { + mTunerService.showResetRequest( + mUserTracker.getUserHandle(), + () -> { + // Relaunch settings so that the tuner disappears. + startSettingsActivity(); + }); + } else { + Toast.makeText(getContext(), R.string.tuner_toast, + Toast.LENGTH_LONG).show(); + mTunerService.setTunerEnabled(mUserTracker.getUserHandle(), true); + } + startSettingsActivity(); + + }); + } else { + startSettingsActivity(); + } + } + } + }; + + private boolean mListening; + private boolean mExpanded; + + @Inject + QSFooterViewController(QSFooterView view, UserManager userManager, + UserInfoController userInfoController, ActivityStarter activityStarter, + DeviceProvisionedController deviceProvisionedController, UserTracker userTracker, + QSPanelController qsPanelController, TunerService tunerService, + MetricsLogger metricsLogger) { + super(view); + mUserManager = userManager; + mUserInfoController = userInfoController; + mActivityStarter = activityStarter; + mDeviceProvisionedController = deviceProvisionedController; + mUserTracker = userTracker; + mQsPanelController = qsPanelController; + mTunerService = tunerService; + mMetricsLogger = metricsLogger; + + mSettingsButton = mView.findViewById(R.id.settings_button); + mBuildText = mView.findViewById(R.id.build); + mEdit = mView.findViewById(android.R.id.edit); + } + + + @Override + protected void onViewAttached() { + mSettingsButton.setOnClickListener(mSettingsOnClickListener); + mBuildText.setOnLongClickListener(view -> { + CharSequence buildText = mBuildText.getText(); + if (!TextUtils.isEmpty(buildText)) { + ClipboardManager service = + mUserTracker.getUserContext().getSystemService(ClipboardManager.class); + String label = getResources().getString(R.string.build_number_clip_data_label); + service.setPrimaryClip(ClipData.newPlainText(label, buildText)); + Toast.makeText(getContext(), R.string.build_number_copy_toast, Toast.LENGTH_SHORT) + .show(); + return true; + } + return false; + }); + + mEdit.setOnClickListener(view -> + mActivityStarter.postQSRunnableDismissingKeyguard(() -> + mQsPanelController.showEdit(view))); + + mView.updateEverything(isTunerEnabled()); + } + + @Override + protected void onViewDetached() { + setListening(false); + } + + + @Override + public void setQSPanel(@Nullable QSPanel panel) { + mView.setQSPanel(panel); + } + + @Override + public void setVisibility(int visibility) { + mView.setVisibility(visibility); + } + + @Override + public void setExpanded(boolean expanded) { + mExpanded = expanded; + mView.setExpanded(expanded, isTunerEnabled()); + } + + + @Override + public int getHeight() { + return mView.getHeight(); + } + + @Override + public void setExpansion(float expansion) { + mView.setExpansion(expansion); + } + + @Override + public void setListening(boolean listening) { + if (mListening == listening) { + return; + } + + mListening = listening; + if (mListening) { + mUserInfoController.addCallback(mOnUserInfoChangedListener); + } else { + mUserInfoController.removeCallback(mOnUserInfoChangedListener); + } + } + + @Override + public void setKeyguardShowing(boolean keyguardShowing) { + mView.setKeyguardShowing(); + } + + /** */ + @Override + public void setExpandClickListener(View.OnClickListener onClickListener) { + mView.setExpandClickListener(onClickListener); + } + + @Override + public void setQQSPanel(@Nullable QuickQSPanel panel) { + mView.setQQSPanel(panel); + } + + @Override + public void disable(int state1, int state2, boolean animate) { + mView.disable(state2, isTunerEnabled()); + } + + + private void startSettingsActivity() { + mActivityStarter.startActivity(new Intent(android.provider.Settings.ACTION_SETTINGS), + true /* dismissShade */); + } + + private boolean isTunerEnabled() { + return mTunerService.isTunerEnabled(mUserTracker.getUserHandle()); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index 3a783653a2d8..e1bca4a42b89 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -35,11 +35,11 @@ import androidx.annotation.VisibleForTesting; import com.android.systemui.Interpolators; import com.android.systemui.R; -import com.android.systemui.R.id; import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.qs.QS; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.qs.customize.QSCustomizer; +import com.android.systemui.qs.customize.QSCustomizerController; +import com.android.systemui.qs.dagger.QSFragmentComponent; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; @@ -69,8 +69,6 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca private QSAnimator mQSAnimator; private HeightListener mPanelView; protected QuickStatusBarHeader mHeader; - private QSCustomizer mQSCustomizer; - protected QSPanel mQSPanel; protected NonInterceptingScrollView mQSPanelScrollView; private QSDetail mQSDetail; private boolean mListening; @@ -82,7 +80,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca private final RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler; private final InjectionInflationController mInjectionInflater; - private final QSContainerImplController.Builder mQSContainerImplControllerBuilder; + private final QSFragmentComponent.Factory mQsComponentFactory; private final QSTileHost mHost; private boolean mShowCollapsedOnKeyguard; private boolean mLastKeyguardAndExpanded; @@ -96,15 +94,18 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca private int[] mTmpLocation = new int[2]; private int mLastViewHeight; private float mLastHeaderTranslation; + private QSPanelController mQSPanelController; + private QuickQSPanelController mQuickQSPanelController; + private QSCustomizerController mQSCustomizerController; @Inject public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler, InjectionInflationController injectionInflater, QSTileHost qsTileHost, StatusBarStateController statusBarStateController, CommandQueue commandQueue, - QSContainerImplController.Builder qsContainerImplControllerBuilder) { + QSFragmentComponent.Factory qsComponentFactory) { mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler; mInjectionInflater = injectionInflater; - mQSContainerImplControllerBuilder = qsContainerImplControllerBuilder; + mQsComponentFactory = qsComponentFactory; commandQueue.observe(getLifecycle(), this); mHost = qsTileHost; mStatusBarStateController = statusBarStateController; @@ -120,8 +121,13 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - mQSPanel = view.findViewById(R.id.quick_settings_panel); + QSFragmentComponent qsFragmentComponent = mQsComponentFactory.create(this); + mQSPanelController = qsFragmentComponent.getQSPanelController(); + mQuickQSPanelController = qsFragmentComponent.getQuickQSPanelController(); + + mQSPanelController.init(); + mQuickQSPanelController.init(); + mQSPanelScrollView = view.findViewById(R.id.expanded_qs_scroll_view); mQSPanelScrollView.addOnLayoutChangeListener( (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { @@ -135,28 +141,26 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca }); mQSDetail = view.findViewById(R.id.qs_detail); mHeader = view.findViewById(R.id.header); - mQSPanel.setHeaderContainer(view.findViewById(R.id.header_text_container)); - mFooter = view.findViewById(R.id.qs_footer); - mContainer = view.findViewById(id.quick_settings_container); + mQSPanelController.setHeaderContainer(view.findViewById(R.id.header_text_container)); + mFooter = qsFragmentComponent.getQSFooter(); - mQSContainerImplController = mQSContainerImplControllerBuilder - .setQSContainerImpl((QSContainerImpl) view) - .build(); + mQSContainerImplController = qsFragmentComponent.getQSContainerImplController(); mQSContainerImplController.init(); + mContainer = mQSContainerImplController.getView(); - mQSDetail.setQsPanel(mQSPanel, mHeader, (View) mFooter); - mQSAnimator = new QSAnimator(this, mHeader.findViewById(R.id.quick_qs_panel), mQSPanel); - + mQSDetail.setQsPanel(mQSPanelController, mHeader, mFooter); + mQSAnimator = qsFragmentComponent.getQSAnimator(); - mQSCustomizer = view.findViewById(R.id.qs_customize); - mQSCustomizer.setQs(this); + mQSCustomizerController = qsFragmentComponent.getQSCustomizerController(); + mQSCustomizerController.init(); + mQSCustomizerController.setQs(this); if (savedInstanceState != null) { setExpanded(savedInstanceState.getBoolean(EXTRA_EXPANDED)); setListening(savedInstanceState.getBoolean(EXTRA_LISTENING)); setEditLocation(view); - mQSCustomizer.restoreInstanceState(savedInstanceState); + mQSCustomizerController.restoreInstanceState(savedInstanceState); if (mQsExpanded) { - mQSPanel.getTileLayout().restoreInstanceState(savedInstanceState); + mQSPanelController.getTileLayout().restoreInstanceState(savedInstanceState); } } setHost(mHost); @@ -178,7 +182,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca if (mListening) { setListening(false); } - mQSCustomizer.setQs(null); + mQSCustomizerController.setQs(null); } @Override @@ -186,9 +190,9 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca super.onSaveInstanceState(outState); outState.putBoolean(EXTRA_EXPANDED, mQsExpanded); outState.putBoolean(EXTRA_LISTENING, mListening); - mQSCustomizer.saveInstanceState(outState); + mQSCustomizerController.saveInstanceState(outState); if (mQsExpanded) { - mQSPanel.getTileLayout().saveInstanceState(outState); + mQSPanelController.getTileLayout().saveInstanceState(outState); } } @@ -233,30 +237,25 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca int[] loc = edit.getLocationOnScreen(); int x = loc[0] + edit.getWidth() / 2; int y = loc[1] + edit.getHeight() / 2; - mQSCustomizer.setEditLocation(x, y); + mQSCustomizerController.setEditLocation(x, y); } @Override public void setContainer(ViewGroup container) { if (container instanceof NotificationsQuickSettingsContainer) { - mQSCustomizer.setContainer((NotificationsQuickSettingsContainer) container); + mQSCustomizerController.setContainer((NotificationsQuickSettingsContainer) container); } } @Override public boolean isCustomizing() { - return mQSCustomizer.isCustomizing(); + return mQSCustomizerController.isCustomizing(); } public void setHost(QSTileHost qsh) { - mQSPanel.setHost(qsh, mQSCustomizer); - mHeader.setQSPanel(mQSPanel); - mFooter.setQSPanel(mQSPanel); + mHeader.setQSPanel(mQSPanelController.getView()); + mFooter.setQSPanel(mQSPanelController.getView()); mQSDetail.setHost(qsh); - - if (mQSAnimator != null) { - mQSAnimator.setHost(qsh); - } } @Override @@ -278,7 +277,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca private void updateQsState() { final boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling || mHeaderAnimating; - mQSPanel.setExpanded(mQsExpanded); + mQSPanelController.setExpanded(mQsExpanded); mQSDetail.setExpanded(mQsExpanded); boolean keyguardShowing = isKeyguardShowing(); mHeader.setVisibility((mQsExpanded || !keyguardShowing || mHeaderAnimating @@ -294,7 +293,8 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca : View.INVISIBLE); mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard) || (mQsExpanded && !mStackScrollerOverscrolling)); - mQSPanel.setVisibility(!mQsDisabled && expandVisually ? View.VISIBLE : View.INVISIBLE); + mQSPanelController.setVisibility( + !mQsDisabled && expandVisually ? View.VISIBLE : View.INVISIBLE); } private boolean isKeyguardShowing() { @@ -317,17 +317,17 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca } } - public QSPanel getQsPanel() { - return mQSPanel; + public QSPanelController getQSPanelController() { + return mQSPanelController; } - public QSCustomizer getCustomizer() { - return mQSCustomizer; + public QSPanel getQsPanel() { + return mQSPanelController.getView(); } @Override public boolean isShowingDetail() { - return mQSPanel.isShowingCustomize() || mQSDetail.isShowingDetail(); + return mQSCustomizerController.isCustomizing() || mQSDetail.isShowingDetail(); } @Override @@ -339,7 +339,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca public void setExpanded(boolean expanded) { if (DEBUG) Log.d(TAG, "setExpanded " + expanded); mQsExpanded = expanded; - mQSPanel.setListening(mListening, mQsExpanded); + mQSPanelController.setListening(mListening, mQsExpanded); updateQsState(); } @@ -368,7 +368,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca mListening = listening; mQSContainerImplController.setListening(listening); mFooter.setListening(listening); - mQSPanel.setListening(mListening, mQsExpanded); + mQSPanelController.setListening(mListening, mQsExpanded); } @Override @@ -406,11 +406,15 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca float panelTranslationY = translationScaleY * heightDiff; // Let the views animate their contents correctly by giving them the necessary context. - mHeader.setExpansion(onKeyguardAndExpanded, expansion, - panelTranslationY); + mHeader.setExpansion(onKeyguardAndExpanded, expansion, panelTranslationY); + if (expansion < 1 && expansion > 0.99) { + if (mQuickQSPanelController.switchTileLayout(false)) { + mHeader.updateResources(); + } + } mFooter.setExpansion(onKeyguardAndExpanded ? 1 : expansion); - mQSPanel.getQsTileRevealController().setExpansion(expansion); - mQSPanel.getTileLayout().setExpansion(expansion); + mQSPanelController.getQsTileRevealController().setExpansion(expansion); + mQSPanelController.getTileLayout().setExpansion(expansion); mQSPanelScrollView.setTranslationY(translationScaleY * heightDiff); if (fullyCollapsed) { mQSPanelScrollView.setScrollY(0); @@ -448,7 +452,8 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca float expandedMediaPosition = absoluteBottomPosition - mQSPanelScrollView.getScrollY() + mQSPanelScrollView.getScrollRange(); // The expanded media host should never move below the laid out position - pinToBottom(expandedMediaPosition, mQSPanel.getMediaHost(), true /* expanded */); + pinToBottom( + expandedMediaPosition, mQSPanelController.getMediaHost(), true /* expanded */); // The expanded media host should never move above the laid out position pinToBottom(absoluteBottomPosition, mHeader.getHeaderQsPanel().getMediaHost(), false /* expanded */); @@ -538,27 +543,28 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca @Override public void closeDetail() { - mQSPanel.closeDetail(); + mQSPanelController.closeDetail(); } public void notifyCustomizeChanged() { // The customize state changed, so our height changed. mContainer.updateExpansion(); - mQSPanelScrollView.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE + mQSPanelScrollView.setVisibility(!mQSCustomizerController.isCustomizing() ? View.VISIBLE : View.INVISIBLE); - mFooter.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE); + mFooter.setVisibility( + !mQSCustomizerController.isCustomizing() ? View.VISIBLE : View.INVISIBLE); // Let the panel know the position changed and it needs to update where notifications // and whatnot are. mPanelView.onQsHeightChanged(); } /** - * The height this view wants to be. This is different from {@link #getMeasuredHeight} such that - * during closing the detail panel, this already returns the smaller height. + * The height this view wants to be. This is different from {@link View#getMeasuredHeight} such + * that during closing the detail panel, this already returns the smaller height. */ @Override public int getDesiredHeight() { - if (mQSCustomizer.isCustomizing()) { + if (mQSCustomizerController.isCustomizing()) { return getView().getHeight(); } if (mQSDetail.isClosingDetail()) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 61c6d3a629ab..758e0c566e5d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -16,16 +16,15 @@ package com.android.systemui.qs; +import static com.android.systemui.media.dagger.MediaModule.QS_PANEL; import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT; import static com.android.systemui.util.Utils.useQsMediaPlayer; import android.annotation.NonNull; import android.annotation.Nullable; -import android.content.ComponentName; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; -import android.metrics.LogMaker; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -42,41 +41,28 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.widget.RemeasuringLinearLayout; import com.android.systemui.Dependency; -import com.android.systemui.Dumpable; import com.android.systemui.R; -import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.dump.DumpManager; import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.qs.DetailAdapter; import com.android.systemui.plugins.qs.QSTile; -import com.android.systemui.plugins.qs.QSTileView; -import com.android.systemui.qs.QSHost.Callback; -import com.android.systemui.qs.customize.QSCustomizer; -import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.logging.QSLogger; -import com.android.systemui.settings.BrightnessController; import com.android.systemui.settings.ToggleSliderView; -import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; import com.android.systemui.util.animation.DisappearParameters; -import java.io.FileDescriptor; -import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Collection; +import java.util.List; import java.util.function.Consumer; -import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Named; /** View that represents the quick settings tile panel (when expanded/pulled down). **/ -public class QSPanel extends LinearLayout implements Tunable, Callback, BrightnessMirrorListener, - Dumpable { +public class QSPanel extends LinearLayout implements Tunable, BrightnessMirrorListener { public static final String QS_SHOW_BRIGHTNESS = "qs_show_brightness"; public static final String QS_SHOW_HEADER = "qs_show_header"; @@ -84,24 +70,18 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne private static final String TAG = "QSPanel"; protected final Context mContext; - protected final ArrayList<TileRecord> mRecords = new ArrayList<>(); - private final BroadcastDispatcher mBroadcastDispatcher; protected final MediaHost mMediaHost; /** * The index where the content starts that needs to be moved between parents */ private final int mMovableContentStartIndex; - private String mCachedSpecs = ""; @Nullable protected View mBrightnessView; - @Nullable - private BrightnessController mBrightnessController; private final H mHandler = new H(); private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); - private QSTileRevealController mQsTileRevealController; /** Whether or not the QS media player feature is enabled. */ protected boolean mUsingMediaPlayer; private int mVisualMarginStart; @@ -111,14 +91,14 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne protected boolean mListening; private QSDetail.Callback mCallback; - private final DumpManager mDumpManager; private final QSLogger mQSLogger; protected final UiEventLogger mUiEventLogger; protected QSTileHost mHost; - private final UserTracker mUserTracker; + private final List<OnConfigurationChangedListener> mOnConfigurationChangedListeners = + new ArrayList<>(); @Nullable - protected QSSecurityFooter mSecurityFooter; + protected View mSecurityFooter; @Nullable protected View mFooter; @@ -134,7 +114,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne private int mVisualTilePadding; private boolean mUsingHorizontalLayout; - private QSCustomizer mCustomizePanel; private Record mDetailRecord; private BrightnessMirrorController mBrightnessMirrorController; @@ -155,12 +134,9 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne public QSPanel( @Named(VIEW_CONTEXT) Context context, AttributeSet attrs, - DumpManager dumpManager, - BroadcastDispatcher broadcastDispatcher, QSLogger qsLogger, - MediaHost mediaHost, - UiEventLogger uiEventLogger, - UserTracker userTracker + @Named(QS_PANEL) MediaHost mediaHost, + UiEventLogger uiEventLogger ) { super(context, attrs); mUsingMediaPlayer = useQsMediaPlayer(context); @@ -173,16 +149,14 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne }); mContext = context; mQSLogger = qsLogger; - mDumpManager = dumpManager; - mBroadcastDispatcher = broadcastDispatcher; mUiEventLogger = uiEventLogger; - mUserTracker = userTracker; setOrientation(VERTICAL); addViewsAboveTiles(); mMovableContentStartIndex = getChildCount(); mRegularTileLayout = createRegularTileLayout(); + mTileLayout = mRegularTileLayout; if (mUsingMediaPlayer) { mHorizontalLinearLayout = new RemeasuringLinearLayout(mContext); @@ -208,35 +182,23 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne initMediaHostState(); } - addSecurityFooter(); - if (mRegularTileLayout instanceof PagedTileLayout) { - mQsTileRevealController = new QSTileRevealController(mContext, this, - (PagedTileLayout) mRegularTileLayout); - } - mQSLogger.logAllTilesChangeListening(mListening, getDumpableTag(), mCachedSpecs); - updateResources(); + mQSLogger.logAllTilesChangeListening(mListening, getDumpableTag(), ""); } protected void onMediaVisibilityChanged(Boolean visible) { - switchTileLayout(); if (mMediaVisibilityChangedListener != null) { mMediaVisibilityChangedListener.accept(visible); } } - protected void addSecurityFooter() { - mSecurityFooter = new QSSecurityFooter(this, mContext, mUserTracker); - } - protected void addViewsAboveTiles() { mBrightnessView = LayoutInflater.from(mContext).inflate( R.layout.quick_settings_brightness_dialog, this, false); addView(mBrightnessView); - mBrightnessController = new BrightnessController(getContext(), - findViewById(R.id.brightness_slider), mBroadcastDispatcher); } - protected QSTileLayout createRegularTileLayout() { + /** */ + public QSTileLayout createRegularTileLayout() { if (mRegularTileLayout == null) { mRegularTileLayout = (QSTileLayout) LayoutInflater.from(mContext).inflate( R.layout.qs_paged_tile_layout, this, false); @@ -327,46 +289,11 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne setMeasuredDimension(getMeasuredWidth(), height); } - public QSTileRevealController getQsTileRevealController() { - return mQsTileRevealController; - } - - public boolean isShowingCustomize() { - return mCustomizePanel != null && mCustomizePanel.isCustomizing(); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - final TunerService tunerService = Dependency.get(TunerService.class); - tunerService.addTunable(this, QS_SHOW_BRIGHTNESS); - - if (mHost != null) { - setTiles(mHost.getTiles()); - } - if (mBrightnessMirrorController != null) { - mBrightnessMirrorController.addCallback(this); - } - mDumpManager.registerDumpable(getDumpableTag(), this); - } - @Override protected void onDetachedFromWindow() { - Dependency.get(TunerService.class).removeTunable(this); - if (mHost != null) { - mHost.removeCallback(this); - } if (mTileLayout != null) { mTileLayout.setListening(false); } - for (TileRecord record : mRecords) { - record.tile.removeCallbacks(); - } - mRecords.clear(); - if (mBrightnessMirrorController != null) { - mBrightnessMirrorController.removeCallback(this); - } - mDumpManager.unregisterDumpable(getDumpableTag()); super.onDetachedFromWindow(); } @@ -375,11 +302,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } @Override - public void onTilesChanged() { - setTiles(mHost.getTiles()); - } - - @Override public void onTuningChanged(String key, String newValue) { if (QS_SHOW_BRIGHTNESS.equals(key) && mBrightnessView != null) { updateViewVisibilityForTuningValue(mBrightnessView, newValue); @@ -390,8 +312,8 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne view.setVisibility(TunerService.parseIntegerSwitch(newValue, true) ? VISIBLE : GONE); } - public void openDetails(String subPanel) { - QSTile tile = getTile(subPanel); + /** */ + public void openDetails(QSTile tile) { // If there's no tile with that name (as defined in QSFactoryImpl or other QSFactory), // QSFactory will not be able to create a tile and getTile will return null if (tile != null) { @@ -399,15 +321,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } } - private QSTile getTile(String subPanel) { - for (int i = 0; i < mRecords.size(); i++) { - if (subPanel.equals(mRecords.get(i).tile.getTileSpec())) { - return mRecords.get(i).tile; - } - } - return mHost.createTile(subPanel); - } - public void setBrightnessMirror(BrightnessMirrorController c) { if (mBrightnessMirrorController != null) { mBrightnessMirrorController.removeCallback(this); @@ -433,19 +346,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne mCallback = callback; } - public void setHost(QSTileHost host, QSCustomizer customizer) { - mHost = host; - mHost.addCallback(this); - setTiles(mHost.getTiles()); - if (mSecurityFooter != null) { - mSecurityFooter.setHostEnvironment(host); - } - mCustomizePanel = customizer; - if (mCustomizePanel != null) { - mCustomizePanel.setHost(mHost); - } - } - /** * Links the footer's page indicator, which is used in landscape orientation to save space. * @@ -482,12 +382,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne updatePageIndicator(); - for (TileRecord r : mRecords) { - r.tile.clearState(); - } - if (mListening) { - refreshAllTiles(); - } if (mTileLayout != null) { mTileLayout.updateResources(); } @@ -508,20 +402,21 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom)); } + void addOnConfigurationChangedListener(OnConfigurationChangedListener listener) { + mOnConfigurationChangedListeners.add(listener); + } + + void removeOnConfigurationChangedListener(OnConfigurationChangedListener listener) { + mOnConfigurationChangedListeners.remove(listener); + } + @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - if (mSecurityFooter != null) { - mSecurityFooter.onConfigurationChanged(); - } - updateResources(); + mOnConfigurationChangedListeners.forEach( + listener -> listener.onConfigurationChange(newConfig)); updateBrightnessMirror(); - - if (newConfig.orientation != mLastOrientation) { - mLastOrientation = newConfig.orientation; - switchTileLayout(); - } } @Override @@ -529,14 +424,9 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne super.onFinishInflate(); mFooter = findViewById(R.id.qs_footer); mDivider = findViewById(R.id.divider); - switchTileLayout(true /* force */); } - boolean switchTileLayout() { - return switchTileLayout(false /* force */); - } - - private boolean switchTileLayout(boolean force) { + boolean switchTileLayout(boolean force, List<QSPanelControllerBase.TileRecord> records) { /** Whether or not the QuickQSPanel currently contains a media player. */ boolean horizontal = shouldUseHorizontalLayout(); if (mDivider != null) { @@ -564,14 +454,12 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne reAttachMediaHost(); if (mTileLayout != null) { mTileLayout.setListening(false); - for (TileRecord record : mRecords) { + for (QSPanelControllerBase.TileRecord record : records) { mTileLayout.removeTile(record); record.tile.removeCallback(record.callback); } } mTileLayout = newLayout; - if (mHost != null) setTiles(mHost.getTiles()); - newLayout.setListening(mListening); if (needsDynamicRowsAndColumns()) { newLayout.setMinRows(horizontal ? 2 : 1); // Let's use 3 columns to match the current layout @@ -589,6 +477,14 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne return false; } + /** + * Sets the listening state of the current layout to the state of the view. Used after + * switching layouts. + */ + public void reSetLayoutListening() { + mTileLayout.setListening(mListening); + } + private void updateHorizontalLinearLayoutMargins() { if (mHorizontalLinearLayout != null && !displayMediaMarginsOnMedia()) { LayoutParams lp = (LayoutParams) mHorizontalLinearLayout.getLayoutParams(); @@ -619,20 +515,20 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne index++; if (mSecurityFooter != null) { - View view = mSecurityFooter.getView(); - LinearLayout.LayoutParams layoutParams = (LayoutParams) view.getLayoutParams(); + LinearLayout.LayoutParams layoutParams = + (LayoutParams) mSecurityFooter.getLayoutParams(); if (mUsingHorizontalLayout && mHeaderContainer != null) { // Adding the security view to the header, that enables us to avoid scrolling layoutParams.width = 0; layoutParams.weight = 1.6f; - switchToParent(view, mHeaderContainer, 1 /* always in second place */); + switchToParent(mSecurityFooter, mHeaderContainer, 1 /* always in second place */); } else { layoutParams.width = LayoutParams.WRAP_CONTENT; layoutParams.weight = 0; - switchToParent(view, parent, index); + switchToParent(mSecurityFooter, parent, index); index++; } - view.setLayoutParams(layoutParams); + mSecurityFooter.setLayoutParams(layoutParams); } if (mFooter != null) { @@ -692,12 +588,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } } - public void onCollapse() { - if (mCustomizePanel != null && mCustomizePanel.isShown()) { - mCustomizePanel.hide(); - } - } - public void setExpanded(boolean expanded) { if (mExpanded == expanded) return; mQSLogger.logPanelExpanded(expanded, getDumpableTag()); @@ -705,14 +595,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne if (!mExpanded && mTileLayout instanceof PagedTileLayout) { ((PagedTileLayout) mTileLayout).setCurrentItem(0, false); } - mMetricsLogger.visibility(MetricsEvent.QS_PANEL, mExpanded); - if (!mExpanded) { - mUiEventLogger.log(closePanelEvent()); - closeDetail(); - } else { - mUiEventLogger.log(openPanelEvent()); - logTiles(); - } } public void setPageListener(final PagedTileLayout.PageListener pageListener) { @@ -725,56 +607,16 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne return mExpanded; } - public void setListening(boolean listening) { + /** */ + public void setListening(boolean listening, String cachedSpecs) { if (mListening == listening) return; mListening = listening; if (mTileLayout != null) { - mQSLogger.logAllTilesChangeListening(listening, getDumpableTag(), mCachedSpecs); + mQSLogger.logAllTilesChangeListening(listening, getDumpableTag(), cachedSpecs); mTileLayout.setListening(listening); } - if (mListening) { - refreshAllTiles(); - } } - private String getTilesSpecs() { - return mRecords.stream() - .map(tileRecord -> tileRecord.tile.getTileSpec()) - .collect(Collectors.joining(",")); - } - - public void setListening(boolean listening, boolean expanded) { - setListening(listening && expanded); - if (mSecurityFooter != null) { - mSecurityFooter.setListening(listening); - } - // Set the listening as soon as the QS fragment starts listening regardless of the expansion, - // so it will update the current brightness before the slider is visible. - setBrightnessListening(listening); - } - - public void setBrightnessListening(boolean listening) { - if (mBrightnessController == null) { - return; - } - if (listening) { - mBrightnessController.registerCallbacks(); - } else { - mBrightnessController.unregisterCallbacks(); - } - } - - public void refreshAllTiles() { - if (mBrightnessController != null) { - mBrightnessController.checkRestrictionAndSetEnabled(); - } - for (TileRecord r : mRecords) { - r.tile.refreshState(); - } - if (mSecurityFooter != null) { - mSecurityFooter.refreshState(); - } - } public void showDetailAdapter(boolean show, DetailAdapter adapter, int[] locationInWindow) { int xInWindow = locationInWindow[0]; @@ -796,33 +638,10 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0, r).sendToTarget(); } - public void setTiles(Collection<QSTile> tiles) { - setTiles(tiles, false); - } - - public void setTiles(Collection<QSTile> tiles, boolean collapsedView) { - if (!collapsedView) { - mQsTileRevealController.updateRevealedTiles(tiles); - } - for (TileRecord record : mRecords) { - mTileLayout.removeTile(record); - record.tile.removeCallback(record.callback); - } - mRecords.clear(); - mCachedSpecs = ""; - for (QSTile tile : tiles) { - addTile(tile, collapsedView); - } - } - - protected void drawTile(TileRecord r, QSTile.State state) { + protected void drawTile(QSPanelControllerBase.TileRecord r, QSTile.State state) { r.tileView.onStateChanged(state); } - protected QSTileView createTileView(QSTile tile, boolean collapsedView) { - return mHost.createTileView(tile, collapsedView); - } - protected QSEvent openPanelEvent() { return QSEvent.QS_PANEL_EXPANDED; } @@ -839,14 +658,11 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne return mExpanded; } - protected TileRecord addTile(final QSTile tile, boolean collapsedView) { - final TileRecord r = new TileRecord(); - r.tile = tile; - r.tileView = createTileView(tile, collapsedView); + void addTile(QSPanelControllerBase.TileRecord tileRecord) { final QSTile.Callback callback = new QSTile.Callback() { @Override public void onStateChanged(QSTile.State state) { - drawTile(r, state); + drawTile(tileRecord, state); } @Override @@ -854,22 +670,22 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne // Both the collapsed and full QS panels get this callback, this check determines // which one should handle showing the detail. if (shouldShowDetail()) { - QSPanel.this.showDetail(show, r); + QSPanel.this.showDetail(show, tileRecord); } } @Override public void onToggleStateChanged(boolean state) { - if (mDetailRecord == r) { + if (mDetailRecord == tileRecord) { fireToggleStateChanged(state); } } @Override public void onScanStateChanged(boolean state) { - r.scanState = state; - if (mDetailRecord == r) { - fireScanStateChanged(r.scanState); + tileRecord.scanState = state; + if (mDetailRecord == tileRecord) { + fireScanStateChanged(tileRecord.scanState); } } @@ -881,44 +697,22 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } } }; - r.tile.addCallback(callback); - r.callback = callback; - r.tileView.init(r.tile); - r.tile.refreshState(); - mRecords.add(r); - mCachedSpecs = getTilesSpecs(); + + tileRecord.tile.addCallback(callback); + tileRecord.callback = callback; + tileRecord.tileView.init(tileRecord.tile); + tileRecord.tile.refreshState(); if (mTileLayout != null) { - mTileLayout.addTile(r); + mTileLayout.addTile(tileRecord); } - - return r; } - - public void showEdit(final View v) { - v.post(new Runnable() { - @Override - public void run() { - if (mCustomizePanel != null) { - if (!mCustomizePanel.isCustomizing()) { - int[] loc = v.getLocationOnScreen(); - int x = loc[0] + v.getWidth() / 2; - int y = loc[1] + v.getHeight() / 2; - mCustomizePanel.show(x, y); - } - } - - } - }); + void removeTile(QSPanelControllerBase.TileRecord tileRecord) { + mTileLayout.removeTile(tileRecord); } - public void closeDetail() { - if (mCustomizePanel != null && mCustomizePanel.isShown()) { - // Treat this as a detail panel for now, to make things easy. - mCustomizePanel.hide(); - return; - } + void closeDetail() { showDetail(false, mDetailRecord); } @@ -927,8 +721,8 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } protected void handleShowDetail(Record r, boolean show) { - if (r instanceof TileRecord) { - handleShowDetailTile((TileRecord) r, show); + if (r instanceof QSPanelControllerBase.TileRecord) { + handleShowDetailTile((QSPanelControllerBase.TileRecord) r, show); } else { int x = 0; int y = 0; @@ -940,7 +734,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } } - private void handleShowDetailTile(TileRecord r, boolean show) { + private void handleShowDetailTile(QSPanelControllerBase.TileRecord r, boolean show) { if ((mDetailRecord != null) == show && mDetailRecord == r) return; if (show) { @@ -961,8 +755,8 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne protected void setDetailRecord(Record r) { if (r == mDetailRecord) return; mDetailRecord = r; - final boolean scanState = mDetailRecord instanceof TileRecord - && ((TileRecord) mDetailRecord).scanState; + final boolean scanState = mDetailRecord instanceof QSPanelControllerBase.TileRecord + && ((QSPanelControllerBase.TileRecord) mDetailRecord).scanState; fireScanStateChanged(scanState); } @@ -974,15 +768,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } mGridContentVisible = visible; } - - private void logTiles() { - for (int i = 0; i < mRecords.size(); i++) { - QSTile tile = mRecords.get(i).tile; - mMetricsLogger.write(tile.populate(new LogMaker(tile.getMetricsCategory()) - .setType(MetricsEvent.TYPE_OPEN))); - } - } - private void fireShowingDetail(DetailAdapter detail, int x, int y) { if (mCallback != null) { mCallback.onShowingDetail(detail, x, y); @@ -1001,46 +786,15 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } } - public void clickTile(ComponentName tile) { - final String spec = CustomTile.toSpec(tile); - final int N = mRecords.size(); - for (int i = 0; i < N; i++) { - if (mRecords.get(i).tile.getTileSpec().equals(spec)) { - mRecords.get(i).tile.click(); - break; - } - } - } - QSTileLayout getTileLayout() { return mTileLayout; } - QSTileView getTileView(QSTile tile) { - for (TileRecord r : mRecords) { - if (r.tile == tile) { - return r.tileView; - } - } - return null; - } - - @Nullable - public QSSecurityFooter getSecurityFooter() { - return mSecurityFooter; - } - @Nullable public View getDivider() { return mDivider; } - public void showDeviceMonitoringDialog() { - if (mSecurityFooter != null) { - mSecurityFooter.showDeviceMonitoringDialog(); - } - } - public void setContentMargins(int startMargin, int endMargin) { // Only some views actually want this content padding, others want to go all the way // to the edge like the brightness slider @@ -1123,9 +877,11 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne */ protected void updateMargins(View view, int start, int end) { LayoutParams lp = (LayoutParams) view.getLayoutParams(); - lp.setMarginStart(start); - lp.setMarginEnd(end); - view.setLayoutParams(lp); + if (lp != null) { + lp.setMarginStart(start); + lp.setMarginEnd(end); + view.setLayoutParams(lp); + } } public MediaHost getMediaHost() { @@ -1143,6 +899,14 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne mMediaVisibilityChangedListener = visibilityChangedListener; } + public boolean isListening() { + return mListening; + } + + public void setSecurityFooter(View view) { + mSecurityFooter = view; + } + private class H extends Handler { private static final int SHOW_DETAIL = 1; private static final int SET_TILE_VISIBILITY = 2; @@ -1158,46 +922,32 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } } - @Override - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println(getClass().getSimpleName() + ":"); - pw.println(" Tile records:"); - for (TileRecord record : mRecords) { - if (record.tile instanceof Dumpable) { - pw.print(" "); ((Dumpable) record.tile).dump(fd, pw, args); - pw.print(" "); pw.println(record.tileView.toString()); - } - } - } - - protected static class Record { DetailAdapter detailAdapter; int x; int y; } - public static final class TileRecord extends Record { - public QSTile tile; - public com.android.systemui.plugins.qs.QSTileView tileView; - public boolean scanState; - public QSTile.Callback callback; - } - public interface QSTileLayout { - + /** */ default void saveInstanceState(Bundle outState) {} + /** */ default void restoreInstanceState(Bundle savedInstanceState) {} - void addTile(TileRecord tile); + /** */ + void addTile(QSPanelControllerBase.TileRecord tile); - void removeTile(TileRecord tile); + /** */ + void removeTile(QSPanelControllerBase.TileRecord tile); - int getOffsetTop(TileRecord tile); + /** */ + int getOffsetTop(QSPanelControllerBase.TileRecord tile); + /** */ boolean updateResources(); + /** */ void setListening(boolean listening); /** @@ -1224,4 +974,8 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne int getNumVisibleTiles(); } + + interface OnConfigurationChangedListener { + void onConfigurationChange(Configuration newConfig); + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java new file mode 100644 index 000000000000..e9670a959cee --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java @@ -0,0 +1,212 @@ +/* + * 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.qs; + +import static com.android.systemui.qs.QSPanel.QS_SHOW_BRIGHTNESS; + +import android.annotation.NonNull; +import android.content.res.Configuration; +import android.view.View; +import android.view.ViewGroup; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.UiEventLogger; +import com.android.systemui.R; +import com.android.systemui.dump.DumpManager; +import com.android.systemui.media.MediaHost; +import com.android.systemui.plugins.qs.QSTile; +import com.android.systemui.qs.customize.QSCustomizerController; +import com.android.systemui.qs.dagger.QSScope; +import com.android.systemui.settings.BrightnessController; +import com.android.systemui.statusbar.policy.BrightnessMirrorController; +import com.android.systemui.tuner.TunerService; + +import javax.inject.Inject; + +/** + * Controller for {@link QSPanel}. + */ +@QSScope +public class QSPanelController extends QSPanelControllerBase<QSPanel> { + private final QSSecurityFooter mQsSecurityFooter; + private final TunerService mTunerService; + private final QSCustomizerController mQsCustomizerController; + private final BrightnessController mBrightnessController; + + private final QSPanel.OnConfigurationChangedListener mOnConfigurationChangedListener = + new QSPanel.OnConfigurationChangedListener() { + @Override + public void onConfigurationChange(Configuration newConfig) { + mView.updateResources(); + mQsSecurityFooter.onConfigurationChanged(); + if (mView.isListening()) { + refreshAllTiles(); + } + } + }; + private BrightnessMirrorController mBrightnessMirrorController; + + @Inject + QSPanelController(QSPanel view, QSSecurityFooter qsSecurityFooter, TunerService tunerService, + QSTileHost qstileHost, QSCustomizerController qsCustomizerController, + QSTileRevealController.Factory qsTileRevealControllerFactory, + DumpManager dumpManager, MetricsLogger metricsLogger, UiEventLogger uiEventLogger, + BrightnessController.Factory brightnessControllerFactory) { + super(view, qstileHost, qsCustomizerController, qsTileRevealControllerFactory, + metricsLogger, uiEventLogger, dumpManager); + mQsSecurityFooter = qsSecurityFooter; + mTunerService = tunerService; + mQsCustomizerController = qsCustomizerController; + mQsSecurityFooter.setHostEnvironment(qstileHost); + mBrightnessController = brightnessControllerFactory.create( + mView.findViewById(R.id.brightness_slider)); + } + + @Override + public void onInit() { + mQsCustomizerController.init(); + } + + @Override + protected void onViewAttached() { + super.onViewAttached(); + mTunerService.addTunable(mView, QS_SHOW_BRIGHTNESS); + mView.updateResources(); + if (mView.isListening()) { + refreshAllTiles(); + } + mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener); + mView.setSecurityFooter(mQsSecurityFooter.getView()); + switchTileLayout(true); + if (mBrightnessMirrorController != null) { + mBrightnessMirrorController.addCallback(mView); + } + } + + @Override + protected void onViewDetached() { + mTunerService.removeTunable(mView); + mView.removeOnConfigurationChangedListener(mOnConfigurationChangedListener); + if (mBrightnessMirrorController != null) { + mBrightnessMirrorController.removeCallback(mView); + } + super.onViewDetached(); + } + + /** TODO(b/168904199): Remove this method once view is controllerized. */ + QSPanel getView() { + return mView; + } + + /** + * Set the header container of quick settings. + */ + public void setHeaderContainer(@NonNull ViewGroup headerContainer) { + mView.setHeaderContainer(headerContainer); + } + + public QSPanel.QSTileLayout getTileLayout() { + return mView.getTileLayout(); + } + + /** */ + public void setVisibility(int visibility) { + mView.setVisibility(visibility); + } + + /** */ + public void setListening(boolean listening, boolean expanded) { + setListening(listening && expanded); + if (mView.isListening()) { + refreshAllTiles(); + } + + mQsSecurityFooter.setListening(listening); + + // Set the listening as soon as the QS fragment starts listening regardless of the + //expansion, so it will update the current brightness before the slider is visible. + if (listening) { + mBrightnessController.registerCallbacks(); + } else { + mBrightnessController.unregisterCallbacks(); + } + } + + /** */ + public MediaHost getMediaHost() { + return mView.getMediaHost(); + } + + /** */ + public void setBrightnessMirror(BrightnessMirrorController brightnessMirrorController) { + mBrightnessMirrorController = brightnessMirrorController; + mView.setBrightnessMirror(brightnessMirrorController); + } + + /** Get the QSTileHost this panel uses. */ + public QSTileHost getHost() { + return mHost; + } + + + /** Open the details for a specific tile.. */ + public void openDetails(String subPanel) { + QSTile tile = getTile(subPanel); + if (tile != null) { + mView.openDetails(tile); + } + } + + /** Show the device monitoring dialog. */ + public void showDeviceMonitoringDialog() { + mQsSecurityFooter.showDeviceMonitoringDialog(); + } + + /** Update appearance of QSPanel. */ + public void updateResources() { + mView.updateResources(); + } + + /** Update state of all tiles. */ + public void refreshAllTiles() { + mBrightnessController.checkRestrictionAndSetEnabled(); + super.refreshAllTiles(); + mQsSecurityFooter.refreshState(); + } + + /** Start customizing the Quick Settings. */ + public void showEdit(View view) { + view.post(() -> { + if (!mQsCustomizerController.isCustomizing()) { + int[] loc = view.getLocationOnScreen(); + int x = loc[0] + view.getWidth() / 2; + int y = loc[1] + view.getHeight() / 2; + mQsCustomizerController.show(x, y, false); + } + }); + } + + /** */ + public void setCallback(QSDetail.Callback qsPanelCallback) { + mView.setCallback(qsPanelCallback); + } + + /** */ + public void setGridContentVisibility(boolean visible) { + mView.setGridContentVisibility(visible); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java new file mode 100644 index 000000000000..0a4151b38210 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java @@ -0,0 +1,273 @@ +/* + * 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.qs; + +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent; + +import android.content.ComponentName; +import android.content.res.Configuration; +import android.metrics.LogMaker; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.UiEventLogger; +import com.android.systemui.Dumpable; +import com.android.systemui.dump.DumpManager; +import com.android.systemui.media.MediaHost; +import com.android.systemui.plugins.qs.QSTile; +import com.android.systemui.plugins.qs.QSTileView; +import com.android.systemui.qs.customize.QSCustomizerController; +import com.android.systemui.qs.external.CustomTile; +import com.android.systemui.util.ViewController; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.stream.Collectors; + +/** + * Controller for QSPanel views. + * + * @param <T> Type of QSPanel. + */ +public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewController<T> + implements Dumpable{ + protected final QSTileHost mHost; + private final QSCustomizerController mQsCustomizerController; + private final QSTileRevealController.Factory mQsTileRevealControllerFactory; + private final MediaHost mMediaHost; + private final MetricsLogger mMetricsLogger; + private final UiEventLogger mUiEventLogger; + private final DumpManager mDumpManager; + protected final ArrayList<TileRecord> mRecords = new ArrayList<>(); + + private int mLastOrientation; + private QSTileRevealController mQsTileRevealController; + + private final QSHost.Callback mQSHostCallback = this::setTiles; + + private final QSPanel.OnConfigurationChangedListener mOnConfigurationChangedListener = + new QSPanel.OnConfigurationChangedListener() { + @Override + public void onConfigurationChange(Configuration newConfig) { + if (newConfig.orientation != mLastOrientation) { + mLastOrientation = newConfig.orientation; + switchTileLayout(false); + } + } + }; + private String mCachedSpecs = ""; + + protected QSPanelControllerBase(T view, QSTileHost host, + QSCustomizerController qsCustomizerController, + QSTileRevealController.Factory qsTileRevealControllerFactory, + MetricsLogger metricsLogger, UiEventLogger uiEventLogger, DumpManager dumpManager) { + super(view); + mHost = host; + mQsCustomizerController = qsCustomizerController; + mQsTileRevealControllerFactory = qsTileRevealControllerFactory; + mMediaHost = mView.getMediaHost(); + mMetricsLogger = metricsLogger; + mUiEventLogger = uiEventLogger; + mDumpManager = dumpManager; + } + + @Override + protected void onViewAttached() { + QSPanel.QSTileLayout regularTileLayout = mView.createRegularTileLayout(); + if (regularTileLayout instanceof PagedTileLayout) { + mQsTileRevealController = mQsTileRevealControllerFactory.create( + (PagedTileLayout) regularTileLayout); + } + + mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener); + mHost.addCallback(mQSHostCallback); + mMediaHost.addVisibilityChangeListener(aBoolean -> { + switchTileLayout(false); + return null; + }); + setTiles(); + switchTileLayout(true); + mDumpManager.registerDumpable(mView.getDumpableTag(), this); + } + + @Override + protected void onViewDetached() { + mView.removeOnConfigurationChangedListener(mOnConfigurationChangedListener); + mHost.removeCallback(mQSHostCallback); + + for (TileRecord record : mRecords) { + record.tile.removeCallbacks(); + } + mRecords.clear(); + mDumpManager.unregisterDumpable(mView.getDumpableTag()); + } + + /** */ + public void setTiles() { + setTiles(mHost.getTiles(), false); + } + + /** */ + public void setTiles(Collection<QSTile> tiles, boolean collapsedView) { + if (!collapsedView) { + mQsTileRevealController.updateRevealedTiles(tiles); + } + for (QSPanelControllerBase.TileRecord record : mRecords) { + mView.removeTile(record); + record.tile.removeCallback(record.callback); + } + mRecords.clear(); + mCachedSpecs = ""; + for (QSTile tile : tiles) { + addTile(tile, collapsedView); + } + } + + /** */ + public void refreshAllTiles() { + for (QSPanelControllerBase.TileRecord r : mRecords) { + r.tile.refreshState(); + } + } + + private void addTile(final QSTile tile, boolean collapsedView) { + final TileRecord r = new TileRecord(); + r.tile = tile; + r.tileView = mHost.createTileView(tile, collapsedView); + mView.addTile(r); + mRecords.add(r); + mCachedSpecs = getTilesSpecs(); + + } + + /** */ + public void clickTile(ComponentName tile) { + final String spec = CustomTile.toSpec(tile); + for (TileRecord record : mRecords) { + if (record.tile.getTileSpec().equals(spec)) { + record.tile.click(); + break; + } + } + } + protected QSTile getTile(String subPanel) { + for (int i = 0; i < mRecords.size(); i++) { + if (subPanel.equals(mRecords.get(i).tile.getTileSpec())) { + return mRecords.get(i).tile; + } + } + return mHost.createTile(subPanel); + } + + + QSTileView getTileView(QSTile tile) { + for (QSPanelControllerBase.TileRecord r : mRecords) { + if (r.tile == tile) { + return r.tileView; + } + } + return null; + } + + private String getTilesSpecs() { + return mRecords.stream() + .map(tileRecord -> tileRecord.tile.getTileSpec()) + .collect(Collectors.joining(",")); + } + + + /** */ + public void setExpanded(boolean expanded) { + mView.setExpanded(expanded); + mMetricsLogger.visibility(MetricsEvent.QS_PANEL, expanded); + if (!expanded) { + mUiEventLogger.log(mView.closePanelEvent()); + closeDetail(); + } else { + mUiEventLogger.log(mView.openPanelEvent()); + logTiles(); + } + } + + /** */ + public void closeDetail() { + if (mQsCustomizerController.isShown()) { + mQsCustomizerController.hide(); + return; + } + mView.closeDetail(); + } + + /** */ + public void openDetails(String subPanel) { + QSTile tile = getTile(subPanel); + // If there's no tile with that name (as defined in QSFactoryImpl or other QSFactory), + // QSFactory will not be able to create a tile and getTile will return null + if (tile != null) { + mView.showDetailAdapter( + true, tile.getDetailAdapter(), new int[]{mView.getWidth() / 2, 0}); + } + } + + + void setListening(boolean listening) { + mView.setListening(listening, mCachedSpecs); + } + + boolean switchTileLayout(boolean force) { + if (mView.switchTileLayout(force, mRecords)) { + setTiles(); + mView.reSetLayoutListening(); + return true; + } + return false; + } + + private void logTiles() { + for (int i = 0; i < mRecords.size(); i++) { + QSTile tile = mRecords.get(i).tile; + mMetricsLogger.write(tile.populate(new LogMaker(tile.getMetricsCategory()) + .setType(MetricsEvent.TYPE_OPEN))); + } + } + + /** */ + public QSTileRevealController getQsTileRevealController() { + return mQsTileRevealController; + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println(getClass().getSimpleName() + ":"); + pw.println(" Tile records:"); + for (QSPanelControllerBase.TileRecord record : mRecords) { + if (record.tile instanceof Dumpable) { + pw.print(" "); ((Dumpable) record.tile).dump(fd, pw, args); + pw.print(" "); pw.println(record.tileView.toString()); + } + } + } + + /** */ + public static final class TileRecord extends QSPanel.Record { + public QSTile tile; + public com.android.systemui.plugins.qs.QSTileView tileView; + public boolean scanState; + public QSTile.Callback callback; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java index 0891972c11d2..270fcbffbd71 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java @@ -16,11 +16,13 @@ package com.android.systemui.qs; import android.app.AlertDialog; +import android.app.admin.DeviceAdminInfo; import android.app.admin.DevicePolicyEventLogger; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.UserInfo; +import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -39,16 +41,22 @@ import android.view.Window; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.VisibleForTesting; + import com.android.internal.util.FrameworkStatsLog; import com.android.systemui.Dependency; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.qs.dagger.QSScope; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.SecurityController; -public class QSSecurityFooter implements OnClickListener, DialogInterface.OnClickListener { +import javax.inject.Inject; + +@QSScope +class QSSecurityFooter implements OnClickListener, DialogInterface.OnClickListener { protected static final String TAG = "QSSecurityFooter"; protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final boolean DEBUG_FORCE_VISIBLE = false; @@ -72,12 +80,13 @@ public class QSSecurityFooter implements OnClickListener, DialogInterface.OnClic private int mFooterTextId; private int mFooterIconId; + @Inject public QSSecurityFooter(QSPanel qsPanel, Context context, UserTracker userTracker) { mRootView = LayoutInflater.from(context) .inflate(R.layout.quick_settings_footer, qsPanel, false); mRootView.setOnClickListener(this); - mFooterText = (TextView) mRootView.findViewById(R.id.footer_text); - mFooterIcon = (ImageView) mRootView.findViewById(R.id.footer_icon); + mFooterText = mRootView.findViewById(R.id.footer_text); + mFooterIcon = mRootView.findViewById(R.id.footer_icon); mFooterIconId = R.drawable.ic_info_outline; mContext = context; mMainHandler = new Handler(Looper.myLooper()); @@ -151,15 +160,16 @@ public class QSSecurityFooter implements OnClickListener, DialogInterface.OnClic mSecurityController.getWorkProfileOrganizationName(); final boolean isProfileOwnerOfOrganizationOwnedDevice = mSecurityController.isProfileOwnerOfOrganizationOwnedDevice(); + final boolean isParentalControlsEnabled = mSecurityController.isParentalControlsEnabled(); // Update visibility of footer mIsVisible = (isDeviceManaged && !isDemoDevice) || hasCACerts || hasCACertsInWorkProfile || vpnName != null || vpnNameWorkProfile != null - || isProfileOwnerOfOrganizationOwnedDevice; + || isProfileOwnerOfOrganizationOwnedDevice || isParentalControlsEnabled; // Update the string mFooterTextContent = getFooterText(isDeviceManaged, hasWorkProfile, hasCACerts, hasCACertsInWorkProfile, isNetworkLoggingEnabled, vpnName, vpnNameWorkProfile, organizationName, workProfileOrganizationName, - isProfileOwnerOfOrganizationOwnedDevice); + isProfileOwnerOfOrganizationOwnedDevice, isParentalControlsEnabled); // Update the icon int footerIconId = R.drawable.ic_info_outline; if (vpnName != null || vpnNameWorkProfile != null) { @@ -180,7 +190,10 @@ public class QSSecurityFooter implements OnClickListener, DialogInterface.OnClic boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled, String vpnName, String vpnNameWorkProfile, CharSequence organizationName, CharSequence workProfileOrganizationName, - boolean isProfileOwnerOfOrganizationOwnedDevice) { + boolean isProfileOwnerOfOrganizationOwnedDevice, boolean isParentalControlsEnabled) { + if (isParentalControlsEnabled) { + return mContext.getString(R.string.quick_settings_disclosure_parental_controls); + } if (isDeviceManaged || DEBUG_FORCE_VISIBLE) { if (hasCACerts || hasCACertsInWorkProfile || isNetworkLoggingEnabled) { if (organizationName == null) { @@ -263,6 +276,27 @@ public class QSSecurityFooter implements OnClickListener, DialogInterface.OnClic } private void createDialog() { + mDialog = new SystemUIDialog(mContext); + mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + mDialog.setButton(DialogInterface.BUTTON_POSITIVE, getPositiveButton(), this); + mDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getNegativeButton(), this); + + mDialog.setView(createDialogView()); + + mDialog.show(); + mDialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + + @VisibleForTesting + View createDialogView() { + if (mSecurityController.isParentalControlsEnabled()) { + return createParentalControlsDialogView(); + } + return createOrganizationDialogView(); + } + + private View createOrganizationDialogView() { final boolean isDeviceManaged = mSecurityController.isDeviceManaged(); boolean isProfileOwnerOfOrganizationOwnedDevice = mSecurityController.isProfileOwnerOfOrganizationOwnedDevice(); @@ -277,13 +311,10 @@ public class QSSecurityFooter implements OnClickListener, DialogInterface.OnClic final String vpnName = mSecurityController.getPrimaryVpnName(); final String vpnNameWorkProfile = mSecurityController.getWorkProfileVpnName(); - mDialog = new SystemUIDialog(mContext); - mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + View dialogView = LayoutInflater.from( new ContextThemeWrapper(mContext, R.style.Theme_SystemUI_Dialog)) .inflate(R.layout.quick_settings_footer_dialog, null, false); - mDialog.setView(dialogView); - mDialog.setButton(DialogInterface.BUTTON_POSITIVE, getPositiveButton(), this); // device management section CharSequence managementMessage = getManagementMessage(isDeviceManaged, @@ -348,9 +379,26 @@ public class QSSecurityFooter implements OnClickListener, DialogInterface.OnClic vpnMessage != null, dialogView); - mDialog.show(); - mDialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT); + return dialogView; + } + + private View createParentalControlsDialogView() { + View dialogView = LayoutInflater.from( + new ContextThemeWrapper(mContext, R.style.Theme_SystemUI_Dialog)) + .inflate(R.layout.quick_settings_footer_dialog_parental_controls, null, false); + + DeviceAdminInfo info = mSecurityController.getDeviceAdminInfo(); + Drawable icon = mSecurityController.getIcon(info); + if (icon != null) { + ImageView imageView = (ImageView) dialogView.findViewById(R.id.parental_controls_icon); + imageView.setImageDrawable(icon); + } + + TextView parentalControlsTitle = + (TextView) dialogView.findViewById(R.id.parental_controls_title); + parentalControlsTitle.setText(mSecurityController.getLabel(info)); + + return dialogView; } protected void configSubtitleVisibility(boolean showDeviceManagement, boolean showCaCerts, @@ -389,6 +437,13 @@ public class QSSecurityFooter implements OnClickListener, DialogInterface.OnClic return mContext.getString(R.string.ok); } + private String getNegativeButton() { + if (mSecurityController.isParentalControlsEnabled()) { + return mContext.getString(R.string.monitoring_button_view_controls); + } + return null; + } + protected CharSequence getManagementMessage(boolean isDeviceManaged, CharSequence organizationName, boolean isProfileOwnerOfOrganizationOwnedDevice, CharSequence workProfileOrganizationName) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileRevealController.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileRevealController.java index 2f012e6e608e..9414d0e3ed52 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileRevealController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileRevealController.java @@ -8,17 +8,23 @@ import android.util.ArraySet; import com.android.systemui.Prefs; import com.android.systemui.plugins.qs.QSTile; +import com.android.systemui.qs.customize.QSCustomizerController; +import com.android.systemui.qs.dagger.QSScope; import java.util.Collection; import java.util.Collections; import java.util.Set; +import javax.inject.Inject; + +/** */ public class QSTileRevealController { private static final long QS_REVEAL_TILES_DELAY = 500L; private final Context mContext; private final QSPanel mQSPanel; private final PagedTileLayout mPagedTileLayout; + private final QSCustomizerController mQsCustomizerController; private final ArraySet<String> mTilesToReveal = new ArraySet<>(); private final Handler mHandler = new Handler(); @@ -33,11 +39,12 @@ public class QSTileRevealController { }); } }; - - QSTileRevealController(Context context, QSPanel qsPanel, PagedTileLayout pagedTileLayout) { + QSTileRevealController(Context context, QSPanel qsPanel, PagedTileLayout pagedTileLayout, + QSCustomizerController qsCustomizerController) { mContext = context; mQSPanel = qsPanel; mPagedTileLayout = pagedTileLayout; + mQsCustomizerController = qsCustomizerController; } public void setExpansion(float expansion) { @@ -56,7 +63,7 @@ public class QSTileRevealController { final Set<String> revealedTiles = Prefs.getStringSet( mContext, QS_TILE_SPECS_REVEALED, Collections.EMPTY_SET); - if (revealedTiles.isEmpty() || mQSPanel.isShowingCustomize()) { + if (revealedTiles.isEmpty() || mQsCustomizerController.isCustomizing()) { // Do not reveal QS tiles the user has upon first load or those that they directly // added through customization. addTileSpecsToRevealed(tileSpecs); @@ -73,4 +80,24 @@ public class QSTileRevealController { revealedTiles.addAll(specs); Prefs.putStringSet(mContext, QS_TILE_SPECS_REVEALED, revealedTiles); } + + /** TODO(b/168904199): Remove this once QSPanel has its rejection removed. */ + @QSScope + static class Factory { + private final Context mContext; + private final QSPanel mQsPanel; + private final QSCustomizerController mQsCustomizerController; + + @Inject + Factory(Context context, QSPanel qsPanel, QSCustomizerController qsCustomizerController) { + mContext = context; + mQsPanel = qsPanel; + mQsCustomizerController = qsCustomizerController; + } + + QSTileRevealController create(PagedTileLayout pagedTileLayout) { + return new QSTileRevealController(mContext, mQsPanel, pagedTileLayout, + mQsCustomizerController); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java index ea036f6fe0e5..ed0900d07b56 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java @@ -16,6 +16,7 @@ package com.android.systemui.qs; +import static com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL; import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT; import android.content.Context; @@ -27,23 +28,13 @@ import android.view.View; import android.widget.LinearLayout; import com.android.internal.logging.UiEventLogger; -import com.android.systemui.Dependency; import com.android.systemui.R; -import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.dump.DumpManager; import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTile.SignalState; import com.android.systemui.plugins.qs.QSTile.State; -import com.android.systemui.qs.customize.QSCustomizer; import com.android.systemui.qs.logging.QSLogger; -import com.android.systemui.settings.UserTracker; -import com.android.systemui.tuner.TunerService; -import com.android.systemui.tuner.TunerService.Tunable; - -import java.util.ArrayList; -import java.util.Collection; import javax.inject.Inject; import javax.inject.Named; @@ -67,15 +58,10 @@ public class QuickQSPanel extends QSPanel { public QuickQSPanel( @Named(VIEW_CONTEXT) Context context, AttributeSet attrs, - DumpManager dumpManager, - BroadcastDispatcher broadcastDispatcher, QSLogger qsLogger, - MediaHost mediaHost, - UiEventLogger uiEventLogger, - UserTracker userTracker - ) { - super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, mediaHost, uiEventLogger, - userTracker); + @Named(QUICK_QS_PANEL) MediaHost mediaHost, + UiEventLogger uiEventLogger) { + super(context, attrs, qsLogger, mediaHost, uiEventLogger); sDefaultMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_columns); applyBottomMargin((View) mRegularTileLayout); } @@ -88,17 +74,12 @@ public class QuickQSPanel extends QSPanel { } @Override - protected void addSecurityFooter() { - // No footer needed - } - - @Override protected void addViewsAboveTiles() { // Nothing to add above the tiles } @Override - protected TileLayout createRegularTileLayout() { + public TileLayout createRegularTileLayout() { return new QuickQSPanel.HeaderTileLayout(mContext, mUiEventLogger); } @@ -108,6 +89,7 @@ public class QuickQSPanel extends QSPanel { } @Override + protected void initMediaHostState() { mMediaHost.setExpansion(0.0f); mMediaHost.setShowsOnlyActiveMedia(true); @@ -131,18 +113,6 @@ public class QuickQSPanel extends QSPanel { } @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - Dependency.get(TunerService.class).addTunable(mNumTiles, NUM_QUICK_TILES); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - Dependency.get(TunerService.class).removeTunable(mNumTiles); - } - - @Override protected String getDumpableTag() { return TAG; } @@ -157,7 +127,7 @@ public class QuickQSPanel extends QSPanel { } @Override - protected void drawTile(TileRecord r, State state) { + protected void drawTile(QSPanelControllerBase.TileRecord r, State state) { if (state instanceof SignalState) { SignalState copy = new SignalState(); state.copyTo(copy); @@ -169,17 +139,8 @@ public class QuickQSPanel extends QSPanel { super.drawTile(r, state); } - @Override - public void setHost(QSTileHost host, QSCustomizer customizer) { - super.setHost(host, customizer); - setTiles(mHost.getTiles()); - } - public void setMaxTiles(int maxTiles) { mMaxTiles = maxTiles; - if (mHost != null) { - setTiles(mHost.getTiles()); - } } @Override @@ -190,25 +151,6 @@ public class QuickQSPanel extends QSPanel { } } - @Override - public void setTiles(Collection<QSTile> tiles) { - ArrayList<QSTile> quickTiles = new ArrayList<>(); - for (QSTile tile : tiles) { - quickTiles.add(tile); - if (quickTiles.size() == mMaxTiles) { - break; - } - } - super.setTiles(quickTiles, true); - } - - private final Tunable mNumTiles = new Tunable() { - @Override - public void onTuningChanged(String key, String newValue) { - setMaxTiles(parseNumTiles(newValue)); - } - }; - public int getNumQuickTiles() { return mMaxTiles; } @@ -306,7 +248,7 @@ public class QuickQSPanel extends QSPanel { } @Override - protected void addTileView(TileRecord tile) { + protected void addTileView(QSPanelControllerBase.TileRecord tile) { addView(tile.tileView, getChildCount(), generateTileLayoutParams()); } @@ -369,7 +311,7 @@ public class QuickQSPanel extends QSPanel { private void setAccessibilityOrder() { if (mRecords != null && mRecords.size() > 0) { View previousView = this; - for (TileRecord record : mRecords) { + for (QSPanelControllerBase.TileRecord record : mRecords) { if (record.tileView.getVisibility() == GONE) continue; previousView = record.tileView.updateAccessibilityOrder(previousView); } @@ -381,7 +323,7 @@ public class QuickQSPanel extends QSPanel { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Measure each QS tile. - for (TileRecord record : mRecords) { + for (QSPanelControllerBase.TileRecord record : mRecords) { if (record.tileView.getVisibility() == GONE) continue; record.tileView.measure(exactly(mCellWidth), exactly(mCellHeight)); } @@ -394,7 +336,7 @@ public class QuickQSPanel extends QSPanel { @Override public int getNumVisibleTiles() { - return mColumns; + return Math.min(mRecords.size(), mColumns); } @Override @@ -411,6 +353,7 @@ public class QuickQSPanel extends QSPanel { boolean startedListening = !mListening && listening; super.setListening(listening); if (startedListening) { + // getNumVisibleTiles() <= mRecords.size() for (int i = 0; i < getNumVisibleTiles(); i++) { QSTile tile = mRecords.get(i).tile; mUiEventLogger.logWithInstanceId(QSEvent.QQS_TILE_VISIBLE, 0, diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java new file mode 100644 index 000000000000..a718271998c9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java @@ -0,0 +1,87 @@ +/* + * 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.qs; + +import static com.android.systemui.qs.QuickQSPanel.NUM_QUICK_TILES; +import static com.android.systemui.qs.QuickQSPanel.parseNumTiles; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.UiEventLogger; +import com.android.systemui.dump.DumpManager; +import com.android.systemui.plugins.qs.QSTile; +import com.android.systemui.qs.customize.QSCustomizerController; +import com.android.systemui.qs.dagger.QSScope; +import com.android.systemui.tuner.TunerService; +import com.android.systemui.tuner.TunerService.Tunable; + +import java.util.ArrayList; + +import javax.inject.Inject; + +/** Controller for {@link QuickQSPanel}. */ +@QSScope +public class QuickQSPanelController extends QSPanelControllerBase<QuickQSPanel> { + private final Tunable mNumTiles = + (key, newValue) -> setMaxTiles(parseNumTiles(newValue)); + + private final TunerService mTunerService; + + @Inject + QuickQSPanelController(QuickQSPanel view, TunerService tunerService, QSTileHost qsTileHost, + QSCustomizerController qsCustomizerController, + QSTileRevealController.Factory qsTileRevealControllerFactory, + MetricsLogger metricsLogger, UiEventLogger uiEventLogger, + DumpManager dumpManager) { + super(view, qsTileHost, qsCustomizerController, qsTileRevealControllerFactory, + metricsLogger, uiEventLogger, dumpManager); + mTunerService = tunerService; + } + + @Override + protected void onViewAttached() { + super.onViewAttached(); + mTunerService.addTunable(mNumTiles, NUM_QUICK_TILES); + + } + + @Override + protected void onViewDetached() { + super.onViewDetached(); + mTunerService.removeTunable(mNumTiles); + } + + public boolean isListening() { + return mView.isListening(); + } + + private void setMaxTiles(int parseNumTiles) { + mView.setMaxTiles(parseNumTiles); + setTiles(); + } + + @Override + public void setTiles() { + ArrayList<QSTile> quickTiles = new ArrayList<>(); + for (QSTile tile : mHost.getTiles()) { + quickTiles.add(tile); + if (quickTiles.size() == mView.getNumQuickTiles()) { + break; + } + } + super.setTiles(quickTiles, true); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java index a9fbc744b38e..5757602b9d0f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java @@ -353,11 +353,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements LifecycleOwn mPrivacyChip.setExpanded(expansionFraction > 0.5); mPrivacyChipAlphaAnimator.setPosition(keyguardExpansionFraction); } - if (expansionFraction < 1 && expansionFraction > 0.99) { - if (mHeaderQsPanel.switchTileLayout()) { - updateResources(); - } - } + mKeyguardExpansionFraction = keyguardExpansionFraction; } @@ -446,7 +442,6 @@ public class QuickStatusBarHeader extends RelativeLayout implements LifecycleOwn public void setQSPanel(final QSPanel qsPanel) { //host.setHeaderView(mExpandIndicator); mHeaderQsPanel.setQSPanelAndHeader(qsPanel, this); - mHeaderQsPanel.setHost(qsPanel.getHost(), null /* No customization in header */); Rect tintArea = new Rect(0, 0, 0, 0); int colorForeground = Utils.getColorAttrDefaultColor(getContext(), diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java index 676a300b0ff2..32904a21cd3c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java @@ -43,7 +43,9 @@ import com.android.systemui.privacy.OngoingPrivacyChip; import com.android.systemui.privacy.PrivacyChipEvent; import com.android.systemui.privacy.PrivacyItem; import com.android.systemui.privacy.PrivacyItemController; +import com.android.systemui.privacy.logging.PrivacyLogger; import com.android.systemui.qs.carrier.QSCarrierGroupController; +import com.android.systemui.qs.dagger.QSScope; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.phone.StatusBarIconController; @@ -64,6 +66,7 @@ import javax.inject.Inject; /** * Controller for {@link QuickStatusBarHeader}. */ +@QSScope class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader> { private static final String TAG = "QuickStatusBarHeader"; @@ -74,7 +77,7 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader private final ActivityStarter mActivityStarter; private final UiEventLogger mUiEventLogger; private final QSCarrierGroupController mQSCarrierGroupController; - private final QuickQSPanel mHeaderQsPanel; + private final QuickQSPanelController mHeaderQsPanelController; private final LifecycleRegistry mLifecycle; private final OngoingPrivacyChip mPrivacyChip; private final Clock mClockView; @@ -88,11 +91,13 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader private final StatusIconContainer mIconContainer; private final StatusBarIconController.TintedIconManager mIconManager; private final DemoMode mDemoModeReceiver; + private final PrivacyLogger mPrivacyLogger; private boolean mListening; private AlarmClockInfo mNextAlarm; private boolean mAllIndicatorsEnabled; private boolean mMicCameraIndicatorsEnabled; + private boolean mLocationIndicatorsEnabled; private boolean mPrivacyChipLogged; private int mRingerMode = AudioManager.RINGER_MODE_NORMAL; @@ -156,6 +161,14 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader } } + @Override + public void onFlagLocationChanged(boolean flag) { + if (mLocationIndicatorsEnabled != flag) { + mLocationIndicatorsEnabled = flag; + update(); + } + } + private void update() { StatusIconContainer iconContainer = mView.requireViewById(R.id.statusIcons); iconContainer.setIgnoredSlots(getIgnoredIconSlots()); @@ -194,14 +207,16 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader } }; - private QuickStatusBarHeaderController(QuickStatusBarHeader view, + @Inject + QuickStatusBarHeaderController(QuickStatusBarHeader view, ZenModeController zenModeController, NextAlarmController nextAlarmController, PrivacyItemController privacyItemController, RingerModeTracker ringerModeTracker, ActivityStarter activityStarter, UiEventLogger uiEventLogger, QSTileHost qsTileHost, StatusBarIconController statusBarIconController, CommandQueue commandQueue, DemoModeController demoModeController, - UserTracker userTracker, - QSCarrierGroupController.Builder qsCarrierGroupControllerBuilder) { + UserTracker userTracker, QuickQSPanelController quickQSPanelController, + QSCarrierGroupController.Builder qsCarrierGroupControllerBuilder, + PrivacyLogger privacyLogger) { super(view); mZenModeController = zenModeController; mNextAlarmController = nextAlarmController; @@ -215,6 +230,8 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader mDemoModeController = demoModeController; mUserTracker = userTracker; mLifecycle = new LifecycleRegistry(mLifecycleOwner); + mHeaderQsPanelController = quickQSPanelController; + mPrivacyLogger = privacyLogger; mQSCarrierGroupController = qsCarrierGroupControllerBuilder .setQSCarrierGroup(mView.findViewById(R.id.carrier_group)) @@ -222,7 +239,6 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader mPrivacyChip = mView.findViewById(R.id.privacy_chip); - mHeaderQsPanel = mView.findViewById(R.id.quick_qs_panel); mNextAlarmContainer = mView.findViewById(R.id.alarm_container); mClockView = mView.findViewById(R.id.clock); mRingerContainer = mView.findViewById(R.id.ringer_container); @@ -245,14 +261,15 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader mRingerContainer.setOnClickListener(mOnClickListener); mPrivacyChip.setOnClickListener(mOnClickListener); + mAllIndicatorsEnabled = mPrivacyItemController.getAllIndicatorsAvailable(); + mMicCameraIndicatorsEnabled = mPrivacyItemController.getMicCameraAvailable(); + mLocationIndicatorsEnabled = mPrivacyItemController.getLocationAvailable(); + // Ignore privacy icons because they show in the space above QQS - mIconContainer.addIgnoredSlots(getIgnoredIconSlots()); + mIconContainer.setIgnoredSlots(getIgnoredIconSlots()); mIconContainer.setShouldRestrictIcons(false); mStatusBarIconController.addIconGroup(mIconManager); - mAllIndicatorsEnabled = mPrivacyItemController.getAllIndicatorsAvailable(); - mMicCameraIndicatorsEnabled = mPrivacyItemController.getMicCameraAvailable(); - setChipVisibility(mPrivacyChip.getVisibility() == View.VISIBLE); mView.onAttach(mIconManager); @@ -280,8 +297,12 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader } mListening = listening; - mHeaderQsPanel.setListening(listening); - if (mHeaderQsPanel.switchTileLayout()) { + mHeaderQsPanelController.setListening(listening); + if (mHeaderQsPanelController.isListening()) { + mHeaderQsPanelController.refreshAllTiles(); + } + + if (mHeaderQsPanelController.switchTileLayout(false)) { mView.updateResources(); } @@ -292,6 +313,7 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader // Get the most up to date info mAllIndicatorsEnabled = mPrivacyItemController.getAllIndicatorsAvailable(); mMicCameraIndicatorsEnabled = mPrivacyItemController.getMicCameraAvailable(); + mLocationIndicatorsEnabled = mPrivacyItemController.getLocationAvailable(); mPrivacyItemController.addCallback(mPICCallback); } else { mZenModeController.removeCallback(mZenModeControllerCallback); @@ -305,6 +327,7 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader private void setChipVisibility(boolean chipVisible) { if (chipVisible && getChipEnabled()) { mPrivacyChip.setVisibility(View.VISIBLE); + mPrivacyLogger.logChipVisible(true); // Makes sure that the chip is logged as viewed at most once each time QS is opened // mListening makes sure that the callback didn't return after the user closed QS if (!mPrivacyChipLogged && mListening) { @@ -312,6 +335,7 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader mUiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_VIEW); } } else { + mPrivacyLogger.logChipVisible(false); mPrivacyChip.setVisibility(View.GONE); } } @@ -319,21 +343,22 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader private List<String> getIgnoredIconSlots() { ArrayList<String> ignored = new ArrayList<>(); if (getChipEnabled()) { - ignored.add(mView.getResources().getString( - com.android.internal.R.string.status_bar_camera)); - ignored.add(mView.getResources().getString( - com.android.internal.R.string.status_bar_microphone)); - if (mAllIndicatorsEnabled) { + if (mAllIndicatorsEnabled || mMicCameraIndicatorsEnabled) { + ignored.add(mView.getResources().getString( + com.android.internal.R.string.status_bar_camera)); + ignored.add(mView.getResources().getString( + com.android.internal.R.string.status_bar_microphone)); + } + if (mAllIndicatorsEnabled || mLocationIndicatorsEnabled) { ignored.add(mView.getResources().getString( com.android.internal.R.string.status_bar_location)); } } - return ignored; } private boolean getChipEnabled() { - return mMicCameraIndicatorsEnabled || mAllIndicatorsEnabled; + return mMicCameraIndicatorsEnabled || mLocationIndicatorsEnabled || mAllIndicatorsEnabled; } private boolean isZenOverridingRinger() { @@ -369,55 +394,4 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader mClockView.onDemoModeFinished(); } } - - static class Builder { - private final ZenModeController mZenModeController; - private final NextAlarmController mNextAlarmController; - private final PrivacyItemController mPrivacyItemController; - private final RingerModeTracker mRingerModeTracker; - private final ActivityStarter mActivityStarter; - private final UiEventLogger mUiEventLogger; - private final QSTileHost mQsTileHost; - private final StatusBarIconController mStatusBarIconController; - private final CommandQueue mCommandQueue; - private final DemoModeController mDemoModeController; - private final UserTracker mUserTracker; - private final QSCarrierGroupController.Builder mQSCarrierGroupControllerBuilder; - private QuickStatusBarHeader mView; - - @Inject - Builder(ZenModeController zenModeController, NextAlarmController nextAlarmController, - PrivacyItemController privacyItemController, RingerModeTracker ringerModeTracker, - ActivityStarter activityStarter, UiEventLogger uiEventLogger, QSTileHost qsTileHost, - StatusBarIconController statusBarIconController, CommandQueue commandQueue, - DemoModeController demoModeController, UserTracker userTracker, - QSCarrierGroupController.Builder qsCarrierGroupControllerBuilder) { - mZenModeController = zenModeController; - mNextAlarmController = nextAlarmController; - mPrivacyItemController = privacyItemController; - mRingerModeTracker = ringerModeTracker; - mActivityStarter = activityStarter; - mUiEventLogger = uiEventLogger; - mQsTileHost = qsTileHost; - mStatusBarIconController = statusBarIconController; - mCommandQueue = commandQueue; - mDemoModeController = demoModeController; - mUserTracker = userTracker; - mQSCarrierGroupControllerBuilder = qsCarrierGroupControllerBuilder; - } - - public Builder setQuickStatusBarHeader(QuickStatusBarHeader view) { - mView = view; - return this; - } - - - QuickStatusBarHeaderController build() { - return new QuickStatusBarHeaderController(mView, mZenModeController, - mNextAlarmController, mPrivacyItemController, mRingerModeTracker, - mActivityStarter, mUiEventLogger, mQsTileHost, mStatusBarIconController, - mCommandQueue, mDemoModeController, mUserTracker, - mQSCarrierGroupControllerBuilder); - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/SecureSetting.java b/packages/SystemUI/src/com/android/systemui/qs/SecureSetting.java index 3ee3e117fb0f..994da9a174df 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/SecureSetting.java +++ b/packages/SystemUI/src/com/android/systemui/qs/SecureSetting.java @@ -16,18 +16,17 @@ package com.android.systemui.qs; -import android.content.Context; import android.database.ContentObserver; import android.os.Handler; -import android.provider.Settings.Secure; import com.android.systemui.statusbar.policy.Listenable; +import com.android.systemui.util.settings.SecureSettings; /** Helper for managing a secure setting. **/ public abstract class SecureSetting extends ContentObserver implements Listenable { private static final int DEFAULT = 0; - private final Context mContext; + private SecureSettings mSecureSettings; private final String mSettingName; private boolean mListening; @@ -36,19 +35,20 @@ public abstract class SecureSetting extends ContentObserver implements Listenabl protected abstract void handleValueChanged(int value, boolean observedChange); - public SecureSetting(Context context, Handler handler, String settingName, int userId) { + public SecureSetting(SecureSettings secureSettings, Handler handler, String settingName, + int userId) { super(handler); - mContext = context; + mSecureSettings = secureSettings; mSettingName = settingName; mUserId = userId; } public int getValue() { - return Secure.getIntForUser(mContext.getContentResolver(), mSettingName, DEFAULT, mUserId); + return mSecureSettings.getIntForUser(mSettingName, DEFAULT, mUserId); } public void setValue(int value) { - Secure.putIntForUser(mContext.getContentResolver(), mSettingName, value, mUserId); + mSecureSettings.putIntForUser(mSettingName, value, mUserId); } @Override @@ -57,10 +57,10 @@ public abstract class SecureSetting extends ContentObserver implements Listenabl mListening = listening; if (listening) { mObservedValue = getValue(); - mContext.getContentResolver().registerContentObserver( - Secure.getUriFor(mSettingName), false, this, mUserId); + mSecureSettings.registerContentObserverForUser( + mSecureSettings.getUriFor(mSettingName), false, this, mUserId); } else { - mContext.getContentResolver().unregisterContentObserver(this); + mSecureSettings.unregisterContentObserver(this); mObservedValue = DEFAULT; } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java index 694492a33524..4ab7afd46602 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java @@ -11,7 +11,7 @@ import android.view.ViewGroup; import com.android.systemui.R; import com.android.systemui.qs.QSPanel.QSTileLayout; -import com.android.systemui.qs.QSPanel.TileRecord; +import com.android.systemui.qs.QSPanelControllerBase.TileRecord; import java.util.ArrayList; diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java index 55b67e061c13..3291aa0e2099 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java @@ -20,41 +20,23 @@ import android.animation.Animator.AnimatorListener; import android.animation.AnimatorListenerAdapter; import android.content.Context; import android.content.res.Configuration; -import android.os.Bundle; import android.util.AttributeSet; import android.util.TypedValue; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.Menu; -import android.view.MenuItem; import android.view.View; import android.widget.LinearLayout; import android.widget.Toolbar; -import android.widget.Toolbar.OnMenuItemClickListener; -import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.recyclerview.widget.DefaultItemAnimator; -import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.android.internal.logging.UiEventLogger; -import com.android.internal.logging.UiEventLoggerImpl; import com.android.systemui.R; -import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.plugins.qs.QS; -import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.qs.QSDetailClipper; -import com.android.systemui.qs.QSEditEvent; -import com.android.systemui.qs.QSTileHost; import com.android.systemui.statusbar.phone.LightBarController; import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer; -import com.android.systemui.statusbar.policy.KeyguardStateController; -import com.android.systemui.statusbar.policy.KeyguardStateController.Callback; - -import java.util.ArrayList; -import java.util.List; - -import javax.inject.Inject; /** * Allows full-screen customization of QS, through show() and hide(). @@ -62,24 +44,16 @@ import javax.inject.Inject; * This adds itself to the status bar window, so it can appear on top of quick settings and * *someday* do fancy animations to get into/out of it. */ -public class QSCustomizer extends LinearLayout implements OnMenuItemClickListener { +public class QSCustomizer extends LinearLayout { - private static final int MENU_RESET = Menu.FIRST; - private static final String EXTRA_QS_CUSTOMIZING = "qs_customizing"; - private static final String TAG = "QSCustomizer"; + static final int MENU_RESET = Menu.FIRST; + static final String EXTRA_QS_CUSTOMIZING = "qs_customizing"; private final QSDetailClipper mClipper; - private final LightBarController mLightBarController; - private KeyguardStateController mKeyguardStateController; - private final ScreenLifecycle mScreenLifecycle; - private final TileQueryHelper mTileQueryHelper; private final View mTransparentView; private boolean isShown; - private QSTileHost mHost; - private RecyclerView mRecyclerView; - private TileAdapter mTileAdapter; - private Toolbar mToolbar; + private final RecyclerView mRecyclerView; private boolean mCustomizing; private NotificationsQuickSettingsContainer mNotifQsContainer; private QS mQs; @@ -87,92 +61,47 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene private int mY; private boolean mOpening; private boolean mIsShowingNavBackdrop; - private UiEventLogger mUiEventLogger = new UiEventLoggerImpl(); - - @Inject - public QSCustomizer(Context context, AttributeSet attrs, - LightBarController lightBarController, - KeyguardStateController keyguardStateController, - ScreenLifecycle screenLifecycle, - TileQueryHelper tileQueryHelper, - UiEventLogger uiEventLogger) { + + public QSCustomizer(Context context, AttributeSet attrs) { super(new ContextThemeWrapper(context, R.style.edit_theme), attrs); LayoutInflater.from(getContext()).inflate(R.layout.qs_customize_panel_content, this); mClipper = new QSDetailClipper(findViewById(R.id.customize_container)); - mToolbar = findViewById(com.android.internal.R.id.action_bar); + Toolbar toolbar = findViewById(com.android.internal.R.id.action_bar); TypedValue value = new TypedValue(); mContext.getTheme().resolveAttribute(android.R.attr.homeAsUpIndicator, value, true); - mToolbar.setNavigationIcon( + toolbar.setNavigationIcon( getResources().getDrawable(value.resourceId, mContext.getTheme())); - mToolbar.setNavigationOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - hide(); - } - }); - mToolbar.setOnMenuItemClickListener(this); - mToolbar.getMenu().add(Menu.NONE, MENU_RESET, 0, + + toolbar.getMenu().add(Menu.NONE, MENU_RESET, 0, mContext.getString(com.android.internal.R.string.reset)); - mToolbar.setTitle(R.string.qs_edit); + toolbar.setTitle(R.string.qs_edit); mRecyclerView = findViewById(android.R.id.list); mTransparentView = findViewById(R.id.customizer_transparent_view); - mTileAdapter = new TileAdapter(getContext(), uiEventLogger); - mTileQueryHelper = tileQueryHelper; - mTileQueryHelper.setListener(mTileAdapter); - mRecyclerView.setAdapter(mTileAdapter); - mTileAdapter.getItemTouchHelper().attachToRecyclerView(mRecyclerView); - GridLayoutManager layout = new GridLayoutManager(getContext(), 3) { - @Override - public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, - RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) { - // Do not read row and column every time it changes. - } - }; - layout.setSpanSizeLookup(mTileAdapter.getSizeLookup()); - mRecyclerView.setLayoutManager(layout); - mRecyclerView.addItemDecoration(mTileAdapter.getItemDecoration()); - mRecyclerView.addItemDecoration(mTileAdapter.getMarginItemDecoration()); DefaultItemAnimator animator = new DefaultItemAnimator(); animator.setMoveDuration(TileAdapter.MOVE_DURATION); mRecyclerView.setItemAnimator(animator); - mLightBarController = lightBarController; - mKeyguardStateController = keyguardStateController; - mScreenLifecycle = screenLifecycle; - updateNavBackDrop(getResources().getConfiguration()); } - @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - updateNavBackDrop(newConfig); - updateResources(); - } - - private void updateResources() { + void updateResources() { LayoutParams lp = (LayoutParams) mTransparentView.getLayoutParams(); lp.height = mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.quick_qs_offset_height); mTransparentView.setLayoutParams(lp); } - private void updateNavBackDrop(Configuration newConfig) { + void updateNavBackDrop(Configuration newConfig, LightBarController lightBarController) { View navBackdrop = findViewById(R.id.nav_bar_background); mIsShowingNavBackdrop = newConfig.smallestScreenWidthDp >= 600 || newConfig.orientation != Configuration.ORIENTATION_LANDSCAPE; if (navBackdrop != null) { navBackdrop.setVisibility(mIsShowingNavBackdrop ? View.VISIBLE : View.GONE); } - updateNavColors(); + updateNavColors(lightBarController); } - private void updateNavColors() { - mLightBarController.setQsCustomizing(mIsShowingNavBackdrop && isShown); - } - - public void setHost(QSTileHost host) { - mHost = host; - mTileAdapter.setHost(host); + void updateNavColors(LightBarController lightBarController) { + lightBarController.setQsCustomizing(mIsShowingNavBackdrop && isShown); } public void setContainer(NotificationsQuickSettingsContainer notificationsQsContainer) { @@ -186,39 +115,30 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene /** Animate and show QSCustomizer panel. * @param x,y Location on screen of {@code edit} button to determine center of animation. */ - public void show(int x, int y) { + void show(int x, int y, TileAdapter tileAdapter) { if (!isShown) { - int containerLocation[] = findViewById(R.id.customize_container).getLocationOnScreen(); + int[] containerLocation = findViewById(R.id.customize_container).getLocationOnScreen(); mX = x - containerLocation[0]; mY = y - containerLocation[1]; - mUiEventLogger.log(QSEditEvent.QS_EDIT_OPEN); isShown = true; mOpening = true; - setTileSpecs(); setVisibility(View.VISIBLE); - mClipper.animateCircularClip(mX, mY, true, mExpandAnimationListener); - queryTiles(); + mClipper.animateCircularClip(mX, mY, true, new ExpandAnimatorListener(tileAdapter)); mNotifQsContainer.setCustomizerAnimating(true); mNotifQsContainer.setCustomizerShowing(true); - mKeyguardStateController.addCallback(mKeyguardCallback); - updateNavColors(); } } - public void showImmediately() { + void showImmediately() { if (!isShown) { setVisibility(VISIBLE); mClipper.cancelAnimator(); mClipper.showBackground(); isShown = true; - setTileSpecs(); setCustomizing(true); - queryTiles(); mNotifQsContainer.setCustomizerAnimating(false); mNotifQsContainer.setCustomizerShowing(true); - mKeyguardStateController.addCallback(mKeyguardCallback); - updateNavColors(); } } @@ -227,9 +147,6 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene * {@link TileAdapter}. */ public void setContentPaddings(int paddingStart, int paddingEnd) { - int halfMargin = mContext.getResources() - .getDimensionPixelSize(R.dimen.qs_tile_margin_horizontal) / 2; - mTileAdapter.changeHalfMargin(halfMargin); mRecyclerView.setPaddingRelative( paddingStart, mRecyclerView.getPaddingTop(), @@ -238,22 +155,14 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene ); } - private void queryTiles() { - mTileQueryHelper.queryTiles(mHost); - } - - public void hide() { - final boolean animate = mScreenLifecycle.getScreenState() != ScreenLifecycle.SCREEN_OFF; + /** Hide the customizer. */ + public void hide(boolean animate) { if (isShown) { - mUiEventLogger.log(QSEditEvent.QS_EDIT_CLOSED); isShown = false; - mToolbar.dismissPopupMenus(); mClipper.cancelAnimator(); // Make sure we're not opening (because we're closing). Nobody can think we are // customizing after the next two lines. mOpening = false; - setCustomizing(false); - save(); if (animate) { mClipper.animateCircularClip(mX, mY, false, mCollapseAnimationListener); } else { @@ -261,8 +170,6 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene } mNotifQsContainer.setCustomizerAnimating(animate); mNotifQsContainer.setCustomizerShowing(false); - mKeyguardStateController.removeCallback(mKeyguardCallback); - updateNavColors(); } } @@ -270,7 +177,7 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene return isShown; } - private void setCustomizing(boolean customizing) { + void setCustomizing(boolean customizing) { mCustomizing = customizing; mQs.notifyCustomizeChanged(); } @@ -279,78 +186,21 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene return mCustomizing || mOpening; } - @Override - public boolean onMenuItemClick(MenuItem item) { - switch (item.getItemId()) { - case MENU_RESET: - mUiEventLogger.log(QSEditEvent.QS_EDIT_RESET); - reset(); - break; - } - return false; - } - - private void reset() { - mTileAdapter.resetTileSpecs(mHost, QSTileHost.getDefaultSpecs(mContext)); - } - - private void setTileSpecs() { - List<String> specs = new ArrayList<>(); - for (QSTile tile : mHost.getTiles()) { - specs.add(tile.getTileSpec()); - } - mTileAdapter.setTileSpecs(specs); - mRecyclerView.setAdapter(mTileAdapter); - } - - private void save() { - if (mTileQueryHelper.isFinished()) { - mTileAdapter.saveSpecs(mHost); - } - } - - - public void saveInstanceState(Bundle outState) { - if (isShown) { - mKeyguardStateController.removeCallback(mKeyguardCallback); - } - outState.putBoolean(EXTRA_QS_CUSTOMIZING, mCustomizing); - } - - public void restoreInstanceState(Bundle savedInstanceState) { - boolean customizing = savedInstanceState.getBoolean(EXTRA_QS_CUSTOMIZING); - if (customizing) { - setVisibility(VISIBLE); - addOnLayoutChangeListener(new OnLayoutChangeListener() { - @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, - int oldLeft, - int oldTop, int oldRight, int oldBottom) { - removeOnLayoutChangeListener(this); - showImmediately(); - } - }); - } - } /** @param x,y Location on screen of animation center. */ public void setEditLocation(int x, int y) { - int containerLocation[] = findViewById(R.id.customize_container).getLocationOnScreen(); + int[] containerLocation = findViewById(R.id.customize_container).getLocationOnScreen(); mX = x - containerLocation[0]; mY = y - containerLocation[1]; } - private final Callback mKeyguardCallback = new Callback() { - @Override - public void onKeyguardShowingChanged() { - if (!isAttachedToWindow()) return; - if (mKeyguardStateController.isShowing() && !mOpening) { - hide(); - } + class ExpandAnimatorListener extends AnimatorListenerAdapter { + private final TileAdapter mTileAdapter; + + ExpandAnimatorListener(TileAdapter tileAdapter) { + mTileAdapter = tileAdapter; } - }; - private final AnimatorListener mExpandAnimationListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (isShown) { @@ -358,6 +208,7 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene } mOpening = false; mNotifQsContainer.setCustomizerAnimating(false); + mRecyclerView.setAdapter(mTileAdapter); } @Override @@ -365,7 +216,7 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene mOpening = false; mNotifQsContainer.setCustomizerAnimating(false); } - }; + } private final AnimatorListener mCollapseAnimationListener = new AnimatorListenerAdapter() { @Override @@ -374,7 +225,6 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene setVisibility(View.GONE); } mNotifQsContainer.setCustomizerAnimating(false); - mRecyclerView.setAdapter(mTileAdapter); } @Override @@ -385,4 +235,12 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene mNotifQsContainer.setCustomizerAnimating(false); } }; -} + + public RecyclerView getRecyclerView() { + return mRecyclerView; + } + + public boolean isOpening() { + return mOpening; + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java new file mode 100644 index 000000000000..9f4c58b58cab --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java @@ -0,0 +1,247 @@ +/* + * 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.qs.customize; + +import static com.android.systemui.qs.customize.QSCustomizer.EXTRA_QS_CUSTOMIZING; +import static com.android.systemui.qs.customize.QSCustomizer.MENU_RESET; + +import android.content.res.Configuration; +import android.os.Bundle; +import android.view.MenuItem; +import android.view.View; +import android.widget.Toolbar; +import android.widget.Toolbar.OnMenuItemClickListener; + +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.internal.logging.UiEventLogger; +import com.android.systemui.R; +import com.android.systemui.keyguard.ScreenLifecycle; +import com.android.systemui.plugins.qs.QSTile; +import com.android.systemui.qs.QSEditEvent; +import com.android.systemui.qs.QSFragment; +import com.android.systemui.qs.QSTileHost; +import com.android.systemui.qs.dagger.QSScope; +import com.android.systemui.statusbar.phone.LightBarController; +import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer; +import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; +import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.ViewController; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +/** {@link ViewController} for {@link QSCustomizer}. */ +@QSScope +public class QSCustomizerController extends ViewController<QSCustomizer> { + private final TileQueryHelper mTileQueryHelper; + private final QSTileHost mQsTileHost; + private final TileAdapter mTileAdapter; + private final ScreenLifecycle mScreenLifecycle; + private final KeyguardStateController mKeyguardStateController; + private final LightBarController mLightBarController; + private final ConfigurationController mConfigurationController; + private final UiEventLogger mUiEventLogger; + private final Toolbar mToolbar; + + private final OnMenuItemClickListener mOnMenuItemClickListener = new OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + if (item.getItemId() == MENU_RESET) { + mUiEventLogger.log(QSEditEvent.QS_EDIT_RESET); + reset(); + } + return false; + } + }; + + private final KeyguardStateController.Callback mKeyguardCallback = + new KeyguardStateController.Callback() { + @Override + public void onKeyguardShowingChanged() { + if (!mView.isAttachedToWindow()) return; + if (mKeyguardStateController.isShowing() && !mView.isOpening()) { + hide(); + } + } + }; + + private final ConfigurationListener mConfigurationListener = new ConfigurationListener() { + @Override + public void onConfigChanged(Configuration newConfig) { + mView.updateNavBackDrop(newConfig, mLightBarController); + mView.updateResources(); + } + }; + + @Inject + protected QSCustomizerController(QSCustomizer view, TileQueryHelper tileQueryHelper, + QSTileHost qsTileHost, TileAdapter tileAdapter, ScreenLifecycle screenLifecycle, + KeyguardStateController keyguardStateController, LightBarController lightBarController, + ConfigurationController configurationController, UiEventLogger uiEventLogger) { + super(view); + mTileQueryHelper = tileQueryHelper; + mQsTileHost = qsTileHost; + mTileAdapter = tileAdapter; + mScreenLifecycle = screenLifecycle; + mKeyguardStateController = keyguardStateController; + mLightBarController = lightBarController; + mConfigurationController = configurationController; + mUiEventLogger = uiEventLogger; + + mToolbar = mView.findViewById(com.android.internal.R.id.action_bar); + } + + @Override + protected void onViewAttached() { + mView.updateNavBackDrop(getResources().getConfiguration(), mLightBarController); + + mConfigurationController.addCallback(mConfigurationListener); + + mTileQueryHelper.setListener(mTileAdapter); + int halfMargin = + getResources().getDimensionPixelSize(R.dimen.qs_tile_margin_horizontal) / 2; + mTileAdapter.changeHalfMargin(halfMargin); + + RecyclerView recyclerView = mView.getRecyclerView(); + recyclerView.setAdapter(mTileAdapter); + mTileAdapter.getItemTouchHelper().attachToRecyclerView(recyclerView); + GridLayoutManager layout = new GridLayoutManager(getContext(), 3) { + @Override + public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, + RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) { + // Do not read row and column every time it changes. + } + }; + layout.setSpanSizeLookup(mTileAdapter.getSizeLookup()); + recyclerView.setLayoutManager(layout); + recyclerView.addItemDecoration(mTileAdapter.getItemDecoration()); + recyclerView.addItemDecoration(mTileAdapter.getMarginItemDecoration()); + + mToolbar.setOnMenuItemClickListener(mOnMenuItemClickListener); + mToolbar.setNavigationOnClickListener(v -> hide()); + } + + @Override + protected void onViewDetached() { + mTileQueryHelper.setListener(null); + mToolbar.setOnMenuItemClickListener(null); + mConfigurationController.removeCallback(mConfigurationListener); + } + + + private void reset() { + mTileAdapter.resetTileSpecs(QSTileHost.getDefaultSpecs(getContext())); + } + + public boolean isCustomizing() { + return mView.isCustomizing(); + } + + /** */ + public void show(int x, int y, boolean immediate) { + if (!mView.isShown()) { + setTileSpecs(); + if (immediate) { + mView.showImmediately(); + } else { + mView.show(x, y, mTileAdapter); + mUiEventLogger.log(QSEditEvent.QS_EDIT_OPEN); + } + mTileQueryHelper.queryTiles(mQsTileHost); + mKeyguardStateController.addCallback(mKeyguardCallback); + mView.updateNavColors(mLightBarController); + } + } + + /** */ + public void setQs(QSFragment qsFragment) { + mView.setQs(qsFragment); + } + + /** */ + public void restoreInstanceState(Bundle savedInstanceState) { + boolean customizing = savedInstanceState.getBoolean(EXTRA_QS_CUSTOMIZING); + if (customizing) { + mView.setVisibility(View.VISIBLE); + mView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, + int oldTop, int oldRight, int oldBottom) { + mView.removeOnLayoutChangeListener(this); + show(0, 0, true); + } + }); + } + } + + /** */ + public void saveInstanceState(Bundle outState) { + if (mView.isShown()) { + mKeyguardStateController.removeCallback(mKeyguardCallback); + } + outState.putBoolean(EXTRA_QS_CUSTOMIZING, mView.isCustomizing()); + } + + /** */ + public void setEditLocation(int x, int y) { + mView.setEditLocation(x, y); + } + + /** */ + public void setContainer(NotificationsQuickSettingsContainer container) { + mView.setContainer(container); + } + + public boolean isShown() { + return mView.isShown(); + } + + /** Hice the customizer. */ + public void hide() { + final boolean animate = mScreenLifecycle.getScreenState() != ScreenLifecycle.SCREEN_OFF; + if (mView.isShown()) { + mUiEventLogger.log(QSEditEvent.QS_EDIT_CLOSED); + mToolbar.dismissPopupMenus(); + mView.setCustomizing(false); + save(); + mView.hide(animate); + mView.updateNavColors(mLightBarController); + mKeyguardStateController.removeCallback(mKeyguardCallback); + } + } + + private void save() { + if (mTileQueryHelper.isFinished()) { + mTileAdapter.saveSpecs(mQsTileHost); + } + } + + private void setTileSpecs() { + List<String> specs = new ArrayList<>(); + for (QSTile tile : mQsTileHost.getTiles()) { + specs.add(tile.getTileSpec()); + } + mTileAdapter.setTileSpecs(specs); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java index b471dfae02d1..dfc771beab1c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java @@ -46,12 +46,17 @@ import com.android.systemui.qs.QSTileHost; import com.android.systemui.qs.customize.TileAdapter.Holder; import com.android.systemui.qs.customize.TileQueryHelper.TileInfo; import com.android.systemui.qs.customize.TileQueryHelper.TileStateListener; +import com.android.systemui.qs.dagger.QSScope; import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.tileimpl.QSIconViewImpl; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; + +/** */ +@QSScope public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileStateListener { private static final long DRAG_LENGTH = 100; private static final float DRAG_SCALE = 1.2f; @@ -78,6 +83,7 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta private final ItemDecoration mDecoration; private final MarginTileDecoration mMarginDecoration; private final int mMinNumTiles; + private final QSTileHost mHost; private int mEditIndex; private int mTileDividerIndex; private int mFocusIndex; @@ -89,13 +95,14 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta private Holder mCurrentDrag; private int mAccessibilityAction = ACTION_NONE; private int mAccessibilityFromIndex; - private QSTileHost mHost; private final UiEventLogger mUiEventLogger; private final AccessibilityDelegateCompat mAccessibilityDelegate; private RecyclerView mRecyclerView; - public TileAdapter(Context context, UiEventLogger uiEventLogger) { + @Inject + public TileAdapter(Context context, QSTileHost qsHost, UiEventLogger uiEventLogger) { mContext = context; + mHost = qsHost; mUiEventLogger = uiEventLogger; mItemTouchHelper = new ItemTouchHelper(mCallbacks); mDecoration = new TileItemDecoration(context); @@ -114,10 +121,6 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta mRecyclerView = null; } - public void setHost(QSTileHost host) { - mHost = host; - } - public ItemTouchHelper getItemTouchHelper() { return mItemTouchHelper; } @@ -154,9 +157,10 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta mAccessibilityAction = ACTION_NONE; } - public void resetTileSpecs(QSTileHost host, List<String> specs) { + /** */ + public void resetTileSpecs(List<String> specs) { // Notify the host so the tiles get removed callbacks. - host.changeTiles(mCurrentSpecs, specs); + mHost.changeTiles(mCurrentSpecs, specs); setTileSpecs(specs); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java index b795a5f5ea19..59490c666a83 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java @@ -37,6 +37,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTile.State; import com.android.systemui.qs.QSTileHost; +import com.android.systemui.qs.dagger.QSScope; import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon; import com.android.systemui.settings.UserTracker; @@ -50,6 +51,8 @@ import java.util.concurrent.Executor; import javax.inject.Inject; +/** */ +@QSScope public class TileQueryHelper { private static final String TAG = "TileQueryHelper"; diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentComponent.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentComponent.java new file mode 100644 index 000000000000..8cc05026e1f1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentComponent.java @@ -0,0 +1,60 @@ +/* + * 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.qs.dagger; + +import com.android.systemui.qs.QSAnimator; +import com.android.systemui.qs.QSContainerImplController; +import com.android.systemui.qs.QSFooter; +import com.android.systemui.qs.QSFragment; +import com.android.systemui.qs.QSPanelController; +import com.android.systemui.qs.QuickQSPanelController; +import com.android.systemui.qs.customize.QSCustomizerController; + +import dagger.BindsInstance; +import dagger.Subcomponent; + +/** + * Dagger Subcomponent for {@link QSFragment}. + */ +@Subcomponent(modules = {QSFragmentModule.class}) +@QSScope +public interface QSFragmentComponent { + + /** Factory for building a {@link QSFragmentComponent}. */ + @Subcomponent.Factory + interface Factory { + QSFragmentComponent create(@BindsInstance QSFragment qsFragment); + } + + /** Construct a {@link QSPanelController}. */ + QSPanelController getQSPanelController(); + + /** Construct a {@link QuickQSPanelController}. */ + QuickQSPanelController getQuickQSPanelController(); + + /** Construct a {@link QSAnimator}. */ + QSAnimator getQSAnimator(); + + /** Construct a {@link QSContainerImplController}. */ + QSContainerImplController getQSContainerImplController(); + + /** Construct a {@link QSFooter} */ + QSFooter getQSFooter(); + + /** Construct a {@link QSCustomizerController}. */ + QSCustomizerController getQSCustomizerController(); +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java new file mode 100644 index 000000000000..354b2c944248 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java @@ -0,0 +1,98 @@ +/* + * 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.qs.dagger; + +import android.view.View; + +import com.android.systemui.R; +import com.android.systemui.dagger.qualifiers.RootView; +import com.android.systemui.plugins.qs.QS; +import com.android.systemui.qs.QSContainerImpl; +import com.android.systemui.qs.QSFooter; +import com.android.systemui.qs.QSFooterView; +import com.android.systemui.qs.QSFooterViewController; +import com.android.systemui.qs.QSFragment; +import com.android.systemui.qs.QSPanel; +import com.android.systemui.qs.QuickQSPanel; +import com.android.systemui.qs.QuickStatusBarHeader; +import com.android.systemui.qs.customize.QSCustomizer; + +import dagger.Binds; +import dagger.Module; +import dagger.Provides; + +/** + * Dagger Module for {@link QSFragmentComponent}. + */ +@Module +public interface QSFragmentModule { + /** */ + @Provides + @RootView + static View provideRootView(QSFragment qsFragment) { + return qsFragment.getView(); + } + + /** */ + @Provides + static QSPanel provideQSPanel(@RootView View view) { + return view.findViewById(R.id.quick_settings_panel); + } + + /** */ + @Provides + static QSContainerImpl providesQSContainerImpl(@RootView View view) { + return view.findViewById(R.id.quick_settings_container); + } + + /** */ + @Binds + QS bindQS(QSFragment qsFragment); + + /** */ + @Provides + static QuickStatusBarHeader providesQuickStatusBarHeader(@RootView View view) { + return view.findViewById(R.id.header); + } + + /** */ + @Provides + static QuickQSPanel providesQuickQSPanel(QuickStatusBarHeader quickStatusBarHeader) { + return quickStatusBarHeader.findViewById(R.id.quick_qs_panel); + } + + /** */ + @Provides + static QSFooterView providesQSFooterView(@RootView View view) { + return view.findViewById(R.id.qs_footer); + } + + /** */ + @Provides + @QSScope + static QSFooter providesQSFooter(QSFooterViewController qsFooterViewController) { + qsFooterViewController.init(); + return qsFooterViewController; + } + + /** */ + @Provides + @QSScope + static QSCustomizer providesQSCutomizer(@RootView View view) { + return view.findViewById(R.id.qs_customize); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java index 8ff96c8a4a37..cfc81eee9b3c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java +++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java @@ -21,6 +21,7 @@ import android.hardware.display.NightDisplayListener; import android.os.Handler; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.media.dagger.MediaModule; import com.android.systemui.qs.AutoAddTracker; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QSTileHost; @@ -29,6 +30,7 @@ import com.android.systemui.statusbar.phone.ManagedProfileController; import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.DataSaverController; import com.android.systemui.statusbar.policy.HotspotController; +import com.android.systemui.util.settings.SecureSettings; import dagger.Binds; import dagger.Module; @@ -37,8 +39,8 @@ import dagger.Provides; /** * Module for QS dependencies */ -// TODO: Add other QS classes -@Module +@Module(subcomponents = {QSFragmentComponent.class}, + includes = {MediaModule.class}) public interface QSModule { @Provides @@ -47,19 +49,28 @@ public interface QSModule { AutoAddTracker.Builder autoAddTrackerBuilder, QSTileHost host, @Background Handler handler, + SecureSettings secureSettings, HotspotController hotspotController, DataSaverController dataSaverController, ManagedProfileController managedProfileController, NightDisplayListener nightDisplayListener, CastController castController) { - AutoTileManager manager = new AutoTileManager(context, autoAddTrackerBuilder, - host, handler, hotspotController, dataSaverController, managedProfileController, - nightDisplayListener, castController); + AutoTileManager manager = new AutoTileManager( + context, + autoAddTrackerBuilder, + host, + handler, + secureSettings, + hotspotController, + dataSaverController, + managedProfileController, + nightDisplayListener, + castController + ); manager.init(); return manager; } - /** */ @Binds QSHost provideQsHost(QSTileHost controllerImpl); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleEntity.kt b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSScope.java index 24768cd84a76..f615eabb67dc 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleEntity.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSScope.java @@ -13,17 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.bubbles.storage -import android.annotation.DimenRes -import android.annotation.UserIdInt +package com.android.systemui.qs.dagger; -data class BubbleEntity( - @UserIdInt val userId: Int, - val packageName: String, - val shortcutId: String, - val key: String, - val desiredHeight: Int, - @DimenRes val desiredHeightResId: Int, - val title: String? = null -) +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Scope; + +/** + * Scope annotation for singleton items within the {@link QSFragmentComponent}. + */ +@Documented +@Retention(RUNTIME) +@Scope +public @interface QSScope {} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java index c64fc50b8237..bf3e4be9b9db 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java @@ -36,6 +36,7 @@ import com.android.systemui.qs.SecureSetting; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.util.settings.SecureSettings; import javax.inject.Inject; @@ -62,15 +63,20 @@ public class BatterySaverTile extends QSTileImpl<BooleanState> implements StatusBarStateController statusBarStateController, ActivityStarter activityStarter, QSLogger qsLogger, - BatteryController batteryController + BatteryController batteryController, + SecureSettings secureSettings ) { super(host, backgroundLooper, mainHandler, metricsLogger, statusBarStateController, activityStarter, qsLogger); mBatteryController = batteryController; mBatteryController.observe(getLifecycle(), this); int currentUser = host.getUserContext().getUserId(); - mSetting = new SecureSetting(mContext, mHandler, Secure.LOW_POWER_WARNING_ACKNOWLEDGED, - currentUser) { + mSetting = new SecureSetting( + secureSettings, + mHandler, + Secure.LOW_POWER_WARNING_ACKNOWLEDGED, + currentUser + ) { @Override protected void handleValueChanged(int value, boolean observedChange) { // mHandler is the background handler so calling this is OK diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java index eeb9de4f88a9..abffbba8a90c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java @@ -25,6 +25,7 @@ import android.content.Intent; import android.content.res.Resources; import android.os.Handler; import android.os.Looper; +import android.os.UserHandle; import android.provider.Settings; import android.service.quicksettings.Tile; import android.telephony.SubscriptionManager; @@ -244,7 +245,8 @@ public class CellularTile extends QSTileImpl<SignalState> { @Override public boolean isAvailable() { - return mController.hasMobileDataFeature(); + return mController.hasMobileDataFeature() + && mHost.getUserContext().getUserId() == UserHandle.USER_SYSTEM; } private static final class CallbackInfo { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java index 98782f7c8b55..39952488799a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java @@ -39,6 +39,7 @@ import com.android.systemui.qs.SecureSetting; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; import javax.inject.Inject; @@ -63,12 +64,13 @@ public class ColorInversionTile extends QSTileImpl<BooleanState> { StatusBarStateController statusBarStateController, ActivityStarter activityStarter, QSLogger qsLogger, - UserTracker userTracker + UserTracker userTracker, + SecureSettings secureSettings ) { super(host, backgroundLooper, mainHandler, metricsLogger, statusBarStateController, activityStarter, qsLogger); - mSetting = new SecureSetting(mContext, mainHandler, + mSetting = new SecureSetting(secureSettings, mainHandler, Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, userTracker.getUserId()) { @Override protected void handleValueChanged(int value, boolean observedChange) { diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 5279a20a67a7..ddf30ad663dd 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -95,7 +95,6 @@ import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedEvents; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; -import com.android.wm.shell.pip.phone.PipUtils; import com.android.wm.shell.splitscreen.SplitScreen; import java.io.FileDescriptor; @@ -151,7 +150,6 @@ public class OverviewProxyService extends CurrentUserTracker implements private int mConnectionBackoffAttempts; private boolean mBound; private boolean mIsEnabled; - private boolean mHasPipFeature; private int mCurrentBoundedUserId = -1; private float mNavBarButtonAlpha; private boolean mInputFocusTransferStarted; @@ -377,9 +375,7 @@ public class OverviewProxyService extends CurrentUserTracker implements @Override public void setShelfHeight(boolean visible, int shelfHeight) { - if (!verifyCaller("setShelfHeight") || !mHasPipFeature) { - Log.w(TAG_OPS, - "ByPass setShelfHeight, FEATURE_PICTURE_IN_PICTURE:" + mHasPipFeature); + if (!verifyCaller("setShelfHeight")) { return; } final long token = Binder.clearCallingIdentity(); @@ -405,9 +401,7 @@ public class OverviewProxyService extends CurrentUserTracker implements @Override public void notifySwipeToHomeFinished() { - if (!verifyCaller("notifySwipeToHomeFinished") || !mHasPipFeature) { - Log.w(TAG_OPS, "ByPass notifySwipeToHomeFinished, FEATURE_PICTURE_IN_PICTURE:" - + mHasPipFeature); + if (!verifyCaller("notifySwipeToHomeFinished")) { return; } final long token = Binder.clearCallingIdentity(); @@ -422,9 +416,7 @@ public class OverviewProxyService extends CurrentUserTracker implements @Override public void setPinnedStackAnimationListener(IPinnedStackAnimationListener listener) { - if (!verifyCaller("setPinnedStackAnimationListener") || !mHasPipFeature) { - Log.w(TAG_OPS, "ByPass setPinnedStackAnimationListener, FEATURE_PICTURE_IN_PICTURE:" - + mHasPipFeature); + if (!verifyCaller("setPinnedStackAnimationListener")) { return; } mIPinnedStackAnimationListener = listener; @@ -509,7 +501,7 @@ public class OverviewProxyService extends CurrentUserTracker implements public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, PictureInPictureParams pictureInPictureParams, int launcherRotation, int shelfHeight) { - if (!verifyCaller("startSwipePipToHome") || !mHasPipFeature) { + if (!verifyCaller("startSwipePipToHome")) { return null; } final long binderToken = Binder.clearCallingIdentity(); @@ -525,7 +517,7 @@ public class OverviewProxyService extends CurrentUserTracker implements @Override public void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds) { - if (!verifyCaller("stopSwipePipToHome") || !mHasPipFeature) { + if (!verifyCaller("stopSwipePipToHome")) { return; } final long binderToken = Binder.clearCallingIdentity(); @@ -650,7 +642,6 @@ public class OverviewProxyService extends CurrentUserTracker implements super(broadcastDispatcher); mContext = context; mPipOptional = pipOptional; - mHasPipFeature = PipUtils.hasSystemFeature(mContext); mStatusBarOptionalLazy = statusBarOptionalLazy; mHandler = new Handler(); mNavBarControllerLazy = navBarControllerLazy; diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java index 1a9abb9cf27d..45564b0bfa6b 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java @@ -30,6 +30,8 @@ import android.graphics.Bitmap; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import android.media.MediaFormat; import android.media.MediaMuxer; import android.media.MediaRecorder; import android.media.ThumbnailUtils; @@ -124,17 +126,19 @@ public class ScreenMediaRecorder { DisplayMetrics metrics = new DisplayMetrics(); WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); wm.getDefaultDisplay().getRealMetrics(metrics); - int screenWidth = metrics.widthPixels; - int screenHeight = metrics.heightPixels; - int refereshRate = (int) wm.getDefaultDisplay().getRefreshRate(); - int vidBitRate = screenHeight * screenWidth * refereshRate / VIDEO_FRAME_RATE + int refreshRate = (int) wm.getDefaultDisplay().getRefreshRate(); + int[] dimens = getSupportedSize(metrics.widthPixels, metrics.heightPixels, refreshRate); + int width = dimens[0]; + int height = dimens[1]; + refreshRate = dimens[2]; + int vidBitRate = width * height * refreshRate / VIDEO_FRAME_RATE * VIDEO_FRAME_RATE_TO_RESOLUTION_RATIO; mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); mMediaRecorder.setVideoEncodingProfileLevel( MediaCodecInfo.CodecProfileLevel.AVCProfileHigh, - MediaCodecInfo.CodecProfileLevel.AVCLevel42); - mMediaRecorder.setVideoSize(screenWidth, screenHeight); - mMediaRecorder.setVideoFrameRate(refereshRate); + MediaCodecInfo.CodecProfileLevel.AVCLevel3); + mMediaRecorder.setVideoSize(width, height); + mMediaRecorder.setVideoFrameRate(refreshRate); mMediaRecorder.setVideoEncodingBitRate(vidBitRate); mMediaRecorder.setMaxDuration(MAX_DURATION_MS); mMediaRecorder.setMaxFileSize(MAX_FILESIZE_BYTES); @@ -153,8 +157,8 @@ public class ScreenMediaRecorder { mInputSurface = mMediaRecorder.getSurface(); mVirtualDisplay = mMediaProjection.createVirtualDisplay( "Recording Display", - screenWidth, - screenHeight, + width, + height, metrics.densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mInputSurface, @@ -173,6 +177,84 @@ public class ScreenMediaRecorder { } /** + * Find the highest supported screen resolution and refresh rate for the given dimensions on + * this device, up to actual size and given rate. + * If possible this will return the same values as given, but values may be smaller on some + * devices. + * + * @param screenWidth Actual pixel width of screen + * @param screenHeight Actual pixel height of screen + * @param refreshRate Desired refresh rate + * @return array with supported width, height, and refresh rate + */ + private int[] getSupportedSize(int screenWidth, int screenHeight, int refreshRate) { + double maxScale = 0; + + MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + MediaCodecInfo.VideoCapabilities maxInfo = null; + for (MediaCodecInfo codec : codecList.getCodecInfos()) { + String videoType = MediaFormat.MIMETYPE_VIDEO_AVC; + String[] types = codec.getSupportedTypes(); + for (String t : types) { + if (!t.equalsIgnoreCase(videoType)) { + continue; + } + MediaCodecInfo.CodecCapabilities capabilities = + codec.getCapabilitiesForType(videoType); + if (capabilities != null && capabilities.getVideoCapabilities() != null) { + MediaCodecInfo.VideoCapabilities vc = capabilities.getVideoCapabilities(); + + int width = vc.getSupportedWidths().getUpper(); + int height = vc.getSupportedHeights().getUpper(); + + if (width >= screenWidth && height >= screenHeight + && vc.isSizeSupported(screenWidth, screenHeight)) { + + // Desired size is supported, now get the rate + int maxRate = vc.getSupportedFrameRatesFor(screenWidth, screenHeight) + .getUpper().intValue(); + + if (maxRate < refreshRate) { + refreshRate = maxRate; + } + Log.d(TAG, "Screen size supported at rate " + refreshRate); + return new int[]{screenWidth, screenHeight, refreshRate}; + } + + // Otherwise, continue searching + double scale = Math.min(((double) width / screenWidth), + ((double) height / screenHeight)); + if (scale > maxScale) { + maxScale = scale; + maxInfo = vc; + } + } + } + } + + // Resize for max supported size + int scaledWidth = (int) (screenWidth * maxScale); + int scaledHeight = (int) (screenHeight * maxScale); + if (scaledWidth % maxInfo.getWidthAlignment() != 0) { + scaledWidth -= (scaledWidth % maxInfo.getWidthAlignment()); + } + if (scaledHeight % maxInfo.getHeightAlignment() != 0) { + scaledHeight -= (scaledHeight % maxInfo.getHeightAlignment()); + } + + // Find max supported rate for size + int maxRate = maxInfo.getSupportedFrameRatesFor(scaledWidth, scaledHeight) + .getUpper().intValue(); + if (maxRate < refreshRate) { + refreshRate = maxRate; + } + + Log.d(TAG, "Resized by " + maxScale + ": " + scaledWidth + ", " + scaledHeight + + ", " + refreshRate); + return new int[]{scaledWidth, scaledHeight, refreshRate}; + } + + /** * Start screen recording */ void start() throws IOException, RemoteException, IllegalStateException { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java b/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java index 3fd7f94514f3..5c26d9400c9f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java @@ -16,12 +16,12 @@ package com.android.systemui.screenshot; -import static com.android.systemui.screenshot.GlobalScreenshot.ACTION_TYPE_EDIT; -import static com.android.systemui.screenshot.GlobalScreenshot.ACTION_TYPE_SHARE; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ACTION_INTENT; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_DISALLOW_ENTER_PIP; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED; +import static com.android.systemui.screenshot.ScreenshotController.ACTION_TYPE_EDIT; +import static com.android.systemui.screenshot.ScreenshotController.ACTION_TYPE_SHARE; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ACTION_INTENT; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_DISALLOW_ENTER_PIP; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ID; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED; import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASON_SCREENSHOT; import android.app.ActivityOptions; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/DeleteScreenshotReceiver.java b/packages/SystemUI/src/com/android/systemui/screenshot/DeleteScreenshotReceiver.java index 9028bb57c8e5..35839f39b491 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/DeleteScreenshotReceiver.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/DeleteScreenshotReceiver.java @@ -16,10 +16,10 @@ package com.android.systemui.screenshot; -import static com.android.systemui.screenshot.GlobalScreenshot.ACTION_TYPE_DELETE; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED; -import static com.android.systemui.screenshot.GlobalScreenshot.SCREENSHOT_URI_ID; +import static com.android.systemui.screenshot.ScreenshotController.ACTION_TYPE_DELETE; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ID; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED; +import static com.android.systemui.screenshot.ScreenshotController.SCREENSHOT_URI_ID; import android.content.BroadcastReceiver; import android.content.ContentResolver; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java deleted file mode 100644 index aaa335c25d5d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java +++ /dev/null @@ -1,1144 +0,0 @@ -/* - * 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.screenshot; - -import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; -import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; -import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ValueAnimator; -import android.annotation.Nullable; -import android.annotation.SuppressLint; -import android.app.ActivityManager; -import android.app.Notification; -import android.app.PendingIntent; -import android.content.ComponentName; -import android.content.Context; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Insets; -import android.graphics.Outline; -import android.graphics.PixelFormat; -import android.graphics.PointF; -import android.graphics.Rect; -import android.graphics.Region; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.InsetDrawable; -import android.graphics.drawable.LayerDrawable; -import android.media.MediaActionSound; -import android.net.Uri; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.Message; -import android.os.RemoteException; -import android.provider.Settings; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.MathUtils; -import android.view.Display; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.SurfaceControl; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewOutlineProvider; -import android.view.ViewTreeObserver; -import android.view.WindowInsets; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.AnimationUtils; -import android.view.animation.Interpolator; -import android.widget.FrameLayout; -import android.widget.HorizontalScrollView; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.Toast; - -import com.android.internal.logging.UiEventLogger; -import com.android.systemui.R; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.shared.system.QuickStepContract; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - -import javax.inject.Inject; - -/** - * Class for handling device screen shots - */ -@SysUISingleton -public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInsetsListener { - - /** - * POD used in the AsyncTask which saves an image in the background. - */ - static class SaveImageInBackgroundData { - public Bitmap image; - public Consumer<Uri> finisher; - public GlobalScreenshot.ActionsReadyListener mActionsReadyListener; - - void clearImage() { - image = null; - } - } - - /** - * Structure returned by the SaveImageInBackgroundTask - */ - static class SavedImageData { - public Uri uri; - public Notification.Action shareAction; - public Notification.Action editAction; - public Notification.Action deleteAction; - public List<Notification.Action> smartActions; - - /** - * Used to reset the return data on error - */ - public void reset() { - uri = null; - shareAction = null; - editAction = null; - deleteAction = null; - smartActions = null; - } - } - - abstract static class ActionsReadyListener { - abstract void onActionsReady(SavedImageData imageData); - } - - // These strings are used for communicating the action invoked to - // ScreenshotNotificationSmartActionsProvider. - static final String EXTRA_ACTION_TYPE = "android:screenshot_action_type"; - static final String EXTRA_ID = "android:screenshot_id"; - static final String ACTION_TYPE_DELETE = "Delete"; - static final String ACTION_TYPE_SHARE = "Share"; - static final String ACTION_TYPE_EDIT = "Edit"; - static final String EXTRA_SMART_ACTIONS_ENABLED = "android:smart_actions_enabled"; - static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent"; - - static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id"; - static final String EXTRA_CANCEL_NOTIFICATION = "android:screenshot_cancel_notification"; - static final String EXTRA_DISALLOW_ENTER_PIP = "android:screenshot_disallow_enter_pip"; - - // From WizardManagerHelper.java - private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete"; - - private static final String TAG = "GlobalScreenshot"; - - private static final long SCREENSHOT_FLASH_IN_DURATION_MS = 133; - private static final long SCREENSHOT_FLASH_OUT_DURATION_MS = 217; - // delay before starting to fade in dismiss button - private static final long SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS = 200; - private static final long SCREENSHOT_TO_CORNER_X_DURATION_MS = 234; - private static final long SCREENSHOT_TO_CORNER_Y_DURATION_MS = 500; - private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234; - private static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400; - private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100; - private static final long SCREENSHOT_DISMISS_Y_DURATION_MS = 350; - private static final long SCREENSHOT_DISMISS_ALPHA_DURATION_MS = 183; - private static final long SCREENSHOT_DISMISS_ALPHA_OFFSET_MS = 50; // delay before starting fade - private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f; - private static final float ROUNDED_CORNER_RADIUS = .05f; - private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000; - private static final int MESSAGE_CORNER_TIMEOUT = 2; - - private final Interpolator mAccelerateInterpolator = new AccelerateInterpolator(); - - private final ScreenshotNotificationsController mNotificationsController; - private final UiEventLogger mUiEventLogger; - - private final Context mContext; - private final ScreenshotSmartActions mScreenshotSmartActions; - private final WindowManager mWindowManager; - private final WindowManager.LayoutParams mWindowLayoutParams; - private final Display mDisplay; - private final DisplayMetrics mDisplayMetrics; - private final AccessibilityManager mAccessibilityManager; - - private View mScreenshotLayout; - private ScreenshotSelectorView mScreenshotSelectorView; - private ImageView mScreenshotAnimatedView; - private ImageView mScreenshotPreview; - private ImageView mScreenshotFlash; - private ImageView mActionsContainerBackground; - private HorizontalScrollView mActionsContainer; - private LinearLayout mActionsView; - private ImageView mBackgroundProtection; - private FrameLayout mDismissButton; - - private Bitmap mScreenBitmap; - private SaveImageInBackgroundTask mSaveInBgTask; - private Animator mScreenshotAnimation; - private Runnable mOnCompleteRunnable; - private Animator mDismissAnimation; - private boolean mInDarkMode; - private boolean mDirectionLTR; - private boolean mOrientationPortrait; - - private float mCornerSizeX; - private float mDismissDeltaY; - - private MediaActionSound mCameraSound; - - private int mNavMode; - private int mLeftInset; - private int mRightInset; - - // standard material ease - private final Interpolator mFastOutSlowIn; - - private final Handler mScreenshotHandler = new Handler(Looper.getMainLooper()) { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MESSAGE_CORNER_TIMEOUT: - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT); - GlobalScreenshot.this.dismissScreenshot("timeout", false); - mOnCompleteRunnable.run(); - break; - default: - break; - } - } - }; - - @Inject - public GlobalScreenshot( - Context context, @Main Resources resources, - ScreenshotSmartActions screenshotSmartActions, - ScreenshotNotificationsController screenshotNotificationsController, - UiEventLogger uiEventLogger) { - mContext = context; - mScreenshotSmartActions = screenshotSmartActions; - mNotificationsController = screenshotNotificationsController; - mUiEventLogger = uiEventLogger; - mAccessibilityManager = AccessibilityManager.getInstance(mContext); - - reloadAssets(); - Configuration config = mContext.getResources().getConfiguration(); - mInDarkMode = config.isNightModeActive(); - mDirectionLTR = config.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; - mOrientationPortrait = config.orientation == ORIENTATION_PORTRAIT; - - // Setup the window that we are going to use - mWindowLayoutParams = new WindowManager.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0, - WindowManager.LayoutParams.TYPE_SCREENSHOT, - WindowManager.LayoutParams.FLAG_FULLSCREEN - | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN - | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL - | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH - | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED - | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, - PixelFormat.TRANSLUCENT); - mWindowLayoutParams.setTitle("ScreenshotAnimation"); - mWindowLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; - mWindowLayoutParams.setFitInsetsTypes(0 /* types */); - mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - mDisplay = mWindowManager.getDefaultDisplay(); - mDisplayMetrics = new DisplayMetrics(); - mDisplay.getRealMetrics(mDisplayMetrics); - - mCornerSizeX = resources.getDimensionPixelSize(R.dimen.global_screenshot_x_scale); - mDismissDeltaY = resources.getDimensionPixelSize(R.dimen.screenshot_dismissal_height_delta); - - mFastOutSlowIn = - AnimationUtils.loadInterpolator(mContext, android.R.interpolator.fast_out_slow_in); - - // Setup the Camera shutter sound - mCameraSound = new MediaActionSound(); - mCameraSound.load(MediaActionSound.SHUTTER_CLICK); - } - - @Override // ViewTreeObserver.OnComputeInternalInsetsListener - public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { - inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); - Region touchRegion = new Region(); - - Rect screenshotRect = new Rect(); - mScreenshotPreview.getBoundsOnScreen(screenshotRect); - touchRegion.op(screenshotRect, Region.Op.UNION); - Rect actionsRect = new Rect(); - mActionsContainer.getBoundsOnScreen(actionsRect); - touchRegion.op(actionsRect, Region.Op.UNION); - Rect dismissRect = new Rect(); - mDismissButton.getBoundsOnScreen(dismissRect); - touchRegion.op(dismissRect, Region.Op.UNION); - - if (QuickStepContract.isGesturalMode(mNavMode)) { - // Receive touches in gesture insets such that they don't cause TOUCH_OUTSIDE - Rect inset = new Rect(0, 0, mLeftInset, mDisplayMetrics.heightPixels); - touchRegion.op(inset, Region.Op.UNION); - inset.set(mDisplayMetrics.widthPixels - mRightInset, 0, mDisplayMetrics.widthPixels, - mDisplayMetrics.heightPixels); - touchRegion.op(inset, Region.Op.UNION); - } - - inoutInfo.touchableRegion.set(touchRegion); - } - - void takeScreenshotFullscreen(Consumer<Uri> finisher, Runnable onComplete) { - mOnCompleteRunnable = onComplete; - - mDisplay.getRealMetrics(mDisplayMetrics); - takeScreenshotInternal( - finisher, - new Rect(0, 0, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels)); - } - - void handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds, - Insets visibleInsets, int taskId, int userId, ComponentName topComponent, - Consumer<Uri> finisher, Runnable onComplete) { - // TODO: use task Id, userId, topComponent for smart handler - mOnCompleteRunnable = onComplete; - - if (screenshot == null) { - Log.e(TAG, "Got null bitmap from screenshot message"); - mNotificationsController.notifyScreenshotError( - R.string.screenshot_failed_to_capture_text); - finisher.accept(null); - mOnCompleteRunnable.run(); - return; - } - - if (aspectRatiosMatch(screenshot, visibleInsets, screenshotScreenBounds)) { - saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, false); - } else { - saveScreenshot(screenshot, finisher, - new Rect(0, 0, screenshot.getWidth(), screenshot.getHeight()), Insets.NONE, - true); - } - } - - /** - * Displays a screenshot selector - */ - @SuppressLint("ClickableViewAccessibility") - void takeScreenshotPartial(final Consumer<Uri> finisher, Runnable onComplete) { - dismissScreenshot("new screenshot requested", true); - mOnCompleteRunnable = onComplete; - - mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); - mScreenshotSelectorView.setOnTouchListener((v, event) -> { - ScreenshotSelectorView view = (ScreenshotSelectorView) v; - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - view.startSelection((int) event.getX(), (int) event.getY()); - return true; - case MotionEvent.ACTION_MOVE: - view.updateSelection((int) event.getX(), (int) event.getY()); - return true; - case MotionEvent.ACTION_UP: - view.setVisibility(View.GONE); - mWindowManager.removeView(mScreenshotLayout); - final Rect rect = view.getSelectionRect(); - if (rect != null) { - if (rect.width() != 0 && rect.height() != 0) { - // Need mScreenshotLayout to handle it after the view disappears - mScreenshotLayout.post(() -> takeScreenshotInternal(finisher, rect)); - } - } - - view.stopSelection(); - return true; - } - - return false; - }); - mScreenshotLayout.post(() -> { - mScreenshotSelectorView.setVisibility(View.VISIBLE); - mScreenshotSelectorView.requestFocus(); - }); - } - - /** - * Cancels screenshot request - */ - void stopScreenshot() { - // If the selector layer still presents on screen, we remove it and resets its state. - if (mScreenshotSelectorView.getSelectionRect() != null) { - mWindowManager.removeView(mScreenshotLayout); - mScreenshotSelectorView.stopSelection(); - } - } - - /** - * Clears current screenshot - */ - void dismissScreenshot(String reason, boolean immediate) { - Log.v(TAG, "clearing screenshot: " + reason); - mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT); - mScreenshotLayout.getViewTreeObserver().removeOnComputeInternalInsetsListener(this); - if (!immediate) { - mDismissAnimation = createScreenshotDismissAnimation(); - mDismissAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - clearScreenshot(); - } - }); - mDismissAnimation.start(); - } else { - clearScreenshot(); - } - } - - private void onConfigChanged(Configuration newConfig) { - boolean needsUpdate = false; - // dark mode - if (newConfig.isNightModeActive()) { - // Night mode is active, we're using dark theme - if (!mInDarkMode) { - mInDarkMode = true; - needsUpdate = true; - } - } else { - // Night mode is not active, we're using the light theme - if (mInDarkMode) { - mInDarkMode = false; - needsUpdate = true; - } - } - - // RTL configuration - switch (newConfig.getLayoutDirection()) { - case View.LAYOUT_DIRECTION_LTR: - if (!mDirectionLTR) { - mDirectionLTR = true; - needsUpdate = true; - } - break; - case View.LAYOUT_DIRECTION_RTL: - if (mDirectionLTR) { - mDirectionLTR = false; - needsUpdate = true; - } - break; - } - - // portrait/landscape orientation - switch (newConfig.orientation) { - case ORIENTATION_PORTRAIT: - if (!mOrientationPortrait) { - mOrientationPortrait = true; - needsUpdate = true; - } - break; - case ORIENTATION_LANDSCAPE: - if (mOrientationPortrait) { - mOrientationPortrait = false; - needsUpdate = true; - } - break; - } - - if (needsUpdate) { - reloadAssets(); - } - - mNavMode = mContext.getResources().getInteger( - com.android.internal.R.integer.config_navBarInteractionMode); - } - - /** - * Update assets (called when the dark theme status changes). We only need to update the dismiss - * button and the actions container background, since the buttons are re-inflated on demand. - */ - private void reloadAssets() { - boolean wasAttached = mScreenshotLayout != null && mScreenshotLayout.isAttachedToWindow(); - if (wasAttached) { - mWindowManager.removeView(mScreenshotLayout); - } - - // Inflate the screenshot layout - mScreenshotLayout = LayoutInflater.from(mContext).inflate(R.layout.global_screenshot, null); - // TODO(159460485): Remove this when focus is handled properly in the system - mScreenshotLayout.setOnTouchListener((v, event) -> { - if (event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) { - // Once the user touches outside, stop listening for input - setWindowFocusable(false); - } - return false; - }); - mScreenshotLayout.setOnApplyWindowInsetsListener((v, insets) -> { - if (QuickStepContract.isGesturalMode(mNavMode)) { - Insets gestureInsets = insets.getInsets( - WindowInsets.Type.systemGestures()); - mLeftInset = gestureInsets.left; - mRightInset = gestureInsets.right; - } else { - mLeftInset = mRightInset = 0; - } - return mScreenshotLayout.onApplyWindowInsets(insets); - }); - mScreenshotLayout.setOnKeyListener((v, keyCode, event) -> { - if (keyCode == KeyEvent.KEYCODE_BACK) { - dismissScreenshot("back pressed", false); - return true; - } - return false; - }); - // Get focus so that the key events go to the layout. - mScreenshotLayout.setFocusableInTouchMode(true); - mScreenshotLayout.requestFocus(); - - mScreenshotAnimatedView = - mScreenshotLayout.findViewById(R.id.global_screenshot_animated_view); - mScreenshotAnimatedView.setClipToOutline(true); - mScreenshotAnimatedView.setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - outline.setRoundRect(new Rect(0, 0, view.getWidth(), view.getHeight()), - ROUNDED_CORNER_RADIUS * view.getWidth()); - } - }); - mScreenshotPreview = mScreenshotLayout.findViewById(R.id.global_screenshot_preview); - mScreenshotPreview.setClipToOutline(true); - mScreenshotPreview.setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - outline.setRoundRect(new Rect(0, 0, view.getWidth(), view.getHeight()), - ROUNDED_CORNER_RADIUS * view.getWidth()); - } - }); - - mActionsContainerBackground = mScreenshotLayout.findViewById( - R.id.global_screenshot_actions_container_background); - mActionsContainer = mScreenshotLayout.findViewById( - R.id.global_screenshot_actions_container); - mActionsView = mScreenshotLayout.findViewById(R.id.global_screenshot_actions); - mBackgroundProtection = mScreenshotLayout.findViewById( - R.id.global_screenshot_actions_background); - mDismissButton = mScreenshotLayout.findViewById(R.id.global_screenshot_dismiss_button); - mDismissButton.setOnClickListener(view -> { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL); - dismissScreenshot("dismiss_button", false); - mOnCompleteRunnable.run(); - }); - - mScreenshotFlash = mScreenshotLayout.findViewById(R.id.global_screenshot_flash); - mScreenshotSelectorView = mScreenshotLayout.findViewById(R.id.global_screenshot_selector); - mScreenshotLayout.setFocusable(true); - mScreenshotSelectorView.setFocusable(true); - mScreenshotSelectorView.setFocusableInTouchMode(true); - mScreenshotAnimatedView.setPivotX(0); - mScreenshotAnimatedView.setPivotY(0); - mActionsContainer.setScrollX(0); - - if (wasAttached) { - mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); - } - } - - /** - * Takes a screenshot of the current display and shows an animation. - */ - private void takeScreenshotInternal(Consumer<Uri> finisher, Rect crop) { - // copy the input Rect, since SurfaceControl.screenshot can mutate it - Rect screenRect = new Rect(crop); - int width = crop.width(); - int height = crop.height(); - final IBinder displayToken = SurfaceControl.getInternalDisplayToken(); - final SurfaceControl.DisplayCaptureArgs captureArgs = - new SurfaceControl.DisplayCaptureArgs.Builder(displayToken) - .setSourceCrop(crop) - .setSize(width, height) - .build(); - final SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer = - SurfaceControl.captureDisplay(captureArgs); - Bitmap screenshot = screenshotBuffer == null ? null : screenshotBuffer.asBitmap(); - - if (screenshot == null) { - Log.e(TAG, "Screenshot bitmap was null"); - mNotificationsController.notifyScreenshotError( - R.string.screenshot_failed_to_capture_text); - finisher.accept(null); - mOnCompleteRunnable.run(); - return; - } - - saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, true); - } - - private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect, - Insets screenInsets, boolean showFlash) { - if (mAccessibilityManager.isEnabled()) { - AccessibilityEvent event = - new AccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); - event.setContentDescription( - mContext.getResources().getString(R.string.screenshot_saving_title)); - mAccessibilityManager.sendAccessibilityEvent(event); - } - - if (mScreenshotLayout.isAttachedToWindow()) { - // if we didn't already dismiss for another reason - if (mDismissAnimation == null || !mDismissAnimation.isRunning()) { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED); - } - dismissScreenshot("new screenshot requested", true); - } - - mScreenBitmap = screenshot; - - if (!isUserSetupComplete()) { - // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing - // and sharing shouldn't be exposed to the user. - saveScreenshotAndToast(finisher); - return; - } - - // Optimizations - mScreenBitmap.setHasAlpha(false); - mScreenBitmap.prepareToDraw(); - - onConfigChanged(mContext.getResources().getConfiguration()); - - if (mDismissAnimation != null && mDismissAnimation.isRunning()) { - mDismissAnimation.cancel(); - } - - // The window is focusable by default - setWindowFocusable(true); - - // Start the post-screenshot animation - startAnimation(finisher, screenRect, screenInsets, showFlash); - } - - /** - * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on - * failure). - */ - private void saveScreenshotAndToast(Consumer<Uri> finisher) { - // Play the shutter sound to notify that we've taken a screenshot - mScreenshotHandler.post(() -> { - mCameraSound.play(MediaActionSound.SHUTTER_CLICK); - }); - - saveScreenshotInWorkerThread(finisher, new ActionsReadyListener() { - @Override - void onActionsReady(SavedImageData imageData) { - finisher.accept(imageData.uri); - if (imageData.uri == null) { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED); - mNotificationsController.notifyScreenshotError( - R.string.screenshot_failed_to_save_text); - } else { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED); - - mScreenshotHandler.post(() -> { - Toast.makeText(mContext, R.string.screenshot_saved_title, - Toast.LENGTH_SHORT).show(); - }); - } - } - }); - } - - /** - * Starts the animation after taking the screenshot - */ - private void startAnimation(final Consumer<Uri> finisher, Rect screenRect, Insets screenInsets, - boolean showFlash) { - mScreenshotHandler.post(() -> { - if (!mScreenshotLayout.isAttachedToWindow()) { - mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); - } - mScreenshotAnimatedView.setImageDrawable( - createScreenDrawable(mScreenBitmap, screenInsets)); - setAnimatedViewSize(screenRect.width(), screenRect.height()); - // Show when the animation starts - mScreenshotAnimatedView.setVisibility(View.GONE); - - mScreenshotPreview.setImageDrawable(createScreenDrawable(mScreenBitmap, screenInsets)); - // make static preview invisible (from gone) so we can query its location on screen - mScreenshotPreview.setVisibility(View.INVISIBLE); - - mScreenshotHandler.post(() -> { - mScreenshotLayout.getViewTreeObserver().addOnComputeInternalInsetsListener(this); - - mScreenshotAnimation = - createScreenshotDropInAnimation(screenRect, showFlash); - - saveScreenshotInWorkerThread(finisher, new ActionsReadyListener() { - @Override - void onActionsReady(SavedImageData imageData) { - showUiOnActionsReady(imageData); - } - }); - - // Play the shutter sound to notify that we've taken a screenshot - mCameraSound.play(MediaActionSound.SHUTTER_CLICK); - - mScreenshotPreview.setLayerType(View.LAYER_TYPE_HARDWARE, null); - mScreenshotPreview.buildLayer(); - mScreenshotAnimation.start(); - }); - }); - } - - /** - * Creates a new worker thread and saves the screenshot to the media store. - */ - private void saveScreenshotInWorkerThread( - Consumer<Uri> finisher, @Nullable ActionsReadyListener actionsReadyListener) { - SaveImageInBackgroundData data = new SaveImageInBackgroundData(); - data.image = mScreenBitmap; - data.finisher = finisher; - data.mActionsReadyListener = actionsReadyListener; - - if (mSaveInBgTask != null) { - // just log success/failure for the pre-existing screenshot - mSaveInBgTask.setActionsReadyListener(new ActionsReadyListener() { - @Override - void onActionsReady(SavedImageData imageData) { - logSuccessOnActionsReady(imageData); - } - }); - } - - mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data); - mSaveInBgTask.execute(); - } - - /** - * Sets up the action shade and its entrance animation, once we get the screenshot URI. - */ - private void showUiOnActionsReady(SavedImageData imageData) { - logSuccessOnActionsReady(imageData); - - AccessibilityManager accessibilityManager = (AccessibilityManager) - mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); - long timeoutMs = accessibilityManager.getRecommendedTimeoutMillis( - SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS, - AccessibilityManager.FLAG_CONTENT_CONTROLS); - - mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT); - mScreenshotHandler.sendMessageDelayed( - mScreenshotHandler.obtainMessage(MESSAGE_CORNER_TIMEOUT), - timeoutMs); - - if (imageData.uri != null) { - mScreenshotHandler.post(() -> { - if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { - mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - createScreenshotActionsShadeAnimation(imageData).start(); - } - }); - } else { - createScreenshotActionsShadeAnimation(imageData).start(); - } - }); - } - } - - /** - * Logs success/failure of the screenshot saving task, and shows an error if it failed. - */ - private void logSuccessOnActionsReady(SavedImageData imageData) { - if (imageData.uri == null) { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED); - mNotificationsController.notifyScreenshotError( - R.string.screenshot_failed_to_save_text); - } else { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED); - } - } - - private AnimatorSet createScreenshotDropInAnimation(Rect bounds, boolean showFlash) { - Rect previewBounds = new Rect(); - mScreenshotPreview.getBoundsOnScreen(previewBounds); - - float cornerScale = - mCornerSizeX / (mOrientationPortrait ? bounds.width() : bounds.height()); - final float currentScale = 1f; - - mScreenshotAnimatedView.setScaleX(currentScale); - mScreenshotAnimatedView.setScaleY(currentScale); - - mDismissButton.setAlpha(0); - mDismissButton.setVisibility(View.VISIBLE); - - AnimatorSet dropInAnimation = new AnimatorSet(); - ValueAnimator flashInAnimator = ValueAnimator.ofFloat(0, 1); - flashInAnimator.setDuration(SCREENSHOT_FLASH_IN_DURATION_MS); - flashInAnimator.setInterpolator(mFastOutSlowIn); - flashInAnimator.addUpdateListener(animation -> - mScreenshotFlash.setAlpha((float) animation.getAnimatedValue())); - - ValueAnimator flashOutAnimator = ValueAnimator.ofFloat(1, 0); - flashOutAnimator.setDuration(SCREENSHOT_FLASH_OUT_DURATION_MS); - flashOutAnimator.setInterpolator(mFastOutSlowIn); - flashOutAnimator.addUpdateListener(animation -> - mScreenshotFlash.setAlpha((float) animation.getAnimatedValue())); - - // animate from the current location, to the static preview location - final PointF startPos = new PointF(bounds.centerX(), bounds.centerY()); - final PointF finalPos = new PointF(previewBounds.centerX(), previewBounds.centerY()); - - ValueAnimator toCorner = ValueAnimator.ofFloat(0, 1); - toCorner.setDuration(SCREENSHOT_TO_CORNER_Y_DURATION_MS); - float xPositionPct = - SCREENSHOT_TO_CORNER_X_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; - float dismissPct = - SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; - float scalePct = - SCREENSHOT_TO_CORNER_SCALE_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; - toCorner.addUpdateListener(animation -> { - float t = animation.getAnimatedFraction(); - if (t < scalePct) { - float scale = MathUtils.lerp( - currentScale, cornerScale, mFastOutSlowIn.getInterpolation(t / scalePct)); - mScreenshotAnimatedView.setScaleX(scale); - mScreenshotAnimatedView.setScaleY(scale); - } else { - mScreenshotAnimatedView.setScaleX(cornerScale); - mScreenshotAnimatedView.setScaleY(cornerScale); - } - - float currentScaleX = mScreenshotAnimatedView.getScaleX(); - float currentScaleY = mScreenshotAnimatedView.getScaleY(); - - if (t < xPositionPct) { - float xCenter = MathUtils.lerp(startPos.x, finalPos.x, - mFastOutSlowIn.getInterpolation(t / xPositionPct)); - mScreenshotAnimatedView.setX(xCenter - bounds.width() * currentScaleX / 2f); - } else { - mScreenshotAnimatedView.setX(finalPos.x - bounds.width() * currentScaleX / 2f); - } - float yCenter = MathUtils.lerp( - startPos.y, finalPos.y, mFastOutSlowIn.getInterpolation(t)); - mScreenshotAnimatedView.setY(yCenter - bounds.height() * currentScaleY / 2f); - - if (t >= dismissPct) { - mDismissButton.setAlpha((t - dismissPct) / (1 - dismissPct)); - float currentX = mScreenshotAnimatedView.getX(); - float currentY = mScreenshotAnimatedView.getY(); - mDismissButton.setY(currentY - mDismissButton.getHeight() / 2f); - if (mDirectionLTR) { - mDismissButton.setX(currentX - + bounds.width() * currentScaleX - mDismissButton.getWidth() / 2f); - } else { - mDismissButton.setX(currentX - mDismissButton.getWidth() / 2f); - } - } - }); - - toCorner.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - super.onAnimationStart(animation); - mScreenshotAnimatedView.setVisibility(View.VISIBLE); - } - }); - - mScreenshotFlash.setAlpha(0f); - mScreenshotFlash.setVisibility(View.VISIBLE); - - if (showFlash) { - dropInAnimation.play(flashOutAnimator).after(flashInAnimator); - dropInAnimation.play(flashOutAnimator).with(toCorner); - } else { - dropInAnimation.play(toCorner); - } - - dropInAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - mDismissButton.setAlpha(1); - float dismissOffset = mDismissButton.getWidth() / 2f; - float finalDismissX = mDirectionLTR - ? finalPos.x - dismissOffset + bounds.width() * cornerScale / 2f - : finalPos.x - dismissOffset - bounds.width() * cornerScale / 2f; - mDismissButton.setX(finalDismissX); - mDismissButton.setY( - finalPos.y - dismissOffset - bounds.height() * cornerScale / 2f); - mScreenshotAnimatedView.setScaleX(1); - mScreenshotAnimatedView.setScaleY(1); - mScreenshotAnimatedView.setX(finalPos.x - bounds.width() * cornerScale / 2f); - mScreenshotAnimatedView.setY(finalPos.y - bounds.height() * cornerScale / 2f); - mScreenshotAnimatedView.setVisibility(View.GONE); - mScreenshotPreview.setVisibility(View.VISIBLE); - mScreenshotLayout.forceLayout(); - } - }); - - return dropInAnimation; - } - - private ValueAnimator createScreenshotActionsShadeAnimation(SavedImageData imageData) { - LayoutInflater inflater = LayoutInflater.from(mContext); - mActionsView.removeAllViews(); - mScreenshotLayout.invalidate(); - mScreenshotLayout.requestLayout(); - mScreenshotLayout.getViewTreeObserver().dispatchOnGlobalLayout(); - - // By default the activities won't be able to start immediately; override this to keep - // the same behavior as if started from a notification - try { - ActivityManager.getService().resumeAppSwitches(); - } catch (RemoteException e) { - } - - ArrayList<ScreenshotActionChip> chips = new ArrayList<>(); - - for (Notification.Action smartAction : imageData.smartActions) { - ScreenshotActionChip actionChip = (ScreenshotActionChip) inflater.inflate( - R.layout.global_screenshot_action_chip, mActionsView, false); - actionChip.setText(smartAction.title); - actionChip.setIcon(smartAction.getIcon(), false); - actionChip.setPendingIntent(smartAction.actionIntent, - () -> { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED); - dismissScreenshot("chip tapped", false); - mOnCompleteRunnable.run(); - }); - mActionsView.addView(actionChip); - chips.add(actionChip); - } - - ScreenshotActionChip shareChip = (ScreenshotActionChip) inflater.inflate( - R.layout.global_screenshot_action_chip, mActionsView, false); - shareChip.setText(imageData.shareAction.title); - shareChip.setIcon(imageData.shareAction.getIcon(), true); - shareChip.setPendingIntent(imageData.shareAction.actionIntent, () -> { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED); - dismissScreenshot("chip tapped", false); - mOnCompleteRunnable.run(); - }); - mActionsView.addView(shareChip); - chips.add(shareChip); - - ScreenshotActionChip editChip = (ScreenshotActionChip) inflater.inflate( - R.layout.global_screenshot_action_chip, mActionsView, false); - editChip.setText(imageData.editAction.title); - editChip.setIcon(imageData.editAction.getIcon(), true); - editChip.setPendingIntent(imageData.editAction.actionIntent, () -> { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED); - dismissScreenshot("chip tapped", false); - mOnCompleteRunnable.run(); - }); - mActionsView.addView(editChip); - chips.add(editChip); - - mScreenshotPreview.setOnClickListener(v -> { - try { - imageData.editAction.actionIntent.send(); - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED); - dismissScreenshot("screenshot preview tapped", false); - mOnCompleteRunnable.run(); - } catch (PendingIntent.CanceledException e) { - Log.e(TAG, "Intent cancelled", e); - } - }); - mScreenshotPreview.setContentDescription(imageData.editAction.title); - - // remove the margin from the last chip so that it's correctly aligned with the end - LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) - mActionsView.getChildAt(mActionsView.getChildCount() - 1).getLayoutParams(); - params.setMarginEnd(0); - - ValueAnimator animator = ValueAnimator.ofFloat(0, 1); - animator.setDuration(SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS); - float alphaFraction = (float) SCREENSHOT_ACTIONS_ALPHA_DURATION_MS - / SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS; - mActionsContainer.setAlpha(0f); - mActionsContainerBackground.setAlpha(0f); - mActionsContainer.setVisibility(View.VISIBLE); - mActionsContainerBackground.setVisibility(View.VISIBLE); - - animator.addUpdateListener(animation -> { - float t = animation.getAnimatedFraction(); - mBackgroundProtection.setAlpha(t); - float containerAlpha = t < alphaFraction ? t / alphaFraction : 1; - mActionsContainer.setAlpha(containerAlpha); - mActionsContainerBackground.setAlpha(containerAlpha); - float containerScale = SCREENSHOT_ACTIONS_START_SCALE_X - + (t * (1 - SCREENSHOT_ACTIONS_START_SCALE_X)); - mActionsContainer.setScaleX(containerScale); - mActionsContainerBackground.setScaleX(containerScale); - for (ScreenshotActionChip chip : chips) { - chip.setAlpha(t); - chip.setScaleX(1 / containerScale); // invert to keep size of children constant - } - mActionsContainer.setScrollX(mDirectionLTR ? 0 : mActionsContainer.getWidth()); - mActionsContainer.setPivotX(mDirectionLTR ? 0 : mActionsContainer.getWidth()); - mActionsContainerBackground.setPivotX( - mDirectionLTR ? 0 : mActionsContainerBackground.getWidth()); - }); - return animator; - } - - private AnimatorSet createScreenshotDismissAnimation() { - ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); - alphaAnim.setStartDelay(SCREENSHOT_DISMISS_ALPHA_OFFSET_MS); - alphaAnim.setDuration(SCREENSHOT_DISMISS_ALPHA_DURATION_MS); - alphaAnim.addUpdateListener(animation -> { - mScreenshotLayout.setAlpha(1 - animation.getAnimatedFraction()); - }); - - ValueAnimator yAnim = ValueAnimator.ofFloat(0, 1); - yAnim.setInterpolator(mAccelerateInterpolator); - yAnim.setDuration(SCREENSHOT_DISMISS_Y_DURATION_MS); - float screenshotStartY = mScreenshotPreview.getTranslationY(); - float dismissStartY = mDismissButton.getTranslationY(); - yAnim.addUpdateListener(animation -> { - float yDelta = MathUtils.lerp(0, mDismissDeltaY, animation.getAnimatedFraction()); - mScreenshotPreview.setTranslationY(screenshotStartY + yDelta); - mDismissButton.setTranslationY(dismissStartY + yDelta); - mActionsContainer.setTranslationY(yDelta); - mActionsContainerBackground.setTranslationY(yDelta); - }); - - AnimatorSet animSet = new AnimatorSet(); - animSet.play(yAnim).with(alphaAnim); - - return animSet; - } - - private void clearScreenshot() { - if (mScreenshotLayout.isAttachedToWindow()) { - mWindowManager.removeView(mScreenshotLayout); - } - - // Clear any references to the bitmap - mScreenshotPreview.setImageDrawable(null); - mScreenshotAnimatedView.setImageDrawable(null); - mScreenshotAnimatedView.setVisibility(View.GONE); - mActionsContainerBackground.setVisibility(View.GONE); - mActionsContainer.setVisibility(View.GONE); - mBackgroundProtection.setAlpha(0f); - mDismissButton.setVisibility(View.GONE); - mScreenshotPreview.setVisibility(View.GONE); - mScreenshotPreview.setLayerType(View.LAYER_TYPE_NONE, null); - mScreenshotPreview.setContentDescription( - mContext.getResources().getString(R.string.screenshot_preview_description)); - mScreenshotPreview.setOnClickListener(null); - mScreenshotLayout.setAlpha(1); - mDismissButton.setTranslationY(0); - mActionsContainer.setTranslationY(0); - mActionsContainerBackground.setTranslationY(0); - mScreenshotPreview.setTranslationY(0); - } - - private void setAnimatedViewSize(int width, int height) { - ViewGroup.LayoutParams layoutParams = mScreenshotAnimatedView.getLayoutParams(); - layoutParams.width = width; - layoutParams.height = height; - mScreenshotAnimatedView.setLayoutParams(layoutParams); - } - - /** - * Updates the window focusability. If the window is already showing, then it updates the - * window immediately, otherwise the layout params will be applied when the window is next - * shown. - */ - private void setWindowFocusable(boolean focusable) { - if (focusable) { - mWindowLayoutParams.flags &= ~FLAG_NOT_FOCUSABLE; - } else { - mWindowLayoutParams.flags |= FLAG_NOT_FOCUSABLE; - } - if (mScreenshotLayout.isAttachedToWindow()) { - mWindowManager.updateViewLayout(mScreenshotLayout, mWindowLayoutParams); - } - } - - private boolean isUserSetupComplete() { - return Settings.Secure.getInt(mContext.getContentResolver(), - SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; - } - - /** Does the aspect ratio of the bitmap with insets removed match the bounds. */ - private boolean aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets, Rect screenBounds) { - int insettedWidth = bitmap.getWidth() - bitmapInsets.left - bitmapInsets.right; - int insettedHeight = bitmap.getHeight() - bitmapInsets.top - bitmapInsets.bottom; - - if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0 - || bitmap.getHeight() == 0) { - Log.e(TAG, String.format( - "Provided bitmap and insets create degenerate region: %dx%d %s", - bitmap.getWidth(), bitmap.getHeight(), bitmapInsets)); - return false; - } - - float insettedBitmapAspect = ((float) insettedWidth) / insettedHeight; - float boundsAspect = ((float) screenBounds.width()) / screenBounds.height(); - - boolean matchWithinTolerance = Math.abs(insettedBitmapAspect - boundsAspect) < 0.1f; - if (!matchWithinTolerance) { - Log.d(TAG, String.format("aspectRatiosMatch: don't match bitmap: %f, bounds: %f", - insettedBitmapAspect, boundsAspect)); - } - - return matchWithinTolerance; - } - - /** - * Create a drawable using the size of the bitmap and insets as the fractional inset parameters. - */ - private Drawable createScreenDrawable(Bitmap bitmap, Insets insets) { - int insettedWidth = bitmap.getWidth() - insets.left - insets.right; - int insettedHeight = bitmap.getHeight() - insets.top - insets.bottom; - - BitmapDrawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap); - if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0 - || bitmap.getHeight() == 0) { - Log.e(TAG, String.format( - "Can't create insetted drawable, using 0 insets " - + "bitmap and insets create degenerate region: %dx%d %s", - bitmap.getWidth(), bitmap.getHeight(), insets)); - return bitmapDrawable; - } - - InsetDrawable insetDrawable = new InsetDrawable(bitmapDrawable, - -1f * insets.left / insettedWidth, - -1f * insets.top / insettedHeight, - -1f * insets.right / insettedWidth, - -1f * insets.bottom / insettedHeight); - - if (insets.left < 0 || insets.top < 0 || insets.right < 0 || insets.bottom < 0) { - // Are any of the insets negative, meaning the bitmap is smaller than the bounds so need - // to fill in the background of the drawable. - return new LayerDrawable(new Drawable[]{ - new ColorDrawable(Color.BLACK), insetDrawable}); - } else { - return insetDrawable; - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index f0ea597c458d..b2ebf3f700b9 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -82,8 +82,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { private final Context mContext; private final ScreenshotSmartActions mScreenshotSmartActions; - private final GlobalScreenshot.SaveImageInBackgroundData mParams; - private final GlobalScreenshot.SavedImageData mImageData; + private final ScreenshotController.SaveImageInBackgroundData mParams; + private final ScreenshotController.SavedImageData mImageData; private final String mImageFileName; private final long mImageTime; private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider; @@ -92,10 +92,10 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { private final Random mRandom = new Random(); SaveImageInBackgroundTask(Context context, ScreenshotSmartActions screenshotSmartActions, - GlobalScreenshot.SaveImageInBackgroundData data) { + ScreenshotController.SaveImageInBackgroundData data) { mContext = context; mScreenshotSmartActions = screenshotSmartActions; - mImageData = new GlobalScreenshot.SavedImageData(); + mImageData = new ScreenshotController.SavedImageData(); // Prepare all the output metadata mParams = data; @@ -234,7 +234,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { * Update the listener run when the saving task completes. Used to avoid showing UI for the * first screenshot when a second one is taken. */ - void setActionsReadyListener(GlobalScreenshot.ActionsReadyListener listener) { + void setActionsReadyListener(ScreenshotController.ActionsReadyListener listener) { mParams.mActionsReadyListener = listener; } @@ -281,20 +281,23 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // cancel current pending intent (if any) since clipData isn't used for matching - PendingIntent pendingIntent = PendingIntent.getActivityAsUser(context, 0, - sharingChooserIntent, PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT); + PendingIntent pendingIntent = PendingIntent.getActivityAsUser( + context, 0, sharingChooserIntent, + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, + null, UserHandle.CURRENT); // Create a share action for the notification PendingIntent shareAction = PendingIntent.getBroadcastAsUser(context, requestCode, new Intent(context, ActionProxyReceiver.class) - .putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, pendingIntent) - .putExtra(GlobalScreenshot.EXTRA_DISALLOW_ENTER_PIP, true) - .putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId) - .putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED, + .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, pendingIntent) + .putExtra(ScreenshotController.EXTRA_DISALLOW_ENTER_PIP, true) + .putExtra(ScreenshotController.EXTRA_ID, mScreenshotId) + .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, mSmartActionsEnabled) .setAction(Intent.ACTION_SEND) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), - PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.SYSTEM); + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, + UserHandle.SYSTEM); Notification.Action.Builder shareActionBuilder = new Notification.Action.Builder( Icon.createWithResource(r, R.drawable.ic_screenshot_share), @@ -323,7 +326,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { editIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntent.getActivityAsUser(context, 0, - editIntent, 0, null, UserHandle.CURRENT); + editIntent, PendingIntent.FLAG_IMMUTABLE, null, UserHandle.CURRENT); // Make sure pending intents for the system user are still unique across users // by setting the (otherwise unused) request code to the current user id. @@ -332,13 +335,14 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { // Create a edit action PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode, new Intent(context, ActionProxyReceiver.class) - .putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, pendingIntent) - .putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId) - .putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED, + .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, pendingIntent) + .putExtra(ScreenshotController.EXTRA_ID, mScreenshotId) + .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, mSmartActionsEnabled) .setAction(Intent.ACTION_EDIT) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), - PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.SYSTEM); + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, + UserHandle.SYSTEM); Notification.Action.Builder editActionBuilder = new Notification.Action.Builder( Icon.createWithResource(r, R.drawable.ic_screenshot_edit), r.getString(com.android.internal.R.string.screenshot_edit), editAction); @@ -355,12 +359,14 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { // Create a delete action for the notification PendingIntent deleteAction = PendingIntent.getBroadcast(context, requestCode, new Intent(context, DeleteScreenshotReceiver.class) - .putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString()) - .putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId) - .putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED, + .putExtra(ScreenshotController.SCREENSHOT_URI_ID, uri.toString()) + .putExtra(ScreenshotController.EXTRA_ID, mScreenshotId) + .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, mSmartActionsEnabled) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), - PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); + PendingIntent.FLAG_CANCEL_CURRENT + | PendingIntent.FLAG_ONE_SHOT + | PendingIntent.FLAG_IMMUTABLE); Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder( Icon.createWithResource(r, R.drawable.ic_screenshot_delete), r.getString(com.android.internal.R.string.delete), deleteAction); @@ -395,13 +401,13 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { ScreenshotNotificationSmartActionsProvider.ACTION_TYPE, ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE); Intent intent = new Intent(context, SmartActionsReceiver.class) - .putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, action.actionIntent) + .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, action.actionIntent) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); addIntentExtras(mScreenshotId, intent, actionType, mSmartActionsEnabled); PendingIntent broadcastIntent = PendingIntent.getBroadcast(context, mRandom.nextInt(), intent, - PendingIntent.FLAG_CANCEL_CURRENT); + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); broadcastActions.add(new Notification.Action.Builder(action.getIcon(), action.title, broadcastIntent).setContextual(true).addExtras(extras).build()); } @@ -411,9 +417,9 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { private static void addIntentExtras(String screenshotId, Intent intent, String actionType, boolean smartActionsEnabled) { intent - .putExtra(GlobalScreenshot.EXTRA_ACTION_TYPE, actionType) - .putExtra(GlobalScreenshot.EXTRA_ID, screenshotId) - .putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED, smartActionsEnabled); + .putExtra(ScreenshotController.EXTRA_ACTION_TYPE, actionType) + .putExtra(ScreenshotController.EXTRA_ID, screenshotId) + .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, smartActionsEnabled); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionChip.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionChip.java index a48870240384..3370946ec274 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionChip.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionChip.java @@ -16,7 +16,6 @@ package com.android.systemui.screenshot; -import android.annotation.ColorInt; import android.app.PendingIntent; import android.content.Context; import android.graphics.drawable.Icon; @@ -35,9 +34,9 @@ public class ScreenshotActionChip extends FrameLayout { private static final String TAG = "ScreenshotActionChip"; - private ImageView mIcon; - private TextView mText; - private @ColorInt int mIconColor; + private ImageView mIconView; + private TextView mTextView; + private boolean mIsPending = false; public ScreenshotActionChip(Context context) { this(context, null); @@ -54,25 +53,29 @@ public class ScreenshotActionChip extends FrameLayout { public ScreenshotActionChip( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - - mIconColor = context.getColor(R.color.global_screenshot_button_icon); } @Override protected void onFinishInflate() { - mIcon = findViewById(R.id.screenshot_action_chip_icon); - mText = findViewById(R.id.screenshot_action_chip_text); + mIconView = findViewById(R.id.screenshot_action_chip_icon); + mTextView = findViewById(R.id.screenshot_action_chip_text); + } + + @Override + public void setPressed(boolean pressed) { + // override pressed state to true if there is an action pending + super.setPressed(mIsPending || pressed); } void setIcon(Icon icon, boolean tint) { - mIcon.setImageIcon(icon); + mIconView.setImageIcon(icon); if (!tint) { - mIcon.setImageTintList(null); + mIconView.setImageTintList(null); } } void setText(CharSequence text) { - mText.setText(text); + mTextView.setText(text); } void setPendingIntent(PendingIntent intent, Runnable finisher) { @@ -85,4 +88,9 @@ public class ScreenshotActionChip extends FrameLayout { } }); } + + void setIsPending(boolean isPending) { + mIsPending = isPending; + setPressed(mIsPending); + } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java new file mode 100644 index 000000000000..0dde931d78b2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -0,0 +1,689 @@ +/* + * 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.screenshot; + +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.view.Display.DEFAULT_DISPLAY; + +import static java.util.Objects.requireNonNull; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.app.Notification; +import android.content.ComponentName; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Insets; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.media.MediaActionSound; +import android.net.Uri; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.provider.DeviceConfig; +import android.provider.Settings; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Display; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.SurfaceControl; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.widget.Toast; + +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; +import com.android.internal.logging.UiEventLogger; +import com.android.systemui.R; +import com.android.systemui.util.DeviceConfigProxy; + +import java.util.List; +import java.util.function.Consumer; + +import javax.inject.Inject; + +/** + * Controls the state and flow for screenshots. + */ +public class ScreenshotController { + /** + * POD used in the AsyncTask which saves an image in the background. + */ + static class SaveImageInBackgroundData { + public Bitmap image; + public Consumer<Uri> finisher; + public ScreenshotController.ActionsReadyListener mActionsReadyListener; + + void clearImage() { + image = null; + } + } + + /** + * Structure returned by the SaveImageInBackgroundTask + */ + static class SavedImageData { + public Uri uri; + public Notification.Action shareAction; + public Notification.Action editAction; + public Notification.Action deleteAction; + public List<Notification.Action> smartActions; + + /** + * Used to reset the return data on error + */ + public void reset() { + uri = null; + shareAction = null; + editAction = null; + deleteAction = null; + smartActions = null; + } + } + + abstract static class ActionsReadyListener { + abstract void onActionsReady(ScreenshotController.SavedImageData imageData); + } + + private static final String TAG = "ScreenshotController"; + + // These strings are used for communicating the action invoked to + // ScreenshotNotificationSmartActionsProvider. + static final String EXTRA_ACTION_TYPE = "android:screenshot_action_type"; + static final String EXTRA_ID = "android:screenshot_id"; + static final String ACTION_TYPE_DELETE = "Delete"; + static final String ACTION_TYPE_SHARE = "Share"; + static final String ACTION_TYPE_EDIT = "Edit"; + static final String EXTRA_SMART_ACTIONS_ENABLED = "android:smart_actions_enabled"; + static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent"; + + static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id"; + static final String EXTRA_CANCEL_NOTIFICATION = "android:screenshot_cancel_notification"; + static final String EXTRA_DISALLOW_ENTER_PIP = "android:screenshot_disallow_enter_pip"; + + + private static final int MESSAGE_CORNER_TIMEOUT = 2; + private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000; + + // From WizardManagerHelper.java + private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete"; + + private final Context mContext; + private final ScreenshotNotificationsController mNotificationsController; + private final ScreenshotSmartActions mScreenshotSmartActions; + private final UiEventLogger mUiEventLogger; + + private final WindowManager mWindowManager; + private final WindowManager.LayoutParams mWindowLayoutParams; + private final Display mDisplay; + private final DisplayMetrics mDisplayMetrics; + private final AccessibilityManager mAccessibilityManager; + private final MediaActionSound mCameraSound; + private final ScrollCaptureClient mScrollCaptureClient; + private final DeviceConfigProxy mConfigProxy; + + private final Binder mWindowToken; + private ScreenshotView mScreenshotView; + private Bitmap mScreenBitmap; + private SaveImageInBackgroundTask mSaveInBgTask; + + private Animator mScreenshotAnimation; + private Animator mDismissAnimation; + + private Runnable mOnCompleteRunnable; + private boolean mInDarkMode; + private boolean mDirectionLTR; + private boolean mOrientationPortrait; + + private final Handler mScreenshotHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_CORNER_TIMEOUT: + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT); + ScreenshotController.this.dismissScreenshot(false); + mOnCompleteRunnable.run(); + break; + default: + break; + } + } + }; + + @Inject + ScreenshotController( + Context context, + ScreenshotSmartActions screenshotSmartActions, + ScreenshotNotificationsController screenshotNotificationsController, + ScrollCaptureClient scrollCaptureClient, + UiEventLogger uiEventLogger, + DeviceConfigProxy configProxy) { + mScreenshotSmartActions = screenshotSmartActions; + mNotificationsController = screenshotNotificationsController; + mScrollCaptureClient = scrollCaptureClient; + mUiEventLogger = uiEventLogger; + + final DisplayManager dm = requireNonNull(context.getSystemService(DisplayManager.class)); + mDisplay = dm.getDisplay(DEFAULT_DISPLAY); + mContext = context.createDisplayContext(mDisplay); + mWindowManager = mContext.getSystemService(WindowManager.class); + + mAccessibilityManager = AccessibilityManager.getInstance(mContext); + mConfigProxy = configProxy; + + reloadAssets(); + Configuration config = mContext.getResources().getConfiguration(); + mInDarkMode = config.isNightModeActive(); + mDirectionLTR = config.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; + mOrientationPortrait = config.orientation == ORIENTATION_PORTRAIT; + mWindowToken = new Binder("ScreenshotController"); + mScrollCaptureClient.setHostWindowToken(mWindowToken); + + // Setup the window that we are going to use + mWindowLayoutParams = new WindowManager.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0, + WindowManager.LayoutParams.TYPE_SCREENSHOT, + WindowManager.LayoutParams.FLAG_FULLSCREEN + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, + PixelFormat.TRANSLUCENT); + mWindowLayoutParams.setTitle("ScreenshotAnimation"); + mWindowLayoutParams.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + mWindowLayoutParams.setFitInsetsTypes(0 /* types */); + mWindowLayoutParams.token = mWindowToken; + + mDisplayMetrics = new DisplayMetrics(); + mDisplay.getRealMetrics(mDisplayMetrics); + + // Setup the Camera shutter sound + mCameraSound = new MediaActionSound(); + mCameraSound.load(MediaActionSound.SHUTTER_CLICK); + } + + void takeScreenshotFullscreen(Consumer<Uri> finisher, Runnable onComplete) { + mOnCompleteRunnable = onComplete; + + mDisplay.getRealMetrics(mDisplayMetrics); + takeScreenshotInternal( + finisher, + new Rect(0, 0, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels)); + } + + void handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds, + Insets visibleInsets, int taskId, int userId, ComponentName topComponent, + Consumer<Uri> finisher, Runnable onComplete) { + // TODO: use task Id, userId, topComponent for smart handler + mOnCompleteRunnable = onComplete; + + if (screenshot == null) { + Log.e(TAG, "Got null bitmap from screenshot message"); + mNotificationsController.notifyScreenshotError( + R.string.screenshot_failed_to_capture_text); + finisher.accept(null); + mOnCompleteRunnable.run(); + return; + } + + if (aspectRatiosMatch(screenshot, visibleInsets, screenshotScreenBounds)) { + saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, false); + } else { + saveScreenshot(screenshot, finisher, + new Rect(0, 0, screenshot.getWidth(), screenshot.getHeight()), Insets.NONE, + true); + } + } + + /** + * Displays a screenshot selector + */ + @SuppressLint("ClickableViewAccessibility") + void takeScreenshotPartial(final Consumer<Uri> finisher, Runnable onComplete) { + dismissScreenshot(true); + mOnCompleteRunnable = onComplete; + + mWindowManager.addView(mScreenshotView, mWindowLayoutParams); + + mScreenshotView.takePartialScreenshot( + rect -> takeScreenshotInternal(finisher, rect)); + } + + boolean isDismissing() { + return (mDismissAnimation != null && mDismissAnimation.isRunning()); + } + + /** + * Clears current screenshot + */ + void dismissScreenshot(boolean immediate) { + Log.v(TAG, "clearing screenshot"); + mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT); + mScreenshotView.getViewTreeObserver().removeOnComputeInternalInsetsListener( + mScreenshotView); + if (!immediate) { + mDismissAnimation = mScreenshotView.createScreenshotDismissAnimation(); + mDismissAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + clearScreenshot(); + } + }); + mDismissAnimation.start(); + } else { + clearScreenshot(); + } + } + + private void onConfigChanged(Configuration newConfig) { + boolean needsUpdate = false; + // dark mode + if (newConfig.isNightModeActive()) { + // Night mode is active, we're using dark theme + if (!mInDarkMode) { + mInDarkMode = true; + needsUpdate = true; + } + } else { + // Night mode is not active, we're using the light theme + if (mInDarkMode) { + mInDarkMode = false; + needsUpdate = true; + } + } + + // RTL configuration + switch (newConfig.getLayoutDirection()) { + case View.LAYOUT_DIRECTION_LTR: + if (!mDirectionLTR) { + mDirectionLTR = true; + needsUpdate = true; + } + break; + case View.LAYOUT_DIRECTION_RTL: + if (mDirectionLTR) { + mDirectionLTR = false; + needsUpdate = true; + } + break; + } + + // portrait/landscape orientation + switch (newConfig.orientation) { + case ORIENTATION_PORTRAIT: + if (!mOrientationPortrait) { + mOrientationPortrait = true; + needsUpdate = true; + } + break; + case ORIENTATION_LANDSCAPE: + if (mOrientationPortrait) { + mOrientationPortrait = false; + needsUpdate = true; + } + break; + } + + if (needsUpdate) { + reloadAssets(); + } + } + + /** + * Update assets (called when the dark theme status changes). We only need to update the dismiss + * button and the actions container background, since the buttons are re-inflated on demand. + */ + private void reloadAssets() { + boolean wasAttached = mScreenshotView != null && mScreenshotView.isAttachedToWindow(); + if (wasAttached) { + mWindowManager.removeView(mScreenshotView); + } + + // Inflate the screenshot layout + mScreenshotView = (ScreenshotView) + LayoutInflater.from(mContext).inflate(R.layout.global_screenshot, null); + + // TODO(159460485): Remove this when focus is handled properly in the system + mScreenshotView.setOnTouchListener((v, event) -> { + if (event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) { + // Once the user touches outside, stop listening for input + setWindowFocusable(false); + } + return false; + }); + + mScreenshotView.setOnKeyListener((v, keyCode, event) -> { + if (keyCode == KeyEvent.KEYCODE_BACK) { + dismissScreenshot(false); + return true; + } + return false; + }); + + if (wasAttached) { + mWindowManager.addView(mScreenshotView, mWindowLayoutParams); + } + } + + /** + * Takes a screenshot of the current display and shows an animation. + */ + private void takeScreenshotInternal(Consumer<Uri> finisher, Rect crop) { + // copy the input Rect, since SurfaceControl.screenshot can mutate it + Rect screenRect = new Rect(crop); + int width = crop.width(); + int height = crop.height(); + final IBinder displayToken = SurfaceControl.getInternalDisplayToken(); + final SurfaceControl.DisplayCaptureArgs captureArgs = + new SurfaceControl.DisplayCaptureArgs.Builder(displayToken) + .setSourceCrop(crop) + .setSize(width, height) + .build(); + final SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer = + SurfaceControl.captureDisplay(captureArgs); + Bitmap screenshot = screenshotBuffer == null ? null : screenshotBuffer.asBitmap(); + + if (screenshot == null) { + Log.e(TAG, "Screenshot bitmap was null"); + mNotificationsController.notifyScreenshotError( + R.string.screenshot_failed_to_capture_text); + finisher.accept(null); + mOnCompleteRunnable.run(); + return; + } + + saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, true); + } + + private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect, + Insets screenInsets, boolean showFlash) { + if (mAccessibilityManager.isEnabled()) { + AccessibilityEvent event = + new AccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + event.setContentDescription( + mContext.getResources().getString(R.string.screenshot_saving_title)); + mAccessibilityManager.sendAccessibilityEvent(event); + } + + if (mScreenshotView.isAttachedToWindow()) { + // if we didn't already dismiss for another reason + if (mDismissAnimation == null || !mDismissAnimation.isRunning()) { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED); + } + dismissScreenshot(true); + } + + mScreenBitmap = screenshot; + + if (!isUserSetupComplete()) { + // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing + // and sharing shouldn't be exposed to the user. + saveScreenshotAndToast(finisher); + return; + } + + // Optimizations + mScreenBitmap.setHasAlpha(false); + mScreenBitmap.prepareToDraw(); + + onConfigChanged(mContext.getResources().getConfiguration()); + + if (mDismissAnimation != null && mDismissAnimation.isRunning()) { + mDismissAnimation.cancel(); + } + + // The window is focusable by default + setWindowFocusable(true); + + // Start the post-screenshot animation + startAnimation(finisher, screenRect, screenInsets, showFlash); + + if (mConfigProxy.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.SCREENSHOT_SCROLLING_ENABLED, false)) { + mScrollCaptureClient.request(DEFAULT_DISPLAY, (connection) -> + mScreenshotView.showScrollChip(() -> + runScrollCapture(connection, + () -> dismissScreenshot(false)))); + } + } + + private void runScrollCapture(ScrollCaptureClient.Connection connection, + Runnable after) { + new ScrollCaptureController(mContext, connection).run(after); + } + + /** + * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on + * failure). + */ + private void saveScreenshotAndToast(Consumer<Uri> finisher) { + // Play the shutter sound to notify that we've taken a screenshot + mScreenshotHandler.post(() -> { + mCameraSound.play(MediaActionSound.SHUTTER_CLICK); + }); + + saveScreenshotInWorkerThread(finisher, + new ScreenshotController.ActionsReadyListener() { + @Override + void onActionsReady(ScreenshotController.SavedImageData imageData) { + finisher.accept(imageData.uri); + if (imageData.uri == null) { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED); + mNotificationsController.notifyScreenshotError( + R.string.screenshot_failed_to_save_text); + } else { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED); + + mScreenshotHandler.post(() -> { + Toast.makeText(mContext, R.string.screenshot_saved_title, + Toast.LENGTH_SHORT).show(); + }); + } + } + }); + } + + /** + * Starts the animation after taking the screenshot + */ + private void startAnimation(final Consumer<Uri> finisher, Rect screenRect, Insets screenInsets, + boolean showFlash) { + mScreenshotHandler.post(() -> { + if (!mScreenshotView.isAttachedToWindow()) { + mWindowManager.addView(mScreenshotView, mWindowLayoutParams); + } + + mScreenshotView.prepareForAnimation(mScreenBitmap, screenRect, screenInsets); + + mScreenshotHandler.post(() -> { + mScreenshotView.getViewTreeObserver().addOnComputeInternalInsetsListener( + mScreenshotView); + + mScreenshotAnimation = + mScreenshotView.createScreenshotDropInAnimation(screenRect, showFlash, + this::onElementTapped); + + saveScreenshotInWorkerThread(finisher, + new ScreenshotController.ActionsReadyListener() { + @Override + void onActionsReady( + ScreenshotController.SavedImageData imageData) { + showUiOnActionsReady(imageData); + } + }); + + // Play the shutter sound to notify that we've taken a screenshot + mCameraSound.play(MediaActionSound.SHUTTER_CLICK); + + mScreenshotAnimation.start(); + }); + }); + } + + /** + * Creates a new worker thread and saves the screenshot to the media store. + */ + private void saveScreenshotInWorkerThread( + Consumer<Uri> finisher, + @Nullable ScreenshotController.ActionsReadyListener actionsReadyListener) { + ScreenshotController.SaveImageInBackgroundData + data = new ScreenshotController.SaveImageInBackgroundData(); + data.image = mScreenBitmap; + data.finisher = finisher; + data.mActionsReadyListener = actionsReadyListener; + + if (mSaveInBgTask != null) { + // just log success/failure for the pre-existing screenshot + mSaveInBgTask.setActionsReadyListener( + new ScreenshotController.ActionsReadyListener() { + @Override + void onActionsReady(ScreenshotController.SavedImageData imageData) { + logSuccessOnActionsReady(imageData); + } + }); + } + + mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data); + mSaveInBgTask.execute(); + } + + /** + * Sets up the action shade and its entrance animation, once we get the screenshot URI. + */ + private void showUiOnActionsReady(ScreenshotController.SavedImageData imageData) { + logSuccessOnActionsReady(imageData); + + AccessibilityManager accessibilityManager = (AccessibilityManager) + mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); + long timeoutMs = accessibilityManager.getRecommendedTimeoutMillis( + SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS, + AccessibilityManager.FLAG_CONTENT_CONTROLS); + + mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT); + mScreenshotHandler.sendMessageDelayed( + mScreenshotHandler.obtainMessage(MESSAGE_CORNER_TIMEOUT), + timeoutMs); + + if (imageData.uri != null) { + mScreenshotHandler.post(() -> { + if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { + mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mScreenshotView.setChipIntents( + imageData, event -> onElementTapped(event)); + } + }); + } else { + mScreenshotView.setChipIntents( + imageData, this::onElementTapped); + } + }); + } + } + + private void onElementTapped(ScreenshotEvent event) { + mUiEventLogger.log(event); + dismissScreenshot(false); + mOnCompleteRunnable.run(); + } + + /** + * Logs success/failure of the screenshot saving task, and shows an error if it failed. + */ + private void logSuccessOnActionsReady(ScreenshotController.SavedImageData imageData) { + if (imageData.uri == null) { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED); + mNotificationsController.notifyScreenshotError( + R.string.screenshot_failed_to_save_text); + } else { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED); + } + } + + private void clearScreenshot() { + if (mScreenshotView.isAttachedToWindow()) { + mWindowManager.removeView(mScreenshotView); + } + + mScreenshotView.reset(); + } + + private boolean isUserSetupComplete() { + return Settings.Secure.getInt(mContext.getContentResolver(), + SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; + } + + + /** + * Updates the window focusability. If the window is already showing, then it updates the + * window immediately, otherwise the layout params will be applied when the window is next + * shown. + */ + private void setWindowFocusable(boolean focusable) { + if (focusable) { + mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + } else { + mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + } + if (mScreenshotView.isAttachedToWindow()) { + mWindowManager.updateViewLayout(mScreenshotView, mWindowLayoutParams); + } + } + + /** Does the aspect ratio of the bitmap with insets removed match the bounds. */ + private static boolean aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets, + Rect screenBounds) { + int insettedWidth = bitmap.getWidth() - bitmapInsets.left - bitmapInsets.right; + int insettedHeight = bitmap.getHeight() - bitmapInsets.top - bitmapInsets.bottom; + + if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0 + || bitmap.getHeight() == 0) { + Log.e(TAG, String.format( + "Provided bitmap and insets create degenerate region: %dx%d %s", + bitmap.getWidth(), bitmap.getHeight(), bitmapInsets)); + return false; + } + + float insettedBitmapAspect = ((float) insettedWidth) / insettedHeight; + float boundsAspect = ((float) screenBounds.width()) / screenBounds.height(); + + boolean matchWithinTolerance = Math.abs(insettedBitmapAspect - boundsAspect) < 0.1f; + if (!matchWithinTolerance) { + Log.d(TAG, String.format("aspectRatiosMatch: don't match bitmap: %f, bounds: %f", + insettedBitmapAspect, boundsAspect)); + } + + return matchWithinTolerance; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java index 6d1299ba98ac..db5a4941ca58 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java @@ -25,13 +25,6 @@ import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.Intent; import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.ColorMatrix; -import android.graphics.ColorMatrixColorFilter; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.Picture; import android.os.UserHandle; import android.util.DisplayMetrics; import android.view.WindowManager; @@ -52,179 +45,16 @@ public class ScreenshotNotificationsController { private final Context mContext; private final Resources mResources; private final NotificationManager mNotificationManager; - private final Notification.BigPictureStyle mNotificationStyle; - - private int mIconSize; - private int mPreviewWidth, mPreviewHeight; - private Notification.Builder mNotificationBuilder, mPublicNotificationBuilder; @Inject ScreenshotNotificationsController(Context context, WindowManager windowManager) { mContext = context; mResources = context.getResources(); - mNotificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE); - mIconSize = mResources.getDimensionPixelSize( - android.R.dimen.notification_large_icon_height); - DisplayMetrics displayMetrics = new DisplayMetrics(); windowManager.getDefaultDisplay().getRealMetrics(displayMetrics); - - - // determine the optimal preview size - int panelWidth = 0; - try { - panelWidth = mResources.getDimensionPixelSize(R.dimen.notification_panel_width); - } catch (Resources.NotFoundException e) { - } - if (panelWidth <= 0) { - // includes notification_panel_width==match_parent (-1) - panelWidth = displayMetrics.widthPixels; - } - mPreviewWidth = panelWidth; - mPreviewHeight = mResources.getDimensionPixelSize(R.dimen.notification_max_height); - - // Setup the notification - mNotificationStyle = new Notification.BigPictureStyle(); - } - - /** - * Resets the notification builders. - */ - public void reset() { - // The public notification will show similar info but with the actual screenshot omitted - mPublicNotificationBuilder = - new Notification.Builder(mContext, NotificationChannels.SCREENSHOTS_HEADSUP); - mNotificationBuilder = - new Notification.Builder(mContext, NotificationChannels.SCREENSHOTS_HEADSUP); - } - - /** - * Sets the current screenshot bitmap. - * - * @param image the bitmap of the current screenshot (used for preview) - */ - public void setImage(Bitmap image) { - // Create the large notification icon - int imageWidth = image.getWidth(); - int imageHeight = image.getHeight(); - - Paint paint = new Paint(); - ColorMatrix desat = new ColorMatrix(); - desat.setSaturation(0.25f); - paint.setColorFilter(new ColorMatrixColorFilter(desat)); - Matrix matrix = new Matrix(); - int overlayColor = 0x40FFFFFF; - - matrix.setTranslate((mPreviewWidth - imageWidth) / 2f, (mPreviewHeight - imageHeight) / 2f); - - Bitmap picture = generateAdjustedHwBitmap( - image, mPreviewWidth, mPreviewHeight, matrix, paint, overlayColor); - - mNotificationStyle.bigPicture(picture.asShared()); - - // Note, we can't use the preview for the small icon, since it is non-square - float scale = (float) mIconSize / Math.min(imageWidth, imageHeight); - matrix.setScale(scale, scale); - matrix.postTranslate( - (mIconSize - (scale * imageWidth)) / 2, - (mIconSize - (scale * imageHeight)) / 2); - Bitmap icon = - generateAdjustedHwBitmap(image, mIconSize, mIconSize, matrix, paint, overlayColor); - - /** - * NOTE: The following code prepares the notification builder for updating the - * notification after the screenshot has been written to disk. - */ - - // On the tablet, the large icon makes the notification appear as if it is clickable - // (and on small devices, the large icon is not shown) so defer showing the large icon - // until we compose the final post-save notification below. - mNotificationBuilder.setLargeIcon(icon.asShared()); - // But we still don't set it for the expanded view, allowing the smallIcon to show here. - mNotificationStyle.bigLargeIcon((Bitmap) null); - } - - /** - * Shows a notification to inform the user that a screenshot is currently being saved. - */ - public void showSavingScreenshotNotification() { - final long now = System.currentTimeMillis(); - - mPublicNotificationBuilder - .setContentTitle(mResources.getString(R.string.screenshot_saving_title)) - .setSmallIcon(R.drawable.stat_notify_image) - .setCategory(Notification.CATEGORY_PROGRESS) - .setWhen(now) - .setShowWhen(true) - .setColor(mResources.getColor( - com.android.internal.R.color.system_notification_accent_color)); - SystemUI.overrideNotificationAppName(mContext, mPublicNotificationBuilder, true); - - mNotificationBuilder - .setContentTitle(mResources.getString(R.string.screenshot_saving_title)) - .setSmallIcon(R.drawable.stat_notify_image) - .setWhen(now) - .setShowWhen(true) - .setColor(mResources.getColor( - com.android.internal.R.color.system_notification_accent_color)) - .setStyle(mNotificationStyle) - .setPublicVersion(mPublicNotificationBuilder.build()); - mNotificationBuilder.setFlag(Notification.FLAG_NO_CLEAR, true); - SystemUI.overrideNotificationAppName(mContext, mNotificationBuilder, true); - - mNotificationManager.notify(SystemMessageProto.SystemMessage.NOTE_GLOBAL_SCREENSHOT, - mNotificationBuilder.build()); - } - - /** - * Shows a notification with the saved screenshot and actions that can be taken with it. - * - * @param actionData SavedImageData struct with image URI and actions - */ - public void showScreenshotActionsNotification( - GlobalScreenshot.SavedImageData actionData) { - mNotificationBuilder.addAction(actionData.shareAction); - mNotificationBuilder.addAction(actionData.editAction); - mNotificationBuilder.addAction(actionData.deleteAction); - for (Notification.Action smartAction : actionData.smartActions) { - mNotificationBuilder.addAction(smartAction); - } - - // Create the intent to show the screenshot in gallery - Intent launchIntent = new Intent(Intent.ACTION_VIEW); - launchIntent.setDataAndType(actionData.uri, "image/png"); - launchIntent.setFlags( - Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION); - - final long now = System.currentTimeMillis(); - - // Update the text and the icon for the existing notification - mPublicNotificationBuilder - .setContentTitle(mResources.getString(R.string.screenshot_saved_title)) - .setContentText(mResources.getString(R.string.screenshot_saved_text)) - .setContentIntent(PendingIntent - .getActivity(mContext, 0, launchIntent, PendingIntent.FLAG_IMMUTABLE)) - .setWhen(now) - .setAutoCancel(true) - .setColor(mContext.getColor( - com.android.internal.R.color.system_notification_accent_color)); - mNotificationBuilder - .setContentTitle(mResources.getString(R.string.screenshot_saved_title)) - .setContentText(mResources.getString(R.string.screenshot_saved_text)) - .setContentIntent(PendingIntent - .getActivity(mContext, 0, launchIntent, PendingIntent.FLAG_IMMUTABLE)) - .setWhen(now) - .setAutoCancel(true) - .setColor(mContext.getColor( - com.android.internal.R.color.system_notification_accent_color)) - .setPublicVersion(mPublicNotificationBuilder.build()) - .setFlag(Notification.FLAG_NO_CLEAR, false); - - mNotificationManager.notify(SystemMessageProto.SystemMessage.NOTE_GLOBAL_SCREENSHOT, - mNotificationBuilder.build()); } /** @@ -263,31 +93,4 @@ public class ScreenshotNotificationsController { .build(); mNotificationManager.notify(SystemMessageProto.SystemMessage.NOTE_GLOBAL_SCREENSHOT, n); } - - /** - * Cancels the current screenshot notification. - */ - public void cancelNotification() { - mNotificationManager.cancel(SystemMessageProto.SystemMessage.NOTE_GLOBAL_SCREENSHOT); - } - - /** - * Generates a new hardware bitmap with specified values, copying the content from the - * passed in bitmap. - */ - private Bitmap generateAdjustedHwBitmap(Bitmap bitmap, int width, int height, Matrix matrix, - Paint paint, int color) { - Picture picture = new Picture(); - Canvas canvas = picture.beginRecording(width, height); - canvas.drawColor(color); - canvas.drawBitmap(bitmap, matrix, paint); - picture.endRecording(); - return Bitmap.createBitmap(picture); - } - - static void cancelScreenshotNotification(Context context) { - final NotificationManager nm = - (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE); - nm.cancel(SystemMessageProto.SystemMessage.NOTE_GLOBAL_SCREENSHOT); - } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSelectorView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSelectorView.java index 07a92460f3ea..c793b5b9639e 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSelectorView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSelectorView.java @@ -26,8 +26,11 @@ import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.util.AttributeSet; +import android.view.MotionEvent; import android.view.View; +import java.util.function.Consumer; + /** * Draws a selection rectangle while taking screenshot */ @@ -36,6 +39,8 @@ public class ScreenshotSelectorView extends View { private Rect mSelectionRect; private final Paint mPaintSelection, mPaintBackground; + private Consumer<Rect> mOnScreenshotSelected; + public ScreenshotSelectorView(Context context) { this(context, null); } @@ -46,14 +51,54 @@ public class ScreenshotSelectorView extends View { mPaintBackground.setAlpha(160); mPaintSelection = new Paint(Color.TRANSPARENT); mPaintSelection.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + + setOnTouchListener((v, event) -> { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + startSelection((int) event.getX(), (int) event.getY()); + return true; + case MotionEvent.ACTION_MOVE: + updateSelection((int) event.getX(), (int) event.getY()); + return true; + case MotionEvent.ACTION_UP: + setVisibility(View.GONE); + final Rect rect = getSelectionRect(); + if (mOnScreenshotSelected != null + && rect != null + && rect.width() != 0 && rect.height() != 0) { + mOnScreenshotSelected.accept(rect); + } + stopSelection(); + return true; + } + return false; + }); + } + + @Override + public void draw(Canvas canvas) { + canvas.drawRect(mLeft, mTop, mRight, mBottom, mPaintBackground); + if (mSelectionRect != null) { + canvas.drawRect(mSelectionRect, mPaintSelection); + } + } + + void setOnScreenshotSelected(Consumer<Rect> onScreenshotSelected) { + mOnScreenshotSelected = onScreenshotSelected; + } + + void stop() { + if (getSelectionRect() != null) { + stopSelection(); + } } - public void startSelection(int x, int y) { + private void startSelection(int x, int y) { mStartPoint = new Point(x, y); mSelectionRect = new Rect(x, y, x, y); } - public void updateSelection(int x, int y) { + private void updateSelection(int x, int y) { if (mSelectionRect != null) { mSelectionRect.left = Math.min(mStartPoint.x, x); mSelectionRect.right = Math.max(mStartPoint.x, x); @@ -63,20 +108,12 @@ public class ScreenshotSelectorView extends View { } } - public Rect getSelectionRect() { + private Rect getSelectionRect() { return mSelectionRect; } - public void stopSelection() { + private void stopSelection() { mStartPoint = null; mSelectionRect = null; } - - @Override - public void draw(Canvas canvas) { - canvas.drawRect(mLeft, mTop, mRight, mBottom, mPaintBackground); - if (mSelectionRect != null) { - canvas.drawRect(mSelectionRect, mPaintSelection); - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java new file mode 100644 index 000000000000..3383f80cd2b0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -0,0 +1,597 @@ +/* + * 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.screenshot; + +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; + +import static java.util.Objects.requireNonNull; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.app.ActivityManager; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Insets; +import android.graphics.Outline; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.graphics.drawable.InsetDrawable; +import android.graphics.drawable.LayerDrawable; +import android.os.RemoteException; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.MathUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.view.ViewTreeObserver; +import android.view.WindowInsets; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.FrameLayout; +import android.widget.HorizontalScrollView; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import com.android.systemui.R; +import com.android.systemui.shared.system.QuickStepContract; + +import java.util.ArrayList; +import java.util.function.Consumer; + +/** + * Handles the visual elements and animations for the screenshot flow. + */ +public class ScreenshotView extends FrameLayout implements + ViewTreeObserver.OnComputeInternalInsetsListener { + + private static final String TAG = "ScreenshotView"; + + private static final long SCREENSHOT_FLASH_IN_DURATION_MS = 133; + private static final long SCREENSHOT_FLASH_OUT_DURATION_MS = 217; + // delay before starting to fade in dismiss button + private static final long SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS = 200; + private static final long SCREENSHOT_TO_CORNER_X_DURATION_MS = 234; + private static final long SCREENSHOT_TO_CORNER_Y_DURATION_MS = 500; + private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234; + private static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400; + private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100; + private static final long SCREENSHOT_DISMISS_Y_DURATION_MS = 350; + private static final long SCREENSHOT_DISMISS_ALPHA_DURATION_MS = 183; + private static final long SCREENSHOT_DISMISS_ALPHA_OFFSET_MS = 50; // delay before starting fade + private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f; + private static final float ROUNDED_CORNER_RADIUS = .05f; + + private final Interpolator mAccelerateInterpolator = new AccelerateInterpolator(); + + private final Resources mResources; + private final Interpolator mFastOutSlowIn; + private final DisplayMetrics mDisplayMetrics; + private final float mCornerSizeX; + private final float mDismissDeltaY; + + private int mNavMode; + private int mLeftInset; + private int mRightInset; + private boolean mOrientationPortrait; + private boolean mDirectionLTR; + + private ScreenshotSelectorView mScreenshotSelectorView; + private ImageView mScreenshotPreview; + private ImageView mScreenshotFlash; + private ImageView mActionsContainerBackground; + private HorizontalScrollView mActionsContainer; + private LinearLayout mActionsView; + private ImageView mBackgroundProtection; + private FrameLayout mDismissButton; + private ScreenshotActionChip mShareChip; + private ScreenshotActionChip mEditChip; + private ScreenshotActionChip mScrollChip; + + private final ArrayList<ScreenshotActionChip> mSmartChips = new ArrayList<>(); + private PendingInteraction mPendingInteraction; + + private enum PendingInteraction { + PREVIEW, + EDIT, + SHARE + } + + public ScreenshotView(Context context) { + this(context, null); + } + + public ScreenshotView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ScreenshotView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ScreenshotView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + mResources = mContext.getResources(); + + mCornerSizeX = mResources.getDimensionPixelSize(R.dimen.global_screenshot_x_scale); + mDismissDeltaY = mResources.getDimensionPixelSize( + R.dimen.screenshot_dismissal_height_delta); + + // standard material ease + mFastOutSlowIn = + AnimationUtils.loadInterpolator(mContext, android.R.interpolator.fast_out_slow_in); + + mDisplayMetrics = new DisplayMetrics(); + mContext.getDisplay().getRealMetrics(mDisplayMetrics); + } + + /** + * Called to display the scroll action chip when support is detected. + * + * @param onClick the action to take when the chip is clicked. + */ + public void showScrollChip(Runnable onClick) { + mScrollChip.setVisibility(VISIBLE); + mScrollChip.setOnClickListener((v) -> + onClick.run() + // TODO Logging, store event consumer to a field + //onElementTapped.accept(ScreenshotEvent.SCREENSHOT_SCROLL_TAPPED); + ); + } + + @Override // ViewTreeObserver.OnComputeInternalInsetsListener + public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { + inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + Region touchRegion = new Region(); + + Rect screenshotRect = new Rect(); + mScreenshotPreview.getBoundsOnScreen(screenshotRect); + touchRegion.op(screenshotRect, Region.Op.UNION); + Rect actionsRect = new Rect(); + mActionsContainer.getBoundsOnScreen(actionsRect); + touchRegion.op(actionsRect, Region.Op.UNION); + Rect dismissRect = new Rect(); + mDismissButton.getBoundsOnScreen(dismissRect); + touchRegion.op(dismissRect, Region.Op.UNION); + + if (QuickStepContract.isGesturalMode(mNavMode)) { + // Receive touches in gesture insets such that they don't cause TOUCH_OUTSIDE + Rect inset = new Rect(0, 0, mLeftInset, mDisplayMetrics.heightPixels); + touchRegion.op(inset, Region.Op.UNION); + inset.set(mDisplayMetrics.widthPixels - mRightInset, 0, mDisplayMetrics.widthPixels, + mDisplayMetrics.heightPixels); + touchRegion.op(inset, Region.Op.UNION); + } + + inoutInfo.touchableRegion.set(touchRegion); + } + + @Override // View + protected void onFinishInflate() { + mScreenshotPreview = requireNonNull(findViewById(R.id.global_screenshot_preview)); + mActionsContainerBackground = requireNonNull(findViewById( + R.id.global_screenshot_actions_container_background)); + mActionsContainer = requireNonNull(findViewById(R.id.global_screenshot_actions_container)); + mActionsView = requireNonNull(findViewById(R.id.global_screenshot_actions)); + mBackgroundProtection = requireNonNull( + findViewById(R.id.global_screenshot_actions_background)); + mDismissButton = requireNonNull(findViewById(R.id.global_screenshot_dismiss_button)); + mScreenshotFlash = requireNonNull(findViewById(R.id.global_screenshot_flash)); + mScreenshotSelectorView = requireNonNull(findViewById(R.id.global_screenshot_selector)); + mShareChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_share_chip)); + mEditChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_edit_chip)); + mScrollChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_scroll_chip)); + + mScreenshotPreview.setClipToOutline(true); + mScreenshotPreview.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(new Rect(0, 0, view.getWidth(), view.getHeight()), + ROUNDED_CORNER_RADIUS * view.getWidth()); + } + }); + + setFocusable(true); + mScreenshotSelectorView.setFocusable(true); + mScreenshotSelectorView.setFocusableInTouchMode(true); + mActionsContainer.setScrollX(0); + + mNavMode = getResources().getInteger( + com.android.internal.R.integer.config_navBarInteractionMode); + mOrientationPortrait = + getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT; + mDirectionLTR = + getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; + + setOnApplyWindowInsetsListener((v, insets) -> { + if (QuickStepContract.isGesturalMode(mNavMode)) { + Insets gestureInsets = insets.getInsets( + WindowInsets.Type.systemGestures()); + mLeftInset = gestureInsets.left; + mRightInset = gestureInsets.right; + } else { + mLeftInset = mRightInset = 0; + } + return ScreenshotView.this.onApplyWindowInsets(insets); + }); + + // Get focus so that the key events go to the layout. + setFocusableInTouchMode(true); + requestFocus(); + } + + void takePartialScreenshot(Consumer<Rect> onPartialScreenshotSelected) { + mScreenshotSelectorView.setOnScreenshotSelected(onPartialScreenshotSelected); + mScreenshotSelectorView.setVisibility(View.VISIBLE); + mScreenshotSelectorView.requestFocus(); + } + + void prepareForAnimation(Bitmap bitmap, Rect screenRect, Insets screenInsets) { + mScreenshotPreview.setImageDrawable(createScreenDrawable(mResources, bitmap, screenInsets)); + // make static preview invisible (from gone) so we can query its location on screen + mScreenshotPreview.setVisibility(View.INVISIBLE); + } + + AnimatorSet createScreenshotDropInAnimation(Rect bounds, boolean showFlash, + Consumer<ScreenshotEvent> onElementTapped) { + mScreenshotPreview.setLayerType(View.LAYER_TYPE_HARDWARE, null); + mScreenshotPreview.buildLayer(); + + Rect previewBounds = new Rect(); + mScreenshotPreview.getBoundsOnScreen(previewBounds); + + float cornerScale = + mCornerSizeX / (mOrientationPortrait ? bounds.width() : bounds.height()); + final float currentScale = 1 / cornerScale; + + mScreenshotPreview.setScaleX(currentScale); + mScreenshotPreview.setScaleY(currentScale); + + mDismissButton.setAlpha(0); + mDismissButton.setVisibility(View.VISIBLE); + + AnimatorSet dropInAnimation = new AnimatorSet(); + ValueAnimator flashInAnimator = ValueAnimator.ofFloat(0, 1); + flashInAnimator.setDuration(SCREENSHOT_FLASH_IN_DURATION_MS); + flashInAnimator.setInterpolator(mFastOutSlowIn); + flashInAnimator.addUpdateListener(animation -> + mScreenshotFlash.setAlpha((float) animation.getAnimatedValue())); + + ValueAnimator flashOutAnimator = ValueAnimator.ofFloat(1, 0); + flashOutAnimator.setDuration(SCREENSHOT_FLASH_OUT_DURATION_MS); + flashOutAnimator.setInterpolator(mFastOutSlowIn); + flashOutAnimator.addUpdateListener(animation -> + mScreenshotFlash.setAlpha((float) animation.getAnimatedValue())); + + // animate from the current location, to the static preview location + final PointF startPos = new PointF(bounds.centerX(), bounds.centerY()); + final PointF finalPos = new PointF(previewBounds.centerX(), previewBounds.centerY()); + + ValueAnimator toCorner = ValueAnimator.ofFloat(0, 1); + toCorner.setDuration(SCREENSHOT_TO_CORNER_Y_DURATION_MS); + float xPositionPct = + SCREENSHOT_TO_CORNER_X_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; + float dismissPct = + SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; + float scalePct = + SCREENSHOT_TO_CORNER_SCALE_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; + toCorner.addUpdateListener(animation -> { + float t = animation.getAnimatedFraction(); + if (t < scalePct) { + float scale = MathUtils.lerp( + currentScale, 1, mFastOutSlowIn.getInterpolation(t / scalePct)); + mScreenshotPreview.setScaleX(scale); + mScreenshotPreview.setScaleY(scale); + } else { + mScreenshotPreview.setScaleX(1); + mScreenshotPreview.setScaleY(1); + } + + if (t < xPositionPct) { + float xCenter = MathUtils.lerp(startPos.x, finalPos.x, + mFastOutSlowIn.getInterpolation(t / xPositionPct)); + mScreenshotPreview.setX(xCenter - mScreenshotPreview.getWidth() / 2f); + } else { + mScreenshotPreview.setX(finalPos.x - mScreenshotPreview.getWidth() / 2f); + } + float yCenter = MathUtils.lerp( + startPos.y, finalPos.y, mFastOutSlowIn.getInterpolation(t)); + mScreenshotPreview.setY(yCenter - mScreenshotPreview.getHeight() / 2f); + + if (t >= dismissPct) { + mDismissButton.setAlpha((t - dismissPct) / (1 - dismissPct)); + float currentX = mScreenshotPreview.getX(); + float currentY = mScreenshotPreview.getY(); + mDismissButton.setY(currentY - mDismissButton.getHeight() / 2f); + if (mDirectionLTR) { + mDismissButton.setX(currentX + mScreenshotPreview.getWidth() + - mDismissButton.getWidth() / 2f); + } else { + mDismissButton.setX(currentX - mDismissButton.getWidth() / 2f); + } + } + }); + + toCorner.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + mScreenshotPreview.setVisibility(View.VISIBLE); + } + }); + + mScreenshotFlash.setAlpha(0f); + mScreenshotFlash.setVisibility(View.VISIBLE); + + if (showFlash) { + dropInAnimation.play(flashOutAnimator).after(flashInAnimator); + dropInAnimation.play(flashOutAnimator).with(toCorner); + } else { + dropInAnimation.play(toCorner); + } + + dropInAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mDismissButton.setOnClickListener(view -> + onElementTapped.accept(ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL)); + mDismissButton.setAlpha(1); + float dismissOffset = mDismissButton.getWidth() / 2f; + float finalDismissX = mDirectionLTR + ? finalPos.x - dismissOffset + bounds.width() * cornerScale / 2f + : finalPos.x - dismissOffset - bounds.width() * cornerScale / 2f; + mDismissButton.setX(finalDismissX); + mDismissButton.setY( + finalPos.y - dismissOffset - bounds.height() * cornerScale / 2f); + mScreenshotPreview.setScaleX(1); + mScreenshotPreview.setScaleY(1); + mScreenshotPreview.setX(finalPos.x - bounds.width() * cornerScale / 2f); + mScreenshotPreview.setY(finalPos.y - bounds.height() * cornerScale / 2f); + requestLayout(); + createScreenshotActionsShadeAnimation().start(); + } + }); + + return dropInAnimation; + } + + ValueAnimator createScreenshotActionsShadeAnimation() { + // By default the activities won't be able to start immediately; override this to keep + // the same behavior as if started from a notification + try { + ActivityManager.getService().resumeAppSwitches(); + } catch (RemoteException e) { + } + + ArrayList<ScreenshotActionChip> chips = new ArrayList<>(); + + mShareChip.setText(mContext.getString(com.android.internal.R.string.share)); + mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true); + mShareChip.setOnClickListener(v -> { + mShareChip.setIsPending(true); + mEditChip.setIsPending(false); + mPendingInteraction = PendingInteraction.SHARE; + }); + chips.add(mShareChip); + + mEditChip.setText(mContext.getString(R.string.screenshot_edit_label)); + mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true); + mEditChip.setOnClickListener(v -> { + mEditChip.setIsPending(true); + mShareChip.setIsPending(false); + mPendingInteraction = PendingInteraction.EDIT; + }); + chips.add(mEditChip); + + mScreenshotPreview.setOnClickListener(v -> { + mShareChip.setIsPending(false); + mEditChip.setIsPending(false); + mPendingInteraction = PendingInteraction.PREVIEW; + }); + + mScrollChip.setText(mContext.getString(R.string.screenshot_scroll_label)); + mScrollChip.setIcon(Icon.createWithResource(mContext, + R.drawable.ic_screenshot_scroll), true); + chips.add(mScrollChip); + + // remove the margin from the last chip so that it's correctly aligned with the end + LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) + mActionsView.getChildAt(0).getLayoutParams(); + params.setMarginEnd(0); + mActionsView.getChildAt(0).setLayoutParams(params); + + ValueAnimator animator = ValueAnimator.ofFloat(0, 1); + animator.setDuration(SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS); + float alphaFraction = (float) SCREENSHOT_ACTIONS_ALPHA_DURATION_MS + / SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS; + mActionsContainer.setAlpha(0f); + mActionsContainerBackground.setAlpha(0f); + mActionsContainer.setVisibility(View.VISIBLE); + mActionsContainerBackground.setVisibility(View.VISIBLE); + + animator.addUpdateListener(animation -> { + float t = animation.getAnimatedFraction(); + mBackgroundProtection.setAlpha(t); + float containerAlpha = t < alphaFraction ? t / alphaFraction : 1; + mActionsContainer.setAlpha(containerAlpha); + mActionsContainerBackground.setAlpha(containerAlpha); + float containerScale = SCREENSHOT_ACTIONS_START_SCALE_X + + (t * (1 - SCREENSHOT_ACTIONS_START_SCALE_X)); + mActionsContainer.setScaleX(containerScale); + mActionsContainerBackground.setScaleX(containerScale); + for (ScreenshotActionChip chip : chips) { + chip.setAlpha(t); + chip.setScaleX(1 / containerScale); // invert to keep size of children constant + } + mActionsContainer.setScrollX(mDirectionLTR ? 0 : mActionsContainer.getWidth()); + mActionsContainer.setPivotX(mDirectionLTR ? 0 : mActionsContainer.getWidth()); + mActionsContainerBackground.setPivotX( + mDirectionLTR ? 0 : mActionsContainerBackground.getWidth()); + }); + return animator; + } + + void setChipIntents(ScreenshotController.SavedImageData imageData, + Consumer<ScreenshotEvent> onElementTapped) { + mShareChip.setPendingIntent(imageData.shareAction.actionIntent, + () -> onElementTapped.accept(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED)); + mEditChip.setPendingIntent(imageData.editAction.actionIntent, + () -> onElementTapped.accept(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED)); + mScreenshotPreview.setOnClickListener(v -> { + try { + imageData.editAction.actionIntent.send(); + } catch (PendingIntent.CanceledException e) { + Log.e(TAG, "Intent cancelled", e); + } + onElementTapped.accept(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED); + }); + + if (mPendingInteraction != null) { + switch (mPendingInteraction) { + case PREVIEW: + mScreenshotPreview.callOnClick(); + break; + case SHARE: + mShareChip.callOnClick(); + break; + case EDIT: + mEditChip.callOnClick(); + break; + } + } else { + LayoutInflater inflater = LayoutInflater.from(mContext); + + for (Notification.Action smartAction : imageData.smartActions) { + ScreenshotActionChip actionChip = (ScreenshotActionChip) inflater.inflate( + R.layout.global_screenshot_action_chip, mActionsView, false); + actionChip.setText(smartAction.title); + actionChip.setIcon(smartAction.getIcon(), false); + actionChip.setPendingIntent(smartAction.actionIntent, + () -> onElementTapped.accept( + ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED)); + mActionsView.addView(actionChip); + mSmartChips.add(actionChip); + } + } + } + + + AnimatorSet createScreenshotDismissAnimation() { + ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); + alphaAnim.setStartDelay(SCREENSHOT_DISMISS_ALPHA_OFFSET_MS); + alphaAnim.setDuration(SCREENSHOT_DISMISS_ALPHA_DURATION_MS); + alphaAnim.addUpdateListener(animation -> { + setAlpha(1 - animation.getAnimatedFraction()); + }); + + ValueAnimator yAnim = ValueAnimator.ofFloat(0, 1); + yAnim.setInterpolator(mAccelerateInterpolator); + yAnim.setDuration(SCREENSHOT_DISMISS_Y_DURATION_MS); + float screenshotStartY = mScreenshotPreview.getTranslationY(); + float dismissStartY = mDismissButton.getTranslationY(); + yAnim.addUpdateListener(animation -> { + float yDelta = MathUtils.lerp(0, mDismissDeltaY, animation.getAnimatedFraction()); + mScreenshotPreview.setTranslationY(screenshotStartY + yDelta); + mDismissButton.setTranslationY(dismissStartY + yDelta); + mActionsContainer.setTranslationY(yDelta); + mActionsContainerBackground.setTranslationY(yDelta); + }); + + AnimatorSet animSet = new AnimatorSet(); + animSet.play(yAnim).with(alphaAnim); + + return animSet; + } + + void reset() { + // Clear any references to the bitmap + mScreenshotPreview.setImageDrawable(null); + mActionsContainerBackground.setVisibility(View.GONE); + mActionsContainer.setVisibility(View.GONE); + mBackgroundProtection.setAlpha(0f); + mDismissButton.setVisibility(View.GONE); + mScreenshotPreview.setVisibility(View.GONE); + mScreenshotPreview.setLayerType(View.LAYER_TYPE_NONE, null); + mScreenshotPreview.setContentDescription( + mContext.getResources().getString(R.string.screenshot_preview_description)); + mScreenshotPreview.setOnClickListener(null); + mShareChip.setOnClickListener(null); + mEditChip.setOnClickListener(null); + mShareChip.setIsPending(false); + mEditChip.setIsPending(false); + mPendingInteraction = null; + for (ScreenshotActionChip chip : mSmartChips) { + mActionsView.removeView(chip); + } + mSmartChips.clear(); + setAlpha(1); + mDismissButton.setTranslationY(0); + mActionsContainer.setTranslationY(0); + mActionsContainerBackground.setTranslationY(0); + mScreenshotPreview.setTranslationY(0); + mScreenshotSelectorView.stop(); + } + + /** + * Create a drawable using the size of the bitmap and insets as the fractional inset parameters. + */ + private static Drawable createScreenDrawable(Resources res, Bitmap bitmap, Insets insets) { + int insettedWidth = bitmap.getWidth() - insets.left - insets.right; + int insettedHeight = bitmap.getHeight() - insets.top - insets.bottom; + + BitmapDrawable bitmapDrawable = new BitmapDrawable(res, bitmap); + if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0 + || bitmap.getHeight() == 0) { + Log.e(TAG, String.format( + "Can't create insetted drawable, using 0 insets " + + "bitmap and insets create degenerate region: %dx%d %s", + bitmap.getWidth(), bitmap.getHeight(), insets)); + return bitmapDrawable; + } + + InsetDrawable insetDrawable = new InsetDrawable(bitmapDrawable, + -1f * insets.left / insettedWidth, + -1f * insets.top / insettedHeight, + -1f * insets.right / insettedWidth, + -1f * insets.bottom / insettedHeight); + + if (insets.left < 0 || insets.top < 0 || insets.right < 0 || insets.bottom < 0) { + // Are any of the insets negative, meaning the bitmap is smaller than the bounds so need + // to fill in the background of the drawable. + return new LayerDrawable(new Drawable[]{ + new ColorDrawable(Color.BLACK), insetDrawable}); + } else { + return insetDrawable; + } + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureClient.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureClient.java new file mode 100644 index 000000000000..ea835fa94fe8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureClient.java @@ -0,0 +1,346 @@ +/* + * 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.screenshot; + +import static java.util.Objects.requireNonNull; + +import android.annotation.UiContext; +import android.app.ActivityTaskManager; +import android.content.Context; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.graphics.Rect; +import android.hardware.HardwareBuffer; +import android.media.Image; +import android.media.ImageReader; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.view.IScrollCaptureCallbacks; +import android.view.IScrollCaptureConnection; +import android.view.IWindowManager; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.view.ScrollCaptureViewSupport; + +import java.util.function.Consumer; + +import javax.inject.Inject; + +/** + * High level interface to scroll capture API. + */ +public class ScrollCaptureClient { + + @VisibleForTesting + static final int MATCH_ANY_TASK = ActivityTaskManager.INVALID_TASK_ID; + + private static final String TAG = "ScrollCaptureClient"; + + /** Whether to log method names and arguments for most calls */ + private static final boolean DEBUG_TRACE = false; + + /** + * A connection to a remote window. Starts a capture session. + */ + public interface Connection { + /** + * Session start should be deferred until UI is active because of resource allocation and + * potential visible side effects in the target window. + * + * @param maxBuffers the maximum number of buffers (tiles) that may be in use at one + * time, tiles are not cached anywhere so set this to a large enough + * number to retain offscreen content until it is no longer needed + * @param sessionConsumer listener to receive the session once active + */ + void start(int maxBuffers, Consumer<Session> sessionConsumer); + + /** + * Close the connection. + */ + void close(); + } + + static class CaptureResult { + public final Image image; + /** + * The area requested, in content rect space, relative to scroll-bounds. + */ + public final Rect requested; + /** + * The actual area captured, in content rect space, relative to scroll-bounds. This may be + * cropped or empty depending on available content. + */ + public final Rect captured; + + // Error? + + private CaptureResult(Image image, Rect request, Rect captured) { + this.image = image; + this.requested = request; + this.captured = captured; + } + } + + /** + * Represents the connection to a target window and provides a mechanism for requesting tiles. + */ + interface Session { + /** + * Request the given horizontal strip. Values are y-coordinates in captured space, relative + * to start position. + * + * @param contentRect the area to capture, in content rect space, relative to scroll-bounds + * @param consumer listener to be informed of the result + */ + void requestTile(Rect contentRect, Consumer<CaptureResult> consumer); + + /** + * End the capture session, return the target app to original state. The returned + * stage must be waited for to complete to allow the target app a chance to restore to + * original state before becoming visible. + * + * @return a stage presenting the session shutdown + */ + void end(Runnable listener); + + int getMaxTileHeight(); + + int getMaxTileWidth(); + } + + private final IWindowManager mWindowManagerService; + private IBinder mHostWindowToken; + + @Inject + public ScrollCaptureClient(@UiContext Context context, IWindowManager windowManagerService) { + requireNonNull(context.getDisplay(), "context must be associated with a Display!"); + mWindowManagerService = windowManagerService; + } + + public void setHostWindowToken(IBinder token) { + mHostWindowToken = token; + } + + /** + * Check for scroll capture support. + * + * @param displayId id for the display containing the target window + * @param consumer receives a connection when available + */ + public void request(int displayId, Consumer<Connection> consumer) { + request(displayId, MATCH_ANY_TASK, consumer); + } + + /** + * Check for scroll capture support. + * + * @param displayId id for the display containing the target window + * @param taskId id for the task containing the target window or {@link #MATCH_ANY_TASK}. + * @param consumer receives a connection when available + */ + public void request(int displayId, int taskId, Consumer<Connection> consumer) { + try { + if (DEBUG_TRACE) { + Log.d(TAG, "requestScrollCapture(displayId=" + displayId + ", " + mHostWindowToken + + ", taskId=" + taskId + ", consumer=" + consumer + ")"); + } + mWindowManagerService.requestScrollCapture(displayId, mHostWindowToken, taskId, + new ControllerCallbacks(consumer)); + } catch (RemoteException e) { + Log.e(TAG, "Ignored remote exception", e); + } + } + + private static class ControllerCallbacks extends IScrollCaptureCallbacks.Stub implements + Connection, Session, IBinder.DeathRecipient { + + private IScrollCaptureConnection mConnection; + private Consumer<Connection> mConnectionConsumer; + private Consumer<Session> mSessionConsumer; + private Consumer<CaptureResult> mResultConsumer; + private Runnable mShutdownListener; + + private ImageReader mReader; + private Rect mScrollBounds; + private Rect mRequestRect; + private boolean mStarted; + + private ControllerCallbacks(Consumer<Connection> connectionConsumer) { + mConnectionConsumer = connectionConsumer; + } + + // IScrollCaptureCallbacks + + @Override + public void onConnected(IScrollCaptureConnection connection, Rect scrollBounds, + Point positionInWindow) throws RemoteException { + if (DEBUG_TRACE) { + Log.d(TAG, "onConnected(connection=" + connection + ", scrollBounds=" + scrollBounds + + ", positionInWindow=" + positionInWindow + ")"); + } + mConnection = connection; + mConnection.asBinder().linkToDeath(this, 0); + mScrollBounds = scrollBounds; + mConnectionConsumer.accept(this); + mConnectionConsumer = null; + } + + @Override + public void onUnavailable() throws RemoteException { + if (DEBUG_TRACE) { + Log.d(TAG, "onUnavailable"); + } + // The targeted app does not support scroll capture + // or the window could not be found... etc etc. + } + + @Override + public void onCaptureStarted() { + if (DEBUG_TRACE) { + Log.d(TAG, "onCaptureStarted()"); + } + mSessionConsumer.accept(this); + mSessionConsumer = null; + } + + @Override + public void onCaptureBufferSent(long frameNumber, Rect contentArea) { + Image image = null; + if (frameNumber != ScrollCaptureViewSupport.NO_FRAME_PRODUCED) { + image = mReader.acquireNextImage(); + } + if (DEBUG_TRACE) { + Log.d(TAG, "onCaptureBufferSent(frameNumber=" + frameNumber + + ", contentArea=" + contentArea + ") image=" + image); + } + // Save and clear first, since the consumer will likely request the next + // tile, otherwise the new consumer will be wiped out. + Consumer<CaptureResult> consumer = mResultConsumer; + mResultConsumer = null; + consumer.accept(new CaptureResult(image, mRequestRect, contentArea)); + } + + @Override + public void onConnectionClosed() { + if (DEBUG_TRACE) { + Log.d(TAG, "onConnectionClosed()"); + } + disconnect(); + if (mShutdownListener != null) { + mShutdownListener.run(); + mShutdownListener = null; + } + } + + // Misc + + private void disconnect() { + if (mConnection != null) { + mConnection.asBinder().unlinkToDeath(this, 0); + } + mConnection = null; + } + + // ScrollCaptureController.Connection + + // -> Error handling: BiConsumer<Session, Throwable> ? + @Override + public void start(int maxBufferCount, Consumer<Session> sessionConsumer) { + if (DEBUG_TRACE) { + Log.d(TAG, "start(maxBufferCount=" + maxBufferCount + + ", sessionConsumer=" + sessionConsumer + ")"); + } + mReader = ImageReader.newInstance(mScrollBounds.width(), mScrollBounds.height(), + PixelFormat.RGBA_8888, maxBufferCount, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE); + mSessionConsumer = sessionConsumer; + try { + mConnection.startCapture(mReader.getSurface()); + mStarted = true; + } catch (RemoteException e) { + Log.w(TAG, "should not be happening :-("); + // ? + //mSessionListener.onError(e); + //mSessionListener = null; + } + } + + @Override + public void close() { + end(null); + } + + // ScrollCaptureController.Session + + @Override + public void end(Runnable listener) { + if (DEBUG_TRACE) { + Log.d(TAG, "end(listener=" + listener + ")"); + } + if (mStarted) { + mShutdownListener = listener; + try { + // listener called from onConnectionClosed callback + mConnection.endCapture(); + } catch (RemoteException e) { + Log.d(TAG, "Ignored exception from endCapture()", e); + disconnect(); + listener.run(); + } + } else { + disconnect(); + listener.run(); + } + } + + @Override + public int getMaxTileHeight() { + return mScrollBounds.height(); + } + + @Override + public int getMaxTileWidth() { + return mScrollBounds.width(); + } + + @Override + public void requestTile(Rect contentRect, Consumer<CaptureResult> consumer) { + if (DEBUG_TRACE) { + Log.d(TAG, "requestTile(contentRect=" + contentRect + "consumer=" + consumer + ")"); + } + mRequestRect = new Rect(contentRect); + mResultConsumer = consumer; + try { + mConnection.requestImage(mRequestRect); + } catch (RemoteException e) { + Log.e(TAG, "Caught remote exception from requestImage", e); + // ? + } + } + + /** + * The process hosting the window went away abruptly! + */ + @Override + public void binderDied() { + if (DEBUG_TRACE) { + Log.d(TAG, "binderDied()"); + } + disconnect(); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java index 5ced40cb1b3b..800d67969f8b 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java @@ -16,46 +16,231 @@ package com.android.systemui.screenshot; -import android.os.IBinder; -import android.view.IWindowManager; +import static android.graphics.ColorSpace.Named.SRGB; -import javax.inject.Inject; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorSpace; +import android.graphics.Picture; +import android.graphics.Rect; +import android.media.ExifInterface; +import android.media.Image; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.os.UserHandle; +import android.provider.MediaStore; +import android.text.format.DateUtils; +import android.util.Log; +import android.widget.Toast; + +import com.android.systemui.screenshot.ScrollCaptureClient.Connection; +import com.android.systemui.screenshot.ScrollCaptureClient.Session; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.sql.Date; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Objects; +import java.util.UUID; +import java.util.function.Consumer; /** - * Stub + * Interaction controller between the UI and ScrollCaptureClient. */ public class ScrollCaptureController { + private static final String TAG = "ScrollCaptureController"; - public static final int STATUS_A = 0; - public static final int STATUS_B = 1; + public static final int MAX_PAGES = 5; + public static final int MAX_HEIGHT = 12000; - private final IWindowManager mWindowManagerService; - private StatusListener mListener; + private final Connection mConnection; + private final Context mContext; + private Picture mPicture; + + public ScrollCaptureController(Context context, Connection connection) { + mContext = context; + mConnection = connection; + } /** + * Run scroll capture! * - * @param windowManagerService + * @param after action to take after the flow is complete */ - @Inject - public ScrollCaptureController(IWindowManager windowManagerService) { - mWindowManagerService = windowManagerService; + public void run(final Runnable after) { + mConnection.start(MAX_PAGES, (session) -> startCapture(session, after)); } - interface StatusListener { - void onScrollCaptureStatus(boolean available); - } + private void startCapture(Session session, final Runnable after) { + Rect requestRect = new Rect(0, 0, + session.getMaxTileWidth(), session.getMaxTileHeight()); + Consumer<ScrollCaptureClient.CaptureResult> consumer = + new Consumer<ScrollCaptureClient.CaptureResult>() { + + int mFrameCount = 0; + + @Override + public void accept(ScrollCaptureClient.CaptureResult result) { + mFrameCount++; + boolean emptyFrame = result.captured.height() == 0; + if (!emptyFrame) { + mPicture = stackBelow(mPicture, result.image, result.captured.width(), + result.captured.height()); + } + if (emptyFrame || mFrameCount > MAX_PAGES + || requestRect.bottom > MAX_HEIGHT) { + Uri uri = null; + if (mPicture != null) { + // This is probably on a binder thread right now ¯\_(ツ)_/¯ + uri = writeImage(Bitmap.createBitmap(mPicture)); + // Release those buffers! + mPicture.close(); + } + if (uri != null) { + launchViewer(uri); + } else { + Toast.makeText(mContext, "Failed to create tall screenshot", + Toast.LENGTH_SHORT).show(); + } + session.end(after); // end session, close connection, after.run() + return; + } + requestRect.offset(0, session.getMaxTileHeight()); + session.requestTile(requestRect, /* consumer */ this); + } + }; + + // fire it up! + session.requestTile(requestRect, consumer); + }; + /** + * Combine the top {@link Picture} with an {@link Image} by appending the image directly + * below, creating a result that is the combined height of both. + * <p> + * Note: no pixel data is transferred here, only a record of drawing commands. Backing + * hardware buffers must not be modified/recycled until the picture is + * {@link Picture#close closed}. + * + * @param top the existing picture + * @param below the image to append below + * @param cropWidth the width of the pixel data to use from the image + * @param cropHeight the height of the pixel data to use from the image * - * @param window - * @param listener + * @return a new Picture which draws the previous picture with the image below it */ - public void getStatus(IBinder window, StatusListener listener) { - mListener = listener; -// try { -// mWindowManagerService.requestScrollCapture(window, new ClientCallbacks()); -// } catch (RemoteException e) { -// } + private static Picture stackBelow(Picture top, Image below, int cropWidth, int cropHeight) { + int width = cropWidth; + int height = cropHeight; + if (top != null) { + height += top.getHeight(); + width = Math.max(width, top.getWidth()); + } + Picture combined = new Picture(); + Canvas canvas = combined.beginRecording(width, height); + int y = 0; + if (top != null) { + canvas.drawPicture(top, new Rect(0, 0, top.getWidth(), top.getHeight())); + y += top.getHeight(); + } + canvas.drawBitmap(Bitmap.wrapHardwareBuffer( + below.getHardwareBuffer(), ColorSpace.get(SRGB)), 0, y, null); + combined.endRecording(); + return combined; } + Uri writeImage(Bitmap image) { + ContentResolver resolver = mContext.getContentResolver(); + long mImageTime = System.currentTimeMillis(); + String imageDate = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(mImageTime)); + String mImageFileName = String.format("tall_Screenshot_%s.png", imageDate); + String mScreenshotId = String.format("Screenshot_%s", UUID.randomUUID()); + try { + // Save the screenshot to the MediaStore + final ContentValues values = new ContentValues(); + values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + + File.separator + Environment.DIRECTORY_SCREENSHOTS); + values.put(MediaStore.MediaColumns.DISPLAY_NAME, mImageFileName); + values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png"); + values.put(MediaStore.MediaColumns.DATE_ADDED, mImageTime / 1000); + values.put(MediaStore.MediaColumns.DATE_MODIFIED, mImageTime / 1000); + values.put( + MediaStore.MediaColumns.DATE_EXPIRES, + (mImageTime + DateUtils.DAY_IN_MILLIS) / 1000); + values.put(MediaStore.MediaColumns.IS_PENDING, 1); + + final Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + values); + try { + try (OutputStream out = resolver.openOutputStream(uri)) { + if (!image.compress(Bitmap.CompressFormat.PNG, 100, out)) { + throw new IOException("Failed to compress"); + } + } + + // Next, write metadata to help index the screenshot + try (ParcelFileDescriptor pfd = resolver.openFile(uri, "rw", null)) { + final ExifInterface exif = new ExifInterface(pfd.getFileDescriptor()); + + exif.setAttribute(ExifInterface.TAG_SOFTWARE, + "Android " + Build.DISPLAY); + + exif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, + Integer.toString(image.getWidth())); + exif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, + Integer.toString(image.getHeight())); + + final ZonedDateTime time = ZonedDateTime.ofInstant( + Instant.ofEpochMilli(mImageTime), ZoneId.systemDefault()); + exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, + DateTimeFormatter.ofPattern("yyyy:MM:dd HH:mm:ss").format(time)); + exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, + DateTimeFormatter.ofPattern("SSS").format(time)); + + if (Objects.equals(time.getOffset(), ZoneOffset.UTC)) { + exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "+00:00"); + } else { + exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, + DateTimeFormatter.ofPattern("XXX").format(time)); + } + exif.saveAttributes(); + } + + // Everything went well above, publish it! + values.clear(); + values.put(MediaStore.MediaColumns.IS_PENDING, 0); + values.putNull(MediaStore.MediaColumns.DATE_EXPIRES); + resolver.update(uri, values, null, null); + return uri; + } catch (Exception e) { + resolver.delete(uri, null); + throw e; + } + } catch (Exception e) { + Log.e(TAG, "unable to save screenshot", e); + } + return null; + } + + void launchViewer(Uri uri) { + Intent editIntent = new Intent(Intent.ACTION_VIEW); + editIntent.setType("image/png"); + editIntent.setData(uri); + editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + editIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + mContext.startActivityAsUser(editIntent, UserHandle.CURRENT); + } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java b/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java index 217235b16ecf..f32529fdaf04 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java @@ -16,9 +16,9 @@ package com.android.systemui.screenshot; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ACTION_INTENT; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ACTION_TYPE; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ACTION_INTENT; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ACTION_TYPE; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ID; import android.app.ActivityOptions; import android.app.PendingIntent; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java index a043f0f1e50c..4e2283396e25 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java @@ -52,7 +52,7 @@ import javax.inject.Inject; public class TakeScreenshotService extends Service { private static final String TAG = "TakeScreenshotService"; - private final GlobalScreenshot mScreenshot; + private final ScreenshotController mScreenshot; private final UserManager mUserManager; private final UiEventLogger mUiEventLogger; @@ -61,7 +61,7 @@ public class TakeScreenshotService extends Service { @Override public void onReceive(Context context, Intent intent) { if (ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction()) && mScreenshot != null) { - mScreenshot.dismissScreenshot("close system dialogs", false); + mScreenshot.dismissScreenshot(false); } } }; @@ -125,7 +125,7 @@ public class TakeScreenshotService extends Service { }; @Inject - public TakeScreenshotService(GlobalScreenshot globalScreenshot, UserManager userManager, + public TakeScreenshotService(ScreenshotController globalScreenshot, UserManager userManager, UiEventLogger uiEventLogger) { mScreenshot = globalScreenshot; mUserManager = userManager; @@ -144,7 +144,9 @@ public class TakeScreenshotService extends Service { @Override public boolean onUnbind(Intent intent) { - if (mScreenshot != null) mScreenshot.stopScreenshot(); + if (mScreenshot != null && !mScreenshot.isDismissing()) { + mScreenshot.dismissScreenshot(true); + } unregisterReceiver(mBroadcastReceiver); return true; } diff --git a/packages/SystemUI/src/com/android/systemui/settings/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/BrightnessController.java index 1bea72aea2ba..72034f84fd30 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/BrightnessController.java +++ b/packages/SystemUI/src/com/android/systemui/settings/BrightnessController.java @@ -50,6 +50,8 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import java.util.ArrayList; +import javax.inject.Inject; + public class BrightnessController implements ToggleSlider.Listener { private static final String TAG = "StatusBar.BrightnessController"; private static final int SLIDER_ANIMATION_DURATION = 3000; @@ -475,4 +477,20 @@ public class BrightnessController implements ToggleSlider.Listener { mSliderAnimator.start(); } + /** Factory for creating a {@link BrightnessController}. */ + public static class Factory { + private final Context mContext; + private final BroadcastDispatcher mBroadcastDispatcher; + + @Inject + public Factory(Context context, BroadcastDispatcher broadcastDispatcher) { + mContext = context; + mBroadcastDispatcher = broadcastDispatcher; + } + + /** Create a {@link BrightnessController} */ + public BrightnessController create(ToggleSlider toggleSlider) { + return new BrightnessController(mContext, toggleSlider, mBroadcastDispatcher); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index dcee9fa9e648..5b3763e66307 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -284,7 +284,7 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< default void showAuthenticationDialog(PromptInfo promptInfo, IBiometricSysuiReceiver receiver, - @BiometricAuthenticator.Modality int biometricModality, + int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation, int userId, String opPackageName, long operationId) { } default void onBiometricAuthenticated() { } @@ -829,17 +829,18 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< @Override public void showAuthenticationDialog(PromptInfo promptInfo, IBiometricSysuiReceiver receiver, - @BiometricAuthenticator.Modality int biometricModality, boolean requireConfirmation, + int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation, int userId, String opPackageName, long operationId) { synchronized (mLock) { SomeArgs args = SomeArgs.obtain(); args.arg1 = promptInfo; args.arg2 = receiver; - args.argi1 = biometricModality; - args.arg3 = requireConfirmation; - args.argi2 = userId; - args.arg4 = opPackageName; - args.arg5 = operationId; + args.arg3 = sensorIds; // + args.arg4 = credentialAllowed; // + args.arg5 = requireConfirmation; + args.argi1 = userId; + args.arg6 = opPackageName; + args.arg7 = operationId; mHandler.obtainMessage(MSG_BIOMETRIC_SHOW, args) .sendToTarget(); } @@ -1264,11 +1265,12 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< mCallbacks.get(i).showAuthenticationDialog( (PromptInfo) someArgs.arg1, (IBiometricSysuiReceiver) someArgs.arg2, - someArgs.argi1 /* biometricModality */, - (boolean) someArgs.arg3 /* requireConfirmation */, - someArgs.argi2 /* userId */, - (String) someArgs.arg4 /* opPackageName */, - (long) someArgs.arg5 /* operationId */); + (int[]) someArgs.arg3 /* sensorIds */, + (boolean) someArgs.arg4 /* credentialAllowed */, + (boolean) someArgs.arg5 /* requireConfirmation */, + someArgs.argi1 /* userId */, + (String) someArgs.arg6 /* opPackageName */, + (long) someArgs.arg7 /* operationId */); } someArgs.recycle(); break; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index a59ff38896dc..a252a7a12274 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -121,6 +121,7 @@ public class KeyguardIndicationController implements StateListener, private int mChargingSpeed; private int mChargingWattage; private int mBatteryLevel; + private boolean mBatteryPresent = true; private long mChargingTimeRemaining; private float mDisclosureMaxAlpha; private String mMessageToShowOnScreenOn; @@ -389,86 +390,103 @@ public class KeyguardIndicationController implements StateListener, mWakeLock.setAcquired(false); } - if (mVisible) { - // Walk down a precedence-ordered list of what indication - // should be shown based on user or device state - if (mDozing) { - // When dozing we ignore any text color and use white instead, because - // colors can be hard to read in low brightness. - mTextView.setTextColor(Color.WHITE); - if (!TextUtils.isEmpty(mTransientIndication)) { - mTextView.switchIndication(mTransientIndication); - } else if (!TextUtils.isEmpty(mAlignmentIndication)) { - mTextView.switchIndication(mAlignmentIndication); - mTextView.setTextColor(mContext.getColor(R.color.misalignment_text_color)); - } else if (mPowerPluggedIn) { - String indication = computePowerIndication(); - if (animate) { - animateText(mTextView, indication); - } else { - mTextView.switchIndication(indication); - } - } else { - String percentage = NumberFormat.getPercentInstance() - .format(mBatteryLevel / 100f); - mTextView.switchIndication(percentage); - } - return; - } + if (!mVisible) { + return; + } - int userId = KeyguardUpdateMonitor.getCurrentUser(); - String trustGrantedIndication = getTrustGrantedIndication(); - String trustManagedIndication = getTrustManagedIndication(); + // A few places might need to hide the indication, so always start by making it visible + mIndicationArea.setVisibility(View.VISIBLE); - String powerIndication = null; - if (mPowerPluggedIn) { - powerIndication = computePowerIndication(); - } - - boolean isError = false; - if (!mKeyguardUpdateMonitor.isUserUnlocked(userId)) { - mTextView.switchIndication(com.android.internal.R.string.lockscreen_storage_locked); - } else if (!TextUtils.isEmpty(mTransientIndication)) { - if (powerIndication != null && !mTransientIndication.equals(powerIndication)) { - String indication = mContext.getResources().getString( - R.string.keyguard_indication_trust_unlocked_plugged_in, - mTransientIndication, powerIndication); - mTextView.switchIndication(indication); - } else { - mTextView.switchIndication(mTransientIndication); - } - isError = mTransientTextIsError; - } else if (!TextUtils.isEmpty(trustGrantedIndication) - && mKeyguardUpdateMonitor.getUserHasTrust(userId)) { - if (powerIndication != null) { - String indication = mContext.getResources().getString( - R.string.keyguard_indication_trust_unlocked_plugged_in, - trustGrantedIndication, powerIndication); - mTextView.switchIndication(indication); - } else { - mTextView.switchIndication(trustGrantedIndication); - } + // Walk down a precedence-ordered list of what indication + // should be shown based on user or device state + if (mDozing) { + // When dozing we ignore any text color and use white instead, because + // colors can be hard to read in low brightness. + mTextView.setTextColor(Color.WHITE); + if (!TextUtils.isEmpty(mTransientIndication)) { + mTextView.switchIndication(mTransientIndication); + } else if (!mBatteryPresent) { + // If there is no battery detected, hide the indication and bail + mIndicationArea.setVisibility(View.GONE); } else if (!TextUtils.isEmpty(mAlignmentIndication)) { mTextView.switchIndication(mAlignmentIndication); - isError = true; + mTextView.setTextColor(mContext.getColor(R.color.misalignment_text_color)); } else if (mPowerPluggedIn) { - if (DEBUG_CHARGING_SPEED) { - powerIndication += ", " + (mChargingWattage / 1000) + " mW"; - } + String indication = computePowerIndication(); if (animate) { - animateText(mTextView, powerIndication); + animateText(mTextView, indication); } else { - mTextView.switchIndication(powerIndication); + mTextView.switchIndication(indication); } - } else if (!TextUtils.isEmpty(trustManagedIndication) - && mKeyguardUpdateMonitor.getUserTrustIsManaged(userId) - && !mKeyguardUpdateMonitor.getUserHasTrust(userId)) { - mTextView.switchIndication(trustManagedIndication); } else { - mTextView.switchIndication(mRestingIndication); + String percentage = NumberFormat.getPercentInstance() + .format(mBatteryLevel / 100f); + mTextView.switchIndication(percentage); } - mTextView.setTextColor(isError ? Utils.getColorError(mContext) - : mInitialTextColorState); + return; + } + + int userId = KeyguardUpdateMonitor.getCurrentUser(); + String trustGrantedIndication = getTrustGrantedIndication(); + String trustManagedIndication = getTrustManagedIndication(); + + String powerIndication = null; + if (mPowerPluggedIn) { + powerIndication = computePowerIndication(); + } + + // Some cases here might need to hide the indication (if the battery is not present) + boolean hideIndication = false; + boolean isError = false; + if (!mKeyguardUpdateMonitor.isUserUnlocked(userId)) { + mTextView.switchIndication(com.android.internal.R.string.lockscreen_storage_locked); + } else if (!TextUtils.isEmpty(mTransientIndication)) { + if (powerIndication != null && !mTransientIndication.equals(powerIndication)) { + String indication = mContext.getResources().getString( + R.string.keyguard_indication_trust_unlocked_plugged_in, + mTransientIndication, powerIndication); + mTextView.switchIndication(indication); + hideIndication = !mBatteryPresent; + } else { + mTextView.switchIndication(mTransientIndication); + } + isError = mTransientTextIsError; + } else if (!TextUtils.isEmpty(trustGrantedIndication) + && mKeyguardUpdateMonitor.getUserHasTrust(userId)) { + if (powerIndication != null) { + String indication = mContext.getResources().getString( + R.string.keyguard_indication_trust_unlocked_plugged_in, + trustGrantedIndication, powerIndication); + mTextView.switchIndication(indication); + hideIndication = !mBatteryPresent; + } else { + mTextView.switchIndication(trustGrantedIndication); + } + } else if (!TextUtils.isEmpty(mAlignmentIndication)) { + mTextView.switchIndication(mAlignmentIndication); + isError = true; + hideIndication = !mBatteryPresent; + } else if (mPowerPluggedIn) { + if (DEBUG_CHARGING_SPEED) { + powerIndication += ", " + (mChargingWattage / 1000) + " mW"; + } + if (animate) { + animateText(mTextView, powerIndication); + } else { + mTextView.switchIndication(powerIndication); + } + hideIndication = !mBatteryPresent; + } else if (!TextUtils.isEmpty(trustManagedIndication) + && mKeyguardUpdateMonitor.getUserTrustIsManaged(userId) + && !mKeyguardUpdateMonitor.getUserHasTrust(userId)) { + mTextView.switchIndication(trustManagedIndication); + } else { + mTextView.switchIndication(mRestingIndication); + } + mTextView.setTextColor(isError ? Utils.getColorError(mContext) + : mInitialTextColorState); + if (hideIndication) { + mIndicationArea.setVisibility(View.GONE); } } @@ -647,6 +665,7 @@ public class KeyguardIndicationController implements StateListener, pw.println(" mMessageToShowOnScreenOn: " + mMessageToShowOnScreenOn); pw.println(" mDozing: " + mDozing); pw.println(" mBatteryLevel: " + mBatteryLevel); + pw.println(" mBatteryPresent: " + mBatteryPresent); pw.println(" mTextView.getText(): " + (mTextView == null ? null : mTextView.getText())); pw.println(" computePowerIndication(): " + computePowerIndication()); } @@ -685,6 +704,7 @@ public class KeyguardIndicationController implements StateListener, mChargingWattage = status.maxChargingWattage; mChargingSpeed = status.getChargingSpeed(mContext); mBatteryLevel = status.level; + mBatteryPresent = status.present; try { mChargingTimeRemaining = mPowerPluggedIn ? mBatteryInfo.computeChargeTimeRemaining() : -1; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/MediaTransferManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/MediaTransferManager.java deleted file mode 100644 index 1b1a51b8a57b..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/MediaTransferManager.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright (C) 2019 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.statusbar; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; -import android.graphics.drawable.RippleDrawable; -import android.service.notification.StatusBarNotification; -import android.util.FeatureFlagUtils; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewParent; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.android.settingslib.bluetooth.LocalBluetoothManager; -import com.android.settingslib.media.InfoMediaManager; -import com.android.settingslib.media.LocalMediaManager; -import com.android.settingslib.media.MediaDevice; -import com.android.settingslib.widget.AdaptiveIcon; -import com.android.systemui.Dependency; -import com.android.systemui.media.dialog.MediaOutputDialogFactory; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; - -import java.util.ArrayList; -import java.util.List; - -/** - * Class for handling MediaTransfer state over a set of notifications. - */ -public class MediaTransferManager { - private final Context mContext; - private final MediaOutputDialogFactory mMediaOutputDialogFactory; - private MediaDevice mDevice; - private List<View> mViews = new ArrayList<>(); - private LocalMediaManager mLocalMediaManager; - - private static final String TAG = "MediaTransferManager"; - - private final View.OnClickListener mOnClickHandler = new View.OnClickListener() { - @Override - public void onClick(View view) { - if (handleMediaTransfer(view)) { - return; - } - } - - private boolean handleMediaTransfer(View view) { - if (view.findViewById(com.android.internal.R.id.media_seamless) == null) { - return false; - } - - ViewParent parent = view.getParent(); - StatusBarNotification statusBarNotification = - getRowForParent(parent).getEntry().getSbn(); - mMediaOutputDialogFactory.create(statusBarNotification.getPackageName(), true); - return true; - } - }; - - private final LocalMediaManager.DeviceCallback mMediaDeviceCallback = - new LocalMediaManager.DeviceCallback() { - @Override - public void onDeviceListUpdate(List<MediaDevice> devices) { - MediaDevice currentDevice = mLocalMediaManager.getCurrentConnectedDevice(); - // Check because this can be called several times while changing devices - if (mDevice == null || !mDevice.equals(currentDevice)) { - mDevice = currentDevice; - updateAllChips(); - } - } - - @Override - public void onSelectedDeviceStateChanged(MediaDevice device, int state) { - if (mDevice == null || !mDevice.equals(device)) { - mDevice = device; - updateAllChips(); - } - } - }; - - public MediaTransferManager(Context context) { - mContext = context; - mMediaOutputDialogFactory = Dependency.get(MediaOutputDialogFactory.class); - LocalBluetoothManager lbm = Dependency.get(LocalBluetoothManager.class); - InfoMediaManager imm = new InfoMediaManager(mContext, null, null, lbm); - mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, null); - } - - /** - * Mark a view as removed. If no views remain the media device listener will be unregistered. - * @param root - */ - public void setRemoved(View root) { - if (!FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SEAMLESS_TRANSFER) - || mLocalMediaManager == null || root == null) { - return; - } - View view = root.findViewById(com.android.internal.R.id.media_seamless); - if (mViews.remove(view)) { - if (mViews.size() == 0) { - mLocalMediaManager.unregisterCallback(mMediaDeviceCallback); - } - } else { - Log.e(TAG, "Tried to remove unknown view " + view); - } - } - - private ExpandableNotificationRow getRowForParent(ViewParent parent) { - while (parent != null) { - if (parent instanceof ExpandableNotificationRow) { - return ((ExpandableNotificationRow) parent); - } - parent = parent.getParent(); - } - return null; - } - - /** - * apply the action button for MediaTransfer - * - * @param root The parent container of the view. - * @param entry The entry of MediaTransfer action button. - */ - public void applyMediaTransferView(ViewGroup root, NotificationEntry entry) { - if (!FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SEAMLESS_TRANSFER) - || mLocalMediaManager == null || root == null) { - return; - } - - View view = root.findViewById(com.android.internal.R.id.media_seamless); - if (view == null) { - return; - } - - view.setVisibility(View.VISIBLE); - view.setOnClickListener(mOnClickHandler); - if (!mViews.contains(view)) { - mViews.add(view); - if (mViews.size() == 1) { - mLocalMediaManager.registerCallback(mMediaDeviceCallback); - } - } - - // Initial update - mLocalMediaManager.startScan(); - mDevice = mLocalMediaManager.getCurrentConnectedDevice(); - updateChip(view); - } - - private void updateAllChips() { - for (View view : mViews) { - updateChip(view); - } - } - - private void updateChip(View view) { - ExpandableNotificationRow enr = getRowForParent(view.getParent()); - int fgColor = enr.getNotificationHeader().getOriginalIconColor(); - ColorStateList fgTintList = ColorStateList.valueOf(fgColor); - int bgColor = enr.getCurrentBackgroundTint(); - - // Update outline color - LinearLayout viewLayout = (LinearLayout) view; - RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground(); - GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0); - rect.setStroke(2, fgColor); - rect.setColor(bgColor); - - ImageView iconView = view.findViewById(com.android.internal.R.id.media_seamless_image); - TextView deviceName = view.findViewById(com.android.internal.R.id.media_seamless_text); - deviceName.setTextColor(fgTintList); - - if (mDevice != null) { - Drawable icon = mDevice.getIcon(); - iconView.setVisibility(View.VISIBLE); - iconView.setImageTintList(fgTintList); - - if (icon instanceof AdaptiveIcon) { - AdaptiveIcon aIcon = (AdaptiveIcon) icon; - aIcon.setBackgroundColor(bgColor); - iconView.setImageDrawable(aIcon); - } else { - iconView.setImageDrawable(icon); - } - deviceName.setText(mDevice.getName()); - } else { - // Reset to default - iconView.setVisibility(View.GONE); - deviceName.setText(com.android.internal.R.string.ext_media_seamless_action); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java index 670a65f55844..25c8e7feb9b2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java @@ -17,18 +17,16 @@ package com.android.systemui.statusbar; import android.app.Notification; -import android.content.res.Configuration; -import android.graphics.PorterDuff; import android.graphics.drawable.Icon; import android.text.TextUtils; -import android.view.NotificationHeaderView; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import com.android.internal.util.ContrastColorUtil; +import com.android.internal.widget.CachingIconView; import com.android.internal.widget.ConversationLayout; +import com.android.internal.widget.NotificationExpandButton; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationContentView; @@ -67,32 +65,14 @@ public class NotificationHeaderUtil { private final static ResultApplicator mGreyApplicator = new ResultApplicator() { @Override public void apply(View parent, View view, boolean apply, boolean reset) { - NotificationHeaderView header = (NotificationHeaderView) view; - ImageView icon = (ImageView) view.findViewById( - com.android.internal.R.id.icon); - ImageView expand = (ImageView) view.findViewById( - com.android.internal.R.id.expand_button); - applyToChild(icon, apply, header.getOriginalIconColor()); - applyToChild(expand, apply, header.getOriginalNotificationColor()); - } - - private void applyToChild(View view, boolean shouldApply, int originalColor) { - if (originalColor != NotificationHeaderView.NO_COLOR) { - ImageView imageView = (ImageView) view; - imageView.getDrawable().mutate(); - if (shouldApply) { - // lets gray it out - Configuration config = view.getContext().getResources().getConfiguration(); - boolean inNightMode = (config.uiMode & Configuration.UI_MODE_NIGHT_MASK) - == Configuration.UI_MODE_NIGHT_YES; - int grey = ContrastColorUtil.resolveColor(view.getContext(), - Notification.COLOR_DEFAULT, inNightMode); - imageView.getDrawable().setColorFilter(grey, PorterDuff.Mode.SRC_ATOP); - } else { - // lets reset it - imageView.getDrawable().setColorFilter(originalColor, - PorterDuff.Mode.SRC_ATOP); - } + CachingIconView icon = view.findViewById(com.android.internal.R.id.icon); + if (icon != null) { + icon.setGrayedOut(apply); + } + NotificationExpandButton expand = + view.findViewById(com.android.internal.R.id.expand_button); + if (expand != null) { + expand.setGrayedOut(apply); } } }; @@ -178,7 +158,7 @@ public class NotificationHeaderUtil { private void sanitizeHeaderViews(ExpandableNotificationRow row) { if (row.isSummaryWithChildren()) { - sanitizeHeader(row.getNotificationHeader()); + sanitizeHeader(row.getNotificationViewWrapper().getNotificationHeader()); return; } final NotificationContentView layout = row.getPrivateLayout(); @@ -190,7 +170,7 @@ public class NotificationHeaderUtil { private void sanitizeChild(View child) { if (child != null) { ViewGroup header = child.findViewById( - com.android.internal.R.id.notification_header); + com.android.internal.R.id.notification_top_line); sanitizeHeader(header); } } @@ -275,7 +255,8 @@ public class NotificationHeaderUtil { } public void init() { - mParentView = mParentRow.getNotificationHeader().findViewById(mId); + mParentView = mParentRow.getNotificationViewWrapper().getNotificationHeader() + .findViewById(mId); mParentData = mExtractor == null ? null : mExtractor.extractData(mParentRow); mApply = !mComparator.isEmpty(mParentView); } @@ -305,7 +286,7 @@ public class NotificationHeaderUtil { public void apply(ExpandableNotificationRow row, boolean reset) { boolean apply = mApply && !reset; if (row.isSummaryWithChildren()) { - applyToView(apply, reset, row.getNotificationHeader()); + applyToView(apply, reset, row.getNotificationViewWrapper().getNotificationHeader()); return; } applyToView(apply, reset, row.getPrivateLayout().getContractedChild()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index 8d82270c9ca7..3c4830272099 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -54,7 +54,6 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.statusbar.notification.NotificationEntryManager; -import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.policy.DeviceProvisionedController; @@ -345,8 +344,7 @@ public class NotificationLockscreenUserManagerImpl implements return false; } boolean exceedsPriorityThreshold; - if (NotificationUtils.useNewInterruptionModel(mContext) - && hideSilentNotificationsOnLockscreen()) { + if (hideSilentNotificationsOnLockscreen()) { exceedsPriorityThreshold = entry.getBucket() == BUCKET_MEDIA_CONTROLS || (entry.getBucket() != BUCKET_SILENT diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java index 53179ba4be90..9bd34ad1937f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java @@ -26,7 +26,6 @@ import android.view.View; import android.view.ViewGroup; import com.android.systemui.R; -import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.dagger.StatusBarModule; @@ -43,6 +42,7 @@ import com.android.systemui.statusbar.notification.stack.ForegroundServiceSectio import com.android.systemui.statusbar.notification.stack.NotificationListContainer; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.util.Assert; +import com.android.wm.shell.bubbles.Bubbles; import java.util.ArrayList; import java.util.HashMap; @@ -159,7 +159,8 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle for (int i = 0; i < N; i++) { NotificationEntry ent = activeNotifications.get(i); final boolean isBubbleNotificationSuppressedFromShade = mBubblesOptional.isPresent() - && mBubblesOptional.get().isBubbleNotificationSuppressedFromShade(ent); + && mBubblesOptional.get().isBubbleNotificationSuppressedFromShade( + ent.getKey(), ent.getSbn().getGroupKey()); if (ent.isRowDismissed() || ent.isRowRemoved() || isBubbleNotificationSuppressedFromShade || mFgsSectionController.hasEntry(ent)) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ViewTransformationHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/ViewTransformationHelper.java index 83e51cd43ed2..7ecdc812cbd3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ViewTransformationHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ViewTransformationHelper.java @@ -106,12 +106,8 @@ public class ViewTransformationHelper implements TransformableView, mViewTransformationAnimation.cancel(); } mViewTransformationAnimation = ValueAnimator.ofFloat(0.0f, 1.0f); - mViewTransformationAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - transformTo(notification, animation.getAnimatedFraction()); - } - }); + mViewTransformationAnimation.addUpdateListener( + animation -> transformTo(notification, animation.getAnimatedFraction())); mViewTransformationAnimation.setInterpolator(Interpolators.LINEAR); mViewTransformationAnimation.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); mViewTransformationAnimation.addListener(new AnimatorListenerAdapter() { @@ -167,12 +163,8 @@ public class ViewTransformationHelper implements TransformableView, mViewTransformationAnimation.cancel(); } mViewTransformationAnimation = ValueAnimator.ofFloat(0.0f, 1.0f); - mViewTransformationAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - transformFrom(notification, animation.getAnimatedFraction()); - } - }); + mViewTransformationAnimation.addUpdateListener( + animation -> transformFrom(notification, animation.getAnimatedFraction())); mViewTransformationAnimation.addListener(new AnimatorListenerAdapter() { public boolean mCancelled; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java index cee9c70f53eb..efd0519d6608 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java @@ -21,7 +21,6 @@ import android.content.Context; import android.os.Handler; import com.android.internal.statusbar.IStatusBarService; -import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.media.MediaDataManager; @@ -62,6 +61,7 @@ import com.android.systemui.statusbar.policy.RemoteInputUriController; import com.android.systemui.tracing.ProtoTracer; import com.android.systemui.util.DeviceConfigProxy; import com.android.systemui.util.concurrency.DelayableExecutor; +import com.android.wm.shell.bubbles.Bubbles; import java.util.Optional; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java index 382715a3fb77..45e8098e71d3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification; +import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_APP_START; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; @@ -32,6 +34,7 @@ import android.view.SyncRtSurfaceTransactionApplier; import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams; import android.view.View; +import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.systemui.Interpolators; import com.android.systemui.statusbar.NotificationShadeDepthController; @@ -226,10 +229,27 @@ public class ActivityLaunchAnimator { } }); anim.addListener(new AnimatorListenerAdapter() { + private boolean mWasCancelled; + + @Override + public void onAnimationStart(Animator animation) { + InteractionJankMonitor.getInstance().begin(CUJ_NOTIFICATION_APP_START); + } + + @Override + public void onAnimationCancel(Animator animation) { + mWasCancelled = true; + } + @Override public void onAnimationEnd(Animator animation) { setExpandAnimationRunning(false); invokeCallback(iRemoteAnimationFinishedCallback); + if (!mWasCancelled) { + InteractionJankMonitor.getInstance().end(CUJ_NOTIFICATION_APP_START); + } else { + InteractionJankMonitor.getInstance().cancel(CUJ_NOTIFICATION_APP_START); + } } }); anim.start(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java index eee9cc683e2b..967524ce308d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java @@ -16,9 +16,7 @@ package com.android.systemui.statusbar.notification; -import android.graphics.drawable.Drawable; import android.util.FloatProperty; -import android.util.Log; import android.util.Property; import android.view.View; @@ -34,9 +32,14 @@ public abstract class AnimatableProperty { public static final AnimatableProperty X = AnimatableProperty.from(View.X, R.id.x_animator_tag, R.id.x_animator_tag_start_value, R.id.x_animator_tag_end_value); + public static final AnimatableProperty Y = AnimatableProperty.from(View.Y, R.id.y_animator_tag, R.id.y_animator_tag_start_value, R.id.y_animator_tag_end_value); + public static final AnimatableProperty TRANSLATION_X = AnimatableProperty.from( + View.TRANSLATION_X, R.id.x_animator_tag, R.id.x_animator_tag_start_value, + R.id.x_animator_tag_end_value); + /** * Similar to X, however this doesn't allow for any other modifications other than from this * property. When using X, it's possible that the view is laid out during the animation, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt index ddfa18e65ee0..44b9bd26aa38 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt @@ -46,7 +46,7 @@ class ConversationNotificationProcessor @Inject constructor( Notification.MessagingStyle.CONVERSATION_TYPE_IMPORTANT else Notification.MessagingStyle.CONVERSATION_TYPE_NORMAL - entry.ranking.shortcutInfo?.let { shortcutInfo -> + entry.ranking.conversationShortcutInfo?.let { shortcutInfo -> messagingStyle.shortcutIcon = launcherApps.getShortcutIcon(shortcutInfo) shortcutInfo.label?.let { label -> messagingStyle.conversationTitle = label diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationChannelHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationChannelHelper.java index 5794f73a98f4..65e333f14df6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationChannelHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationChannelHelper.java @@ -47,7 +47,7 @@ public class NotificationChannelHelper { final String pkg = entry.getSbn().getPackageName(); final int appUid = entry.getSbn().getUid(); if (TextUtils.isEmpty(conversationId) || TextUtils.isEmpty(pkg) - || entry.getRanking().getShortcutInfo() == null) { + || entry.getRanking().getConversationShortcutInfo() == null) { return channel; } @@ -56,7 +56,7 @@ public class NotificationChannelHelper { try { channel.setName(getName(entry)); notificationManager.createConversationNotificationChannelForPackage( - pkg, appUid, entry.getSbn().getKey(), channel, + pkg, appUid, channel, conversationId); channel = notificationManager.getConversationNotificationChannel( context.getOpPackageName(), UserHandle.getUserId(appUid), pkg, @@ -68,8 +68,8 @@ public class NotificationChannelHelper { } private static CharSequence getName(NotificationEntry entry) { - if (entry.getRanking().getShortcutInfo().getLabel() != null) { - return entry.getRanking().getShortcutInfo().getLabel().toString(); + if (entry.getRanking().getConversationShortcutInfo().getLabel() != null) { + return entry.getRanking().getConversationShortcutInfo().getLabel().toString(); } Bundle extras = entry.getSbn().getNotification().extras; CharSequence nameString = extras.getCharSequence(Notification.EXTRA_CONVERSATION_TITLE); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java index 7d8979ca1129..aef01e9bd811 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java @@ -22,10 +22,10 @@ import android.util.Log; import android.view.View; import com.android.systemui.DejankUtils; -import com.android.systemui.bubbles.Bubbles; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.phone.StatusBar; +import com.android.wm.shell.bubbles.Bubbles; import java.util.Optional; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java index e1e77b0723a4..7c3b791aed09 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java @@ -618,6 +618,7 @@ public class NotificationEntryManager implements NotificationEntry entry = mPendingNotifications.get(key); if (entry != null) { entry.setSbn(notification); + entry.setRanking(ranking); } else { entry = new NotificationEntry( notification, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt index ce6013f776af..cd8897ea229d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt @@ -61,10 +61,8 @@ class NotificationSectionsFeatureManager @Inject constructor( isFilteringEnabled() && !isMediaControlsEnabled() -> intArrayOf(BUCKET_HEADS_UP, BUCKET_FOREGROUND_SERVICE, BUCKET_PEOPLE, BUCKET_ALERTING, BUCKET_SILENT) - NotificationUtils.useNewInterruptionModel(context) -> - intArrayOf(BUCKET_ALERTING, BUCKET_SILENT) else -> - intArrayOf(BUCKET_ALERTING) + intArrayOf(BUCKET_ALERTING, BUCKET_SILENT) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java index 1af47dd0f4c0..cbc113ba91bf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java @@ -16,12 +16,9 @@ package com.android.systemui.statusbar.notification; -import static android.provider.Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL; - import android.annotation.Nullable; import android.content.Context; import android.graphics.Color; -import android.provider.Settings; import android.view.View; import android.widget.ImageView; @@ -76,15 +73,4 @@ public class NotificationUtils { return (int) (dimensionPixelSize * factor); } - /** - * Returns the value of the new interruption model setting. This result is cached and cannot - * change except through reboots/process restarts. - */ - public static boolean useNewInterruptionModel(Context context) { - if (sUseNewInterruptionModel == null) { - sUseNewInterruptionModel = Settings.Secure.getInt(context.getContentResolver(), - NOTIFICATION_NEW_INTERRUPTION_MODEL, 1) != 0; - } - return sUseNewInterruptionModel; - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java index 0455b0f18afc..29a030f910a4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java @@ -16,8 +16,6 @@ package com.android.systemui.statusbar.notification.collection.coordinator; -import com.android.systemui.bubbles.BubbleController; -import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.statusbar.notification.collection.NotifCollection; import com.android.systemui.statusbar.notification.collection.NotifPipeline; @@ -25,6 +23,9 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor; +import com.android.systemui.wmshell.BubblesManager; +import com.android.wm.shell.bubbles.BubbleController; +import com.android.wm.shell.bubbles.Bubbles; import java.util.HashSet; import java.util.Optional; @@ -56,6 +57,7 @@ import javax.inject.Inject; public class BubbleCoordinator implements Coordinator { private static final String TAG = "BubbleCoordinator"; + private final Optional<BubblesManager> mBubblesManagerOptional; private final Optional<Bubbles> mBubblesOptional; private final NotifCollection mNotifCollection; private final Set<String> mInterceptedDismissalEntries = new HashSet<>(); @@ -64,8 +66,10 @@ public class BubbleCoordinator implements Coordinator { @Inject public BubbleCoordinator( + Optional<BubblesManager> bubblesManagerOptional, Optional<Bubbles> bubblesOptional, NotifCollection notifCollection) { + mBubblesManagerOptional = bubblesManagerOptional; mBubblesOptional = bubblesOptional; mNotifCollection = notifCollection; } @@ -75,8 +79,8 @@ public class BubbleCoordinator implements Coordinator { mNotifPipeline = pipeline; mNotifPipeline.addNotificationDismissInterceptor(mDismissInterceptor); mNotifPipeline.addFinalizeFilter(mNotifFilter); - if (mBubblesOptional.isPresent()) { - mBubblesOptional.get().addNotifCallback(mNotifCallback); + if (mBubblesManagerOptional.isPresent()) { + mBubblesManagerOptional.get().addNotifCallback(mNotifCallback); } } @@ -85,7 +89,8 @@ public class BubbleCoordinator implements Coordinator { @Override public boolean shouldFilterOut(NotificationEntry entry, long now) { return mBubblesOptional.isPresent() - && mBubblesOptional.get().isBubbleNotificationSuppressedFromShade(entry); + && mBubblesOptional.get().isBubbleNotificationSuppressedFromShade( + entry.getKey(), entry.getSbn().getGroupKey()); } }; @@ -102,9 +107,8 @@ public class BubbleCoordinator implements Coordinator { @Override public boolean shouldInterceptDismissal(NotificationEntry entry) { - // for experimental bubbles - if (mBubblesOptional.isPresent() - && mBubblesOptional.get().handleDismissalInterception(entry)) { + if (mBubblesManagerOptional.isPresent() + && mBubblesManagerOptional.get().handleDismissalInterception(entry)) { mInterceptedDismissalEntries.add(entry.getKey()); return true; } else { @@ -119,8 +123,7 @@ public class BubbleCoordinator implements Coordinator { } }; - private final BubbleController.NotifCallback mNotifCallback = - new BubbleController.NotifCallback() { + private final BubblesManager.NotifCallback mNotifCallback = new BubblesManager.NotifCallback() { @Override public void removeNotification( NotificationEntry entry, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java index 318cdb171fa5..b1c6f535ba87 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java @@ -37,7 +37,6 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.NotificationLockscreenUserManager; -import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.collection.GroupEntry; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; @@ -150,8 +149,7 @@ public class KeyguardCoordinator implements Coordinator { if (entry == null) { return false; } - if (NotificationUtils.useNewInterruptionModel(mContext) - && mHideSilentNotificationsOnLockscreen) { + if (mHideSilentNotificationsOnLockscreen) { return mHighPriorityProvider.isHighPriority(entry); } else { return entry.getRepresentativeEntry() != null diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java index 133ddfebe84f..6da4d8b70944 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java @@ -41,8 +41,6 @@ import javax.inject.Inject; */ @SysUISingleton public class RankingCoordinator implements Coordinator { - private static final String TAG = "RankingNotificationCoordinator"; - private final StatusBarStateController mStatusBarStateController; private final HighPriorityProvider mHighPriorityProvider; private final NodeController mSilentHeaderController; @@ -65,7 +63,7 @@ public class RankingCoordinator implements Coordinator { mStatusBarStateController.addCallback(mStatusBarStateCallback); pipeline.addPreGroupFilter(mSuspendedFilter); - pipeline.addPreGroupFilter(mDozingFilter); + pipeline.addPreGroupFilter(mDndVisualEffectsFilter); } public NotifSectioner getAlertingSectioner() { @@ -114,10 +112,10 @@ public class RankingCoordinator implements Coordinator { } }; - private final NotifFilter mDozingFilter = new NotifFilter("IsDozingFilter") { + private final NotifFilter mDndVisualEffectsFilter = new NotifFilter( + "DndSuppressingVisualEffects") { @Override public boolean shouldFilterOut(NotificationEntry entry, long now) { - // Dozing + DND Settings from Ranking object if (mStatusBarStateController.isDozing() && entry.shouldSuppressAmbient()) { return true; } @@ -130,7 +128,7 @@ public class RankingCoordinator implements Coordinator { new StatusBarStateController.StateListener() { @Override public void onDozingChanged(boolean isDozing) { - mDozingFilter.invalidateList(); + mDndVisualEffectsFilter.invalidateList(); } }; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/NotificationGroupManagerLegacy.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/NotificationGroupManagerLegacy.java index 490989dbb39e..3db544011700 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/NotificationGroupManagerLegacy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/NotificationGroupManagerLegacy.java @@ -22,7 +22,6 @@ import android.util.ArraySet; import android.util.Log; import com.android.systemui.Dumpable; -import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; @@ -34,6 +33,7 @@ import com.android.systemui.statusbar.notification.collection.render.GroupMember import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; +import com.android.wm.shell.bubbles.Bubbles; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -64,7 +64,7 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, new ArraySet<>(); private final ArraySet<OnGroupChangeListener> mGroupChangeListeners = new ArraySet<>(); private final Lazy<PeopleNotificationIdentifier> mPeopleNotificationIdentifier; - private final Optional<Lazy<Bubbles>> mBubblesOptional; + private final Optional<Bubbles> mBubblesOptional; private int mBarState = -1; private HashMap<String, StatusBarNotification> mIsolatedEntries = new HashMap<>(); private HeadsUpManager mHeadsUpManager; @@ -74,7 +74,7 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, public NotificationGroupManagerLegacy( StatusBarStateController statusBarStateController, Lazy<PeopleNotificationIdentifier> peopleNotificationIdentifier, - Optional<Lazy<Bubbles>> bubblesOptional) { + Optional<Bubbles> bubblesOptional) { statusBarStateController.addCallback(this); mPeopleNotificationIdentifier = peopleNotificationIdentifier; mBubblesOptional = bubblesOptional; @@ -242,8 +242,9 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, int childCount = 0; boolean hasBubbles = false; for (NotificationEntry entry : group.children.values()) { - if (mBubblesOptional.isPresent() && !mBubblesOptional.get().get() - .isBubbleNotificationSuppressedFromShade(entry)) { + if (mBubblesOptional.isPresent() && !mBubblesOptional.get() + .isBubbleNotificationSuppressedFromShade( + entry.getKey(), entry.getSbn().getGroupKey())) { childCount++; } else { hasBubbles = true; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index 4fff99b482d8..ff55cd60ab3b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -26,7 +26,6 @@ import android.view.accessibility.AccessibilityManager; import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.R; -import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; @@ -73,6 +72,7 @@ import com.android.systemui.statusbar.notification.row.PriorityOnboardingDialogC import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.util.leak.LeakDetector; +import com.android.systemui.wmshell.BubblesManager; import java.util.Optional; import java.util.concurrent.Executor; @@ -133,7 +133,7 @@ public interface NotificationsModule { UserContextProvider contextTracker, Provider<PriorityOnboardingDialogController.Builder> builderProvider, AssistantFeedbackController assistantFeedbackController, - Optional<Bubbles> bubblesOptional, + Optional<BubblesManager> bubblesManagerOptional, UiEventLogger uiEventLogger, OnUserInteractionCallback onUserInteractionCallback) { return new NotificationGutsManager( @@ -150,7 +150,7 @@ public interface NotificationsModule { contextTracker, builderProvider, assistantFeedbackController, - bubblesOptional, + bubblesManagerOptional, uiEventLogger, onUserInteractionCallback); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt index 13f7a53f5e54..ba45f9a687ed 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt @@ -255,7 +255,7 @@ class IconManager @Inject constructor( private fun createPeopleAvatar(entry: NotificationEntry): Icon? { var ic: Icon? = null - val shortcut = entry.ranking.shortcutInfo + val shortcut = entry.ranking.conversationShortcutInfo if (shortcut != null) { ic = launcherApps.getShortcutIcon(shortcut) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt index 049b471aa7cb..52b9b0606e81 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt @@ -17,13 +17,13 @@ package com.android.systemui.statusbar.notification.init import android.service.notification.StatusBarNotification -import com.android.systemui.bubbles.Bubbles import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption import com.android.systemui.statusbar.NotificationPresenter import com.android.systemui.statusbar.notification.NotificationActivityStarter import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl import com.android.systemui.statusbar.notification.stack.NotificationListContainer import com.android.systemui.statusbar.phone.StatusBar +import com.android.wm.shell.bubbles.Bubbles import java.io.FileDescriptor import java.io.PrintWriter import java.util.Optional diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt index 45a5d1044b9a..8f352ad55041 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.notification.init import android.service.notification.StatusBarNotification -import com.android.systemui.bubbles.Bubbles import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption import com.android.systemui.statusbar.FeatureFlags @@ -41,6 +40,7 @@ import com.android.systemui.statusbar.phone.StatusBar import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.RemoteInputUriController +import com.android.wm.shell.bubbles.Bubbles import dagger.Lazy import java.io.FileDescriptor import java.io.PrintWriter diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt index 7569c1bdbb73..d0e68bf75373 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.notification.init import android.service.notification.StatusBarNotification -import com.android.systemui.bubbles.Bubbles import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption import com.android.systemui.statusbar.NotificationListener import com.android.systemui.statusbar.NotificationPresenter @@ -25,6 +24,7 @@ import com.android.systemui.statusbar.notification.NotificationActivityStarter import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl import com.android.systemui.statusbar.notification.stack.NotificationListContainer import com.android.systemui.statusbar.phone.StatusBar +import com.android.wm.shell.bubbles.Bubbles import java.io.FileDescriptor import java.io.PrintWriter import java.util.Optional diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubNotificationListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubNotificationListener.kt index 99b2fcc9d610..cd9ba4e690e9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubNotificationListener.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubNotificationListener.kt @@ -220,7 +220,7 @@ class PeopleHubDataSourceImpl @Inject constructor( } val clickRunnable = Runnable { notificationListener.unsnoozeNotification(key) } val extras = sbn.notification.extras - val name = ranking.shortcutInfo?.label + val name = ranking.conversationShortcutInfo?.label ?: extras.getCharSequence(Notification.EXTRA_CONVERSATION_TITLE) ?: extras.getCharSequence(Notification.EXTRA_TITLE) ?: return null @@ -238,9 +238,9 @@ class PeopleHubDataSourceImpl @Inject constructor( iconFactory: ConversationIconFactory, sbn: StatusBarNotification ): Drawable? = - shortcutInfo?.let { shortcutInfo -> + conversationShortcutInfo?.let { conversationShortcutInfo -> iconFactory.getConversationDrawable( - shortcutInfo, + conversationShortcutInfo, sbn.packageName, sbn.uid, channel.isImportantConversation diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt index 0d92616767f3..691f1f452da8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt @@ -103,7 +103,7 @@ class PeopleNotificationIdentifierImpl @Inject constructor( private val Ranking.personTypeInfo get() = when { !isConversation -> TYPE_NON_PERSON - shortcutInfo == null -> TYPE_PERSON + conversationShortcutInfo == null -> TYPE_PERSON channel?.isImportantConversation == true -> TYPE_IMPORTANT_PERSON else -> TYPE_FULL_PERSON } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index 094e8661d262..10273cbbebad 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -33,6 +33,7 @@ import android.view.accessibility.AccessibilityManager; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; +import com.android.internal.jank.InteractionJankMonitor; import com.android.systemui.Gefingerpoken; import com.android.systemui.Interpolators; import com.android.systemui.R; @@ -750,12 +751,16 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView if (!mWasCancelled) { enableAppearDrawing(false); onAppearAnimationFinished(isAppearing); + InteractionJankMonitor.getInstance().end(getCujType(isAppearing)); + } else { + InteractionJankMonitor.getInstance().cancel(getCujType(isAppearing)); } } @Override public void onAnimationStart(Animator animation) { mWasCancelled = false; + InteractionJankMonitor.getInstance().begin(getCujType(isAppearing)); } @Override @@ -766,6 +771,18 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView mAppearAnimator.start(); } + private int getCujType(boolean isAppearing) { + if (mIsHeadsUpAnimation) { + return isAppearing + ? InteractionJankMonitor.CUJ_NOTIFICATION_HEADS_UP_APPEAR + : InteractionJankMonitor.CUJ_NOTIFICATION_HEADS_UP_DISAPPEAR; + } else { + return isAppearing + ? InteractionJankMonitor.CUJ_NOTIFICATION_ADD + : InteractionJankMonitor.CUJ_NOTIFICATION_REMOVE; + } + } + protected void onAppearAnimationFinished(boolean wasAppearing) { } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index d8d412bf2d41..a011d36af11d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -35,6 +35,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Color; import android.graphics.Path; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.AnimationDrawable; @@ -43,6 +44,7 @@ import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; +import android.provider.Settings; import android.service.notification.StatusBarNotification; import android.util.ArraySet; import android.util.AttributeSet; @@ -72,7 +74,6 @@ import com.android.internal.widget.CachingIconView; import com.android.systemui.Dependency; import com.android.systemui.Interpolators; import com.android.systemui.R; -import com.android.systemui.bubbles.Bubbles; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; @@ -102,12 +103,14 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.InflatedSmartReplies.SmartRepliesAndActions; +import com.android.systemui.wmshell.BubblesManager; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.BooleanSupplier; import java.util.function.Consumer; @@ -147,6 +150,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private LayoutListener mLayoutListener; private RowContentBindStage mRowContentBindStage; private PeopleNotificationIdentifier mPeopleNotificationIdentifier; + private Optional<BubblesManager> mBubblesManagerOptional; private int mIconTransformContentShift; private int mMaxHeadsUpHeightBeforeN; private int mMaxHeadsUpHeightBeforeP; @@ -304,7 +308,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } }; - private boolean mForceUnlocked; private boolean mKeepInParent; private boolean mRemoved; private static final Property<ExpandableNotificationRow, Float> TRANSLATE_CONTENT = @@ -326,6 +329,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private boolean mShelfIconVisible; private boolean mAboveShelf; private OnUserInteractionCallback mOnUserInteractionCallback; + private NotificationGutsManager mNotificationGutsManager; private boolean mIsLowPriority; private boolean mIsColorized; private boolean mUseIncreasedCollapsedHeight; @@ -410,8 +414,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView setIconAnimationRunning(running, l); } if (mIsSummaryWithChildren) { - setIconAnimationRunningForChild(running, mChildrenContainer.getHeaderView()); - setIconAnimationRunningForChild(running, mChildrenContainer.getLowPriorityHeaderView()); + NotificationViewWrapper viewWrapper = mChildrenContainer.getNotificationViewWrapper(); + if (viewWrapper != null) { + setIconAnimationRunningForChild(running, viewWrapper.getIcon()); + } + NotificationViewWrapper lowPriWrapper = mChildrenContainer.getLowPriorityViewWrapper(); + if (lowPriWrapper != null) { + setIconAnimationRunningForChild(running, lowPriWrapper.getIcon()); + } List<ExpandableNotificationRow> notificationChildren = mChildrenContainer.getAttachedChildren(); for (int i = 0; i < notificationChildren.size(); i++) { @@ -435,10 +445,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private void setIconAnimationRunningForChild(boolean running, View child) { if (child != null) { - ImageView icon = (ImageView) child.findViewById(com.android.internal.R.id.icon); + ImageView icon = child.findViewById(com.android.internal.R.id.icon); setIconRunning(icon, running); - ImageView rightIcon = (ImageView) child.findViewById( - com.android.internal.R.id.right_icon); + ImageView rightIcon = child.findViewById(com.android.internal.R.id.right_icon); setIconRunning(rightIcon, running); } } @@ -497,7 +506,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * Returns whether this row is considered non-blockable (i.e. it's a non-blockable system notif - * or is in a whitelist). + * or is in an allowList). */ public boolean getIsNonblockable() { // If the SystemNotifAsyncTask hasn't finished running or retrieved a value, we'll try once @@ -594,7 +603,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView public int getOriginalIconColor() { if (mIsSummaryWithChildren && !shouldShowPublic()) { - return mChildrenContainer.getVisibleHeader().getOriginalIconColor(); + return mChildrenContainer.getVisibleWrapper().getOriginalIconColor(); } int color = getShowingLayout().getOriginalIconColor(); if (color != Notification.COLOR_INVALID) { @@ -1040,22 +1049,25 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } - public NotificationHeaderView getNotificationHeader() { + /** + * @return the main notification view wrapper. + */ + public NotificationViewWrapper getNotificationViewWrapper() { if (mIsSummaryWithChildren) { - return mChildrenContainer.getHeaderView(); + return mChildrenContainer.getNotificationViewWrapper(); } - return mPrivateLayout.getNotificationHeader(); + return mPrivateLayout.getNotificationViewWrapper(); } /** - * @return the currently visible notification header. This can be different from - * {@link #getNotificationHeader()} in case it is a low-priority group. + * @return the currently visible notification view wrapper. This can be different from + * {@link #getNotificationViewWrapper()} in case it is a low-priority group. */ - public NotificationHeaderView getVisibleNotificationHeader() { + public NotificationViewWrapper getVisibleNotificationViewWrapper() { if (mIsSummaryWithChildren && !shouldShowPublic()) { - return mChildrenContainer.getVisibleHeader(); + return mChildrenContainer.getVisibleWrapper(); } - return getShowingLayout().getVisibleNotificationHeader(); + return getShowingLayout().getVisibleWrapper(); } public void setLongPressListener(LongPressListener longPressListener) { @@ -1071,13 +1083,19 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** The click listener for the bubble button. */ public View.OnClickListener getBubbleClickListener() { - return new View.OnClickListener() { - @Override - public void onClick(View v) { - Dependency.get(Bubbles.class) - .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */); - mHeadsUpManager.removeNotification(mEntry.getKey(), true /* releaseImmediately */); + return v -> { + if (mBubblesManagerOptional.isPresent()) { + mBubblesManagerOptional.get() + .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */); } + mHeadsUpManager.removeNotification(mEntry.getKey(), true /* releaseImmediately */); + }; + } + + /** The click listener for the snooze button. */ + public View.OnClickListener getSnoozeClickListener(MenuItem item) { + return v -> { + mNotificationGutsManager.openGuts(this, 0, 0, item); }; } @@ -1147,10 +1165,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView */ @Nullable public NotificationMenuRowPlugin createMenu() { - if (mMenuRow == null) { + final boolean removeShelf = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.SHOW_NEW_NOTIF_DISMISS, 0 /* show shelf by default */) == 1; + if (mMenuRow == null || removeShelf) { return null; } - if (mMenuRow.getMenuView() == null) { mMenuRow.createMenu(this, mEntry.getSbn()); mMenuRow.setAppName(mAppName); @@ -1294,16 +1313,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView onAttachedChildrenCountChanged(); } - public void setForceUnlocked(boolean forceUnlocked) { - mForceUnlocked = forceUnlocked; - if (mIsSummaryWithChildren) { - List<ExpandableNotificationRow> notificationChildren = getAttachedChildren(); - for (ExpandableNotificationRow child : notificationChildren) { - child.setForceUnlocked(forceUnlocked); - } - } - } - @Override public void dismiss(boolean refocusOnDismiss) { super.dismiss(refocusOnDismiss); @@ -1422,7 +1431,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override public View getShelfTransformationTarget() { if (mIsSummaryWithChildren && !shouldShowPublic()) { - return mChildrenContainer.getVisibleHeader().getIcon(); + return mChildrenContainer.getVisibleWrapper().getShelfTransformationTarget(); } return getShowingLayout().getShelfTransformationTarget(); } @@ -1556,7 +1565,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView FalsingManager falsingManager, StatusBarStateController statusBarStateController, PeopleNotificationIdentifier peopleNotificationIdentifier, - OnUserInteractionCallback onUserInteractionCallback) { + OnUserInteractionCallback onUserInteractionCallback, + Optional<BubblesManager> bubblesManagerOptional, + NotificationGutsManager gutsManager) { mEntry = entry; mAppName = appName; if (mMenuRow == null) { @@ -1584,6 +1595,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView l.setPeopleNotificationIdentifier(mPeopleNotificationIdentifier); } mOnUserInteractionCallback = onUserInteractionCallback; + mBubblesManagerOptional = bubblesManagerOptional; + mNotificationGutsManager = gutsManager; cacheIsSystemNotification(); } @@ -1646,19 +1659,15 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** Sets the last time the notification being displayed audibly alerted the user. */ public void setLastAudiblyAlertedMs(long lastAudiblyAlertedMs) { - if (NotificationUtils.useNewInterruptionModel(mContext)) { - long timeSinceAlertedAudibly = System.currentTimeMillis() - lastAudiblyAlertedMs; - boolean alertedRecently = - timeSinceAlertedAudibly < RECENTLY_ALERTED_THRESHOLD_MS; + long timeSinceAlertedAudibly = System.currentTimeMillis() - lastAudiblyAlertedMs; + boolean alertedRecently = timeSinceAlertedAudibly < RECENTLY_ALERTED_THRESHOLD_MS; - applyAudiblyAlertedRecently(alertedRecently); + applyAudiblyAlertedRecently(alertedRecently); - removeCallbacks(mExpireRecentlyAlertedFlag); - if (alertedRecently) { - long timeUntilNoLongerRecent = - RECENTLY_ALERTED_THRESHOLD_MS - timeSinceAlertedAudibly; - postDelayed(mExpireRecentlyAlertedFlag, timeUntilNoLongerRecent); - } + removeCallbacks(mExpireRecentlyAlertedFlag); + if (alertedRecently) { + long timeUntilNoLongerRecent = RECENTLY_ALERTED_THRESHOLD_MS - timeSinceAlertedAudibly; + postDelayed(mExpireRecentlyAlertedFlag, timeUntilNoLongerRecent); } } @@ -1693,37 +1702,30 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override protected void onFinishInflate() { super.onFinishInflate(); - mPublicLayout = (NotificationContentView) findViewById(R.id.expandedPublic); - mPrivateLayout = (NotificationContentView) findViewById(R.id.expanded); + mPublicLayout = findViewById(R.id.expandedPublic); + mPrivateLayout = findViewById(R.id.expanded); mLayouts = new NotificationContentView[] {mPrivateLayout, mPublicLayout}; for (NotificationContentView l : mLayouts) { l.setExpandClickListener(mExpandClickListener); l.setContainingNotification(this); } - mGutsStub = (ViewStub) findViewById(R.id.notification_guts_stub); - mGutsStub.setOnInflateListener(new ViewStub.OnInflateListener() { - @Override - public void onInflate(ViewStub stub, View inflated) { - mGuts = (NotificationGuts) inflated; - mGuts.setClipTopAmount(getClipTopAmount()); - mGuts.setActualHeight(getActualHeight()); - mGutsStub = null; - } + mGutsStub = findViewById(R.id.notification_guts_stub); + mGutsStub.setOnInflateListener((stub, inflated) -> { + mGuts = (NotificationGuts) inflated; + mGuts.setClipTopAmount(getClipTopAmount()); + mGuts.setActualHeight(getActualHeight()); + mGutsStub = null; }); - mChildrenContainerStub = (ViewStub) findViewById(R.id.child_container_stub); - mChildrenContainerStub.setOnInflateListener(new ViewStub.OnInflateListener() { + mChildrenContainerStub = findViewById(R.id.child_container_stub); + mChildrenContainerStub.setOnInflateListener((stub, inflated) -> { + mChildrenContainer = (NotificationChildrenContainer) inflated; + mChildrenContainer.setIsLowPriority(mIsLowPriority); + mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this); + mChildrenContainer.onNotificationUpdated(); - @Override - public void onInflate(ViewStub stub, View inflated) { - mChildrenContainer = (NotificationChildrenContainer) inflated; - mChildrenContainer.setIsLowPriority(mIsLowPriority); - mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this); - mChildrenContainer.onNotificationUpdated(); - - if (mShouldTranslateContents) { - mTranslateableViews.add(mChildrenContainer); - } + if (mShouldTranslateContents) { + mTranslateableViews.add(mChildrenContainer); } }); @@ -2183,7 +2185,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public boolean isUserLocked() { - return mUserLocked && !mForceUnlocked; + return mUserLocked; } public void setUserLocked(boolean userLocked) { @@ -2303,8 +2305,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private void onAttachedChildrenCountChanged() { mIsSummaryWithChildren = mChildrenContainer != null && mChildrenContainer.getNotificationChildCount() > 0; - if (mIsSummaryWithChildren && mChildrenContainer.getHeaderView() == null) { - mChildrenContainer.recreateNotificationHeader(mExpandClickListener, isConversation()); + if (mIsSummaryWithChildren) { + NotificationViewWrapper wrapper = mChildrenContainer.getNotificationViewWrapper(); + if (wrapper == null || wrapper.getNotificationHeader() == null) { + mChildrenContainer.recreateNotificationHeader(mExpandClickListener, + isConversation()); + } } getShowingLayout().updateBackgroundColor(false /* animate */); mPrivateLayout.updateExpandButtons(isExpandable()); @@ -2414,9 +2420,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView * the top. */ private void updateContentShiftHeight() { - NotificationHeaderView notificationHeader = getVisibleNotificationHeader(); - if (notificationHeader != null) { - CachingIconView icon = notificationHeader.getIcon(); + NotificationViewWrapper wrapper = getVisibleNotificationViewWrapper(); + CachingIconView icon = wrapper == null ? null : wrapper.getIcon(); + if (icon != null) { mIconTransformContentShift = getRelativeTopPadding(icon) + icon.getHeight(); } else { mIconTransformContentShift = mContentShift; @@ -2504,12 +2510,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView .alpha(0f) .setStartDelay(delay) .setDuration(duration) - .withEndAction(new Runnable() { - @Override - public void run() { - hiddenView.setVisibility(View.INVISIBLE); - } - }); + .withEndAction(() -> hiddenView.setVisibility(View.INVISIBLE)); } for (View showView : shownChildren) { showView.setVisibility(View.VISIBLE); @@ -2867,7 +2868,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } float x = event.getX(); float y = event.getY(); - NotificationHeaderView header = getVisibleNotificationHeader(); + NotificationViewWrapper wrapper = getVisibleNotificationViewWrapper(); + NotificationHeaderView header = wrapper == null ? null : wrapper.getNotificationHeader(); if (header != null && header.isInTouchRect(x - getTranslation(), y)) { return true; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index c995e324ecfe..cb2af54a25cb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -43,8 +43,10 @@ import com.android.systemui.statusbar.notification.stack.NotificationListContain import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.util.time.SystemClock; +import com.android.systemui.wmshell.BubblesManager; import java.util.List; +import java.util.Optional; import javax.inject.Inject; import javax.inject.Named; @@ -79,6 +81,7 @@ public class ExpandableNotificationRowController implements NodeController { private final FalsingManager mFalsingManager; private final boolean mAllowLongPress; private final PeopleNotificationIdentifier mPeopleNotificationIdentifier; + private final Optional<BubblesManager> mBubblesManagerOptional; @Inject public ExpandableNotificationRowController( @@ -102,7 +105,8 @@ public class ExpandableNotificationRowController implements NodeController { @Named(ALLOW_NOTIFICATION_LONG_PRESS_NAME) boolean allowLongPress, OnUserInteractionCallback onUserInteractionCallback, FalsingManager falsingManager, - PeopleNotificationIdentifier peopleNotificationIdentifier) { + PeopleNotificationIdentifier peopleNotificationIdentifier, + Optional<BubblesManager> bubblesManagerOptional) { mView = view; mListContainer = listContainer; mActivatableNotificationViewController = activatableNotificationViewController; @@ -125,6 +129,7 @@ public class ExpandableNotificationRowController implements NodeController { mAllowLongPress = allowLongPress; mFalsingManager = falsingManager; mPeopleNotificationIdentifier = peopleNotificationIdentifier; + mBubblesManagerOptional = bubblesManagerOptional; } /** @@ -148,8 +153,9 @@ public class ExpandableNotificationRowController implements NodeController { mFalsingManager, mStatusBarStateController, mPeopleNotificationIdentifier, - mOnUserInteractionCallback - + mOnUserInteractionCallback, + mBubblesManagerOptional, + mNotificationGutsManager ); mView.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); if (mAllowLongPress) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 8a644ed4d3ff..79c300782ad9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -18,12 +18,14 @@ package com.android.systemui.statusbar.notification.row; import static android.provider.Settings.Global.NOTIFICATION_BUBBLES; +import static android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Notification; import android.app.PendingIntent; import android.content.Context; +import android.content.res.Resources; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; @@ -31,6 +33,7 @@ import android.provider.Settings; import android.util.ArrayMap; import android.util.AttributeSet; import android.util.Log; +import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.NotificationHeaderView; import android.view.View; @@ -44,7 +47,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ContrastColorUtil; import com.android.systemui.Dependency; import com.android.systemui.R; -import com.android.systemui.statusbar.MediaTransferManager; +import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.TransformableView; @@ -173,12 +176,10 @@ public class NotificationContentView extends FrameLayout { private boolean mIsContentExpandable; private boolean mRemoteInputVisible; private int mUnrestrictedContentHeight; - private MediaTransferManager mMediaTransferManager; public NotificationContentView(Context context, AttributeSet attrs) { super(context, attrs); mHybridGroupManager = new HybridGroupManager(getContext()); - mMediaTransferManager = new MediaTransferManager(getContext()); mSmartReplyConstants = Dependency.get(SmartReplyConstants.class); mSmartReplyController = Dependency.get(SmartReplyController.class); initView(); @@ -339,7 +340,6 @@ public class NotificationContentView extends FrameLayout { ? contractedHeader.getPaddingLeft() : paddingEnd, contractedHeader.getPaddingBottom()); - contractedHeader.setShowWorkBadgeAtEnd(false); return true; } } @@ -462,7 +462,7 @@ public class NotificationContentView extends FrameLayout { mExpandedWrapper = NotificationViewWrapper.wrap(getContext(), child, mContainingNotification); if (mContainingNotification != null) { - applyBubbleAction(mExpandedChild, mContainingNotification.getEntry()); + applySystemActions(mExpandedChild, mContainingNotification.getEntry()); } } @@ -504,7 +504,7 @@ public class NotificationContentView extends FrameLayout { mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child, mContainingNotification); if (mContainingNotification != null) { - applyBubbleAction(mHeadsUpChild, mContainingNotification.getEntry()); + applySystemActions(mHeadsUpChild, mContainingNotification.getEntry()); } } @@ -1021,6 +1021,10 @@ public class NotificationContentView extends FrameLayout { mSingleLineView }; } + public NotificationViewWrapper getVisibleWrapper() { + return getVisibleWrapper(mVisibleType); + } + public NotificationViewWrapper getVisibleWrapper(int visibleType) { switch (visibleType) { case VISIBLE_TYPE_EXPANDED: @@ -1157,13 +1161,12 @@ public class NotificationContentView extends FrameLayout { mHeadsUpWrapper.onContentUpdated(row); } applyRemoteInputAndSmartReply(entry); - applyMediaTransfer(entry); updateLegacy(); mForceSelectNextLayout = true; mPreviousExpandedRemoteInputIntent = null; mPreviousHeadsUpRemoteInputIntent = null; - applyBubbleAction(mExpandedChild, entry); - applyBubbleAction(mHeadsUpChild, entry); + applySystemActions(mExpandedChild, entry); + applySystemActions(mHeadsUpChild, entry); } private void updateAllSingleLineViews() { @@ -1185,22 +1188,6 @@ public class NotificationContentView extends FrameLayout { } } - private void applyMediaTransfer(final NotificationEntry entry) { - if (!entry.isMediaNotification()) { - return; - } - - View bigContentView = mExpandedChild; - if (bigContentView != null && (bigContentView instanceof ViewGroup)) { - mMediaTransferManager.applyMediaTransferView((ViewGroup) bigContentView, entry); - } - - View smallContentView = mContractedChild; - if (smallContentView != null && (smallContentView instanceof ViewGroup)) { - mMediaTransferManager.applyMediaTransferView((ViewGroup) smallContentView, entry); - } - } - /** * Returns whether the {@link Notification} represented by entry has a free-form remote input. * Such an input can be used e.g. to implement smart reply buttons - by passing the replies @@ -1357,6 +1344,14 @@ public class NotificationContentView extends FrameLayout { NOTIFICATION_BUBBLES, 0) == 1; } + /** + * Setup icon buttons provided by System UI. + */ + private void applySystemActions(View layout, NotificationEntry entry) { + applySnoozeAction(layout); + applyBubbleAction(layout, entry); + } + private void applyBubbleAction(View layout, NotificationEntry entry) { if (layout == null || mContainingNotification == null || mPeopleIdentifier == null) { return; @@ -1376,8 +1371,8 @@ public class NotificationContentView extends FrameLayout { && entry.getBubbleMetadata() != null; if (showButton) { Drawable d = mContext.getResources().getDrawable(entry.isBubble() - ? R.drawable.ic_stop_bubble - : R.drawable.ic_create_bubble); + ? R.drawable.bubble_ic_stop_bubble + : R.drawable.bubble_ic_create_bubble); mContainingNotification.updateNotificationColor(); final int tint = mContainingNotification.getNotificationColor(); d.setTint(tint); @@ -1404,6 +1399,45 @@ public class NotificationContentView extends FrameLayout { } } + private void applySnoozeAction(View layout) { + if (layout == null || mContainingNotification == null) { + return; + } + ImageView snoozeButton = layout.findViewById(com.android.internal.R.id.snooze_button); + View actionContainer = layout.findViewById(com.android.internal.R.id.actions_container); + LinearLayout actionContainerLayout = + layout.findViewById(com.android.internal.R.id.actions_container_layout); + if (snoozeButton == null || actionContainer == null || actionContainerLayout == null) { + return; + } + final boolean showSnooze = Settings.Secure.getInt(mContext.getContentResolver(), + SHOW_NOTIFICATION_SNOOZE, 0) == 1; + if (!showSnooze) { + snoozeButton.setVisibility(GONE); + return; + } + + Resources res = mContext.getResources(); + Drawable snoozeDrawable = res.getDrawable(R.drawable.ic_snooze); + mContainingNotification.updateNotificationColor(); + snoozeDrawable.setTint(mContainingNotification.getNotificationColor()); + snoozeButton.setImageDrawable(snoozeDrawable); + + final NotificationSnooze snoozeGuts = (NotificationSnooze) LayoutInflater.from(mContext) + .inflate(R.layout.notification_snooze, null, false); + final String snoozeDescription = res.getString( + R.string.notification_menu_snooze_description); + final NotificationMenuRowPlugin.MenuItem snoozeMenuItem = + new NotificationMenuRow.NotificationMenuItem( + mContext, snoozeDescription, snoozeGuts, R.drawable.ic_snooze); + snoozeButton.setContentDescription( + mContext.getResources().getString(R.string.notification_menu_snooze_description)); + snoozeButton.setOnClickListener( + mContainingNotification.getSnoozeClickListener(snoozeMenuItem)); + snoozeButton.setVisibility(VISIBLE); + actionContainer.setVisibility(VISIBLE); + } + private void applySmartReplyView( SmartRepliesAndActions smartRepliesAndActions, NotificationEntry entry) { @@ -1561,18 +1595,20 @@ public class NotificationContentView extends FrameLayout { mIsContentExpandable = expandable; } - public NotificationHeaderView getNotificationHeader() { - NotificationHeaderView header = null; - if (mContractedChild != null) { - header = mContractedWrapper.getNotificationHeader(); + /** + * @return a view wrapper for one of the inflated states of the notification. + */ + public NotificationViewWrapper getNotificationViewWrapper() { + if (mContractedChild != null && mContractedWrapper != null) { + return mContractedWrapper; } - if (header == null && mExpandedChild != null) { - header = mExpandedWrapper.getNotificationHeader(); + if (mExpandedChild != null && mExpandedWrapper != null) { + return mExpandedWrapper; } - if (header == null && mHeadsUpChild != null) { - header = mHeadsUpWrapper.getNotificationHeader(); + if (mHeadsUpChild != null && mHeadsUpWrapper != null) { + return mHeadsUpWrapper; } - return header; + return null; } public void showFeedbackIcon(boolean show) { @@ -1600,11 +1636,6 @@ public class NotificationContentView extends FrameLayout { } } - public NotificationHeaderView getVisibleNotificationHeader() { - NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType); - return wrapper == null ? null : wrapper.getNotificationHeader(); - } - public void setContainingNotification(ExpandableNotificationRow containingNotification) { mContainingNotification = containingNotification; } @@ -1662,11 +1693,9 @@ public class NotificationContentView extends FrameLayout { } if (mExpandedWrapper != null) { mExpandedWrapper.setRemoved(); - mMediaTransferManager.setRemoved(mExpandedChild); } if (mContractedWrapper != null) { mContractedWrapper.setRemoved(); - mMediaTransferManager.setRemoved(mContractedChild); } if (mHeadsUpWrapper != null) { mHeadsUpWrapper.setRemoved(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java index 07a4a188bc48..e43130f1698b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java @@ -67,12 +67,12 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.notification.ConversationIconFactory; import com.android.systemui.Prefs; import com.android.systemui.R; -import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.statusbar.notification.NotificationChannelHelper; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; +import com.android.systemui.wmshell.BubblesManager; import java.lang.annotation.Retention; import java.util.Optional; @@ -94,7 +94,7 @@ public class NotificationConversationInfo extends LinearLayout implements private OnUserInteractionCallback mOnUserInteractionCallback; private Handler mMainHandler; private Handler mBgHandler; - private Optional<Bubbles> mBubblesOptional; + private Optional<BubblesManager> mBubblesManagerOptional; private String mPackageName; private String mAppName; private int mAppUid; @@ -223,7 +223,7 @@ public class NotificationConversationInfo extends LinearLayout implements @Main Handler mainHandler, @Background Handler bgHandler, OnConversationSettingsClickListener onConversationSettingsClickListener, - Optional<Bubbles> bubblesOptional) { + Optional<BubblesManager> bubblesManagerOptional) { mSelectedAction = -1; mINotificationManager = iNotificationManager; mOnUserInteractionCallback = onUserInteractionCallback; @@ -242,12 +242,12 @@ public class NotificationConversationInfo extends LinearLayout implements mIconFactory = conversationIconFactory; mUserContext = userContext; mBubbleMetadata = bubbleMetadata; - mBubblesOptional = bubblesOptional; + mBubblesManagerOptional = bubblesManagerOptional; mBuilderProvider = builderProvider; mMainHandler = mainHandler; mBgHandler = bgHandler; mShortcutManager = shortcutManager; - mShortcutInfo = entry.getRanking().getShortcutInfo(); + mShortcutInfo = entry.getRanking().getConversationShortcutInfo(); if (mShortcutInfo == null) { throw new IllegalArgumentException("Does not have required information"); } @@ -641,9 +641,9 @@ public class NotificationConversationInfo extends LinearLayout implements mINotificationManager.setBubblesAllowed(mAppPkg, mAppUid, BUBBLE_PREFERENCE_SELECTED); } - if (mBubblesOptional.isPresent()) { + if (mBubblesManagerOptional.isPresent()) { post(() -> { - mBubblesOptional.get().onUserChangedImportance(mEntry); + mBubblesManagerOptional.get().onUserChangedImportance(mEntry); }); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java index 373f20e6ba96..d2cfb2908e9c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java @@ -47,7 +47,6 @@ import com.android.settingslib.notification.ConversationIconFactory; import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.R; -import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; @@ -67,6 +66,7 @@ import com.android.systemui.statusbar.notification.row.NotificationInfo.CheckSav import com.android.systemui.statusbar.notification.stack.NotificationListContainer; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.policy.DeviceProvisionedController; +import com.android.systemui.wmshell.BubblesManager; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -117,7 +117,7 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx private final Lazy<StatusBar> mStatusBarLazy; private final Handler mMainHandler; private final Handler mBgHandler; - private final Optional<Bubbles> mBubblesOptional; + private final Optional<BubblesManager> mBubblesManagerOptional; private Runnable mOpenRunnable; private final INotificationManager mNotificationManager; private final LauncherApps mLauncherApps; @@ -142,7 +142,7 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx UserContextProvider contextTracker, Provider<PriorityOnboardingDialogController.Builder> builderProvider, AssistantFeedbackController assistantFeedbackController, - Optional<Bubbles> bubblesOptional, + Optional<BubblesManager> bubblesManagerOptional, UiEventLogger uiEventLogger, OnUserInteractionCallback onUserInteractionCallback) { mContext = context; @@ -158,7 +158,7 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx mBuilderProvider = builderProvider; mChannelEditorDialogController = channelEditorDialogController; mAssistantFeedbackController = assistantFeedbackController; - mBubblesOptional = bubblesOptional; + mBubblesManagerOptional = bubblesManagerOptional; mUiEventLogger = uiEventLogger; mOnUserInteractionCallback = onUserInteractionCallback; } @@ -491,7 +491,7 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx mMainHandler, mBgHandler, onConversationSettingsListener, - mBubblesOptional); + mBubblesManagerOptional); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java index 7a976ac82223..6ff5ed1bceae 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java @@ -236,6 +236,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G NotificationChannel.DEFAULT_CHANNEL_ID) && numTotalChannels == 1; } + mIsAutomaticChosen = getAlertingBehavior() == BEHAVIOR_AUTOMATIC; bindHeader(); bindChannelDetails(); @@ -658,13 +659,14 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G try { if (mChannelToUpdate != null) { if (mUnlockImportance) { - mChannelToUpdate.unlockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); + mINotificationManager.unlockNotificationChannel( + mPackageName, mAppUid, mChannelToUpdate.getId()); } else { mChannelToUpdate.setImportance(mNewImportance); mChannelToUpdate.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); + mINotificationManager.updateNotificationChannelForPackage( + mPackageName, mAppUid, mChannelToUpdate); } - mINotificationManager.updateNotificationChannelForPackage( - mPackageName, mAppUid, mChannelToUpdate); } else { // For notifications with more than one channel, update notification enabled // state. If the importance was lowered, we disable notifications. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigTextTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigTextTemplateViewWrapper.java index 41f93cceacc7..d58c183f27e3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigTextTemplateViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigTextTemplateViewWrapper.java @@ -37,7 +37,7 @@ public class NotificationBigTextTemplateViewWrapper extends NotificationTemplate } private void resolveViews(StatusBarNotification notification) { - mBigtext = (ImageFloatingTextView) mView.findViewById(com.android.internal.R.id.big_text); + mBigtext = mView.findViewById(com.android.internal.R.id.big_text); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java index 3f5867477f16..5aeacaba6f64 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java @@ -22,8 +22,10 @@ import android.app.Notification; import android.content.Context; import android.util.ArraySet; import android.view.NotificationHeaderView; +import android.view.NotificationTopLineView; import android.view.View; import android.view.ViewGroup; +import android.view.ViewGroup.MarginLayoutParams; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import android.widget.FrameLayout; @@ -34,19 +36,17 @@ import com.android.internal.widget.CachingIconView; import com.android.internal.widget.NotificationExpandButton; import com.android.settingslib.Utils; import com.android.systemui.Interpolators; -import com.android.systemui.R; import com.android.systemui.statusbar.TransformableView; import com.android.systemui.statusbar.ViewTransformationHelper; import com.android.systemui.statusbar.notification.CustomInterpolatorTransformation; import com.android.systemui.statusbar.notification.ImageTransformState; -import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.TransformState; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import java.util.Stack; /** - * Wraps a notification header view. + * Wraps a notification view which may or may not include a header. */ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { @@ -55,11 +55,10 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { protected final ViewTransformationHelper mTransformationHelper; - protected int mColor; - private CachingIconView mIcon; private NotificationExpandButton mExpandButton; protected NotificationHeaderView mNotificationHeader; + protected NotificationTopLineView mNotificationTopLine; private TextView mHeaderText; private TextView mAppNameText; private ImageView mWorkProfileImage; @@ -69,13 +68,9 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { private boolean mIsLowPriority; private boolean mTransformLowPriorityTitle; - private boolean mShowExpandButtonAtEnd; protected NotificationHeaderViewWrapper(Context ctx, View view, ExpandableNotificationRow row) { super(ctx, view, row); - mShowExpandButtonAtEnd = ctx.getResources().getBoolean( - R.bool.config_showNotificationExpandButtonAtEnd) - || NotificationUtils.useNewInterruptionModel(ctx); mTransformationHelper = new ViewTransformationHelper(); // we want to avoid that the header clashes with the other text when transforming @@ -115,18 +110,15 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { mExpandButton = mView.findViewById(com.android.internal.R.id.expand_button); mWorkProfileImage = mView.findViewById(com.android.internal.R.id.profile_badge); mNotificationHeader = mView.findViewById(com.android.internal.R.id.notification_header); + mNotificationTopLine = mView.findViewById(com.android.internal.R.id.notification_top_line); mAudiblyAlertedIcon = mView.findViewById(com.android.internal.R.id.alerted_icon); mFeedbackIcon = mView.findViewById(com.android.internal.R.id.feedback); - if (mNotificationHeader != null) { - mNotificationHeader.setShowExpandButtonAtEnd(mShowExpandButtonAtEnd); - mColor = mNotificationHeader.getOriginalIconColor(); - } } private void addFeedbackOnClickListener(ExpandableNotificationRow row) { View.OnClickListener listener = row.getFeedbackOnClickListener(); - if (mNotificationHeader != null) { - mNotificationHeader.setFeedbackOnClickListener(listener); + if (mNotificationTopLine != null) { + mNotificationTopLine.setFeedbackOnClickListener(listener); } if (mFeedbackIcon != null) { mFeedbackIcon.setOnClickListener(listener); @@ -170,13 +162,11 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { mAppNameText.setTextAppearance( com.android.internal.R.style .TextAppearance_DeviceDefault_Notification_Conversation_AppName); - ViewGroup.MarginLayoutParams layoutParams = - (ViewGroup.MarginLayoutParams) mAppNameText.getLayoutParams(); + MarginLayoutParams layoutParams = (MarginLayoutParams) mAppNameText.getLayoutParams(); layoutParams.setMarginStart(0); } if (mIconContainer != null) { - ViewGroup.MarginLayoutParams layoutParams = - (ViewGroup.MarginLayoutParams) mIconContainer.getLayoutParams(); + MarginLayoutParams layoutParams = (MarginLayoutParams) mIconContainer.getLayoutParams(); layoutParams.width = mIconContainer.getContext().getResources().getDimensionPixelSize( com.android.internal.R.dimen.conversation_content_start); @@ -186,8 +176,7 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { layoutParams.setMarginStart(marginStart * -1); } if (mIcon != null) { - ViewGroup.MarginLayoutParams layoutParams = - (ViewGroup.MarginLayoutParams) mIcon.getLayoutParams(); + MarginLayoutParams layoutParams = (MarginLayoutParams) mIcon.getLayoutParams(); layoutParams.setMarginEnd(0); } } @@ -199,21 +188,18 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { com.android.internal.R.attr.notificationHeaderTextAppearance, com.android.internal.R.style.TextAppearance_DeviceDefault_Notification_Info); mAppNameText.setTextAppearance(textAppearance); - ViewGroup.MarginLayoutParams layoutParams = - (ViewGroup.MarginLayoutParams) mAppNameText.getLayoutParams(); + MarginLayoutParams layoutParams = (MarginLayoutParams) mAppNameText.getLayoutParams(); final int marginStart = mAppNameText.getContext().getResources().getDimensionPixelSize( com.android.internal.R.dimen.notification_header_app_name_margin_start); layoutParams.setMarginStart(marginStart); } if (mIconContainer != null) { - ViewGroup.MarginLayoutParams layoutParams = - (ViewGroup.MarginLayoutParams) mIconContainer.getLayoutParams(); + MarginLayoutParams layoutParams = (MarginLayoutParams) mIconContainer.getLayoutParams(); layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; layoutParams.setMarginStart(0); } if (mIcon != null) { - ViewGroup.MarginLayoutParams layoutParams = - (ViewGroup.MarginLayoutParams) mIcon.getLayoutParams(); + MarginLayoutParams layoutParams = (MarginLayoutParams) mIcon.getLayoutParams(); final int marginEnd = mIcon.getContext().getResources().getDimensionPixelSize( com.android.internal.R.dimen.notification_header_icon_margin_end); layoutParams.setMarginEnd(marginEnd); @@ -273,12 +259,18 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { @Override public void updateExpandability(boolean expandable, View.OnClickListener onClickListener) { mExpandButton.setVisibility(expandable ? View.VISIBLE : View.GONE); + mExpandButton.setOnClickListener(expandable ? onClickListener : null); if (mNotificationHeader != null) { mNotificationHeader.setOnClickListener(expandable ? onClickListener : null); } } @Override + public void setExpanded(boolean expanded) { + mExpandButton.setExpanded(expanded); + } + + @Override public void setRecentlyAudiblyAlerted(boolean audiblyAlerted) { if (mAudiblyAlertedIcon != null) { mAudiblyAlertedIcon.setVisibility(audiblyAlerted ? View.VISIBLE : View.GONE); @@ -296,6 +288,11 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { } @Override + public CachingIconView getIcon() { + return mIcon; + } + + @Override public int getOriginalIconColor() { return mIcon.getOriginalIconColor(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java index 4a8e0d583433..159e05589ff7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java @@ -397,7 +397,7 @@ public class NotificationMediaTemplateViewWrapper extends NotificationTemplateVi return; } - int tintColor = getNotificationHeader().getOriginalIconColor(); + int tintColor = getOriginalIconColor(); mSeekBarElapsedTime.setTextColor(tintColor); mSeekBarTotalTime.setTextColor(tintColor); mSeekBarTotalTime.setShadowLayer(1.5f, 1.5f, 1.5f, mBackgroundColor); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java index 14aab9d62dfd..76ec59e0ec28 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java @@ -140,13 +140,13 @@ public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapp } private void resolveTemplateViews(StatusBarNotification notification) { - mPicture = (ImageView) mView.findViewById(com.android.internal.R.id.right_icon); + mPicture = mView.findViewById(com.android.internal.R.id.right_icon); if (mPicture != null) { mPicture.setTag(ImageTransformState.ICON_TAG, notification.getNotification().getLargeIcon()); } - mTitle = (TextView) mView.findViewById(com.android.internal.R.id.title); - mText = (TextView) mView.findViewById(com.android.internal.R.id.text); + mTitle = mView.findViewById(com.android.internal.R.id.title); + mText = mView.findViewById(com.android.internal.R.id.text); final View progress = mView.findViewById(com.android.internal.R.id.progress); if (progress instanceof ProgressBar) { mProgressBar = (ProgressBar) progress; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java index 42f5e389d5a8..6920e3f4a7c6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java @@ -29,7 +29,6 @@ import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; -import android.util.ArraySet; import android.view.NotificationHeaderView; import android.view.View; import android.view.ViewGroup; @@ -38,7 +37,7 @@ import android.widget.TextView; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; import com.android.internal.util.ContrastColorUtil; -import com.android.internal.widget.ConversationLayout; +import com.android.internal.widget.CachingIconView; import com.android.systemui.statusbar.CrossFadeHelper; import com.android.systemui.statusbar.TransformableView; import com.android.systemui.statusbar.notification.TransformState; @@ -67,8 +66,7 @@ public abstract class NotificationViewWrapper implements TransformableView { } else if ("messaging".equals(v.getTag())) { return new NotificationMessagingTemplateViewWrapper(ctx, v, row); } else if ("conversation".equals(v.getTag())) { - return new NotificationConversationTemplateViewWrapper(ctx, (ConversationLayout) v, - row); + return new NotificationConversationTemplateViewWrapper(ctx, v, row); } Class<? extends Notification.Style> style = row.getEntry().getSbn().getNotification().getNotificationStyle(); @@ -231,6 +229,9 @@ public abstract class NotificationViewWrapper implements TransformableView { */ public void updateExpandability(boolean expandable, View.OnClickListener onClickListener) {} + /** Set the expanded state on the view wrapper */ + public void setExpanded(boolean expanded) {} + /** * @return the notification header if it exists */ @@ -241,7 +242,16 @@ public abstract class NotificationViewWrapper implements TransformableView { /** * @return the expand button if it exists */ - public @Nullable View getExpandButton() { + @Nullable + public View getExpandButton() { + return null; + } + + /** + * @return the icon if it exists + */ + @Nullable + public CachingIconView getIcon() { return null; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index a396305a49b6..00bccfc1a323 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -31,6 +31,7 @@ import android.widget.RemoteViews; import android.widget.TextView; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.widget.CachingIconView; import com.android.systemui.R; import com.android.systemui.statusbar.CrossFadeHelper; import com.android.systemui.statusbar.NotificationHeaderUtil; @@ -318,9 +319,8 @@ public class NotificationChildrenContainer extends ViewGroup { RemoteViews header = builder.makeNotificationHeader(); if (mNotificationHeader == null) { mNotificationHeader = (NotificationHeaderView) header.apply(getContext(), this); - final View expandButton = mNotificationHeader.findViewById( - com.android.internal.R.id.expand_button); - expandButton.setVisibility(VISIBLE); + mNotificationHeader.findViewById(com.android.internal.R.id.expand_button) + .setVisibility(VISIBLE); mNotificationHeader.setOnClickListener(mHeaderClickListener); mNotificationHeaderWrapper = NotificationViewWrapper.wrap(getContext(), mNotificationHeader, mContainingNotification); @@ -361,9 +361,8 @@ public class NotificationChildrenContainer extends ViewGroup { if (mNotificationHeaderLowPriority == null) { mNotificationHeaderLowPriority = (NotificationHeaderView) header.apply(getContext(), this); - final View expandButton = mNotificationHeaderLowPriority.findViewById( - com.android.internal.R.id.expand_button); - expandButton.setVisibility(VISIBLE); + mNotificationHeaderLowPriority.findViewById(com.android.internal.R.id.expand_button) + .setVisibility(VISIBLE); mNotificationHeaderLowPriority.setOnClickListener(mHeaderClickListener); mNotificationHeaderWrapperLowPriority = NotificationViewWrapper.wrap(getContext(), mNotificationHeaderLowPriority, mContainingNotification); @@ -849,8 +848,8 @@ public class NotificationChildrenContainer extends ViewGroup { public void setChildrenExpanded(boolean childrenExpanded) { mChildrenExpanded = childrenExpanded; updateExpansionStates(); - if (mNotificationHeader != null) { - mNotificationHeader.setExpanded(childrenExpanded); + if (mNotificationHeaderWrapper != null) { + mNotificationHeaderWrapper.setExpanded(childrenExpanded); } final int count = mAttachedChildren.size(); for (int childIdx = 0; childIdx < count; childIdx++) { @@ -869,12 +868,12 @@ public class NotificationChildrenContainer extends ViewGroup { return mContainingNotification; } - public NotificationHeaderView getHeaderView() { - return mNotificationHeader; + public NotificationViewWrapper getNotificationViewWrapper() { + return mNotificationHeaderWrapper; } - public NotificationHeaderView getLowPriorityHeaderView() { - return mNotificationHeaderLowPriority; + public NotificationViewWrapper getLowPriorityViewWrapper() { + return mNotificationHeaderWrapperLowPriority; } @VisibleForTesting @@ -1224,16 +1223,15 @@ public class NotificationChildrenContainer extends ViewGroup { public void setShelfIconVisible(boolean iconVisible) { if (mNotificationHeaderWrapper != null) { - NotificationHeaderView header = mNotificationHeaderWrapper.getNotificationHeader(); - if (header != null) { - header.getIcon().setForceHidden(iconVisible); + CachingIconView icon = mNotificationHeaderWrapper.getIcon(); + if (icon != null) { + icon.setForceHidden(iconVisible); } } if (mNotificationHeaderWrapperLowPriority != null) { - NotificationHeaderView header - = mNotificationHeaderWrapperLowPriority.getNotificationHeader(); - if (header != null) { - header.getIcon().setForceHidden(iconVisible); + CachingIconView icon = mNotificationHeaderWrapperLowPriority.getIcon(); + if (icon != null) { + icon.setForceHidden(iconVisible); } } } @@ -1254,12 +1252,14 @@ public class NotificationChildrenContainer extends ViewGroup { } } - public NotificationHeaderView getVisibleHeader() { - NotificationHeaderView header = mNotificationHeader; + /** + * @return the view wrapper for the currently showing priority. + */ + public NotificationViewWrapper getVisibleWrapper() { if (showingAsLowPriority()) { - header = mNotificationHeaderLowPriority; + return mNotificationHeaderWrapperLowPriority; } - return header; + return mNotificationHeaderWrapper; } public void onExpansionChanged() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 93204995c5b0..2a2a0b14a2d2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -835,7 +835,13 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (!mShouldDrawNotificationBackground) { return; } - + final boolean clearUndershelf = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.SHOW_NEW_NOTIF_DISMISS, 0 /* show background by default */) == 1; + if (clearUndershelf) { + mBackgroundPaint.setColor(Color.TRANSPARENT); + invalidate(); + return; + } // Interpolate between semi-transparent notification panel background color // and white AOD separator. float colorInterpolation = MathUtils.smoothStep(0.4f /* start */, 1f /* end */, @@ -1340,8 +1346,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @ShadeViewRefactor(RefactorComponent.COORDINATOR) private float getAppearStartPosition() { if (isHeadsUpTransition()) { - return mHeadsUpInset - + getFirstVisibleSection().getFirstVisibleChild().getPinnedHeadsUpHeight(); + final NotificationSection firstVisibleSection = getFirstVisibleSection(); + final int pinnedHeight = firstVisibleSection != null + ? firstVisibleSection.getFirstVisibleChild().getPinnedHeadsUpHeight() + : 0; + return mHeadsUpInset + pinnedHeight; } return getMinExpansionHeight(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java index 76c5baf6e9f6..3622f1cd3952 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java @@ -36,6 +36,7 @@ import com.android.systemui.statusbar.policy.DataSaverController.Listener; import com.android.systemui.statusbar.policy.HotspotController; import com.android.systemui.statusbar.policy.HotspotController.Callback; import com.android.systemui.util.UserAwareController; +import com.android.systemui.util.settings.SecureSettings; import java.util.ArrayList; import java.util.Objects; @@ -60,6 +61,7 @@ public class AutoTileManager implements UserAwareController { protected final Context mContext; protected final QSTileHost mHost; protected final Handler mHandler; + protected final SecureSettings mSecureSettings; protected final AutoAddTracker mAutoTracker; private final HotspotController mHotspotController; private final DataSaverController mDataSaverController; @@ -71,6 +73,7 @@ public class AutoTileManager implements UserAwareController { public AutoTileManager(Context context, AutoAddTracker.Builder autoAddTrackerBuilder, QSTileHost host, @Background Handler handler, + SecureSettings secureSettings, HotspotController hotspotController, DataSaverController dataSaverController, ManagedProfileController managedProfileController, @@ -78,6 +81,7 @@ public class AutoTileManager implements UserAwareController { CastController castController) { mContext = context; mHost = host; + mSecureSettings = secureSettings; mCurrentUser = mHost.getUserContext().getUser(); mAutoTracker = autoAddTrackerBuilder.setUserId(mCurrentUser.getIdentifier()).build(); mHandler = handler; @@ -170,7 +174,7 @@ public class AutoTileManager implements UserAwareController { String spec = split[1]; // Populate all the settings. As they may not have been added in other users AutoAddSetting s = new AutoAddSetting( - mContext, mHandler, setting, mCurrentUser.getIdentifier(), spec); + mSecureSettings, mHandler, setting, mCurrentUser.getIdentifier(), spec); mAutoAddSettingList.add(s); } else { Log.w(TAG, "Malformed item in array: " + tile); @@ -321,13 +325,13 @@ public class AutoTileManager implements UserAwareController { private final String mSpec; AutoAddSetting( - Context context, + SecureSettings secureSettings, Handler handler, String setting, int userId, String tileSpec ) { - super(context, handler, setting, userId); + super(secureSettings, handler, setting, userId); mSpec = tileSpec; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java index af6ac223ada1..2767b7e1627d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java @@ -38,9 +38,9 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.keyguard.ViewMediatorCallback; import com.android.keyguard.dagger.KeyguardBouncerComponent; -import com.android.keyguard.dagger.RootView; import com.android.systemui.DejankUtils; import com.android.systemui.biometrics.AuthController; +import com.android.systemui.dagger.qualifiers.RootView; import com.android.systemui.keyguard.DismissCallbackRegistry; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.shared.system.SysUiStatsLog; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java index 1fdf631a858d..5c225e5a3529 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java @@ -86,9 +86,14 @@ public class KeyguardClockPositionAlgorithm { private int mMaxShadeBottom; /** - * Minimum distance from the status bar. + * Recommended distance from the status bar without the lock icon. */ - private int mContainerTopPadding; + private int mContainerTopPaddingWithoutLockIcon; + + /** + * Recommended distance from the status bar with the lock icon. + */ + private int mContainerTopPaddingWithLockIcon; /** * @see NotificationPanelViewController#getExpandedFraction() @@ -131,24 +136,31 @@ public class KeyguardClockPositionAlgorithm { public void loadDimens(Resources res) { mClockNotificationsMargin = res.getDimensionPixelSize( R.dimen.keyguard_clock_notifications_margin); + + mContainerTopPaddingWithoutLockIcon = + res.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) / 2; // Consider the lock icon when determining the minimum top padding between the status bar // and top of the clock. - mContainerTopPadding = Math.max(res.getDimensionPixelSize( - R.dimen.keyguard_clock_top_margin), - res.getDimensionPixelSize(R.dimen.keyguard_lock_height) - + res.getDimensionPixelSize(R.dimen.keyguard_lock_padding) - + res.getDimensionPixelSize(R.dimen.keyguard_clock_lock_margin)); + mContainerTopPaddingWithLockIcon = + Math.max(res.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin), + res.getDimensionPixelSize(R.dimen.keyguard_lock_height) + + res.getDimensionPixelSize(R.dimen.keyguard_lock_padding) + + res.getDimensionPixelSize(R.dimen.keyguard_clock_lock_margin)); mBurnInPreventionOffsetX = res.getDimensionPixelSize( R.dimen.burn_in_prevention_offset_x); mBurnInPreventionOffsetY = res.getDimensionPixelSize( R.dimen.burn_in_prevention_offset_y); } - public void setup(int minTopMargin, int maxShadeBottom, int notificationStackHeight, + /** + * Sets up algorithm values. + */ + public void setup(int statusBarMinHeight, int maxShadeBottom, int notificationStackHeight, float panelExpansion, int parentHeight, int keyguardStatusHeight, int clockPreferredY, boolean hasCustomClock, boolean hasVisibleNotifs, float dark, float emptyDragAmount, - boolean bypassEnabled, int unlockedStackScrollerPadding) { - mMinTopMargin = minTopMargin + mContainerTopPadding; + boolean bypassEnabled, int unlockedStackScrollerPadding, boolean udfpsEnrolled) { + mMinTopMargin = statusBarMinHeight + (udfpsEnrolled ? mContainerTopPaddingWithoutLockIcon : + mContainerTopPaddingWithLockIcon); mMaxShadeBottom = maxShadeBottom; mNotificationStackHeight = notificationStackHeight; mPanelExpansion = panelExpansion; @@ -175,8 +187,8 @@ public class KeyguardClockPositionAlgorithm { } /** - * Update lock screen mode for testing different layouts - */ + * Update lock screen mode for testing different layouts + */ public void onLockScreenModeChanged(int mode) { mLockScreenMode = mode; } @@ -241,6 +253,13 @@ public class KeyguardClockPositionAlgorithm { clockYDark = MathUtils.lerp(clockYBouncer, clockYDark, shadeExpansion); float darkAmount = mBypassEnabled && !mHasCustomClock ? 1.0f : mDarkAmount; + + // TODO(b/12836565) - prototyping only adjustment + if (mLockScreenMode != KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL) { + // This will keep the clock at the top for AOD + return (int) (clockY + burnInPreventionOffsetY() + mEmptyDragAmount); + } + return (int) (MathUtils.lerp(clockY, clockYDark, darkAmount) + mEmptyDragAmount); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenLockIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenLockIconController.java index 77ae059a0cb9..289ff71dcb46 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenLockIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenLockIconController.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.phone; +import static android.view.View.GONE; + import static com.android.systemui.statusbar.phone.LockIcon.STATE_BIOMETRICS_ERROR; import static com.android.systemui.statusbar.phone.LockIcon.STATE_LOCKED; import static com.android.systemui.statusbar.phone.LockIcon.STATE_LOCK_OPEN; @@ -502,6 +504,11 @@ public class LockscreenLockIconController { * @return true if the visibility changed */ private boolean updateIconVisibility() { + if (mKeyguardUpdateMonitor.isUdfpsEnrolled()) { + boolean changed = mLockIcon.getVisibility() == GONE; + mLockIcon.setVisibility(GONE); + return changed; + } boolean onAodOrDocked = mStatusBarStateController.isDozing() || mDocked; boolean invisible = onAodOrDocked || mWakeAndUnlockRunning || mShowingLaunchAffordance; boolean fingerprintOrBypass = mFingerprintUnlock diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java index d1c83555c062..ac91b7050ae9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java @@ -19,7 +19,6 @@ import com.android.internal.util.ContrastColorUtil; import com.android.settingslib.Utils; import com.android.systemui.Interpolators; import com.android.systemui.R; -import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.demomode.DemoMode; import com.android.systemui.demomode.DemoModeController; @@ -32,10 +31,14 @@ import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationShelfController; import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.notification.AnimatableProperty; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; +import com.android.systemui.statusbar.notification.PropertyAnimator; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.stack.AnimationProperties; +import com.android.wm.shell.bubbles.Bubbles; import java.util.ArrayList; import java.util.List; @@ -91,9 +94,7 @@ public class NotificationIconAreaController implements private boolean mAnimationsEnabled; private int mAodIconTint; - private boolean mFullyHidden; private boolean mAodIconsVisible; - private boolean mIsPulsing; private boolean mShowLowPriority = true; @VisibleForTesting @@ -158,22 +159,33 @@ public class NotificationIconAreaController implements } /** - * Called by the StatusBar. The StatusBar passes the NotificationIconContainer which holds - * the aod icons. + * Called by the Keyguard*ViewController whose view contains the aod icons. */ - void setupAodIcons(@NonNull NotificationIconContainer aodIcons) { + public void setupAodIcons(@NonNull NotificationIconContainer aodIcons, + int lockScreenMode) { boolean changed = mAodIcons != null; if (changed) { mAodIcons.setAnimationsEnabled(false); mAodIcons.removeAllViews(); } mAodIcons = aodIcons; - mAodIcons.setOnLockScreen(true); + mAodIcons.setOnLockScreen(true, lockScreenMode); updateAodIconsVisibility(false /* animate */); updateAnimations(); if (changed) { updateAodNotificationIcons(); } + updateIconLayoutParams(mContext); + } + + /** + * Update position of the view, with optional animation + */ + public void updatePosition(int x, AnimationProperties props, boolean animate) { + if (mAodIcons != null) { + PropertyAnimator.setProperty(mAodIcons, AnimatableProperty.TRANSLATION_X, x, props, + animate); + } } public void setupShelf(NotificationShelfController notificationShelfController) { @@ -182,23 +194,31 @@ public class NotificationIconAreaController implements } public void onDensityOrFontScaleChanged(Context context) { + updateIconLayoutParams(context); + } + + private void updateIconLayoutParams(Context context) { reloadDimens(context); final FrameLayout.LayoutParams params = generateIconLayoutParams(); for (int i = 0; i < mNotificationIcons.getChildCount(); i++) { View child = mNotificationIcons.getChildAt(i); child.setLayoutParams(params); } - for (int i = 0; i < mShelfIcons.getChildCount(); i++) { - View child = mShelfIcons.getChildAt(i); - child.setLayoutParams(params); - } for (int i = 0; i < mCenteredIcon.getChildCount(); i++) { View child = mCenteredIcon.getChildAt(i); child.setLayoutParams(params); } - for (int i = 0; i < mAodIcons.getChildCount(); i++) { - View child = mAodIcons.getChildAt(i); - child.setLayoutParams(params); + if (mShelfIcons != null) { + for (int i = 0; i < mShelfIcons.getChildCount(); i++) { + View child = mShelfIcons.getChildAt(i); + child.setLayoutParams(params); + } + } + if (mAodIcons != null) { + for (int i = 0; i < mAodIcons.getChildCount(); i++) { + View child = mAodIcons.getChildAt(i); + child.setLayoutParams(params); + } } } @@ -299,7 +319,8 @@ public class NotificationIconAreaController implements || !entry.isPulseSuppressed())) { return false; } - if (mBubblesOptional.isPresent() && mBubblesOptional.get().isBubbleExpanded(entry)) { + if (mBubblesOptional.isPresent() + && mBubblesOptional.get().isBubbleExpanded(entry.getKey())) { return false; } return true; @@ -358,6 +379,9 @@ public class NotificationIconAreaController implements } public void updateAodNotificationIcons() { + if (mAodIcons == null) { + return; + } updateIconsForLayout(entry -> entry.getIcons().getAodIcon(), mAodIcons, false /* showAmbient */, true /* showLowPriority */, @@ -546,6 +570,9 @@ public class NotificationIconAreaController implements @Override public void onDozingChanged(boolean isDozing) { + if (mAodIcons == null) { + return; + } boolean animate = mDozeParameters.getAlwaysOn() && !mDozeParameters.getDisplayNeedsBlanking(); mAodIcons.setDozing(isDozing, animate, 0); @@ -564,7 +591,9 @@ public class NotificationIconAreaController implements private void updateAnimations() { boolean inShade = mStatusBarStateController.getState() == StatusBarState.SHADE; - mAodIcons.setAnimationsEnabled(mAnimationsEnabled && !inShade); + if (mAodIcons != null) { + mAodIcons.setAnimationsEnabled(mAnimationsEnabled && !inShade); + } mCenteredIcon.setAnimationsEnabled(mAnimationsEnabled && inShade); mNotificationIcons.setAnimationsEnabled(mAnimationsEnabled && inShade); } @@ -575,6 +604,9 @@ public class NotificationIconAreaController implements } public void appearAodIcons() { + if (mAodIcons == null) { + return; + } if (mDozeParameters.shouldControlScreenOff()) { mAodIcons.setTranslationY(-mAodIconAppearTranslation); mAodIcons.setAlpha(0); @@ -637,6 +669,9 @@ public class NotificationIconAreaController implements } private void updateAodIconsVisibility(boolean animate) { + if (mAodIcons == null) { + return; + } boolean visible = mBypassController.getBypassEnabled() || mWakeUpCoordinator.getNotificationsFullyHidden(); if (mStatusBarStateController.getState() != StatusBarState.KEYGUARD) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java index bf858520c338..9561851ab28b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java @@ -34,6 +34,7 @@ import android.view.animation.Interpolator; import androidx.collection.ArrayMap; import com.android.internal.statusbar.StatusBarIcon; +import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.statusbar.AlphaOptimizedFrameLayout; @@ -148,6 +149,7 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout { private float mActualPaddingStart = NO_VALUE; private boolean mDozing; private boolean mOnLockScreen; + private int mLockScreenMode = KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL; private boolean mChangingViewPositions; private int mAddAnimationStartIndex = -1; private int mCannedAnimationStartIndex = -1; @@ -453,7 +455,8 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout { mFirstVisibleIconState = mIconStates.get(getChildAt(0)); } - boolean center = mOnLockScreen; + boolean center = mOnLockScreen + && mLockScreenMode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL; if (center && translationX < getLayoutEnd()) { float initialTranslation = mFirstVisibleIconState == null ? 0 : mFirstVisibleIconState.xTranslation; @@ -686,8 +689,13 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout { } } - public void setOnLockScreen(boolean onLockScreen) { + /** + * Set whether the device is on the lockscreen and which lockscreen mode the device is + * configured to. Depending on these values, the layout of the AOD icons change. + */ + public void setOnLockScreen(boolean onLockScreen, int lockScreenMode) { mOnLockScreen = onLockScreen; + mLockScreenMode = lockScreenMode; } public class IconState extends ViewState { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index 86d4ac1cb443..231d157322ed 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -70,12 +70,14 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.LatencyTracker; import com.android.keyguard.KeyguardClockSwitchController; import com.android.keyguard.KeyguardStatusView; +import com.android.keyguard.KeyguardStatusViewController; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.keyguard.dagger.KeyguardStatusViewComponent; import com.android.systemui.DejankUtils; import com.android.systemui.Interpolators; import com.android.systemui.R; +import com.android.systemui.biometrics.AuthController; import com.android.systemui.classifier.Classifier; import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.dagger.qualifiers.Main; @@ -202,9 +204,6 @@ public class NotificationPanelViewController extends PanelViewController { private static final Rect M_DUMMY_DIRTY_RECT = new Rect(0, 0, 1, 1); private static final Rect EMPTY_RECT = new Rect(); - private static final AnimationProperties - CLOCK_ANIMATION_PROPERTIES = - new AnimationProperties().setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); private final AnimatableProperty KEYGUARD_HEADS_UP_SHOWING_AMOUNT = AnimatableProperty.from( "KEYGUARD_HEADS_UP_SHOWING_AMOUNT", (notificationPanelView, aFloat) -> setKeyguardHeadsUpShowingAmount(aFloat), @@ -265,6 +264,7 @@ public class NotificationPanelViewController extends PanelViewController { private final KeyguardBypassController mKeyguardBypassController; private final KeyguardUpdateMonitor mUpdateMonitor; private final ConversationNotificationManager mConversationNotificationManager; + private final AuthController mAuthController; private final MediaHierarchyManager mMediaHierarchyManager; private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private final KeyguardStatusViewComponent.Factory mKeyguardStatusViewComponentFactory; @@ -280,7 +280,7 @@ public class NotificationPanelViewController extends PanelViewController { private ViewGroup mBigClockContainer; private QS mQs; private FrameLayout mQsFrame; - private KeyguardStatusView mKeyguardStatusView; + private KeyguardStatusViewController mKeyguardStatusViewController; private View mQsNavbarScrim; private NotificationsQuickSettingsContainer mNotificationContainerParent; private boolean mAnimateNextPositionUpdate; @@ -344,7 +344,6 @@ public class NotificationPanelViewController extends PanelViewController { private boolean mIsLaunchTransitionRunning; private Runnable mLaunchAnimationEndRunnable; private boolean mOnlyAffordanceInThisMotion; - private boolean mKeyguardStatusViewAnimating; private ValueAnimator mQsSizeChangeAnimator; private boolean mQsScrimEnabled = true; @@ -444,7 +443,6 @@ public class NotificationPanelViewController extends PanelViewController { private KeyguardIndicationController mKeyguardIndicationController; private Consumer<Boolean> mAffordanceLaunchListener; private int mShelfHeight; - private Runnable mOnReinflationListener; private int mDarkIconSize; private int mHeadsUpInset; private boolean mHeadsUpPinnedMode; @@ -520,7 +518,8 @@ public class NotificationPanelViewController extends PanelViewController { NotificationStackScrollLayoutController notificationStackScrollLayoutController, KeyguardStatusViewComponent.Factory keyguardStatusViewComponentFactory, NotificationGroupManagerLegacy groupManager, - NotificationIconAreaController notificationIconAreaController) { + NotificationIconAreaController notificationIconAreaController, + AuthController authController) { super(view, falsingManager, dozeLog, keyguardStateController, (SysuiStatusBarStateController) statusBarStateController, vibratorHelper, latencyTracker, flingAnimationUtilsBuilder, statusBarTouchableRegionManager); @@ -585,6 +584,7 @@ public class NotificationPanelViewController extends PanelViewController { mLockscreenUserManager = notificationLockscreenUserManager; mEntryManager = notificationEntryManager; mConversationNotificationManager = conversationNotificationManager; + mAuthController = authController; mView.setBackgroundColor(Color.TRANSPARENT); OnAttachStateChangeListener onAttachStateChangeListener = new OnAttachStateChangeListener(); @@ -606,16 +606,8 @@ public class NotificationPanelViewController extends PanelViewController { private void onFinishInflate() { loadDimens(); mKeyguardStatusBar = mView.findViewById(R.id.keyguard_header); - mKeyguardStatusView = mView.findViewById(R.id.keyguard_status_view); - - KeyguardClockSwitchController keyguardClockSwitchController = - mKeyguardStatusViewComponentFactory - .build(mKeyguardStatusView) - .getKeyguardClockSwitchController(); - keyguardClockSwitchController.init(); mBigClockContainer = mView.findViewById(R.id.big_clock_container); - keyguardClockSwitchController.setBigClockContainer(mBigClockContainer); - + updateViewControllers(mView.findViewById(R.id.keyguard_status_view)); mNotificationContainerParent = mView.findViewById(R.id.notification_container_parent); NotificationStackScrollLayout stackScrollLayout = mView.findViewById( R.id.notification_stack_scroller); @@ -689,11 +681,24 @@ public class NotificationPanelViewController extends PanelViewController { R.dimen.heads_up_status_bar_padding); } + private void updateViewControllers(KeyguardStatusView keyguardStatusView) { + // Re-associate the KeyguardStatusViewController + KeyguardStatusViewComponent statusViewComponent = + mKeyguardStatusViewComponentFactory.build(keyguardStatusView); + mKeyguardStatusViewController = statusViewComponent.getKeyguardStatusViewController(); + mKeyguardStatusViewController.init(); + + // Re-associate the clock container with the keyguard clock switch. + KeyguardClockSwitchController keyguardClockSwitchController = + statusViewComponent.getKeyguardClockSwitchController(); + keyguardClockSwitchController.setBigClockContainer(mBigClockContainer); + } + /** * Returns if there's a custom clock being presented. */ public boolean hasCustomClock() { - return mKeyguardStatusView.hasCustomClock(); + return mKeyguardStatusViewController.hasCustomClock(); } private void setStatusBar(StatusBar bar) { @@ -730,21 +735,16 @@ public class NotificationPanelViewController extends PanelViewController { private void reInflateViews() { // Re-inflate the status view group. - int index = mView.indexOfChild(mKeyguardStatusView); - mView.removeView(mKeyguardStatusView); - mKeyguardStatusView = (KeyguardStatusView) mInjectionInflationController.injectable( + KeyguardStatusView keyguardStatusView = mView.findViewById(R.id.keyguard_status_view); + int index = mView.indexOfChild(keyguardStatusView); + mView.removeView(keyguardStatusView); + keyguardStatusView = (KeyguardStatusView) mInjectionInflationController.injectable( LayoutInflater.from(mView.getContext())).inflate( R.layout.keyguard_status_view, mView, false); - mView.addView(mKeyguardStatusView, index); + mView.addView(keyguardStatusView, index); - // Re-associate the clock container with the keyguard clock switch. mBigClockContainer.removeAllViews(); - KeyguardClockSwitchController keyguardClockSwitchController = - mKeyguardStatusViewComponentFactory - .build(mKeyguardStatusView) - .getKeyguardClockSwitchController(); - keyguardClockSwitchController.init(); - keyguardClockSwitchController.setBigClockContainer(mBigClockContainer); + updateViewControllers(keyguardStatusView); // Update keyguard bottom area index = mView.indexOfChild(mKeyguardBottomArea); @@ -764,11 +764,12 @@ public class NotificationPanelViewController extends PanelViewController { mKeyguardStatusBar.onThemeChanged(); } - setKeyguardStatusViewVisibility(mBarState, false, false); + mKeyguardStatusViewController.setKeyguardStatusViewVisibility( + mBarState, + false, + false, + mBarState); setKeyguardBottomAreaVisibility(mBarState, false); - if (mOnReinflationListener != null) { - mOnReinflationListener.run(); - } } private void initBottomArea() { @@ -858,23 +859,24 @@ public class NotificationPanelViewController extends PanelViewController { } else { int totalHeight = mView.getHeight(); int bottomPadding = Math.max(mIndicationBottomPadding, mAmbientIndicationBottomPadding); - int clockPreferredY = mKeyguardStatusView.getClockPreferredY(totalHeight); + int clockPreferredY = mKeyguardStatusViewController.getClockPreferredY(totalHeight); boolean bypassEnabled = mKeyguardBypassController.getBypassEnabled(); final boolean hasVisibleNotifications = !bypassEnabled && mNotificationStackScrollLayoutController.getVisibleNotificationCount() != 0; - mKeyguardStatusView.setHasVisibleNotifications(hasVisibleNotifications); + mKeyguardStatusViewController.setHasVisibleNotifications(hasVisibleNotifications); mClockPositionAlgorithm.setup(mStatusBarMinHeight, totalHeight - bottomPadding, mNotificationStackScrollLayoutController.getIntrinsicContentHeight(), getExpandedFraction(), - totalHeight, (int) (mKeyguardStatusView.getHeight() - mShelfHeight / 2.0f - - mDarkIconSize / 2.0f), clockPreferredY, hasCustomClock(), + totalHeight, + (int) (mKeyguardStatusViewController.getHeight() + - mShelfHeight / 2.0f - mDarkIconSize / 2.0f), + clockPreferredY, hasCustomClock(), hasVisibleNotifications, mInterpolatedDarkAmount, mEmptyDragAmount, - bypassEnabled, getUnlockedStackScrollerPadding()); + bypassEnabled, getUnlockedStackScrollerPadding(), + mUpdateMonitor.isUdfpsEnrolled()); mClockPositionAlgorithm.run(mClockPositionResult); - PropertyAnimator.setProperty(mKeyguardStatusView, AnimatableProperty.X, - mClockPositionResult.clockX, CLOCK_ANIMATION_PROPERTIES, animateClock); - PropertyAnimator.setProperty(mKeyguardStatusView, AnimatableProperty.Y, - mClockPositionResult.clockY, CLOCK_ANIMATION_PROPERTIES, animateClock); + mKeyguardStatusViewController.updatePosition( + mClockPositionResult.clockX, mClockPositionResult.clockY, animateClock); updateNotificationTranslucency(); updateClock(); stackScrollerPadding = mClockPositionResult.stackScrollerPaddingExpanded; @@ -910,7 +912,14 @@ public class NotificationPanelViewController extends PanelViewController { float availableSpace = mNotificationStackScrollLayoutController.getHeight() - minPadding - shelfSize - Math.max(mIndicationBottomPadding, mAmbientIndicationBottomPadding) - - mKeyguardStatusView.getLogoutButtonHeight(); + - mKeyguardStatusViewController.getLogoutButtonHeight(); + + if (mUpdateMonitor.isUdfpsEnrolled()) { + availableSpace = mNotificationStackScrollLayoutController.getHeight() + - minPadding - shelfSize + - (mStatusBar.getDisplayHeight() - mAuthController.getUdfpsRegion().top); + } + int count = 0; ExpandableView previousView = null; for (int i = 0; i < mNotificationStackScrollLayoutController.getChildCount(); i++) { @@ -1005,9 +1014,7 @@ public class NotificationPanelViewController extends PanelViewController { } private void updateClock() { - if (!mKeyguardStatusViewAnimating) { - mKeyguardStatusView.setAlpha(mClockPositionResult.clockAlpha); - } + mKeyguardStatusViewController.setAlpha(mClockPositionResult.clockAlpha); } public void animateToFullShade(long delay) { @@ -1605,29 +1612,6 @@ public class NotificationPanelViewController extends PanelViewController { } } - private final Runnable mAnimateKeyguardStatusViewInvisibleEndRunnable = new Runnable() { - @Override - public void run() { - mKeyguardStatusViewAnimating = false; - mKeyguardStatusView.setVisibility(View.INVISIBLE); - } - }; - - private final Runnable mAnimateKeyguardStatusViewGoneEndRunnable = new Runnable() { - @Override - public void run() { - mKeyguardStatusViewAnimating = false; - mKeyguardStatusView.setVisibility(View.GONE); - } - }; - - private final Runnable mAnimateKeyguardStatusViewVisibleEndRunnable = new Runnable() { - @Override - public void run() { - mKeyguardStatusViewAnimating = false; - } - }; - private final Runnable mAnimateKeyguardStatusBarInvisibleEndRunnable = new Runnable() { @Override public void run() { @@ -1705,46 +1689,6 @@ public class NotificationPanelViewController extends PanelViewController { } } - private void setKeyguardStatusViewVisibility(int statusBarState, boolean keyguardFadingAway, - boolean goingToFullShade) { - mKeyguardStatusView.animate().cancel(); - mKeyguardStatusViewAnimating = false; - if ((!keyguardFadingAway && mBarState == KEYGUARD - && statusBarState != KEYGUARD) || goingToFullShade) { - mKeyguardStatusViewAnimating = true; - mKeyguardStatusView.animate().alpha(0f).setStartDelay(0).setDuration( - 160).setInterpolator(Interpolators.ALPHA_OUT).withEndAction( - mAnimateKeyguardStatusViewGoneEndRunnable); - if (keyguardFadingAway) { - mKeyguardStatusView.animate().setStartDelay( - mKeyguardStateController.getKeyguardFadingAwayDelay()).setDuration( - mKeyguardStateController.getShortenedFadingAwayDuration()).start(); - } - } else if (mBarState == StatusBarState.SHADE_LOCKED - && statusBarState == KEYGUARD) { - mKeyguardStatusView.setVisibility(View.VISIBLE); - mKeyguardStatusViewAnimating = true; - mKeyguardStatusView.setAlpha(0f); - mKeyguardStatusView.animate().alpha(1f).setStartDelay(0).setDuration( - 320).setInterpolator(Interpolators.ALPHA_IN).withEndAction( - mAnimateKeyguardStatusViewVisibleEndRunnable); - } else if (statusBarState == KEYGUARD) { - if (keyguardFadingAway) { - mKeyguardStatusViewAnimating = true; - mKeyguardStatusView.animate().alpha(0).translationYBy( - -getHeight() * 0.05f).setInterpolator( - Interpolators.FAST_OUT_LINEAR_IN).setDuration(125).setStartDelay( - 0).withEndAction(mAnimateKeyguardStatusViewInvisibleEndRunnable).start(); - } else { - mKeyguardStatusView.setVisibility(View.VISIBLE); - mKeyguardStatusView.setAlpha(1f); - } - } else { - mKeyguardStatusView.setVisibility(View.GONE); - mKeyguardStatusView.setAlpha(1f); - } - } - private void updateQsState() { mNotificationStackScrollLayoutController.setQsExpanded(mQsExpanded); mNotificationStackScrollLayoutController.setScrollingEnabled( @@ -2075,7 +2019,7 @@ public class NotificationPanelViewController extends PanelViewController { private int getMaxPanelHeightBypass() { int position = mClockPositionAlgorithm.getExpandedClockPosition() - + mKeyguardStatusView.getHeight(); + + mKeyguardStatusViewController.getHeight(); if (mNotificationStackScrollLayoutController.getVisibleNotificationCount() != 0) { position += mShelfHeight / 2.0f + mDarkIconSize / 2.0f; } @@ -2156,7 +2100,7 @@ public class NotificationPanelViewController extends PanelViewController { int minKeyguardPanelBottom = mClockPositionAlgorithm.getExpandedClockPosition() - + mKeyguardStatusView.getHeight() + + mKeyguardStatusViewController.getHeight() + mNotificationStackScrollLayoutController.getIntrinsicContentHeight(); return Math.max(maxHeight, minKeyguardPanelBottom); } else { @@ -2604,7 +2548,7 @@ public class NotificationPanelViewController extends PanelViewController { } public void onScreenTurningOn() { - mKeyguardStatusView.dozeTimeTick(); + mKeyguardStatusViewController.dozeTimeTick(); } @Override @@ -2989,7 +2933,6 @@ public class NotificationPanelViewController extends PanelViewController { mAnimateNextPositionUpdate = false; } mNotificationStackScrollLayoutController.setPulsing(pulsing, animatePulse); - mKeyguardStatusView.setPulsing(pulsing); } public void setAmbientIndicationBottomPadding(int ambientIndicationBottomPadding) { @@ -3001,14 +2944,14 @@ public class NotificationPanelViewController extends PanelViewController { public void dozeTimeTick() { mKeyguardBottomArea.dozeTimeTick(); - mKeyguardStatusView.dozeTimeTick(); + mKeyguardStatusViewController.dozeTimeTick(); if (mInterpolatedDarkAmount > 0) { positionClockAndNotifications(); } } public void setStatusAccessibilityImportance(int mode) { - mKeyguardStatusView.setImportantForAccessibility(mode); + mKeyguardStatusViewController.setStatusAccessibilityImportance(mode); } /** @@ -3068,8 +3011,11 @@ public class NotificationPanelViewController extends PanelViewController { * security view of the bouncer. */ public void onBouncerPreHideAnimation() { - setKeyguardStatusViewVisibility(mBarState, true /* keyguardFadingAway */, - false /* goingToFullShade */); + mKeyguardStatusViewController.setKeyguardStatusViewVisibility( + mBarState, + true /* keyguardFadingAway */, + false /* goingToFullShade */, + mBarState); } /** @@ -3164,10 +3110,6 @@ public class NotificationPanelViewController extends PanelViewController { mKeyguardIndicationController.showTransientIndication(id); } - public void setOnReinflationListener(Runnable onReinflationListener) { - mOnReinflationListener = onReinflationListener; - } - public void setAlpha(float alpha) { mView.setAlpha(alpha); } @@ -3639,7 +3581,11 @@ public class NotificationPanelViewController extends PanelViewController { int oldState = mBarState; boolean keyguardShowing = statusBarState == KEYGUARD; - setKeyguardStatusViewVisibility(statusBarState, keyguardFadingAway, goingToFullShade); + mKeyguardStatusViewController.setKeyguardStatusViewVisibility( + statusBarState, + keyguardFadingAway, + goingToFullShade, + mBarState); setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade); mBarState = statusBarState; @@ -3690,7 +3636,7 @@ public class NotificationPanelViewController extends PanelViewController { public void onDozeAmountChanged(float linearAmount, float amount) { mInterpolatedDarkAmount = amount; mLinearDarkAmount = linearAmount; - mKeyguardStatusView.setDarkAmount(mInterpolatedDarkAmount); + mKeyguardStatusViewController.setDarkAmount(mInterpolatedDarkAmount); mKeyguardBottomArea.setDarkAmount(mInterpolatedDarkAmount); positionClockAndNotifications(); } @@ -3736,9 +3682,10 @@ public class NotificationPanelViewController extends PanelViewController { setIsFullWidth(mNotificationStackScrollLayoutController.getWidth() == mView.getWidth()); // Update Clock Pivot - mKeyguardStatusView.setPivotX(mView.getWidth() / 2); - mKeyguardStatusView.setPivotY( - (FONT_HEIGHT - CAP_HEIGHT) / 2048f * mKeyguardStatusView.getClockTextSize()); + mKeyguardStatusViewController.setPivotX(mView.getWidth() / 2); + mKeyguardStatusViewController.setPivotY( + (FONT_HEIGHT - CAP_HEIGHT) / 2048f + * mKeyguardStatusViewController.getClockTextSize()); // Calculate quick setting heights. int oldMaxHeight = mQsMaxExpansionHeight; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java index 625489659bd4..38e1d0303a13 100755 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -52,6 +52,7 @@ import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.privacy.PrivacyItem; import com.android.systemui.privacy.PrivacyItemController; import com.android.systemui.privacy.PrivacyType; +import com.android.systemui.privacy.logging.PrivacyLogger; import com.android.systemui.qs.tiles.DndTile; import com.android.systemui.qs.tiles.RotationLockTile; import com.android.systemui.screenrecord.RecordingController; @@ -147,6 +148,7 @@ public class PhoneStatusBarPolicy private final SensorPrivacyController mSensorPrivacyController; private final RecordingController mRecordingController; private final RingerModeTracker mRingerModeTracker; + private final PrivacyLogger mPrivacyLogger; private boolean mZenVisible; private boolean mVolumeVisible; @@ -174,7 +176,8 @@ public class PhoneStatusBarPolicy @Nullable TelecomManager telecomManager, @DisplayId int displayId, @Main SharedPreferences sharedPreferences, DateFormatUtil dateFormatUtil, RingerModeTracker ringerModeTracker, - PrivacyItemController privacyItemController) { + PrivacyItemController privacyItemController, + PrivacyLogger privacyLogger) { mIconController = iconController; mCommandQueue = commandQueue; mBroadcastDispatcher = broadcastDispatcher; @@ -199,6 +202,7 @@ public class PhoneStatusBarPolicy mUiBgExecutor = uiBgExecutor; mTelecomManager = telecomManager; mRingerModeTracker = ringerModeTracker; + mPrivacyLogger = privacyLogger; mSlotCast = resources.getString(com.android.internal.R.string.status_bar_cast); mSlotHotspot = resources.getString(com.android.internal.R.string.status_bar_hotspot); @@ -673,14 +677,19 @@ public class PhoneStatusBarPolicy mIconController.setIconVisibility(mSlotCamera, showCamera); mIconController.setIconVisibility(mSlotMicrophone, showMicrophone); - if (mPrivacyItemController.getAllIndicatorsAvailable()) { + if (mPrivacyItemController.getAllIndicatorsAvailable() + || mPrivacyItemController.getLocationAvailable()) { mIconController.setIconVisibility(mSlotLocation, showLocation); } + mPrivacyLogger.logStatusBarIconsVisible(showCamera, showMicrophone, showLocation); } @Override public void onLocationActiveChanged(boolean active) { - if (!mPrivacyItemController.getAllIndicatorsAvailable()) updateLocationFromController(); + if (!mPrivacyItemController.getAllIndicatorsAvailable() + && !mPrivacyItemController.getLocationAvailable()) { + updateLocationFromController(); + } } // Updates the status view based on the current state of location requests. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java index af2f3e55c9ce..a930a897c2dc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java @@ -22,13 +22,13 @@ import android.view.ViewTreeObserver; import android.view.WindowManager; import com.android.systemui.assist.AssistManager; -import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.StatusBarState; +import com.android.wm.shell.bubbles.Bubbles; import java.util.ArrayList; import java.util.Optional; @@ -51,7 +51,7 @@ public class ShadeControllerImpl implements ShadeController { private final int mDisplayId; protected final Lazy<StatusBar> mStatusBarLazy; private final Lazy<AssistManager> mAssistManagerLazy; - private final Optional<Lazy<Bubbles>> mBubblesOptional; + private final Optional<Bubbles> mBubblesOptional; private final ArrayList<Runnable> mPostCollapseRunnables = new ArrayList<>(); @@ -64,7 +64,7 @@ public class ShadeControllerImpl implements ShadeController { WindowManager windowManager, Lazy<StatusBar> statusBarLazy, Lazy<AssistManager> assistManagerLazy, - Optional<Lazy<Bubbles>> bubblesOptional + Optional<Bubbles> bubblesOptional ) { mCommandQueue = commandQueue; mStatusBarStateController = statusBarStateController; @@ -135,7 +135,7 @@ public class ShadeControllerImpl implements ShadeController { getStatusBar().getNotificationShadeWindowViewController().cancelExpandHelper(); getStatusBarView().collapsePanel(true /* animate */, delayed, speedUpFactor); } else if (mBubblesOptional.isPresent()) { - mBubblesOptional.get().get().collapseStack(); + mBubblesOptional.get().collapseStack(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index 28a22cd6a194..6012ae3f2da6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -144,8 +144,6 @@ import com.android.systemui.R; import com.android.systemui.SystemUI; import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.bubbles.BubbleController; -import com.android.systemui.bubbles.Bubbles; import com.android.systemui.charging.WirelessChargingAnimation; import com.android.systemui.classifier.FalsingLog; import com.android.systemui.colorextraction.SysuiColorExtractor; @@ -172,7 +170,7 @@ import com.android.systemui.plugins.qs.QS; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSFragment; -import com.android.systemui.qs.QSPanel; +import com.android.systemui.qs.QSPanelController; import com.android.systemui.recents.ScreenPinningRequest; import com.android.systemui.shared.plugins.PluginManager; import com.android.systemui.shared.system.WindowManagerWrapper; @@ -229,6 +227,8 @@ import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler; import com.android.systemui.statusbar.policy.UserInfoControllerImpl; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.volume.VolumeComponent; +import com.android.systemui.wmshell.BubblesManager; +import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.splitscreen.SplitScreen; import java.io.FileDescriptor; @@ -408,7 +408,7 @@ public class StatusBar extends SystemUI implements DemoMode, protected NotificationPanelViewController mNotificationPanelViewController; // settings - private QSPanel mQSPanel; + private QSPanelController mQSPanelController; KeyguardIndicationController mKeyguardIndicationController; @@ -648,8 +648,9 @@ public class StatusBar extends SystemUI implements DemoMode, protected StatusBarNotificationPresenter mPresenter; private NotificationActivityStarter mNotificationActivityStarter; private Lazy<NotificationShadeDepthController> mNotificationShadeDepthControllerLazy; + private final Optional<BubblesManager> mBubblesManagerOptional; private final Optional<Bubbles> mBubblesOptional; - private final BubbleController.BubbleExpandListener mBubbleExpandListener; + private final Bubbles.BubbleExpandListener mBubbleExpandListener; private ActivityIntentHelper mActivityIntentHelper; private NotificationStackScrollLayoutController mStackScrollerController; @@ -697,6 +698,7 @@ public class StatusBar extends SystemUI implements DemoMode, WakefulnessLifecycle wakefulnessLifecycle, SysuiStatusBarStateController statusBarStateController, VibratorHelper vibratorHelper, + Optional<BubblesManager> bubblesManagerOptional, Optional<Bubbles> bubblesOptional, VisualStabilityManager visualStabilityManager, DeviceProvisionedController deviceProvisionedController, @@ -776,6 +778,7 @@ public class StatusBar extends SystemUI implements DemoMode, mWakefulnessLifecycle = wakefulnessLifecycle; mStatusBarStateController = statusBarStateController; mVibratorHelper = vibratorHelper; + mBubblesManagerOptional = bubblesManagerOptional; mBubblesOptional = bubblesOptional; mVisualStabilityManager = visualStabilityManager; mDeviceProvisionedController = deviceProvisionedController; @@ -1035,10 +1038,8 @@ public class StatusBar extends SystemUI implements DemoMode, mStackScrollerController.getNotificationListContainer(); mNotificationLogger.setUpWithContainer(notifListContainer); - updateAodIconArea(); inflateShelf(); mNotificationIconAreaController.setupShelf(mNotificationShelfController); - mNotificationPanelViewController.setOnReinflationListener(this::updateAodIconArea); mNotificationPanelViewController.addExpansionListener(mWakeUpCoordinator); // Allow plugins to reference DarkIconDispatcher and StatusBarStateController @@ -1143,8 +1144,8 @@ public class StatusBar extends SystemUI implements DemoMode, ScrimView scrimBehind = mNotificationShadeWindowView.findViewById(R.id.scrim_behind); ScrimView scrimInFront = mNotificationShadeWindowView.findViewById(R.id.scrim_in_front); - ScrimView scrimForBubble = mBubblesOptional.isPresent() - ? mBubblesOptional.get().getScrimForBubble() : null; + ScrimView scrimForBubble = mBubblesManagerOptional.isPresent() + ? mBubblesManagerOptional.get().getScrimForBubble() : null; mScrimController.setScrimVisibleListener(scrimsVisible -> { mNotificationShadeWindowController.setScrimsVisibility(scrimsVisible); @@ -1200,8 +1201,8 @@ public class StatusBar extends SystemUI implements DemoMode, fragmentHostManager.addTagListener(QS.TAG, (tag, f) -> { QS qs = (QS) f; if (qs instanceof QSFragment) { - mQSPanel = ((QSFragment) qs).getQsPanel(); - mQSPanel.setBrightnessMirror(mBrightnessMirrorController); + mQSPanelController = ((QSFragment) qs).getQSPanelController(); + mQSPanelController.setBrightnessMirror(mBrightnessMirrorController); } }); } @@ -1270,12 +1271,6 @@ public class StatusBar extends SystemUI implements DemoMode, ThreadedRenderer.overrideProperty("ambientRatio", String.valueOf(1.5f)); } - private void updateAodIconArea() { - mNotificationIconAreaController.setupAodIcons( - getNotificationShadeWindowView() - .findViewById(R.id.clock_notification_icon_container)); - } - @NonNull @Override public Lifecycle getLifecycle() { @@ -1593,19 +1588,19 @@ public class StatusBar extends SystemUI implements DemoMode, } public void addQsTile(ComponentName tile) { - if (mQSPanel != null && mQSPanel.getHost() != null) { - mQSPanel.getHost().addTile(tile); + if (mQSPanelController != null && mQSPanelController.getHost() != null) { + mQSPanelController.getHost().addTile(tile); } } public void remQsTile(ComponentName tile) { - if (mQSPanel != null && mQSPanel.getHost() != null) { - mQSPanel.getHost().removeTile(tile); + if (mQSPanelController != null && mQSPanelController.getHost() != null) { + mQSPanelController.getHost().removeTile(tile); } } public void clickTile(ComponentName tile) { - mQSPanel.clickTile(tile); + mQSPanelController.clickTile(tile); } /** @@ -2197,7 +2192,7 @@ public class StatusBar extends SystemUI implements DemoMode, if (!mUserSetup) return; if (subPanel != null) { - mQSPanel.openDetails(subPanel); + mQSPanelController.openDetails(subPanel); } mNotificationPanelViewController.expandWithQs(); @@ -2845,7 +2840,7 @@ public class StatusBar extends SystemUI implements DemoMode, resetUserExpandedStates(); } else if (DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG.equals(action)) { - mQSPanel.showDeviceMonitoringDialog(); + mQSPanelController.showDeviceMonitoringDialog(); } Trace.endSection(); } @@ -2936,8 +2931,8 @@ public class StatusBar extends SystemUI implements DemoMode, */ void updateResources() { // Update the quick setting tiles - if (mQSPanel != null) { - mQSPanel.updateResources(); + if (mQSPanelController != null) { + mQSPanelController.updateResources(); } if (mStatusBarWindowController != null) { @@ -3402,8 +3397,8 @@ public class StatusBar extends SystemUI implements DemoMode, // Keyguard state has changed, but QS is not listening anymore. Make sure to update the tile // visibilities so next time we open the panel we know the correct height already. - if (mQSPanel != null) { - mQSPanel.refreshAllTiles(); + if (mQSPanelController != null) { + mQSPanelController.refreshAllTiles(); } mHandler.removeMessages(MSG_LAUNCH_TRANSITION_TIMEOUT); releaseGestureWakeLock(); @@ -3988,7 +3983,8 @@ public class StatusBar extends SystemUI implements DemoMode, PackageManager pm = mContext.getPackageManager(); ResolveInfo resolveInfo = pm.resolveActivity(emergencyIntent, /*flags=*/0); if (resolveInfo == null) { - Log.wtf(TAG, "Couldn't find an app to process the emergency intent."); + // TODO(b/171084088) Upgrade log to wtf when we have default app in main branch. + Log.d(TAG, "Couldn't find an app to process the emergency intent."); return; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 256ee2081f41..acca953629c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -48,7 +48,6 @@ import com.android.internal.widget.LockPatternUtils; import com.android.systemui.ActivityIntentHelper; import com.android.systemui.EventLogTags; import com.android.systemui.assist.AssistManager; -import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.UiBackground; @@ -76,6 +75,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback; import com.android.systemui.statusbar.policy.HeadsUpUtil; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.wmshell.BubblesManager; import java.util.Optional; import java.util.concurrent.Executor; @@ -104,7 +104,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private final KeyguardManager mKeyguardManager; private final IDreamManager mDreamManager; - private final Optional<Bubbles> mBubblesOptional; + private final Optional<BubblesManager> mBubblesManagerOptional; private final Lazy<AssistManager> mAssistManagerLazy; private final NotificationRemoteInputManager mRemoteInputManager; private final GroupMembershipManager mGroupMembershipManager; @@ -142,7 +142,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit StatusBarKeyguardViewManager statusBarKeyguardViewManager, KeyguardManager keyguardManager, IDreamManager dreamManager, - Optional<Bubbles> bubblesOptional, + Optional<BubblesManager> bubblesManagerOptional, Lazy<AssistManager> assistManagerLazy, NotificationRemoteInputManager remoteInputManager, GroupMembershipManager groupMembershipManager, @@ -176,7 +176,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mKeyguardManager = keyguardManager; mDreamManager = dreamManager; - mBubblesOptional = bubblesOptional; + mBubblesManagerOptional = bubblesManagerOptional; mAssistManagerLazy = assistManagerLazy; mRemoteInputManager = remoteInputManager; mGroupMembershipManager = groupMembershipManager; @@ -360,15 +360,11 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit collapseOnMainThread(); } - final int count = getVisibleNotificationsCount(); - final int rank = entry.getRanking().getRank(); NotificationVisibility.NotificationLocation location = NotificationLogger.getNotificationLocation(entry); - final NotificationVisibility nv = NotificationVisibility.obtain(notificationKey, - rank, count, true, location); + final NotificationVisibility nv = NotificationVisibility.obtain(entry.getKey(), + entry.getRanking().getRank(), getVisibleNotificationsCount(), true, location); - // NMS will officially remove notification if the notification has FLAG_AUTO_CANCEL: - mClickNotifier.onNotificationClick(notificationKey, nv); // TODO (b/162832756): delete these notification removals when migrating to the new // pipeline; this is taken care of in {@link NotifCollection#tryRemoveNotification} @@ -378,23 +374,40 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit if (shouldAutoCancel(entry.getSbn()) || mRemoteInputManager.isNotificationKeptForRemoteInputHistory( notificationKey)) { - // manually call notification removal in order to cancel any lifetime extenders - removeNotification(row.getEntry()); + // Immediately remove notification from visually showing. + // We have to post the removal to the UI thread for synchronization. + mMainThreadHandler.post(() -> { + final Runnable removeNotification = () -> { + mOnUserInteractionCallback.onDismiss(entry, REASON_CLICK); + mClickNotifier.onNotificationClick(entry.getKey(), nv); + }; + if (mPresenter.isCollapsing()) { + // To avoid lags we're only performing the remove + // after the shade is collapsed + mShadeController.addPostCollapseAction(removeNotification); + } else { + removeNotification.run(); + } + }); } + } else { + // inform NMS that the notification was clicked + mClickNotifier.onNotificationClick(notificationKey, nv); } mIsCollapsingToShowActivityOverLockscreen = false; } private void expandBubbleStackOnMainThread(NotificationEntry entry) { - if (!mBubblesOptional.isPresent()) { + if (!mBubblesManagerOptional.isPresent()) { return; } if (Looper.getMainLooper().isCurrentThread()) { - mBubblesOptional.get().expandStackAndSelectBubble(entry); + mBubblesManagerOptional.get().expandStackAndSelectBubble(entry); } else { - mMainThreadHandler.post(() -> mBubblesOptional.get().expandStackAndSelectBubble(entry)); + mMainThreadHandler.post( + () -> mBubblesManagerOptional.get().expandStackAndSelectBubble(entry)); } } @@ -565,21 +578,6 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit return entry.shouldSuppressFullScreenIntent(); } - private void removeNotification(NotificationEntry entry) { - // We have to post it to the UI thread for synchronization - mMainThreadHandler.post(() -> { - if (mPresenter.isCollapsing()) { - // To avoid lags we're only performing the remove - // after the shade was collapsed - mShadeController.addPostCollapseAction( - () -> mOnUserInteractionCallback.onDismiss(entry, REASON_CLICK) - ); - } else { - mOnUserInteractionCallback.onDismiss(entry, REASON_CLICK); - } - }); - } - // --------------------- NotificationEntryManager/NotifPipeline methods ------------------------ private int getVisibleNotificationsCount() { @@ -609,7 +607,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private final KeyguardManager mKeyguardManager; private final IDreamManager mDreamManager; - private final Optional<Bubbles> mBubblesOptional; + private final Optional<BubblesManager> mBubblesManagerOptional; private final Lazy<AssistManager> mAssistManagerLazy; private final NotificationRemoteInputManager mRemoteInputManager; private final GroupMembershipManager mGroupMembershipManager; @@ -646,7 +644,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit StatusBarKeyguardViewManager statusBarKeyguardViewManager, KeyguardManager keyguardManager, IDreamManager dreamManager, - Optional<Bubbles> bubblesOptional, + Optional<BubblesManager> bubblesManager, Lazy<AssistManager> assistManagerLazy, NotificationRemoteInputManager remoteInputManager, GroupMembershipManager groupMembershipManager, @@ -676,7 +674,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mKeyguardManager = keyguardManager; mDreamManager = dreamManager; - mBubblesOptional = bubblesOptional; + mBubblesManagerOptional = bubblesManager; mAssistManagerLazy = assistManagerLazy; mRemoteInputManager = remoteInputManager; mGroupMembershipManager = groupMembershipManager; @@ -732,7 +730,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mStatusBarKeyguardViewManager, mKeyguardManager, mDreamManager, - mBubblesOptional, + mBubblesManagerOptional, mAssistManagerLazy, mRemoteInputManager, mGroupMembershipManager, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java index 6d4099b656cb..13d5bf59fb4b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java @@ -31,7 +31,6 @@ import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.InitController; import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.bubbles.Bubbles; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.UiBackground; @@ -97,6 +96,8 @@ import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler; import com.android.systemui.statusbar.policy.UserInfoControllerImpl; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.volume.VolumeComponent; +import com.android.systemui.wmshell.BubblesManager; +import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.splitscreen.SplitScreen; import java.util.Optional; @@ -155,6 +156,7 @@ public interface StatusBarPhoneModule { WakefulnessLifecycle wakefulnessLifecycle, SysuiStatusBarStateController statusBarStateController, VibratorHelper vibratorHelper, + Optional<BubblesManager> bubblesManagerOptional, Optional<Bubbles> bubblesOptional, VisualStabilityManager visualStabilityManager, DeviceProvisionedController deviceProvisionedController, @@ -233,6 +235,7 @@ public interface StatusBarPhoneModule { wakefulnessLifecycle, statusBarStateController, vibratorHelper, + bubblesManagerOptional, bubblesOptional, visualStabilityManager, deviceProvisionedController, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java index 06e4731265e3..08e70a97e0ca 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java @@ -97,6 +97,9 @@ public interface BatteryController extends DemoMode, Dumpable, default void onPowerSaveChanged(boolean isPowerSave) { } + default void onBatteryUnknownStateChanged(boolean isUnknown) { + } + default void onReverseChanged(boolean isReverse, int level, String name) { } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java index 57ac85e1e86d..d8710bf85a6c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.policy; +import static android.os.BatteryManager.EXTRA_PRESENT; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -75,6 +77,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC protected int mLevel; protected boolean mPluggedIn; protected boolean mCharging; + private boolean mStateUnknown = false; private boolean mCharged; protected boolean mPowerSave; private boolean mAodPowerSave; @@ -138,6 +141,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC pw.print(" mCharging="); pw.println(mCharging); pw.print(" mCharged="); pw.println(mCharged); pw.print(" mPowerSave="); pw.println(mPowerSave); + pw.print(" mStateUnknown="); pw.println(mStateUnknown); } @Override @@ -180,6 +184,13 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC mWirelessCharging = mCharging && intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) == BatteryManager.BATTERY_PLUGGED_WIRELESS; + boolean present = intent.getBooleanExtra(EXTRA_PRESENT, true); + boolean unknown = !present; + if (unknown != mStateUnknown) { + mStateUnknown = unknown; + fireBatteryUnknownStateChanged(); + } + fireBatteryLevelChanged(); } else if (action.equals(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)) { updatePowerSave(); @@ -328,6 +339,15 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC } } + private void fireBatteryUnknownStateChanged() { + synchronized (mChangeCallbacks) { + final int n = mChangeCallbacks.size(); + for (int i = 0; i < n; i++) { + mChangeCallbacks.get(i).onBatteryUnknownStateChanged(mStateUnknown); + } + } + } + private void firePowerSaveChanged() { synchronized (mChangeCallbacks) { final int N = mChangeCallbacks.size(); @@ -346,6 +366,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC String level = args.getString("level"); String plugged = args.getString("plugged"); String powerSave = args.getString("powersave"); + String present = args.getString("present"); if (level != null) { mLevel = Math.min(Math.max(Integer.parseInt(level), 0), 100); } @@ -356,6 +377,10 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC mPowerSave = powerSave.equals("true"); firePowerSaveChanged(); } + if (present != null) { + mStateUnknown = !present.equals("true"); + fireBatteryUnknownStateChanged(); + } fireBatteryLevelChanged(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryStateNotifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryStateNotifier.kt new file mode 100644 index 000000000000..92e5b78f776a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryStateNotifier.kt @@ -0,0 +1,90 @@ +/* + * 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.statusbar.policy + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.android.systemui.R +import com.android.systemui.util.concurrency.DelayableExecutor +import javax.inject.Inject + +/** + * Listens for important battery states and sends non-dismissible system notifications if there is a + * problem + */ +class BatteryStateNotifier @Inject constructor( + val controller: BatteryController, + val noMan: NotificationManager, + val delayableExecutor: DelayableExecutor, + val context: Context +) : BatteryController.BatteryStateChangeCallback { + var stateUnknown = false + + fun startListening() { + controller.addCallback(this) + } + + fun stopListening() { + controller.removeCallback(this) + } + + override fun onBatteryUnknownStateChanged(isUnknown: Boolean) { + stateUnknown = isUnknown + if (stateUnknown) { + val channel = NotificationChannel("battery_status", "Battery status", + NotificationManager.IMPORTANCE_DEFAULT) + noMan.createNotificationChannel(channel) + + val intent = Intent(Intent.ACTION_VIEW, + Uri.parse(context.getString(R.string.config_batteryStateUnknownUrl))) + val pi = PendingIntent.getActivity(context, 0, intent, 0) + + val builder = Notification.Builder(context, channel.id) + .setAutoCancel(false) + .setContentTitle( + context.getString(R.string.battery_state_unknown_notification_title)) + .setContentText( + context.getString(R.string.battery_state_unknown_notification_text)) + .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb) + .setContentIntent(pi) + .setAutoCancel(true) + .setOngoing(true) + + noMan.notify(TAG, ID, builder.build()) + } else { + scheduleNotificationCancel() + } + } + + private fun scheduleNotificationCancel() { + val r = { + if (!stateUnknown) { + noMan.cancel(ID) + } + } + delayableExecutor.executeDelayed(r, DELAY_MILLIS) + } +} + +private const val TAG = "BatteryStateNotifier" +private const val ID = 666 +private const val DELAY_MILLIS: Long = 4 * 60 * 60 * 1000
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java index c15560ae9f38..0e10fdd88e00 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java @@ -26,7 +26,6 @@ import android.os.Handler; import android.os.Looper; import android.provider.Settings.Global; import android.telephony.Annotation; -import android.telephony.CdmaEriInformation; import android.telephony.CellSignalStrength; import android.telephony.CellSignalStrengthCdma; import android.telephony.CellSignalStrengthNr; @@ -591,11 +590,9 @@ public class MobileSignalController extends SignalController< if (isCarrierNetworkChangeActive()) { return false; } - if (isCdma() && mServiceState != null) { - final int iconMode = mPhone.getCdmaEriInformation().getEriIconMode(); - return mPhone.getCdmaEriInformation().getEriIconIndex() != CdmaEriInformation.ERI_OFF - && (iconMode == CdmaEriInformation.ERI_ICON_MODE_NORMAL - || iconMode == CdmaEriInformation.ERI_ICON_MODE_FLASH); + if (isCdma()) { + return mPhone.getCdmaEnhancedRoamingIndicatorDisplayNumber() + != TelephonyManager.ERI_OFF; } else { return mServiceState != null && mServiceState.getRoaming(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index 9c3395f9332d..4552026ced4b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -18,11 +18,13 @@ package com.android.systemui.statusbar.policy; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.Notification; import android.app.PendingIntent; import android.app.RemoteInput; +import android.content.ClipData; import android.content.ClipDescription; import android.content.Context; import android.content.Intent; @@ -43,6 +45,7 @@ import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; +import android.view.OnReceiveContentCallback; import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewGroup; @@ -57,9 +60,6 @@ import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; -import androidx.core.view.inputmethod.InputConnectionCompat; -import androidx.core.view.inputmethod.InputContentInfoCompat; - import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto; import com.android.internal.statusbar.IStatusBarService; @@ -313,6 +313,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene mRemoteInputs = remoteInputs; mRemoteInput = remoteInput; mEditText.setHint(mRemoteInput.getLabel()); + mEditText.mSupportedMimeTypes = (remoteInput.getAllowedDataTypes() == null) ? null + : remoteInput.getAllowedDataTypes().toArray(new String[0]); mEntry.editedSuggestionInfo = editedSuggestionInfo; if (editedSuggestionInfo != null) { @@ -571,6 +573,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene boolean mShowImeOnInputConnection; private LightBarController mLightBarController; UserHandle mUser; + private String[] mSupportedMimeTypes; public RemoteEditText(Context context, AttributeSet attrs) { super(context, attrs); @@ -578,6 +581,36 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene mLightBarController = Dependency.get(LightBarController.class); } + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + if (mSupportedMimeTypes != null && mSupportedMimeTypes.length > 0) { + setOnReceiveContentCallback(mSupportedMimeTypes, + new OnReceiveContentCallback<View>() { + @Override + public boolean onReceiveContent(@NonNull View view, + @NonNull Payload payload) { + ClipData clip = payload.getClip(); + if (clip.getItemCount() == 0) { + return false; + } + Uri contentUri = clip.getItemAt(0).getUri(); + ClipDescription description = clip.getDescription(); + String mimeType = null; + if (description.getMimeTypeCount() > 0) { + mimeType = description.getMimeType(0); + } + if (mimeType != null) { + Intent dataIntent = mRemoteInputView + .prepareRemoteInputFromData(mimeType, contentUri); + mRemoteInputView.sendRemoteInput(dataIntent); + } + return true; + } + }); + } + } + private void defocusIfNeeded(boolean animate) { if (mRemoteInputView != null && mRemoteInputView.mEntry.getRow().isChangingPosition() || isTemporarilyDetached()) { @@ -670,36 +703,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { - // TODO: Pass RemoteInput data types to allow image insertion. - // String[] allowedDataTypes = mRemoteInputView.mRemoteInput.getAllowedDataTypes() - // .toArray(new String[0]); - // EditorInfoCompat.setContentMimeTypes(outAttrs, allowedDataTypes); - final InputConnection inputConnection = super.onCreateInputConnection(outAttrs); - - final InputConnectionCompat.OnCommitContentListener callback = - new InputConnectionCompat.OnCommitContentListener() { - @Override - public boolean onCommitContent( - InputContentInfoCompat inputContentInfoCompat, int i, - Bundle bundle) { - Uri contentUri = inputContentInfoCompat.getContentUri(); - ClipDescription description = inputContentInfoCompat.getDescription(); - String mimeType = null; - if (description != null && description.getMimeTypeCount() > 0) { - mimeType = description.getMimeType(0); - } - if (mimeType != null) { - Intent dataIntent = mRemoteInputView.prepareRemoteInputFromData( - mimeType, contentUri); - mRemoteInputView.sendRemoteInput(dataIntent); - } - return true; - } - }; - - InputConnection ic = inputConnection == null ? null : - InputConnectionCompat.createWrapper(inputConnection, outAttrs, callback); - + final InputConnection ic = super.onCreateInputConnection(outAttrs); Context userContext = null; try { userContext = mContext.createPackageContextAsUser( @@ -747,7 +751,6 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } else { setBackground(null); } - } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java index 79d264ca4577..e8331a176134 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java @@ -15,6 +15,9 @@ */ package com.android.systemui.statusbar.policy; +import android.app.admin.DeviceAdminInfo; +import android.graphics.drawable.Drawable; + import com.android.systemui.Dumpable; import com.android.systemui.statusbar.policy.SecurityController.SecurityControllerCallback; @@ -40,6 +43,15 @@ public interface SecurityController extends CallbackController<SecurityControlle boolean hasCACertInCurrentUser(); boolean hasCACertInWorkProfile(); void onUserSwitched(int newUserId); + /** Whether or not parental controls is enabled */ + boolean isParentalControlsEnabled(); + /** DeviceAdminInfo for active admin */ + DeviceAdminInfo getDeviceAdminInfo(); + /** Icon for admin */ + Drawable getIcon(DeviceAdminInfo info); + /** Label for admin */ + CharSequence getLabel(DeviceAdminInfo info); + public interface SecurityControllerCallback { void onStateChanged(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java index 7e54e8d1c1c3..1d778419cbaf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java @@ -16,15 +16,19 @@ package com.android.systemui.statusbar.policy; import android.app.ActivityManager; +import android.app.admin.DeviceAdminInfo; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; +import android.graphics.drawable.Drawable; import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; import android.net.IConnectivityManager; @@ -53,7 +57,10 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.settings.CurrentUserTracker; +import org.xmlpull.v1.XmlPullParserException; + import java.io.FileDescriptor; +import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.concurrent.Executor; @@ -306,6 +313,50 @@ public class SecurityControllerImpl extends CurrentUserTracker implements Securi fireCallbacks(); } + @Override + public boolean isParentalControlsEnabled() { + return getProfileOwnerOrDeviceOwnerSupervisionComponent() != null; + } + + @Override + public DeviceAdminInfo getDeviceAdminInfo() { + return getDeviceAdminInfo(getProfileOwnerOrDeviceOwnerComponent()); + } + + @Override + public Drawable getIcon(DeviceAdminInfo info) { + return (info == null) ? null : info.loadIcon(mPackageManager); + } + + @Override + public CharSequence getLabel(DeviceAdminInfo info) { + return (info == null) ? null : info.loadLabel(mPackageManager); + } + + private ComponentName getProfileOwnerOrDeviceOwnerSupervisionComponent() { + UserHandle currentUser = new UserHandle(mCurrentUserId); + return mDevicePolicyManager + .getProfileOwnerOrDeviceOwnerSupervisionComponent(currentUser); + } + + // Returns the ComponentName of the current DO/PO. Right now it only checks the supervision + // component but can be changed to check for other DO/POs. This change would make getIcon() + // and getLabel() work for all admins. + private ComponentName getProfileOwnerOrDeviceOwnerComponent() { + return getProfileOwnerOrDeviceOwnerSupervisionComponent(); + } + + private DeviceAdminInfo getDeviceAdminInfo(ComponentName componentName) { + try { + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = mPackageManager.getReceiverInfo(componentName, + PackageManager.GET_META_DATA); + return new DeviceAdminInfo(mContext, resolveInfo); + } catch (NameNotFoundException | XmlPullParserException | IOException e) { + return null; + } + } + private void refreshCACerts(int userId) { mBgExecutor.execute(() -> { Pair<Integer, Boolean> idWithCert = null; diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java index 1c682e3bb7dc..409d1361223c 100644 --- a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java +++ b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java @@ -129,6 +129,8 @@ public class ToastUI extends SystemUI implements CommandQueue.Callbacks { mCallback = callback; mPresenter = new ToastPresenter(context, mIAccessibilityManager, mNotificationManager, packageName); + // Set as trusted overlay so touches can pass through toasts + mPresenter.getLayoutParams().setTrustedOverlay(); mToastLogger.logOnShowToast(uid, packageName, text.toString(), token.toString()); mPresenter.show(mToast.getView(), token, windowToken, duration, mToast.getGravity(), mToast.getXOffset(), mToast.getYOffset(), mToast.getHorizontalMargin(), diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java index 70bba263ab90..b67574d1c4de 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java @@ -32,6 +32,7 @@ import com.android.systemui.statusbar.phone.SystemUIDialog; public abstract class TunerService { public static final String ACTION_CLEAR = "com.android.systemui.action.CLEAR_TUNER"; + private final Context mContext; public abstract void clearAll(); public abstract void destroy(); @@ -50,6 +51,10 @@ public abstract class TunerService { void onTuningChanged(String key, String newValue); } + public TunerService(Context context) { + mContext = context; + } + private static Context userContext(Context context, UserHandle user) { try { return context.createPackageContextAsUser(context.getPackageName(), 0, user); @@ -58,6 +63,11 @@ public abstract class TunerService { } } + /** Enables or disables the tuner for the supplied user. */ + public void setTunerEnabled(UserHandle user, boolean enabled) { + setTunerEnabled(mContext, user, enabled); + } + public static final void setTunerEnabled(Context context, UserHandle user, boolean enabled) { userContext(context, user).getPackageManager().setComponentEnabledSetting( new ComponentName(context, TunerActivity.class), @@ -66,6 +76,11 @@ public abstract class TunerService { PackageManager.DONT_KILL_APP); } + /** Returns true if the tuner is enabled for the supplied user. */ + public boolean isTunerEnabled(UserHandle user) { + return isTunerEnabled(mContext, user); + } + public static final boolean isTunerEnabled(Context context, UserHandle user) { return userContext(context, user).getPackageManager().getComponentEnabledSetting( new ComponentName(context, TunerActivity.class)) @@ -81,6 +96,11 @@ public abstract class TunerService { } } + /** */ + public void showResetRequest(UserHandle user, final Runnable onDisabled) { + showResetRequest(mContext, user, onDisabled); + } + public static final void showResetRequest(final Context context, UserHandle user, final Runnable onDisabled) { SystemUIDialog dialog = new SystemUIDialog(context); diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java index 22f03e074b06..027c282ba352 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java @@ -94,6 +94,7 @@ public class TunerServiceImpl extends TunerService { DemoModeController demoModeController, BroadcastDispatcher broadcastDispatcher, UserTracker userTracker) { + super(context); mContext = context; mContentResolver = mContext.getContentResolver(); mLeakDetector = leakDetector; diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvGlobalRootComponent.java b/packages/SystemUI/src/com/android/systemui/tv/TvGlobalRootComponent.java index df741a0e98ff..89ab23b50cd8 100644 --- a/packages/SystemUI/src/com/android/systemui/tv/TvGlobalRootComponent.java +++ b/packages/SystemUI/src/com/android/systemui/tv/TvGlobalRootComponent.java @@ -42,6 +42,12 @@ public interface TvGlobalRootComponent extends GlobalRootComponent { TvGlobalRootComponent build(); } + /** + * Builder for a WMComponent. + */ + @Override + TvWMComponent.Builder getWMComponentBuilder(); + @Override TvSysUIComponent.Builder getSysUIComponent(); } diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSysUIComponentModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSysUIComponentModule.java index 334bb013ae69..9621e5f5f1a0 100644 --- a/packages/SystemUI/src/com/android/systemui/tv/TvSysUIComponentModule.java +++ b/packages/SystemUI/src/com/android/systemui/tv/TvSysUIComponentModule.java @@ -19,7 +19,7 @@ package com.android.systemui.tv; import dagger.Module; /** - * Dagger module for including the WMComponent. + * Dagger module for including the SysUIComponent. */ @Module(subcomponents = {TvSysUIComponent.class}) public abstract class TvSysUIComponentModule { diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIBinder.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIBinder.java index bde88b1b5533..2c3ea4f452bb 100644 --- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIBinder.java +++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIBinder.java @@ -22,7 +22,7 @@ import com.android.systemui.wmshell.TvPipModule; import dagger.Binds; import dagger.Module; -@Module(includes = TvPipModule.class) +@Module interface TvSystemUIBinder { @Binds GlobalRootComponent bindGlobalRootComponent(TvGlobalRootComponent globalRootComponent); diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java index c5bb9c1b6f48..8ffc7cf568ff 100644 --- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java @@ -62,7 +62,6 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.DeviceProvisionedControllerImpl; import com.android.systemui.statusbar.policy.HeadsUpManager; -import com.android.systemui.wmshell.TvWMShellModule; import javax.inject.Named; @@ -75,8 +74,7 @@ import dagger.Provides; * overridden by the System UI implementation. */ @Module(includes = { - QSModule.class, - TvWMShellModule.class, + QSModule.class }, subcomponents = { }) diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvWMComponent.java b/packages/SystemUI/src/com/android/systemui/tv/TvWMComponent.java new file mode 100644 index 000000000000..f678513f2ce6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/tv/TvWMComponent.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2019 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.tv; + +import com.android.systemui.dagger.WMComponent; +import com.android.systemui.dagger.WMSingleton; +import com.android.systemui.wmshell.TvWMShellModule; + +import dagger.Subcomponent; + + +/** + * Dagger Subcomponent for WindowManager. + */ +@WMSingleton +@Subcomponent(modules = {TvWMShellModule.class}) +public interface TvWMComponent extends WMComponent { + + /** + * Builder for a SysUIComponent. + */ + @Subcomponent.Builder + interface Builder extends WMComponent.Builder { + TvWMComponent build(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java b/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java index 66f8f74c7cab..6b5556b3ea91 100644 --- a/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java +++ b/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java @@ -23,13 +23,19 @@ import android.content.Context; import android.provider.DeviceConfig; import android.provider.Settings; +import com.android.systemui.dagger.SysUISingleton; + import java.util.concurrent.Executor; +import javax.inject.Inject; + /** * Wrapper around DeviceConfig useful for testing. */ +@SysUISingleton public class DeviceConfigProxy { + @Inject public DeviceConfigProxy() { } diff --git a/packages/SystemUI/src/com/android/systemui/util/InjectionInflationController.java b/packages/SystemUI/src/com/android/systemui/util/InjectionInflationController.java index 344f0d2f5506..4b4e1df21bd0 100644 --- a/packages/SystemUI/src/com/android/systemui/util/InjectionInflationController.java +++ b/packages/SystemUI/src/com/android/systemui/util/InjectionInflationController.java @@ -24,10 +24,8 @@ import android.view.LayoutInflater; import android.view.View; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.qs.QSFooterImpl; import com.android.systemui.qs.QSPanel; import com.android.systemui.qs.QuickQSPanel; -import com.android.systemui.qs.customize.QSCustomizer; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import java.lang.reflect.InvocationTargetException; @@ -92,11 +90,6 @@ public class InjectionInflationController { } /** - * Creates the QSFooterImpl. - */ - QSFooterImpl createQsFooter(); - - /** * Creates the NotificationStackScrollLayout. */ NotificationStackScrollLayout createNotificationStackScrollLayout(); @@ -110,11 +103,6 @@ public class InjectionInflationController { * Creates the QuickQSPanel. */ QuickQSPanel createQuickQSPanel(); - - /** - * Creates the QSCustomizer. - */ - QSCustomizer createQSCustomizer(); } diff --git a/packages/SystemUI/src/com/android/systemui/util/ViewController.java b/packages/SystemUI/src/com/android/systemui/util/ViewController.java index c7aa780fcacb..880d09abeb60 100644 --- a/packages/SystemUI/src/com/android/systemui/util/ViewController.java +++ b/packages/SystemUI/src/com/android/systemui/util/ViewController.java @@ -16,6 +16,8 @@ package com.android.systemui.util; +import android.content.Context; +import android.content.res.Resources; import android.view.View; import android.view.View.OnAttachStateChangeListener; @@ -24,8 +26,8 @@ import android.view.View.OnAttachStateChangeListener; * * Implementations should handle setup and teardown related activities inside of * {@link #onViewAttached()} and {@link #onViewDetached()}. Be sure to call {@link #init()} on - * any child controllers that this uses. This can be done in {@link init()} if the controllers - * are injected, or right after creation time of the child controller. + * any child controllers that this uses. This can be done in {@link #onInit()} if the + * controllers are injected, or right after creation time of the child controller. * * Tip: View "attachment" happens top down - parents are notified that they are attached before * any children. That means that if you call a method on a child controller in @@ -60,11 +62,18 @@ public abstract class ViewController<T extends View> { mView = view; } - /** Call immediately after constructing Controller in order to handle view lifecycle events. */ + /** + * Call immediately after constructing Controller in order to handle view lifecycle events. + * + * Generally speaking, you don't want to override this method. Instead, override + * {@link #onInit()} as a way to have an run-once idempotent method that you can use for + * setup of your ViewController. + */ public void init() { if (mInited) { return; } + onInit(); mInited = true; if (mView != null) { @@ -76,6 +85,22 @@ public abstract class ViewController<T extends View> { } /** + * Run once when {@link #init()} is called. + * + * Override this to perform idempotent, one-time setup that your controller needs. It will + * be called before {@link #onViewAttached()}. + */ + protected void onInit() {} + + protected Context getContext() { + return mView.getContext(); + } + + protected Resources getResources() { + return mView.getResources(); + } + + /** * Called when the view is attached and a call to {@link #init()} has been made in either order. */ protected abstract void onViewAttached(); diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java new file mode 100644 index 000000000000..844f12e0c43e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java @@ -0,0 +1,725 @@ +/* + * 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.wmshell; + +import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED; +import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; +import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL; +import static android.service.notification.NotificationListenerService.REASON_CANCEL; +import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL; +import static android.service.notification.NotificationListenerService.REASON_CLICK; +import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED; +import static android.service.notification.NotificationStats.DISMISSAL_BUBBLE; +import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL; + +import static com.android.systemui.statusbar.StatusBarState.SHADE; +import static com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; + +import android.app.INotificationManager; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.content.res.Configuration; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.service.notification.NotificationListenerService.RankingMap; +import android.service.notification.ZenModeConfig; +import android.util.ArraySet; +import android.util.Log; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.statusbar.IStatusBarService; +import com.android.internal.statusbar.NotificationVisibility; +import com.android.systemui.Dumpable; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dump.DumpManager; +import com.android.systemui.model.SysUiState; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.shared.system.QuickStepContract; +import com.android.systemui.statusbar.FeatureFlags; +import com.android.systemui.statusbar.NotificationLockscreenUserManager; +import com.android.systemui.statusbar.NotificationShadeWindowController; +import com.android.systemui.statusbar.ScrimView; +import com.android.systemui.statusbar.notification.NotificationChannelHelper; +import com.android.systemui.statusbar.notification.NotificationEntryListener; +import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.collection.NotifCollection; +import com.android.systemui.statusbar.notification.collection.NotifPipeline; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.coordinator.BubbleCoordinator; +import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy; +import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats; +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; +import com.android.systemui.statusbar.notification.logging.NotificationLogger; +import com.android.systemui.statusbar.phone.ScrimController; +import com.android.systemui.statusbar.phone.ShadeController; +import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.ZenModeController; +import com.android.wm.shell.bubbles.BubbleEntry; +import com.android.wm.shell.bubbles.Bubbles; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.IntConsumer; + +/** + * The SysUi side bubbles manager which communicate with other SysUi components. + */ +@SysUISingleton +public class BubblesManager implements Dumpable { + + private static final String TAG = TAG_WITH_CLASS_NAME ? "BubblesManager" : TAG_BUBBLES; + + private final Context mContext; + private final Bubbles mBubbles; + private final NotificationShadeWindowController mNotificationShadeWindowController; + private final ShadeController mShadeController; + private final IStatusBarService mBarService; + private final INotificationManager mNotificationManager; + private final NotificationInterruptStateProvider mNotificationInterruptStateProvider; + private final NotificationGroupManagerLegacy mNotificationGroupManager; + private final NotificationEntryManager mNotificationEntryManager; + private final NotifPipeline mNotifPipeline; + + private final ScrimView mBubbleScrim; + private final Bubbles.SysuiProxy mSysuiProxy; + // TODO (b/145659174): allow for multiple callbacks to support the "shadow" new notif pipeline + private final List<NotifCallback> mCallbacks = new ArrayList<>(); + + /** + * Creates {@link BubblesManager}, returns {@code null} if Optional {@link Bubbles} not present + * which means bubbles feature not support. + */ + @Nullable + public static BubblesManager create(Context context, + Optional<Bubbles> bubblesOptional, + NotificationShadeWindowController notificationShadeWindowController, + StatusBarStateController statusBarStateController, + ShadeController shadeController, + ConfigurationController configurationController, + @Nullable IStatusBarService statusBarService, + INotificationManager notificationManager, + NotificationInterruptStateProvider interruptionStateProvider, + ZenModeController zenModeController, + NotificationLockscreenUserManager notifUserManager, + NotificationGroupManagerLegacy groupManager, + NotificationEntryManager entryManager, + NotifPipeline notifPipeline, + SysUiState sysUiState, + FeatureFlags featureFlags, + DumpManager dumpManager) { + if (bubblesOptional.isPresent()) { + return new BubblesManager(context, bubblesOptional.get(), + notificationShadeWindowController, statusBarStateController, shadeController, + configurationController, statusBarService, notificationManager, + interruptionStateProvider, zenModeController, notifUserManager, + groupManager, entryManager, notifPipeline, sysUiState, featureFlags, + dumpManager); + } else { + return null; + } + } + + @VisibleForTesting + BubblesManager(Context context, + Bubbles bubbles, + NotificationShadeWindowController notificationShadeWindowController, + StatusBarStateController statusBarStateController, + ShadeController shadeController, + ConfigurationController configurationController, + @Nullable IStatusBarService statusBarService, + INotificationManager notificationManager, + NotificationInterruptStateProvider interruptionStateProvider, + ZenModeController zenModeController, + NotificationLockscreenUserManager notifUserManager, + NotificationGroupManagerLegacy groupManager, + NotificationEntryManager entryManager, + NotifPipeline notifPipeline, + SysUiState sysUiState, + FeatureFlags featureFlags, + DumpManager dumpManager) { + mContext = context; + mBubbles = bubbles; + mNotificationShadeWindowController = notificationShadeWindowController; + mShadeController = shadeController; + mNotificationManager = notificationManager; + mNotificationInterruptStateProvider = interruptionStateProvider; + mNotificationGroupManager = groupManager; + mNotificationEntryManager = entryManager; + mNotifPipeline = notifPipeline; + + mBarService = statusBarService == null + ? IStatusBarService.Stub.asInterface( + ServiceManager.getService(Context.STATUS_BAR_SERVICE)) + : statusBarService; + + mBubbleScrim = new ScrimView(mContext); + mBubbleScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + mBubbles.setBubbleScrim(mBubbleScrim); + + if (featureFlags.isNewNotifPipelineRenderingEnabled()) { + setupNotifPipeline(); + } else { + setupNEM(); + } + + dumpManager.registerDumpable(TAG, this); + + statusBarStateController.addCallback(new StatusBarStateController.StateListener() { + @Override + public void onStateChanged(int newState) { + boolean isShade = newState == SHADE; + bubbles.onStatusBarStateChanged(isShade); + } + }); + + configurationController.addCallback(new ConfigurationController.ConfigurationListener() { + @Override + public void onConfigChanged(Configuration newConfig) { + mBubbles.onConfigChanged(newConfig); + } + + @Override + public void onUiModeChanged() { + mBubbles.updateForThemeChanges(); + } + + @Override + public void onThemeChanged() { + mBubbles.updateForThemeChanges(); + } + }); + + zenModeController.addCallback(new ZenModeController.Callback() { + @Override + public void onZenChanged(int zen) { + mBubbles.onZenStateChanged(); + } + + @Override + public void onConfigChanged(ZenModeConfig config) { + mBubbles.onZenStateChanged(); + } + }); + + notifUserManager.addUserChangedListener( + new NotificationLockscreenUserManager.UserChangedListener() { + @Override + public void onUserChanged(int userId) { + mBubbles.onUserChanged(userId); + } + }); + + mSysuiProxy = new Bubbles.SysuiProxy() { + @Override + @Nullable + public BubbleEntry getPendingOrActiveEntry(String key) { + NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif(key); + return entry == null ? null : notifToBubbleEntry(entry); + } + + @Override + public List<BubbleEntry> getShouldRestoredEntries(ArraySet<String> savedBubbleKeys) { + List<BubbleEntry> result = new ArrayList<>(); + List<NotificationEntry> activeEntries = + mNotificationEntryManager.getActiveNotificationsForCurrentUser(); + for (int i = 0; i < activeEntries.size(); i++) { + NotificationEntry entry = activeEntries.get(i); + if (savedBubbleKeys.contains(entry.getKey()) + && mNotificationInterruptStateProvider.shouldBubbleUp(entry) + && entry.isBubble()) { + result.add(notifToBubbleEntry(entry)); + } + } + return result; + } + + @Override + public boolean isNotificationShadeExpand() { + return mNotificationShadeWindowController.getPanelExpanded(); + } + + @Override + public boolean shouldBubbleUp(String key) { + final NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif( + key); + if (entry != null) { + return mNotificationInterruptStateProvider.shouldBubbleUp(entry); + } + return false; + } + + @Override + public void setNotificationInterruption(String key) { + final NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif( + key); + if (entry != null && entry.getImportance() >= NotificationManager.IMPORTANCE_HIGH) { + entry.setInterruption(); + } + } + + @Override + public void requestNotificationShadeTopUi(boolean requestTopUi, String componentTag) { + mNotificationShadeWindowController.setRequestTopUi(requestTopUi, componentTag); + } + + @Override + public void notifyRemoveNotification(String key, int reason) { + final NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif( + key); + if (entry != null) { + for (NotifCallback cb : mCallbacks) { + cb.removeNotification(entry, getDismissedByUserStats(entry, true), reason); + } + } + } + + @Override + public void notifyInvalidateNotifications(String reason) { + for (NotifCallback cb : mCallbacks) { + cb.invalidateNotifications(reason); + } + } + + @Override + public void notifyMaybeCancelSummary(String key) { + final NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif( + key); + if (entry != null) { + for (NotifCallback cb : mCallbacks) { + cb.maybeCancelSummary(entry); + } + } + } + + @Override + public void removeNotificationEntry(String key) { + final NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif( + key); + if (entry != null) { + mNotificationGroupManager.onEntryRemoved(entry); + } + } + + @Override + public void updateNotificationBubbleButton(String key) { + final NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif( + key); + if (entry != null && entry.getRow() != null) { + entry.getRow().updateBubbleButton(); + } + } + + @Override + public void updateNotificationSuppression(String key) { + final NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif( + key); + if (entry != null) { + mNotificationGroupManager.updateSuppression(entry); + } + } + + @Override + public void onStackExpandChanged(boolean shouldExpand) { + sysUiState + .setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED, shouldExpand) + .commitUpdate(mContext.getDisplayId()); + } + + @Override + public void onUnbubbleConversation(String key) { + final NotificationEntry entry = + mNotificationEntryManager.getPendingOrActiveNotif(key); + if (entry != null) { + onUserChangedBubble(entry, false /* shouldBubble */); + } + } + }; + mBubbles.setSysuiProxy(mSysuiProxy); + } + + private void setupNEM() { + mNotificationEntryManager.addNotificationEntryListener( + new NotificationEntryListener() { + @Override + public void onPendingEntryAdded(NotificationEntry entry) { + BubblesManager.this.onEntryAdded(entry); + } + + @Override + public void onPreEntryUpdated(NotificationEntry entry) { + BubblesManager.this.onEntryUpdated(entry); + } + + @Override + public void onEntryRemoved(NotificationEntry entry, + @Nullable NotificationVisibility visibility, + boolean removedByUser, int reason) { + BubblesManager.this.onEntryRemoved(entry); + } + + @Override + public void onNotificationRankingUpdated(RankingMap rankingMap) { + BubblesManager.this.onRankingUpdate(rankingMap); + } + }); + + // The new pipeline takes care of this as a NotifDismissInterceptor BubbleCoordinator + mNotificationEntryManager.addNotificationRemoveInterceptor( + (key, entry, dismissReason) -> { + final boolean isClearAll = dismissReason == REASON_CANCEL_ALL; + final boolean isUserDismiss = dismissReason == REASON_CANCEL + || dismissReason == REASON_CLICK; + final boolean isAppCancel = dismissReason == REASON_APP_CANCEL + || dismissReason == REASON_APP_CANCEL_ALL; + final boolean isSummaryCancel = + dismissReason == REASON_GROUP_SUMMARY_CANCELED; + + // Need to check for !appCancel here because the notification may have + // previously been dismissed & entry.isRowDismissed would still be true + boolean userRemovedNotif = + (entry != null && entry.isRowDismissed() && !isAppCancel) + || isClearAll || isUserDismiss || isSummaryCancel; + + if (userRemovedNotif) { + return handleDismissalInterception(entry); + } + return false; + }); + + mNotificationGroupManager.registerGroupChangeListener( + new NotificationGroupManagerLegacy.OnGroupChangeListener() { + @Override + public void onGroupSuppressionChanged( + NotificationGroupManagerLegacy.NotificationGroup group, + boolean suppressed) { + // More notifications could be added causing summary to no longer + // be suppressed -- in this case need to remove the key. + final String groupKey = group.summary != null + ? group.summary.getSbn().getGroupKey() + : null; + if (!suppressed && groupKey != null + && mBubbles.isSummarySuppressed(groupKey)) { + mBubbles.removeSuppressedSummary(groupKey); + } + } + }); + + addNotifCallback(new NotifCallback() { + @Override + public void removeNotification(NotificationEntry entry, + DismissedByUserStats dismissedByUserStats, int reason) { + mNotificationEntryManager.performRemoveNotification(entry.getSbn(), + dismissedByUserStats, reason); + } + + @Override + public void invalidateNotifications(String reason) { + mNotificationEntryManager.updateNotifications(reason); + } + + @Override + public void maybeCancelSummary(NotificationEntry entry) { + // Check if removed bubble has an associated suppressed group summary that needs + // to be removed now. + final String groupKey = entry.getSbn().getGroupKey(); + if (mBubbles.isSummarySuppressed(groupKey)) { + mBubbles.removeSuppressedSummary(groupKey); + + final NotificationEntry summary = + mNotificationEntryManager.getActiveNotificationUnfiltered( + mBubbles.getSummaryKey(groupKey)); + if (summary != null) { + mNotificationEntryManager.performRemoveNotification( + summary.getSbn(), + getDismissedByUserStats(summary, false), + UNDEFINED_DISMISS_REASON); + } + } + + // Check if we still need to remove the summary from NoManGroup because the summary + // may not be in the mBubbleData.mSuppressedGroupKeys list and removed above. + // For example: + // 1. Bubbled notifications (group) is posted to shade and are visible bubbles + // 2. User expands bubbles so now their respective notifications in the shade are + // hidden, including the group summary + // 3. User removes all bubbles + // 4. We expect all the removed bubbles AND the summary (note: the summary was + // never added to the suppressedSummary list in BubbleData, so we add this check) + NotificationEntry summary = mNotificationGroupManager.getLogicalGroupSummary(entry); + if (summary != null) { + ArrayList<NotificationEntry> summaryChildren = + mNotificationGroupManager.getLogicalChildren(summary.getSbn()); + boolean isSummaryThisNotif = summary.getKey().equals(entry.getKey()); + if (!isSummaryThisNotif && (summaryChildren == null + || summaryChildren.isEmpty())) { + mNotificationEntryManager.performRemoveNotification( + summary.getSbn(), + getDismissedByUserStats(summary, false), + UNDEFINED_DISMISS_REASON); + } + } + } + }); + } + + private void setupNotifPipeline() { + mNotifPipeline.addCollectionListener(new NotifCollectionListener() { + @Override + public void onEntryAdded(NotificationEntry entry) { + BubblesManager.this.onEntryAdded(entry); + } + + @Override + public void onEntryUpdated(NotificationEntry entry) { + BubblesManager.this.onEntryUpdated(entry); + } + + @Override + public void onEntryRemoved(NotificationEntry entry, + @NotifCollection.CancellationReason int reason) { + BubblesManager.this.onEntryRemoved(entry); + } + + @Override + public void onRankingUpdate(RankingMap rankingMap) { + BubblesManager.this.onRankingUpdate(rankingMap); + } + }); + } + + void onEntryAdded(NotificationEntry entry) { + if (mNotificationInterruptStateProvider.shouldBubbleUp(entry) + && entry.isBubble()) { + mBubbles.onEntryAdded(notifToBubbleEntry(entry)); + } + } + + void onEntryUpdated(NotificationEntry entry) { + mBubbles.onEntryUpdated(notifToBubbleEntry(entry), + mNotificationInterruptStateProvider.shouldBubbleUp(entry)); + } + + void onEntryRemoved(NotificationEntry entry) { + mBubbles.onEntryRemoved(notifToBubbleEntry(entry)); + } + + void onRankingUpdate(RankingMap rankingMap) { + mBubbles.onRankingUpdated(rankingMap); + } + + /** + * Gets the DismissedByUserStats used by {@link NotificationEntryManager}. + * Will not be necessary when using the new notification pipeline's {@link NotifCollection}. + * Instead, this is taken care of by {@link BubbleCoordinator}. + */ + private DismissedByUserStats getDismissedByUserStats( + NotificationEntry entry, + boolean isVisible) { + return new DismissedByUserStats( + DISMISSAL_BUBBLE, + DISMISS_SENTIMENT_NEUTRAL, + NotificationVisibility.obtain( + entry.getKey(), + entry.getRanking().getRank(), + mNotificationEntryManager.getActiveNotificationsCount(), + isVisible, + NotificationLogger.getNotificationLocation(entry))); + } + + /** + * Returns the scrim drawn behind the bubble stack. This is managed by {@link ScrimController} + * since we want the scrim's appearance and behavior to be identical to that of the notification + * shade scrim. + */ + public ScrimView getScrimForBubble() { + return mBubbleScrim; + } + + /** + * We intercept notification entries (including group summaries) dismissed by the user when + * there is an active bubble associated with it. We do this so that developers can still + * cancel it (and hence the bubbles associated with it). + * + * @return true if we want to intercept the dismissal of the entry, else false. + * @see Bubbles#handleDismissalInterception(BubbleEntry, List, IntConsumer) + */ + public boolean handleDismissalInterception(NotificationEntry entry) { + if (entry == null) { + return false; + } + + List<NotificationEntry> children = entry.getAttachedNotifChildren(); + List<BubbleEntry> bubbleChildren = null; + if (children != null) { + bubbleChildren = new ArrayList<>(); + for (int i = 0; i < children.size(); i++) { + bubbleChildren.add(notifToBubbleEntry(children.get(i))); + } + } + + return mBubbles.handleDismissalInterception(notifToBubbleEntry(entry), bubbleChildren, + // TODO : b/171847985 should re-work on notification side to make this more clear. + (int i) -> { + if (i >= 0) { + for (NotifCallback cb : mCallbacks) { + cb.removeNotification(children.get(i), + getDismissedByUserStats(children.get(i), true), + REASON_GROUP_SUMMARY_CANCELED); + } + } else { + mNotificationGroupManager.onEntryRemoved(entry); + } + }); + } + + /** + * Request the stack expand if needed, then select the specified Bubble as current. + * If no bubble exists for this entry, one is created. + * + * @param entry the notification for the bubble to be selected + */ + public void expandStackAndSelectBubble(NotificationEntry entry) { + mBubbles.expandStackAndSelectBubble(notifToBubbleEntry(entry)); + } + + /** See {@link NotifCallback}. */ + public void addNotifCallback(NotifCallback callback) { + mCallbacks.add(callback); + } + + /** + * When a notification is marked Priority, expand the stack if needed, + * then (maybe create and) select the given bubble. + * + * @param entry the notification for the bubble to show + */ + public void onUserChangedImportance(NotificationEntry entry) { + try { + int flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; + flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE; + mBarService.onNotificationBubbleChanged(entry.getKey(), true, flags); + } catch (RemoteException e) { + Log.e(TAG, e.getMessage()); + } + mShadeController.collapsePanel(true); + if (entry.getRow() != null) { + entry.getRow().updateBubbleButton(); + } + } + + /** + * Called when a user has indicated that an active notification should be shown as a bubble. + * <p> + * This method will collapse the shade, create the bubble without a flyout or dot, and suppress + * the notification from appearing in the shade. + * + * @param entry the notification to change bubble state for. + * @param shouldBubble whether the notification should show as a bubble or not. + */ + public void onUserChangedBubble(@NonNull final NotificationEntry entry, boolean shouldBubble) { + NotificationChannel channel = entry.getChannel(); + final String appPkg = entry.getSbn().getPackageName(); + final int appUid = entry.getSbn().getUid(); + if (channel == null || appPkg == null) { + return; + } + + // Update the state in NotificationManagerService + try { + int flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; + flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE; + mBarService.onNotificationBubbleChanged(entry.getKey(), shouldBubble, flags); + } catch (RemoteException e) { + } + + // Change the settings + channel = NotificationChannelHelper.createConversationChannelIfNeeded(mContext, + mNotificationManager, entry, channel); + channel.setAllowBubbles(shouldBubble); + try { + int currentPref = mNotificationManager.getBubblePreferenceForPackage(appPkg, appUid); + if (shouldBubble && currentPref == BUBBLE_PREFERENCE_NONE) { + mNotificationManager.setBubblesAllowed(appPkg, appUid, BUBBLE_PREFERENCE_SELECTED); + } + mNotificationManager.updateNotificationChannelForPackage(appPkg, appUid, channel); + } catch (RemoteException e) { + Log.e(TAG, e.getMessage()); + } + + if (shouldBubble) { + mShadeController.collapsePanel(true); + if (entry.getRow() != null) { + entry.getRow().updateBubbleButton(); + } + } + } + + @Override + public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { + mBubbles.dump(fd, pw, args); + } + + static BubbleEntry notifToBubbleEntry(NotificationEntry e) { + return new BubbleEntry(e.getSbn(), e.getRanking(), e.isClearable(), + e.shouldSuppressNotificationDot(), e.shouldSuppressNotificationList(), + e.shouldSuppressPeek()); + } + + /** + * Callback for when the BubbleController wants to interact with the notification pipeline to: + * - Remove a previously bubbled notification + * - Update the notification shade since bubbled notification should/shouldn't be showing + */ + public interface NotifCallback { + /** + * Called when a bubbled notification that was hidden from the shade is now being removed + * This can happen when an app cancels a bubbled notification or when the user dismisses a + * bubble. + */ + void removeNotification(@NonNull NotificationEntry entry, + @NonNull DismissedByUserStats stats, int reason); + + /** + * Called when a bubbled notification has changed whether it should be + * filtered from the shade. + */ + void invalidateNotifications(@NonNull String reason); + + /** + * Called on a bubbled entry that has been removed when there are no longer + * bubbled entries in its group. + * + * Checks whether its group has any other (non-bubbled) children. If it doesn't, + * removes all remnants of the group's summary from the notification pipeline. + * TODO: (b/145659174) Only old pipeline needs this - delete post-migration. + */ + void maybeCancelSummary(@NonNull NotificationEntry entry); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java index f55445ca1de3..a59c87652632 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java @@ -17,16 +17,15 @@ package com.android.systemui.wmshell; import android.content.Context; -import android.os.Handler; -import android.view.LayoutInflater; -import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.WMSingleton; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipBoundsHandler; import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipUiEventLogger; @@ -46,52 +45,61 @@ import dagger.Provides; */ @Module public abstract class TvPipModule { - - @SysUISingleton + @WMSingleton @Provides - static Pip providePipController(Context context, + static Optional<Pip> providePip( + Context context, + PipBoundsState pipBoundsState, PipBoundsHandler pipBoundsHandler, PipTaskOrganizer pipTaskOrganizer, + PipMediaController pipMediaController, + PipNotification pipNotification, WindowManagerShellWrapper windowManagerShellWrapper) { - return new PipController(context, pipBoundsHandler, pipTaskOrganizer, - windowManagerShellWrapper); + return Optional.of( + new PipController( + context, + pipBoundsState, + pipBoundsHandler, + pipTaskOrganizer, + pipMediaController, + pipNotification, + windowManagerShellWrapper)); } - @SysUISingleton + @WMSingleton @Provides - static PipControlsViewController providePipControlsViewContrller( - PipControlsView pipControlsView, PipController pipController, - LayoutInflater layoutInflater, Handler handler) { - return new PipControlsViewController(pipControlsView, pipController, layoutInflater, - handler); + static PipControlsViewController providePipControlsViewController( + PipControlsView pipControlsView, PipController pipController) { + return new PipControlsViewController(pipControlsView, pipController); } - @SysUISingleton + @WMSingleton @Provides static PipControlsView providePipControlsView(Context context) { return new PipControlsView(context, null); } - @SysUISingleton + @WMSingleton @Provides static PipNotification providePipNotification(Context context, - PipController pipController) { - return new PipNotification(context, pipController); + PipMediaController pipMediaController) { + return new PipNotification(context, pipMediaController); } - @SysUISingleton + @WMSingleton @Provides - static PipBoundsHandler providePipBoundsHandler(Context context) { - return new PipBoundsHandler(context); + static PipBoundsHandler providePipBoundsHandler(Context context, + PipBoundsState pipBoundsState) { + return new PipBoundsHandler(context, pipBoundsState); } - @SysUISingleton + @WMSingleton @Provides static PipBoundsState providePipBoundsState() { return new PipBoundsState(); } - @SysUISingleton + @WMSingleton @Provides static PipTaskOrganizer providePipTaskOrganizer(Context context, PipBoundsState pipBoundsState, @@ -100,7 +108,7 @@ public abstract class TvPipModule { Optional<SplitScreen> splitScreenOptional, DisplayController displayController, PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer) { return new PipTaskOrganizer(context, pipBoundsState, pipBoundsHandler, - pipSurfaceTransactionHelper, splitScreenOptional, displayController, - pipUiEventLogger, shellTaskOrganizer); + null /* menuActivityController */, pipSurfaceTransactionHelper, splitScreenOptional, + displayController, pipUiEventLogger, shellTaskOrganizer); } } diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/TvWMShellModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/TvWMShellModule.java index 56efffc29d85..294c749a2abe 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/TvWMShellModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/TvWMShellModule.java @@ -20,7 +20,7 @@ import android.content.Context; import android.os.Handler; import android.view.IWindowManager; -import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.WMSingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; @@ -40,10 +40,9 @@ import dagger.Provides; * Provides dependencies from {@link com.android.wm.shell} which could be customized among different * branches of SystemUI. */ -// TODO(b/162923491): Move most of these dependencies into WMSingleton scope. @Module(includes = {WMShellBaseModule.class, TvPipModule.class}) public class TvWMShellModule { - @SysUISingleton + @WMSingleton @Provides static DisplayImeController provideDisplayImeController(IWindowManager wmService, DisplayController displayController, @Main Executor mainExecutor, @@ -52,6 +51,8 @@ public class TvWMShellModule { transactionPool); } + @WMSingleton + @Provides static SplitScreen provideSplitScreen(Context context, DisplayController displayController, SystemWindows systemWindows, DisplayImeController displayImeController, @Main Handler handler, diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index 9281a090fd97..f896891c5039 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -64,15 +64,14 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.UserInfoController; import com.android.systemui.tracing.ProtoTracer; import com.android.systemui.tracing.nano.SystemUiTraceProto; -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.ShellDump; import com.android.wm.shell.nano.WmShellTraceProto; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedEvents; import com.android.wm.shell.onehanded.OneHandedGestureHandler.OneHandedGestureEventCallback; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; import com.android.wm.shell.pip.Pip; -import com.android.wm.shell.pip.phone.PipUtils; +import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogImpl; import com.android.wm.shell.splitscreen.SplitScreen; @@ -101,7 +100,6 @@ public final class WMShell extends SystemUI private final CommandQueue mCommandQueue; private final ConfigurationController mConfigurationController; - private final DisplayImeController mDisplayImeController; private final InputConsumerController mInputConsumerController; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final TaskStackChangeListeners mTaskStackChangeListeners; @@ -111,10 +109,9 @@ public final class WMShell extends SystemUI private final Optional<Pip> mPipOptional; private final Optional<SplitScreen> mSplitScreenOptional; private final Optional<OneHanded> mOneHandedOptional; - // Inject the organizer directly in case the optionals aren't loaded to depend on it. There - // are non-optional windowing features like FULLSCREEN. - private final ShellTaskOrganizer mShellTaskOrganizer; private final ProtoTracer mProtoTracer; + private final Optional<ShellDump> mShellDump; + private boolean mIsSysUiStateValid; private KeyguardUpdateMonitorCallback mSplitScreenKeyguardCallback; private KeyguardUpdateMonitorCallback mPipKeyguardCallback; @@ -126,40 +123,34 @@ public final class WMShell extends SystemUI InputConsumerController inputConsumerController, KeyguardUpdateMonitor keyguardUpdateMonitor, TaskStackChangeListeners taskStackChangeListeners, - DisplayImeController displayImeController, NavigationModeController navigationModeController, ScreenLifecycle screenLifecycle, SysUiState sysUiState, Optional<Pip> pipOptional, Optional<SplitScreen> splitScreenOptional, Optional<OneHanded> oneHandedOptional, - ShellTaskOrganizer shellTaskOrganizer, - ProtoTracer protoTracer) { + ProtoTracer protoTracer, + Optional<ShellDump> shellDump) { super(context); mCommandQueue = commandQueue; mConfigurationController = configurationController; mInputConsumerController = inputConsumerController; mKeyguardUpdateMonitor = keyguardUpdateMonitor; mTaskStackChangeListeners = taskStackChangeListeners; - mDisplayImeController = displayImeController; mNavigationModeController = navigationModeController; mScreenLifecycle = screenLifecycle; mSysUiState = sysUiState; mPipOptional = pipOptional; mSplitScreenOptional = splitScreenOptional; mOneHandedOptional = oneHandedOptional; - mShellTaskOrganizer = shellTaskOrganizer; mProtoTracer = protoTracer; mProtoTracer.add(this); + mShellDump = shellDump; } @Override public void start() { mCommandQueue.addCallback(this); - // This is to prevent circular init problem by separating registration step out of its - // constructor. And make sure the initialization of DisplayImeController won't depend on - // specific feature anymore. - mDisplayImeController.startMonitorDisplays(); mPipOptional.ifPresent(this::initPip); mSplitScreenOptional.ifPresent(this::initSplitScreen); mOneHandedOptional.ifPresent(this::initOneHanded); @@ -167,9 +158,6 @@ public final class WMShell extends SystemUI @VisibleForTesting void initPip(Pip pip) { - if (!PipUtils.hasSystemFeature(mContext)) { - return; - } mCommandQueue.addCallback(new CommandQueue.Callbacks() { @Override public void showPictureInPictureMenu() { @@ -204,6 +192,7 @@ public final class WMShell extends SystemUI } }); + // TODO: Move this into the shell // Handle for system task stack changes. mTaskStackChangeListeners.registerTaskStackListener( new TaskStackChangeListener() { @@ -222,8 +211,7 @@ public final class WMShell extends SystemUI @Override public void onActivityUnpinned() { final Pair<ComponentName, Integer> topPipActivityInfo = - PipUtils.getTopPipActivity( - mContext, ActivityManager.getService()); + PipUtils.getTopPipActivity(mContext); final ComponentName topActivity = topPipActivityInfo.first; pip.onActivityUnpinned(topActivity); mInputConsumerController.unregisterInputConsumer(); @@ -420,7 +408,7 @@ public final class WMShell extends SystemUI return; } // Dump WMShell stuff here if no commands were handled - mOneHandedOptional.ifPresent(oneHanded -> oneHanded.dump(pw)); + mShellDump.ifPresent((shellDump) -> shellDump.dump(pw)); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java index 09678b5d1772..bdca503f40c1 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java @@ -18,34 +18,40 @@ package com.android.systemui.wmshell; import android.app.IActivityManager; import android.content.Context; +import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.os.Handler; -import android.util.DisplayMetrics; import android.view.IWindowManager; +import android.view.WindowManager; import com.android.internal.logging.UiEventLogger; -import com.android.systemui.bubbles.Bubbles; -import com.android.systemui.dagger.SysUISingleton; +import com.android.internal.statusbar.IStatusBarService; +import com.android.systemui.dagger.WMSingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.shared.system.InputConsumerController; -import com.android.systemui.util.DeviceConfigProxy; +import com.android.wm.shell.ShellDump; +import com.android.wm.shell.ShellInit; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; -import com.android.wm.shell.animation.FlingAnimationUtils; +import com.android.wm.shell.bubbles.BubbleController; +import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.common.AnimationThread; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.HandlerExecutor; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.pip.phone.PipAppOpsListener; -import com.android.wm.shell.pip.phone.PipMediaController; import com.android.wm.shell.pip.phone.PipTouchHandler; import com.android.wm.shell.splitscreen.SplitScreen; @@ -59,41 +65,74 @@ import dagger.Provides; * Provides basic dependencies from {@link com.android.wm.shell}, the dependencies declared here * should be shared among different branches of SystemUI. */ -// TODO(b/162923491): Move most of these dependencies into WMSingleton scope. @Module public abstract class WMShellBaseModule { - @SysUISingleton + + @WMSingleton + @Provides + static ShellInit provideShellInit(DisplayImeController displayImeController, + DragAndDropController dragAndDropController, + ShellTaskOrganizer shellTaskOrganizer, + Optional<SplitScreen> splitScreenOptional) { + return new ShellInit(displayImeController, + dragAndDropController, + shellTaskOrganizer, + splitScreenOptional); + } + + /** + * Note, this is only optional because we currently pass this to the SysUI component scope and + * for non-primary users, we may inject a null-optional for that dependency. + */ + @WMSingleton + @Provides + static Optional<ShellDump> provideShellDump(ShellTaskOrganizer shellTaskOrganizer, + Optional<SplitScreen> splitScreenOptional, + Optional<Pip> pipOptional, + Optional<OneHanded> oneHandedOptional) { + return Optional.of(new ShellDump(shellTaskOrganizer, splitScreenOptional, pipOptional, + oneHandedOptional)); + } + + @WMSingleton @Provides static TransactionPool provideTransactionPool() { return new TransactionPool(); } - @SysUISingleton + @WMSingleton @Provides static DisplayController provideDisplayController(Context context, @Main Handler handler, IWindowManager wmService) { return new DisplayController(context, handler, wmService); } - @SysUISingleton + @WMSingleton @Provides - static DeviceConfigProxy provideDeviceConfigProxy() { - return new DeviceConfigProxy(); + static DragAndDropController provideDragAndDropController(Context context, + DisplayController displayController) { + return new DragAndDropController(context, displayController); } - @SysUISingleton + @WMSingleton @Provides static InputConsumerController provideInputConsumerController() { return InputConsumerController.getPipInputConsumer(); } - @SysUISingleton + @WMSingleton @Provides static FloatingContentCoordinator provideFloatingContentCoordinator() { return new FloatingContentCoordinator(); } - @SysUISingleton + @WMSingleton + @Provides + static WindowManagerShellWrapper provideWindowManagerShellWrapper() { + return new WindowManagerShellWrapper(); + } + + @WMSingleton @Provides static PipAppOpsListener providePipAppOpsListener(Context context, IActivityManager activityManager, @@ -101,76 +140,77 @@ public abstract class WMShellBaseModule { return new PipAppOpsListener(context, activityManager, pipTouchHandler.getMotionHelper()); } - @SysUISingleton + @WMSingleton @Provides - static PipMediaController providePipMediaController(Context context, - IActivityManager activityManager) { - return new PipMediaController(context, activityManager); + static PipMediaController providePipMediaController(Context context) { + return new PipMediaController(context); } - @SysUISingleton + @WMSingleton @Provides static PipUiEventLogger providePipUiEventLogger(UiEventLogger uiEventLogger, PackageManager packageManager) { return new PipUiEventLogger(uiEventLogger, packageManager); } - @SysUISingleton + @WMSingleton @Provides - static PipSurfaceTransactionHelper providesPipSurfaceTransactionHelper(Context context) { + static PipSurfaceTransactionHelper providePipSurfaceTransactionHelper(Context context) { return new PipSurfaceTransactionHelper(context); } - @SysUISingleton + @WMSingleton @Provides static SystemWindows provideSystemWindows(DisplayController displayController, IWindowManager wmService) { return new SystemWindows(displayController, wmService); } - @SysUISingleton + @WMSingleton @Provides static SyncTransactionQueue provideSyncTransactionQueue(@Main Handler handler, TransactionPool pool) { return new SyncTransactionQueue(pool, handler); } - @SysUISingleton + @WMSingleton @Provides static ShellTaskOrganizer provideShellTaskOrganizer(SyncTransactionQueue syncQueue, - @Main Handler handler, TransactionPool transactionPool) { - ShellTaskOrganizer organizer = new ShellTaskOrganizer(syncQueue, transactionPool, - new HandlerExecutor(handler), AnimationThread.instance().getExecutor()); - organizer.registerOrganizer(); - return organizer; - } - - @SysUISingleton - @Provides - static WindowManagerShellWrapper provideWindowManagerShellWrapper() { - return new WindowManagerShellWrapper(); - } - - @SysUISingleton - @Provides - static FlingAnimationUtils.Builder provideFlingAnimationUtilsBuilder( - DisplayMetrics displayMetrics) { - return new FlingAnimationUtils.Builder(displayMetrics); + ShellExecutor mainExecutor, TransactionPool transactionPool) { + return new ShellTaskOrganizer(syncQueue, transactionPool, + mainExecutor, AnimationThread.instance().getExecutor()); } @BindsOptionalOf - abstract Pip optionalPip(); - - @BindsOptionalOf abstract SplitScreen optionalSplitScreen(); - @BindsOptionalOf - abstract Bubbles optionalBubbles(); + @WMSingleton + @Provides + static Optional<Bubbles> provideBubbles(Context context, + FloatingContentCoordinator floatingContentCoordinator, + IStatusBarService statusBarService, + WindowManager windowManager, + WindowManagerShellWrapper windowManagerShellWrapper, + LauncherApps launcherApps, + UiEventLogger uiEventLogger, + @Main Handler mainHandler, + ShellTaskOrganizer organizer) { + return Optional.of(BubbleController.create(context, null /* synchronizer */, + floatingContentCoordinator, statusBarService, windowManager, + windowManagerShellWrapper, launcherApps, uiEventLogger, mainHandler, organizer)); + } - @SysUISingleton + @WMSingleton @Provides static Optional<OneHanded> provideOneHandedController(Context context, DisplayController displayController) { return Optional.ofNullable(OneHandedController.create(context, displayController)); } + + @WMSingleton + @Provides + static ShellExecutor provideMainShellExecutor(@Main Handler handler) { + return new HandlerExecutor(handler); + } + } diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java index 975757a4c259..0f8fb7bbd0ec 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java @@ -20,25 +20,26 @@ import android.content.Context; import android.os.Handler; import android.view.IWindowManager; -import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.WMSingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipBoundsHandler; import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.pip.phone.PipAppOpsListener; import com.android.wm.shell.pip.phone.PipController; -import com.android.wm.shell.pip.phone.PipMediaController; import com.android.wm.shell.pip.phone.PipMenuActivityController; import com.android.wm.shell.pip.phone.PipTouchHandler; import com.android.wm.shell.splitscreen.SplitScreen; @@ -54,10 +55,9 @@ import dagger.Provides; * Provides dependencies from {@link com.android.wm.shell} which could be customized among different * branches of SystemUI. */ -// TODO(b/162923491): Move most of these dependencies into WMSingleton scope. @Module(includes = WMShellBaseModule.class) public class WMShellModule { - @SysUISingleton + @WMSingleton @Provides static DisplayImeController provideDisplayImeController(IWindowManager wmService, DisplayController displayController, @Main Executor mainExecutor, @@ -66,25 +66,7 @@ public class WMShellModule { transactionPool); } - @SysUISingleton - @Provides - static Pip providePipController(Context context, - DisplayController displayController, - PipAppOpsListener pipAppOpsListener, - PipBoundsHandler pipBoundsHandler, - PipBoundsState pipBoundsState, - PipMediaController pipMediaController, - PipMenuActivityController pipMenuActivityController, - PipTaskOrganizer pipTaskOrganizer, - PipTouchHandler pipTouchHandler, - WindowManagerShellWrapper windowManagerShellWrapper) { - return new PipController(context, displayController, - pipAppOpsListener, pipBoundsHandler, pipBoundsState, pipMediaController, - pipMenuActivityController, pipTaskOrganizer, pipTouchHandler, - windowManagerShellWrapper); - } - - @SysUISingleton + @WMSingleton @Provides static SplitScreen provideSplitScreen(Context context, DisplayController displayController, SystemWindows systemWindows, @@ -95,28 +77,43 @@ public class WMShellModule { displayImeController, handler, transactionPool, shellTaskOrganizer, syncQueue); } - @SysUISingleton + @WMSingleton + @Provides + static Optional<Pip> providePip(Context context, DisplayController displayController, + PipAppOpsListener pipAppOpsListener, PipBoundsHandler pipBoundsHandler, + PipBoundsState pipBoundsState, PipMediaController pipMediaController, + PipMenuActivityController pipMenuActivityController, PipTaskOrganizer pipTaskOrganizer, + PipTouchHandler pipTouchHandler, WindowManagerShellWrapper windowManagerShellWrapper, + ShellExecutor mainExecutor) { + return Optional.ofNullable(PipController.create(context, displayController, + pipAppOpsListener, pipBoundsHandler, pipBoundsState, pipMediaController, + pipMenuActivityController, pipTaskOrganizer, pipTouchHandler, + windowManagerShellWrapper, mainExecutor)); + } + + @WMSingleton @Provides static PipBoundsState providePipBoundsState() { return new PipBoundsState(); } - @SysUISingleton + @WMSingleton @Provides - static PipBoundsHandler providesPipBoundsHandler(Context context) { - return new PipBoundsHandler(context); + static PipBoundsHandler providesPipBoundsHandler(Context context, + PipBoundsState pipBoundsState) { + return new PipBoundsHandler(context, pipBoundsState); } - @SysUISingleton + @WMSingleton @Provides static PipMenuActivityController providesPipMenuActivityController(Context context, - PipMediaController pipMediaController, PipTaskOrganizer pipTaskOrganizer) { - return new PipMenuActivityController(context, pipMediaController, pipTaskOrganizer); + PipMediaController pipMediaController, SystemWindows systemWindows) { + return new PipMenuActivityController(context, pipMediaController, systemWindows); } - @SysUISingleton + @WMSingleton @Provides - static PipTouchHandler providesPipTouchHandler(Context context, + static PipTouchHandler providePipTouchHandler(Context context, PipMenuActivityController menuActivityController, PipBoundsHandler pipBoundsHandler, PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer, @@ -126,16 +123,17 @@ public class WMShellModule { pipBoundsState, pipTaskOrganizer, floatingContentCoordinator, pipUiEventLogger); } - @SysUISingleton + @WMSingleton @Provides - static PipTaskOrganizer providesPipTaskOrganizer(Context context, + static PipTaskOrganizer providePipTaskOrganizer(Context context, PipBoundsState pipBoundsState, PipBoundsHandler pipBoundsHandler, + PipMenuActivityController menuActivityController, PipSurfaceTransactionHelper pipSurfaceTransactionHelper, Optional<SplitScreen> splitScreenOptional, DisplayController displayController, PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer) { return new PipTaskOrganizer(context, pipBoundsState, pipBoundsHandler, - pipSurfaceTransactionHelper, splitScreenOptional, displayController, - pipUiEventLogger, shellTaskOrganizer); + menuActivityController, pipSurfaceTransactionHelper, splitScreenOptional, + displayController, pipUiEventLogger, shellTaskOrganizer); } } |