Merge "Adding hover state to system icons in status bar and shade header" into udc-qpr-dev

This commit is contained in:
Michał Brzeziński
2023-07-27 09:49:46 +00:00
committed by Android (Google) Code Review
15 changed files with 455 additions and 54 deletions

View File

@@ -26,6 +26,7 @@ import com.android.systemui.plugins.annotations.DependsOn;
import com.android.systemui.plugins.annotations.ProvidesInterface; import com.android.systemui.plugins.annotations.ProvidesInterface;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
/** /**
* Dispatches events to {@link DarkReceiver}s about changes in darkness, tint area and dark * 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 * @return the tint to apply to view depending on the desired tint color and
* the screen tintArea in which to apply that tint * the screen tintArea in which to apply that tint
*/ */
static int getTint(ArrayList<Rect> tintAreas, View view, int color) { static int getTint(Collection<Rect> tintAreas, View view, int color) {
if (isInAreas(tintAreas, view)) { if (isInAreas(tintAreas, view)) {
return color; return color;
} else { } else {
@@ -90,7 +91,7 @@ public interface DarkIconDispatcher {
* @return true if more than half of the view area are in any of the given * @return true if more than half of the view area are in any of the given
* areas, false otherwise * areas, false otherwise
*/ */
static boolean isInAreas(ArrayList<Rect> areas, View view) { static boolean isInAreas(Collection<Rect> areas, View view) {
if (areas.isEmpty()) { if (areas.isEmpty()) {
return true; return true;
} }

View File

@@ -26,6 +26,8 @@
<color name="qs_detail_button_white">#B3FFFFFF</color><!-- 70% white --> <color name="qs_detail_button_white">#B3FFFFFF</color><!-- 70% white -->
<color name="status_bar_clock_color">#FFFFFFFF</color> <color name="status_bar_clock_color">#FFFFFFFF</color>
<color name="qs_tile_disabled_color">#9E9E9E</color> <!-- 38% black --> <color name="qs_tile_disabled_color">#9E9E9E</color> <!-- 38% black -->
<color name="status_bar_icons_hover_color_light">#38FFFFFF</color> <!-- 22% white -->
<color name="status_bar_icons_hover_color_dark">#38000000</color> <!-- 22% black -->
<!-- The color of the background in the separated list of the Global Actions menu --> <!-- The color of the background in the separated list of the Global Actions menu -->
<color name="global_actions_separated_background">#F5F5F5</color> <color name="global_actions_separated_background">#F5F5F5</color>

View File

@@ -475,6 +475,8 @@
<!-- Margin start of the system icons super container --> <!-- Margin start of the system icons super container -->
<dimen name="system_icons_super_container_margin_start">16dp</dimen> <dimen name="system_icons_super_container_margin_start">16dp</dimen>
<dimen name="status_icons_hover_state_background_radius">16dp</dimen>
<!-- Width for the notification panel and related windows --> <!-- Width for the notification panel and related windows -->
<dimen name="match_parent">-1px</dimen> <dimen name="match_parent">-1px</dimen>

View File

@@ -28,6 +28,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.StatusBarStateControllerImpl; import com.android.systemui.statusbar.StatusBarStateControllerImpl;
import com.android.systemui.statusbar.phone.ActivityStarterImpl; import com.android.systemui.statusbar.phone.ActivityStarterImpl;
import com.android.systemui.statusbar.phone.DarkIconDispatcherImpl; import com.android.systemui.statusbar.phone.DarkIconDispatcherImpl;
import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher;
import com.android.systemui.volume.VolumeDialogControllerImpl; import com.android.systemui.volume.VolumeDialogControllerImpl;
import dagger.Binds; import dagger.Binds;
@@ -49,6 +50,10 @@ public abstract class PluginModule {
@Binds @Binds
abstract DarkIconDispatcher provideDarkIconDispatcher(DarkIconDispatcherImpl controllerImpl); abstract DarkIconDispatcher provideDarkIconDispatcher(DarkIconDispatcherImpl controllerImpl);
@Binds
abstract SysuiDarkIconDispatcher provideSysuiDarkIconDispatcher(
DarkIconDispatcherImpl controllerImpl);
/** */ /** */
@Binds @Binds
abstract FalsingManager provideFalsingManager(FalsingManagerProxy falsingManagerImpl); abstract FalsingManager provideFalsingManager(FalsingManagerProxy falsingManagerImpl);

View File

@@ -61,6 +61,7 @@ import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.phone.StatusBarIconController import com.android.systemui.statusbar.phone.StatusBarIconController
import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.phone.StatusIconContainer 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.Clock
import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.NextAlarmController import com.android.systemui.statusbar.policy.NextAlarmController
@@ -99,6 +100,7 @@ constructor(
private val qsBatteryModeController: QsBatteryModeController, private val qsBatteryModeController: QsBatteryModeController,
private val nextAlarmController: NextAlarmController, private val nextAlarmController: NextAlarmController,
private val activityStarter: ActivityStarter, private val activityStarter: ActivityStarter,
private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
) : ViewController<View>(header), Dumpable { ) : ViewController<View>(header), Dumpable {
companion object { companion object {
@@ -326,6 +328,9 @@ constructor(
demoModeController.addCallback(demoModeReceiver) demoModeController.addCallback(demoModeReceiver)
statusBarIconController.addIconGroup(iconManager) statusBarIconController.addIconGroup(iconManager)
nextAlarmController.addCallback(nextAlarmCallback) nextAlarmController.addCallback(nextAlarmCallback)
systemIcons.setOnHoverListener(
statusOverlayHoverListenerFactory.createListener(systemIcons)
)
} }
override fun onViewDetached() { override fun onViewDetached() {
@@ -336,6 +341,7 @@ constructor(
demoModeController.removeCallback(demoModeReceiver) demoModeController.removeCallback(demoModeReceiver)
statusBarIconController.removeIconGroup(iconManager) statusBarIconController.removeIconGroup(iconManager)
nextAlarmController.removeCallback(nextAlarmCallback) nextAlarmController.removeCallback(nextAlarmCallback)
systemIcons.setOnHoverListener(null)
} }
fun disable(state1: Int, state2: Int, animate: Boolean) { fun disable(state1: Int, state2: Int, animate: Boolean) {

View File

@@ -32,6 +32,11 @@ import java.util.ArrayList;
import javax.inject.Inject; 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 @SysUISingleton
@@ -47,6 +52,9 @@ public class DarkIconDispatcherImpl implements SysuiDarkIconDispatcher,
private int mDarkModeIconColorSingleTone; private int mDarkModeIconColorSingleTone;
private int mLightModeIconColorSingleTone; private int mLightModeIconColorSingleTone;
private final MutableStateFlow<DarkChange> mDarkChangeFlow = StateFlowKt.MutableStateFlow(
DarkChange.EMPTY);
/** /**
*/ */
@Inject @Inject
@@ -66,6 +74,11 @@ public class DarkIconDispatcherImpl implements SysuiDarkIconDispatcher,
return mTransitionsController; return mTransitionsController;
} }
@Override
public StateFlow<DarkChange> darkChangeFlow() {
return FlowKt.asStateFlow(mDarkChangeFlow);
}
public void addDarkReceiver(DarkReceiver receiver) { public void addDarkReceiver(DarkReceiver receiver) {
mReceivers.put(receiver, receiver); mReceivers.put(receiver, receiver);
receiver.onDarkChanged(mTintAreas, mDarkIntensity, mIconTint); receiver.onDarkChanged(mTintAreas, mDarkIntensity, mIconTint);
@@ -122,6 +135,7 @@ public class DarkIconDispatcherImpl implements SysuiDarkIconDispatcher,
} }
private void applyIconTint() { private void applyIconTint() {
mDarkChangeFlow.setValue(new DarkChange(mTintAreas, mDarkIntensity, mIconTint));
for (int i = 0; i < mReceivers.size(); i++) { for (int i = 0; i < mReceivers.size(); i++) {
mReceivers.valueAt(i).onDarkChanged(mTintAreas, mDarkIntensity, mIconTint); mReceivers.valueAt(i).onDarkChanged(mTintAreas, mDarkIntensity, mIconTint);
} }

View File

@@ -34,7 +34,6 @@ import android.view.DisplayCutout;
import android.view.Gravity; import android.view.Gravity;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
@@ -43,11 +42,11 @@ import android.widget.TextView;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.android.app.animation.Interpolators;
import com.android.settingslib.Utils; import com.android.settingslib.Utils;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.battery.BatteryMeterView; import com.android.systemui.battery.BatteryMeterView;
import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; 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.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder; import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder;
import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel; 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.io.PrintWriter;
import java.util.ArrayList; 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. * The header group on Keyguard.
*/ */
@@ -83,6 +87,8 @@ public class KeyguardStatusBarView extends RelativeLayout {
private int mStatusBarPaddingEnd; private int mStatusBarPaddingEnd;
private int mMinDotWidth; private int mMinDotWidth;
private View mSystemIconsContainer; private View mSystemIconsContainer;
private final MutableStateFlow<DarkChange> mDarkChange = StateFlowKt.MutableStateFlow(
DarkChange.EMPTY);
private View mCutoutSpace; private View mCutoutSpace;
private ViewGroup mStatusIconArea; private ViewGroup mStatusIconArea;
@@ -374,49 +380,6 @@ public class KeyguardStatusBarView extends RelativeLayout {
return mKeyguardUserAvatarEnabled; 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 @Override
public void setVisibility(int visibility) { public void setVisibility(int visibility) {
super.setVisibility(visibility); super.setVisibility(visibility);
@@ -474,6 +437,7 @@ public class KeyguardStatusBarView extends RelativeLayout {
iconManager.setTint(iconColor); iconManager.setTint(iconColor);
} }
mDarkChange.setValue(new DarkChange(mEmptyTintRect, intensity, iconColor));
applyDarkness(R.id.battery, mEmptyTintRect, intensity, iconColor); applyDarkness(R.id.battery, mEmptyTintRect, intensity, iconColor);
applyDarkness(R.id.clock, mEmptyTintRect, intensity, iconColor); applyDarkness(R.id.clock, mEmptyTintRect, intensity, iconColor);
} }
@@ -536,4 +500,8 @@ public class KeyguardStatusBarView extends RelativeLayout {
super.onMeasure(widthMeasureSpec, heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Trace.endSection(); Trace.endSection();
} }
public StateFlow<DarkChange> darkChangeFlow() {
return FlowKt.asStateFlow(mDarkChange);
}
} }

View File

@@ -69,8 +69,6 @@ import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel;
import com.android.systemui.util.ViewController; import com.android.systemui.util.ViewController;
import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.util.settings.SecureSettings;
import kotlin.Unit;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -78,6 +76,8 @@ import java.util.concurrent.Executor;
import javax.inject.Inject; import javax.inject.Inject;
import kotlin.Unit;
/** View Controller for {@link com.android.systemui.statusbar.phone.KeyguardStatusBarView}. */ /** View Controller for {@link com.android.systemui.statusbar.phone.KeyguardStatusBarView}. */
public class KeyguardStatusBarViewController extends ViewController<KeyguardStatusBarView> { public class KeyguardStatusBarViewController extends ViewController<KeyguardStatusBarView> {
private static final String TAG = "KeyguardStatusBarViewController"; private static final String TAG = "KeyguardStatusBarViewController";
@@ -119,6 +119,9 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
private final Object mLock = new Object(); private final Object mLock = new Object();
private final KeyguardLogger mLogger; private final KeyguardLogger mLogger;
private View mSystemIconsContainer;
private final StatusOverlayHoverListenerFactory mStatusOverlayHoverListenerFactory;
// TODO(b/273443374): remove // TODO(b/273443374): remove
private NotificationMediaManager mNotificationMediaManager; private NotificationMediaManager mNotificationMediaManager;
@@ -286,7 +289,8 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
CommandQueue commandQueue, CommandQueue commandQueue,
@Main Executor mainExecutor, @Main Executor mainExecutor,
KeyguardLogger logger, KeyguardLogger logger,
NotificationMediaManager notificationMediaManager NotificationMediaManager notificationMediaManager,
StatusOverlayHoverListenerFactory statusOverlayHoverListenerFactory
) { ) {
super(view); super(view);
mCarrierTextController = carrierTextController; mCarrierTextController = carrierTextController;
@@ -339,6 +343,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
this::updateViewState this::updateViewState
); );
mNotificationMediaManager = notificationMediaManager; mNotificationMediaManager = notificationMediaManager;
mStatusOverlayHoverListenerFactory = statusOverlayHoverListenerFactory;
} }
@Override @Override
@@ -363,6 +368,10 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
mTintedIconManager.setBlockList(getBlockedIcons()); mTintedIconManager.setBlockList(getBlockedIcons());
mStatusBarIconController.addIconGroup(mTintedIconManager); mStatusBarIconController.addIconGroup(mTintedIconManager);
} }
mSystemIconsContainer = mView.findViewById(R.id.system_icons);
StatusOverlayHoverListener hoverListener = mStatusOverlayHoverListenerFactory
.createDarkAwareListener(mSystemIconsContainer, mView.darkChangeFlow());
mSystemIconsContainer.setOnHoverListener(hoverListener);
mView.setOnApplyWindowInsetsListener( mView.setOnApplyWindowInsetsListener(
(view, windowInsets) -> mView.updateWindowInsets(windowInsets, mInsetsProvider)); (view, windowInsets) -> mView.updateWindowInsets(windowInsets, mInsetsProvider));
mSecureSettings.registerContentObserverForUser( mSecureSettings.registerContentObserverForUser(
@@ -376,6 +385,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
@Override @Override
protected void onViewDetached() { protected void onViewDetached() {
mSystemIconsContainer.setOnHoverListener(null);
mConfigurationController.removeCallback(mConfigurationListener); mConfigurationController.removeCallback(mConfigurationListener);
mAnimationScheduler.removeCallback(mAnimationCallback); mAnimationScheduler.removeCallback(mAnimationCallback);
mUserInfoController.removeCallback(mOnUserInfoChangedListener); mUserInfoController.removeCallback(mOnUserInfoChangedListener);

View File

@@ -63,9 +63,12 @@ class PhoneStatusBarViewController private constructor(
private val userChipViewModel: StatusBarUserChipViewModel, private val userChipViewModel: StatusBarUserChipViewModel,
private val viewUtil: ViewUtil, private val viewUtil: ViewUtil,
private val featureFlags: FeatureFlags, private val featureFlags: FeatureFlags,
private val configurationController: ConfigurationController private val configurationController: ConfigurationController,
private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
) : ViewController<PhoneStatusBarView>(view) { ) : ViewController<PhoneStatusBarView>(view) {
private lateinit var statusContainer: View
private val configurationListener = object : ConfigurationController.ConfigurationListener { private val configurationListener = object : ConfigurationController.ConfigurationListener {
override fun onConfigChanged(newConfig: Configuration?) { override fun onConfigChanged(newConfig: Configuration?) {
mView.updateResources() mView.updateResources()
@@ -73,6 +76,9 @@ class PhoneStatusBarViewController private constructor(
} }
override fun onViewAttached() { override fun onViewAttached() {
statusContainer = mView.findViewById(R.id.system_icons)
statusContainer.setOnHoverListener(
statusOverlayHoverListenerFactory.createDarkAwareListener(statusContainer))
if (moveFromCenterAnimationController == null) return if (moveFromCenterAnimationController == null) return
val statusBarLeftSide: View = mView.findViewById(R.id.status_bar_start_side_except_heads_up) 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() { override fun onViewDetached() {
statusContainer.setOnHoverListener(null)
progressProvider?.setReadyToHandleTransition(false) progressProvider?.setReadyToHandleTransition(false)
moveFromCenterAnimationController?.onViewDetached() moveFromCenterAnimationController?.onViewDetached()
configurationController.removeCallback(configurationListener) configurationController.removeCallback(configurationListener)
@@ -245,6 +252,7 @@ class PhoneStatusBarViewController private constructor(
private val shadeLogger: ShadeLogger, private val shadeLogger: ShadeLogger,
private val viewUtil: ViewUtil, private val viewUtil: ViewUtil,
private val configurationController: ConfigurationController, private val configurationController: ConfigurationController,
private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
) { ) {
fun create( fun create(
view: PhoneStatusBarView view: PhoneStatusBarView
@@ -268,7 +276,8 @@ class PhoneStatusBarViewController private constructor(
userChipViewModel, userChipViewModel,
viewUtil, viewUtil,
featureFlags, featureFlags,
configurationController configurationController,
statusOverlayHoverListenerFactory,
) )
} }
} }

View File

@@ -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<DarkChange>) =
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<HoverTheme>,
) : 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)
}
}

View File

@@ -16,9 +16,16 @@
package com.android.systemui.statusbar.phone; package com.android.systemui.statusbar.phone;
import android.graphics.Rect;
import com.android.systemui.Dumpable; import com.android.systemui.Dumpable;
import com.android.systemui.plugins.DarkIconDispatcher; 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 * Dispatches events to {@link DarkReceiver}s about changes in darkness, tint area
* and dark intensity. * and dark intensity.
@@ -29,4 +36,26 @@ public interface SysuiDarkIconDispatcher extends DarkIconDispatcher, Dumpable {
* @return LightBarTransitionsController * @return LightBarTransitionsController
*/ */
LightBarTransitionsController getTransitionsController(); LightBarTransitionsController getTransitionsController();
/**
* Flow equivalent of registering {@link DarkReceiver} using
* {@link DarkIconDispatcher#addDarkReceiver(DarkReceiver)}
*/
StateFlow<DarkChange> darkChangeFlow();
/** Model for {@link #darkChangeFlow()} */
class DarkChange {
public static final DarkChange EMPTY = new DarkChange(new ArrayList<>(), 0, 0);
public DarkChange(Collection<Rect> areas, float darkIntensity, int tint) {
this.areas = areas;
this.darkIntensity = darkIntensity;
this.tint = tint;
}
public final Collection<Rect> areas;
public final float darkIntensity;
public final int tint;
}
} }

View File

@@ -55,6 +55,7 @@ import com.android.systemui.shade.carrier.ShadeCarrierGroupController
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.phone.StatusBarIconController import com.android.systemui.statusbar.phone.StatusBarIconController
import com.android.systemui.statusbar.phone.StatusIconContainer 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.Clock
import com.android.systemui.statusbar.policy.FakeConfigurationController import com.android.systemui.statusbar.policy.FakeConfigurationController
import com.android.systemui.statusbar.policy.NextAlarmController import com.android.systemui.statusbar.policy.NextAlarmController
@@ -123,6 +124,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() {
@Mock private lateinit var qsBatteryModeController: QsBatteryModeController @Mock private lateinit var qsBatteryModeController: QsBatteryModeController
@Mock private lateinit var nextAlarmController: NextAlarmController @Mock private lateinit var nextAlarmController: NextAlarmController
@Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var activityStarter: ActivityStarter
@Mock private lateinit var mStatusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory
@JvmField @Rule val mockitoRule = MockitoJUnit.rule() @JvmField @Rule val mockitoRule = MockitoJUnit.rule()
var viewVisibility = View.GONE var viewVisibility = View.GONE
@@ -194,6 +196,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() {
qsBatteryModeController, qsBatteryModeController,
nextAlarmController, nextAlarmController,
activityStarter, activityStarter,
mStatusOverlayHoverListenerFactory
) )
whenever(view.isAttachedToWindow).thenReturn(true) whenever(view.isAttachedToWindow).thenReturn(true)
shadeHeaderController.init() shadeHeaderController.init()

View File

@@ -122,6 +122,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase {
@Mock private KeyguardLogger mLogger; @Mock private KeyguardLogger mLogger;
@Mock private NotificationMediaManager mNotificationMediaManager; @Mock private NotificationMediaManager mNotificationMediaManager;
@Mock private StatusOverlayHoverListenerFactory mStatusOverlayHoverListenerFactory;
private TestShadeViewStateProvider mShadeViewStateProvider; private TestShadeViewStateProvider mShadeViewStateProvider;
private KeyguardStatusBarView mKeyguardStatusBarView; private KeyguardStatusBarView mKeyguardStatusBarView;
@@ -171,7 +172,8 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase {
mCommandQueue, mCommandQueue,
mFakeExecutor, mFakeExecutor,
mLogger, mLogger,
mNotificationMediaManager mNotificationMediaManager,
mStatusOverlayHoverListenerFactory
); );
} }

View File

@@ -69,6 +69,8 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
@Mock @Mock
private lateinit var configurationController: ConfigurationController private lateinit var configurationController: ConfigurationController
@Mock @Mock
private lateinit var mStatusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory
@Mock
private lateinit var userChipViewModel: StatusBarUserChipViewModel private lateinit var userChipViewModel: StatusBarUserChipViewModel
@Mock @Mock
private lateinit var centralSurfacesImpl: CentralSurfacesImpl private lateinit var centralSurfacesImpl: CentralSurfacesImpl
@@ -204,7 +206,8 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
sceneInteractor, sceneInteractor,
shadeLogger, shadeLogger,
viewUtil, viewUtil,
configurationController configurationController,
mStatusOverlayHoverListenerFactory
).create(view).also { ).create(view).also {
it.init() it.init()
} }

View File

@@ -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<ViewGroupOverlay>()
private val overlayCaptor = argumentCaptor<Drawable>()
private val darkDispatcher = mock<SysuiDarkIconDispatcher>()
private val darkChange: MutableStateFlow<DarkChange> = 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
)
}
}