Merge "Phase 1 of status bar system event animations" into sc-dev

This commit is contained in:
TreeHugger Robot
2021-04-15 22:46:11 +00:00
committed by Android (Google) Code Review
19 changed files with 1395 additions and 21 deletions

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 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.
-->
<!-- dot drawable for ongoing system privacy events -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid
android:color="@color/privacy_circle"/>
<size
android:width="6dp"
android:height="6dp"
/>
</shape>

View File

@@ -14,6 +14,8 @@
** See the License for the specific language governing permissions and
** limitations under the License.
-->
<!-- TODO: remove this in favor of requiring top and bottom layouts -->
<com.android.systemui.RegionInterceptingFrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rounded_corners_default"
@@ -26,6 +28,25 @@
android:layout_gravity="left|top"
android:tint="#ff000000"
android:src="@drawable/rounded"/>
<FrameLayout
android:id="@+id/privacy_dot_left_container"
android:layout_height="@dimen/status_bar_height"
android:layout_width="wrap_content"
android:layout_marginTop="@dimen/status_bar_padding_top"
android:layout_marginLeft="8dp"
android:layout_gravity="left|top"
android:visibility="invisible" >
<ImageView
android:id="@+id/privacy_dot_left"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_gravity="center"
android:src="@drawable/system_animation_ongoing_dot"
android:visibility="visible" />
</FrameLayout>
<ImageView
android:id="@+id/right"
android:layout_width="12dp"
@@ -33,4 +54,22 @@
android:tint="#ff000000"
android:layout_gravity="right|bottom"
android:src="@drawable/rounded"/>
<FrameLayout
android:id="@+id/privacy_dot_right_container"
android:layout_height="@dimen/status_bar_height"
android:layout_width="wrap_content"
android:layout_marginTop="@dimen/status_bar_padding_top"
android:layout_marginRight="8dp"
android:layout_gravity="right|top"
android:visibility="invisible" >
<ImageView
android:id="@+id/privacy_dot_right"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_gravity="center"
android:src="@drawable/system_animation_ongoing_dot"
android:visibility="visible" />
</FrameLayout>
</com.android.systemui.RegionInterceptingFrameLayout>

View File

@@ -26,6 +26,24 @@
android:layout_gravity="left|bottom"
android:tint="#ff000000"
android:src="@drawable/rounded_corner_bottom"/>
<FrameLayout
android:id="@+id/privacy_dot_left_container"
android:layout_height="@dimen/status_bar_height"
android:layout_width="wrap_content"
android:layout_marginTop="@dimen/status_bar_padding_top"
android:layout_marginLeft="0dp"
android:layout_gravity="left|bottom"
android:visibility="invisible" >
<ImageView
android:id="@+id/privacy_dot"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_gravity="center_vertical|right"
android:src="@drawable/system_animation_ongoing_dot"
android:visibility="visible" />
</FrameLayout>
<ImageView
android:id="@+id/right"
android:layout_width="12dp"
@@ -33,4 +51,21 @@
android:tint="#ff000000"
android:layout_gravity="right|bottom"
android:src="@drawable/rounded_corner_bottom"/>
<FrameLayout
android:id="@+id/privacy_dot_right_container"
android:layout_height="@dimen/status_bar_height"
android:layout_width="wrap_content"
android:layout_marginTop="@dimen/status_bar_padding_top"
android:layout_marginRight="0dp"
android:layout_gravity="right|bottom"
android:visibility="invisible" >
<ImageView
android:id="@+id/privacy_dot"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_gravity="center_vertical|left"
android:src="@drawable/system_animation_ongoing_dot"
android:visibility="visible" />
</FrameLayout>
</com.android.systemui.RegionInterceptingFrameLayout>

View File

@@ -26,6 +26,24 @@
android:layout_gravity="left|top"
android:tint="#ff000000"
android:src="@drawable/rounded_corner_top"/>
<FrameLayout
android:id="@+id/privacy_dot_left_container"
android:layout_height="@*android:dimen/status_bar_height_portrait"
android:layout_width="wrap_content"
android:layout_marginTop="@dimen/status_bar_padding_top"
android:layout_marginLeft="0dp"
android:layout_gravity="left|top"
android:visibility="invisible" >
<ImageView
android:id="@+id/privacy_dot"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_gravity="center_vertical|right"
android:src="@drawable/system_animation_ongoing_dot"
android:visibility="visible" />
</FrameLayout>
<ImageView
android:id="@+id/right"
android:layout_width="12dp"
@@ -33,4 +51,24 @@
android:tint="#ff000000"
android:layout_gravity="right|top"
android:src="@drawable/rounded_corner_top"/>
<FrameLayout
android:id="@+id/privacy_dot_right_container"
android:layout_height="@*android:dimen/status_bar_height_portrait"
android:layout_width="wrap_content"
android:layout_marginTop="@dimen/status_bar_padding_top"
android:layout_marginRight="0dp"
android:layout_gravity="right|top"
android:visibility="invisible" >
<ImageView
android:id="@+id/privacy_dot"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_gravity="center_vertical|left"
android:src="@drawable/system_animation_ongoing_dot"
android:visibility="visible" />
</FrameLayout>
</com.android.systemui.RegionInterceptingFrameLayout>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical|end"
android:paddingTop="@dimen/status_bar_padding_top"
android:paddingEnd="8dp"
>
<ImageView
android:id="@+id/dot_view"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_gravity="center_vertical|end"
android:src="@drawable/system_animation_ongoing_dot"
android:visibility="invisible"
/>
</FrameLayout>

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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")
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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"

View File

@@ -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

View File

@@ -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);
}
};
}

View File

@@ -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());
}
}

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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);
}
}

View File

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