Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7b16fe58 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add focus handling to single press global action for TV" into main

parents c5ce7518 279130f1
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -2201,3 +2201,9 @@ flag {
    bug: "403422950"
}

flag {
    name: "tv_global_actions_focus"
    namespace: "systemui"
    description: "Enables global actions focus on TV."
    bug: "402759931"
}
+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>
+1 −0
Original line number Diff line number Diff line
@@ -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>
+42 −4
Original line number Diff line number Diff line
@@ -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);
@@ -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() {
@@ -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() {
+71 −0
Original line number Diff line number Diff line
@@ -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);