Loading packages/SystemUI/aconfig/systemui.aconfig +6 −0 Original line number Diff line number Diff line Loading @@ -2201,3 +2201,9 @@ flag { bug: "403422950" } flag { name: "tv_global_actions_focus" namespace: "systemui" description: "Enables global actions focus on TV." bug: "402759931" } packages/SystemUI/res/drawable/global_actions_lite_button_background.xml 0 → 100644 +29 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- 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. --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_focused="false" > <shape android:shape="oval"> <solid android:color="@color/global_actions_lite_button_background"/> </shape> </item> <item android:state_focused="true" > <shape android:shape="oval"> <solid android:color="@color/global_actions_lite_button_background_focused"/> </shape> </item> </selector> packages/SystemUI/res/values/colors.xml +1 −0 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ <!-- Colors for Power Menu Lite --> <color name="global_actions_lite_background">#191C18</color> <color name="global_actions_lite_button_background">#303030</color> <color name="global_actions_lite_button_background_focused">#808080</color> <color name="global_actions_lite_text">#F0F0F0</color> <color name="global_actions_lite_emergency_background">#F85D4D</color> <color name="global_actions_lite_emergency_icon">@color/GM2_grey_900</color> Loading packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +42 −4 Original line number Diff line number Diff line Loading @@ -253,6 +253,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene private boolean mHasTelephony; private boolean mHasVibrator; private final boolean mShowSilentToggle; private final boolean mIsTv; private final EmergencyAffordanceManager mEmergencyAffordanceManager; private final ScreenshotHelper mScreenshotHelper; private final SysuiColorExtractor mSysuiColorExtractor; Loading Loading @@ -475,6 +476,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter); mHasTelephony = packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY); mIsTv = packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK); // get notified of phone state changes mTelephonyListenerManager.addServiceStateListener(mPhoneStateListener); Loading Loading @@ -860,6 +862,11 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene dismissDialog(); } @VisibleForTesting boolean isTv() { return mIsTv; } @VisibleForTesting protected final class PowerOptionsAction extends SinglePressAction { private PowerOptionsAction() { Loading Loading @@ -1861,17 +1868,20 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene * A single press action maintains no state, just responds to a press and takes an action. */ private abstract class SinglePressAction implements Action { @VisibleForTesting abstract class SinglePressAction implements Action { private final int mIconResId; private final Drawable mIcon; private final int mMessageResId; private final CharSequence mMessage; @VisibleForTesting ImageView mIconView; protected SinglePressAction(int iconResId, int messageResId) { mIconResId = iconResId; mMessageResId = messageResId; mMessage = null; mIcon = null; mIconView = null; } protected SinglePressAction(int iconResId, Drawable icon, CharSequence message) { Loading Loading @@ -1922,12 +1932,24 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene // ConstraintLayout flow needs an ID to reference v.setId(View.generateViewId()); ImageView icon = v.findViewById(R.id.icon); mIconView = v.findViewById(R.id.icon); TextView messageView = v.findViewById(R.id.message); messageView.setSelected(true); // necessary for marquee to work icon.setImageDrawable(getIcon(context)); icon.setScaleType(ScaleType.CENTER_CROP); mIconView.setImageDrawable(getIcon(context)); mIconView.setScaleType(ScaleType.CENTER_CROP); if (com.android.systemui.Flags.tvGlobalActionsFocus()) { if (isTv()) { mIconView.setFocusable(true); mIconView.setClickable(true); mIconView.setBackground(mContext.getDrawable(com.android.systemui.res.R.drawable .global_actions_lite_button_background)); mIconView.setOnClickListener(i -> onClick()); if (mItems.get(0) == this) { mIconView.requestFocus(); } } } if (mMessage != null) { messageView.setText(mMessage); Loading @@ -1937,6 +1959,22 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene return v; } private void onClick() { if (mDialog != null) { // don't dismiss the dialog if we're opening the power options menu if (!(this instanceof PowerOptionsAction)) { // Usually clicking an item shuts down the phone, locks, or starts an // activity. We don't want to animate back into the power button when that // happens, so we disable the dialog animation before dismissing. mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations(); mDialog.dismiss(); } } else { Log.w(TAG, "Action icon clicked while mDialog is null."); } onPress(); } } protected int getGridItemLayoutResource() { Loading packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java +71 −0 Original line number Diff line number Diff line Loading @@ -40,6 +40,7 @@ import android.media.AudioManager; import android.os.Handler; import android.os.PowerManager; import android.os.UserManager; import android.platform.test.annotations.EnableFlags; import android.provider.Settings; import android.testing.TestableLooper; import android.view.Display; Loading @@ -61,6 +62,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.DialogTransitionAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; Loading Loading @@ -904,6 +906,75 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { mGlobalActionsDialogLite.showOrHideDialog(false, false, null, Display.DEFAULT_DISPLAY); } @Test @EnableFlags(Flags.FLAG_TV_GLOBAL_ACTIONS_FOCUS) public void testCreateActionItems_noneTv_actionsNotFocuseableAndClickable() { // Test like a TV, which only has standby and shut down. mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite); doReturn(2).when(mGlobalActionsDialogLite).getMaxShownPowerItems(); doReturn(false).when(mGlobalActionsDialogLite).isTv(); String[] actions = { GlobalActionsDialogLite.GLOBAL_ACTION_KEY_STANDBY, GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER}; doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions(); GlobalActionsDialogLite.ActionsDialogLite dialog = mGlobalActionsDialogLite.createDialog(); dialog.create(); dialog.show(); mTestableLooper.processAllMessages(); assertThat(dialog.isShowing()).isTrue(); final GlobalActionsDialogLite.SinglePressAction action = (GlobalActionsDialogLite.SinglePressAction) mGlobalActionsDialogLite.mItems.get(0); assertThat(action.mIconView.isClickable()).isFalse(); assertThat(action.mIconView.isFocusable()).isFalse(); assertThat(action.mIconView.performClick()).isFalse(); assertThat(dialog.isShowing()).isTrue(); final GlobalActionsDialogLite.SinglePressAction action1 = (GlobalActionsDialogLite.SinglePressAction) mGlobalActionsDialogLite.mItems.get(1); assertThat(action1.mIconView.isClickable()).isFalse(); assertThat(action1.mIconView.isFocusable()).isFalse(); assertThat(action1.mIconView.performClick()).isFalse(); assertThat(dialog.isShowing()).isTrue(); dialog.dismiss(); } @Test @EnableFlags(Flags.FLAG_TV_GLOBAL_ACTIONS_FOCUS) public void testCreateActionItems_tv_actionsFocusableAndClickable() { // Test like a TV, which only has standby and shut down. mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite); doReturn(2).when(mGlobalActionsDialogLite).getMaxShownPowerItems(); doReturn(true).when(mGlobalActionsDialogLite).isTv(); String[] actions = { GlobalActionsDialogLite.GLOBAL_ACTION_KEY_STANDBY, GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER}; doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions(); GlobalActionsDialogLite.ActionsDialogLite dialog = mGlobalActionsDialogLite.createDialog(); dialog.create(); dialog.show(); mTestableLooper.processAllMessages(); assertThat(dialog.isShowing()).isTrue(); final GlobalActionsDialogLite.SinglePressAction action = (GlobalActionsDialogLite.SinglePressAction) mGlobalActionsDialogLite.mItems.get(0); assertThat(action.mIconView.isClickable()).isTrue(); assertThat(action.mIconView.isFocusable()).isTrue(); final GlobalActionsDialogLite.SinglePressAction action1 = (GlobalActionsDialogLite.SinglePressAction) mGlobalActionsDialogLite.mItems.get(1); assertThat(action1.mIconView.isClickable()).isTrue(); assertThat(action1.mIconView.isFocusable()).isTrue(); assertThat(action.mIconView.performClick()).isTrue(); verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_STANDBY_PRESS); dialog.dismiss(); } private UserInfo mockCurrentUser(int flags) { return new UserInfo(10, "A User", flags); Loading Loading
packages/SystemUI/aconfig/systemui.aconfig +6 −0 Original line number Diff line number Diff line Loading @@ -2201,3 +2201,9 @@ flag { bug: "403422950" } flag { name: "tv_global_actions_focus" namespace: "systemui" description: "Enables global actions focus on TV." bug: "402759931" }
packages/SystemUI/res/drawable/global_actions_lite_button_background.xml 0 → 100644 +29 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- 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. --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_focused="false" > <shape android:shape="oval"> <solid android:color="@color/global_actions_lite_button_background"/> </shape> </item> <item android:state_focused="true" > <shape android:shape="oval"> <solid android:color="@color/global_actions_lite_button_background_focused"/> </shape> </item> </selector>
packages/SystemUI/res/values/colors.xml +1 −0 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ <!-- Colors for Power Menu Lite --> <color name="global_actions_lite_background">#191C18</color> <color name="global_actions_lite_button_background">#303030</color> <color name="global_actions_lite_button_background_focused">#808080</color> <color name="global_actions_lite_text">#F0F0F0</color> <color name="global_actions_lite_emergency_background">#F85D4D</color> <color name="global_actions_lite_emergency_icon">@color/GM2_grey_900</color> Loading
packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +42 −4 Original line number Diff line number Diff line Loading @@ -253,6 +253,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene private boolean mHasTelephony; private boolean mHasVibrator; private final boolean mShowSilentToggle; private final boolean mIsTv; private final EmergencyAffordanceManager mEmergencyAffordanceManager; private final ScreenshotHelper mScreenshotHelper; private final SysuiColorExtractor mSysuiColorExtractor; Loading Loading @@ -475,6 +476,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter); mHasTelephony = packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY); mIsTv = packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK); // get notified of phone state changes mTelephonyListenerManager.addServiceStateListener(mPhoneStateListener); Loading Loading @@ -860,6 +862,11 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene dismissDialog(); } @VisibleForTesting boolean isTv() { return mIsTv; } @VisibleForTesting protected final class PowerOptionsAction extends SinglePressAction { private PowerOptionsAction() { Loading Loading @@ -1861,17 +1868,20 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene * A single press action maintains no state, just responds to a press and takes an action. */ private abstract class SinglePressAction implements Action { @VisibleForTesting abstract class SinglePressAction implements Action { private final int mIconResId; private final Drawable mIcon; private final int mMessageResId; private final CharSequence mMessage; @VisibleForTesting ImageView mIconView; protected SinglePressAction(int iconResId, int messageResId) { mIconResId = iconResId; mMessageResId = messageResId; mMessage = null; mIcon = null; mIconView = null; } protected SinglePressAction(int iconResId, Drawable icon, CharSequence message) { Loading Loading @@ -1922,12 +1932,24 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene // ConstraintLayout flow needs an ID to reference v.setId(View.generateViewId()); ImageView icon = v.findViewById(R.id.icon); mIconView = v.findViewById(R.id.icon); TextView messageView = v.findViewById(R.id.message); messageView.setSelected(true); // necessary for marquee to work icon.setImageDrawable(getIcon(context)); icon.setScaleType(ScaleType.CENTER_CROP); mIconView.setImageDrawable(getIcon(context)); mIconView.setScaleType(ScaleType.CENTER_CROP); if (com.android.systemui.Flags.tvGlobalActionsFocus()) { if (isTv()) { mIconView.setFocusable(true); mIconView.setClickable(true); mIconView.setBackground(mContext.getDrawable(com.android.systemui.res.R.drawable .global_actions_lite_button_background)); mIconView.setOnClickListener(i -> onClick()); if (mItems.get(0) == this) { mIconView.requestFocus(); } } } if (mMessage != null) { messageView.setText(mMessage); Loading @@ -1937,6 +1959,22 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene return v; } private void onClick() { if (mDialog != null) { // don't dismiss the dialog if we're opening the power options menu if (!(this instanceof PowerOptionsAction)) { // Usually clicking an item shuts down the phone, locks, or starts an // activity. We don't want to animate back into the power button when that // happens, so we disable the dialog animation before dismissing. mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations(); mDialog.dismiss(); } } else { Log.w(TAG, "Action icon clicked while mDialog is null."); } onPress(); } } protected int getGridItemLayoutResource() { Loading
packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java +71 −0 Original line number Diff line number Diff line Loading @@ -40,6 +40,7 @@ import android.media.AudioManager; import android.os.Handler; import android.os.PowerManager; import android.os.UserManager; import android.platform.test.annotations.EnableFlags; import android.provider.Settings; import android.testing.TestableLooper; import android.view.Display; Loading @@ -61,6 +62,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.DialogTransitionAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; Loading Loading @@ -904,6 +906,75 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { mGlobalActionsDialogLite.showOrHideDialog(false, false, null, Display.DEFAULT_DISPLAY); } @Test @EnableFlags(Flags.FLAG_TV_GLOBAL_ACTIONS_FOCUS) public void testCreateActionItems_noneTv_actionsNotFocuseableAndClickable() { // Test like a TV, which only has standby and shut down. mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite); doReturn(2).when(mGlobalActionsDialogLite).getMaxShownPowerItems(); doReturn(false).when(mGlobalActionsDialogLite).isTv(); String[] actions = { GlobalActionsDialogLite.GLOBAL_ACTION_KEY_STANDBY, GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER}; doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions(); GlobalActionsDialogLite.ActionsDialogLite dialog = mGlobalActionsDialogLite.createDialog(); dialog.create(); dialog.show(); mTestableLooper.processAllMessages(); assertThat(dialog.isShowing()).isTrue(); final GlobalActionsDialogLite.SinglePressAction action = (GlobalActionsDialogLite.SinglePressAction) mGlobalActionsDialogLite.mItems.get(0); assertThat(action.mIconView.isClickable()).isFalse(); assertThat(action.mIconView.isFocusable()).isFalse(); assertThat(action.mIconView.performClick()).isFalse(); assertThat(dialog.isShowing()).isTrue(); final GlobalActionsDialogLite.SinglePressAction action1 = (GlobalActionsDialogLite.SinglePressAction) mGlobalActionsDialogLite.mItems.get(1); assertThat(action1.mIconView.isClickable()).isFalse(); assertThat(action1.mIconView.isFocusable()).isFalse(); assertThat(action1.mIconView.performClick()).isFalse(); assertThat(dialog.isShowing()).isTrue(); dialog.dismiss(); } @Test @EnableFlags(Flags.FLAG_TV_GLOBAL_ACTIONS_FOCUS) public void testCreateActionItems_tv_actionsFocusableAndClickable() { // Test like a TV, which only has standby and shut down. mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite); doReturn(2).when(mGlobalActionsDialogLite).getMaxShownPowerItems(); doReturn(true).when(mGlobalActionsDialogLite).isTv(); String[] actions = { GlobalActionsDialogLite.GLOBAL_ACTION_KEY_STANDBY, GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER}; doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions(); GlobalActionsDialogLite.ActionsDialogLite dialog = mGlobalActionsDialogLite.createDialog(); dialog.create(); dialog.show(); mTestableLooper.processAllMessages(); assertThat(dialog.isShowing()).isTrue(); final GlobalActionsDialogLite.SinglePressAction action = (GlobalActionsDialogLite.SinglePressAction) mGlobalActionsDialogLite.mItems.get(0); assertThat(action.mIconView.isClickable()).isTrue(); assertThat(action.mIconView.isFocusable()).isTrue(); final GlobalActionsDialogLite.SinglePressAction action1 = (GlobalActionsDialogLite.SinglePressAction) mGlobalActionsDialogLite.mItems.get(1); assertThat(action1.mIconView.isClickable()).isTrue(); assertThat(action1.mIconView.isFocusable()).isTrue(); assertThat(action.mIconView.performClick()).isTrue(); verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_STANDBY_PRESS); dialog.dismiss(); } private UserInfo mockCurrentUser(int flags) { return new UserInfo(10, "A User", flags); Loading