Merge "Add focus handling to single press global action for TV" into main
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index ed87a7d..3cda579 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -2201,3 +2201,9 @@
     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 0000000..a40fbd3
--- /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 cb656ca0..e1318dd 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 8e857b3..9444ae1 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 @@
     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 @@
         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 @@
     }
 
     @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 @@
      * 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 @@
             // 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 @@
 
             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 cd6757c..fdf420b 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.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.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 @@
         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);