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

Commit 33cb0a5d authored by Prince's avatar Prince
Browse files

Updated Action Click accessibility talk back to "Double tap to dismiss" on screen saver

Test: device tested
Fixes: 316099478
Flag: NA
Change-Id: I3e712e2d2b5073cbd766c7449c6111a87a398dc0
parent 9adec260
Loading
Loading
Loading
Loading
+13 −1
Original line number Diff line number Diff line
@@ -52,6 +52,7 @@ import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.service.controls.flags.Flags;
import android.service.dreams.utils.DreamAccessibility;
import android.util.AttributeSet;
import android.util.Log;
import android.util.MathUtils;
@@ -273,6 +274,7 @@ public class DreamService extends Service implements Window.Callback {
    private boolean mDebug = false;

    private ComponentName mDreamComponent;
    private DreamAccessibility mDreamAccessibility;
    private boolean mShouldShowComplications;

    private DreamServiceWrapper mDreamServiceWrapper;
@@ -664,6 +666,7 @@ public class DreamService extends Service implements Window.Callback {
     */
    public void setInteractive(boolean interactive) {
        mInteractive = interactive;
        updateAccessibilityMessage();
    }

    /**
@@ -1430,7 +1433,7 @@ public class DreamService extends Service implements Window.Callback {
        // Hide all insets when the dream is showing
        mWindow.getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars());
        mWindow.setDecorFitsSystemWindows(false);

        updateAccessibilityMessage();
        mWindow.getDecorView().addOnAttachStateChangeListener(
                new View.OnAttachStateChangeListener() {
                    private Consumer<IDreamOverlayClient> mDreamStartOverlayConsumer;
@@ -1477,6 +1480,15 @@ public class DreamService extends Service implements Window.Callback {
                });
    }

    private void updateAccessibilityMessage() {
        if (mWindow == null) return;
        if (mDreamAccessibility == null) {
            final View rootView = mWindow.getDecorView();
            mDreamAccessibility = new DreamAccessibility(this, rootView);
        }
        mDreamAccessibility.updateAccessibilityConfiguration(isInteractive());
    }

    private boolean getWindowFlagValue(int flag, boolean defaultValue) {
        return mWindow == null ? defaultValue : (mWindow.getAttributes().flags & flag) != 0;
    }
+88 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.
 */

package android.service.dreams.utils;

import android.annotation.NonNull;
import android.content.Context;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;

import com.android.internal.R;

/**
 * {@link DreamAccessibility} allows customization of accessibility
 * actions for the root view of the dream overlay.
 * @hide
 */
public class DreamAccessibility {
    private final Context mContext;
    private final View mView;
    private final View.AccessibilityDelegate mAccessibilityDelegate;

    public DreamAccessibility(@NonNull Context context, @NonNull View view) {
        mContext = context;
        mView = view;
        mAccessibilityDelegate = createNewAccessibilityDelegate(mContext);
    }

    /**
     * @param interactive
     * Removes and add accessibility configuration depending if the dream is interactive or not
     */
    public void updateAccessibilityConfiguration(Boolean interactive) {
        if (!interactive) {
            addAccessibilityConfiguration();
        } else {
            removeCustomAccessibilityAction();
        }
    }

    /**
     * Configures the accessibility actions for the given root view.
     */
    private void addAccessibilityConfiguration() {
        mView.setAccessibilityDelegate(mAccessibilityDelegate);
    }

    /**
     * Removes Configured the accessibility actions for the given root view.
     */
    private void removeCustomAccessibilityAction() {
        if (mView.getAccessibilityDelegate() == mAccessibilityDelegate) {
            mView.setAccessibilityDelegate(null);
        }
    }

    private View.AccessibilityDelegate createNewAccessibilityDelegate(Context context) {
        return new View.AccessibilityDelegate() {
            @Override
            public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
                super.onInitializeAccessibilityNodeInfo(host, info);
                for (AccessibilityNodeInfo.AccessibilityAction action : info.getActionList()) {
                    if (action.getId() == AccessibilityNodeInfo.ACTION_CLICK) {
                        info.removeAction(action);
                        break;
                    }
                }
                info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
                        AccessibilityNodeInfo.ACTION_CLICK,
                        context.getResources().getString(R.string.dream_accessibility_action_click)
                ));
            }
        };
    }
}
+3 −0
Original line number Diff line number Diff line
@@ -1018,6 +1018,9 @@
    <!-- The title to use when a dream is opened in preview mode. [CHAR LIMIT=NONE] -->
    <string name="dream_preview_title">Preview, <xliff:g id="dream_name" example="Clock">%1$s</xliff:g></string>

    <!-- The title to use when a dream is open in accessibility mode to let users know to double tap to dismiss the dream  [CHAR LIMIT=32] -->
    <string name="dream_accessibility_action_click">dismiss</string>

    <!--  Permissions -->

    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+1 −0
Original line number Diff line number Diff line
@@ -4471,6 +4471,7 @@
  <java-symbol type="string" name="capability_title_canTakeScreenshot" />

  <java-symbol type="string" name="dream_preview_title" />
  <java-symbol type="string" name="dream_accessibility_action_click" />

  <java-symbol type="string" name="config_servicesExtensionPackage" />

+163 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.
 */

package com.android.server.dreams;


import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.content.res.Resources;
import android.service.dreams.utils.DreamAccessibility;
import android.text.TextUtils;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.internal.R;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.ArrayList;
import java.util.Collections;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class DreamAccessibilityTest {

    @Mock
    private View mView;

    @Mock
    private Context mContext;

    @Mock
    private Resources mResources;

    @Mock
    private AccessibilityNodeInfo mAccessibilityNodeInfo;

    @Captor
    private ArgumentCaptor<View.AccessibilityDelegate> mAccessibilityDelegateArgumentCaptor;

    private DreamAccessibility mDreamAccessibility;
    private static final String CUSTOM_ACTION = "Custom Action";
    private static final String EXISTING_ACTION = "Existing Action";

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mDreamAccessibility = new DreamAccessibility(mContext, mView);

        when(mContext.getResources()).thenReturn(mResources);
        when(mResources.getString(R.string.dream_accessibility_action_click))
                .thenReturn(CUSTOM_ACTION);
    }
    /**
     * Test to verify the configuration of accessibility actions within a view delegate.
     */
    @Test
    public void testConfigureAccessibilityActions() {
        when(mAccessibilityNodeInfo.getActionList()).thenReturn(new ArrayList<>());

        mDreamAccessibility.updateAccessibilityConfiguration(false);

        verify(mView).setAccessibilityDelegate(mAccessibilityDelegateArgumentCaptor.capture());
        View.AccessibilityDelegate capturedDelegate =
                mAccessibilityDelegateArgumentCaptor.getValue();

        capturedDelegate.onInitializeAccessibilityNodeInfo(mView, mAccessibilityNodeInfo);

        verify(mAccessibilityNodeInfo).addAction(argThat(action ->
                action.getId() == AccessibilityNodeInfo.ACTION_CLICK
                        && TextUtils.equals(action.getLabel(), CUSTOM_ACTION)));
    }

    /**
     * Test to verify the configuration of accessibility actions within a view delegate,
     * specifically checking the removal of an existing click action and addition
     * of a new custom action.
     */
    @Test
    public void testConfigureAccessibilityActions_RemovesExistingClickAction() {
        AccessibilityNodeInfo.AccessibilityAction existingAction =
                new AccessibilityNodeInfo.AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK,
                        EXISTING_ACTION);
        when(mAccessibilityNodeInfo.getActionList())
                .thenReturn(Collections.singletonList(existingAction));

        mDreamAccessibility.updateAccessibilityConfiguration(false);

        verify(mView).setAccessibilityDelegate(mAccessibilityDelegateArgumentCaptor.capture());
        View.AccessibilityDelegate capturedDelegate =
                mAccessibilityDelegateArgumentCaptor.getValue();

        capturedDelegate.onInitializeAccessibilityNodeInfo(mView, mAccessibilityNodeInfo);

        verify(mAccessibilityNodeInfo).removeAction(existingAction);
        verify(mAccessibilityNodeInfo).addAction(argThat(action ->
                action.getId() == AccessibilityNodeInfo.ACTION_CLICK
                        && TextUtils.equals(action.getLabel(), CUSTOM_ACTION)));

    }

    /**
     * Test to verify the removal of a custom accessibility action within a view delegate.
     */
    @Test
    public void testRemoveCustomAccessibilityAction() {

        AccessibilityNodeInfo.AccessibilityAction existingAction =
                new AccessibilityNodeInfo.AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK,
                        EXISTING_ACTION);
        when(mAccessibilityNodeInfo.getActionList())
                .thenReturn(Collections.singletonList(existingAction));

        mDreamAccessibility.updateAccessibilityConfiguration(false);
        verify(mView).setAccessibilityDelegate(mAccessibilityDelegateArgumentCaptor.capture());
        View.AccessibilityDelegate capturedDelegate =
                mAccessibilityDelegateArgumentCaptor.getValue();
        when(mView.getAccessibilityDelegate()).thenReturn(capturedDelegate);
        clearInvocations(mView);

        mDreamAccessibility.updateAccessibilityConfiguration(true);
        verify(mView).setAccessibilityDelegate(null);
    }

    /**
     * Test to verify the removal of custom accessibility action is not called if delegate is not
     * set by the dreamService.
     */
    @Test
    public void testRemoveCustomAccessibility_DoesNotRemoveDelegateNotSetByDreamAccessibility() {
        mDreamAccessibility.updateAccessibilityConfiguration(true);
        verify(mView, never()).setAccessibilityDelegate(any());
    }
}