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

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

Merge "Updated Action Click accessibility talk back to "Double tap to dismiss"...

Merge "Updated Action Click accessibility talk back to "Double tap to dismiss" on screen saver" into main
parents 3f548c00 33cb0a5d
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());
    }
}