diff options
27 files changed, 1298 insertions, 51 deletions
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 8f350a7ecb3e..04c96ab5927f 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -38,6 +38,13 @@ flag { } 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 000000000000..5e6caf0d0317 --- /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 000000000000..89053d1d05fa --- /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 b0e69317e0ee..000000000000 --- 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 7fa762a6614f..2052c70e740d 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.content.res.Configuration 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 @@ interface ConfigurationRepository { val onConfigurationChange: Flow<Unit> val scaleForResolution: Flow<Float> + val configurationValues: Flow<Configuration> fun getResolutionScale(): Float @@ -68,7 +69,7 @@ constructor( 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 @@ constructor( } override val onConfigurationChange: Flow<Unit> = - ConflatedCallbackFlow.conflatedCallbackFlow { + conflatedCallbackFlow { val callback = object : ConfigurationController.ConfigurationListener { override fun onConfigChanged(newConfig: Configuration) { @@ -103,6 +104,20 @@ constructor( 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 b34b4599cf70..9fc86adce091 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.domain.interactor.BouncerInteractorModule; 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 @@ import javax.inject.Named; 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 823caa0805bd..285cb5a93b33 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 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump && !mQsController.getFullyExpanded()) { alpha *= mClockPositionResult.clockAlpha; } - mNotificationStackScrollLayoutController.setAlpha(alpha); + mNotificationStackScrollLayoutController.setMaxAlphaForExpansion(alpha); } private float getFadeoutAlpha() { @@ -4697,7 +4697,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump 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 a0ad560e817e..2f8a3754379e 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 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable 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 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable 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 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @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 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable 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 44140b926011..99b3a005ab0c 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 @@ package com.android.systemui.statusbar.notification.stack; 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.NotificationStac 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.provider.Settings; 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.MetricsLogger; 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.tuner.TunerService; 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 @@ import javax.inject.Named; * 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 @@ public class NotificationStackScrollLayoutController { private final GroupExpansionManager mGroupExpansionManager; private final SeenNotificationsInteractor mSeenNotificationsInteractor; private final KeyguardTransitionRepository mKeyguardTransitionRepo; - private NotificationStackScrollLayout mView; private NotificationSwipeHelper mSwipeHelper; @Nullable @@ -240,6 +247,22 @@ public class NotificationStackScrollLayoutController { } }; + 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 @@ public class NotificationStackScrollLayoutController { }; private NotifStats mNotifStats = NotifStats.getEmpty(); + private float mMaxAlphaForExpansion = 1.0f; + private float mMaxAlphaForUnhide = 1.0f; private final NotificationListViewBinder mViewBinder; @@ -713,6 +738,7 @@ public class NotificationStackScrollLayoutController { mDismissibilityProvider = dismissibilityProvider; mActivityStarter = activityStarter; mView.passSplitShadeStateController(splitShadeStateController); + mDumpManager.registerDumpable(this); updateResources(); setUpView(); } @@ -818,7 +844,7 @@ public class NotificationStackScrollLayoutController { 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 @@ public class NotificationStackScrollLayoutController { mView.requestLayout(); } + public void addOneShotPreDrawListener(Runnable runnable) { + OneShotPreDrawListener.add(mView, runnable); + } + public Display getDisplay() { return mView.getDisplay(); } @@ -1157,12 +1187,49 @@ public class NotificationStackScrollLayoutController { 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 @@ public class NotificationStackScrollLayoutController { } } + @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 000000000000..4de3a7fd9ff1 --- /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 000000000000..98c173402109 --- /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 000000000000..274bf94566cc --- /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 d55c0ded502d..6cf56102d65f 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.footer.ui.viewbinder.FooterVi 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 @@ constructor( 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 000000000000..e1d14d1f5e0e --- /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 f01245f264e7..4f7668060c69 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 @@ class NotificationListViewModel @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 71314f1f1775..7b628f8d676f 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.Application 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 @@ class UnfoldTransitionModule { return resultingProvider?.get()?.orElse(null)?.let { unfoldProgressProvider -> UnfoldProgressProvider(unfoldProgressProvider, foldProvider) - } - ?: ShellUnfoldProgressProvider.NO_PROVIDER + } ?: ShellUnfoldProgressProvider.NO_PROVIDER } @Provides @@ -162,6 +165,10 @@ class UnfoldTransitionModule { @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 000000000000..0d3682c9a24b --- /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 000000000000..a2e77afedea6 --- /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 000000000000..adae782eeb98 --- /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 981bf01164e3..9c8a481fcb76 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 @@ package com.android.systemui.util.dagger; 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 @@ public interface UtilModule { /** */ @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 000000000000..bcbc89c3ccd7 --- /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 000000000000..bfa36412ceb5 --- /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 1a8d4f9bf599..d5737643e0d4 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 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo // 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 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo // 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 000000000000..46e8453a6d1d --- /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 000000000000..6a801e01a4a5 --- /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 000000000000..e72235ca508f --- /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 d0fa27e3b79a..6b38d6ea315a 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 @@ class FakeConfigurationRepository @Inject constructor() : ConfigurationRepositor 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 @@ class FakeConfigurationRepository @Inject constructor() : ConfigurationRepositor _onConfigurationChange.tryEmit(Unit) } + fun onConfigurationChange(configChange: Configuration) { + _configurationChangeValues.tryEmit(configChange) + onAnyConfigurationChange() + } + fun setScaleForResolution(scale: Float) { _scaleForResolution.value = scale } |