summaryrefslogtreecommitdiff
path: root/packages/SystemUI/src
diff options
context:
space:
mode:
authorEvan Laird <evanlaird@google.com>2021-03-05 10:11:04 -0500
committerEvan Laird <evanlaird@google.com>2021-04-14 22:55:02 -0400
commit1b73896e5a7357e6205f289f10d4f86b658ebeb1 (patch)
tree987765e48ac689f09badb81ea183af1f2e2923ac /packages/SystemUI/src
parent1198ed066befaaa96ce12e0479a92626ec3baf41 (diff)
Phase 1 of status bar system event animations
Sketch of how things might work: - SystemEventCoordinator ties together arbitrary events and funnels them to the scheduler - Scheduler - a very simple prioritization and scheduling utility, since we don't want a spammy status bar - Animation controller - this class is probably going to evolve the most. Once an animation has a clear path, this class will actually make sure views are visible and allow individual view to animate in and out properly. It also potentially keeps the status bar open in immersive mode This CL stands up a couple of things meant to achieve system animations (currently only privacy items) that live in the status bar: 1. SystemStatusAnimationScheduler - simple prioritization queue which can take requests to play system status animations. It also handles the running of said animations by maintaining ValueAnimators and providing callbacks for the system to get out of the way while animations are happening. 2. SystemEventCoordinator - This is the front end for generating system events which trigger animations. Pretty simple, it sends StatusEvent objects to the scheduler to maybe play the relevant animation. 3. StatusEvent - Events are sent to be scheduled, and wrap a priority int. They also create the view that is to be animated (still WIP) 4. SystemStatusChipAnimationController - handles the presentation of the chip view. Still needs to be updated with prettier animations 5. PrivacyDotViewController - the most bespoke of classes added here. controls the location of the privacy dots (more on this below) System spaces are expected to listen for callbacks from the scheduler and do whatever is appropriate to get out of the way. E.g., hide the status bar system elements, keep the status bar window open in immersive mode, or hide keyguard elements. And finally, a note on the dot: for reasons(tm), the dot lives in ScreenDecorations. But since screen decorations creates very special windows that don't "just rotate", the locations are controlled by the PrivacyDotController. However, to create the illusion that the dots are in the status bar, they are always positioned on either side of where the status bar happens to be. Bug: 177323724 Test: coming soon to a theater near you Change-Id: If528bd7e9d1e0ca3be7391f83f7416fa10e7a41b
Diffstat (limited to 'packages/SystemUI/src')
-rw-r--r--packages/SystemUI/src/com/android/systemui/Dependency.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/ScreenDecorations.java54
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt352
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt67
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt137
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt112
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt333
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java96
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java43
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java13
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java6
11 files changed, 1202 insertions, 19 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index a686fc086b40..cbfdce5d0c69 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<DeviceConfigProxy> mDeviceConfigProxy;
@Inject Lazy<NavigationBarOverlayController> mNavbarButtonsControllerLazy;
@Inject Lazy<TelephonyListenerManager> mTelephonyListenerManager;
+ @Inject Lazy<SystemStatusAnimationScheduler> mSystemStatusAnimationSchedulerLazy;
+ @Inject Lazy<PrivacyDotViewController> 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 5fa98bccfc69..d07723e475fb 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 000000000000..5ab71bc62fe6
--- /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<View>
+ 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<View>(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 000000000000..6380dc0b9b46
--- /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<PrivacyItem> = 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 000000000000..620963060b49
--- /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 000000000000..b4818233aea3
--- /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<PrivacyItem>()
+
+ override fun onPrivacyItemsChanged(privacyItems: List<PrivacyItem>) {
+ 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 000000000000..b85d0310092c
--- /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<SystemStatusAnimationCallback> {
+
+ /** 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<SystemStatusAnimationCallback>()
+
+ 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<Animator> = listeners.mapNotNull {
+ it.onSystemStatusAnimationTransitionToPersistentDot()
+ }
+ if (anims.isNotEmpty()) {
+ val aSet = AnimatorSet()
+ aSet.playTogether(anims)
+ return aSet
+ }
+
+ return null
+ }
+
+ private fun notifyHidePersistentDot(): Animator? {
+ val anims: List<Animator> = 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 40e6b40ffa14..4e57e44f38d7 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<String> 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 06947376b9f1..c22fec97db01 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 d386ebde26bf..ce9c2f5c95b8 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 2c2779e53e16..24e6db818ef6 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);
}