/*
 * Copyright (C) 2019 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.accessibility;

import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_HOME;
import static android.accessibilityservice.AccessibilityServiceInfo.CAPABILITY_CAN_CONTROL_MAGNIFICATION;
import static android.accessibilityservice.AccessibilityServiceInfo.CAPABILITY_CAN_PERFORM_GESTURES;
import static android.accessibilityservice.AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS;
import static android.accessibilityservice.AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION;
import static android.accessibilityservice.AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT;
import static android.accessibilityservice.AccessibilityServiceInfo.DEFAULT;
import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_HAPTIC;
import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_SPOKEN;
import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS;
import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON;
import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS;
import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_REQUEST_FINGERPRINT_GESTURES;
import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
import static android.content.pm.PackageManager.FEATURE_FINGERPRINT;
import static android.view.View.FOCUS_DOWN;
import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_CLICKED;
import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_LONG_CLICKED;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import static android.view.accessibility.AccessibilityNodeInfo.FOCUS_INPUT;
import static android.view.accessibility.AccessibilityNodeInfo.ROOT_NODE_ID;

import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.IAccessibilityServiceClient;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.graphics.Region;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.IPowerManager;
import android.os.PowerManager;
import android.os.Process;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.testing.DexmakerShareClassLoaderRule;
import android.view.Display;
import android.view.KeyEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
import android.view.accessibility.IAccessibilityInteractionConnection;
import android.view.accessibility.IAccessibilityInteractionConnectionCallback;

import com.android.server.accessibility.AccessibilityWindowManager.RemoteAccessibilityConnection;
import com.android.server.accessibility.test.MessageCapturingHandler;
import com.android.server.wm.WindowManagerInternal;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;

/**
 * Tests for the AbstractAccessibilityServiceConnection
 */
public class AbstractAccessibilityServiceConnectionTest {
    private static final ComponentName COMPONENT_NAME = new ComponentName(
            "com.android.server.accessibility", ".AbstractAccessibilityServiceConnectionTest");
    private static final String PACKAGE_NAME1 = "com.android.server.accessibility1";
    private static final String PACKAGE_NAME2 = "com.android.server.accessibility2";
    private static final String VIEWID_RESOURCE_NAME = "test_viewid_resource_name";
    private static final String VIEW_TEXT = "test_view_text";
    private static final int WINDOWID = 12;
    private static final int PIP_WINDOWID = 13;
    private static final int WINDOWID_ONSECONDDISPLAY = 14;
    private static final int SECONDARY_DISPLAY_ID = Display.DEFAULT_DISPLAY + 1;
    private static final int SERVICE_ID = 42;
    private static final int A11Y_SERVICE_CAPABILITY = CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT
            | CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION
            | CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS
            | CAPABILITY_CAN_CONTROL_MAGNIFICATION
            | CAPABILITY_CAN_PERFORM_GESTURES;
    private static final int A11Y_SERVICE_FLAG = DEFAULT
            | FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
            | FLAG_REPORT_VIEW_IDS
            | FLAG_REQUEST_TOUCH_EXPLORATION_MODE
            | FLAG_REQUEST_FILTER_KEY_EVENTS
            | FLAG_REQUEST_FINGERPRINT_GESTURES
            | FLAG_REQUEST_ACCESSIBILITY_BUTTON
            | FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
    private static final int USER_ID = 1;
    private static final int USER_ID2 = 2;
    private static final int INTERACTION_ID = 199;
    private static final int PID = Process.myPid();
    private static final long TID = Process.myTid();
    private static final int UID = Process.myUid();

    private AbstractAccessibilityServiceConnection mServiceConnection;
    private MessageCapturingHandler mHandler = new MessageCapturingHandler(null);
    private final List<AccessibilityWindowInfo> mA11yWindowInfos = new ArrayList<>();
    private final List<AccessibilityWindowInfo> mA11yWindowInfosOnSecondDisplay = new ArrayList<>();
    private Callable[] mFindA11yNodesFunctions;
    private Callable<Boolean> mPerformA11yAction;
    private ArrayList<Integer> mDisplayList = new ArrayList<>(Arrays.asList(
            Display.DEFAULT_DISPLAY, SECONDARY_DISPLAY_ID));

    // To mock package-private class.
    @Rule public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
            new DexmakerShareClassLoaderRule();

    @Mock private Context mMockContext;
    @Mock private IPowerManager mMockIPowerManager;
    @Mock private PackageManager mMockPackageManager;
    @Spy  private AccessibilityServiceInfo mSpyServiceInfo = new AccessibilityServiceInfo();
    @Mock private AccessibilitySecurityPolicy mMockSecurityPolicy;
    @Mock private AccessibilityWindowManager mMockA11yWindowManager;
    @Mock private AbstractAccessibilityServiceConnection.SystemSupport mMockSystemSupport;
    @Mock private WindowManagerInternal mMockWindowManagerInternal;
    @Mock private SystemActionPerformer mMockSystemActionPerformer;
    @Mock private IBinder mMockService;
    @Mock private IAccessibilityServiceClient mMockServiceInterface;
    @Mock private KeyEventDispatcher mMockKeyEventDispatcher;
    @Mock private IAccessibilityInteractionConnection mMockIA11yInteractionConnection;
    @Mock private IAccessibilityInteractionConnectionCallback mMockCallback;
    @Mock private FingerprintGestureDispatcher mMockFingerprintGestureDispatcher;
    @Mock private MagnificationController mMockMagnificationController;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);

        when(mMockSystemSupport.getCurrentUserIdLocked()).thenReturn(USER_ID);
        when(mMockSystemSupport.getKeyEventDispatcher()).thenReturn(mMockKeyEventDispatcher);
        when(mMockSystemSupport.getFingerprintGestureDispatcher())
                .thenReturn(mMockFingerprintGestureDispatcher);
        when(mMockSystemSupport.getMagnificationController())
                .thenReturn(mMockMagnificationController);

        PowerManager powerManager =
                new PowerManager(mMockContext, mMockIPowerManager, mHandler);
        when(mMockContext.getSystemService(Context.POWER_SERVICE)).thenReturn(powerManager);
        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
        when(mMockPackageManager.hasSystemFeature(FEATURE_FINGERPRINT)).thenReturn(true);

        // Fake a11yWindowInfo and remote a11y connection for tests.
        addA11yWindowInfo(mA11yWindowInfos, WINDOWID, false, Display.DEFAULT_DISPLAY);
        addA11yWindowInfo(mA11yWindowInfos, PIP_WINDOWID, true, Display.DEFAULT_DISPLAY);
        addA11yWindowInfo(mA11yWindowInfosOnSecondDisplay, WINDOWID_ONSECONDDISPLAY, false,
                SECONDARY_DISPLAY_ID);
        when(mMockA11yWindowManager.getDisplayListLocked()).thenReturn(mDisplayList);
        when(mMockA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY))
                .thenReturn(mA11yWindowInfos);
        when(mMockA11yWindowManager.findA11yWindowInfoByIdLocked(WINDOWID))
                .thenReturn(mA11yWindowInfos.get(0));
        when(mMockA11yWindowManager.findA11yWindowInfoByIdLocked(PIP_WINDOWID))
                .thenReturn(mA11yWindowInfos.get(1));
        when(mMockA11yWindowManager.getDisplayIdByUserIdAndWindowIdLocked(USER_ID,
            WINDOWID_ONSECONDDISPLAY)).thenReturn(SECONDARY_DISPLAY_ID);
        when(mMockA11yWindowManager.getWindowListLocked(SECONDARY_DISPLAY_ID))
            .thenReturn(mA11yWindowInfosOnSecondDisplay);
        when(mMockA11yWindowManager.findA11yWindowInfoByIdLocked(WINDOWID_ONSECONDDISPLAY))
            .thenReturn(mA11yWindowInfosOnSecondDisplay.get(0));
        final RemoteAccessibilityConnection conn = getRemoteA11yConnection(
                WINDOWID, mMockIA11yInteractionConnection, PACKAGE_NAME1);
        final RemoteAccessibilityConnection connPip = getRemoteA11yConnection(
                PIP_WINDOWID, mMockIA11yInteractionConnection, PACKAGE_NAME2);
        when(mMockA11yWindowManager.getConnectionLocked(USER_ID, WINDOWID)).thenReturn(conn);
        when(mMockA11yWindowManager.getConnectionLocked(USER_ID, PIP_WINDOWID)).thenReturn(connPip);
        when(mMockA11yWindowManager.getPictureInPictureActionReplacingConnection())
                .thenReturn(connPip);

        // Update a11yServiceInfo to full capability, full flags and target sdk jelly bean
        final ResolveInfo mockResolveInfo = mock(ResolveInfo.class);
        mockResolveInfo.serviceInfo = mock(ServiceInfo.class);
        mockResolveInfo.serviceInfo.applicationInfo = mock(ApplicationInfo.class);
        mockResolveInfo.serviceInfo.applicationInfo.targetSdkVersion =
                Build.VERSION_CODES.JELLY_BEAN;
        doReturn(mockResolveInfo).when(mSpyServiceInfo).getResolveInfo();
        mSpyServiceInfo.setCapabilities(A11Y_SERVICE_CAPABILITY);
        updateServiceInfo(mSpyServiceInfo, 0, 0, A11Y_SERVICE_FLAG, null, 0);

        mServiceConnection = new TestAccessibilityServiceConnection(mMockContext, COMPONENT_NAME,
                mSpyServiceInfo, SERVICE_ID, mHandler, new Object(), mMockSecurityPolicy,
                mMockSystemSupport, mMockWindowManagerInternal, mMockSystemActionPerformer,
                mMockA11yWindowManager);
        // Assume that the service is connected
        mServiceConnection.mService = mMockService;
        mServiceConnection.mServiceInterface = mMockServiceInterface;

        // Update security policy for this service
        when(mMockSecurityPolicy.checkAccessibilityAccess(mServiceConnection)).thenReturn(true);
        when(mMockSecurityPolicy.canRetrieveWindowsLocked(mServiceConnection)).thenReturn(true);
        when(mMockSecurityPolicy.canGetAccessibilityNodeInfoLocked(
                eq(USER_ID), eq(mServiceConnection), anyInt())).thenReturn(true);
        when(mMockSecurityPolicy.canControlMagnification(mServiceConnection)).thenReturn(true);

        // init test functions for accessAccessibilityNodeInfo test case.
        initTestFunctions();
    }

    @Test
    public void getCapabilities() {
        assertThat(mServiceConnection.getCapabilities(), is(A11Y_SERVICE_CAPABILITY));
    }

    @Test
    public void onKeyEvent() throws RemoteException {
        final int sequenceNumber = 100;
        final KeyEvent mockKeyEvent = mock(KeyEvent.class);

        mServiceConnection.onKeyEvent(mockKeyEvent, sequenceNumber);
        verify(mMockServiceInterface).onKeyEvent(mockKeyEvent, sequenceNumber);
    }

    @Test
    public void setServiceInfo_invokeOnClientChange() {
        final AccessibilityServiceInfo serviceInfo = new AccessibilityServiceInfo();
        updateServiceInfo(serviceInfo,
                TYPE_VIEW_CLICKED | TYPE_VIEW_LONG_CLICKED,
                FEEDBACK_SPOKEN | FEEDBACK_HAPTIC,
                A11Y_SERVICE_FLAG,
                new String[] {PACKAGE_NAME1, PACKAGE_NAME2},
                1000);

        mServiceConnection.setServiceInfo(serviceInfo);
        verify(mMockSystemSupport).onClientChangeLocked(true);
    }

    @Test
    public void canReceiveEvents_hasEventType_returnTrue() {
        final AccessibilityServiceInfo serviceInfo = new AccessibilityServiceInfo();
        updateServiceInfo(serviceInfo,
                TYPE_VIEW_CLICKED | TYPE_VIEW_LONG_CLICKED, 0,
                0, null, 0);

        mServiceConnection.setServiceInfo(serviceInfo);
        assertThat(mServiceConnection.canReceiveEventsLocked(), is(true));
    }

    @Test
    public void setOnKeyEventResult() {
        final int sequenceNumber = 100;
        final boolean handled = true;
        mServiceConnection.setOnKeyEventResult(handled, sequenceNumber);

        verify(mMockKeyEventDispatcher).setOnKeyEventResult(
                mServiceConnection, handled, sequenceNumber);
    }

    @Test
    public void getWindows() {
        final AccessibilityWindowInfo.WindowListSparseArray allWindows =
                mServiceConnection.getWindows();

        assertEquals(2, allWindows.size());
        assertThat(allWindows.get(Display.DEFAULT_DISPLAY), is(mA11yWindowInfos));
        assertEquals(2, allWindows.get(Display.DEFAULT_DISPLAY).size());
        assertThat(allWindows.get(SECONDARY_DISPLAY_ID), is(mA11yWindowInfosOnSecondDisplay));
        assertEquals(1, allWindows.get(SECONDARY_DISPLAY_ID).size());
    }

    @Test
    public void getWindows_returnNull() {
        // no canRetrieveWindows, should return null
        when(mMockSecurityPolicy.canRetrieveWindowsLocked(mServiceConnection)).thenReturn(false);
        assertThat(mServiceConnection.getWindows(), is(nullValue()));

        // no checkAccessibilityAccess, should return null
        when(mMockSecurityPolicy.canRetrieveWindowsLocked(mServiceConnection)).thenReturn(true);
        when(mMockSecurityPolicy.checkAccessibilityAccess(mServiceConnection)).thenReturn(false);
        assertThat(mServiceConnection.getWindows(), is(nullValue()));
    }

    @Test
    public void getWindows_notTrackingWindows_invokeOnClientChange() {
        when(mMockA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY)).thenReturn(null);
        when(mMockA11yWindowManager.isTrackingWindowsLocked(Display.DEFAULT_DISPLAY))
                .thenReturn(false);

        mServiceConnection.getWindows();
        verify(mMockSystemSupport).onClientChangeLocked(false);
    }

    @Test
    public void getWindow() {
        assertThat(mServiceConnection.getWindow(WINDOWID), is(mA11yWindowInfos.get(0)));
    }

    @Test
    public void getWindow_returnNull() {
        // no canRetrieveWindows, should return null
        when(mMockSecurityPolicy.canRetrieveWindowsLocked(mServiceConnection)).thenReturn(false);
        assertThat(mServiceConnection.getWindow(WINDOWID), is(nullValue()));

        // no checkAccessibilityAccess, should return null
        when(mMockSecurityPolicy.canRetrieveWindowsLocked(mServiceConnection)).thenReturn(true);
        when(mMockSecurityPolicy.checkAccessibilityAccess(mServiceConnection)).thenReturn(false);
        assertThat(mServiceConnection.getWindow(WINDOWID), is(nullValue()));
    }

    @Test
    public void getWindow_notTrackingWindows_invokeOnClientChange() {
        when(mMockA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY)).thenReturn(null);
        when(mMockA11yWindowManager.isTrackingWindowsLocked(Display.DEFAULT_DISPLAY))
                .thenReturn(false);

        mServiceConnection.getWindow(WINDOWID);
        verify(mMockSystemSupport).onClientChangeLocked(false);
    }

    @Test
    public void getWindow_onNonDefaultDisplay() {
        assertThat(mServiceConnection.getWindow(WINDOWID_ONSECONDDISPLAY),
                is(mA11yWindowInfosOnSecondDisplay.get(0)));
    }

    @Test
    public void accessAccessibilityNodeInfo_whenCantGetInfo_returnNullOrFalse()
            throws Exception {
        when(mMockSecurityPolicy.canGetAccessibilityNodeInfoLocked(
                USER_ID, mServiceConnection, WINDOWID)).thenReturn(false);
        for (int i = 0; i < mFindA11yNodesFunctions.length; i++) {
            assertThat(mFindA11yNodesFunctions[i].call(), is(nullValue()));
        }
        assertThat(mPerformA11yAction.call(), is(false));

        verifyNoMoreInteractions(mMockIA11yInteractionConnection);
        verify(mMockSecurityPolicy, never()).computeValidReportedPackages(any(), anyInt());
    }

    @Test
    public void accessAccessibilityNodeInfo_whenNoA11yAccess_returnNullOrFalse()
            throws Exception {
        when(mMockSecurityPolicy.checkAccessibilityAccess(mServiceConnection)).thenReturn(false);
        for (int i = 0; i < mFindA11yNodesFunctions.length; i++) {
            assertThat(mFindA11yNodesFunctions[i].call(), is(nullValue()));
        }
        assertThat(mPerformA11yAction.call(), is(false));

        verifyNoMoreInteractions(mMockIA11yInteractionConnection);
        verify(mMockSecurityPolicy, never()).computeValidReportedPackages(any(), anyInt());
    }

    @Test
    public void accessAccessibilityNodeInfo_whenNoRemoteA11yConnection_returnNullOrFalse()
            throws Exception {
        when(mMockA11yWindowManager.getConnectionLocked(USER_ID, WINDOWID)).thenReturn(null);
        for (int i = 0; i < mFindA11yNodesFunctions.length; i++) {
            assertThat(mFindA11yNodesFunctions[i].call(), is(nullValue()));
        }
        assertThat(mPerformA11yAction.call(), is(false));

        verifyNoMoreInteractions(mMockIA11yInteractionConnection);
        verify(mMockSecurityPolicy, never()).computeValidReportedPackages(any(), anyInt());
    }

    @Test
    public void findAccessibilityNodeInfosByViewId_withPipWindow_shouldReplaceCallback()
            throws RemoteException {
        final ArgumentCaptor<IAccessibilityInteractionConnectionCallback> captor =
                ArgumentCaptor.forClass(IAccessibilityInteractionConnectionCallback.class);
        mServiceConnection.findAccessibilityNodeInfosByViewId(PIP_WINDOWID, ROOT_NODE_ID,
                VIEWID_RESOURCE_NAME, INTERACTION_ID, mMockCallback, TID);
        verify(mMockIA11yInteractionConnection).findAccessibilityNodeInfosByViewId(
                eq(ROOT_NODE_ID), eq(VIEWID_RESOURCE_NAME), any(), eq(INTERACTION_ID),
                captor.capture(), anyInt(), eq(PID), eq(TID), any());
        verify(mMockSecurityPolicy).computeValidReportedPackages(any(), anyInt());
        verifyReplaceActions(captor.getValue());
    }

    @Test
    public void findAccessibilityNodeInfosByText_withPipWindow_shouldReplaceCallback()
            throws RemoteException {
        final ArgumentCaptor<IAccessibilityInteractionConnectionCallback> captor =
                ArgumentCaptor.forClass(IAccessibilityInteractionConnectionCallback.class);
        mServiceConnection.findAccessibilityNodeInfosByText(PIP_WINDOWID, ROOT_NODE_ID,
                VIEW_TEXT, INTERACTION_ID, mMockCallback, TID);
        verify(mMockIA11yInteractionConnection).findAccessibilityNodeInfosByText(
                eq(ROOT_NODE_ID), eq(VIEW_TEXT), any(), eq(INTERACTION_ID),
                captor.capture(), anyInt(), eq(PID), eq(TID), any());
        verify(mMockSecurityPolicy).computeValidReportedPackages(any(), anyInt());
        verifyReplaceActions(captor.getValue());
    }

    @Test
    public void findAccessibilityNodeInfoByAccessibilityId_withPipWindow_shouldReplaceCallback()
            throws RemoteException {
        final ArgumentCaptor<IAccessibilityInteractionConnectionCallback> captor =
                ArgumentCaptor.forClass(IAccessibilityInteractionConnectionCallback.class);
        mServiceConnection.findAccessibilityNodeInfoByAccessibilityId(PIP_WINDOWID, ROOT_NODE_ID,
                INTERACTION_ID, mMockCallback, 0, TID, null);
        verify(mMockIA11yInteractionConnection).findAccessibilityNodeInfoByAccessibilityId(
                eq(ROOT_NODE_ID), any(), eq(INTERACTION_ID), captor.capture(), anyInt(),
                eq(PID), eq(TID), any(), any());
        verify(mMockSecurityPolicy).computeValidReportedPackages(any(), anyInt());
        verifyReplaceActions(captor.getValue());
    }

    @Test
    public void findFocus_withPipWindow_shouldReplaceCallback()
            throws RemoteException {
        final ArgumentCaptor<IAccessibilityInteractionConnectionCallback> captor =
                ArgumentCaptor.forClass(IAccessibilityInteractionConnectionCallback.class);
        mServiceConnection.findFocus(PIP_WINDOWID, ROOT_NODE_ID, FOCUS_INPUT, INTERACTION_ID,
                mMockCallback, TID);
        verify(mMockIA11yInteractionConnection).findFocus(eq(ROOT_NODE_ID), eq(FOCUS_INPUT),
                any(), eq(INTERACTION_ID), captor.capture(), anyInt(), eq(PID), eq(TID), any());
        verify(mMockSecurityPolicy).computeValidReportedPackages(any(), anyInt());
        verifyReplaceActions(captor.getValue());
    }

    @Test
    public void focusSearch_withPipWindow_shouldReplaceCallback()
            throws RemoteException {
        final ArgumentCaptor<IAccessibilityInteractionConnectionCallback> captor =
                ArgumentCaptor.forClass(IAccessibilityInteractionConnectionCallback.class);
        mServiceConnection.focusSearch(PIP_WINDOWID, ROOT_NODE_ID, FOCUS_DOWN, INTERACTION_ID,
                mMockCallback, TID);
        verify(mMockIA11yInteractionConnection).focusSearch(eq(ROOT_NODE_ID), eq(FOCUS_DOWN),
                any(), eq(INTERACTION_ID), captor.capture(), anyInt(), eq(PID), eq(TID), any());
        verify(mMockSecurityPolicy).computeValidReportedPackages(any(), anyInt());
        verifyReplaceActions(captor.getValue());
    }

    @Test
    public void performAccessibilityAction_withPipWindow_invokeGetPipReplacingConnection()
            throws RemoteException {
        mServiceConnection.performAccessibilityAction(PIP_WINDOWID, ROOT_NODE_ID,
                ACTION_ACCESSIBILITY_FOCUS, null, INTERACTION_ID, mMockCallback, TID);

        verify(mMockIPowerManager).userActivity(anyLong(), anyInt(), anyInt());
        verify(mMockIA11yInteractionConnection).performAccessibilityAction(eq(ROOT_NODE_ID),
                eq(ACTION_ACCESSIBILITY_FOCUS), any(), eq(INTERACTION_ID), eq(mMockCallback),
                anyInt(), eq(PID), eq(TID));
        verify(mMockA11yWindowManager).getPictureInPictureActionReplacingConnection();
    }

    @Test
    public void performAccessibilityAction_withClick_shouldNotifyOutsideTouch()
            throws RemoteException {
        mServiceConnection.performAccessibilityAction(WINDOWID, ROOT_NODE_ID,
                ACTION_CLICK, null, INTERACTION_ID, mMockCallback, TID);
        mServiceConnection.performAccessibilityAction(PIP_WINDOWID, ROOT_NODE_ID,
                ACTION_LONG_CLICK, null, INTERACTION_ID, mMockCallback, TID);
        verify(mMockA11yWindowManager).notifyOutsideTouch(eq(USER_ID), eq(WINDOWID));
        verify(mMockA11yWindowManager).notifyOutsideTouch(eq(USER_ID), eq(PIP_WINDOWID));
    }

    @Test
    public void performGlobalAction() {
        mServiceConnection.performGlobalAction(GLOBAL_ACTION_HOME);
        verify(mMockSystemActionPerformer).performSystemAction(GLOBAL_ACTION_HOME);
    }

    @Test
    public void getSystemActions() {
        List<AccessibilityNodeInfo.AccessibilityAction> actions =
                mServiceConnection.getSystemActions();
        verify(mMockSystemActionPerformer).getSystemActions();
    }

    @Test
    public void isFingerprintGestureDetectionAvailable_hasFingerPrintSupport_returnTrue() {
        when(mMockFingerprintGestureDispatcher.isFingerprintGestureDetectionAvailable())
                .thenReturn(true);
        final boolean result = mServiceConnection.isFingerprintGestureDetectionAvailable();
        assertThat(result, is(true));
    }

    @Test
    public void isFingerprintGestureDetectionAvailable_noFingerPrintSupport_returnFalse() {
        when(mMockFingerprintGestureDispatcher.isFingerprintGestureDetectionAvailable())
                .thenReturn(true);

        // Return false if device does not support fingerprint
        when(mMockPackageManager.hasSystemFeature(FEATURE_FINGERPRINT)).thenReturn(false);
        boolean result = mServiceConnection.isFingerprintGestureDetectionAvailable();
        assertThat(result, is(false));

        // Return false if service does not have flag
        when(mMockPackageManager.hasSystemFeature(FEATURE_FINGERPRINT)).thenReturn(true);
        mSpyServiceInfo.flags = A11Y_SERVICE_FLAG & ~FLAG_REQUEST_FINGERPRINT_GESTURES;
        mServiceConnection.setServiceInfo(mSpyServiceInfo);
        result = mServiceConnection.isFingerprintGestureDetectionAvailable();
        assertThat(result, is(false));
    }

    @Test
    public void getMagnificationScale() {
        final int displayId = 1;
        final float scale = 2.0f;
        when(mMockMagnificationController.getScale(displayId)).thenReturn(scale);

        final float result = mServiceConnection.getMagnificationScale(displayId);
        assertThat(result, is(scale));
    }

    @Test
    public void getMagnificationScale_serviceNotBelongCurrentUser_returnNoScale() {
        final int displayId = 1;
        final float scale = 2.0f;
        when(mMockMagnificationController.getScale(displayId)).thenReturn(scale);
        when(mMockSystemSupport.getCurrentUserIdLocked()).thenReturn(USER_ID2);

        final float result = mServiceConnection.getMagnificationScale(displayId);
        assertThat(result, is(1.0f));
    }

    @Test
    public void getMagnificationRegion_notRegistered_shouldRegisterThenUnregister() {
        final int displayId = 1;
        final Region region = new Region(10, 20, 100, 200);
        doAnswer((invocation) -> {
            ((Region) invocation.getArguments()[1]).set(region);
            return null;
        }).when(mMockMagnificationController).getMagnificationRegion(eq(displayId), any());
        when(mMockMagnificationController.isRegistered(displayId)).thenReturn(false);

        final Region result = mServiceConnection.getMagnificationRegion(displayId);
        assertThat(result, is(region));
        verify(mMockMagnificationController).register(displayId);
        verify(mMockMagnificationController).unregister(displayId);
    }

    @Test
    public void getMagnificationRegion_serviceNotBelongCurrentUser_returnEmptyRegion() {
        final int displayId = 1;
        final Region region = new Region(10, 20, 100, 200);
        doAnswer((invocation) -> {
            ((Region) invocation.getArguments()[1]).set(region);
            return null;
        }).when(mMockMagnificationController).getMagnificationRegion(eq(displayId), any());
        when(mMockSystemSupport.getCurrentUserIdLocked()).thenReturn(USER_ID2);

        final Region result = mServiceConnection.getMagnificationRegion(displayId);
        assertThat(result.isEmpty(), is(true));
    }

    @Test
    public void getMagnificationCenterX_notRegistered_shouldRegisterThenUnregister() {
        final int displayId = 1;
        final float centerX = 480.0f;
        when(mMockMagnificationController.getCenterX(displayId)).thenReturn(centerX);
        when(mMockMagnificationController.isRegistered(displayId)).thenReturn(false);

        final float result = mServiceConnection.getMagnificationCenterX(displayId);
        assertThat(result, is(centerX));
        verify(mMockMagnificationController).register(displayId);
        verify(mMockMagnificationController).unregister(displayId);
    }

    @Test
    public void getMagnificationCenterX_serviceNotBelongCurrentUser_returnZero() {
        final int displayId = 1;
        final float centerX = 480.0f;
        when(mMockMagnificationController.getCenterX(displayId)).thenReturn(centerX);
        when(mMockSystemSupport.getCurrentUserIdLocked()).thenReturn(USER_ID2);

        final float result = mServiceConnection.getMagnificationCenterX(displayId);
        assertThat(result, is(0.0f));
    }

    @Test
    public void getMagnificationCenterY_notRegistered_shouldRegisterThenUnregister() {
        final int displayId = 1;
        final float centerY = 640.0f;
        when(mMockMagnificationController.getCenterY(displayId)).thenReturn(centerY);
        when(mMockMagnificationController.isRegistered(displayId)).thenReturn(false);

        final float result = mServiceConnection.getMagnificationCenterY(displayId);
        assertThat(result, is(centerY));
        verify(mMockMagnificationController).register(displayId);
        verify(mMockMagnificationController).unregister(displayId);
    }

    @Test
    public void getMagnificationCenterY_serviceNotBelongCurrentUser_returnZero() {
        final int displayId = 1;
        final float centerY = 640.0f;
        when(mMockMagnificationController.getCenterY(displayId)).thenReturn(centerY);
        when(mMockSystemSupport.getCurrentUserIdLocked()).thenReturn(USER_ID2);

        final float result = mServiceConnection.getMagnificationCenterY(displayId);
        assertThat(result, is(0.0f));
    }

    @Test
    public void resetMagnification() {
        final int displayId = 1;
        when(mMockMagnificationController.reset(displayId, true)).thenReturn(true);

        final boolean result = mServiceConnection.resetMagnification(displayId, true);
        assertThat(result, is(true));
    }

    @Test
    public void resetMagnification_cantControlMagnification_returnFalse() {
        final int displayId = 1;
        when(mMockMagnificationController.reset(displayId, true)).thenReturn(true);
        when(mMockSecurityPolicy.canControlMagnification(mServiceConnection)).thenReturn(false);

        final boolean result = mServiceConnection.resetMagnification(displayId, true);
        assertThat(result, is(false));
    }

    @Test
    public void resetMagnification_serviceNotBelongCurrentUser_returnFalse() {
        final int displayId = 1;
        when(mMockMagnificationController.reset(displayId, true)).thenReturn(true);
        when(mMockSystemSupport.getCurrentUserIdLocked()).thenReturn(USER_ID2);

        final boolean result = mServiceConnection.resetMagnification(displayId, true);
        assertThat(result, is(false));
    }

    @Test
    public void setMagnificationScaleAndCenter_notRegistered_shouldRegister() {
        final int displayId = 1;
        final float scale = 1.8f;
        final float centerX = 50.5f;
        final float centerY = 100.5f;
        when(mMockMagnificationController.setScaleAndCenter(displayId,
                scale, centerX, centerY, true, SERVICE_ID)).thenReturn(true);
        when(mMockMagnificationController.isRegistered(displayId)).thenReturn(false);

        final boolean result = mServiceConnection.setMagnificationScaleAndCenter(
                displayId, scale, centerX, centerY, true);
        assertThat(result, is(true));
        verify(mMockMagnificationController).register(displayId);
    }

    @Test
    public void setMagnificationScaleAndCenter_cantControlMagnification_returnFalse() {
        final int displayId = 1;
        final float scale = 1.8f;
        final float centerX = 50.5f;
        final float centerY = 100.5f;
        when(mMockMagnificationController.setScaleAndCenter(displayId,
                scale, centerX, centerY, true, SERVICE_ID)).thenReturn(true);
        when(mMockSecurityPolicy.canControlMagnification(mServiceConnection)).thenReturn(false);

        final boolean result = mServiceConnection.setMagnificationScaleAndCenter(
                displayId, scale, centerX, centerY, true);
        assertThat(result, is(false));
    }

    @Test
    public void setMagnificationScaleAndCenter_serviceNotBelongCurrentUser_returnFalse() {
        final int displayId = 1;
        final float scale = 1.8f;
        final float centerX = 50.5f;
        final float centerY = 100.5f;
        when(mMockMagnificationController.setScaleAndCenter(displayId,
                scale, centerX, centerY, true, SERVICE_ID)).thenReturn(true);
        when(mMockSystemSupport.getCurrentUserIdLocked()).thenReturn(USER_ID2);

        final boolean result = mServiceConnection.setMagnificationScaleAndCenter(
                displayId, scale, centerX, centerY, true);
        assertThat(result, is(false));
    }

    @Test
    public void takeScreenshot_returnNull() {
        // no checkAccessibilityAccess, should return null.
        when(mMockSecurityPolicy.canTakeScreenshotLocked(mServiceConnection)).thenReturn(true);
        when(mMockSecurityPolicy.checkAccessibilityAccess(mServiceConnection)).thenReturn(false);
        mServiceConnection.takeScreenshot(Display.DEFAULT_DISPLAY, new RemoteCallback((result) -> {
            assertNull(result);
        }));
    }

    @Test (expected = SecurityException.class)
    public void takeScreenshot_throwSecurityException() {
        // no canTakeScreenshot, should throw security exception.
        when(mMockSecurityPolicy.canTakeScreenshotLocked(mServiceConnection)).thenReturn(false);
        mServiceConnection.takeScreenshot(Display.DEFAULT_DISPLAY, new RemoteCallback((result) -> {
        }));
    }

    private void updateServiceInfo(AccessibilityServiceInfo serviceInfo, int eventType,
            int feedbackType, int flags, String[] packageNames, int notificationTimeout) {
        serviceInfo.eventTypes = eventType;
        serviceInfo.feedbackType = feedbackType;
        serviceInfo.flags = flags;
        serviceInfo.packageNames = packageNames;
        serviceInfo.notificationTimeout = notificationTimeout;
    }

    private AccessibilityWindowInfo addA11yWindowInfo(List<AccessibilityWindowInfo> infos,
            int windowId, boolean isPip, int displayId) {
        final AccessibilityWindowInfo info = AccessibilityWindowInfo.obtain();
        info.setId(windowId);
        info.setDisplayId(displayId);
        info.setPictureInPicture(isPip);
        infos.add(info);
        return info;
    }

    private RemoteAccessibilityConnection getRemoteA11yConnection(int windowId,
            IAccessibilityInteractionConnection connection,
            String packageName) {
        return mMockA11yWindowManager.new RemoteAccessibilityConnection(
                windowId, connection, packageName, UID, USER_ID);
    }

    private void initTestFunctions() {
        // Init functions for accessibility nodes finding and searching by different filter rules.
        // We group them together for the tests because they have similar implementation.
        mFindA11yNodesFunctions = new Callable[] {
                // findAccessibilityNodeInfosByViewId
                () -> mServiceConnection.findAccessibilityNodeInfosByViewId(WINDOWID,
                        ROOT_NODE_ID, VIEWID_RESOURCE_NAME, INTERACTION_ID,
                        mMockCallback, TID),
                // findAccessibilityNodeInfosByText
                () -> mServiceConnection.findAccessibilityNodeInfosByText(WINDOWID,
                        ROOT_NODE_ID, VIEW_TEXT, INTERACTION_ID, mMockCallback, TID),
                // findAccessibilityNodeInfoByAccessibilityId
                () -> mServiceConnection.findAccessibilityNodeInfoByAccessibilityId(WINDOWID,
                        ROOT_NODE_ID, INTERACTION_ID, mMockCallback, 0, TID, null),
                // findFocus
                () -> mServiceConnection.findFocus(WINDOWID, ROOT_NODE_ID, FOCUS_INPUT,
                        INTERACTION_ID, mMockCallback, TID),
                // focusSearch
                () -> mServiceConnection.focusSearch(WINDOWID, ROOT_NODE_ID, FOCUS_DOWN,
                        INTERACTION_ID, mMockCallback, TID)
        };
        // performAccessibilityAction
        mPerformA11yAction = () ->  mServiceConnection.performAccessibilityAction(WINDOWID,
                ROOT_NODE_ID, ACTION_ACCESSIBILITY_FOCUS, null, INTERACTION_ID,
                mMockCallback, TID);
    }

    private void verifyReplaceActions(IAccessibilityInteractionConnectionCallback replacedCallback)
            throws RemoteException {
        final AccessibilityNodeInfo nodeFromApp = AccessibilityNodeInfo.obtain();
        nodeFromApp.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID, WINDOWID);

        final AccessibilityNodeInfo nodeFromReplacer = AccessibilityNodeInfo.obtain();
        nodeFromReplacer.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID,
                AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID);
        nodeFromReplacer.addAction(AccessibilityAction.ACTION_CLICK);
        nodeFromReplacer.addAction(AccessibilityAction.ACTION_EXPAND);
        final List<AccessibilityNodeInfo> replacerList = Arrays.asList(nodeFromReplacer);

        replacedCallback.setFindAccessibilityNodeInfoResult(nodeFromApp, INTERACTION_ID);
        replacedCallback.setFindAccessibilityNodeInfosResult(replacerList, INTERACTION_ID + 1);

        final ArgumentCaptor<AccessibilityNodeInfo> captor =
                ArgumentCaptor.forClass(AccessibilityNodeInfo.class);
        verify(mMockCallback).setFindAccessibilityNodeInfoResult(captor.capture(),
                eq(INTERACTION_ID));
        assertThat(captor.getValue().getActionList(),
                hasItems(AccessibilityAction.ACTION_CLICK, AccessibilityAction.ACTION_EXPAND));
    }

    private static class TestAccessibilityServiceConnection
            extends AbstractAccessibilityServiceConnection {
        int mResolvedUserId;

        TestAccessibilityServiceConnection(Context context, ComponentName componentName,
                AccessibilityServiceInfo accessibilityServiceInfo, int id, Handler mainHandler,
                Object lock, AccessibilitySecurityPolicy securityPolicy,
                SystemSupport systemSupport, WindowManagerInternal windowManagerInternal,
                SystemActionPerformer systemActionPerfomer,
                AccessibilityWindowManager a11yWindowManager) {
            super(context, componentName, accessibilityServiceInfo, id, mainHandler, lock,
                    securityPolicy, systemSupport, windowManagerInternal, systemActionPerfomer,
                    a11yWindowManager);
            mResolvedUserId = USER_ID;
        }

        @Override
        protected boolean hasRightsToCurrentUserLocked() {
            return mResolvedUserId == mSystemSupport.getCurrentUserIdLocked();
        }

        @Override
        public void disableSelf() throws RemoteException {}

        @Override
        public boolean setSoftKeyboardShowMode(int showMode) throws RemoteException {
            return false;
        }

        @Override
        public int getSoftKeyboardShowMode() throws RemoteException {
            return 0;
        }

        @Override
        public boolean switchToInputMethod(String imeId) {
            return false;
        }

        @Override
        public boolean isAccessibilityButtonAvailable() throws RemoteException {
            return false;
        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {}

        @Override
        public void onServiceDisconnected(ComponentName name) {}

        @Override
        public void binderDied() {}

        @Override
        public boolean isCapturingFingerprintGestures() {
            return mCaptureFingerprintGestures;
        }

        @Override
        public void onFingerprintGestureDetectionActiveChanged(boolean active) {}

        @Override
        public void onFingerprintGesture(int gesture) {}
    }
}
