diff --git a/packages/SystemUI/res/drawable/system_animation_ongoing_dot.xml b/packages/SystemUI/res/drawable/system_animation_ongoing_dot.xml new file mode 100644 index 0000000000000..4e9d380af3195 --- /dev/null +++ b/packages/SystemUI/res/drawable/system_animation_ongoing_dot.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/layout/rounded_corners.xml b/packages/SystemUI/res/layout/rounded_corners.xml index db892d78c5563..04fe9184cf60c 100644 --- a/packages/SystemUI/res/layout/rounded_corners.xml +++ b/packages/SystemUI/res/layout/rounded_corners.xml @@ -14,6 +14,8 @@ ** See the License for the specific language governing permissions and ** limitations under the License. --> + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/layout/rounded_corners_bottom.xml b/packages/SystemUI/res/layout/rounded_corners_bottom.xml index dde1248356e0d..720e47b1908c1 100644 --- a/packages/SystemUI/res/layout/rounded_corners_bottom.xml +++ b/packages/SystemUI/res/layout/rounded_corners_bottom.xml @@ -26,6 +26,24 @@ android:layout_gravity="left|bottom" android:tint="#ff000000" android:src="@drawable/rounded_corner_bottom"/> + + + + + + + + + diff --git a/packages/SystemUI/res/layout/rounded_corners_top.xml b/packages/SystemUI/res/layout/rounded_corners_top.xml index 813c97d06f579..6abe406e0ea6b 100644 --- a/packages/SystemUI/res/layout/rounded_corners_top.xml +++ b/packages/SystemUI/res/layout/rounded_corners_top.xml @@ -26,6 +26,24 @@ android:layout_gravity="left|top" android:tint="#ff000000" android:src="@drawable/rounded_corner_top"/> + + + + + + + + + + + + diff --git a/packages/SystemUI/res/layout/system_event_animation_window.xml b/packages/SystemUI/res/layout/system_event_animation_window.xml new file mode 100644 index 0000000000000..c92dec9dd6439 --- /dev/null +++ b/packages/SystemUI/res/layout/system_event_animation_window.xml @@ -0,0 +1,35 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index a686fc086b401..cbfdce5d0c690 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -79,6 +79,8 @@ import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.NotificationViewHierarchyManager; import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.VibratorHelper; +import com.android.systemui.statusbar.events.PrivacyDotViewController; +import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.NotificationEntryManager.KeyguardEnvironment; import com.android.systemui.statusbar.notification.NotificationFilter; @@ -352,6 +354,8 @@ public class Dependency { @Inject Lazy mDeviceConfigProxy; @Inject Lazy mNavbarButtonsControllerLazy; @Inject Lazy mTelephonyListenerManager; + @Inject Lazy mSystemStatusAnimationSchedulerLazy; + @Inject Lazy mPrivacyDotViewControllerLazy; @Inject public Dependency() { @@ -561,6 +565,10 @@ public class Dependency { mProviders.put(NavigationBarOverlayController.class, mNavbarButtonsControllerLazy::get); + mProviders.put(SystemStatusAnimationScheduler.class, + mSystemStatusAnimationSchedulerLazy::get); + mProviders.put(PrivacyDotViewController.class, mPrivacyDotViewControllerLazy::get); + Dependency.setInstance(this); } diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index 5fa98bccfc690..d07723e475fbf 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -88,6 +88,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.qs.SecureSetting; import com.android.systemui.settings.UserTracker; +import com.android.systemui.statusbar.events.PrivacyDotViewController; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; import com.android.systemui.util.settings.SecureSettings; @@ -126,6 +127,7 @@ public class ScreenDecorations extends SystemUI implements Tunable { private DisplayManager.DisplayListener mDisplayListener; private CameraAvailabilityListener mCameraListener; private final UserTracker mUserTracker; + private final PrivacyDotViewController mDotViewController; //TODO: These are piecemeal being updated to Points for now to support non-square rounded // corners. for now it is only supposed when reading the intrinsic size from the drawables with @@ -140,6 +142,11 @@ public class ScreenDecorations extends SystemUI implements Tunable { protected View[] mOverlays; @Nullable private DisplayCutoutView[] mCutoutViews; + //TODO: + View mTopLeftDot; + View mTopRightDot; + View mBottomLeftDot; + View mBottomRightDot; private float mDensity; private WindowManager mWindowManager; private int mRotation; @@ -147,6 +154,8 @@ public class ScreenDecorations extends SystemUI implements Tunable { private Handler mHandler; private boolean mPendingRotationChange; private boolean mIsRoundedCornerMultipleRadius; + private int mStatusBarHeightPortrait; + private int mStatusBarHeightLandscape; private CameraAvailabilityListener.CameraTransitionCallback mCameraTransitionCallback = new CameraAvailabilityListener.CameraTransitionCallback() { @@ -205,13 +214,15 @@ public class ScreenDecorations extends SystemUI implements Tunable { SecureSettings secureSettings, BroadcastDispatcher broadcastDispatcher, TunerService tunerService, - UserTracker userTracker) { + UserTracker userTracker, + PrivacyDotViewController dotViewController) { super(context); mMainHandler = handler; mSecureSettings = secureSettings; mBroadcastDispatcher = broadcastDispatcher; mTunerService = tunerService; mUserTracker = userTracker; + mDotViewController = dotViewController; } @Override @@ -222,6 +233,7 @@ public class ScreenDecorations extends SystemUI implements Tunable { } mHandler = startHandlerThread(); mHandler.post(this::startOnScreenDecorationsThread); + mDotViewController.setUiExecutor(mHandler::post); } @VisibleForTesting @@ -286,6 +298,7 @@ public class ScreenDecorations extends SystemUI implements Tunable { private void setupDecorations() { if (hasRoundedCorners() || shouldDrawCutout()) { + updateStatusBarHeight(); final DisplayCutout cutout = getCutout(); final Rect[] bounds = cutout == null ? null : cutout.getBoundingRectsAll(); int rotatedPos; @@ -298,6 +311,10 @@ public class ScreenDecorations extends SystemUI implements Tunable { removeOverlay(i); } } + // Overlays have been created, send the dots to the controller + //TODO: need a better way to do this + mDotViewController.initialize( + mTopLeftDot, mTopRightDot, mBottomLeftDot, mBottomRightDot); } else { removeAllOverlays(); } @@ -431,14 +448,21 @@ public class ScreenDecorations extends SystemUI implements Tunable { private View overlayForPosition(@BoundsPosition int pos) { switch (pos) { case BOUNDS_POSITION_TOP: - return LayoutInflater.from(mContext) + case BOUNDS_POSITION_LEFT: + View top = LayoutInflater.from(mContext) .inflate(R.layout.rounded_corners_top, null); + mTopLeftDot = top.findViewById(R.id.privacy_dot_left_container); + mTopRightDot = top.findViewById(R.id.privacy_dot_right_container); + return top; case BOUNDS_POSITION_BOTTOM: - return LayoutInflater.from(mContext) + case BOUNDS_POSITION_RIGHT: + View bottom = LayoutInflater.from(mContext) .inflate(R.layout.rounded_corners_bottom, null); + mBottomLeftDot = bottom.findViewById(R.id.privacy_dot_left_container); + mBottomRightDot = bottom.findViewById(R.id.privacy_dot_right_container); + return bottom; default: - return LayoutInflater.from(mContext) - .inflate(R.layout.rounded_corners, null); + throw new IllegalArgumentException("Unknown bounds position"); } } @@ -575,6 +599,11 @@ public class ScreenDecorations extends SystemUI implements Tunable { View child; for (int j = 0; j < size; j++) { child = ((ViewGroup) mOverlays[i]).getChildAt(j); + if (child.getId() == R.id.privacy_dot_left_container + || child.getId() == R.id.privacy_dot_right_container) { + // Exclude privacy dot from color inversion (for now?) + continue; + } if (child instanceof ImageView) { ((ImageView) child).setImageTintList(tintList); } else if (child instanceof DisplayCutoutView) { @@ -611,10 +640,15 @@ public class ScreenDecorations extends SystemUI implements Tunable { Preconditions.checkState(mHandler.getLooper().getThread() == Thread.currentThread(), "must call on " + mHandler.getLooper().getThread() + ", but was " + Thread.currentThread()); + + int newRotation = mContext.getDisplay().getRotation(); + if (mRotation != newRotation) { + mDotViewController.updateRotation(newRotation); + } + if (mPendingRotationChange) { return; } - int newRotation = mContext.getDisplay().getRotation(); if (newRotation != mRotation) { mRotation = newRotation; @@ -630,6 +664,14 @@ public class ScreenDecorations extends SystemUI implements Tunable { } } + private void updateStatusBarHeight() { + mStatusBarHeightLandscape = mContext.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_height_landscape); + mStatusBarHeightPortrait = mContext.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_height_portrait); + mDotViewController.setStatusBarHeights(mStatusBarHeightPortrait, mStatusBarHeightLandscape); + } + private void updateRoundedCornerRadii() { // We should eventually move to just using the intrinsic size of the drawables since // they should be sized to the exact pixels they want to cover. Therefore I'm purposely not diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt new file mode 100644 index 0000000000000..5ab71bc62fe60 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2021 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.events + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ObjectAnimator +import android.annotation.UiThread +import android.util.Log +import android.view.Gravity +import android.view.View +import android.widget.FrameLayout + +import com.android.systemui.animation.Interpolators +import com.android.systemui.R +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main + +import java.lang.IllegalStateException +import java.util.concurrent.Executor +import javax.inject.Inject + +/** + * Understands how to keep the persistent privacy dot in the corner of the screen in + * ScreenDecorations, which does not rotate with the device. + * + * The basic principle here is that each dot will sit in a box that is equal to the margins of the + * status bar (specifically the status_bar_contents view in PhoneStatusBarView). Each dot container + * will have its gravity set towards the corner (i.e., top-right corner gets top|right gravity), and + * the contained ImageView will be set to center_vertical and away from the corner horizontally. The + * Views will match the status bar top padding and status bar height so that the dot can appear to + * reside directly after the status bar system contents (basically to the right of the battery). + * + * NOTE: any operation that modifies views directly must run on the provided executor, because + * these views are owned by ScreenDecorations and it runs in its own thread + */ + +@SysUISingleton +class PrivacyDotViewController @Inject constructor( + @Main val mainExecutor: Executor, + val animationScheduler: SystemStatusAnimationScheduler +) { + private var rotation = 0 + private var leftSize = 0 + private var rightSize = 0 + + private var sbHeightPortrait = 0 + private var sbHeightLandscape = 0 + + private var hasMultipleHeights = false + private var needsHeightUpdate = false + private var needsRotationUpdate = false + private var needsMarginUpdate = false + + private lateinit var tl: View + private lateinit var tr: View + private lateinit var bl: View + private lateinit var br: View + + // Track which corner is active (based on orientation + RTL) + private var designatedCorner: View? = null + + // Privacy dots are created in ScreenDecoration's UiThread, which is not the main thread + private var uiExecutor: Executor? = null + + private val views: Sequence + get() = if (!this::tl.isInitialized) sequenceOf() else sequenceOf(tl, tr, br, bl) + + fun setUiExecutor(e: Executor) { + uiExecutor = e + } + + @UiThread + fun updateRotation(rot: Int) { + if (rot == rotation) { + return + } + + // A rotation has started, hide the views to avoid flicker + setCornerVisibilities(View.INVISIBLE) + + if (hasMultipleHeights && (rotation % 2) != (rot % 2)) { + // we've changed from vertical to horizontal; update status bar height + needsHeightUpdate = true + } + + rotation = rot + needsRotationUpdate = true + } + + @UiThread + private fun updateHeights(rot: Int) { + val height = when (rot) { + 0, 2 -> sbHeightPortrait + 1, 3 -> sbHeightLandscape + else -> 0 + } + + views.forEach { it.layoutParams.height = height } + } + + // Update the gravity and margins of the privacy views + @UiThread + private fun updateRotations() { + // To keep a view in the corner, its gravity is always the description of its current corner + // Therefore, just figure out which view is in which corner. This turns out to be something + // like (myCorner - rot) mod 4, where topLeft = 0, topRight = 1, etc. and portrait = 0, and + // rotating the device counter-clockwise increments rotation by 1 + + views.forEach { corner -> + val rotatedCorner = rotatedCorner(cornerForView(corner)) + (corner.layoutParams as FrameLayout.LayoutParams).apply { + gravity = rotatedCorner.toGravity() + } + + // Set the dot's view gravity to hug the status bar + (corner.findViewById(R.id.privacy_dot) + .layoutParams as FrameLayout.LayoutParams) + .gravity = rotatedCorner.innerGravity() + } + } + + @UiThread + private fun updateCornerSizes() { + views.forEach { corner -> + val rotatedCorner = rotatedCorner(cornerForView(corner)) + val w = widthForCorner(rotatedCorner) + Log.d(TAG, "updateCornerSizes: setting (${cornerForView(corner)}) to $w") + (corner.layoutParams as FrameLayout.LayoutParams).width = w + corner.requestLayout() + } + } + + // Designated view will be the one at statusbar's view.END + @UiThread + private fun selectDesignatedCorner(): View? { + if (!this::tl.isInitialized) { + return null + } + + val isRtl = tl.isLayoutRtl + + return when (rotation) { + 0 -> if (isRtl) tl else tr + 1 -> if (isRtl) tr else br + 2 -> if (isRtl) br else bl + 3 -> if (isRtl) bl else tl + else -> throw IllegalStateException("unknown rotation") + } + } + + // Track the current designated corner and maybe animate to a new rotation + @UiThread + private fun updateDesignatedCorner(newCorner: View) { + designatedCorner = newCorner + + if (animationScheduler.hasPersistentDot) { + designatedCorner!!.visibility = View.VISIBLE + designatedCorner!!.alpha = 0f + designatedCorner!!.animate() + .alpha(1.0f) + .setDuration(300) + .start() + } + } + + @UiThread + private fun setCornerVisibilities(vis: Int) { + views.forEach { corner -> + corner.visibility = vis + } + } + + private fun cornerForView(v: View): Int { + return when (v) { + tl -> TOP_LEFT + tr -> TOP_RIGHT + bl -> BOTTOM_LEFT + br -> BOTTOM_RIGHT + else -> throw IllegalArgumentException("not a corner view") + } + } + + private fun rotatedCorner(corner: Int): Int { + var modded = corner - rotation + if (modded < 0) { + modded += 4 + } + + return modded + } + + private fun widthForCorner(corner: Int): Int { + return when (corner) { + TOP_LEFT, BOTTOM_LEFT -> leftSize + TOP_RIGHT, BOTTOM_RIGHT -> rightSize + else -> throw IllegalArgumentException("Unknown corner") + } + } + + fun initialize(topLeft: View, topRight: View, bottomLeft: View, bottomRight: View) { + if (this::tl.isInitialized && this::tr.isInitialized && + this::bl.isInitialized && this::br.isInitialized) { + if (tl == topLeft && tr == topRight && bl == bottomLeft && br == bottomRight) { + return + } + } + + tl = topLeft + tr = topRight + bl = bottomLeft + br = bottomRight + + designatedCorner = selectDesignatedCorner() + mainExecutor.execute { + animationScheduler.addCallback(systemStatusAnimationCallback) + } + } + + /** + * Set the status bar height in portrait and landscape, in pixels. If they are the same you can + * pass the same value twice + */ + fun setStatusBarHeights(portrait: Int, landscape: Int) { + sbHeightPortrait = portrait + sbHeightLandscape = landscape + + hasMultipleHeights = portrait != landscape + } + + /** + * The dot view containers will fill the margin in order to position the dots correctly + * + * @param left the space between the status bar contents and the left side of the screen + * @param right space between the status bar contents and the right side of the screen + */ + fun setStatusBarMargins(left: Int, right: Int) { + leftSize = left + rightSize = right + + needsMarginUpdate = true + + // Margins come after PhoneStatusBarView does a layout pass, and so will always happen + // after rotation changes. It is safe to execute the updates from here + uiExecutor?.execute { + doUpdates(needsRotationUpdate, needsHeightUpdate, needsMarginUpdate) + } + } + + private fun doUpdates(rot: Boolean, height: Boolean, width: Boolean) { + var newDesignatedCorner: View? = null + + if (rot) { + needsRotationUpdate = false + updateRotations() + newDesignatedCorner = selectDesignatedCorner() + } + + if (height) { + needsHeightUpdate = false + updateHeights(rotation) + } + + if (width) { + needsMarginUpdate = false + updateCornerSizes() + } + + if (newDesignatedCorner != null && newDesignatedCorner != designatedCorner) { + updateDesignatedCorner(newDesignatedCorner) + } + } + + private val systemStatusAnimationCallback: SystemStatusAnimationCallback = + object : SystemStatusAnimationCallback { + override fun onSystemStatusAnimationTransitionToPersistentDot(): Animator? { + if (designatedCorner == null) { + return null + } + + val alpha = ObjectAnimator.ofFloat( + designatedCorner, "alpha", 0f, 1f) + alpha.duration = DURATION + alpha.interpolator = Interpolators.ALPHA_OUT + alpha.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animator: Animator) { + uiExecutor?.execute { designatedCorner?.visibility = View.VISIBLE } + } + }) + return alpha + } + + override fun onHidePersistentDot(): Animator? { + if (designatedCorner == null) { + return null + } + + val alpha = ObjectAnimator.ofFloat( + designatedCorner, "alpha", 1f, 0f) + alpha.duration = DURATION + alpha.interpolator = Interpolators.ALPHA_OUT + alpha.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animator: Animator) { + uiExecutor?.execute { designatedCorner?.visibility = View.INVISIBLE } + } + }) + alpha.start() + return null + } + } +} + +const val TOP_LEFT = 0 +const val TOP_RIGHT = 1 +const val BOTTOM_RIGHT = 2 +const val BOTTOM_LEFT = 3 +private const val DURATION = 160L +private const val TAG = "PrivacyDotViewController" + +private fun Int.toGravity(): Int { + return when (this) { + TOP_LEFT -> Gravity.TOP or Gravity.LEFT + TOP_RIGHT -> Gravity.TOP or Gravity.RIGHT + BOTTOM_LEFT -> Gravity.BOTTOM or Gravity.LEFT + BOTTOM_RIGHT -> Gravity.BOTTOM or Gravity.RIGHT + else -> throw IllegalArgumentException("Not a corner") + } +} + +private fun Int.innerGravity(): Int { + return when (this) { + TOP_LEFT -> Gravity.CENTER_VERTICAL or Gravity.RIGHT + TOP_RIGHT -> Gravity.CENTER_VERTICAL or Gravity.LEFT + BOTTOM_LEFT -> Gravity.CENTER_VERTICAL or Gravity.RIGHT + BOTTOM_RIGHT -> Gravity.CENTER_VERTICAL or Gravity.LEFT + else -> throw IllegalArgumentException("Not a corner") + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt new file mode 100644 index 0000000000000..6380dc0b9b46c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2021 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.events + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.View +import android.widget.ImageView +import com.android.settingslib.graph.ThemedBatteryDrawable +import com.android.systemui.R +import com.android.systemui.privacy.OngoingPrivacyChip +import com.android.systemui.privacy.PrivacyItem + +interface StatusEvent { + val priority: Int + // Whether or not to force the status bar open and show a dot + val forceVisible: Boolean + val viewCreator: (context: Context) -> View +} + +class BatteryEvent : StatusEvent { + override val priority = 50 + override val forceVisible = false + + override val viewCreator: (context: Context) -> View = { context -> + val iv = ImageView(context) + iv.setImageDrawable(ThemedBatteryDrawable(context, Color.WHITE)) + iv.setBackgroundDrawable(ColorDrawable(Color.GREEN)) + iv + } + + override fun toString(): String { + return javaClass.simpleName + } +} +class PrivacyEvent : StatusEvent { + override val priority = 100 + override val forceVisible = true + var privacyItems: List = listOf() + + override val viewCreator: (context: Context) -> View = { context -> + val v = LayoutInflater.from(context) + .inflate(R.layout.ongoing_privacy_chip, null) as OngoingPrivacyChip + v.privacyList = privacyItems + v + } + + override fun toString(): String { + return javaClass.simpleName + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt new file mode 100644 index 0000000000000..620963060b496 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2021 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.events + +import android.animation.ValueAnimator +import android.content.Context +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.FrameLayout + +import com.android.systemui.R +import com.android.systemui.statusbar.SuperStatusBarViewFactory +import com.android.systemui.statusbar.phone.StatusBarWindowController +import com.android.systemui.statusbar.phone.StatusBarWindowView + +import javax.inject.Inject + +/** + * //TODO: this _probably_ doesn't control a window anymore + * Controls the window for system event animations. + */ +class SystemEventChipAnimationController @Inject constructor( + private val context: Context, + private val statusBarViewFactory: SuperStatusBarViewFactory, + private val statusBarWindowController: StatusBarWindowController +) : SystemStatusChipAnimationCallback { + var showPersistentDot = false + set(value) { + field = value + statusBarWindowController.setForceStatusBarVisible(value) + maybeUpdateShowDot() + } + + private lateinit var animationWindowView: FrameLayout + private lateinit var animationDotView: View + private lateinit var statusBarWindowView: StatusBarWindowView + private var currentAnimatedView: View? = null + + // TODO: move to dagger + private var initialized = false + + override fun onChipAnimationStart( + viewCreator: (context: Context) -> View, + @SystemAnimationState state: Int + ) { + if (!initialized) init() + + if (state == ANIMATING_IN) { + currentAnimatedView = viewCreator(context) + animationWindowView.addView(currentAnimatedView, layoutParamsDefault) + + // We are animating IN; chip comes in from View.END + currentAnimatedView?.apply { + translationX = width.toFloat() + alpha = 0f + visibility = View.VISIBLE + } + } else { + // We are animating away + currentAnimatedView?.apply { + translationX = 0f + alpha = 1f + } + } + } + + override fun onChipAnimationEnd(@SystemAnimationState state: Int) { + if (state == ANIMATING_IN) { + // Finished animating in + currentAnimatedView?.apply { + translationX = 0f + alpha = 1f + } + } else { + // Finished animating away + currentAnimatedView?.apply { + visibility = View.INVISIBLE + } + animationWindowView.removeView(currentAnimatedView) + } + } + + override fun onChipAnimationUpdate( + animator: ValueAnimator, + @SystemAnimationState state: Int + ) { + // Alpha is parameterized 0,1, and translation from (width, 0) + currentAnimatedView?.apply { + val amt = animator.animatedValue as Float + + alpha = amt + + val w = width + val translation = (1 - amt) * w + translationX = translation + } + } + + private fun maybeUpdateShowDot() { + if (!initialized) return + if (!showPersistentDot && currentAnimatedView == null) { + animationDotView.visibility = View.INVISIBLE + } + } + + private fun init() { + initialized = true + statusBarWindowView = statusBarViewFactory.statusBarWindowView + animationWindowView = LayoutInflater.from(context) + .inflate(R.layout.system_event_animation_window, null) as FrameLayout + animationDotView = animationWindowView.findViewById(R.id.dot_view) + val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + lp.gravity = Gravity.END or Gravity.CENTER_VERTICAL + statusBarWindowView.addView(animationWindowView, lp) + } + + private val layoutParamsDefault = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).also { + it.gravity = Gravity.END or Gravity.CENTER_VERTICAL + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt new file mode 100644 index 0000000000000..b4818233aea37 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2021 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.events + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.privacy.PrivacyItem +import com.android.systemui.privacy.PrivacyItemController +import com.android.systemui.statusbar.policy.BatteryController +import javax.inject.Inject + +/** + * Listens for system events (battery, privacy, connectivity) and allows listeners + * to show status bar animations when they happen + */ +@SysUISingleton +class SystemEventCoordinator @Inject constructor( + private val batteryController: BatteryController, + private val privacyController: PrivacyItemController +) { + private lateinit var scheduler: SystemStatusAnimationScheduler + + fun startObserving() { + /* currently unused + batteryController.addCallback(batteryStateListener) + */ + privacyController.addCallback(privacyStateListener) + } + + fun stopObserving() { + /* currently unused + batteryController.removeCallback(batteryStateListener) + */ + privacyController.removeCallback(privacyStateListener) + } + + fun attachScheduler(s: SystemStatusAnimationScheduler) { + this.scheduler = s + } + + fun notifyPluggedIn() { + scheduler.onStatusEvent(BatteryEvent()) + } + + fun notifyPrivacyItemsEmpty() { + scheduler.setShouldShowPersistentPrivacyIndicator(false) + } + + fun notifyPrivacyItemsChanged() { + val event = PrivacyEvent() + event.privacyItems = privacyStateListener.currentPrivacyItems + scheduler.onStatusEvent(event) + } + + private val batteryStateListener = object : BatteryController.BatteryStateChangeCallback { + var plugged = false + var stateKnown = false + override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) { + if (!stateKnown) { + stateKnown = true + plugged = pluggedIn + notifyListeners() + return + } + + if (plugged != pluggedIn) { + plugged = pluggedIn + notifyListeners() + } + } + + private fun notifyListeners() { + // We only care about the plugged in status + if (plugged) notifyPluggedIn() + } + } + + private val privacyStateListener = object : PrivacyItemController.Callback { + var currentPrivacyItems = listOf() + + override fun onPrivacyItemsChanged(privacyItems: List) { + if (privacyItems.isNotEmpty() && currentPrivacyItems.containsAll(privacyItems)) { + return + } + currentPrivacyItems = privacyItems + notifyListeners() + } + + private fun notifyListeners() { + if (currentPrivacyItems.isEmpty()) { + notifyPrivacyItemsEmpty() + } else { + notifyPrivacyItemsChanged() + } + } + } +} + +private const val TAG = "SystemEventCoordinator" \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt new file mode 100644 index 0000000000000..b85d0310092ce --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt @@ -0,0 +1,333 @@ +/* + * Copyright (C) 2021 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.events + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.annotation.IntDef +import android.content.Context +import android.os.Process +import android.util.Log +import android.view.View + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.statusbar.phone.StatusBarWindowController +import com.android.systemui.statusbar.policy.CallbackController +import com.android.systemui.util.Assert +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.time.SystemClock + +import javax.inject.Inject + +/** + * Dead-simple scheduler for system status events. Obeys the following principles (all values TBD): + * - Avoiding log spam by only allowing 12 events per minute (1event/5s) + * - Waits 100ms to schedule any event for debouncing/prioritization + * - Simple prioritization: Privacy > Battery > connectivity (encoded in StatusEvent) + * - Only schedules a single event, and throws away lowest priority events + * + * There are 4 basic stages of animation at play here: + * 1. System chrome animation OUT + * 2. Chip animation IN + * 3. Chip animation OUT; potentially into a dot + * 4. System chrome animation IN + * + * Thus we can keep all animations synchronized with two separate ValueAnimators, one for system + * chrome and the other for the chip. These can animate from 0,1 and listeners can parameterize + * their respective views based on the progress of the animator. Interpolation differences TBD + */ +@SysUISingleton +class SystemStatusAnimationScheduler @Inject constructor( + private val coordinator: SystemEventCoordinator, + private val chipAnimationController: SystemEventChipAnimationController, + private val statusBarWindowController: StatusBarWindowController, + private val systemClock: SystemClock, + @Main private val executor: DelayableExecutor +) : CallbackController { + + /** True from the time a scheduled event starts until it's animation finishes */ + var isActive = false + private set + + @SystemAnimationState var animationState: Int = IDLE + private set + + /** True if the persistent privacy dot should be active */ + var hasPersistentDot = false + private set + + private var scheduledEvent: StatusEvent? = null + private var cancelExecutionRunnable: Runnable? = null + private val listeners = mutableSetOf() + + init { + coordinator.attachScheduler(this) + } + + fun onStatusEvent(event: StatusEvent) { + // Ignore any updates until the system is up and running + if (isTooEarly()) { + return + } + + // Don't deal with threading for now (no need let's be honest) + Assert.isMainThread() + if (event.priority > scheduledEvent?.priority ?: -1) { + if (DEBUG) { + Log.d(TAG, "scheduling event $event") + } + scheduleEvent(event) + } else { + if (DEBUG) { + Log.d(TAG, "ignoring event $event") + } + } + } + + private fun clearDotIfVisible() { + notifyHidePersistentDot() + } + + fun setShouldShowPersistentPrivacyIndicator(should: Boolean) { + if (hasPersistentDot == should) { + return + } + + hasPersistentDot = should + + if (!hasPersistentDot) { + clearDotIfVisible() + } + } + + private fun isTooEarly(): Boolean { + Log.d(TAG, "time=> ${systemClock.uptimeMillis() - Process.getStartUptimeMillis()}") + return systemClock.uptimeMillis() - Process.getStartUptimeMillis() < MIN_UPTIME + } + + /** + * Clear the scheduled event (if any) and schedule a new one + */ + private fun scheduleEvent(event: StatusEvent) { + scheduledEvent = event + if (scheduledEvent!!.forceVisible) { + hasPersistentDot = true + } + + // Schedule the animation to start after a debounce period + cancelExecutionRunnable = executor.executeDelayed({ + cancelExecutionRunnable = null + animationState = ANIMATING_IN + statusBarWindowController.setForceStatusBarVisible(true) + + val entranceAnimator = ValueAnimator.ofFloat(1f, 0f) + entranceAnimator.duration = ENTRANCE_ANIM_LENGTH + entranceAnimator.addListener(systemAnimatorAdapter) + entranceAnimator.addUpdateListener(systemUpdateListener) + + val chipAnimator = ValueAnimator.ofFloat(0f, 1f) + chipAnimator.duration = CHIP_ANIM_LENGTH + chipAnimator.addListener( + ChipAnimatorAdapter(RUNNING_CHIP_ANIM, scheduledEvent!!.viewCreator)) + chipAnimator.addUpdateListener(chipUpdateListener) + + val aSet2 = AnimatorSet() + aSet2.playSequentially(entranceAnimator, chipAnimator) + aSet2.start() + + executor.executeDelayed({ + animationState = ANIMATING_OUT + + val systemAnimator = ValueAnimator.ofFloat(0f, 1f) + systemAnimator.duration = ENTRANCE_ANIM_LENGTH + systemAnimator.addListener(systemAnimatorAdapter) + systemAnimator.addUpdateListener(systemUpdateListener) + + val chipAnimator = ValueAnimator.ofFloat(1f, 0f) + chipAnimator.duration = CHIP_ANIM_LENGTH + chipAnimator.addListener(ChipAnimatorAdapter(IDLE, scheduledEvent!!.viewCreator)) + chipAnimator.addUpdateListener(chipUpdateListener) + + val aSet2 = AnimatorSet() + + aSet2.play(chipAnimator).before(systemAnimator) + if (hasPersistentDot) { + val dotAnim = notifyTransitionToPersistentDot() + if (dotAnim != null) aSet2.playTogether(systemAnimator, dotAnim) + } + + aSet2.start() + + statusBarWindowController.setForceStatusBarVisible(false) + scheduledEvent = null + }, DISPLAY_LENGTH) + }, DELAY) + } + + private fun notifyTransitionToPersistentDot(): Animator? { + val anims: List = listeners.mapNotNull { + it.onSystemStatusAnimationTransitionToPersistentDot() + } + if (anims.isNotEmpty()) { + val aSet = AnimatorSet() + aSet.playTogether(anims) + return aSet + } + + return null + } + + private fun notifyHidePersistentDot(): Animator? { + val anims: List = listeners.mapNotNull { + it.onHidePersistentDot() + } + + if (anims.isNotEmpty()) { + val aSet = AnimatorSet() + aSet.playTogether(anims) + return aSet + } + + return null + } + + private fun notifySystemStart() { + listeners.forEach { it.onSystemChromeAnimationStart() } + } + + private fun notifySystemFinish() { + listeners.forEach { it.onSystemChromeAnimationEnd() } + } + + private fun notifySystemAnimationUpdate(anim: ValueAnimator) { + listeners.forEach { it.onSystemChromeAnimationUpdate(anim) } + } + + override fun addCallback(listener: SystemStatusAnimationCallback) { + Assert.isMainThread() + + if (listeners.isEmpty()) { + coordinator.startObserving() + } + listeners.add(listener) + } + + override fun removeCallback(listener: SystemStatusAnimationCallback) { + Assert.isMainThread() + + listeners.remove(listener) + if (listeners.isEmpty()) { + coordinator.stopObserving() + } + } + + private val systemUpdateListener = ValueAnimator.AnimatorUpdateListener { + anim -> notifySystemAnimationUpdate(anim) + } + + private val systemAnimatorAdapter = object : AnimatorListenerAdapter() { + override fun onAnimationEnd(p0: Animator?) { + notifySystemFinish() + } + + override fun onAnimationStart(p0: Animator?) { + notifySystemStart() + } + } + + private val chipUpdateListener = ValueAnimator.AnimatorUpdateListener { + anim -> chipAnimationController.onChipAnimationUpdate(anim, animationState) + } + + inner class ChipAnimatorAdapter( + @SystemAnimationState val endState: Int, + val viewCreator: (context: Context) -> View + ) : AnimatorListenerAdapter() { + override fun onAnimationEnd(p0: Animator?) { + chipAnimationController.onChipAnimationEnd(animationState) + animationState = endState + } + + override fun onAnimationStart(p0: Animator?) { + chipAnimationController.onChipAnimationStart(viewCreator, animationState) + } + } +} + +/** + * The general idea here is that this scheduler will run two value animators, and provide + * animator-like callbacks for each kind of animation. The SystemChrome animation is expected to + * create space for the chip animation to display. This means hiding the system elements in the + * status bar and keyguard. + * + * TODO: the chip animation really only has one client, we can probably remove it from this + * interface + * + * The value animators themselves are simple animators from 0.0 to 1.0. Listeners can apply any + * interpolation they choose but realistically these are most likely to be simple alpha transitions + */ +interface SystemStatusAnimationCallback { + @JvmDefault fun onSystemChromeAnimationUpdate(animator: ValueAnimator) {} + @JvmDefault fun onSystemChromeAnimationStart() {} + @JvmDefault fun onSystemChromeAnimationEnd() {} + + // Best method name, change my mind + @JvmDefault fun onSystemStatusAnimationTransitionToPersistentDot(): Animator? { return null } + @JvmDefault fun onHidePersistentDot(): Animator? { return null } +} + +interface SystemStatusChipAnimationCallback { + fun onChipAnimationUpdate(animator: ValueAnimator, @SystemAnimationState state: Int) {} + + fun onChipAnimationStart( + viewCreator: (context: Context) -> View, + @SystemAnimationState state: Int + ) {} + + fun onChipAnimationEnd(@SystemAnimationState state: Int) {} +} + +/** + */ +@Retention(AnnotationRetention.SOURCE) +@IntDef( + value = [ + IDLE, ANIMATING_IN, RUNNING_CHIP_ANIM, ANIMATING_OUT + ] +) +annotation class SystemAnimationState + +/** No animation is in progress */ +const val IDLE = 0 +/** System is animating out, and chip is animating in */ +const val ANIMATING_IN = 1 +/** Chip has animated in and is awaiting exit animation, and optionally playing its own animation */ +const val RUNNING_CHIP_ANIM = 2 +/** Chip is animating away and system is animating back */ +const val ANIMATING_OUT = 3 + +private const val TAG = "SystemStatusAnimationScheduler" +private const val DELAY: Long = 100 +private const val DISPLAY_LENGTH = 5000L +private const val ENTRANCE_ANIM_LENGTH = 500L +private const val CHIP_ANIM_LENGTH = 500L +private const val MIN_UPTIME: Long = 5 * 1000 + +private const val DEBUG = false \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java index 40e6b40ffa14d..4e57e44f38d75 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java @@ -19,10 +19,16 @@ import static android.app.StatusBarManager.DISABLE_CLOCK; import static android.app.StatusBarManager.DISABLE_NOTIFICATION_ICONS; import static android.app.StatusBarManager.DISABLE_SYSTEM_INFO; +import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.ANIMATING_IN; +import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.ANIMATING_OUT; +import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.IDLE; + +import android.animation.ValueAnimator; import android.annotation.Nullable; import android.app.Fragment; import android.os.Bundle; import android.os.Parcelable; +import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; @@ -36,6 +42,9 @@ import com.android.systemui.animation.Interpolators; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.events.PrivacyDotViewController; +import com.android.systemui.statusbar.events.SystemStatusAnimationCallback; +import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; import com.android.systemui.statusbar.phone.StatusBarIconController.DarkIconManager; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallListener; @@ -44,6 +53,8 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.NetworkController; import com.android.systemui.statusbar.policy.NetworkController.SignalCallback; +import org.jetbrains.annotations.NotNull; + import java.util.ArrayList; import java.util.List; @@ -55,7 +66,8 @@ import javax.inject.Inject; * updated by the StatusBarIconController and DarkIconManager while it is attached. */ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue.Callbacks, - StatusBarStateController.StateListener { + StatusBarStateController.StateListener, + SystemStatusAnimationCallback { public static final String TAG = "CollapsedStatusBarFragment"; private static final String EXTRA_PANEL_STATE = "panel_state"; @@ -78,6 +90,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private View mOperatorNameFrame; private CommandQueue mCommandQueue; private OngoingCallController mOngoingCallController; + private final SystemStatusAnimationScheduler mAnimationScheduler; + private final PrivacyDotViewController mDotViewController; private List mBlockedIcons = new ArrayList<>(); @@ -103,8 +117,14 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue }; @Inject - public CollapsedStatusBarFragment(OngoingCallController ongoingCallController) { + public CollapsedStatusBarFragment( + OngoingCallController ongoingCallController, + SystemStatusAnimationScheduler animationScheduler, + PrivacyDotViewController dotViewController + ) { mOngoingCallController = ongoingCallController; + mAnimationScheduler = animationScheduler; + mDotViewController = dotViewController; } @Override @@ -127,6 +147,9 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mStatusBar = (PhoneStatusBarView) view; + View contents = mStatusBar.findViewById(R.id.status_bar_contents); + contents.addOnLayoutChangeListener(mStatusBarLayoutListener); + updateStatusBarLocation(contents.getLeft(), contents.getRight()); if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_PANEL_STATE)) { mStatusBar.restoreHierarchyState( savedInstanceState.getSparseParcelableArray(EXTRA_PANEL_STATE)); @@ -143,6 +166,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue showClock(false); initEmergencyCryptkeeperText(); initOperatorName(); + mAnimationScheduler.addCallback(this); } @Override @@ -208,6 +232,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue if (displayId != getContext().getDisplayId()) { return; } + Log.d(TAG, "disable: "); state1 = adjustDisableFlags(state1); final int old1 = mDisabled1; final int diff1 = state1 ^ old1; @@ -292,19 +317,22 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue return false; } - public void hideSystemIconArea(boolean animate) { + private void hideSystemIconArea(boolean animate) { animateHide(mSystemIconArea, animate); } - public void showSystemIconArea(boolean animate) { - animateShow(mSystemIconArea, animate); + private void showSystemIconArea(boolean animate) { + // Only show the system icon area if we are not currently animating + if (mAnimationScheduler.getAnimationState() == IDLE) { + animateShow(mSystemIconArea, animate); + } } - public void hideClock(boolean animate) { + private void hideClock(boolean animate) { animateHiddenState(mClockView, clockHiddenMode(), animate); } - public void showClock(boolean animate) { + private void showClock(boolean animate) { animateShow(mClockView, animate); } @@ -425,12 +453,60 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue } @Override - public void onStateChanged(int newState) { - - } + public void onStateChanged(int newState) { } @Override public void onDozingChanged(boolean isDozing) { disable(getContext().getDisplayId(), mDisabled1, mDisabled1, false /* animate */); } + + @Override + public void onSystemChromeAnimationStart() { + if (mAnimationScheduler.getAnimationState() == ANIMATING_OUT + && !isSystemIconAreaDisabled()) { + mSystemIconArea.setVisibility(View.VISIBLE); + mSystemIconArea.setAlpha(0f); + } + } + + @Override + public void onSystemChromeAnimationEnd() { + // Make sure the system icons are out of the way + if (mAnimationScheduler.getAnimationState() == ANIMATING_IN) { + mSystemIconArea.setVisibility(View.INVISIBLE); + mSystemIconArea.setAlpha(0f); + } else { + if (isSystemIconAreaDisabled()) { + // don't unhide + return; + } + + mSystemIconArea.setAlpha(1f); + mSystemIconArea.setVisibility(View.VISIBLE); + } + } + + @Override + public void onSystemChromeAnimationUpdate(@NotNull ValueAnimator animator) { + mSystemIconArea.setAlpha((float) animator.getAnimatedValue()); + } + + private boolean isSystemIconAreaDisabled() { + return (mDisabled1 & DISABLE_SYSTEM_INFO) != 0 || (mDisabled2 & DISABLE2_SYSTEM_ICONS) != 0; + } + + private void updateStatusBarLocation(int left, int right) { + int leftMargin = left - mStatusBar.getLeft(); + int rightMargin = mStatusBar.getRight() - right; + + mDotViewController.setStatusBarMargins(leftMargin, rightMargin); + } + + // Listen for view end changes of PhoneStatusBarView and publish that to the privacy dot + private View.OnLayoutChangeListener mStatusBarLayoutListener = + (view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + if (left != oldLeft || right != oldRight) { + updateStatusBarLocation(left, right); + } + }; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index 06947376b9f11..c22fec97db017 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -18,7 +18,10 @@ package com.android.systemui.statusbar.phone; import static com.android.systemui.DejankUtils.whitelistIpcs; import static com.android.systemui.ScreenDecorations.DisplayCutoutView.boundsFromDirection; +import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.ANIMATING_IN; +import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.ANIMATING_OUT; +import android.animation.ValueAnimator; import android.annotation.ColorInt; import android.content.Context; import android.content.res.Configuration; @@ -47,6 +50,8 @@ import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; +import com.android.systemui.statusbar.events.SystemStatusAnimationCallback; +import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; @@ -64,8 +69,11 @@ import java.util.List; /** * The header group on Keyguard. */ -public class KeyguardStatusBarView extends RelativeLayout - implements BatteryStateChangeCallback, OnUserInfoChangedListener, ConfigurationListener { +public class KeyguardStatusBarView extends RelativeLayout implements + BatteryStateChangeCallback, + OnUserInfoChangedListener, + ConfigurationListener, + SystemStatusAnimationCallback { private static final int LAYOUT_NONE = 0; private static final int LAYOUT_CUTOUT = 1; @@ -96,6 +104,8 @@ public class KeyguardStatusBarView extends RelativeLayout private ViewGroup mStatusIconArea; private int mLayoutState = LAYOUT_NONE; + private SystemStatusAnimationScheduler mAnimationScheduler; + /** * Draw this many pixels into the left/right side of the cutout to optimally use the space */ @@ -125,6 +135,7 @@ public class KeyguardStatusBarView extends RelativeLayout loadDimens(); loadBlockList(); mBatteryController = Dependency.get(BatteryController.class); + mAnimationScheduler = Dependency.get(SystemStatusAnimationScheduler.class); } @Override @@ -349,6 +360,7 @@ public class KeyguardStatusBarView extends RelativeLayout mIconManager = new TintedIconManager(findViewById(R.id.statusIcons)); mIconManager.setBlockList(mBlockedIcons); Dependency.get(StatusBarIconController.class).addIconGroup(mIconManager); + mAnimationScheduler.addCallback(this); onThemeChanged(); } @@ -358,6 +370,7 @@ public class KeyguardStatusBarView extends RelativeLayout Dependency.get(UserInfoController.class).removeCallback(this); Dependency.get(StatusBarIconController.class).removeIconGroup(mIconManager); Dependency.get(ConfigurationController.class).removeCallback(this); + mAnimationScheduler.removeCallback(this); } @Override @@ -509,4 +522,30 @@ public class KeyguardStatusBarView extends RelativeLayout mBatteryView.dump(fd, pw, args); } } + + /** SystemStatusAnimationCallback */ + @Override + public void onSystemChromeAnimationStart() { + if (mAnimationScheduler.getAnimationState() == ANIMATING_OUT) { + mSystemIconsContainer.setVisibility(View.VISIBLE); + mSystemIconsContainer.setAlpha(0f); + } + } + + @Override + public void onSystemChromeAnimationEnd() { + // Make sure the system icons are out of the way + if (mAnimationScheduler.getAnimationState() == ANIMATING_IN) { + mSystemIconsContainer.setVisibility(View.INVISIBLE); + mSystemIconsContainer.setAlpha(0f); + } else { + mSystemIconsContainer.setAlpha(1f); + mSystemIconsContainer.setVisibility(View.VISIBLE); + } + } + + @Override + public void onSystemChromeAnimationUpdate(ValueAnimator anim) { + mSystemIconsContainer.setAlpha((float) anim.getAnimatedValue()); + } } 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 d386ebde26bfd..ce9c2f5c95b80 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -208,6 +208,8 @@ import com.android.systemui.statusbar.SuperStatusBarViewFactory; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.charging.WiredChargingRippleController; +import com.android.systemui.statusbar.events.PrivacyDotViewController; +import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider; @@ -426,6 +428,8 @@ public class StatusBar extends SystemUI implements DemoMode, private final DemoModeController mDemoModeController; private NotificationsController mNotificationsController; private final OngoingCallController mOngoingCallController; + private final SystemStatusAnimationScheduler mAnimationScheduler; + private final PrivacyDotViewController mDotViewController; // expanded notifications // the sliding/resizing panel within the notification window @@ -794,6 +798,8 @@ public class StatusBar extends SystemUI implements DemoMode, BrightnessSlider.Factory brightnessSliderFactory, WiredChargingRippleController chargingRippleAnimationController, OngoingCallController ongoingCallController, + SystemStatusAnimationScheduler animationScheduler, + PrivacyDotViewController dotViewController, TunerService tunerService, FeatureFlags featureFlags) { super(context); @@ -875,6 +881,8 @@ public class StatusBar extends SystemUI implements DemoMode, mBrightnessSliderFactory = brightnessSliderFactory; mChargingRippleAnimationController = chargingRippleAnimationController; mOngoingCallController = ongoingCallController; + mAnimationScheduler = animationScheduler; + mDotViewController = dotViewController; mFeatureFlags = featureFlags; tunerService.addTunable( @@ -1171,7 +1179,10 @@ public class StatusBar extends SystemUI implements DemoMode, }).getFragmentManager() .beginTransaction() .replace(R.id.status_bar_container, - new CollapsedStatusBarFragment(mOngoingCallController), + new CollapsedStatusBarFragment( + mOngoingCallController, + mAnimationScheduler, + mDotViewController), CollapsedStatusBarFragment.TAG) .commit(); 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 2c2779e53e161..24e6db818ef65 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 @@ -61,6 +61,8 @@ import com.android.systemui.statusbar.SuperStatusBarViewFactory; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.charging.WiredChargingRippleController; +import com.android.systemui.statusbar.events.PrivacyDotViewController; +import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager; @@ -209,6 +211,8 @@ public interface StatusBarPhoneModule { BrightnessSlider.Factory brightnessSliderFactory, WiredChargingRippleController chargingRippleAnimationController, OngoingCallController ongoingCallController, + SystemStatusAnimationScheduler animationScheduler, + PrivacyDotViewController dotViewController, TunerService tunerService, FeatureFlags featureFlags) { return new StatusBar( @@ -293,6 +297,8 @@ public interface StatusBarPhoneModule { brightnessSliderFactory, chargingRippleAnimationController, ongoingCallController, + animationScheduler, + dotViewController, tunerService, featureFlags); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index 59262cf3231b9..325275047ebb0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -63,6 +63,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.R.dimen; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.settings.UserTracker; +import com.android.systemui.statusbar.events.PrivacyDotViewController; import com.android.systemui.tuner.TunerService; import com.android.systemui.util.settings.FakeSettings; import com.android.systemui.util.settings.SecureSettings; @@ -94,6 +95,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { private BroadcastDispatcher mBroadcastDispatcher; @Mock private UserTracker mUserTracker; + @Mock + private PrivacyDotViewController mDotViewController; @Before public void setup() { @@ -116,7 +119,7 @@ public class ScreenDecorationsTest extends SysuiTestCase { mContext.addMockSystemService(DisplayManager.class, mDisplayManager); mScreenDecorations = spy(new ScreenDecorations(mContext, mMainHandler, mSecureSettings, - mBroadcastDispatcher, mTunerService, mUserTracker) { + mBroadcastDispatcher, mTunerService, mUserTracker, mDotViewController) { @Override public void start() { super.start(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java index 67fd5eb1acac5..929a7e1543fbd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java @@ -37,6 +37,8 @@ import androidx.test.filters.SmallTest; import com.android.systemui.R; import com.android.systemui.SysuiBaseFragmentTest; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.events.PrivacyDotViewController; +import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallListener; @@ -58,6 +60,8 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { private View mCenteredNotificationAreaView; private StatusBarStateController mStatusBarStateController; private OngoingCallController mOngoingCallController; + private SystemStatusAnimationScheduler mAnimationScheduler; + private PrivacyDotViewController mDotViewController; public CollapsedStatusBarFragmentTest() { super(CollapsedStatusBarFragment.class); @@ -212,6 +216,11 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Override protected Fragment instantiate(Context context, String className, Bundle arguments) { mOngoingCallController = mock(OngoingCallController.class); - return new CollapsedStatusBarFragment(mOngoingCallController); + mAnimationScheduler = mock(SystemStatusAnimationScheduler.class); + mDotViewController = mock(PrivacyDotViewController.class); + return new CollapsedStatusBarFragment( + mOngoingCallController, + mAnimationScheduler, + mDotViewController); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java index 08d6d2d7c72d6..3989dfa55991e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java @@ -113,6 +113,8 @@ import com.android.systemui.statusbar.StatusBarStateControllerImpl; import com.android.systemui.statusbar.SuperStatusBarViewFactory; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.charging.WiredChargingRippleController; +import com.android.systemui.statusbar.events.PrivacyDotViewController; +import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; @@ -263,6 +265,8 @@ public class StatusBarTest extends SysuiTestCase { @Mock private BrightnessSlider.Factory mBrightnessSliderFactory; @Mock private WiredChargingRippleController mWiredChargingRippleController; @Mock private OngoingCallController mOngoingCallController; + @Mock private SystemStatusAnimationScheduler mAnimationScheduler; + @Mock private PrivacyDotViewController mDotViewController; @Mock private TunerService mTunerService; @Mock private FeatureFlags mFeatureFlags; private ShadeController mShadeController; @@ -429,6 +433,8 @@ public class StatusBarTest extends SysuiTestCase { mBrightnessSliderFactory, mWiredChargingRippleController, mOngoingCallController, + mAnimationScheduler, + mDotViewController, mTunerService, mFeatureFlags); when(mKeyguardViewMediator.registerStatusBar(any(StatusBar.class), any(ViewGroup.class),