Temporary hide notifications when folding/unfolding
This should reduce latency with complex notifications
by postponing the relayout.
Bug: 293824309
Test: atest HideNotificationsInteractorTest
Test: atest ConfigurationInteractorTest
Test: atest UnfoldTransitionInteractorTest
Flag: ACONFIG notifications_hide_on_display_switch DISABLED
Change-Id: I4cd6b718adc15b6c8261afe90437e7644702f56e
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 8f350a7..04c96ab 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -38,6 +38,13 @@
}
flag {
+ name: "notifications_hide_on_display_switch"
+ namespace: "systemui"
+ description: "Temporary hides notifications when folding/unfolding to reduce unfold latency"
+ bug: "293824309"
+}
+
+flag {
name: "notification_lifetime_extension_refactor"
namespace: "systemui"
description: "Enables moving notification lifetime extension management from SystemUI to "
diff --git a/packages/SystemUI/src/com/android/systemui/common/CommonModule.kt b/packages/SystemUI/src/com/android/systemui/common/CommonModule.kt
new file mode 100644
index 0000000..5e6caf0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/CommonModule.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 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.common
+
+import com.android.systemui.common.domain.interactor.ConfigurationInteractor
+import com.android.systemui.common.domain.interactor.ConfigurationInteractorImpl
+import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl
+import dagger.Binds
+import dagger.Module
+
+@Module
+abstract class CommonModule {
+ @Binds abstract fun bindRepository(impl: ConfigurationRepositoryImpl): ConfigurationRepository
+
+ @Binds abstract fun bindInteractor(impl: ConfigurationInteractorImpl): ConfigurationInteractor
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/domain/interactor/ConfigurationInteractor.kt b/packages/SystemUI/src/com/android/systemui/common/domain/interactor/ConfigurationInteractor.kt
new file mode 100644
index 0000000..89053d1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/domain/interactor/ConfigurationInteractor.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 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.common.domain.interactor
+
+import android.content.res.Configuration
+import android.graphics.Rect
+import android.view.Surface
+import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+
+interface ConfigurationInteractor {
+ /**
+ * Returns screen size adjusted to rotation, so returned screen sizes are stable across all
+ * rotations, could be useful if you need to react to screen resize (e.g. fold/unfold on
+ * foldable devices)
+ */
+ val naturalMaxBounds: Flow<Rect>
+}
+
+class ConfigurationInteractorImpl
+@Inject
+constructor(private val repository: ConfigurationRepository) : ConfigurationInteractor {
+
+ override val naturalMaxBounds: Flow<Rect>
+ get() = repository.configurationValues.map { it.naturalScreenBounds }.distinctUntilChanged()
+
+ /**
+ * Returns screen size adjusted to rotation, so returned screen size is stable across all
+ * rotations
+ */
+ private val Configuration.naturalScreenBounds: Rect
+ get() {
+ val rotation = windowConfiguration.displayRotation
+ val maxBounds = windowConfiguration.maxBounds
+ return if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
+ Rect(0, 0, maxBounds.width(), maxBounds.height())
+ } else {
+ Rect(0, 0, maxBounds.height(), maxBounds.width())
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/data/CommonUiDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/common/ui/data/CommonUiDataLayerModule.kt
deleted file mode 100644
index b0e6931..0000000
--- a/packages/SystemUI/src/com/android/systemui/common/ui/data/CommonUiDataLayerModule.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright (C) 2023 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.common.ui.data
-
-import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryModule
-import dagger.Module
-
-@Module(includes = [ConfigurationRepositoryModule::class]) object CommonUiDataLayerModule
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt
index 7fa762a..2052c70 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt
@@ -22,7 +22,7 @@
import android.view.DisplayInfo
import androidx.annotation.DimenRes
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -49,6 +49,7 @@
val onConfigurationChange: Flow<Unit>
val scaleForResolution: Flow<Float>
+ val configurationValues: Flow<Configuration>
fun getResolutionScale(): Float
@@ -68,7 +69,7 @@
private val displayInfo = MutableStateFlow(DisplayInfo())
override val onAnyConfigurationChange: Flow<Unit> =
- ConflatedCallbackFlow.conflatedCallbackFlow {
+ conflatedCallbackFlow {
val callback =
object : ConfigurationController.ConfigurationListener {
override fun onUiModeChanged() {
@@ -92,7 +93,7 @@
}
override val onConfigurationChange: Flow<Unit> =
- ConflatedCallbackFlow.conflatedCallbackFlow {
+ conflatedCallbackFlow {
val callback =
object : ConfigurationController.ConfigurationListener {
override fun onConfigChanged(newConfig: Configuration) {
@@ -103,6 +104,20 @@
awaitClose { configurationController.removeCallback(callback) }
}
+ override val configurationValues: Flow<Configuration> =
+ conflatedCallbackFlow {
+ val callback =
+ object : ConfigurationController.ConfigurationListener {
+ override fun onConfigChanged(newConfig: Configuration) {
+ trySend(newConfig)
+ }
+ }
+
+ trySend(context.resources.configuration)
+ configurationController.addCallback(callback)
+ awaitClose { configurationController.removeCallback(callback) }
+ }
+
override val scaleForResolution: StateFlow<Float> =
onConfigurationChange
.mapLatest { getResolutionScale() }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index b34b459..9fc86ad 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -41,9 +41,9 @@
import com.android.systemui.bouncer.ui.BouncerViewModule;
import com.android.systemui.classifier.FalsingModule;
import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule;
-import com.android.systemui.common.ui.data.CommonUiDataLayerModule;
import com.android.systemui.communal.dagger.CommunalModule;
import com.android.systemui.complication.dagger.ComplicationComponent;
+import com.android.systemui.common.CommonModule;
import com.android.systemui.controls.dagger.ControlsModule;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dagger.qualifiers.SystemUser;
@@ -172,8 +172,8 @@
BouncerViewModule.class,
ClipboardOverlayModule.class,
ClockRegistryModule.class,
- CommonUiDataLayerModule.class,
CommunalModule.class,
+ CommonModule.class,
ConnectivityModule.class,
ControlsModule.class,
CoroutinesModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 823caa0..285cb5a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -2673,7 +2673,7 @@
&& !mQsController.getFullyExpanded()) {
alpha *= mClockPositionResult.clockAlpha;
}
- mNotificationStackScrollLayoutController.setAlpha(alpha);
+ mNotificationStackScrollLayoutController.setMaxAlphaForExpansion(alpha);
}
private float getFadeoutAlpha() {
@@ -4697,7 +4697,7 @@
NotificationStackScrollLayoutController stackScroller) {
return (Float alpha) -> {
mKeyguardStatusViewController.setAlpha(alpha);
- stackScroller.setAlpha(alpha);
+ stackScroller.setMaxAlphaForExpansion(alpha);
if (keyguardBottomAreaRefactor()) {
mKeyguardInteractor.setAlpha(alpha);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index a0ad560..2f8a375 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -1157,6 +1157,20 @@
return mSpeedBumpIndex;
}
+ private boolean mSuppressChildrenMeasureAndLayout = false;
+
+ /**
+ * Similar to {@link ViewGroup#suppressLayout} but still performs layout of
+ * the container itself and suppresses only measure and layout calls to children.
+ */
+ public void suppressChildrenMeasureAndLayout(boolean suppress) {
+ mSuppressChildrenMeasureAndLayout = suppress;
+
+ if (!suppress) {
+ requestLayout();
+ }
+ }
+
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Trace.beginSection("NotificationStackScrollLayout#onMeasure");
@@ -1169,6 +1183,12 @@
int width = MeasureSpec.getSize(widthMeasureSpec);
updateSidePadding(width);
+
+ if (mSuppressChildrenMeasureAndLayout) {
+ Trace.endSection();
+ return;
+ }
+
int childWidthSpec = MeasureSpec.makeMeasureSpec(width - mSidePaddings * 2,
MeasureSpec.getMode(widthMeasureSpec));
// Don't constrain the height of the children so we know how big they'd like to be
@@ -1192,18 +1212,21 @@
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
- // we layout all our children centered on the top
- float centerX = getWidth() / 2.0f;
- for (int i = 0; i < getChildCount(); i++) {
- View child = getChildAt(i);
- // We need to layout all children even the GONE ones, such that the heights are
- // calculated correctly as they are used to calculate how many we can fit on the screen
- float width = child.getMeasuredWidth();
- float height = child.getMeasuredHeight();
- child.layout((int) (centerX - width / 2.0f),
- 0,
- (int) (centerX + width / 2.0f),
- (int) height);
+ if (!mSuppressChildrenMeasureAndLayout) {
+ // we layout all our children centered on the top
+ float centerX = getWidth() / 2.0f;
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ // We need to layout all children even the GONE ones, such that the heights are
+ // calculated correctly as they are used to calculate how many we can fit on
+ // the screen
+ float width = child.getMeasuredWidth();
+ float height = child.getMeasuredHeight();
+ child.layout((int) (centerX - width / 2.0f),
+ 0,
+ (int) (centerX + width / 2.0f),
+ (int) height);
+ }
}
setMaxLayoutHeight(getHeight());
updateContentHeight();
@@ -5097,6 +5120,7 @@
println(pw, "qsClipDismiss", mDismissUsingRowTranslationX);
println(pw, "visibility", visibilityString(getVisibility()));
println(pw, "alpha", getAlpha());
+ println(pw, "suppressChildrenMeasureLayout", mSuppressChildrenMeasureAndLayout);
println(pw, "scrollY", mAmbientState.getScrollY());
println(pw, "maxTopPadding", mMaxTopPadding);
println(pw, "showShelfOnly", mShouldShowShelfOnly);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 44140b9..99b3a00 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -18,6 +18,8 @@
import static android.service.notification.NotificationStats.DISMISSAL_SHADE;
import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
+
+import static com.android.app.animation.Interpolators.STANDARD;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
@@ -27,8 +29,10 @@
import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_GENTLE;
import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_HIGH_PRIORITY;
import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.SelectedRows;
+import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_STANDARD;
import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
+import android.animation.ObjectAnimator;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Point;
@@ -38,6 +42,7 @@
import android.service.notification.StatusBarNotification;
import android.util.Log;
import android.util.Pair;
+import android.util.Property;
import android.view.Display;
import android.view.MotionEvent;
import android.view.View;
@@ -53,6 +58,8 @@
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.view.OneShotPreDrawListener;
+import com.android.systemui.Dumpable;
import com.android.systemui.ExpandHelper;
import com.android.systemui.Gefingerpoken;
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
@@ -133,6 +140,7 @@
import com.android.systemui.util.Compile;
import com.android.systemui.util.settings.SecureSettings;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
@@ -145,7 +153,7 @@
* Controller for {@link NotificationStackScrollLayout}.
*/
@SysUISingleton
-public class NotificationStackScrollLayoutController {
+public class NotificationStackScrollLayoutController implements Dumpable {
private static final String TAG = "StackScrollerController";
private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
private static final String HIGH_PRIORITY = "high_priority";
@@ -190,7 +198,6 @@
private final GroupExpansionManager mGroupExpansionManager;
private final SeenNotificationsInteractor mSeenNotificationsInteractor;
private final KeyguardTransitionRepository mKeyguardTransitionRepo;
-
private NotificationStackScrollLayout mView;
private NotificationSwipeHelper mSwipeHelper;
@Nullable
@@ -240,6 +247,22 @@
}
};
+ private static final Property<NotificationStackScrollLayoutController, Float>
+ HIDE_ALPHA_PROPERTY = new Property<>(Float.class, "HideNotificationsAlpha") {
+ @Override
+ public Float get(NotificationStackScrollLayoutController object) {
+ return object.mMaxAlphaForUnhide;
+ }
+
+ @Override
+ public void set(NotificationStackScrollLayoutController object, Float value) {
+ object.setMaxAlphaForUnhide(value);
+ }
+ };
+
+ @Nullable
+ private ObjectAnimator mHideAlphaAnimator = null;
+
private final DeviceProvisionedListener mDeviceProvisionedListener =
new DeviceProvisionedListener() {
@Override
@@ -302,6 +325,8 @@
};
private NotifStats mNotifStats = NotifStats.getEmpty();
+ private float mMaxAlphaForExpansion = 1.0f;
+ private float mMaxAlphaForUnhide = 1.0f;
private final NotificationListViewBinder mViewBinder;
@@ -713,6 +738,7 @@
mDismissibilityProvider = dismissibilityProvider;
mActivityStarter = activityStarter;
mView.passSplitShadeStateController(splitShadeStateController);
+ mDumpManager.registerDumpable(this);
updateResources();
setUpView();
}
@@ -818,7 +844,7 @@
mGroupExpansionManager.registerGroupExpansionChangeListener(
(changedRow, expanded) -> mView.onGroupExpandChanged(changedRow, expanded));
- mViewBinder.bind(mView);
+ mViewBinder.bind(mView, this);
collectFlow(mView, mKeyguardTransitionRepo.getTransitions(),
this::onKeyguardTransitionChanged);
@@ -875,6 +901,10 @@
mView.requestLayout();
}
+ public void addOneShotPreDrawListener(Runnable runnable) {
+ OneShotPreDrawListener.add(mView, runnable);
+ }
+
public Display getDisplay() {
return mView.getDisplay();
}
@@ -1157,12 +1187,49 @@
return mView.getEmptyShadeViewHeight();
}
- public void setAlpha(float alpha) {
+ public void setMaxAlphaForExpansion(float alpha) {
+ mMaxAlphaForExpansion = alpha;
+ updateAlpha();
+ }
+
+ private void setMaxAlphaForUnhide(float alpha) {
+ mMaxAlphaForUnhide = alpha;
+ updateAlpha();
+ }
+
+ private void updateAlpha() {
if (mView != null) {
- mView.setAlpha(alpha);
+ mView.setAlpha(Math.min(mMaxAlphaForExpansion, mMaxAlphaForUnhide));
}
}
+ public void setSuppressChildrenMeasureAndLayout(boolean suppressLayout) {
+ mView.suppressChildrenMeasureAndLayout(suppressLayout);
+ }
+
+ public void updateNotificationsContainerVisibility(boolean visible, boolean animate) {
+ if (mHideAlphaAnimator != null) {
+ mHideAlphaAnimator.cancel();
+ }
+
+ final float targetAlpha = visible ? 1f : 0f;
+
+ if (animate) {
+ mHideAlphaAnimator = createAlphaAnimator(targetAlpha);
+ mHideAlphaAnimator.start();
+ } else {
+ HIDE_ALPHA_PROPERTY.set(this, targetAlpha);
+ }
+ }
+
+ private ObjectAnimator createAlphaAnimator(float targetAlpha) {
+ final ObjectAnimator objectAnimator = ObjectAnimator
+ .ofFloat(this, HIDE_ALPHA_PROPERTY, targetAlpha);
+ objectAnimator.setInterpolator(STANDARD);
+ objectAnimator.setDuration(ANIMATION_DURATION_STANDARD);
+ return objectAnimator;
+ }
+
public float calculateAppearFraction(float height) {
return mView.calculateAppearFraction(height);
}
@@ -1635,6 +1702,12 @@
}
}
+ @Override
+ public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+ pw.println("mMaxAlphaForExpansion=" + mMaxAlphaForExpansion);
+ pw.println("mMaxAlphaForUnhide=" + mMaxAlphaForUnhide);
+ }
+
/**
* Enum for UiEvent logged from this class
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractor.kt
new file mode 100644
index 0000000..4de3a7f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractor.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2023 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.stack.domain.interactor
+
+import android.graphics.Rect
+import android.util.Log
+import com.android.app.tracing.FlowTracing.traceEach
+import com.android.systemui.common.domain.interactor.ConfigurationInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_ON
+import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
+import com.android.systemui.util.animation.data.repository.AnimationStatusRepository
+import com.android.systemui.util.kotlin.WithPrev
+import com.android.systemui.util.kotlin.area
+import com.android.systemui.util.kotlin.pairwise
+import com.android.systemui.util.kotlin.race
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.withTimeout
+import javax.inject.Inject
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class HideNotificationsInteractor
+@Inject
+constructor(
+ private val unfoldTransitionInteractor: UnfoldTransitionInteractor,
+ private val configurationInteractor: ConfigurationInteractor,
+ private val animationsStatus: AnimationStatusRepository,
+ private val powerInteractor: PowerInteractor
+) {
+
+ val shouldHideNotifications: Flow<Boolean>
+ get() =
+ if (!unfoldTransitionInteractor.isAvailable) {
+ // Do nothing on non-foldable devices
+ emptyFlow()
+ } else {
+ screenSizeChangesFlow
+ .flatMapLatest {
+ flow {
+ // Hide notifications on each display resize
+ emit(true)
+ try {
+ waitForDisplaySwitchFinish(it)
+ } catch (_: TimeoutCancellationException) {
+ Log.e(TAG, "Timed out waiting for display switch")
+ } finally {
+ emit(false)
+ }
+ }
+ }
+ .distinctUntilChanged()
+ .traceEach(HIDE_STATUS_TRACK_NAME, logcat = true) { shouldHide ->
+ if (shouldHide) "hidden" else "visible"
+ }
+ }
+
+ private suspend fun waitForDisplaySwitchFinish(screenSizeChange: WithPrev<Rect, Rect>) {
+ withTimeout(timeMillis = DISPLAY_SWITCH_TIMEOUT_MILLIS) {
+ val waitForDisplaySwitchOrAnimation: suspend () -> Unit = {
+ if (shouldWaitForAnimationEnd(screenSizeChange)) {
+ unfoldTransitionInteractor.waitForTransitionFinish()
+ } else {
+ waitForScreenTurnedOn()
+ }
+ }
+
+ race({ waitForDisplaySwitchOrAnimation() }, { waitForGoingToSleep() })
+ }
+ }
+
+ private suspend fun shouldWaitForAnimationEnd(screenSizeChange: WithPrev<Rect, Rect>): Boolean =
+ animationsStatus.areAnimationsEnabled().first() && screenSizeChange.isUnfold
+
+ private suspend fun waitForScreenTurnedOn() =
+ powerInteractor.screenPowerState.filter { it == SCREEN_ON }.first()
+
+ private suspend fun waitForGoingToSleep() =
+ powerInteractor.detailedWakefulness.filter { it.isAsleep() }.first()
+
+ private val screenSizeChangesFlow: Flow<WithPrev<Rect, Rect>>
+ get() = configurationInteractor.naturalMaxBounds.pairwise()
+
+ private val WithPrev<Rect, Rect>.isUnfold: Boolean
+ get() = newValue.area > previousValue.area
+
+ private companion object {
+ private const val TAG = "DisplaySwitchNotificationsHideInteractor"
+ private const val HIDE_STATUS_TRACK_NAME = "NotificationsHiddenForDisplayChange"
+ private const val DISPLAY_SWITCH_TIMEOUT_MILLIS = 5_000L
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/DisplaySwitchNotificationsHiderFlag.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/DisplaySwitchNotificationsHiderFlag.kt
new file mode 100644
index 0000000..98c1734
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/DisplaySwitchNotificationsHiderFlag.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 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.stack.shared
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the DisplaySwitchNotificationsHider flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object DisplaySwitchNotificationsHiderFlag {
+ const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_HIDE_ON_DISPLAY_SWITCH
+
+ /** Is the hiding enabled? */
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.notificationsHideOnDisplaySwitch()
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This protects users from the
+ * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+ * build to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun isUnexpectedlyInLegacyMode() =
+ RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is enabled to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
new file mode 100644
index 0000000..274bf94
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 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.stack.ui.viewbinder
+
+import androidx.core.view.doOnDetach
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
+import kotlinx.coroutines.launch
+
+/**
+ * Binds a [NotificationStackScrollLayoutController] to its [view model][NotificationListViewModel].
+ */
+object HideNotificationsBinder {
+ fun bindHideList(
+ viewController: NotificationStackScrollLayoutController,
+ viewModel: NotificationListViewModel
+ ) {
+ viewController.view.repeatWhenAttached {
+ lifecycleScope.launch {
+ viewModel.hideListViewModel.shouldHideListForPerformance.collect { shouldHide ->
+ viewController.bindHideState(shouldHide)
+ }
+ }
+ }
+
+ viewController.view.doOnDetach { viewController.bindHideState(shouldHide = false) }
+ }
+
+ private fun NotificationStackScrollLayoutController.bindHideState(shouldHide: Boolean) {
+ if (shouldHide) {
+ updateNotificationsContainerVisibility(/* visible= */ false, /* animate=*/ false)
+ setSuppressChildrenMeasureAndLayout(true)
+ } else {
+ setSuppressChildrenMeasureAndLayout(false)
+
+ // Show notifications back only after layout has finished because we need
+ // to wait until they have resized to the new display size
+ addOneShotPreDrawListener {
+ updateNotificationsContainerVisibility(/* visible= */ true, /* animate=*/ true)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index d55c0de..6cf5610 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -29,6 +29,8 @@
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore
import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.HideNotificationsBinder.bindHideList
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
import com.android.systemui.statusbar.phone.NotificationIconAreaController
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -46,9 +48,13 @@
private val shelfIconViewStore: ShelfNotificationIconViewStore,
) {
- fun bind(view: NotificationStackScrollLayout) {
+ fun bind(
+ view: NotificationStackScrollLayout,
+ viewController: NotificationStackScrollLayoutController
+ ) {
bindShelf(view)
bindFooter(view)
+ bindHideList(viewController, viewModel)
}
private fun bindShelf(parentView: NotificationStackScrollLayout) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/HideListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/HideListViewModel.kt
new file mode 100644
index 0000000..e1d14d1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/HideListViewModel.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 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.stack.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.stack.domain.interactor.HideNotificationsInteractor
+import com.android.systemui.statusbar.notification.stack.shared.DisplaySwitchNotificationsHiderFlag
+import javax.inject.Inject
+import javax.inject.Provider
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+
+@SysUISingleton
+class HideListViewModel
+@Inject
+constructor(
+ private val hideNotificationsInteractor: Provider<HideNotificationsInteractor>,
+) {
+ /**
+ * Emits `true` whenever we want to hide the notifications list for performance reasons, then it
+ * emits 'false' to show notifications back. This is used on foldable devices and emits
+ * *nothing* on other devices.
+ */
+ val shouldHideListForPerformance: Flow<Boolean>
+ get() =
+ if (DisplaySwitchNotificationsHiderFlag.isEnabled) {
+ hideNotificationsInteractor.get().shouldHideNotifications
+ } else emptyFlow()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index f01245f..4f76680 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -26,5 +26,6 @@
@Inject
constructor(
val shelf: NotificationShelfViewModel,
- val footer: Optional<FooterViewModel>,
+ val hideListViewModel: HideListViewModel,
+ val footer: Optional<FooterViewModel>
)
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
index 71314f1..7b628f8 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
@@ -24,6 +24,10 @@
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.LifecycleScreenStatusProvider
import com.android.systemui.unfold.config.UnfoldTransitionConfig
+import com.android.systemui.unfold.data.repository.UnfoldTransitionRepository
+import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl
+import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
+import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractorImpl
import com.android.systemui.unfold.system.SystemUnfoldSharedModule
import com.android.systemui.unfold.updates.FoldProvider
import com.android.systemui.unfold.updates.FoldStateProvider
@@ -149,8 +153,7 @@
return resultingProvider?.get()?.orElse(null)?.let { unfoldProgressProvider ->
UnfoldProgressProvider(unfoldProgressProvider, foldProvider)
- }
- ?: ShellUnfoldProgressProvider.NO_PROVIDER
+ } ?: ShellUnfoldProgressProvider.NO_PROVIDER
}
@Provides
@@ -162,6 +165,10 @@
@IntoMap
@ClassKey(UnfoldTraceLogger::class)
fun bindUnfoldTraceLogger(impl: UnfoldTraceLogger): CoreStartable
+
+ @Binds fun bindRepository(impl: UnfoldTransitionRepositoryImpl): UnfoldTransitionRepository
+
+ @Binds fun bindInteractor(impl: UnfoldTransitionInteractorImpl): UnfoldTransitionInteractor
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt
new file mode 100644
index 0000000..0d3682c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 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.unfold.data.repository
+
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished
+import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted
+import com.android.systemui.util.kotlin.getOrNull
+import java.util.Optional
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+
+/** Repository for fold/unfold transitions */
+interface UnfoldTransitionRepository {
+ /** Returns false if fold/unfold transitions are not available on this device */
+ val isAvailable: Boolean
+
+ /**
+ * Emits current transition state on each transition change such as transition start or finish
+ * [UnfoldTransitionStatus]
+ */
+ val transitionStatus: Flow<UnfoldTransitionStatus>
+}
+
+/** Transition event of fold/unfold transition */
+sealed class UnfoldTransitionStatus {
+ /** Status that is sent when fold or unfold transition is in started state */
+ data object TransitionStarted : UnfoldTransitionStatus()
+ /** Status that is sent when fold or unfold transition is finished */
+ data object TransitionFinished : UnfoldTransitionStatus()
+}
+
+class UnfoldTransitionRepositoryImpl
+@Inject
+constructor(
+ private val unfoldProgressProvider: Optional<UnfoldTransitionProgressProvider>,
+) : UnfoldTransitionRepository {
+
+ override val isAvailable: Boolean
+ get() = unfoldProgressProvider.isPresent
+
+ override val transitionStatus: Flow<UnfoldTransitionStatus>
+ get() {
+ val provider = unfoldProgressProvider.getOrNull() ?: return emptyFlow()
+
+ return conflatedCallbackFlow {
+ val callback =
+ object : UnfoldTransitionProgressProvider.TransitionProgressListener {
+ override fun onTransitionStarted() {
+ trySend(TransitionStarted)
+ }
+
+ override fun onTransitionFinished() {
+ trySend(TransitionFinished)
+ }
+ }
+ provider.addCallback(callback)
+ awaitClose { provider.removeCallback(callback) }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt
new file mode 100644
index 0000000..a2e77af
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 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.unfold.domain.interactor
+
+import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished
+import com.android.systemui.unfold.data.repository.UnfoldTransitionRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
+
+interface UnfoldTransitionInteractor {
+ val isAvailable: Boolean
+
+ suspend fun waitForTransitionFinish()
+}
+
+class UnfoldTransitionInteractorImpl
+@Inject
+constructor(private val repository: UnfoldTransitionRepository) : UnfoldTransitionInteractor {
+
+ override val isAvailable: Boolean
+ get() = repository.isAvailable
+
+ override suspend fun waitForTransitionFinish() {
+ repository.transitionStatus.filter { it is TransitionFinished }.first()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt b/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt
new file mode 100644
index 0000000..adae782
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 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.animation.data.repository
+
+import android.content.ContentResolver
+import android.database.ContentObserver
+import android.os.Handler
+import android.provider.Settings
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.unfold.util.ScaleAwareTransitionProgressProvider.Companion.areAnimationsEnabled
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.withContext
+
+/** Utility class that could give information about if animation are enabled in the system */
+interface AnimationStatusRepository {
+ fun areAnimationsEnabled(): Flow<Boolean>
+}
+
+class AnimationStatusRepositoryImpl
+@Inject
+constructor(
+ private val resolver: ContentResolver,
+ @Background private val backgroundHandler: Handler,
+ @Background private val backgroundDispatcher: CoroutineDispatcher
+) : AnimationStatusRepository {
+
+ /**
+ * Emits true if animations are enabled in the system, after subscribing it immediately emits
+ * the current state
+ */
+ override fun areAnimationsEnabled(): Flow<Boolean> = conflatedCallbackFlow {
+ val initialValue = withContext(backgroundDispatcher) { resolver.areAnimationsEnabled() }
+ trySend(initialValue)
+
+ val observer =
+ object : ContentObserver(backgroundHandler) {
+ override fun onChange(selfChange: Boolean) {
+ val updatedValue = resolver.areAnimationsEnabled()
+ trySend(updatedValue)
+ }
+ }
+
+ resolver.registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
+ /* notifyForDescendants= */ false,
+ observer
+ )
+
+ awaitClose { resolver.unregisterContentObserver(observer) }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java b/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java
index 981bf01..9c8a481 100644
--- a/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java
+++ b/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java
@@ -18,6 +18,8 @@
import com.android.systemui.util.RingerModeTracker;
import com.android.systemui.util.RingerModeTrackerImpl;
+import com.android.systemui.util.animation.data.repository.AnimationStatusRepository;
+import com.android.systemui.util.animation.data.repository.AnimationStatusRepositoryImpl;
import com.android.systemui.util.wrapper.UtilWrapperModule;
import dagger.Binds;
@@ -31,4 +33,8 @@
/** */
@Binds
RingerModeTracker provideRingerModeTracker(RingerModeTrackerImpl ringerModeTrackerImpl);
+
+ @Binds
+ AnimationStatusRepository provideAnimationStatus(
+ AnimationStatusRepositoryImpl ringerModeTrackerImpl);
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Rect.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Rect.kt
new file mode 100644
index 0000000..bcbc89c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Rect.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 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 android.graphics.Rect
+
+/** Returns the area of this rectangle */
+val Rect.area: Long
+ get() = width().toLong() * height().toLong()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/domain/interactor/ConfigurationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/common/domain/interactor/ConfigurationInteractorTest.kt
new file mode 100644
index 0000000..bfa3641
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/common/domain/interactor/ConfigurationInteractorTest.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2023 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.common.domain.interactor
+
+import android.content.res.Configuration
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.view.Surface.ROTATION_0
+import android.view.Surface.ROTATION_90
+import android.view.Surface.Rotation
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.statusbar.policy.FakeConfigurationController
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+open class ConfigurationInteractorTest : SysuiTestCase() {
+
+ private val testScope = TestScope()
+
+ private val configurationController = FakeConfigurationController()
+ private val configurationRepository =
+ ConfigurationRepositoryImpl(
+ configurationController,
+ context,
+ testScope.backgroundScope,
+ mock()
+ )
+
+ private lateinit var configuration: Configuration
+ private lateinit var underTest: ConfigurationInteractor
+
+ @Before
+ fun setUp() {
+ configuration = context.resources.configuration
+
+ val testableResources = context.getOrCreateTestableResources()
+ testableResources.overrideConfiguration(configuration)
+
+ underTest = ConfigurationInteractorImpl(configurationRepository)
+ }
+
+ @Test
+ fun maxBoundsChange_emitsMaxBoundsChange() =
+ testScope.runTest {
+ val values by collectValues(underTest.naturalMaxBounds)
+
+ updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
+ runCurrent()
+ updateDisplay(width = DISPLAY_WIDTH * 2, height = DISPLAY_HEIGHT * 3)
+ runCurrent()
+
+ assertThat(values)
+ .containsExactly(
+ Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT),
+ Rect(0, 0, DISPLAY_WIDTH * 2, DISPLAY_HEIGHT * 3),
+ )
+ .inOrder()
+ }
+
+ @Test
+ fun maxBoundsSameOnConfigChange_doesNotEmitMaxBoundsChange() =
+ testScope.runTest {
+ val values by collectValues(underTest.naturalMaxBounds)
+
+ updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
+ runCurrent()
+ updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
+ runCurrent()
+
+ assertThat(values).containsExactly(Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT))
+ }
+
+ @Test
+ fun firstMaxBoundsChange_emitsMaxBoundsChange() =
+ testScope.runTest {
+ val values by collectValues(underTest.naturalMaxBounds)
+
+ updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
+ runCurrent()
+
+ assertThat(values).containsExactly(Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT))
+ }
+
+ @Test
+ fun displayRotatedButMaxBoundsTheSame_doesNotEmitNewMaxBoundsChange() =
+ testScope.runTest {
+ val values by collectValues(underTest.naturalMaxBounds)
+
+ updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
+ runCurrent()
+ updateDisplay(width = DISPLAY_HEIGHT, height = DISPLAY_WIDTH, rotation = ROTATION_90)
+ runCurrent()
+
+ assertThat(values).containsExactly(Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT))
+ }
+
+ private fun updateDisplay(
+ width: Int = DISPLAY_WIDTH,
+ height: Int = DISPLAY_HEIGHT,
+ @Rotation rotation: Int = ROTATION_0
+ ) {
+ configuration.windowConfiguration.maxBounds.set(Rect(0, 0, width, height))
+ configuration.windowConfiguration.displayRotation = rotation
+
+ configurationController.onConfigurationChanged(configuration)
+ }
+
+ private companion object {
+ private const val DISPLAY_WIDTH = 100
+ private const val DISPLAY_HEIGHT = 200
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index 1a8d4f9..d573764 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -793,7 +793,7 @@
// We are interested in the last value of the stack alpha.
ArgumentCaptor<Float> alphaCaptor = ArgumentCaptor.forClass(Float.class);
verify(mNotificationStackScrollLayoutController, atLeastOnce())
- .setAlpha(alphaCaptor.capture());
+ .setMaxAlphaForExpansion(alphaCaptor.capture());
assertThat(alphaCaptor.getValue()).isEqualTo(1.0f);
}
@@ -814,7 +814,7 @@
// We are interested in the last value of the stack alpha.
ArgumentCaptor<Float> alphaCaptor = ArgumentCaptor.forClass(Float.class);
verify(mNotificationStackScrollLayoutController, atLeastOnce())
- .setAlpha(alphaCaptor.capture());
+ .setMaxAlphaForExpansion(alphaCaptor.capture());
assertThat(alphaCaptor.getValue()).isEqualTo(0.0f);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt
new file mode 100644
index 0000000..46e8453
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2023 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.stack.domain.interactor
+
+import android.content.res.Configuration
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.view.Surface
+import android.view.Surface.ROTATION_0
+import android.view.Surface.ROTATION_90
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.domain.interactor.ConfigurationInteractorImpl
+import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_ON
+import com.android.systemui.power.shared.model.WakefulnessState.STARTING_TO_SLEEP
+import com.android.systemui.statusbar.policy.FakeConfigurationController
+import com.android.systemui.unfold.TestUnfoldTransitionProvider
+import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl
+import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractorImpl
+import com.android.systemui.util.animation.FakeAnimationStatusRepository
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+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.MockitoAnnotations
+import java.time.Duration
+import java.util.Optional
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+open class HideNotificationsInteractorTest : SysuiTestCase() {
+
+ private val testScope = TestScope()
+
+ private val animationStatus = FakeAnimationStatusRepository()
+ private val configurationController = FakeConfigurationController()
+ private val unfoldTransitionProgressProvider = TestUnfoldTransitionProvider()
+ private val powerRepository = FakePowerRepository()
+ private val powerInteractor =
+ PowerInteractor(
+ repository = powerRepository,
+ falsingCollector = mock(),
+ screenOffAnimationController = mock(),
+ statusBarStateController = mock()
+ )
+
+ private val unfoldTransitionRepository =
+ UnfoldTransitionRepositoryImpl(Optional.of(unfoldTransitionProgressProvider))
+ private val unfoldTransitionInteractor =
+ UnfoldTransitionInteractorImpl(unfoldTransitionRepository)
+
+ private val configurationRepository =
+ ConfigurationRepositoryImpl(
+ configurationController,
+ context,
+ testScope.backgroundScope,
+ mock()
+ )
+ private val configurationInteractor = ConfigurationInteractorImpl(configurationRepository)
+
+ private lateinit var configuration: Configuration
+ private lateinit var underTest: HideNotificationsInteractor
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ configuration = context.resources.configuration
+
+ val testableResources = context.getOrCreateTestableResources()
+ testableResources.overrideConfiguration(configuration)
+
+ updateDisplay()
+
+ underTest =
+ HideNotificationsInteractor(
+ unfoldTransitionInteractor,
+ configurationInteractor,
+ animationStatus,
+ powerInteractor
+ )
+ }
+
+ @Test
+ fun displaySwitch_hidesNotifications() =
+ testScope.runTest {
+ val values by collectValues(hideNotificationsFlow)
+
+ runCurrent()
+ updateDisplay(width = INITIAL_DISPLAY_WIDTH * 2)
+ runCurrent()
+
+ assertThat(values).containsExactly(true).inOrder()
+ }
+
+ @Test
+ fun displaySwitch_sizeIsTheSame_noChangesToNotifications() =
+ testScope.runTest {
+ val values by collectValues(hideNotificationsFlow)
+
+ runCurrent()
+ updateDisplay(width = INITIAL_DISPLAY_WIDTH)
+ runCurrent()
+
+ assertThat(values).isEmpty()
+ }
+
+ @Test
+ fun displaySwitch_sizeIsTheSameAfterRotation_noChangesToNotifications() =
+ testScope.runTest {
+ val values by collectValues(hideNotificationsFlow)
+
+ runCurrent()
+ updateDisplay(
+ width = INITIAL_DISPLAY_HEIGHT,
+ height = INITIAL_DISPLAY_WIDTH,
+ rotation = ROTATION_90
+ )
+ runCurrent()
+
+ assertThat(values).isEmpty()
+ }
+
+ @Test
+ fun displaySwitch_noAnimations_screenTurnedOn_showsNotificationsBack() =
+ testScope.runTest {
+ givenAnimationsEnabled(false)
+ val values by collectValues(hideNotificationsFlow)
+
+ runCurrent()
+ updateDisplay(width = INITIAL_DISPLAY_WIDTH * 2)
+ runCurrent()
+ powerRepository.setScreenPowerState(SCREEN_ON)
+ runCurrent()
+
+ assertThat(values).containsExactly(true, false).inOrder()
+ }
+
+ @Test
+ fun displaySwitchUnfold_animationsEnabled_screenTurnedOn_doesNotShowNotifications() =
+ testScope.runTest {
+ givenAnimationsEnabled(true)
+ val values by collectValues(hideNotificationsFlow)
+
+ runCurrent()
+ updateDisplay(width = INITIAL_DISPLAY_WIDTH * 2)
+ runCurrent()
+ powerRepository.setScreenPowerState(SCREEN_ON)
+ runCurrent()
+
+ assertThat(values).containsExactly(true).inOrder()
+ }
+
+ @Test
+ fun displaySwitchFold_animationsEnabled_screenTurnedOn_showsNotifications() =
+ testScope.runTest {
+ givenAnimationsEnabled(true)
+ val values by collectValues(hideNotificationsFlow)
+
+ runCurrent()
+ updateDisplay(width = INITIAL_DISPLAY_WIDTH / 2)
+ runCurrent()
+ powerRepository.setScreenPowerState(SCREEN_ON)
+ runCurrent()
+
+ assertThat(values).containsExactly(true, false).inOrder()
+ }
+
+ @Test
+ fun displaySwitch_noAnimations_screenGoesToSleep_showsNotificationsBack() =
+ testScope.runTest {
+ givenAnimationsEnabled(false)
+ val values by collectValues(hideNotificationsFlow)
+
+ runCurrent()
+ updateDisplay(width = INITIAL_DISPLAY_WIDTH * 2)
+ runCurrent()
+ powerRepository.updateWakefulness(STARTING_TO_SLEEP)
+ runCurrent()
+
+ assertThat(values).containsExactly(true, false).inOrder()
+ }
+
+ @Test
+ fun displaySwitch_animationsEnabled_screenGoesToSleep_showsNotificationsBack() =
+ testScope.runTest {
+ givenAnimationsEnabled(true)
+ val values by collectValues(hideNotificationsFlow)
+
+ runCurrent()
+ updateDisplay(width = INITIAL_DISPLAY_WIDTH * 2)
+ runCurrent()
+ powerRepository.updateWakefulness(STARTING_TO_SLEEP)
+ runCurrent()
+
+ assertThat(values).containsExactly(true, false).inOrder()
+ }
+
+ @Test
+ fun displaySwitch_animationsEnabled_unfoldAnimationNotFinished_notificationsHidden() =
+ testScope.runTest {
+ givenAnimationsEnabled(true)
+ val values by collectValues(hideNotificationsFlow)
+
+ runCurrent()
+ updateDisplay(width = INITIAL_DISPLAY_WIDTH * 2)
+ runCurrent()
+
+ assertThat(values).containsExactly(true).inOrder()
+ }
+
+ @Test
+ fun displaySwitch_animationsEnabled_unfoldAnimationFinishes_showsNotificationsBack() =
+ testScope.runTest {
+ givenAnimationsEnabled(true)
+ val values by collectValues(hideNotificationsFlow)
+
+ runCurrent()
+ updateDisplay(width = INITIAL_DISPLAY_WIDTH * 2)
+ runCurrent()
+ unfoldTransitionProgressProvider.onTransitionFinished()
+ runCurrent()
+
+ assertThat(values).containsExactly(true, false).inOrder()
+ }
+
+ @Test
+ fun displaySwitch_noEvents_afterTimeout_showsNotificationsBack() =
+ testScope.runTest {
+ givenAnimationsEnabled(true)
+ val values by collectValues(hideNotificationsFlow)
+
+ runCurrent()
+ updateDisplay(width = INITIAL_DISPLAY_WIDTH * 2)
+ runCurrent()
+ advanceTimeBy(Duration.ofMillis(10_000).toMillis())
+
+ assertThat(values).containsExactly(true, false).inOrder()
+ }
+
+ @Test
+ fun displaySwitch_noEvents_beforeTimeout_doesNotShowNotifications() =
+ testScope.runTest {
+ givenAnimationsEnabled(true)
+ val values by collectValues(hideNotificationsFlow)
+
+ runCurrent()
+ updateDisplay(width = INITIAL_DISPLAY_WIDTH * 2)
+ runCurrent()
+ advanceTimeBy(Duration.ofMillis(500).toMillis())
+
+ assertThat(values).containsExactly(true).inOrder()
+ }
+
+ private val hideNotificationsFlow: Flow<Boolean>
+ get() = underTest.shouldHideNotifications
+
+ private fun updateDisplay(
+ width: Int = INITIAL_DISPLAY_WIDTH,
+ height: Int = INITIAL_DISPLAY_HEIGHT,
+ @Surface.Rotation rotation: Int = ROTATION_0
+ ) {
+ configuration.windowConfiguration.maxBounds.set(Rect(0, 0, width, height))
+ configuration.windowConfiguration.displayRotation = rotation
+
+ configurationController.onConfigurationChanged(configuration)
+ }
+
+ private fun givenAnimationsEnabled(enabled: Boolean) {
+ animationStatus.onAnimationStatusChanged(enabled)
+ }
+
+ private companion object {
+ private const val INITIAL_DISPLAY_WIDTH = 100
+ private const val INITIAL_DISPLAY_HEIGHT = 200
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractorTest.kt
new file mode 100644
index 0000000..6a801e0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractorTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2023 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.unfold.domain.interactor
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.unfold.TestUnfoldTransitionProvider
+import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+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.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+open class UnfoldTransitionInteractorTest : SysuiTestCase() {
+
+ private val testScope = TestScope()
+
+ private val unfoldTransitionProgressProvider = TestUnfoldTransitionProvider()
+ private val unfoldTransitionRepository =
+ UnfoldTransitionRepositoryImpl(Optional.of(unfoldTransitionProgressProvider))
+
+ private lateinit var underTest: UnfoldTransitionInteractor
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ underTest = UnfoldTransitionInteractorImpl(unfoldTransitionRepository)
+ }
+
+ @Test
+ fun waitForTransitionFinish_noEvents_doesNotComplete() =
+ testScope.runTest {
+ val deferred = async { underTest.waitForTransitionFinish() }
+
+ runCurrent()
+
+ assertThat(deferred.isCompleted).isFalse()
+ deferred.cancel()
+ }
+
+ @Test
+ fun waitForTransitionFinish_finishEvent_completes() =
+ testScope.runTest {
+ val deferred = async { underTest.waitForTransitionFinish() }
+
+ runCurrent()
+ unfoldTransitionProgressProvider.onTransitionFinished()
+ runCurrent()
+
+ assertThat(deferred.isCompleted).isTrue()
+ deferred.cancel()
+ }
+
+ @Test
+ fun waitForTransitionFinish_otherEvent_doesNotComplete() =
+ testScope.runTest {
+ val deferred = async { underTest.waitForTransitionFinish() }
+
+ runCurrent()
+ unfoldTransitionProgressProvider.onTransitionStarted()
+ runCurrent()
+
+ assertThat(deferred.isCompleted).isFalse()
+ deferred.cancel()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/animation/FakeAnimationStatusRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/util/animation/FakeAnimationStatusRepository.kt
new file mode 100644
index 0000000..e72235c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/animation/FakeAnimationStatusRepository.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 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.animation
+
+import com.android.systemui.util.animation.data.repository.AnimationStatusRepository
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+
+class FakeAnimationStatusRepository : AnimationStatusRepository {
+
+ // Replay 1 element as real repository always emits current status as a first element
+ private val animationsEnabled: MutableSharedFlow<Boolean> = MutableSharedFlow(replay = 1)
+
+ override fun areAnimationsEnabled(): Flow<Boolean> = animationsEnabled
+
+ fun onAnimationStatusChanged(enabled: Boolean) {
+ animationsEnabled.tryEmit(enabled)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
index d0fa27e..6b38d6e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
@@ -16,6 +16,7 @@
package com.android.systemui.common.ui.data.repository
+import android.content.res.Configuration
import com.android.systemui.dagger.SysUISingleton
import dagger.Binds
import dagger.Module
@@ -36,6 +37,10 @@
MutableSharedFlow<Unit>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
override val onConfigurationChange: Flow<Unit> = _onConfigurationChange.asSharedFlow()
+ private val _configurationChangeValues = MutableSharedFlow<Configuration>()
+ override val configurationValues: Flow<Configuration> =
+ _configurationChangeValues.asSharedFlow()
+
private val _scaleForResolution = MutableStateFlow(1f)
override val scaleForResolution: Flow<Float> = _scaleForResolution.asStateFlow()
@@ -49,6 +54,11 @@
_onConfigurationChange.tryEmit(Unit)
}
+ fun onConfigurationChange(configChange: Configuration) {
+ _configurationChangeValues.tryEmit(configChange)
+ onAnyConfigurationChange()
+ }
+
fun setScaleForResolution(scale: Float) {
_scaleForResolution.value = scale
}