diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/DarkIconDispatcher.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/DarkIconDispatcher.java index b33c5449c1eb6..a5e5aaa499f13 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/DarkIconDispatcher.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/DarkIconDispatcher.java @@ -26,6 +26,7 @@ import com.android.systemui.plugins.annotations.DependsOn; import com.android.systemui.plugins.annotations.ProvidesInterface; import java.util.ArrayList; +import java.util.Collection; /** * Dispatches events to {@link DarkReceiver}s about changes in darkness, tint area and dark @@ -78,7 +79,7 @@ public interface DarkIconDispatcher { * @return the tint to apply to view depending on the desired tint color and * the screen tintArea in which to apply that tint */ - static int getTint(ArrayList tintAreas, View view, int color) { + static int getTint(Collection tintAreas, View view, int color) { if (isInAreas(tintAreas, view)) { return color; } else { @@ -90,7 +91,7 @@ public interface DarkIconDispatcher { * @return true if more than half of the view area are in any of the given * areas, false otherwise */ - static boolean isInAreas(ArrayList areas, View view) { + static boolean isInAreas(Collection areas, View view) { if (areas.isEmpty()) { return true; } diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index ab754985e11db..9f4fc396ae555 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -26,6 +26,8 @@ #B3FFFFFF #FFFFFFFF #9E9E9E + #38FFFFFF + #38000000 #F5F5F5 diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 5a15dcec52233..9588498dddb38 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -475,6 +475,8 @@ 16dp + 16dp + -1px diff --git a/packages/SystemUI/src/com/android/systemui/dagger/PluginModule.java b/packages/SystemUI/src/com/android/systemui/dagger/PluginModule.java index a3e26b881a3bf..d727a70de377d 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/PluginModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/PluginModule.java @@ -28,6 +28,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarStateControllerImpl; import com.android.systemui.statusbar.phone.ActivityStarterImpl; import com.android.systemui.statusbar.phone.DarkIconDispatcherImpl; +import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher; import com.android.systemui.volume.VolumeDialogControllerImpl; import dagger.Binds; @@ -49,6 +50,10 @@ public abstract class PluginModule { @Binds abstract DarkIconDispatcher provideDarkIconDispatcher(DarkIconDispatcherImpl controllerImpl); + @Binds + abstract SysuiDarkIconDispatcher provideSysuiDarkIconDispatcher( + DarkIconDispatcherImpl controllerImpl); + /** */ @Binds abstract FalsingManager provideFalsingManager(FalsingManagerProxy falsingManagerImpl); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt index 529f12e0658ef..c6cb9c4d347c4 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt @@ -61,6 +61,7 @@ import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider import com.android.systemui.statusbar.phone.StatusBarIconController import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.phone.StatusIconContainer +import com.android.systemui.statusbar.phone.StatusOverlayHoverListenerFactory import com.android.systemui.statusbar.policy.Clock import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.NextAlarmController @@ -99,6 +100,7 @@ constructor( private val qsBatteryModeController: QsBatteryModeController, private val nextAlarmController: NextAlarmController, private val activityStarter: ActivityStarter, + private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory, ) : ViewController(header), Dumpable { companion object { @@ -326,6 +328,9 @@ constructor( demoModeController.addCallback(demoModeReceiver) statusBarIconController.addIconGroup(iconManager) nextAlarmController.addCallback(nextAlarmCallback) + systemIcons.setOnHoverListener( + statusOverlayHoverListenerFactory.createListener(systemIcons) + ) } override fun onViewDetached() { @@ -336,6 +341,7 @@ constructor( demoModeController.removeCallback(demoModeReceiver) statusBarIconController.removeIconGroup(iconManager) nextAlarmController.removeCallback(nextAlarmCallback) + systemIcons.setOnHoverListener(null) } fun disable(state1: Int, state2: Int, animate: Boolean) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java index 2677c3f2a8bd0..c4495edbda4ef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java @@ -32,6 +32,11 @@ import java.util.ArrayList; import javax.inject.Inject; +import kotlinx.coroutines.flow.FlowKt; +import kotlinx.coroutines.flow.MutableStateFlow; +import kotlinx.coroutines.flow.StateFlow; +import kotlinx.coroutines.flow.StateFlowKt; + /** */ @SysUISingleton @@ -47,6 +52,9 @@ public class DarkIconDispatcherImpl implements SysuiDarkIconDispatcher, private int mDarkModeIconColorSingleTone; private int mLightModeIconColorSingleTone; + private final MutableStateFlow mDarkChangeFlow = StateFlowKt.MutableStateFlow( + DarkChange.EMPTY); + /** */ @Inject @@ -66,6 +74,11 @@ public class DarkIconDispatcherImpl implements SysuiDarkIconDispatcher, return mTransitionsController; } + @Override + public StateFlow darkChangeFlow() { + return FlowKt.asStateFlow(mDarkChangeFlow); + } + public void addDarkReceiver(DarkReceiver receiver) { mReceivers.put(receiver, receiver); receiver.onDarkChanged(mTintAreas, mDarkIntensity, mIconTint); @@ -122,6 +135,7 @@ public class DarkIconDispatcherImpl implements SysuiDarkIconDispatcher, } private void applyIconTint() { + mDarkChangeFlow.setValue(new DarkChange(mTintAreas, mDarkIntensity, mIconTint)); for (int i = 0; i < mReceivers.size(); i++) { mReceivers.valueAt(i).onDarkChanged(mTintAreas, mDarkIntensity, mIconTint); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index 720eeba0fd4e4..5c1f824bc6871 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -34,7 +34,6 @@ import android.view.DisplayCutout; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; -import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.widget.ImageView; import android.widget.LinearLayout; @@ -43,11 +42,11 @@ import android.widget.TextView; import androidx.annotation.VisibleForTesting; -import com.android.app.animation.Interpolators; import com.android.settingslib.Utils; import com.android.systemui.R; import com.android.systemui.battery.BatteryMeterView; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; +import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange; import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer; import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder; import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel; @@ -55,6 +54,11 @@ import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel; import java.io.PrintWriter; import java.util.ArrayList; +import kotlinx.coroutines.flow.FlowKt; +import kotlinx.coroutines.flow.MutableStateFlow; +import kotlinx.coroutines.flow.StateFlow; +import kotlinx.coroutines.flow.StateFlowKt; + /** * The header group on Keyguard. */ @@ -83,6 +87,8 @@ public class KeyguardStatusBarView extends RelativeLayout { private int mStatusBarPaddingEnd; private int mMinDotWidth; private View mSystemIconsContainer; + private final MutableStateFlow mDarkChange = StateFlowKt.MutableStateFlow( + DarkChange.EMPTY); private View mCutoutSpace; private ViewGroup mStatusIconArea; @@ -374,49 +380,6 @@ public class KeyguardStatusBarView extends RelativeLayout { return mKeyguardUserAvatarEnabled; } - private void animateNextLayoutChange() { - final int systemIconsCurrentX = mSystemIconsContainer.getLeft(); - final boolean userAvatarVisible = mMultiUserAvatar.getParent() == mStatusIconArea; - getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - getViewTreeObserver().removeOnPreDrawListener(this); - boolean userAvatarHiding = userAvatarVisible - && mMultiUserAvatar.getParent() != mStatusIconArea; - mSystemIconsContainer.setX(systemIconsCurrentX); - mSystemIconsContainer.animate() - .translationX(0) - .setDuration(400) - .setStartDelay(userAvatarHiding ? 300 : 0) - .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) - .start(); - if (userAvatarHiding) { - getOverlay().add(mMultiUserAvatar); - mMultiUserAvatar.animate() - .alpha(0f) - .setDuration(300) - .setStartDelay(0) - .setInterpolator(Interpolators.ALPHA_OUT) - .withEndAction(() -> { - mMultiUserAvatar.setAlpha(1f); - getOverlay().remove(mMultiUserAvatar); - }) - .start(); - - } else { - mMultiUserAvatar.setAlpha(0f); - mMultiUserAvatar.animate() - .alpha(1f) - .setDuration(300) - .setStartDelay(200) - .setInterpolator(Interpolators.ALPHA_IN); - } - return true; - } - }); - - } - @Override public void setVisibility(int visibility) { super.setVisibility(visibility); @@ -474,6 +437,7 @@ public class KeyguardStatusBarView extends RelativeLayout { iconManager.setTint(iconColor); } + mDarkChange.setValue(new DarkChange(mEmptyTintRect, intensity, iconColor)); applyDarkness(R.id.battery, mEmptyTintRect, intensity, iconColor); applyDarkness(R.id.clock, mEmptyTintRect, intensity, iconColor); } @@ -536,4 +500,8 @@ public class KeyguardStatusBarView extends RelativeLayout { super.onMeasure(widthMeasureSpec, heightMeasureSpec); Trace.endSection(); } + + public StateFlow darkChangeFlow() { + return FlowKt.asStateFlow(mDarkChange); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java index 680f19a79a052..be336e59f534d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -69,8 +69,6 @@ import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel; import com.android.systemui.util.ViewController; import com.android.systemui.util.settings.SecureSettings; -import kotlin.Unit; - import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; @@ -78,6 +76,8 @@ import java.util.concurrent.Executor; import javax.inject.Inject; +import kotlin.Unit; + /** View Controller for {@link com.android.systemui.statusbar.phone.KeyguardStatusBarView}. */ public class KeyguardStatusBarViewController extends ViewController { private static final String TAG = "KeyguardStatusBarViewController"; @@ -119,6 +119,9 @@ public class KeyguardStatusBarViewController extends ViewController mView.updateWindowInsets(windowInsets, mInsetsProvider)); mSecureSettings.registerContentObserverForUser( @@ -376,6 +385,7 @@ public class KeyguardStatusBarViewController extends ViewController(view) { + private lateinit var statusContainer: View + private val configurationListener = object : ConfigurationController.ConfigurationListener { override fun onConfigChanged(newConfig: Configuration?) { mView.updateResources() @@ -73,6 +76,9 @@ class PhoneStatusBarViewController private constructor( } override fun onViewAttached() { + statusContainer = mView.findViewById(R.id.system_icons) + statusContainer.setOnHoverListener( + statusOverlayHoverListenerFactory.createDarkAwareListener(statusContainer)) if (moveFromCenterAnimationController == null) return val statusBarLeftSide: View = mView.findViewById(R.id.status_bar_start_side_except_heads_up) @@ -104,6 +110,7 @@ class PhoneStatusBarViewController private constructor( } override fun onViewDetached() { + statusContainer.setOnHoverListener(null) progressProvider?.setReadyToHandleTransition(false) moveFromCenterAnimationController?.onViewDetached() configurationController.removeCallback(configurationListener) @@ -245,6 +252,7 @@ class PhoneStatusBarViewController private constructor( private val shadeLogger: ShadeLogger, private val viewUtil: ViewUtil, private val configurationController: ConfigurationController, + private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory, ) { fun create( view: PhoneStatusBarView @@ -268,7 +276,8 @@ class PhoneStatusBarViewController private constructor( userChipViewModel, viewUtil, featureFlags, - configurationController + configurationController, + statusOverlayHoverListenerFactory, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusOverlayHoverListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusOverlayHoverListener.kt new file mode 100644 index 0000000000000..881741ae57461 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusOverlayHoverListener.kt @@ -0,0 +1,155 @@ +/* + * 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.phone + +import android.content.res.Configuration +import android.content.res.Resources +import android.graphics.Color +import android.graphics.drawable.PaintDrawable +import android.view.MotionEvent +import android.view.View +import android.view.View.OnHoverListener +import androidx.annotation.ColorInt +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.R +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.plugins.DarkIconDispatcher +import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +class StatusOverlayHoverListenerFactory +@Inject +constructor( + @Main private val resources: Resources, + private val configurationController: ConfigurationController, + private val darkIconDispatcher: SysuiDarkIconDispatcher, +) { + + /** Creates listener always using the same light color for overlay */ + fun createListener(view: View) = + StatusOverlayHoverListener( + view, + configurationController, + resources, + flowOf(HoverTheme.LIGHT), + ) + + /** + * Creates listener using [DarkIconDispatcher] to determine light or dark color of the overlay + */ + fun createDarkAwareListener(view: View) = + createDarkAwareListener(view, darkIconDispatcher.darkChangeFlow()) + + /** + * Creates listener using provided [DarkChange] producer to determine light or dark color of the + * overlay + */ + fun createDarkAwareListener(view: View, darkFlow: StateFlow) = + StatusOverlayHoverListener( + view, + configurationController, + resources, + darkFlow.map { toHoverTheme(view, it) }, + ) + + private fun toHoverTheme(view: View, darkChange: DarkChange): HoverTheme { + val calculatedTint = DarkIconDispatcher.getTint(darkChange.areas, view, darkChange.tint) + // currently calculated tint is either white or some shade of black. + // So checking for Color.WHITE is deterministic compared to checking for Color.BLACK. + // In the future checking Color.luminance() might be more appropriate. + return if (calculatedTint == Color.WHITE) HoverTheme.LIGHT else HoverTheme.DARK + } +} + +/** + * theme of hover drawable - it's different from device theme. This theme depends on view's + * background and/or dark value returned from [DarkIconDispatcher] + */ +enum class HoverTheme { + LIGHT, + DARK +} + +/** + * [OnHoverListener] that adds [Drawable] overlay on top of the status icons when cursor/stylus + * starts hovering over them and removes overlay when status icons are no longer hovered + */ +class StatusOverlayHoverListener( + view: View, + configurationController: ConfigurationController, + private val resources: Resources, + private val themeFlow: Flow, +) : OnHoverListener { + + @ColorInt private var darkColor: Int = 0 + @ColorInt private var lightColor: Int = 0 + private var cornerRadius = 0f + + private var lastTheme = HoverTheme.LIGHT + + val backgroundColor + get() = if (lastTheme == HoverTheme.LIGHT) lightColor else darkColor + + init { + view.repeatWhenAttached { + lifecycleScope.launch { + val configurationListener = + object : ConfigurationListener { + override fun onConfigChanged(newConfig: Configuration?) { + updateResources() + } + } + repeatOnLifecycle(Lifecycle.State.CREATED) { + configurationController.addCallback(configurationListener) + } + configurationController.removeCallback(configurationListener) + } + lifecycleScope.launch { themeFlow.collect { lastTheme = it } } + } + updateResources() + } + + override fun onHover(v: View, event: MotionEvent): Boolean { + if (event.action == MotionEvent.ACTION_HOVER_ENTER) { + val drawable = + PaintDrawable(backgroundColor).apply { + setCornerRadius(cornerRadius) + setBounds(0, 0, v.width, v.height) + } + v.overlay.add(drawable) + } else if (event.action == MotionEvent.ACTION_HOVER_EXIT) { + v.overlay.clear() + } + return true + } + + private fun updateResources() { + lightColor = resources.getColor(R.color.status_bar_icons_hover_color_light) + darkColor = resources.getColor(R.color.status_bar_icons_hover_color_dark) + cornerRadius = resources.getDimension(R.dimen.status_icons_hover_state_background_radius) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SysuiDarkIconDispatcher.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SysuiDarkIconDispatcher.java index d53772127601b..f5e90348a7c4c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SysuiDarkIconDispatcher.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SysuiDarkIconDispatcher.java @@ -16,9 +16,16 @@ package com.android.systemui.statusbar.phone; +import android.graphics.Rect; + import com.android.systemui.Dumpable; import com.android.systemui.plugins.DarkIconDispatcher; +import java.util.ArrayList; +import java.util.Collection; + +import kotlinx.coroutines.flow.StateFlow; + /** * Dispatches events to {@link DarkReceiver}s about changes in darkness, tint area * and dark intensity. @@ -29,4 +36,26 @@ public interface SysuiDarkIconDispatcher extends DarkIconDispatcher, Dumpable { * @return LightBarTransitionsController */ LightBarTransitionsController getTransitionsController(); + + /** + * Flow equivalent of registering {@link DarkReceiver} using + * {@link DarkIconDispatcher#addDarkReceiver(DarkReceiver)} + */ + StateFlow darkChangeFlow(); + + /** Model for {@link #darkChangeFlow()} */ + class DarkChange { + + public static final DarkChange EMPTY = new DarkChange(new ArrayList<>(), 0, 0); + + public DarkChange(Collection areas, float darkIntensity, int tint) { + this.areas = areas; + this.darkIntensity = darkIntensity; + this.tint = tint; + } + + public final Collection areas; + public final float darkIntensity; + public final int tint; + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt index bf25f29752535..2501f85798aaa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt @@ -55,6 +55,7 @@ import com.android.systemui.shade.carrier.ShadeCarrierGroupController import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider import com.android.systemui.statusbar.phone.StatusBarIconController import com.android.systemui.statusbar.phone.StatusIconContainer +import com.android.systemui.statusbar.phone.StatusOverlayHoverListenerFactory import com.android.systemui.statusbar.policy.Clock import com.android.systemui.statusbar.policy.FakeConfigurationController import com.android.systemui.statusbar.policy.NextAlarmController @@ -123,6 +124,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() { @Mock private lateinit var qsBatteryModeController: QsBatteryModeController @Mock private lateinit var nextAlarmController: NextAlarmController @Mock private lateinit var activityStarter: ActivityStarter + @Mock private lateinit var mStatusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory @JvmField @Rule val mockitoRule = MockitoJUnit.rule() var viewVisibility = View.GONE @@ -194,6 +196,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() { qsBatteryModeController, nextAlarmController, activityStarter, + mStatusOverlayHoverListenerFactory ) whenever(view.isAttachedToWindow).thenReturn(true) shadeHeaderController.init() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java index e838a480f3cc1..d100c687d8027 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java @@ -122,6 +122,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { @Mock private KeyguardLogger mLogger; @Mock private NotificationMediaManager mNotificationMediaManager; + @Mock private StatusOverlayHoverListenerFactory mStatusOverlayHoverListenerFactory; private TestShadeViewStateProvider mShadeViewStateProvider; private KeyguardStatusBarView mKeyguardStatusBarView; @@ -171,7 +172,8 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { mCommandQueue, mFakeExecutor, mLogger, - mNotificationMediaManager + mNotificationMediaManager, + mStatusOverlayHoverListenerFactory ); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt index 7de0075c45ff1..2e92bb948c606 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt @@ -69,6 +69,8 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { @Mock private lateinit var configurationController: ConfigurationController @Mock + private lateinit var mStatusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory + @Mock private lateinit var userChipViewModel: StatusBarUserChipViewModel @Mock private lateinit var centralSurfacesImpl: CentralSurfacesImpl @@ -204,7 +206,8 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { sceneInteractor, shadeLogger, viewUtil, - configurationController + configurationController, + mStatusOverlayHoverListenerFactory ).create(view).also { it.init() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusOverlayHoverListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusOverlayHoverListenerTest.kt new file mode 100644 index 0000000000000..63508e193bb8a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusOverlayHoverListenerTest.kt @@ -0,0 +1,192 @@ +/* + * 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.phone + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.graphics.drawable.PaintDrawable +import android.os.SystemClock +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.TestableLooper.RunWithLooper +import android.testing.ViewUtils +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroupOverlay +import android.widget.LinearLayout +import androidx.annotation.ColorInt +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange +import com.android.systemui.statusbar.policy.FakeConfigurationController +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify + +@RunWith(AndroidTestingRunner::class) +@RunWithLooper(setAsMainLooper = true) +@SmallTest +class StatusOverlayHoverListenerTest : SysuiTestCase() { + + private val viewOverlay = mock() + private val overlayCaptor = argumentCaptor() + private val darkDispatcher = mock() + private val darkChange: MutableStateFlow = MutableStateFlow(DarkChange.EMPTY) + + private val factory = + StatusOverlayHoverListenerFactory( + context.resources, + FakeConfigurationController(), + darkDispatcher + ) + private val view = TestableStatusContainer(context, viewOverlay) + + private lateinit var looper: TestableLooper + + @Before + fun setUp() { + looper = TestableLooper.get(this) + whenever(darkDispatcher.darkChangeFlow()).thenReturn(darkChange) + } + + @Test + fun onHoverStarted_addsOverlay() { + view.setUpHoverListener() + + view.hoverStarted() + + assertThat(overlayDrawable).isNotNull() + } + + @Test + fun onHoverEnded_removesOverlay() { + view.setUpHoverListener() + + view.hoverStarted() // stopped callback will be called only if hover has started + view.hoverStopped() + + verify(viewOverlay).clear() + } + + @Test + fun onHoverStarted_overlayHasLightColor() { + view.setUpHoverListener() + + view.hoverStarted() + + assertThat(overlayColor) + .isEqualTo(context.resources.getColor(R.color.status_bar_icons_hover_color_light)) + } + + @Test + fun onDarkAwareHoverStarted_withBlackIcons_overlayHasDarkColor() { + view.setUpDarkAwareHoverListener() + setIconsTint(Color.BLACK) + + view.hoverStarted() + + assertThat(overlayColor) + .isEqualTo(context.resources.getColor(R.color.status_bar_icons_hover_color_dark)) + } + + @Test + fun onHoverStarted_withBlackIcons_overlayHasLightColor() { + view.setUpHoverListener() + setIconsTint(Color.BLACK) + + view.hoverStarted() + + assertThat(overlayColor) + .isEqualTo(context.resources.getColor(R.color.status_bar_icons_hover_color_light)) + } + + @Test + fun onDarkAwareHoverStarted_withWhiteIcons_overlayHasLightColor() { + view.setUpDarkAwareHoverListener() + setIconsTint(Color.WHITE) + + view.hoverStarted() + + assertThat(overlayColor) + .isEqualTo(context.resources.getColor(R.color.status_bar_icons_hover_color_light)) + } + + private fun View.setUpHoverListener() { + setOnHoverListener(factory.createListener(view)) + attachView(view) + } + + private fun View.setUpDarkAwareHoverListener() { + setOnHoverListener(factory.createDarkAwareListener(view)) + attachView(view) + } + + private fun attachView(view: View) { + ViewUtils.attachView(view) + // attaching is async so processAllMessages is required for view.repeatWhenAttached to run + looper.processAllMessages() + } + + private val overlayDrawable: Drawable + get() { + verify(viewOverlay).add(overlayCaptor.capture()) + return overlayCaptor.value + } + + private val overlayColor + get() = (overlayDrawable as PaintDrawable).paint.color + + private fun setIconsTint(@ColorInt color: Int) { + // passing empty ArrayList is equivalent to just accepting passed color as icons color + darkChange.value = DarkChange(/* areas= */ ArrayList(), /* darkIntensity= */ 1f, color) + } + + private fun TestableStatusContainer.hoverStarted() { + injectHoverEvent(hoverEvent(MotionEvent.ACTION_HOVER_ENTER)) + } + + private fun TestableStatusContainer.hoverStopped() { + injectHoverEvent(hoverEvent(MotionEvent.ACTION_HOVER_EXIT)) + } + + class TestableStatusContainer(context: Context, private val mockOverlay: ViewGroupOverlay) : + LinearLayout(context) { + + fun injectHoverEvent(event: MotionEvent) = dispatchHoverEvent(event) + + override fun getOverlay() = mockOverlay + } + + private fun hoverEvent(action: Int): MotionEvent { + return MotionEvent.obtain( + /* downTime= */ SystemClock.uptimeMillis(), + /* eventTime= */ SystemClock.uptimeMillis(), + /* action= */ action, + /* x= */ 0f, + /* y= */ 0f, + /* metaState= */ 0 + ) + } +}