summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig6
-rw-r--r--packages/SystemUI/res/drawable/global_actions_lite_button_background.xml29
-rw-r--r--packages/SystemUI/res/values/colors.xml1
-rw-r--r--packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java46
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java71
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);