diff options
-rw-r--r-- | quickstep/src/com/android/launcher3/statehandlers/DepthController.java | 90 | ||||
-rw-r--r-- | quickstep/tests/src/com/android/launcher3/statehandlers/DepthControllerTest.kt | 105 |
2 files changed, 162 insertions, 33 deletions
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java index 360210b010..d9808308e8 100644 --- a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java +++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java @@ -30,6 +30,8 @@ import android.view.View; import android.view.ViewRootImpl; import android.view.ViewTreeObserver; +import androidx.annotation.VisibleForTesting; + import com.android.launcher3.BaseActivity; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; @@ -46,8 +48,8 @@ import java.util.function.Consumer; */ public class DepthController extends BaseDepthController implements StateHandler<LauncherState>, BaseActivity.MultiWindowModeChangedListener { - - private final ViewTreeObserver.OnDrawListener mOnDrawListener = this::onLauncherDraw; + @VisibleForTesting + final ViewTreeObserver.OnDrawListener mOnDrawListener = this::onLauncherDraw; private final Consumer<Boolean> mCrossWindowBlurListener = this::setCrossWindowBlursEnabled; @@ -58,6 +60,10 @@ public class DepthController extends BaseDepthController implements StateHandler private View.OnAttachStateChangeListener mOnAttachListener; + // Ensure {@link mOnDrawListener} is added only once to avoid spamming DragLayer's mRunQueue + // via {@link View#post(Runnable)} + private boolean mIsOnDrawListenerAdded = false; + public DepthController(Launcher l) { super(l); } @@ -66,33 +72,37 @@ public class DepthController extends BaseDepthController implements StateHandler View view = mLauncher.getDragLayer(); ViewRootImpl viewRootImpl = view.getViewRootImpl(); setBaseSurface(viewRootImpl != null ? viewRootImpl.getSurfaceControl() : null); - view.post(() -> view.getViewTreeObserver().removeOnDrawListener(mOnDrawListener)); + view.post(this::removeOnDrawListener); } private void ensureDependencies() { - if (mLauncher.getRootView() != null && mOnAttachListener == null) { - View rootView = mLauncher.getRootView(); - mOnAttachListener = new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View view) { - UI_HELPER_EXECUTOR.execute(() -> - CrossWindowBlurListeners.getInstance().addListener( - mLauncher.getMainExecutor(), mCrossWindowBlurListener)); - mLauncher.getScrimView().addOpaquenessListener(mOpaquenessListener); - - // To handle the case where window token is invalid during last setDepth call. - applyDepthAndBlur(); - } - - @Override - public void onViewDetachedFromWindow(View view) { - removeSecondaryListeners(); - } - }; - rootView.addOnAttachStateChangeListener(mOnAttachListener); - if (rootView.isAttachedToWindow()) { - mOnAttachListener.onViewAttachedToWindow(rootView); + View rootView = mLauncher.getRootView(); + if (rootView == null) { + return; + } + if (mOnAttachListener != null) { + return; + } + mOnAttachListener = new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View view) { + UI_HELPER_EXECUTOR.execute(() -> + CrossWindowBlurListeners.getInstance().addListener( + mLauncher.getMainExecutor(), mCrossWindowBlurListener)); + mLauncher.getScrimView().addOpaquenessListener(mOpaquenessListener); + + // To handle the case where window token is invalid during last setDepth call. + applyDepthAndBlur(); + } + + @Override + public void onViewDetachedFromWindow(View view) { + removeSecondaryListeners(); } + }; + rootView.addOnAttachStateChangeListener(mOnAttachListener); + if (rootView.isAttachedToWindow()) { + mOnAttachListener.onViewAttachedToWindow(rootView); } } @@ -109,11 +119,9 @@ public class DepthController extends BaseDepthController implements StateHandler } private void removeSecondaryListeners() { - if (mCrossWindowBlurListener != null) { - UI_HELPER_EXECUTOR.execute(() -> - CrossWindowBlurListeners.getInstance() - .removeListener(mCrossWindowBlurListener)); - } + UI_HELPER_EXECUTOR.execute(() -> + CrossWindowBlurListeners.getInstance() + .removeListener(mCrossWindowBlurListener)); if (mOpaquenessListener != null) { mLauncher.getScrimView().removeOpaquenessListener(mOpaquenessListener); } @@ -124,9 +132,9 @@ public class DepthController extends BaseDepthController implements StateHandler */ public void setActivityStarted(boolean isStarted) { if (isStarted) { - mLauncher.getDragLayer().getViewTreeObserver().addOnDrawListener(mOnDrawListener); + addOnDrawListener(); } else { - mLauncher.getDragLayer().getViewTreeObserver().removeOnDrawListener(mOnDrawListener); + removeOnDrawListener(); setBaseSurface(null); } } @@ -139,7 +147,7 @@ public class DepthController extends BaseDepthController implements StateHandler stateDepth.setValue(toState.getDepth(mLauncher)); if (toState == LauncherState.BACKGROUND_APP) { - mLauncher.getDragLayer().getViewTreeObserver().addOnDrawListener(mOnDrawListener); + addOnDrawListener(); } } @@ -165,7 +173,23 @@ public class DepthController extends BaseDepthController implements StateHandler @Override protected void onInvalidSurface() { // Lets wait for surface to become valid again + addOnDrawListener(); + } + + private void addOnDrawListener() { + if (mIsOnDrawListenerAdded) { + return; + } mLauncher.getDragLayer().getViewTreeObserver().addOnDrawListener(mOnDrawListener); + mIsOnDrawListenerAdded = true; + } + + private void removeOnDrawListener() { + if (!mIsOnDrawListenerAdded) { + return; + } + mLauncher.getDragLayer().getViewTreeObserver().removeOnDrawListener(mOnDrawListener); + mIsOnDrawListenerAdded = false; } @Override diff --git a/quickstep/tests/src/com/android/launcher3/statehandlers/DepthControllerTest.kt b/quickstep/tests/src/com/android/launcher3/statehandlers/DepthControllerTest.kt new file mode 100644 index 0000000000..17cca0b7d9 --- /dev/null +++ b/quickstep/tests/src/com/android/launcher3/statehandlers/DepthControllerTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2025 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.launcher3.statehandlers + +import android.content.res.Resources +import android.view.ViewTreeObserver +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.Launcher +import com.android.launcher3.R +import com.android.launcher3.dragndrop.DragLayer +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.reset +import org.mockito.Mockito.same +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DepthControllerTest { + + private lateinit var underTest: DepthController + @Mock private lateinit var launcher: Launcher + @Mock private lateinit var resource: Resources + @Mock private lateinit var dragLayer: DragLayer + @Mock private lateinit var viewTreeObserver: ViewTreeObserver + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + `when`(launcher.resources).thenReturn(resource) + `when`(resource.getInteger(R.integer.max_depth_blur_radius)).thenReturn(30) + `when`(launcher.dragLayer).thenReturn(dragLayer) + `when`(dragLayer.viewTreeObserver).thenReturn(viewTreeObserver) + + underTest = DepthController(launcher) + } + + @Test + fun setActivityStarted_add_onDrawListener() { + underTest.setActivityStarted(true) + + verify(viewTreeObserver).addOnDrawListener(same(underTest.mOnDrawListener)) + } + + @Test + fun setActivityStopped_not_remove_onDrawListener() { + underTest.setActivityStarted(false) + + // Because underTest.mOnDrawListener is never added + verifyNoMoreInteractions(viewTreeObserver) + } + + @Test + fun setActivityStared_then_stopped_remove_onDrawListener() { + underTest.setActivityStarted(true) + reset(viewTreeObserver) + + underTest.setActivityStarted(false) + + verify(viewTreeObserver).removeOnDrawListener(same(underTest.mOnDrawListener)) + } + + @Test + fun setActivityStared_then_stopped_multiple_times_remove_onDrawListener_once() { + underTest.setActivityStarted(true) + reset(viewTreeObserver) + + underTest.setActivityStarted(false) + underTest.setActivityStarted(false) + underTest.setActivityStarted(false) + + // Should just remove mOnDrawListener once + verify(viewTreeObserver).removeOnDrawListener(same(underTest.mOnDrawListener)) + } + + @Test + fun test_onInvalidSurface_multiple_times_add_onDrawListener_once() { + underTest.onInvalidSurface() + underTest.onInvalidSurface() + underTest.onInvalidSurface() + + // We should only call addOnDrawListener 1 time + verify(viewTreeObserver).addOnDrawListener(same(underTest.mOnDrawListener)) + } +} |