diff options
5 files changed, 149 insertions, 4 deletions
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index ed87a7d35f25..3cda579aaf7b 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -2201,3 +2201,9 @@ flag { bug: "403422950" } +flag { + name: "tv_global_actions_focus" + namespace: "systemui" + description: "Enables global actions focus on TV." + bug: "402759931" +} diff --git a/packages/SystemUI/res/drawable/global_actions_lite_button_background.xml b/packages/SystemUI/res/drawable/global_actions_lite_button_background.xml new file mode 100644 index 000000000000..a40fbd368a70 --- /dev/null +++ b/packages/SystemUI/res/drawable/global_actions_lite_button_background.xml @@ -0,0 +1,29 @@ +<?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> diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index cb656ca0a108..e1318dd43c88 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -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> diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index 8e857b3313a7..9444ae1065b5 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -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; @@ -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); @@ -861,6 +863,11 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene } @VisibleForTesting + boolean isTv() { + return mIsTv; + } + + @VisibleForTesting protected final class PowerOptionsAction extends SinglePressAction { private PowerOptionsAction() { super(com.android.systemui.res.R.drawable.ic_settings_power, @@ -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) { @@ -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); @@ -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() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java index cd6757c6e7ea..fdf420b7013f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java @@ -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; @@ -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; @@ -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); |