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),