[Status bar] Delay showing the status bar during dream transition.

Bug: 273314977
Test: manual: trigger lockscreen to dream transition -> verify that the
status bar icons in the top left don't flicker visible for a few frames
(see video attached to bug)
Test: atest CollapsedStatusBarFragmentTest
CollapsedStatusBarViewModelTest

Change-Id: I3474a5c7f6f485e8d26ffc92eac31a9b08901d51
This commit is contained in:
Caitlin Shkuratov
2023-05-08 15:36:10 +00:00
parent 541ef6fb00
commit b11171eef7
9 changed files with 336 additions and 19 deletions

View File

@@ -53,6 +53,7 @@ import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment;
import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragmentLogger;
import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
import com.android.systemui.statusbar.pipeline.shared.ui.binder.CollapsedStatusBarViewBinder;
import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStatusBarViewModel;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.statusbar.window.StatusBarWindowStateController;
@@ -158,6 +159,7 @@ public abstract class StatusBarViewModule {
StatusBarIconController statusBarIconController,
StatusBarIconController.DarkIconManager.Factory darkIconManagerFactory,
CollapsedStatusBarViewModel collapsedStatusBarViewModel,
CollapsedStatusBarViewBinder collapsedStatusBarViewBinder,
StatusBarHideIconsForBouncerManager statusBarHideIconsForBouncerManager,
KeyguardStateController keyguardStateController,
ShadeViewController shadeViewController,
@@ -182,6 +184,7 @@ public abstract class StatusBarViewModule {
statusBarIconController,
darkIconManagerFactory,
collapsedStatusBarViewModel,
collapsedStatusBarViewBinder,
statusBarHideIconsForBouncerManager,
keyguardStateController,
shadeViewController,

View File

@@ -14,8 +14,6 @@
package com.android.systemui.statusbar.phone.fragment;
import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.IDLE;
import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.SHOWING_PERSISTENT_DOT;
@@ -69,6 +67,7 @@ import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentCom
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallListener;
import com.android.systemui.statusbar.pipeline.shared.ui.binder.CollapsedStatusBarViewBinder;
import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarVisibilityChangeListener;
import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStatusBarViewModel;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.statusbar.window.StatusBarWindowStateController;
@@ -131,6 +130,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
private final StatusBarIconController mStatusBarIconController;
private final CarrierConfigTracker mCarrierConfigTracker;
private final CollapsedStatusBarViewModel mCollapsedStatusBarViewModel;
private final CollapsedStatusBarViewBinder mCollapsedStatusBarViewBinder;
private final StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
private final StatusBarIconController.DarkIconManager.Factory mDarkIconManagerFactory;
private final SecureSettings mSecureSettings;
@@ -182,12 +182,22 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
*/
private boolean mWaitingForWindowStateChangeAfterCameraLaunch = false;
/**
* True when a transition from lockscreen to dream has started, but haven't yet received a
* status bar window state change afterward.
*
* Similar to [mWaitingForWindowStateChangeAfterCameraLaunch].
*/
private boolean mTransitionFromLockscreenToDreamStarted = false;
/**
* Listener that updates {@link #mWaitingForWindowStateChangeAfterCameraLaunch} when it receives
* a new status bar window state.
*/
private final StatusBarWindowStateListener mStatusBarWindowStateListener = state ->
mWaitingForWindowStateChangeAfterCameraLaunch = false;
private final StatusBarWindowStateListener mStatusBarWindowStateListener = state -> {
mWaitingForWindowStateChangeAfterCameraLaunch = false;
mTransitionFromLockscreenToDreamStarted = false;
};
@SuppressLint("ValidFragment")
public CollapsedStatusBarFragment(
@@ -201,6 +211,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
StatusBarIconController statusBarIconController,
StatusBarIconController.DarkIconManager.Factory darkIconManagerFactory,
CollapsedStatusBarViewModel collapsedStatusBarViewModel,
CollapsedStatusBarViewBinder collapsedStatusBarViewBinder,
StatusBarHideIconsForBouncerManager statusBarHideIconsForBouncerManager,
KeyguardStateController keyguardStateController,
ShadeViewController shadeViewController,
@@ -224,6 +235,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
mFeatureFlags = featureFlags;
mStatusBarIconController = statusBarIconController;
mCollapsedStatusBarViewModel = collapsedStatusBarViewModel;
mCollapsedStatusBarViewBinder = collapsedStatusBarViewBinder;
mStatusBarHideIconsForBouncerManager = statusBarHideIconsForBouncerManager;
mDarkIconManagerFactory = darkIconManagerFactory;
mKeyguardStateController = keyguardStateController;
@@ -296,8 +308,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
mCarrierConfigTracker.addCallback(mCarrierConfigCallback);
mCarrierConfigTracker.addDefaultDataSubscriptionChangedListener(mDefaultDataListener);
CollapsedStatusBarViewBinder.bind(
mStatusBar, mCollapsedStatusBarViewModel, this::updateStatusBarVisibilities);
mCollapsedStatusBarViewBinder.bind(
mStatusBar, mCollapsedStatusBarViewModel, mStatusBarVisibilityChangeListener);
}
@Override
@@ -411,6 +423,19 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
return mStatusBarFragmentComponent;
}
private StatusBarVisibilityChangeListener mStatusBarVisibilityChangeListener =
new StatusBarVisibilityChangeListener() {
@Override
public void onStatusBarVisibilityMaybeChanged() {
updateStatusBarVisibilities(/* animate= */ true);
}
@Override
public void onTransitionFromLockscreenToDreamStarted() {
mTransitionFromLockscreenToDreamStarted = true;
}
};
@Override
public void disable(int displayId, int state1, int state2, boolean animate) {
if (displayId != getContext().getDisplayId()) {
@@ -423,10 +448,6 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
updateStatusBarVisibilities(animate);
}
private void updateStatusBarVisibilities() {
updateStatusBarVisibilities(/* animate= */ true);
}
private void updateStatusBarVisibilities(boolean animate) {
StatusBarVisibilityModel previousModel = mLastModifiedVisibility;
StatusBarVisibilityModel newModel = calculateInternalModel(mLastSystemVisibility);
@@ -546,6 +567,18 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
return true;
}
// Similar to [hideIconsForSecureCamera]: When dream is launched over lockscreen, the icons
// are momentarily visible because the dream animation has finished, but SysUI has not been
// informed that the dream is full-screen. For extra safety, we double-check that we're
// still dreaming.
final boolean hideIconsForDream =
mTransitionFromLockscreenToDreamStarted
&& mKeyguardUpdateMonitor.isDreaming()
&& mKeyguardStateController.isOccluded();
if (hideIconsForDream) {
return true;
}
// While the status bar is transitioning from lockscreen to an occluded, we don't yet know
// if the occluding activity is fullscreen or not. If it *is* fullscreen, we don't want to
// briefly show the status bar just to immediately hide it again. So, we wait for the

View File

@@ -44,6 +44,8 @@ import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerPr
import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxyImpl
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
import com.android.systemui.statusbar.pipeline.shared.ui.binder.CollapsedStatusBarViewBinder
import com.android.systemui.statusbar.pipeline.shared.ui.binder.CollapsedStatusBarViewBinderImpl
import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositorySwitcher
@@ -107,6 +109,11 @@ abstract class StatusBarPipelineModule {
impl: CollapsedStatusBarViewModelImpl
): CollapsedStatusBarViewModel
@Binds
abstract fun collapsedStatusBarViewBinder(
impl: CollapsedStatusBarViewBinderImpl
): CollapsedStatusBarViewBinder
companion object {
@Provides
@SysUISingleton

View File

@@ -19,34 +19,61 @@ package com.android.systemui.statusbar.pipeline.shared.ui.binder
import android.view.View
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStatusBarViewModel
import javax.inject.Inject
import kotlinx.coroutines.launch
object CollapsedStatusBarViewBinder {
/**
* Interface to assist with binding the [CollapsedStatusBarFragment] to
* [CollapsedStatusBarViewModel]. Used only to enable easy testing of [CollapsedStatusBarFragment].
*/
interface CollapsedStatusBarViewBinder {
/**
* Binds the view to the view-model. [listener] will be notified whenever an event that may
* change the status bar visibility occurs.
*/
@JvmStatic
fun bind(
view: View,
viewModel: CollapsedStatusBarViewModel,
listener: StatusBarVisibilityChangeListener,
)
}
@SysUISingleton
class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBarViewBinder {
override fun bind(
view: View,
viewModel: CollapsedStatusBarViewModel,
listener: StatusBarVisibilityChangeListener,
) {
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.isTransitioningFromLockscreenToOccluded.collect {
listener.onStatusBarVisibilityMaybeChanged()
launch {
viewModel.isTransitioningFromLockscreenToOccluded.collect {
listener.onStatusBarVisibilityMaybeChanged()
}
}
launch {
viewModel.transitionFromLockscreenToDreamStartedEvent.collect {
listener.onTransitionFromLockscreenToDreamStarted()
}
}
}
}
}
}
/**
* Listener to be notified when the status bar visibility might have changed due to the device
* moving to a different state.
*/
fun interface StatusBarVisibilityChangeListener {
/** Listener for various events that may affect the status bar's visibility. */
interface StatusBarVisibilityChangeListener {
/**
* Called when the status bar visibility might have changed due to the device moving to a
* different state.
*/
fun onStatusBarVisibilityMaybeChanged()
/** Called when a transition from lockscreen to dream has started. */
fun onTransitionFromLockscreenToDreamStarted()
}

View File

@@ -22,8 +22,10 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInterac
import com.android.systemui.keyguard.shared.model.TransitionState
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@@ -43,6 +45,9 @@ interface CollapsedStatusBarViewModel {
* otherwise.
*/
val isTransitioningFromLockscreenToOccluded: StateFlow<Boolean>
/** Emits whenever a transition from lockscreen to dream has started. */
val transitionFromLockscreenToDreamStartedEvent: Flow<Unit>
}
@SysUISingleton
@@ -59,4 +64,9 @@ constructor(
it.transitionState == TransitionState.RUNNING
}
.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), initialValue = false)
override val transitionFromLockscreenToDreamStartedEvent: Flow<Unit> =
keyguardTransitionInteractor.lockscreenToDreamingTransition
.filter { it.transitionState == TransitionState.STARTED }
.map {}
}

View File

@@ -74,6 +74,7 @@ import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.phone.StatusBarLocationPublisher;
import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeCollapsedStatusBarViewBinder;
import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeCollapsedStatusBarViewModel;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.statusbar.window.StatusBarWindowStateController;
@@ -129,6 +130,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest {
@Mock
private StatusBarIconController.DarkIconManager mIconManager;
private FakeCollapsedStatusBarViewModel mCollapsedStatusBarViewModel;
private FakeCollapsedStatusBarViewBinder mCollapsedStatusBarViewBinder;
@Mock
private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
@Mock
@@ -612,6 +614,55 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest {
assertEquals(View.VISIBLE, getClockView().getVisibility());
}
@Test
public void testStatusBarIcons_hiddenThroughoutLockscreenToDreamTransition() {
final CollapsedStatusBarFragment fragment = resumeAndGetFragment();
// WHEN a transition to dream has started
mCollapsedStatusBarViewBinder.getListener().onTransitionFromLockscreenToDreamStarted();
when(mKeyguardUpdateMonitor.isDreaming()).thenReturn(true);
when(mKeyguardStateController.isOccluded()).thenReturn(true);
fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
// THEN status icons should be invisible or gone, but certainly not VISIBLE.
assertNotEquals(View.VISIBLE, getEndSideContentView().getVisibility());
assertNotEquals(View.VISIBLE, getClockView().getVisibility());
// WHEN the transition has finished and dream is displaying
mockLockscreenToDreamTransitionFinished();
// (This approximates "dream is displaying")
when(mStatusBarHideIconsForBouncerManager.getShouldHideStatusBarIconsForBouncer())
.thenReturn(true);
fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
// THEN the views still aren't visible because dream is hiding them
assertNotEquals(View.VISIBLE, getEndSideContentView().getVisibility());
assertNotEquals(View.VISIBLE, getClockView().getVisibility());
// WHEN dream has ended
when(mStatusBarHideIconsForBouncerManager.getShouldHideStatusBarIconsForBouncer())
.thenReturn(false);
fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
// THEN the views can be visible again
assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
assertEquals(View.VISIBLE, getClockView().getVisibility());
}
@Test
public void testStatusBarIcons_lockscreenToDreamTransitionButNotDreaming_iconsVisible() {
final CollapsedStatusBarFragment fragment = resumeAndGetFragment();
// WHEN a transition to dream has started but we're *not* dreaming
mCollapsedStatusBarViewBinder.getListener().onTransitionFromLockscreenToDreamStarted();
when(mKeyguardUpdateMonitor.isDreaming()).thenReturn(false);
fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
// THEN the views are still visible
assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
assertEquals(View.VISIBLE, getClockView().getVisibility());
}
@Override
protected Fragment instantiate(Context context, String className, Bundle arguments) {
MockitoAnnotations.initMocks(this);
@@ -631,6 +682,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest {
mShadeExpansionStateManager = new ShadeExpansionStateManager();
mCollapsedStatusBarViewModel = new FakeCollapsedStatusBarViewModel();
mCollapsedStatusBarViewBinder = new FakeCollapsedStatusBarViewBinder();
setUpNotificationIconAreaController();
return new CollapsedStatusBarFragment(
@@ -644,6 +696,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest {
mStatusBarIconController,
mIconManagerFactory,
mCollapsedStatusBarViewModel,
mCollapsedStatusBarViewBinder,
mStatusBarHideIconsForBouncerManager,
mKeyguardStateController,
mShadeViewController,
@@ -718,6 +771,12 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest {
}
}
private void mockLockscreenToDreamTransitionFinished() {
for (StatusBarWindowStateListener listener : mStatusBarWindowStateListeners) {
listener.onStatusBarWindowStateChanged(StatusBarManager.WINDOW_STATE_HIDDEN);
}
}
private CollapsedStatusBarFragment resumeAndGetFragment() {
mFragments.dispatchResume();
processAllMessages();

View File

@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectValues
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -176,4 +177,139 @@ class CollapsedStatusBarViewModelImplTest : SysuiTestCase() {
job.cancel()
}
@Test
fun transitionFromLockscreenToDreamStartedEvent_started_emitted() =
testScope.runTest {
val emissions by collectValues(underTest.transitionFromLockscreenToDreamStartedEvent)
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
KeyguardState.LOCKSCREEN,
KeyguardState.DREAMING,
value = 0f,
TransitionState.STARTED,
)
)
assertThat(emissions.size).isEqualTo(1)
}
@Test
fun transitionFromLockscreenToDreamStartedEvent_startedMultiple_emittedMultiple() =
testScope.runTest {
val emissions by collectValues(underTest.transitionFromLockscreenToDreamStartedEvent)
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
KeyguardState.LOCKSCREEN,
KeyguardState.DREAMING,
value = 0f,
TransitionState.STARTED,
)
)
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
KeyguardState.LOCKSCREEN,
KeyguardState.DREAMING,
value = 0f,
TransitionState.STARTED,
)
)
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
KeyguardState.LOCKSCREEN,
KeyguardState.DREAMING,
value = 0f,
TransitionState.STARTED,
)
)
assertThat(emissions.size).isEqualTo(3)
}
@Test
fun transitionFromLockscreenToDreamStartedEvent_startedThenRunning_emittedOnlyOne() =
testScope.runTest {
val emissions by collectValues(underTest.transitionFromLockscreenToDreamStartedEvent)
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
KeyguardState.LOCKSCREEN,
KeyguardState.DREAMING,
value = 0f,
TransitionState.STARTED,
)
)
assertThat(emissions.size).isEqualTo(1)
// WHEN the transition progresses through its animation by going through the RUNNING
// step with increasing fractions
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
KeyguardState.LOCKSCREEN,
KeyguardState.DREAMING,
value = .1f,
TransitionState.RUNNING,
)
)
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
KeyguardState.LOCKSCREEN,
KeyguardState.DREAMING,
value = .2f,
TransitionState.RUNNING,
)
)
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
KeyguardState.LOCKSCREEN,
KeyguardState.DREAMING,
value = .3f,
TransitionState.RUNNING,
)
)
// THEN the flow does not emit since the flow should only emit when the transition
// starts
assertThat(emissions.size).isEqualTo(1)
}
@Test
fun transitionFromLockscreenToDreamStartedEvent_irrelevantTransition_notEmitted() =
testScope.runTest {
val emissions by collectValues(underTest.transitionFromLockscreenToDreamStartedEvent)
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
KeyguardState.LOCKSCREEN,
KeyguardState.OCCLUDED,
value = 0f,
TransitionState.STARTED,
)
)
assertThat(emissions).isEmpty()
}
@Test
fun transitionFromLockscreenToDreamStartedEvent_irrelevantTransitionState_notEmitted() =
testScope.runTest {
val emissions by collectValues(underTest.transitionFromLockscreenToDreamStartedEvent)
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
KeyguardState.LOCKSCREEN,
KeyguardState.DREAMING,
value = 1.0f,
TransitionState.FINISHED,
)
)
assertThat(emissions).isEmpty()
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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.pipeline.shared.ui.viewmodel
import android.view.View
import com.android.systemui.statusbar.pipeline.shared.ui.binder.CollapsedStatusBarViewBinder
import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarVisibilityChangeListener
/**
* A fake view binder that can be used from Java tests.
*
* Since Java tests can't run tests within test scopes, we need to bypass the flows from
* [CollapsedStatusBarViewModel] and just trigger the listener directly.
*/
class FakeCollapsedStatusBarViewBinder : CollapsedStatusBarViewBinder {
var listener: StatusBarVisibilityChangeListener? = null
override fun bind(
view: View,
viewModel: CollapsedStatusBarViewModel,
listener: StatusBarVisibilityChangeListener,
) {
this.listener = listener
}
}

View File

@@ -16,8 +16,11 @@
package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
class FakeCollapsedStatusBarViewModel : CollapsedStatusBarViewModel {
override val isTransitioningFromLockscreenToOccluded = MutableStateFlow(false)
override val transitionFromLockscreenToDreamStartedEvent = MutableSharedFlow<Unit>()
}