summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/app/Notification.java7
-rw-r--r--ktfmt_includes.txt2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java2
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt305
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/NightDisplayRepositoryTest.kt203
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractorTest.kt98
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractorTest.kt177
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt315
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayChangeEvent.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayState.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/data/repository/NightDisplayRepository.kt196
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileDataInteractor.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractor.kt88
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractor.kt64
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/model/NightDisplayTileModel.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt128
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java13
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt36
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/kotlin/LocationControllerExt.kt37
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProviderImplTest.kt75
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt44
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt363
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/night/NightDisplayTileKosmos.kt24
-rw-r--r--services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java54
-rw-r--r--services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java5
-rw-r--r--services/core/java/com/android/server/pm/Settings.java9
-rw-r--r--services/core/java/com/android/server/wm/InputConfigAdapter.java16
-rw-r--r--services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java71
-rw-r--r--tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java6
44 files changed, 2208 insertions, 410 deletions
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 604b37da600d..0caea7f8084c 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -6595,6 +6595,11 @@ public class Notification implements Parcelable
* @hide
*/
public RemoteViews createCompactHeadsUpContentView() {
+ // Don't show compact heads up for FSI notifications.
+ if (mN.fullScreenIntent != null) {
+ return createHeadsUpContentView(/* increasedHeight= */ false);
+ }
+
if (mStyle != null) {
final RemoteViews styleView = mStyle.makeCompactHeadsUpContentView();
if (styleView != null) {
@@ -10352,7 +10357,7 @@ public class Notification implements Parcelable
@Nullable
@Override
public RemoteViews makeCompactHeadsUpContentView() {
- // TODO(b/336228700): Apply minimal HUN treatment for Call Style.
+ // Use existing heads up for call style.
return makeHeadsUpContentView(false);
}
diff --git a/ktfmt_includes.txt b/ktfmt_includes.txt
index fe4750381fd0..0ac6265ecf27 100644
--- a/ktfmt_includes.txt
+++ b/ktfmt_includes.txt
@@ -5,8 +5,6 @@
-packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt
-packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
-packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt
--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
-packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
-packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/View.kt
-packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 3c788b18429c..7bceb2ce99b0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -2341,8 +2341,8 @@ public class BubbleStackView extends FrameLayout
showScrim(true, null /* runnable */);
updateBubbleShadows(mIsExpanded);
- updateBadges(false /* setBadgeForCollapsedStack */);
mBubbleContainer.setActiveController(mExpandedAnimationController);
+ updateBadges(false /* setBadgeForCollapsedStack */);
updateOverflowVisibility();
updatePointerPosition(false /* forIme */);
mExpandedAnimationController.expandFromStack(() -> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
index 9bf9fa749373..b41454d932a5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
@@ -65,7 +65,7 @@ public class DesktopModeStatus {
"persist.wm.debug.desktop_use_window_shadows_focused_window", false);
/**
- * Flag to indicate whether to apply shadows to windows in desktop mode.
+ * Flag to indicate whether to use rounded corners for windows in desktop mode.
*/
private static final boolean USE_ROUNDED_CORNERS = SystemProperties.getBoolean(
"persist.wm.debug.desktop_use_rounded_corners", true);
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index f539a23d9cb0..bdeab797d165 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -28,16 +28,16 @@ import android.text.format.DateFormat
import android.util.AttributeSet
import android.util.MathUtils.constrainedMap
import android.util.TypedValue
-import android.view.View.MeasureSpec.EXACTLY
import android.view.View
+import android.view.View.MeasureSpec.EXACTLY
import android.widget.TextView
import com.android.app.animation.Interpolators
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.animation.GlyphCallback
import com.android.systemui.animation.TextAnimator
import com.android.systemui.customization.R
-import com.android.systemui.log.core.LogcatOnlyMessageBuffer
import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogcatOnlyMessageBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.core.MessageBuffer
import java.io.PrintWriter
@@ -47,11 +47,13 @@ import java.util.TimeZone
import kotlin.math.min
/**
- * Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30)
- * The time's text color is a gradient that changes its colors based on its controller.
+ * Displays the time with the hour positioned above the minutes (ie: 09 above 30 is 9:30). The
+ * time's text color is a gradient that changes its colors based on its controller.
*/
@SuppressLint("AppCompatCustomView")
-class AnimatableClockView @JvmOverloads constructor(
+class AnimatableClockView
+@JvmOverloads
+constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
@@ -63,7 +65,9 @@ class AnimatableClockView @JvmOverloads constructor(
get() = field ?: DEFAULT_LOGGER
var messageBuffer: MessageBuffer
get() = logger.buffer
- set(value) { logger = Logger(value, TAG) }
+ set(value) {
+ logger = Logger(value, TAG)
+ }
var hasCustomPositionUpdatedAnimation: Boolean = false
var migratedClocks: Boolean = false
@@ -77,16 +81,13 @@ class AnimatableClockView @JvmOverloads constructor(
private var format: CharSequence? = null
private var descFormat: CharSequence? = null
- @ColorInt
- private var dozingColor = 0
-
- @ColorInt
- private var lockScreenColor = 0
+ @ColorInt private var dozingColor = 0
+ @ColorInt private var lockScreenColor = 0
private var lineSpacingScale = 1f
private val chargeAnimationDelay: Int
private var textAnimator: TextAnimator? = null
- private var onTextAnimatorInitialized: Runnable? = null
+ private var onTextAnimatorInitialized: ((TextAnimator) -> Unit)? = null
private var translateForCenterAnimation = false
private val parentWidth: Int
@@ -94,9 +95,11 @@ class AnimatableClockView @JvmOverloads constructor(
// last text size which is not constrained by view height
private var lastUnconstrainedTextSize: Float = Float.MAX_VALUE
- @VisibleForTesting var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator =
- { layout, invalidateCb ->
- TextAnimator(layout, NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb) }
+
+ @VisibleForTesting
+ var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator = { layout, invalidateCb ->
+ TextAnimator(layout, NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb)
+ }
// Used by screenshot tests to provide stability
@VisibleForTesting var isAnimationEnabled: Boolean = true
@@ -109,40 +112,55 @@ class AnimatableClockView @JvmOverloads constructor(
get() = if (useBoldedVersion()) lockScreenWeightInternal + 100 else lockScreenWeightInternal
/**
- * The number of pixels below the baseline. For fonts that support languages such as
- * Burmese, this space can be significant and should be accounted for when computing layout.
+ * The number of pixels below the baseline. For fonts that support languages such as Burmese,
+ * this space can be significant and should be accounted for when computing layout.
*/
- val bottom get() = paint?.fontMetrics?.bottom ?: 0f
+ val bottom: Float
+ get() = paint?.fontMetrics?.bottom ?: 0f
init {
- val animatableClockViewAttributes = context.obtainStyledAttributes(
- attrs, R.styleable.AnimatableClockView, defStyleAttr, defStyleRes
- )
+ val animatableClockViewAttributes =
+ context.obtainStyledAttributes(
+ attrs,
+ R.styleable.AnimatableClockView,
+ defStyleAttr,
+ defStyleRes
+ )
try {
- dozingWeightInternal = animatableClockViewAttributes.getInt(
- R.styleable.AnimatableClockView_dozeWeight,
- /* default = */ 100
- )
- lockScreenWeightInternal = animatableClockViewAttributes.getInt(
- R.styleable.AnimatableClockView_lockScreenWeight,
- /* default = */ 300
- )
- chargeAnimationDelay = animatableClockViewAttributes.getInt(
- R.styleable.AnimatableClockView_chargeAnimationDelay, /* default = */ 200
- )
+ dozingWeightInternal =
+ animatableClockViewAttributes.getInt(
+ R.styleable.AnimatableClockView_dozeWeight,
+ /* default = */ 100
+ )
+ lockScreenWeightInternal =
+ animatableClockViewAttributes.getInt(
+ R.styleable.AnimatableClockView_lockScreenWeight,
+ /* default = */ 300
+ )
+ chargeAnimationDelay =
+ animatableClockViewAttributes.getInt(
+ R.styleable.AnimatableClockView_chargeAnimationDelay,
+ /* default = */ 200
+ )
} finally {
animatableClockViewAttributes.recycle()
}
- val textViewAttributes = context.obtainStyledAttributes(
- attrs, android.R.styleable.TextView,
- defStyleAttr, defStyleRes
- )
+ val textViewAttributes =
+ context.obtainStyledAttributes(
+ attrs,
+ android.R.styleable.TextView,
+ defStyleAttr,
+ defStyleRes
+ )
try {
- isSingleLineInternal = textViewAttributes.getBoolean(
- android.R.styleable.TextView_singleLine, /* default = */ false)
+ isSingleLineInternal =
+ textViewAttributes.getBoolean(
+ android.R.styleable.TextView_singleLine,
+ /* default = */ false
+ )
} finally {
textViewAttributes.recycle()
}
@@ -156,9 +174,7 @@ class AnimatableClockView @JvmOverloads constructor(
refreshFormat()
}
- /**
- * Whether to use a bolded version based on the user specified fontWeightAdjustment.
- */
+ /** Whether to use a bolded version based on the user specified fontWeightAdjustment. */
fun useBoldedVersion(): Boolean {
// "Bold text" fontWeightAdjustment is 300.
return resources.configuration.fontWeightAdjustment > 100
@@ -169,25 +185,30 @@ class AnimatableClockView @JvmOverloads constructor(
contentDescription = DateFormat.format(descFormat, time)
val formattedText = DateFormat.format(format, time)
logger.d({ "refreshTime: new formattedText=$str1" }) { str1 = formattedText?.toString() }
- // Setting text actually triggers a layout pass (because the text view is set to
- // wrap_content width and TextView always relayouts for this). Avoid needless
- // relayout if the text didn't actually change.
- if (!TextUtils.equals(text, formattedText)) {
- text = formattedText
- logger.d({ "refreshTime: done setting new time text to: $str1" }) {
- str1 = formattedText?.toString()
- }
- // Because the TextLayout may mutate under the hood as a result of the new text, we
- // notify the TextAnimator that it may have changed and request a measure/layout. A
- // crash will occur on the next invocation of setTextStyle if the layout is mutated
- // without being notified TextInterpolator being notified.
- if (layout != null) {
- textAnimator?.updateLayout(layout)
- logger.d("refreshTime: done updating textAnimator layout")
- }
- requestLayout()
- logger.d("refreshTime: after requestLayout")
+
+ // Setting text actually triggers a layout pass in TextView (because the text view is set to
+ // wrap_content width and TextView always relayouts for this). This avoids needless relayout
+ // if the text didn't actually change.
+ if (TextUtils.equals(text, formattedText)) {
+ return
}
+
+ text = formattedText
+ logger.d({ "refreshTime: done setting new time text to: $str1" }) {
+ str1 = formattedText?.toString()
+ }
+
+ // Because the TextLayout may mutate under the hood as a result of the new text, we notify
+ // the TextAnimator that it may have changed and request a measure/layout. A crash will
+ // occur on the next invocation of setTextStyle if the layout is mutated without being
+ // notified TextInterpolator being notified.
+ if (layout != null) {
+ textAnimator?.updateLayout(layout)
+ logger.d("refreshTime: done updating textAnimator layout")
+ }
+
+ requestLayout()
+ logger.d("refreshTime: after requestLayout")
}
fun onTimeZoneChanged(timeZone: TimeZone?) {
@@ -206,19 +227,27 @@ class AnimatableClockView @JvmOverloads constructor(
@SuppressLint("DrawAllocation")
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
logger.d("onMeasure")
- if (migratedClocks && !isSingleLineInternal &&
- MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) {
+
+ if (
+ migratedClocks &&
+ !isSingleLineInternal &&
+ MeasureSpec.getMode(heightMeasureSpec) == EXACTLY
+ ) {
// Call straight into TextView.setTextSize to avoid setting lastUnconstrainedTextSize
- super.setTextSize(TypedValue.COMPLEX_UNIT_PX,
- min(lastUnconstrainedTextSize, MeasureSpec.getSize(heightMeasureSpec) / 2F))
+ super.setTextSize(
+ TypedValue.COMPLEX_UNIT_PX,
+ min(lastUnconstrainedTextSize, MeasureSpec.getSize(heightMeasureSpec) / 2F)
+ )
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val animator = textAnimator
if (animator == null) {
- textAnimator = textAnimatorFactory(layout, ::invalidate)
- onTextAnimatorInitialized?.run()
- onTextAnimatorInitialized = null
+ textAnimator =
+ textAnimatorFactory(layout, ::invalidate)?.also {
+ onTextAnimatorInitialized?.invoke(it)
+ onTextAnimatorInitialized = null
+ }
} else {
animator.updateLayout(layout)
}
@@ -243,15 +272,13 @@ class AnimatableClockView @JvmOverloads constructor(
canvas.translate(parentWidth / 4f, 0f)
}
- logger.d({ "onDraw($str1)"}) { str1 = text.toString() }
+ logger.d({ "onDraw($str1)" }) { str1 = text.toString() }
// intentionally doesn't call super.onDraw here or else the text will be rendered twice
textAnimator?.draw(canvas)
canvas.restore()
}
override fun invalidate() {
- @Suppress("UNNECESSARY_SAFE_CALL")
- // logger won't be initialized when called by TextView's constructor
logger.d("invalidate")
super.invalidate()
}
@@ -325,6 +352,7 @@ class AnimatableClockView @JvmOverloads constructor(
if (textAnimator == null) {
return
}
+
logger.d("animateFoldAppear")
setTextStyle(
weight = lockScreenWeightInternal,
@@ -348,10 +376,11 @@ class AnimatableClockView @JvmOverloads constructor(
}
fun animateCharge(isDozing: () -> Boolean) {
+ // Skip charge animation if dozing animation is already playing.
if (textAnimator == null || textAnimator!!.isRunning()) {
- // Skip charge animation if dozing animation is already playing.
return
}
+
logger.d("animateCharge")
val startAnimPhase2 = Runnable {
setTextStyle(
@@ -409,10 +438,9 @@ class AnimatableClockView @JvmOverloads constructor(
/**
* Set text style with an optional animation.
- *
- * By passing -1 to weight, the view preserves its current weight.
- * By passing -1 to textSize, the view preserves its current text size.
- * By passing null to color, the view preserves its current color.
+ * - By passing -1 to weight, the view preserves its current weight.
+ * - By passing -1 to textSize, the view preserves its current text size.
+ * - By passing null to color, the view preserves its current color.
*
* @param weight text weight.
* @param textSize font size.
@@ -428,8 +456,8 @@ class AnimatableClockView @JvmOverloads constructor(
delay: Long,
onAnimationEnd: Runnable?
) {
- if (textAnimator != null) {
- textAnimator?.setTextStyle(
+ textAnimator?.let {
+ it.setTextStyle(
weight = weight,
textSize = textSize,
color = color,
@@ -439,23 +467,24 @@ class AnimatableClockView @JvmOverloads constructor(
delay = delay,
onAnimationEnd = onAnimationEnd
)
- textAnimator?.glyphFilter = glyphFilter
- } else {
- // when the text animator is set, update its start values
- onTextAnimatorInitialized = Runnable {
- textAnimator?.setTextStyle(
- weight = weight,
- textSize = textSize,
- color = color,
- animate = false,
- duration = duration,
- interpolator = interpolator,
- delay = delay,
- onAnimationEnd = onAnimationEnd
- )
- textAnimator?.glyphFilter = glyphFilter
- }
+ it.glyphFilter = glyphFilter
}
+ ?: run {
+ // when the text animator is set, update its start values
+ onTextAnimatorInitialized = { textAnimator ->
+ textAnimator.setTextStyle(
+ weight = weight,
+ textSize = textSize,
+ color = color,
+ animate = false,
+ duration = duration,
+ interpolator = interpolator,
+ delay = delay,
+ onAnimationEnd = onAnimationEnd
+ )
+ textAnimator.glyphFilter = glyphFilter
+ }
+ }
}
private fun setTextStyle(
@@ -483,12 +512,13 @@ class AnimatableClockView @JvmOverloads constructor(
fun refreshFormat(use24HourFormat: Boolean) {
Patterns.update(context)
- format = when {
- isSingleLineInternal && use24HourFormat -> Patterns.sClockView24
- !isSingleLineInternal && use24HourFormat -> DOUBLE_LINE_FORMAT_24_HOUR
- isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
- else -> DOUBLE_LINE_FORMAT_12_HOUR
- }
+ format =
+ when {
+ isSingleLineInternal && use24HourFormat -> Patterns.sClockView24
+ !isSingleLineInternal && use24HourFormat -> DOUBLE_LINE_FORMAT_24_HOUR
+ isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
+ else -> DOUBLE_LINE_FORMAT_12_HOUR
+ }
logger.d({ "refreshFormat($str1)" }) { str1 = format?.toString() }
descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12
@@ -510,10 +540,10 @@ class AnimatableClockView @JvmOverloads constructor(
pw.println(" time=$time")
}
- private val moveToCenterDelays
+ private val moveToCenterDelays: List<Int>
get() = if (isLayoutRtl) MOVE_LEFT_DELAYS else MOVE_RIGHT_DELAYS
- private val moveToSideDelays
+ private val moveToSideDelays: List<Int>
get() = if (isLayoutRtl) MOVE_RIGHT_DELAYS else MOVE_LEFT_DELAYS
/**
@@ -531,7 +561,7 @@ class AnimatableClockView @JvmOverloads constructor(
fun offsetGlyphsForStepClockAnimation(
clockStartLeft: Int,
clockMoveDirection: Int,
- moveFraction: Float
+ moveFraction: Float,
) {
val isMovingToCenter = if (isLayoutRtl) clockMoveDirection < 0 else clockMoveDirection > 0
val currentMoveAmount = left - clockStartLeft
@@ -558,8 +588,8 @@ class AnimatableClockView @JvmOverloads constructor(
*
* @param distance is the total distance in pixels to offset the glyphs when animation
* completes. Negative distance means we are animating the position towards the center.
- * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1
- * means it finished moving.
+ * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
+ * it finished moving.
*/
fun offsetGlyphsForStepClockAnimation(
distance: Float,
@@ -568,13 +598,17 @@ class AnimatableClockView @JvmOverloads constructor(
for (i in 0 until NUM_DIGITS) {
val dir = if (isLayoutRtl) -1 else 1
val digitFraction =
- getDigitFraction(digit = i, isMovingToCenter = distance > 0, fraction = fraction)
+ getDigitFraction(
+ digit = i,
+ isMovingToCenter = distance > 0,
+ fraction = fraction,
+ )
val moveAmountForDigit = dir * distance * digitFraction
glyphOffsets[i] = moveAmountForDigit
if (distance > 0) {
- // If distance > 0 then we are moving from the left towards the center.
- // We need ensure that the glyphs are offset to the initial position.
+ // If distance > 0 then we are moving from the left towards the center. We need to
+ // ensure that the glyphs are offset to the initial position.
glyphOffsets[i] -= dir * distance
}
}
@@ -582,27 +616,25 @@ class AnimatableClockView @JvmOverloads constructor(
}
private fun getDigitFraction(digit: Int, isMovingToCenter: Boolean, fraction: Float): Float {
- // The delay for the digit, in terms of fraction (i.e. the digit should not move
- // during 0.0 - 0.1).
- val digitInitialDelay =
- if (isMovingToCenter) {
- moveToCenterDelays[digit] * MOVE_DIGIT_STEP
- } else {
- moveToSideDelays[digit] * MOVE_DIGIT_STEP
- }
+ // The delay for the digit, in terms of fraction.
+ // (i.e. the digit should not move during 0.0 - 0.1).
+ val delays = if (isMovingToCenter) moveToCenterDelays else moveToSideDelays
+ val digitInitialDelay = delays[digit] * MOVE_DIGIT_STEP
return MOVE_INTERPOLATOR.getInterpolation(
- constrainedMap(
- 0.0f,
- 1.0f,
- digitInitialDelay,
- digitInitialDelay + AVAILABLE_ANIMATION_TIME,
- fraction,
- )
+ constrainedMap(
+ /* rangeMin= */ 0.0f,
+ /* rangeMax= */ 1.0f,
+ /* valueMin= */ digitInitialDelay,
+ /* valueMax= */ digitInitialDelay + AVAILABLE_ANIMATION_TIME,
+ /* value= */ fraction,
)
+ )
}
- // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often.
- // This is an optimization to ensure we only recompute the patterns when the inputs change.
+ /**
+ * DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often. This
+ * is a cache optimization to ensure we only recompute the patterns when the inputs change.
+ */
private object Patterns {
var sClockView12: String? = null
var sClockView24: String? = null
@@ -610,21 +642,22 @@ class AnimatableClockView @JvmOverloads constructor(
fun update(context: Context) {
val locale = Locale.getDefault()
- val res = context.resources
- val clockView12Skel = res.getString(R.string.clock_12hr_format)
- val clockView24Skel = res.getString(R.string.clock_24hr_format)
- val key = locale.toString() + clockView12Skel + clockView24Skel
- if (key == sCacheKey) return
-
- val clockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel)
- sClockView12 = clockView12
-
- // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton
- // format. The following code removes the AM/PM indicator if we didn't want it.
- if (!clockView12Skel.contains("a")) {
- sClockView12 = clockView12.replace("a".toRegex(), "").trim { it <= ' ' }
+ val clockView12Skel = context.resources.getString(R.string.clock_12hr_format)
+ val clockView24Skel = context.resources.getString(R.string.clock_24hr_format)
+ val key = "$locale$clockView12Skel$clockView24Skel"
+ if (key == sCacheKey) {
+ return
}
+ sClockView12 =
+ DateFormat.getBestDateTimePattern(locale, clockView12Skel).let {
+ // CLDR insists on adding an AM/PM indicator even though it wasn't in the format
+ // string. The following code removes the AM/PM indicator if we didn't want it.
+ if (!clockView12Skel.contains("a")) {
+ it.replace("a".toRegex(), "").trim { it <= ' ' }
+ } else it
+ }
+
sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel)
sCacheKey = key
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
index 3d8159e70061..9c9ee53d9c56 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
@@ -24,7 +24,6 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.util.settings.FakeSettings
-import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
@@ -66,7 +65,7 @@ class ColorInversionRepositoryImplTest : SysuiTestCase() {
runCurrent()
- Truth.assertThat(actualValue).isFalse()
+ assertThat(actualValue).isFalse()
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/NightDisplayRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/NightDisplayRepositoryTest.kt
new file mode 100644
index 000000000000..ca824cbdd53b
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/NightDisplayRepositoryTest.kt
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2024 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.accessibility.data.repository
+
+import android.hardware.display.ColorDisplayManager
+import android.hardware.display.NightDisplayListener
+import android.os.UserHandle
+import android.provider.Settings
+import android.testing.LeakCheck
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dagger.NightDisplayListenerModule
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.user.utils.UserScopedService
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.fakeGlobalSettings
+import com.android.systemui.util.settings.fakeSettings
+import com.android.systemui.utils.leaks.FakeLocationController
+import com.google.common.truth.Truth.assertThat
+import java.time.LocalTime
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.verify
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NightDisplayRepositoryTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val testUser = UserHandle.of(1)!!
+ private val testStartTime = LocalTime.MIDNIGHT
+ private val testEndTime = LocalTime.NOON
+ private val colorDisplayManager =
+ mock<ColorDisplayManager> {
+ whenever(nightDisplayAutoMode).thenReturn(ColorDisplayManager.AUTO_MODE_DISABLED)
+ whenever(isNightDisplayActivated).thenReturn(false)
+ whenever(nightDisplayCustomStartTime).thenReturn(testStartTime)
+ whenever(nightDisplayCustomEndTime).thenReturn(testEndTime)
+ }
+ private val locationController = FakeLocationController(LeakCheck())
+ private val nightDisplayListener = mock<NightDisplayListener>()
+ private val listenerBuilder =
+ mock<NightDisplayListenerModule.Builder> {
+ whenever(setUser(ArgumentMatchers.anyInt())).thenReturn(this)
+ whenever(build()).thenReturn(nightDisplayListener)
+ }
+ private val globalSettings = kosmos.fakeGlobalSettings
+ private val secureSettings = kosmos.fakeSettings
+ private val testDispatcher = StandardTestDispatcher()
+ private val scope = TestScope(testDispatcher)
+ private val userScopedColorDisplayManager =
+ mock<UserScopedService<ColorDisplayManager>> {
+ whenever(forUser(eq(testUser))).thenReturn(colorDisplayManager)
+ }
+
+ private val underTest =
+ NightDisplayRepository(
+ testDispatcher,
+ scope.backgroundScope,
+ globalSettings,
+ secureSettings,
+ listenerBuilder,
+ userScopedColorDisplayManager,
+ locationController,
+ )
+
+ @Before
+ fun setup() {
+ enrollInForcedNightDisplayAutoMode(INITIALLY_FORCE_AUTO_MODE, testUser)
+ }
+
+ @Test
+ fun nightDisplayState_matchesAutoMode() =
+ scope.runTest {
+ enrollInForcedNightDisplayAutoMode(INITIALLY_FORCE_AUTO_MODE, testUser)
+ val callbackCaptor = argumentCaptor<NightDisplayListener.Callback>()
+ val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+
+ runCurrent()
+
+ verify(nightDisplayListener).setCallback(callbackCaptor.capture())
+ val callback = callbackCaptor.value
+
+ assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_DISABLED)
+
+ callback.onAutoModeChanged(ColorDisplayManager.AUTO_MODE_CUSTOM_TIME)
+ assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_CUSTOM_TIME)
+
+ callback.onCustomStartTimeChanged(testStartTime)
+ assertThat(lastState!!.startTime).isEqualTo(testStartTime)
+
+ callback.onCustomEndTimeChanged(testEndTime)
+ assertThat(lastState!!.endTime).isEqualTo(testEndTime)
+
+ callback.onAutoModeChanged(ColorDisplayManager.AUTO_MODE_TWILIGHT)
+
+ assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_TWILIGHT)
+ }
+
+ @Test
+ fun nightDisplayState_matchesIsNightDisplayActivated() =
+ scope.runTest {
+ val callbackCaptor = argumentCaptor<NightDisplayListener.Callback>()
+
+ val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+ runCurrent()
+
+ verify(nightDisplayListener).setCallback(callbackCaptor.capture())
+ val callback = callbackCaptor.value
+ assertThat(lastState!!.isActivated)
+ .isEqualTo(colorDisplayManager.isNightDisplayActivated)
+
+ callback.onActivated(true)
+ assertThat(lastState!!.isActivated).isTrue()
+
+ callback.onActivated(false)
+ assertThat(lastState!!.isActivated).isFalse()
+ }
+
+ @Test
+ fun nightDisplayState_matchesController_initiallyCustomAutoMode() =
+ scope.runTest {
+ whenever(colorDisplayManager.nightDisplayAutoMode)
+ .thenReturn(ColorDisplayManager.AUTO_MODE_CUSTOM_TIME)
+
+ val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+ runCurrent()
+
+ assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_CUSTOM_TIME)
+ }
+
+ @Test
+ fun nightDisplayState_matchesController_initiallyTwilightAutoMode() =
+ scope.runTest {
+ whenever(colorDisplayManager.nightDisplayAutoMode)
+ .thenReturn(ColorDisplayManager.AUTO_MODE_TWILIGHT)
+
+ val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+ runCurrent()
+
+ assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_TWILIGHT)
+ }
+
+ @Test
+ fun nightDisplayState_matchesForceAutoMode() =
+ scope.runTest {
+ enrollInForcedNightDisplayAutoMode(false, testUser)
+ val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+ runCurrent()
+
+ assertThat(lastState!!.shouldForceAutoMode).isEqualTo(false)
+
+ enrollInForcedNightDisplayAutoMode(true, testUser)
+ assertThat(lastState!!.shouldForceAutoMode).isEqualTo(true)
+ }
+
+ private fun enrollInForcedNightDisplayAutoMode(enroll: Boolean, userHandle: UserHandle) {
+ globalSettings.putString(
+ Settings.Global.NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE,
+ if (enroll) NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE
+ else NIGHT_DISPLAY_FORCED_AUTO_MODE_UNAVAILABLE
+ )
+ secureSettings.putIntForUser(
+ Settings.Secure.NIGHT_DISPLAY_AUTO_MODE,
+ if (enroll) NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET else NIGHT_DISPLAY_AUTO_MODE_RAW_SET,
+ userHandle.identifier
+ )
+ }
+
+ private companion object {
+ const val INITIALLY_FORCE_AUTO_MODE = false
+ const val NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE = "1"
+ const val NIGHT_DISPLAY_FORCED_AUTO_MODE_UNAVAILABLE = "0"
+ const val NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET = -1
+ const val NIGHT_DISPLAY_AUTO_MODE_RAW_SET = 0
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractorTest.kt
new file mode 100644
index 000000000000..a0aa2d4a9a6c
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractorTest.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night.domain.interactor
+
+import android.hardware.display.ColorDisplayManager
+import android.hardware.display.NightDisplayListener
+import android.os.UserHandle
+import android.testing.LeakCheck
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.accessibility.data.repository.NightDisplayRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dagger.NightDisplayListenerModule
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.user.utils.UserScopedService
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.fakeGlobalSettings
+import com.android.systemui.util.settings.fakeSettings
+import com.android.systemui.util.time.DateFormatUtil
+import com.android.systemui.utils.leaks.FakeLocationController
+import com.google.common.truth.Truth.assertThat
+import java.time.LocalTime
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NightDisplayTileDataInteractorTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val testUser = UserHandle.of(1)!!
+ private val testStartTime = LocalTime.MIDNIGHT
+ private val testEndTime = LocalTime.NOON
+ private val colorDisplayManager =
+ mock<ColorDisplayManager> {
+ whenever(nightDisplayAutoMode).thenReturn(ColorDisplayManager.AUTO_MODE_DISABLED)
+ whenever(isNightDisplayActivated).thenReturn(false)
+ whenever(nightDisplayCustomStartTime).thenReturn(testStartTime)
+ whenever(nightDisplayCustomEndTime).thenReturn(testEndTime)
+ }
+ private val locationController = FakeLocationController(LeakCheck())
+ private val nightDisplayListener = mock<NightDisplayListener>()
+ private val listenerBuilder =
+ mock<NightDisplayListenerModule.Builder> {
+ whenever(setUser(anyInt())).thenReturn(this)
+ whenever(build()).thenReturn(nightDisplayListener)
+ }
+ private val globalSettings = kosmos.fakeGlobalSettings
+ private val secureSettings = kosmos.fakeSettings
+ private val dateFormatUtil = mock<DateFormatUtil> { whenever(is24HourFormat).thenReturn(false) }
+ private val testDispatcher = StandardTestDispatcher()
+ private val scope = TestScope(testDispatcher)
+ private val userScopedColorDisplayManager =
+ mock<UserScopedService<ColorDisplayManager>> {
+ whenever(forUser(eq(testUser))).thenReturn(colorDisplayManager)
+ }
+ private val nightDisplayRepository =
+ NightDisplayRepository(
+ testDispatcher,
+ scope.backgroundScope,
+ globalSettings,
+ secureSettings,
+ listenerBuilder,
+ userScopedColorDisplayManager,
+ locationController,
+ )
+
+ private val underTest: NightDisplayTileDataInteractor =
+ NightDisplayTileDataInteractor(context, dateFormatUtil, nightDisplayRepository)
+
+ @Test
+ fun availability_matchesColorDisplayManager() = runTest {
+ val availability by collectLastValue(underTest.availability(testUser))
+
+ val expectedAvailability = ColorDisplayManager.isNightDisplayAvailable(context)
+ assertThat(availability).isEqualTo(expectedAvailability)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractorTest.kt
new file mode 100644
index 000000000000..adc8bcba5a5c
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractorTest.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night.domain.interactor
+
+import android.hardware.display.ColorDisplayManager
+import android.hardware.display.NightDisplayListener
+import android.os.UserHandle
+import android.provider.Settings
+import android.testing.LeakCheck
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.accessibility.data.repository.NightDisplayRepository
+import com.android.systemui.dagger.NightDisplayListenerModule
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.actions.intentInputs
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
+import com.android.systemui.qs.tiles.impl.custom.qsTileLogger
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.user.utils.UserScopedService
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.fakeGlobalSettings
+import com.android.systemui.util.settings.fakeSettings
+import com.android.systemui.utils.leaks.FakeLocationController
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NightDisplayTileUserActionInteractorTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val qsTileIntentUserActionHandler = FakeQSTileIntentUserInputHandler()
+ private val testUser = UserHandle.of(1)
+ private val colorDisplayManager =
+ mock<ColorDisplayManager> {
+ whenever(nightDisplayAutoMode).thenReturn(ColorDisplayManager.AUTO_MODE_DISABLED)
+ whenever(isNightDisplayActivated).thenReturn(false)
+ }
+ private val locationController = FakeLocationController(LeakCheck())
+ private val nightDisplayListener = mock<NightDisplayListener>()
+ private val listenerBuilder =
+ mock<NightDisplayListenerModule.Builder> {
+ whenever(setUser(ArgumentMatchers.anyInt())).thenReturn(this)
+ whenever(build()).thenReturn(nightDisplayListener)
+ }
+ private val globalSettings = kosmos.fakeGlobalSettings
+ private val secureSettings = kosmos.fakeSettings
+ private val testDispatcher = StandardTestDispatcher()
+ private val scope = TestScope(testDispatcher)
+ private val userScopedColorDisplayManager =
+ mock<UserScopedService<ColorDisplayManager>> {
+ whenever(forUser(eq(testUser))).thenReturn(colorDisplayManager)
+ }
+ private val nightDisplayRepository =
+ NightDisplayRepository(
+ testDispatcher,
+ scope.backgroundScope,
+ globalSettings,
+ secureSettings,
+ listenerBuilder,
+ userScopedColorDisplayManager,
+ locationController,
+ )
+
+ private val underTest =
+ NightDisplayTileUserActionInteractor(
+ nightDisplayRepository,
+ qsTileIntentUserActionHandler,
+ kosmos.qsTileLogger
+ )
+
+ @Test
+ fun handleClick_inactive_activates() =
+ scope.runTest {
+ val startingModel = NightDisplayTileModel.AutoModeOff(false, false)
+
+ underTest.handleInput(QSTileInputTestKtx.click(startingModel, testUser))
+
+ verify(colorDisplayManager).setNightDisplayActivated(true)
+ }
+
+ @Test
+ fun handleClick_active_disables() =
+ scope.runTest {
+ val startingModel = NightDisplayTileModel.AutoModeOff(true, false)
+
+ underTest.handleInput(QSTileInputTestKtx.click(startingModel, testUser))
+
+ verify(colorDisplayManager).setNightDisplayActivated(false)
+ }
+
+ @Test
+ fun handleClick_whenAutoModeTwilight_flipsState() =
+ scope.runTest {
+ val originalState = true
+ val startingModel = NightDisplayTileModel.AutoModeTwilight(originalState, false, false)
+
+ underTest.handleInput(QSTileInputTestKtx.click(startingModel, testUser))
+
+ verify(colorDisplayManager).setNightDisplayActivated(!originalState)
+ }
+
+ @Test
+ fun handleClick_whenAutoModeCustom_flipsState() =
+ scope.runTest {
+ val originalState = true
+ val startingModel =
+ NightDisplayTileModel.AutoModeCustom(originalState, false, null, null, false)
+
+ underTest.handleInput(QSTileInputTestKtx.click(startingModel, testUser))
+
+ verify(colorDisplayManager).setNightDisplayActivated(!originalState)
+ }
+
+ @Test
+ fun handleLongClickWhenEnabled() =
+ scope.runTest {
+ val enabledState = true
+
+ underTest.handleInput(
+ QSTileInputTestKtx.longClick(
+ NightDisplayTileModel.AutoModeOff(enabledState, false),
+ testUser
+ )
+ )
+
+ assertThat(qsTileIntentUserActionHandler.handledInputs).hasSize(1)
+
+ val intentInput = qsTileIntentUserActionHandler.intentInputs.last()
+ val actualIntentAction = intentInput.intent.action
+ val expectedIntentAction = Settings.ACTION_NIGHT_DISPLAY_SETTINGS
+ assertThat(actualIntentAction).isEqualTo(expectedIntentAction)
+ }
+
+ @Test
+ fun handleLongClickWhenDisabled() =
+ scope.runTest {
+ val enabledState = false
+
+ underTest.handleInput(
+ QSTileInputTestKtx.longClick(
+ NightDisplayTileModel.AutoModeOff(enabledState, false),
+ testUser
+ )
+ )
+
+ assertThat(qsTileIntentUserActionHandler.handledInputs).hasSize(1)
+
+ val intentInput = qsTileIntentUserActionHandler.intentInputs.last()
+ val actualIntentAction = intentInput.intent.action
+ val expectedIntentAction = Settings.ACTION_NIGHT_DISPLAY_SETTINGS
+ assertThat(actualIntentAction).isEqualTo(expectedIntentAction)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt
new file mode 100644
index 000000000000..5d2e7013c2f4
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night.ui
+
+import android.graphics.drawable.TestStubDrawable
+import android.service.quicksettings.Tile
+import android.text.TextUtils
+import android.widget.Switch
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.qs.tiles.impl.night.qsNightDisplayTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import com.android.systemui.util.mockito.mock
+import java.time.LocalTime
+import java.time.format.DateTimeFormatter
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NightDisplayTileMapperTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val config = kosmos.qsNightDisplayTileConfig
+
+ private val testStartTime = LocalTime.MIDNIGHT
+ private val testEndTime = LocalTime.NOON
+
+ private lateinit var mapper: NightDisplayTileMapper
+
+ @Before
+ fun setup() {
+ mapper =
+ NightDisplayTileMapper(
+ context.orCreateTestableResources
+ .apply {
+ addOverride(R.drawable.qs_nightlight_icon_on, TestStubDrawable())
+ addOverride(R.drawable.qs_nightlight_icon_off, TestStubDrawable())
+ }
+ .resources,
+ context.theme,
+ mock<QSTileLogger>(),
+ )
+ }
+
+ @Test
+ fun disabledModel_whenAutoModeOff() {
+ val inputModel = NightDisplayTileModel.AutoModeOff(false, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.INACTIVE,
+ context.resources.getStringArray(R.array.tile_states_night)[Tile.STATE_INACTIVE]
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ /** Force enable does not change the mode by itself. */
+ @Test
+ fun disabledModel_whenAutoModeOff_whenForceEnable() {
+ val inputModel = NightDisplayTileModel.AutoModeOff(false, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.INACTIVE,
+ context.resources.getStringArray(R.array.tile_states_night)[Tile.STATE_INACTIVE]
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun enabledModel_whenAutoModeOff() {
+ val inputModel = NightDisplayTileModel.AutoModeOff(true, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.ACTIVE,
+ context.resources.getStringArray(R.array.tile_states_night)[Tile.STATE_ACTIVE]
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun enabledModel_forceAutoMode_whenAutoModeOff() {
+ val inputModel = NightDisplayTileModel.AutoModeOff(true, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.ACTIVE,
+ context.resources.getStringArray(R.array.tile_states_night)[Tile.STATE_ACTIVE]
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun enabledModel_autoModeTwilight_locationOff() {
+ val inputModel = NightDisplayTileModel.AutoModeTwilight(true, false, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState = createNightDisplayTileState(QSTileState.ActivationState.ACTIVE, null)
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun enabledModel_autoModeTwilight_locationOn() {
+ val inputModel = NightDisplayTileModel.AutoModeTwilight(true, false, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.ACTIVE,
+ context.getString(R.string.quick_settings_night_secondary_label_until_sunrise)
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun disabledModel_autoModeTwilight_locationOn() {
+ val inputModel = NightDisplayTileModel.AutoModeTwilight(false, false, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.INACTIVE,
+ context.getString(R.string.quick_settings_night_secondary_label_on_at_sunset)
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun disabledModel_autoModeTwilight_locationOff() {
+ val inputModel = NightDisplayTileModel.AutoModeTwilight(false, false, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState = createNightDisplayTileState(QSTileState.ActivationState.INACTIVE, null)
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun disabledModel_autoModeCustom_24Hour() {
+ val inputModel =
+ NightDisplayTileModel.AutoModeCustom(false, false, testStartTime, null, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.INACTIVE,
+ context.getString(
+ R.string.quick_settings_night_secondary_label_on_at,
+ formatter24Hour.format(testStartTime)
+ )
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun disabledModel_autoModeCustom_12Hour() {
+ val inputModel =
+ NightDisplayTileModel.AutoModeCustom(false, false, testStartTime, null, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.INACTIVE,
+ context.getString(
+ R.string.quick_settings_night_secondary_label_on_at,
+ formatter12Hour.format(testStartTime)
+ )
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ /** Should have the same outcome as [disabledModel_autoModeCustom_12Hour] */
+ @Test
+ fun disabledModel_autoModeCustom_12Hour_isEnrolledForcedAutoMode() {
+ val inputModel =
+ NightDisplayTileModel.AutoModeCustom(false, true, testStartTime, null, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.INACTIVE,
+ context.getString(
+ R.string.quick_settings_night_secondary_label_on_at,
+ formatter12Hour.format(testStartTime)
+ )
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun enabledModel_autoModeCustom_24Hour() {
+ val inputModel = NightDisplayTileModel.AutoModeCustom(true, false, null, testEndTime, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.ACTIVE,
+ context.getString(
+ R.string.quick_settings_secondary_label_until,
+ formatter24Hour.format(testEndTime)
+ )
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun enabledModel_autoModeCustom_12Hour() {
+ val inputModel = NightDisplayTileModel.AutoModeCustom(true, false, null, testEndTime, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.ACTIVE,
+ context.getString(
+ R.string.quick_settings_secondary_label_until,
+ formatter12Hour.format(testEndTime)
+ )
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ /** Should have the same state as [enabledModel_autoModeCustom_24Hour] */
+ @Test
+ fun enabledModel_autoModeCustom_24Hour_forceEnabled() {
+ val inputModel = NightDisplayTileModel.AutoModeCustom(true, true, null, testEndTime, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.ACTIVE,
+ context.getString(
+ R.string.quick_settings_secondary_label_until,
+ formatter24Hour.format(testEndTime)
+ )
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ private fun createNightDisplayTileState(
+ activationState: QSTileState.ActivationState,
+ secondaryLabel: String?
+ ): QSTileState {
+ val label = context.getString(R.string.quick_settings_night_display_label)
+
+ val contentDescription =
+ if (TextUtils.isEmpty(secondaryLabel)) label
+ else TextUtils.concat(label, ", ", secondaryLabel)
+ return QSTileState(
+ {
+ Icon.Loaded(
+ context.getDrawable(
+ if (activationState == QSTileState.ActivationState.ACTIVE)
+ R.drawable.qs_nightlight_icon_on
+ else R.drawable.qs_nightlight_icon_off
+ )!!,
+ null
+ )
+ },
+ label,
+ activationState,
+ secondaryLabel,
+ setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK),
+ contentDescription,
+ null,
+ QSTileState.SideViewIcon.None,
+ QSTileState.EnabledState.ENABLED,
+ Switch::class.qualifiedName
+ )
+ }
+
+ private companion object {
+ val formatter12Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("hh:mm a")
+ val formatter24Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm")
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index 21dc953e79d1..f06e04b70809 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -33,7 +33,10 @@ import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.flags.parameterizeSceneContainerFlag
import com.android.systemui.jank.interactionJankMonitor
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -69,7 +72,7 @@ class StatusBarStateControllerImplTest(flags: FlagsParameterization) : SysuiTest
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
-
+ private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
private val mockDarkAnimator = mock<ObjectAnimator>()
private lateinit var underTest: StatusBarStateControllerImpl
@@ -98,6 +101,7 @@ class StatusBarStateControllerImplTest(flags: FlagsParameterization) : SysuiTest
uiEventLogger,
{ kosmos.interactionJankMonitor },
JavaAdapter(testScope.backgroundScope),
+ { kosmos.keyguardTransitionInteractor },
{ kosmos.shadeInteractor },
{ kosmos.deviceUnlockedInteractor },
{ kosmos.sceneInteractor },
@@ -330,4 +334,25 @@ class StatusBarStateControllerImplTest(flags: FlagsParameterization) : SysuiTest
assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
assertThat(statusBarState).isEqualTo(StatusBarState.SHADE)
}
+
+ @Test
+ fun leaveOpenOnKeyguard_whenGone_isFalse() =
+ testScope.runTest {
+ underTest.start()
+ underTest.setLeaveOpenOnKeyguardHide(true)
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = KeyguardState.LOCKSCREEN,
+ testScope = testScope,
+ )
+ assertThat(underTest.leaveOpenOnKeyguardHide()).isEqualTo(true)
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope = testScope,
+ )
+ assertThat(underTest.leaveOpenOnKeyguardHide()).isEqualTo(false)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayChangeEvent.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayChangeEvent.kt
new file mode 100644
index 000000000000..8f071e4bc874
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayChangeEvent.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 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.accessibility.data.model
+
+import java.time.LocalTime
+
+sealed interface NightDisplayChangeEvent {
+ data class OnAutoModeChanged(val autoMode: Int) : NightDisplayChangeEvent
+ data class OnActivatedChanged(val isActivated: Boolean) : NightDisplayChangeEvent
+ data class OnCustomStartTimeChanged(val startTime: LocalTime?) : NightDisplayChangeEvent
+ data class OnCustomEndTimeChanged(val endTime: LocalTime?) : NightDisplayChangeEvent
+ data class OnForceAutoModeChanged(val shouldForceAutoMode: Boolean) : NightDisplayChangeEvent
+ data class OnLocationEnabledChanged(val locationEnabled: Boolean) : NightDisplayChangeEvent
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayState.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayState.kt
new file mode 100644
index 000000000000..196876e541b1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayState.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 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.accessibility.data.model
+
+import java.time.LocalTime
+
+/** models the state of NightDisplayRepository */
+data class NightDisplayState(
+ val autoMode: Int = 0,
+ val isActivated: Boolean = true,
+ val startTime: LocalTime? = null,
+ val endTime: LocalTime? = null,
+ val shouldForceAutoMode: Boolean = false,
+ val locationEnabled: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/NightDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/NightDisplayRepository.kt
new file mode 100644
index 000000000000..bf44fabc31c4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/NightDisplayRepository.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2024 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.accessibility.data.repository
+
+import android.hardware.display.ColorDisplayManager
+import android.hardware.display.NightDisplayListener
+import android.os.UserHandle
+import android.provider.Settings
+import com.android.systemui.accessibility.data.model.NightDisplayChangeEvent
+import com.android.systemui.accessibility.data.model.NightDisplayState
+import com.android.systemui.dagger.NightDisplayListenerModule
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.policy.LocationController
+import com.android.systemui.user.utils.UserScopedService
+import com.android.systemui.util.kotlin.isLocationEnabledFlow
+import com.android.systemui.util.settings.GlobalSettings
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import java.time.LocalTime
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.scan
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+class NightDisplayRepository
+@Inject
+constructor(
+ @Background private val bgCoroutineContext: CoroutineContext,
+ @Application private val scope: CoroutineScope,
+ private val globalSettings: GlobalSettings,
+ private val secureSettings: SecureSettings,
+ private val nightDisplayListenerBuilder: NightDisplayListenerModule.Builder,
+ private val colorDisplayManagerUserScopedService: UserScopedService<ColorDisplayManager>,
+ private val locationController: LocationController,
+) {
+ private val stateFlowUserMap = mutableMapOf<Int, Flow<NightDisplayState>>()
+
+ fun nightDisplayState(user: UserHandle): Flow<NightDisplayState> =
+ stateFlowUserMap.getOrPut(user.identifier) {
+ return merge(
+ colorDisplayManagerChangeEventFlow(user),
+ shouldForceAutoMode(user).map {
+ NightDisplayChangeEvent.OnForceAutoModeChanged(it)
+ },
+ locationController.isLocationEnabledFlow().map {
+ NightDisplayChangeEvent.OnLocationEnabledChanged(it)
+ }
+ )
+ .scan(initialState(user)) { state, event ->
+ when (event) {
+ is NightDisplayChangeEvent.OnActivatedChanged ->
+ state.copy(isActivated = event.isActivated)
+ is NightDisplayChangeEvent.OnAutoModeChanged ->
+ state.copy(autoMode = event.autoMode)
+ is NightDisplayChangeEvent.OnCustomStartTimeChanged ->
+ state.copy(startTime = event.startTime)
+ is NightDisplayChangeEvent.OnCustomEndTimeChanged ->
+ state.copy(endTime = event.endTime)
+ is NightDisplayChangeEvent.OnForceAutoModeChanged ->
+ state.copy(shouldForceAutoMode = event.shouldForceAutoMode)
+ is NightDisplayChangeEvent.OnLocationEnabledChanged ->
+ state.copy(locationEnabled = event.locationEnabled)
+ }
+ }
+ .conflate()
+ .onStart { emit(initialState(user)) }
+ .flowOn(bgCoroutineContext)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), NightDisplayState())
+ }
+
+ /** Track changes in night display enabled state and its auto mode */
+ private fun colorDisplayManagerChangeEventFlow(user: UserHandle) = callbackFlow {
+ val nightDisplayListener = nightDisplayListenerBuilder.setUser(user.identifier).build()
+ val nightDisplayCallback =
+ object : NightDisplayListener.Callback {
+ override fun onActivated(activated: Boolean) {
+ trySend(NightDisplayChangeEvent.OnActivatedChanged(activated))
+ }
+
+ override fun onAutoModeChanged(autoMode: Int) {
+ trySend(NightDisplayChangeEvent.OnAutoModeChanged(autoMode))
+ }
+
+ override fun onCustomStartTimeChanged(startTime: LocalTime?) {
+ trySend(NightDisplayChangeEvent.OnCustomStartTimeChanged(startTime))
+ }
+
+ override fun onCustomEndTimeChanged(endTime: LocalTime?) {
+ trySend(NightDisplayChangeEvent.OnCustomEndTimeChanged(endTime))
+ }
+ }
+ nightDisplayListener.setCallback(nightDisplayCallback)
+ awaitClose { nightDisplayListener.setCallback(null) }
+ }
+
+ /** @return true when the option to force auto mode is available and a value has not been set */
+ private fun shouldForceAutoMode(userHandle: UserHandle): Flow<Boolean> =
+ combine(isForceAutoModeAvailable, isDisplayAutoModeRawNotSet(userHandle)) {
+ isForceAutoModeAvailable,
+ isDisplayAutoModeRawNotSet,
+ ->
+ isForceAutoModeAvailable && isDisplayAutoModeRawNotSet
+ }
+
+ private val isForceAutoModeAvailable: Flow<Boolean> =
+ globalSettings
+ .observerFlow(IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME)
+ .onStart { emit(Unit) }
+ .map {
+ globalSettings.getString(IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME) ==
+ NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE
+ }
+ .distinctUntilChanged()
+
+ /** Inspired by [ColorDisplayService.getNightDisplayAutoModeRawInternal] */
+ private fun isDisplayAutoModeRawNotSet(userHandle: UserHandle): Flow<Boolean> =
+ if (userHandle.identifier == UserHandle.USER_NULL) {
+ flowOf(IS_AUTO_MODE_RAW_NOT_SET_DEFAULT)
+ } else {
+ secureSettings
+ .observerFlow(userHandle.identifier, DISPLAY_AUTO_MODE_RAW_SETTING_NAME)
+ .onStart { emit(Unit) }
+ .map {
+ secureSettings.getIntForUser(
+ DISPLAY_AUTO_MODE_RAW_SETTING_NAME,
+ userHandle.identifier
+ ) == NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET
+ }
+ }
+ .distinctUntilChanged()
+
+ suspend fun setNightDisplayAutoMode(autoMode: Int, user: UserHandle) {
+ withContext(bgCoroutineContext) {
+ colorDisplayManagerUserScopedService.forUser(user).nightDisplayAutoMode = autoMode
+ }
+ }
+
+ suspend fun setNightDisplayActivated(activated: Boolean, user: UserHandle) {
+ withContext(bgCoroutineContext) {
+ colorDisplayManagerUserScopedService.forUser(user).isNightDisplayActivated = activated
+ }
+ }
+
+ private fun initialState(user: UserHandle): NightDisplayState {
+ val colorDisplayManager = colorDisplayManagerUserScopedService.forUser(user)
+ return NightDisplayState(
+ colorDisplayManager.nightDisplayAutoMode,
+ colorDisplayManager.isNightDisplayActivated,
+ colorDisplayManager.nightDisplayCustomStartTime,
+ colorDisplayManager.nightDisplayCustomEndTime,
+ globalSettings.getString(IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME) ==
+ NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE &&
+ secureSettings.getIntForUser(DISPLAY_AUTO_MODE_RAW_SETTING_NAME, user.identifier) ==
+ NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET,
+ locationController.isLocationEnabled,
+ )
+ }
+
+ private companion object {
+ const val NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET = -1
+ const val NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE = "1"
+ const val IS_AUTO_MODE_RAW_NOT_SET_DEFAULT = true
+ const val IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME =
+ Settings.Global.NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE
+ const val DISPLAY_AUTO_MODE_RAW_SETTING_NAME = Settings.Secure.NIGHT_DISPLAY_AUTO_MODE
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
index 54dd6d00aa48..ed9597ddf559 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
@@ -41,6 +41,10 @@ import com.android.systemui.qs.tiles.impl.inversion.domain.ColorInversionTileMap
import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionTileDataInteractor
import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionUserActionInteractor
import com.android.systemui.qs.tiles.impl.inversion.domain.model.ColorInversionTileModel
+import com.android.systemui.qs.tiles.impl.night.domain.interactor.NightDisplayTileDataInteractor
+import com.android.systemui.qs.tiles.impl.night.domain.interactor.NightDisplayTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.qs.tiles.impl.night.ui.NightDisplayTileMapper
import com.android.systemui.qs.tiles.impl.onehanded.domain.OneHandedModeTileDataInteractor
import com.android.systemui.qs.tiles.impl.onehanded.domain.OneHandedModeTileUserActionInteractor
import com.android.systemui.qs.tiles.impl.onehanded.domain.model.OneHandedModeTileModel
@@ -117,6 +121,7 @@ interface QSAccessibilityModule {
const val FONT_SCALING_TILE_SPEC = "font_scaling"
const val REDUCE_BRIGHTNESS_TILE_SPEC = "reduce_brightness"
const val ONE_HANDED_TILE_SPEC = "onehanded"
+ const val NIGHT_DISPLAY_TILE_SPEC = "night"
@Provides
@IntoMap
@@ -279,5 +284,41 @@ interface QSAccessibilityModule {
mapper,
)
else StubQSTileViewModel
+
+ @Provides
+ @IntoMap
+ @StringKey(NIGHT_DISPLAY_TILE_SPEC)
+ fun provideNightDisplayTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+ QSTileConfig(
+ tileSpec = TileSpec.create(NIGHT_DISPLAY_TILE_SPEC),
+ uiConfig =
+ QSTileUIConfig.Resource(
+ iconRes = R.drawable.qs_nightlight_icon_off,
+ labelRes = R.string.quick_settings_night_display_label,
+ ),
+ instanceId = uiEventLogger.getNewInstanceId(),
+ )
+
+ /**
+ * Inject NightDisplay Tile into tileViewModelMap in QSModule. The tile is hidden behind a
+ * flag.
+ */
+ @Provides
+ @IntoMap
+ @StringKey(NIGHT_DISPLAY_TILE_SPEC)
+ fun provideNightDisplayTileViewModel(
+ factory: QSTileViewModelFactory.Static<NightDisplayTileModel>,
+ mapper: NightDisplayTileMapper,
+ stateInteractor: NightDisplayTileDataInteractor,
+ userActionInteractor: NightDisplayTileUserActionInteractor
+ ): QSTileViewModel =
+ if (Flags.qsNewTilesFuture())
+ factory.create(
+ TileSpec.create(NIGHT_DISPLAY_TILE_SPEC),
+ userActionInteractor,
+ stateInteractor,
+ mapper,
+ )
+ else StubQSTileViewModel
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index e00137e3045e..11e6f7a8c38c 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -225,6 +225,13 @@ public class FrameworkServicesModule {
@Provides
@Singleton
+ static UserScopedService<ColorDisplayManager> provideScopedColorDisplayManager(
+ Context context) {
+ return new UserScopedServiceImpl<>(context, ColorDisplayManager.class);
+ }
+
+ @Provides
+ @Singleton
static CrossWindowBlurListeners provideCrossWindowBlurListeners() {
return CrossWindowBlurListeners.getInstance();
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index e32bfcf81fe2..7f3274c09037 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -134,7 +134,7 @@ constructor(
TransitionInfo(
ownerName = "",
from = KeyguardState.OFF,
- to = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.OFF,
animator = null
)
)
@@ -266,6 +266,14 @@ constructor(
}
override suspend fun emitInitialStepsFromOff(to: KeyguardState) {
+ _currentTransitionInfo.value =
+ TransitionInfo(
+ ownerName = "KeyguardTransitionRepository(boot)",
+ from = KeyguardState.OFF,
+ to = to,
+ animator = null
+ )
+
emitTransition(
TransitionStep(
KeyguardState.OFF,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
index b515ce07cc02..278352c6f69b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
@@ -28,6 +28,7 @@ import com.android.systemui.log.ConstantStringsLoggerImpl
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel.DEBUG
import com.android.systemui.log.core.LogLevel.ERROR
+import com.android.systemui.log.core.LogLevel.INFO
import com.android.systemui.log.core.LogLevel.VERBOSE
import com.android.systemui.log.dagger.QSConfigLog
import com.android.systemui.log.dagger.QSLog
@@ -56,6 +57,9 @@ constructor(
fun d(@CompileTimeConstant msg: String, arg: Any) {
buffer.log(TAG, DEBUG, { str1 = arg.toString() }, { "$msg: $str1" })
}
+ fun i(@CompileTimeConstant msg: String, arg: Any) {
+ buffer.log(TAG, INFO, { str1 = arg.toString() }, { "$msg: $str1" })
+ }
fun logTileAdded(tileSpec: String) {
buffer.log(TAG, DEBUG, { str1 = tileSpec }, { "[$str1] Tile added" })
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
index 065e89f10ef6..f0d72065397d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
@@ -175,6 +175,26 @@ constructor(
)
}
+ /** Log with level [LogLevel.WARNING] */
+ fun logWarning(
+ tileSpec: TileSpec,
+ message: String,
+ ) {
+ tileSpec
+ .getLogBuffer()
+ .log(tileSpec.getLogTag(), LogLevel.WARNING, { str1 = message }, { str1!! })
+ }
+
+ /** Log with level [LogLevel.INFO] */
+ fun logInfo(
+ tileSpec: TileSpec,
+ message: String,
+ ) {
+ tileSpec
+ .getLogBuffer()
+ .log(tileSpec.getLogTag(), LogLevel.INFO, { str1 = message }, { str1!! })
+ }
+
fun logCustomTileUserActionDelivered(tileSpec: TileSpec) {
tileSpec
.getLogBuffer()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileDataInteractor.kt
index d1c80309a1cc..bd2f2c987ccf 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileDataInteractor.kt
@@ -17,15 +17,15 @@
package com.android.systemui.qs.tiles.impl.location.domain.interactor
import android.os.UserHandle
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
import com.android.systemui.qs.tiles.impl.location.domain.model.LocationTileModel
import com.android.systemui.statusbar.policy.LocationController
+import com.android.systemui.util.kotlin.isLocationEnabledFlow
import javax.inject.Inject
-import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
/** Observes location state changes providing the [LocationTileModel]. */
class LocationTileDataInteractor
@@ -38,19 +38,7 @@ constructor(
user: UserHandle,
triggers: Flow<DataUpdateTrigger>
): Flow<LocationTileModel> =
- ConflatedCallbackFlow.conflatedCallbackFlow {
- val initialValue = locationController.isLocationEnabled
- trySend(LocationTileModel(initialValue))
-
- val callback =
- object : LocationController.LocationChangeCallback {
- override fun onLocationSettingsChanged(locationEnabled: Boolean) {
- trySend(LocationTileModel(locationEnabled))
- }
- }
- locationController.addCallback(callback)
- awaitClose { locationController.removeCallback(callback) }
- }
+ locationController.isLocationEnabledFlow().map { LocationTileModel(it) }
override fun availability(user: UserHandle): Flow<Boolean> = flowOf(true)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractor.kt
new file mode 100644
index 000000000000..88bd224881b5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractor.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night.domain.interactor
+
+import android.content.Context
+import android.hardware.display.ColorDisplayManager
+import android.os.UserHandle
+import com.android.systemui.accessibility.data.repository.NightDisplayRepository
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.util.time.DateFormatUtil
+import java.time.LocalTime
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+/** Observes screen record state changes providing the [NightDisplayTileModel]. */
+class NightDisplayTileDataInteractor
+@Inject
+constructor(
+ @Application private val context: Context,
+ private val dateFormatUtil: DateFormatUtil,
+ private val nightDisplayRepository: NightDisplayRepository,
+) : QSTileDataInteractor<NightDisplayTileModel> {
+
+ override fun tileData(
+ user: UserHandle,
+ triggers: Flow<DataUpdateTrigger>
+ ): Flow<NightDisplayTileModel> =
+ nightDisplayRepository.nightDisplayState(user).map {
+ generateModel(
+ it.autoMode,
+ it.isActivated,
+ it.startTime,
+ it.endTime,
+ it.shouldForceAutoMode,
+ it.locationEnabled
+ )
+ }
+
+ /** This checks resources and there fore does not make a binder call. */
+ override fun availability(user: UserHandle): Flow<Boolean> =
+ flowOf(ColorDisplayManager.isNightDisplayAvailable(context))
+
+ private fun generateModel(
+ autoMode: Int,
+ isNightDisplayActivated: Boolean,
+ customStartTime: LocalTime?,
+ customEndTime: LocalTime?,
+ shouldForceAutoMode: Boolean,
+ locationEnabled: Boolean,
+ ): NightDisplayTileModel {
+ if (autoMode == ColorDisplayManager.AUTO_MODE_TWILIGHT) {
+ return NightDisplayTileModel.AutoModeTwilight(
+ isNightDisplayActivated,
+ shouldForceAutoMode,
+ locationEnabled,
+ )
+ } else if (autoMode == ColorDisplayManager.AUTO_MODE_CUSTOM_TIME) {
+ return NightDisplayTileModel.AutoModeCustom(
+ isNightDisplayActivated,
+ shouldForceAutoMode,
+ customStartTime,
+ customEndTime,
+ dateFormatUtil.is24HourFormat,
+ )
+ } else { // auto mode off
+ return NightDisplayTileModel.AutoModeOff(isNightDisplayActivated, shouldForceAutoMode)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractor.kt
new file mode 100644
index 000000000000..5cee8c49527d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractor.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night.domain.interactor
+
+import android.content.Intent
+import android.hardware.display.ColorDisplayManager.AUTO_MODE_CUSTOM_TIME
+import android.provider.Settings
+import com.android.systemui.accessibility.data.repository.NightDisplayRepository
+import com.android.systemui.accessibility.qs.QSAccessibilityModule
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import javax.inject.Inject
+
+/** Handles night display tile clicks. */
+class NightDisplayTileUserActionInteractor
+@Inject
+constructor(
+ private val nightDisplayRepository: NightDisplayRepository,
+ private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
+ private val qsLogger: QSTileLogger,
+) : QSTileUserActionInteractor<NightDisplayTileModel> {
+ override suspend fun handleInput(input: QSTileInput<NightDisplayTileModel>): Unit =
+ with(input) {
+ when (action) {
+ is QSTileUserAction.Click -> {
+ // Enroll in forced auto mode if eligible.
+ if (data.isEnrolledInForcedNightDisplayAutoMode) {
+ nightDisplayRepository.setNightDisplayAutoMode(AUTO_MODE_CUSTOM_TIME, user)
+ qsLogger.logInfo(spec, "Enrolled in forced night display auto mode")
+ }
+ nightDisplayRepository.setNightDisplayActivated(!data.isActivated, user)
+ }
+ is QSTileUserAction.LongClick -> {
+ qsTileIntentUserActionHandler.handle(
+ action.expandable,
+ Intent(Settings.ACTION_NIGHT_DISPLAY_SETTINGS)
+ )
+ }
+ }
+ }
+
+ companion object {
+ val spec = TileSpec.create(QSAccessibilityModule.NIGHT_DISPLAY_TILE_SPEC)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/model/NightDisplayTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/model/NightDisplayTileModel.kt
new file mode 100644
index 000000000000..6b1bd5bc3512
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/model/NightDisplayTileModel.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night.domain.model
+
+import java.time.LocalTime
+
+/** Data model for night display tile */
+sealed interface NightDisplayTileModel {
+ val isActivated: Boolean
+ val isEnrolledInForcedNightDisplayAutoMode: Boolean
+ data class AutoModeTwilight(
+ override val isActivated: Boolean,
+ override val isEnrolledInForcedNightDisplayAutoMode: Boolean,
+ val isLocationEnabled: Boolean
+ ) : NightDisplayTileModel
+ data class AutoModeCustom(
+ override val isActivated: Boolean,
+ override val isEnrolledInForcedNightDisplayAutoMode: Boolean,
+ val startTime: LocalTime?,
+ val endTime: LocalTime?,
+ val is24HourFormat: Boolean
+ ) : NightDisplayTileModel
+ data class AutoModeOff(
+ override val isActivated: Boolean,
+ override val isEnrolledInForcedNightDisplayAutoMode: Boolean
+ ) : NightDisplayTileModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt
new file mode 100644
index 000000000000..5c2dcfcaf37c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night.ui
+
+import android.content.res.Resources
+import android.service.quicksettings.Tile
+import android.text.TextUtils
+import androidx.annotation.StringRes
+import com.android.systemui.accessibility.qs.QSAccessibilityModule
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import java.time.DateTimeException
+import java.time.LocalTime
+import java.time.format.DateTimeFormatter
+import javax.inject.Inject
+
+/** Maps [NightDisplayTileModel] to [QSTileState]. */
+class NightDisplayTileMapper
+@Inject
+constructor(
+ @Main private val resources: Resources,
+ private val theme: Resources.Theme,
+ private val logger: QSTileLogger,
+) : QSTileDataToStateMapper<NightDisplayTileModel> {
+ override fun map(config: QSTileConfig, data: NightDisplayTileModel): QSTileState =
+ QSTileState.build(resources, theme, config.uiConfig) {
+ label = resources.getString(R.string.quick_settings_night_display_label)
+ supportedActions =
+ setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+ sideViewIcon = QSTileState.SideViewIcon.None
+
+ if (data.isActivated) {
+ activationState = QSTileState.ActivationState.ACTIVE
+ val loadedIcon =
+ Icon.Loaded(
+ resources.getDrawable(R.drawable.qs_nightlight_icon_on, theme),
+ contentDescription = null
+ )
+ icon = { loadedIcon }
+ } else {
+ activationState = QSTileState.ActivationState.INACTIVE
+ val loadedIcon =
+ Icon.Loaded(
+ resources.getDrawable(R.drawable.qs_nightlight_icon_off, theme),
+ contentDescription = null
+ )
+ icon = { loadedIcon }
+ }
+
+ secondaryLabel = getSecondaryLabel(data, resources)
+
+ contentDescription =
+ if (TextUtils.isEmpty(secondaryLabel)) label
+ else TextUtils.concat(label, ", ", secondaryLabel)
+ }
+
+ private fun getSecondaryLabel(
+ data: NightDisplayTileModel,
+ resources: Resources
+ ): CharSequence? {
+ when (data) {
+ is NightDisplayTileModel.AutoModeTwilight -> {
+ if (!data.isLocationEnabled) {
+ return null
+ } else {
+ return resources.getString(
+ if (data.isActivated)
+ R.string.quick_settings_night_secondary_label_until_sunrise
+ else R.string.quick_settings_night_secondary_label_on_at_sunset
+ )
+ }
+ }
+ is NightDisplayTileModel.AutoModeOff -> {
+ val subtitleArray = resources.getStringArray(R.array.tile_states_night)
+ return subtitleArray[
+ if (data.isActivated) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE]
+ }
+ is NightDisplayTileModel.AutoModeCustom -> {
+ // User-specified time, approximated to the nearest hour.
+ @StringRes val toggleTimeStringRes: Int
+ val toggleTime: LocalTime
+ if (data.isActivated) {
+ toggleTime = data.endTime ?: return null
+ toggleTimeStringRes = R.string.quick_settings_secondary_label_until
+ } else {
+ toggleTime = data.startTime ?: return null
+ toggleTimeStringRes = R.string.quick_settings_night_secondary_label_on_at
+ }
+
+ try {
+ val formatter = if (data.is24HourFormat) formatter24Hour else formatter12Hour
+ val formatArg = formatter.format(toggleTime)
+ return resources.getString(toggleTimeStringRes, formatArg)
+ } catch (exception: DateTimeException) {
+ logger.logWarning(spec, exception.message.toString())
+ return null
+ }
+ }
+ }
+ }
+
+ private companion object {
+ val formatter12Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("hh:mm a")
+ val formatter24Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm")
+ val spec = TileSpec.create(QSAccessibilityModule.NIGHT_DISPLAY_TILE_SPEC)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index 70632d5aa27a..79218ae4ca20 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_FROM_AOD;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_TO_AOD;
+import static com.android.systemui.keyguard.shared.model.KeyguardState.GONE;
import static com.android.systemui.util.kotlin.JavaAdapterKt.combineFlows;
import android.animation.Animator;
@@ -49,6 +50,7 @@ import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteract
import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus;
import com.android.systemui.keyguard.MigrateClocksToBlueprint;
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
import com.android.systemui.res.R;
import com.android.systemui.scene.domain.interactor.SceneInteractor;
@@ -108,6 +110,7 @@ public class StatusBarStateControllerImpl implements
private final UiEventLogger mUiEventLogger;
private final Lazy<InteractionJankMonitor> mInteractionJankMonitorLazy;
private final JavaAdapter mJavaAdapter;
+ private final Lazy<KeyguardTransitionInteractor> mKeyguardTransitionInteractorLazy;
private final Lazy<ShadeInteractor> mShadeInteractorLazy;
private final Lazy<DeviceUnlockedInteractor> mDeviceUnlockedInteractorLazy;
private final Lazy<SceneInteractor> mSceneInteractorLazy;
@@ -175,6 +178,7 @@ public class StatusBarStateControllerImpl implements
UiEventLogger uiEventLogger,
Lazy<InteractionJankMonitor> interactionJankMonitorLazy,
JavaAdapter javaAdapter,
+ Lazy<KeyguardTransitionInteractor> keyguardTransitionInteractor,
Lazy<ShadeInteractor> shadeInteractorLazy,
Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy,
Lazy<SceneInteractor> sceneInteractorLazy,
@@ -182,6 +186,7 @@ public class StatusBarStateControllerImpl implements
mUiEventLogger = uiEventLogger;
mInteractionJankMonitorLazy = interactionJankMonitorLazy;
mJavaAdapter = javaAdapter;
+ mKeyguardTransitionInteractorLazy = keyguardTransitionInteractor;
mShadeInteractorLazy = shadeInteractorLazy;
mDeviceUnlockedInteractorLazy = deviceUnlockedInteractorLazy;
mSceneInteractorLazy = sceneInteractorLazy;
@@ -193,6 +198,14 @@ public class StatusBarStateControllerImpl implements
@Override
public void start() {
+ mJavaAdapter.alwaysCollectFlow(
+ mKeyguardTransitionInteractorLazy.get().isFinishedInState(GONE),
+ (Boolean isFinishedInState) -> {
+ if (isFinishedInState) {
+ setLeaveOpenOnKeyguardHide(false);
+ }
+ });
+
mJavaAdapter.alwaysCollectFlow(mShadeInteractorLazy.get().isAnyExpanded(),
this::onShadeOrQsExpanded);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt
index 816e5c132432..db3cf5abe618 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification.row
import android.app.Flags
+import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
import javax.inject.Inject
/**
@@ -27,11 +28,14 @@ interface HeadsUpStyleProvider {
fun shouldApplyCompactStyle(): Boolean
}
-class HeadsUpStyleProviderImpl @Inject constructor() : HeadsUpStyleProvider {
+class HeadsUpStyleProviderImpl
+@Inject
+constructor(private val statusBarModeRepositoryStore: StatusBarModeRepositoryStore) :
+ HeadsUpStyleProvider {
- /**
- * TODO(b/270709257) This feature is under development. This method returns Compact when the
- * flag is enabled for fish fooding purpose.
- */
- override fun shouldApplyCompactStyle(): Boolean = Flags.compactHeadsUpNotification()
+ override fun shouldApplyCompactStyle(): Boolean {
+ // Use compact HUN for immersive mode.
+ return Flags.compactHeadsUpNotification() &&
+ statusBarModeRepositoryStore.defaultDisplay.isInFullscreenMode.value
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index be6bef74565a..23674b24d63e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2184,7 +2184,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
}
if (mStatusBarStateController.leaveOpenOnKeyguardHide()) {
if (!mStatusBarStateController.isKeyguardRequested()) {
- mStatusBarStateController.setLeaveOpenOnKeyguardHide(false);
+ if (!MigrateClocksToBlueprint.isEnabled()) {
+ mStatusBarStateController.setLeaveOpenOnKeyguardHide(false);
+ }
}
long delay = mKeyguardStateController.calculateGoingToFullShadeDelay();
mLockscreenShadeTransitionController.onHideKeyguard(delay, previousState);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt
index d9d909a49781..fc54f140dec5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt
@@ -33,6 +33,18 @@ data class SubscriptionModel(
*/
val isOpportunistic: Boolean = false,
+ /**
+ * True if this subscription **only** supports non-terrestrial networks (NTN) and false
+ * otherwise. (non-terrestrial == satellite)
+ *
+ * Note that we intend to filter these subscriptions out, because these connections are actually
+ * supported by
+ * [com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository]. See
+ * [com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor] for
+ * the filtering.
+ */
+ val isExclusivelyNonTerrestrial: Boolean = false,
+
/** Subscriptions in the same group may be filtered or treated as a single subscription */
val groupUuid: ParcelUuid? = null,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index 22785979f3ae..425c58b0074b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -23,6 +23,7 @@ import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import kotlinx.coroutines.flow.StateFlow
@@ -76,7 +77,17 @@ interface MobileConnectionRepository {
*/
val isInService: StateFlow<Boolean>
- /** Reflects [android.telephony.ServiceState.isUsingNonTerrestrialNetwork] */
+ /**
+ * True if this subscription is actively connected to a non-terrestrial network and false
+ * otherwise. Reflects [android.telephony.ServiceState.isUsingNonTerrestrialNetwork].
+ *
+ * Notably: This value reflects that this subscription is **currently** using a non-terrestrial
+ * network, because some subscriptions can switch between terrestrial and non-terrestrial
+ * networks. [SubscriptionModel.isExclusivelyNonTerrestrial] reflects whether a subscription is
+ * configured to exclusively connect to non-terrestrial networks. [isNonTerrestrial] can change
+ * during the lifetime of a subscription but [SubscriptionModel.isExclusivelyNonTerrestrial]
+ * will stay constant.
+ */
val isNonTerrestrial: StateFlow<Boolean>
/** True if [android.telephony.SignalStrength] told us that this connection is using GSM */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
index 5d91ef323ead..0073e9cd3dd8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
@@ -424,6 +424,7 @@ constructor(
SubscriptionModel(
subscriptionId = subscriptionId,
isOpportunistic = isOpportunistic,
+ isExclusivelyNonTerrestrial = isOnlyNonTerrestrialNetwork,
groupUuid = groupUuid,
carrierName = carrierName.toString(),
profileClass = profileClass,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index d555c47f4da2..91d7ca65b30d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -172,21 +172,33 @@ constructor(
private val unfilteredSubscriptions: Flow<List<SubscriptionModel>> =
mobileConnectionsRepo.subscriptions
- /**
- * Any filtering that we can do based purely on the info of each subscription. Currently this
- * only applies the ProfileClass-based filter, but if we need other they can go here
- */
+ /** Any filtering that we can do based purely on the info of each subscription individually. */
private val subscriptionsBasedFilteredSubs =
- unfilteredSubscriptions.map { subs -> applyProvisioningFilter(subs) }.distinctUntilChanged()
+ unfilteredSubscriptions
+ .map { it.filterBasedOnProvisioning().filterBasedOnNtn() }
+ .distinctUntilChanged()
- private fun applyProvisioningFilter(subs: List<SubscriptionModel>): List<SubscriptionModel> =
+ private fun List<SubscriptionModel>.filterBasedOnProvisioning(): List<SubscriptionModel> =
if (!featureFlagsClassic.isEnabled(FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS)) {
- subs
+ this
} else {
- subs.filter { it.profileClass != PROFILE_CLASS_PROVISIONING }
+ this.filter { it.profileClass != PROFILE_CLASS_PROVISIONING }
}
/**
+ * Subscriptions that exclusively support non-terrestrial networks should **never** directly
+ * show any iconography in the status bar. These subscriptions only exist to provide a backing
+ * for the device-based satellite connections, and the iconography for those connections are
+ * already being handled in
+ * [com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository]. We
+ * need to filter out those subscriptions here so we guarantee the subscription never turns into
+ * an icon. See b/336881301.
+ */
+ private fun List<SubscriptionModel>.filterBasedOnNtn(): List<SubscriptionModel> {
+ return this.filter { !it.isExclusivelyNonTerrestrial }
+ }
+
+ /**
* Generally, SystemUI wants to show iconography for each subscription that is listed by
* [SubscriptionManager]. However, in the case of opportunistic subscriptions, we want to only
* show a single representation of the pair of subscriptions. The docs define opportunistic as:
@@ -204,12 +216,8 @@ constructor(
subscriptionsBasedFilteredSubs,
mobileConnectionsRepo.activeMobileDataSubscriptionId,
connectivityRepository.vcnSubId,
- ) { unfilteredSubs, activeId, vcnSubId ->
- filterSubsBasedOnOpportunistic(
- unfilteredSubs,
- activeId,
- vcnSubId,
- )
+ ) { preFilteredSubs, activeId, vcnSubId ->
+ filterSubsBasedOnOpportunistic(preFilteredSubs, activeId, vcnSubId)
}
.distinctUntilChanged()
.logDiffsForTable(
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/LocationControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/LocationControllerExt.kt
new file mode 100644
index 000000000000..ee1b5655f7be
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/LocationControllerExt.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 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.util.kotlin
+
+import com.android.systemui.statusbar.policy.LocationController
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.onStart
+
+fun LocationController.isLocationEnabledFlow(): Flow<Boolean> {
+ return conflatedCallbackFlow {
+ val locationCallback =
+ object : LocationController.LocationChangeCallback {
+ override fun onLocationSettingsChanged(locationEnabled: Boolean) {
+ trySend(locationEnabled)
+ }
+ }
+ addCallback(locationCallback)
+ awaitClose { removeCallback(locationCallback) }
+ }
+ .onStart { emit(isLocationEnabled) }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 3793970394a8..5b47c94bcc90 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -446,6 +446,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
mUiEventLogger,
() -> mKosmos.getInteractionJankMonitor(),
mJavaAdapter,
+ () -> mKeyguardTransitionInteractor,
() -> mShadeInteractor,
() -> mKosmos.getDeviceUnlockedInteractor(),
() -> mKosmos.getSceneInteractor(),
@@ -600,6 +601,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
new UiEventLoggerFake(),
() -> mKosmos.getInteractionJankMonitor(),
mJavaAdapter,
+ () -> mKeyguardTransitionInteractor,
() -> mShadeInteractor,
() -> mKosmos.getDeviceUnlockedInteractor(),
() -> mKosmos.getSceneInteractor(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProviderImplTest.kt
new file mode 100644
index 000000000000..5e50af39203f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProviderImplTest.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 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.notification.row
+
+import android.app.Flags.FLAG_COMPACT_HEADS_UP_NOTIFICATION
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class HeadsUpStyleProviderImplTest : SysuiTestCase() {
+
+ @Rule @JvmField val setFlagsRule = SetFlagsRule()
+
+ private lateinit var statusBarModeRepositoryStore: FakeStatusBarModeRepository
+ private lateinit var headsUpStyleProvider: HeadsUpStyleProviderImpl
+
+ @Before
+ fun setUp() {
+ statusBarModeRepositoryStore = FakeStatusBarModeRepository()
+ statusBarModeRepositoryStore.defaultDisplay.isInFullscreenMode.value = true
+
+ headsUpStyleProvider = HeadsUpStyleProviderImpl(statusBarModeRepositoryStore)
+ }
+
+ @Test
+ @DisableFlags(FLAG_COMPACT_HEADS_UP_NOTIFICATION)
+ fun shouldApplyCompactStyle_returnsFalse_whenCompactFlagDisabled() {
+ assertThat(headsUpStyleProvider.shouldApplyCompactStyle()).isFalse()
+ }
+
+ @Test
+ @EnableFlags(FLAG_COMPACT_HEADS_UP_NOTIFICATION)
+ fun shouldApplyCompactStyle_returnsTrue_whenImmersiveModeEnabled() {
+ // GIVEN
+ statusBarModeRepositoryStore.defaultDisplay.isInFullscreenMode.value = true
+
+ // THEN
+ assertThat(headsUpStyleProvider.shouldApplyCompactStyle()).isTrue()
+ }
+
+ @Test
+ @EnableFlags(FLAG_COMPACT_HEADS_UP_NOTIFICATION)
+ fun shouldApplyCompactStyle_returnsFalse_whenImmersiveModeDisabled() {
+ // GIVEN
+ statusBarModeRepositoryStore.defaultDisplay.isInFullscreenMode.value = false
+
+ // THEN
+ assertThat(headsUpStyleProvider.shouldApplyCompactStyle()).isFalse()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index b5525b1ce8e9..36df61d287a1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -295,6 +295,50 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() {
}
@Test
+ fun subscriptions_subIsOnlyNtn_modelHasExclusivelyNtnTrue() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.subscriptions)
+
+ val onlyNtnSub =
+ mock<SubscriptionInfo>().also {
+ whenever(it.isOnlyNonTerrestrialNetwork).thenReturn(true)
+ whenever(it.subscriptionId).thenReturn(45)
+ whenever(it.groupUuid).thenReturn(GROUP_1)
+ whenever(it.carrierName).thenReturn("NTN only")
+ whenever(it.profileClass).thenReturn(PROFILE_CLASS_UNSET)
+ }
+
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(onlyNtnSub))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0].isExclusivelyNonTerrestrial).isTrue()
+ }
+
+ @Test
+ fun subscriptions_subIsNotOnlyNtn_modelHasExclusivelyNtnFalse() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.subscriptions)
+
+ val notOnlyNtnSub =
+ mock<SubscriptionInfo>().also {
+ whenever(it.isOnlyNonTerrestrialNetwork).thenReturn(false)
+ whenever(it.subscriptionId).thenReturn(45)
+ whenever(it.groupUuid).thenReturn(GROUP_1)
+ whenever(it.carrierName).thenReturn("NTN only")
+ whenever(it.profileClass).thenReturn(PROFILE_CLASS_UNSET)
+ }
+
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(notOnlyNtnSub))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0].isExclusivelyNonTerrestrial).isFalse()
+ }
+
+ @Test
fun testSubscriptions_carrierMergedOnly_listHasCarrierMerged() =
testScope.runTest {
val latest by collectLastValue(underTest.subscriptions)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index 0b14be1eefbd..0f9cbfa66b5b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -42,14 +42,11 @@ import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.UUID
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import kotlinx.coroutines.yield
-import org.junit.After
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
@@ -68,7 +65,7 @@ class MobileIconsInteractorTest : SysuiTestCase() {
set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true)
}
- private val testDispatcher = UnconfinedTestDispatcher()
+ private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
private val tableLogBuffer =
@@ -113,17 +110,12 @@ class MobileIconsInteractorTest : SysuiTestCase() {
)
}
- @After fun tearDown() {}
-
@Test
fun filteredSubscriptions_default() =
testScope.runTest {
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
assertThat(latest).isEqualTo(listOf<SubscriptionModel>())
-
- job.cancel()
}
// Based on the logic from the old pipeline, we'll never filter subs when there are more than 2
@@ -133,12 +125,9 @@ class MobileIconsInteractorTest : SysuiTestCase() {
connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP, SUB_4_OPP))
connectionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID)
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
assertThat(latest).isEqualTo(listOf(SUB_1, SUB_3_OPP, SUB_4_OPP))
-
- job.cancel()
}
@Test
@@ -146,12 +135,9 @@ class MobileIconsInteractorTest : SysuiTestCase() {
testScope.runTest {
connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
-
- job.cancel()
}
@Test
@@ -160,12 +146,9 @@ class MobileIconsInteractorTest : SysuiTestCase() {
connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
assertThat(latest).isEqualTo(listOf(SUB_3_OPP, SUB_4_OPP))
-
- job.cancel()
}
@Test
@@ -180,12 +163,9 @@ class MobileIconsInteractorTest : SysuiTestCase() {
connectionsRepository.setSubscriptions(listOf(sub1, sub2))
connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID)
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
assertThat(latest).isEqualTo(listOf(sub1, sub2))
-
- job.cancel()
}
@Test
@@ -202,13 +182,10 @@ class MobileIconsInteractorTest : SysuiTestCase() {
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(false)
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
// Filtered subscriptions should show the active one when the config is false
assertThat(latest).isEqualTo(listOf(sub3))
-
- job.cancel()
}
@Test
@@ -225,13 +202,10 @@ class MobileIconsInteractorTest : SysuiTestCase() {
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(false)
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
// Filtered subscriptions should show the active one when the config is false
assertThat(latest).isEqualTo(listOf(sub4))
-
- job.cancel()
}
@Test
@@ -248,14 +222,11 @@ class MobileIconsInteractorTest : SysuiTestCase() {
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(true)
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
// Filtered subscriptions should show the primary (non-opportunistic) if the config is
// true
assertThat(latest).isEqualTo(listOf(sub1))
-
- job.cancel()
}
@Test
@@ -272,14 +243,11 @@ class MobileIconsInteractorTest : SysuiTestCase() {
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(true)
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
// Filtered subscriptions should show the primary (non-opportunistic) if the config is
// true
assertThat(latest).isEqualTo(listOf(sub1))
-
- job.cancel()
}
@Test
@@ -297,12 +265,9 @@ class MobileIconsInteractorTest : SysuiTestCase() {
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(false)
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
assertThat(latest).isEqualTo(listOf(sub3))
-
- job.cancel()
}
@Test
@@ -320,12 +285,9 @@ class MobileIconsInteractorTest : SysuiTestCase() {
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(false)
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
assertThat(latest).isEqualTo(listOf(sub1))
-
- job.cancel()
}
@Test
@@ -446,313 +408,345 @@ class MobileIconsInteractorTest : SysuiTestCase() {
}
@Test
+ fun filteredSubscriptions_subNotExclusivelyNonTerrestrial_hasSub() =
+ testScope.runTest {
+ val notExclusivelyNonTerrestrialSub =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = false,
+ subscriptionId = 5,
+ carrierName = "Carrier 5",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+
+ connectionsRepository.setSubscriptions(listOf(notExclusivelyNonTerrestrialSub))
+
+ val latest by collectLastValue(underTest.filteredSubscriptions)
+
+ assertThat(latest).isEqualTo(listOf(notExclusivelyNonTerrestrialSub))
+ }
+
+ @Test
+ fun filteredSubscriptions_subExclusivelyNonTerrestrial_doesNotHaveSub() =
+ testScope.runTest {
+ val exclusivelyNonTerrestrialSub =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = true,
+ subscriptionId = 5,
+ carrierName = "Carrier 5",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+
+ connectionsRepository.setSubscriptions(listOf(exclusivelyNonTerrestrialSub))
+
+ val latest by collectLastValue(underTest.filteredSubscriptions)
+
+ assertThat(latest).isEmpty()
+ }
+
+ @Test
+ fun filteredSubscription_mixOfExclusivelyNonTerrestrialAndOther_hasOtherSubsOnly() =
+ testScope.runTest {
+ val exclusivelyNonTerrestrialSub =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = true,
+ subscriptionId = 5,
+ carrierName = "Carrier 5",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ val otherSub1 =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = false,
+ subscriptionId = 1,
+ carrierName = "Carrier 1",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ val otherSub2 =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = false,
+ subscriptionId = 2,
+ carrierName = "Carrier 2",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+
+ connectionsRepository.setSubscriptions(
+ listOf(otherSub1, exclusivelyNonTerrestrialSub, otherSub2)
+ )
+
+ val latest by collectLastValue(underTest.filteredSubscriptions)
+
+ assertThat(latest).isEqualTo(listOf(otherSub1, otherSub2))
+ }
+
+ @Test
+ fun filteredSubscriptions_exclusivelyNonTerrestrialSub_andOpportunistic_bothFiltersHappen() =
+ testScope.runTest {
+ // Exclusively non-terrestrial sub
+ val exclusivelyNonTerrestrialSub =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = true,
+ subscriptionId = 5,
+ carrierName = "Carrier 5",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+
+ // Opportunistic subs
+ val (sub3, sub4) =
+ createSubscriptionPair(
+ subscriptionIds = Pair(SUB_3_ID, SUB_4_ID),
+ opportunistic = Pair(true, true),
+ grouped = true,
+ )
+
+ // WHEN both an exclusively non-terrestrial sub and opportunistic sub pair is included
+ connectionsRepository.setSubscriptions(listOf(sub3, sub4, exclusivelyNonTerrestrialSub))
+ connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
+
+ val latest by collectLastValue(underTest.filteredSubscriptions)
+
+ // THEN both the only-non-terrestrial sub and the non-active sub are filtered out,
+ // leaving only sub3.
+ assertThat(latest).isEqualTo(listOf(sub3))
+ }
+
+ @Test
fun activeDataConnection_turnedOn() =
testScope.runTest {
CONNECTION_1.setDataEnabled(true)
- var latest: Boolean? = null
- val job =
- underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
- assertThat(latest).isTrue()
+ val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
- job.cancel()
+ assertThat(latest).isTrue()
}
@Test
fun activeDataConnection_turnedOff() =
testScope.runTest {
CONNECTION_1.setDataEnabled(true)
- var latest: Boolean? = null
- val job =
- underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
CONNECTION_1.setDataEnabled(false)
- yield()
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
fun activeDataConnection_invalidSubId() =
testScope.runTest {
- var latest: Boolean? = null
- val job =
- underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
connectionsRepository.setActiveMobileDataSubscriptionId(INVALID_SUBSCRIPTION_ID)
- yield()
// An invalid active subId should tell us that data is off
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
fun failedConnection_default_validated_notFailed() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
connectionsRepository.mobileIsDefault.value = true
connectionsRepository.defaultConnectionIsValidated.value = true
- yield()
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
fun failedConnection_notDefault_notValidated_notFailed() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
connectionsRepository.mobileIsDefault.value = false
connectionsRepository.defaultConnectionIsValidated.value = false
- yield()
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
fun failedConnection_default_notValidated_failed() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
connectionsRepository.mobileIsDefault.value = true
connectionsRepository.defaultConnectionIsValidated.value = false
- yield()
assertThat(latest).isTrue()
-
- job.cancel()
}
@Test
fun failedConnection_carrierMergedDefault_notValidated_failed() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
connectionsRepository.hasCarrierMergedConnection.value = true
connectionsRepository.defaultConnectionIsValidated.value = false
- yield()
assertThat(latest).isTrue()
-
- job.cancel()
}
/** Regression test for b/275076959. */
@Test
fun failedConnection_dataSwitchInSameGroup_notFailed() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
connectionsRepository.mobileIsDefault.value = true
connectionsRepository.defaultConnectionIsValidated.value = true
+ runCurrent()
// WHEN there's a data change in the same subscription group
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
connectionsRepository.defaultConnectionIsValidated.value = false
+ runCurrent()
// THEN the default connection is *not* marked as failed because of forced validation
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
fun failedConnection_dataSwitchNotInSameGroup_isFailed() =
testScope.runTest {
- var latestConnectionFailed: Boolean? = null
- val job =
- underTest.isDefaultConnectionFailed
- .onEach { latestConnectionFailed = it }
- .launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
+
connectionsRepository.mobileIsDefault.value = true
connectionsRepository.defaultConnectionIsValidated.value = true
+ runCurrent()
// WHEN the connection is invalidated without a activeSubChangedInGroupEvent
connectionsRepository.defaultConnectionIsValidated.value = false
// THEN the connection is immediately marked as failed
- assertThat(latestConnectionFailed).isTrue()
-
- job.cancel()
+ assertThat(latest).isTrue()
}
@Test
fun alwaysShowDataRatIcon_configHasTrue() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.alwaysShowDataRatIcon)
val config = MobileMappings.Config()
config.alwaysShowDataRatIcon = true
connectionsRepository.defaultDataSubRatConfig.value = config
- yield()
assertThat(latest).isTrue()
-
- job.cancel()
}
@Test
fun alwaysShowDataRatIcon_configHasFalse() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.alwaysShowDataRatIcon)
val config = MobileMappings.Config()
config.alwaysShowDataRatIcon = false
connectionsRepository.defaultDataSubRatConfig.value = config
- yield()
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
fun alwaysUseCdmaLevel_configHasTrue() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.alwaysUseCdmaLevel.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.alwaysUseCdmaLevel)
val config = MobileMappings.Config()
config.alwaysShowCdmaRssi = true
connectionsRepository.defaultDataSubRatConfig.value = config
- yield()
assertThat(latest).isTrue()
-
- job.cancel()
}
@Test
fun alwaysUseCdmaLevel_configHasFalse() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.alwaysUseCdmaLevel.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.alwaysUseCdmaLevel)
val config = MobileMappings.Config()
config.alwaysShowCdmaRssi = false
connectionsRepository.defaultDataSubRatConfig.value = config
- yield()
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
fun isSingleCarrier_zeroSubscriptions_false() =
testScope.runTest {
- var latest: Boolean? = true
- val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isSingleCarrier)
connectionsRepository.setSubscriptions(emptyList())
- assertThat(latest).isFalse()
- job.cancel()
+ assertThat(latest).isFalse()
}
@Test
fun isSingleCarrier_oneSubscription_true() =
testScope.runTest {
- var latest: Boolean? = false
- val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isSingleCarrier)
connectionsRepository.setSubscriptions(listOf(SUB_1))
- assertThat(latest).isTrue()
- job.cancel()
+ assertThat(latest).isTrue()
}
@Test
fun isSingleCarrier_twoSubscriptions_false() =
testScope.runTest {
- var latest: Boolean? = true
- val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isSingleCarrier)
connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
- assertThat(latest).isFalse()
- job.cancel()
+ assertThat(latest).isFalse()
}
@Test
fun isSingleCarrier_updates() =
testScope.runTest {
- var latest: Boolean? = false
- val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isSingleCarrier)
connectionsRepository.setSubscriptions(listOf(SUB_1))
assertThat(latest).isTrue()
connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
fun mobileIsDefault_mobileFalseAndCarrierMergedFalse_false() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.mobileIsDefault)
connectionsRepository.mobileIsDefault.value = false
connectionsRepository.hasCarrierMergedConnection.value = false
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
fun mobileIsDefault_mobileTrueAndCarrierMergedFalse_true() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.mobileIsDefault)
connectionsRepository.mobileIsDefault.value = true
connectionsRepository.hasCarrierMergedConnection.value = false
assertThat(latest).isTrue()
-
- job.cancel()
}
/** Regression test for b/272586234. */
@Test
fun mobileIsDefault_mobileFalseAndCarrierMergedTrue_true() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.mobileIsDefault)
connectionsRepository.mobileIsDefault.value = false
connectionsRepository.hasCarrierMergedConnection.value = true
assertThat(latest).isTrue()
-
- job.cancel()
}
@Test
fun mobileIsDefault_updatesWhenRepoUpdates() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.mobileIsDefault)
connectionsRepository.mobileIsDefault.value = true
assertThat(latest).isTrue()
@@ -762,8 +756,6 @@ class MobileIconsInteractorTest : SysuiTestCase() {
connectionsRepository.hasCarrierMergedConnection.value = true
assertThat(latest).isTrue()
-
- job.cancel()
}
// The data switch tests are mostly testing the [forcingCellularValidation] flow, but that flow
@@ -772,95 +764,79 @@ class MobileIconsInteractorTest : SysuiTestCase() {
@Test
fun dataSwitch_inSameGroup_validatedMatchesPreviousValue_expiresAfter2s() =
testScope.runTest {
- var latestConnectionFailed: Boolean? = null
- val job =
- underTest.isDefaultConnectionFailed
- .onEach { latestConnectionFailed = it }
- .launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
connectionsRepository.mobileIsDefault.value = true
connectionsRepository.defaultConnectionIsValidated.value = true
+ runCurrent()
// Trigger a data change in the same subscription group that's not yet validated
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
connectionsRepository.defaultConnectionIsValidated.value = false
+ runCurrent()
// After 1s, the force validation bit is still present, so the connection is not marked
// as failed
advanceTimeBy(1000)
- assertThat(latestConnectionFailed).isFalse()
+ assertThat(latest).isFalse()
// After 2s, the force validation expires so the connection updates to failed
advanceTimeBy(1001)
- assertThat(latestConnectionFailed).isTrue()
-
- job.cancel()
+ assertThat(latest).isTrue()
}
@Test
fun dataSwitch_inSameGroup_notValidated_immediatelyMarkedAsFailed() =
testScope.runTest {
- var latestConnectionFailed: Boolean? = null
- val job =
- underTest.isDefaultConnectionFailed
- .onEach { latestConnectionFailed = it }
- .launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
connectionsRepository.mobileIsDefault.value = true
connectionsRepository.defaultConnectionIsValidated.value = false
+ runCurrent()
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
- assertThat(latestConnectionFailed).isTrue()
-
- job.cancel()
+ assertThat(latest).isTrue()
}
@Test
fun dataSwitch_loseValidation_thenSwitchHappens_clearsForcedBit() =
testScope.runTest {
- var latestConnectionFailed: Boolean? = null
- val job =
- underTest.isDefaultConnectionFailed
- .onEach { latestConnectionFailed = it }
- .launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
// GIVEN the network starts validated
connectionsRepository.mobileIsDefault.value = true
connectionsRepository.defaultConnectionIsValidated.value = true
+ runCurrent()
// WHEN a data change happens in the same group
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
// WHEN the validation bit is lost
connectionsRepository.defaultConnectionIsValidated.value = false
+ runCurrent()
// WHEN another data change happens in the same group
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
// THEN the forced validation bit is still used...
- assertThat(latestConnectionFailed).isFalse()
+ assertThat(latest).isFalse()
advanceTimeBy(1000)
- assertThat(latestConnectionFailed).isFalse()
+ assertThat(latest).isFalse()
// ... but expires after 2s
advanceTimeBy(1001)
- assertThat(latestConnectionFailed).isTrue()
-
- job.cancel()
+ assertThat(latest).isTrue()
}
@Test
fun dataSwitch_whileAlreadyForcingValidation_resetsClock() =
testScope.runTest {
- var latestConnectionFailed: Boolean? = null
- val job =
- underTest.isDefaultConnectionFailed
- .onEach { latestConnectionFailed = it }
- .launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
connectionsRepository.mobileIsDefault.value = true
connectionsRepository.defaultConnectionIsValidated.value = true
+ runCurrent()
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
@@ -869,44 +845,37 @@ class MobileIconsInteractorTest : SysuiTestCase() {
// WHEN another change in same group event happens
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
connectionsRepository.defaultConnectionIsValidated.value = false
+ runCurrent()
// THEN the forced validation remains for exactly 2 more seconds from now
// 1.500s from second event
advanceTimeBy(1500)
- assertThat(latestConnectionFailed).isFalse()
+ assertThat(latest).isFalse()
// 2.001s from the second event
advanceTimeBy(501)
- assertThat(latestConnectionFailed).isTrue()
-
- job.cancel()
+ assertThat(latest).isTrue()
}
@Test
fun isForceHidden_repoHasMobileHidden_true() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isForceHidden)
connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE))
assertThat(latest).isTrue()
-
- job.cancel()
}
@Test
fun isForceHidden_repoDoesNotHaveMobileHidden_false() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isForceHidden)
connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
index 3762497656c6..ec56327c1101 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
@@ -20,6 +20,7 @@ import com.android.internal.logging.uiEventLogger
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.jank.interactionJankMonitor
import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
@@ -33,6 +34,7 @@ var Kosmos.statusBarStateController: SysuiStatusBarStateController by
uiEventLogger,
{ interactionJankMonitor },
mock(),
+ { keyguardTransitionInteractor },
{ shadeInteractor },
{ deviceUnlockedInteractor },
{ sceneInteractor },
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/night/NightDisplayTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/night/NightDisplayTileKosmos.kt
new file mode 100644
index 000000000000..5c21ab6e7fa8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/night/NightDisplayTileKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night
+
+import com.android.systemui.accessibility.qs.QSAccessibilityModule
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.qsEventLogger
+
+val Kosmos.qsNightDisplayTileConfig by
+ Kosmos.Fixture { QSAccessibilityModule.provideNightDisplayTileConfig(qsEventLogger) }
diff --git a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
index 0049213cbf55..d932bd4e6d20 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
@@ -32,6 +32,7 @@ import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.os.Bundle;
import android.os.Environment;
import android.os.HandlerThread;
import android.os.LocaleList;
@@ -101,6 +102,11 @@ class LocaleManagerBackupHelper {
// the application setting the app-locale itself.
private final SharedPreferences mDelegateAppLocalePackages;
private final BroadcastReceiver mUserMonitor;
+ // To determine whether an app is pre-archived, check for Intent.EXTRA_ARCHIVAL upon receiving
+ // the initial PACKAGE_ADDED broadcast. If it is indeed pre-archived, perform the data
+ // restoration during the second PACKAGE_ADDED broadcast, which is sent subsequently when the
+ // app is installed.
+ private final Set<String> mPkgsToRestore;
LocaleManagerBackupHelper(LocaleManagerService localeManagerService,
PackageManager packageManager, HandlerThread broadcastHandlerThread) {
@@ -119,6 +125,7 @@ class LocaleManagerBackupHelper {
mStagedData = stagedData;
mDelegateAppLocalePackages = delegateAppLocalePackages != null ? delegateAppLocalePackages
: createPersistedInfo();
+ mPkgsToRestore = new ArraySet<>();
mUserMonitor = new UserMonitor();
IntentFilter filter = new IntentFilter();
@@ -251,6 +258,9 @@ class LocaleManagerBackupHelper {
LocalesInfo localesInfo = pkgStates.get(pkgName);
// Check if the application is already installed for the concerned user.
if (isPackageInstalledForUser(pkgName, userId)) {
+ if (mPkgsToRestore != null) {
+ mPkgsToRestore.remove(pkgName);
+ }
// Don't apply the restore if the locales have already been set for the app.
checkExistingLocalesAndApplyRestore(pkgName, localesInfo, userId);
} else {
@@ -279,23 +289,18 @@ class LocaleManagerBackupHelper {
/**
* <p><b>Note:</b> This is invoked by service's common monitor
- * {@link LocaleManagerServicePackageMonitor#onPackageAdded} when a new package is
+ * {@link LocaleManagerServicePackageMonitor#onPackageAddedWithExtras} when a new package is
* added on device.
*/
- void onPackageAdded(String packageName, int uid) {
- try {
- synchronized (mStagedDataLock) {
- cleanStagedDataForOldEntriesLocked();
-
- int userId = UserHandle.getUserId(uid);
- if (mStagedData.contains(userId)) {
- // Perform lazy restore only if the staged data exists.
- doLazyRestoreLocked(packageName, userId);
- }
+ void onPackageAddedWithExtras(String packageName, int uid, Bundle extras) {
+ boolean archived = false;
+ if (extras != null) {
+ archived = extras.getBoolean(Intent.EXTRA_ARCHIVAL, false);
+ if (archived && mPkgsToRestore != null) {
+ mPkgsToRestore.add(packageName);
}
- } catch (Exception e) {
- Slog.e(TAG, "Exception in onPackageAdded.", e);
}
+ checkStageDataAndApplyRestore(packageName, uid);
}
/**
@@ -305,6 +310,10 @@ class LocaleManagerBackupHelper {
*/
void onPackageUpdateFinished(String packageName, int uid) {
int userId = UserHandle.getUserId(uid);
+ if (mPkgsToRestore != null && mPkgsToRestore.contains(packageName)) {
+ mPkgsToRestore.remove(packageName);
+ checkStageDataAndApplyRestore(packageName, uid);
+ }
cleanApplicationLocalesIfNeeded(packageName, userId);
}
@@ -338,6 +347,25 @@ class LocaleManagerBackupHelper {
}
}
+ private void checkStageDataAndApplyRestore(String packageName, int uid) {
+ try {
+ synchronized (mStagedDataLock) {
+ cleanStagedDataForOldEntriesLocked();
+
+ int userId = UserHandle.getUserId(uid);
+ if (mStagedData.contains(userId)) {
+ if (mPkgsToRestore != null) {
+ mPkgsToRestore.remove(packageName);
+ }
+ // Perform lazy restore only if the staged data exists.
+ doLazyRestoreLocked(packageName, userId);
+ }
+ }
+ } catch (Exception e) {
+ Slog.e(TAG, "Exception in onPackageAdded.", e);
+ }
+ }
+
private boolean isPackageInstalledForUser(String packageName, int userId) {
PackageInfo pkgInfo = null;
try {
diff --git a/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java b/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java
index ecd3614b4ff4..e0a050fbf157 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java
@@ -17,6 +17,7 @@
package com.android.server.locales;
import android.annotation.NonNull;
+import android.os.Bundle;
import android.os.UserHandle;
import com.android.internal.content.PackageMonitor;
@@ -48,8 +49,8 @@ final class LocaleManagerServicePackageMonitor extends PackageMonitor {
}
@Override
- public void onPackageAdded(String packageName, int uid) {
- mBackupHelper.onPackageAdded(packageName, uid);
+ public void onPackageAddedWithExtras(String packageName, int uid, Bundle extras) {
+ mBackupHelper.onPackageAddedWithExtras(packageName, uid, extras);
}
@Override
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 1309e44af6d3..41d6288d4411 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -2139,10 +2139,17 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile
continue;
}
+ ComponentName unflattenOriginalComponentName = ComponentName.unflattenFromString(
+ originalComponentName);
+ if (unflattenOriginalComponentName == null) {
+ Slog.d(TAG, "Incorrect component name from the attributes");
+ continue;
+ }
+
activityInfos.add(
new ArchiveState.ArchiveActivityInfo(
title,
- ComponentName.unflattenFromString(originalComponentName),
+ unflattenOriginalComponentName,
iconPath,
monochromeIconPath));
}
diff --git a/services/core/java/com/android/server/wm/InputConfigAdapter.java b/services/core/java/com/android/server/wm/InputConfigAdapter.java
index 119fafde6f77..ae6e72464555 100644
--- a/services/core/java/com/android/server/wm/InputConfigAdapter.java
+++ b/services/core/java/com/android/server/wm/InputConfigAdapter.java
@@ -20,8 +20,6 @@ import android.os.InputConfig;
import android.view.InputWindowHandle.InputConfigFlags;
import android.view.WindowManager.LayoutParams;
-import java.util.List;
-
/**
* A helper to determine the {@link InputConfigFlags} that control the behavior of an input window
* from several WM attributes.
@@ -47,7 +45,7 @@ class InputConfigAdapter {
* input configurations that can be mapped directly from a corresponding LayoutParams input
* feature.
*/
- private static final List<FlagMapping> INPUT_FEATURE_TO_CONFIG_MAP = List.of(
+ private static final FlagMapping[] INPUT_FEATURE_TO_CONFIG_MAP = {
new FlagMapping(
LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL,
InputConfig.NO_INPUT_CHANNEL, false /* inverted */),
@@ -59,7 +57,8 @@ class InputConfigAdapter {
InputConfig.SPY, false /* inverted */),
new FlagMapping(
LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY,
- InputConfig.SENSITIVE_FOR_PRIVACY, false /* inverted */));
+ InputConfig.SENSITIVE_FOR_PRIVACY, false /* inverted */)
+ };
@InputConfigFlags
private static final int INPUT_FEATURE_TO_CONFIG_MASK =
@@ -72,7 +71,7 @@ class InputConfigAdapter {
* NOTE: The layout params flag {@link LayoutParams#FLAG_NOT_FOCUSABLE} is not handled by this
* adapter, and must be handled explicitly.
*/
- private static final List<FlagMapping> LAYOUT_PARAM_FLAG_TO_CONFIG_MAP = List.of(
+ private static final FlagMapping[] LAYOUT_PARAM_FLAG_TO_CONFIG_MAP = {
new FlagMapping(
LayoutParams.FLAG_NOT_TOUCHABLE,
InputConfig.NOT_TOUCHABLE, false /* inverted */),
@@ -84,7 +83,8 @@ class InputConfigAdapter {
InputConfig.WATCH_OUTSIDE_TOUCH, false /* inverted */),
new FlagMapping(
LayoutParams.FLAG_SLIPPERY,
- InputConfig.SLIPPERY, false /* inverted */));
+ InputConfig.SLIPPERY, false /* inverted */)
+ };
@InputConfigFlags
private static final int LAYOUT_PARAM_FLAG_TO_CONFIG_MASK =
@@ -119,7 +119,7 @@ class InputConfigAdapter {
}
@InputConfigFlags
- private static int applyMapping(int flags, List<FlagMapping> flagToConfigMap) {
+ private static int applyMapping(int flags, FlagMapping[] flagToConfigMap) {
int inputConfig = 0;
for (final FlagMapping mapping : flagToConfigMap) {
final boolean flagSet = (flags & mapping.mFlag) != 0;
@@ -131,7 +131,7 @@ class InputConfigAdapter {
}
@InputConfigFlags
- private static int computeMask(List<FlagMapping> flagToConfigMap) {
+ private static int computeMask(FlagMapping[] flagToConfigMap) {
int mask = 0;
for (final FlagMapping mapping : flagToConfigMap) {
mask |= mapping.mInputConfig;
diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
index 40ecaf1770a9..7dd1847114c8 100644
--- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
@@ -42,6 +42,7 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Binder;
+import android.os.Bundle;
import android.os.HandlerThread;
import android.os.LocaleList;
import android.os.Process;
@@ -488,7 +489,7 @@ public class LocaleManagerBackupRestoreTest {
setUpPackageInstalled(pkgNameA);
- mPackageMonitor.onPackageAdded(pkgNameA, DEFAULT_UID);
+ mPackageMonitor.onPackageAddedWithExtras(pkgNameA, DEFAULT_UID, new Bundle());
verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameA, DEFAULT_USER_ID,
LocaleList.forLanguageTags(langTagsA), false, FrameworkStatsLog
@@ -504,7 +505,7 @@ public class LocaleManagerBackupRestoreTest {
setUpPackageInstalled(pkgNameB);
- mPackageMonitor.onPackageAdded(pkgNameB, DEFAULT_UID);
+ mPackageMonitor.onPackageAddedWithExtras(pkgNameB, DEFAULT_UID, new Bundle());
verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameB, DEFAULT_USER_ID,
LocaleList.forLanguageTags(langTagsB), true, FrameworkStatsLog
@@ -518,6 +519,66 @@ public class LocaleManagerBackupRestoreTest {
}
@Test
+ public void testRestore_appInstalledAfterSUW_restoresFromStage_ArchiveEnabled()
+ throws Exception {
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HashMap<String, LocalesInfo> pkgLocalesMap = new HashMap<>();
+ String pkgNameA = "com.android.myAppA";
+ String pkgNameB = "com.android.myAppB";
+ String langTagsA = "ru";
+ String langTagsB = "hi,fr";
+ LocalesInfo localesInfoA = new LocalesInfo(langTagsA, false);
+ LocalesInfo localesInfoB = new LocalesInfo(langTagsB, true);
+ pkgLocalesMap.put(pkgNameA, localesInfoA);
+ pkgLocalesMap.put(pkgNameB, localesInfoB);
+ writeTestPayload(out, pkgLocalesMap);
+ setUpPackageNotInstalled(pkgNameA);
+ setUpPackageNotInstalled(pkgNameB);
+ setUpLocalesForPackage(pkgNameA, LocaleList.getEmptyLocaleList());
+ setUpLocalesForPackage(pkgNameB, LocaleList.getEmptyLocaleList());
+ setUpPackageNamesForSp(new ArraySet<>());
+
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(Intent.EXTRA_ARCHIVAL, true);
+ mPackageMonitor.onPackageAddedWithExtras(pkgNameA, DEFAULT_UID, bundle);
+ mPackageMonitor.onPackageAddedWithExtras(pkgNameB, DEFAULT_UID, bundle);
+
+ mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
+
+ verifyNothingRestored();
+
+ setUpPackageInstalled(pkgNameA);
+
+ mPackageMonitor.onPackageUpdateFinished(pkgNameA, DEFAULT_UID);
+
+ verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameA, DEFAULT_USER_ID,
+ LocaleList.forLanguageTags(langTagsA), false, FrameworkStatsLog
+ .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE);
+
+ mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, pkgNameA, false, false);
+
+ verify(mMockSpEditor, times(0)).putStringSet(anyString(), any());
+
+ pkgLocalesMap.remove(pkgNameA);
+
+ verifyStageDataForUser(pkgLocalesMap, DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
+
+ setUpPackageInstalled(pkgNameB);
+
+ mPackageMonitor.onPackageUpdateFinished(pkgNameB, DEFAULT_UID);
+
+ verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameB, DEFAULT_USER_ID,
+ LocaleList.forLanguageTags(langTagsB), true, FrameworkStatsLog
+ .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE);
+
+ mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, pkgNameB, true, false);
+
+ verify(mMockSpEditor, times(1)).putStringSet(Integer.toString(DEFAULT_USER_ID),
+ new ArraySet<>(Arrays.asList(pkgNameB)));
+ checkStageDataDoesNotExist(DEFAULT_USER_ID);
+ }
+
+ @Test
public void testRestore_appInstalledAfterSUWAndLocalesAlreadySet_restoresNothing()
throws Exception {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -535,7 +596,7 @@ public class LocaleManagerBackupRestoreTest {
setUpPackageInstalled(DEFAULT_PACKAGE_NAME);
setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, LocaleList.forLanguageTags("hi,mr"));
- mPackageMonitor.onPackageAdded(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
+ mPackageMonitor.onPackageAddedWithExtras(DEFAULT_PACKAGE_NAME, DEFAULT_UID, new Bundle());
// Since locales are already set, we should not restore anything for it.
verifyNothingRestored();
@@ -612,7 +673,7 @@ public class LocaleManagerBackupRestoreTest {
DEFAULT_CREATION_TIME_MILLIS + RETENTION_PERIOD.minusHours(1).toMillis());
setUpPackageInstalled(pkgNameA);
- mPackageMonitor.onPackageAdded(pkgNameA, DEFAULT_UID);
+ mPackageMonitor.onPackageAddedWithExtras(pkgNameA, DEFAULT_UID, new Bundle());
verify(mMockLocaleManagerService, times(1)).setApplicationLocales(
pkgNameA, DEFAULT_USER_ID, LocaleList.forLanguageTags(langTagsA), false,
@@ -627,7 +688,7 @@ public class LocaleManagerBackupRestoreTest {
DEFAULT_CREATION_TIME_MILLIS + RETENTION_PERIOD.plusSeconds(1).toMillis());
setUpPackageInstalled(pkgNameB);
- mPackageMonitor.onPackageAdded(pkgNameB, DEFAULT_UID);
+ mPackageMonitor.onPackageAddedWithExtras(pkgNameB, DEFAULT_UID, new Bundle());
verify(mMockLocaleManagerService, times(0)).setApplicationLocales(eq(pkgNameB), anyInt(),
any(), anyBoolean(), anyInt());
diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java
index e3f84c19653b..f1260008ca59 100644
--- a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java
+++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java
@@ -24,6 +24,7 @@ import static com.android.server.inputmethod.multisessiontest.TestRequestConstan
import android.app.Activity;
import android.os.Bundle;
+import android.os.Process;
import android.util.Log;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
@@ -47,8 +48,9 @@ public final class MainActivity extends ConcurrentUserActivityBase {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- Log.v(TAG, "Create MainActivity as user " + getUserId() + " on display "
- + getDisplayId());
+ Log.v(TAG, "Create MainActivity as user "
+ + Process.myUserHandle().getIdentifier() + " on display "
+ + getDisplay().getDisplayId());
setContentView(R.layout.main_activity);
mImm = getSystemService(InputMethodManager.class);
mEditor = requireViewById(R.id.edit_text);