/*
 * 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.SHOW_MODE_AUTO;
import static android.accessibilityservice.AccessibilityService.SHOW_MODE_HARD_KEYBOARD_ORIGINAL_VALUE;
import static android.accessibilityservice.AccessibilityService.SHOW_MODE_HARD_KEYBOARD_OVERRIDDEN;
import static android.accessibilityservice.AccessibilityService.SHOW_MODE_HIDDEN;
import static android.accessibilityservice.AccessibilityService.SHOW_MODE_IGNORE_HARD_KEYBOARD;
import static android.view.accessibility.AccessibilityManager.STATE_FLAG_ACCESSIBILITY_ENABLED;
import static android.view.accessibility.AccessibilityManager.STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED;
import static android.view.accessibility.AccessibilityManager.STATE_FLAG_TOUCH_EXPLORATION_ENABLED;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;

import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.accessibilityservice.AccessibilityServiceInfo;
import android.content.ComponentName;
import android.content.Context;
import android.provider.Settings;
import android.test.mock.MockContentResolver;
import android.testing.DexmakerShareClassLoaderRule;

import com.android.internal.util.test.FakeSettingsProvider;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

/** Tests for AccessibilityUserState */
public class AccessibilityUserStateTest {

    private static final ComponentName COMPONENT_NAME =
            new ComponentName("com.android.server.accessibility", "AccessibilityUserStateTest");

    // Values of setting key SHOW_IME_WITH_HARD_KEYBOARD
    private static final int STATE_HIDE_IME = 0;
    private static final int STATE_SHOW_IME = 1;

    private static final int USER_ID = 42;

    // Mock package-private class AccessibilityServiceConnection
    @Rule public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
            new DexmakerShareClassLoaderRule();

    @Mock private AccessibilityServiceInfo mMockServiceInfo;

    @Mock private AccessibilityServiceConnection mMockConnection;

    @Mock private AccessibilityUserState.ServiceInfoChangeListener mMockListener;

    @Mock private Context mMockContext;

    private MockContentResolver mMockResolver;

    private AccessibilityUserState mUserState;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        FakeSettingsProvider.clearSettingsProvider();
        mMockResolver = new MockContentResolver();
        mMockResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
        when(mMockContext.getContentResolver()).thenReturn(mMockResolver);
        when(mMockServiceInfo.getComponentName()).thenReturn(COMPONENT_NAME);
        when(mMockConnection.getServiceInfo()).thenReturn(mMockServiceInfo);

        mUserState = new AccessibilityUserState(USER_ID, mMockContext, mMockListener);
    }

    @After
    public void tearDown() {
        FakeSettingsProvider.clearSettingsProvider();
    }

    @Test
    public void onSwitchToAnotherUser_userStateClearedNonDefaultValues() {
        mUserState.getBoundServicesLocked().add(mMockConnection);
        mUserState.getBindingServicesLocked().add(COMPONENT_NAME);
        mUserState.setLastSentClientStateLocked(
                STATE_FLAG_ACCESSIBILITY_ENABLED
                        | STATE_FLAG_TOUCH_EXPLORATION_ENABLED
                        | STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED);
        mUserState.setNonInteractiveUiTimeoutLocked(30);
        mUserState.setInteractiveUiTimeoutLocked(30);
        mUserState.mEnabledServices.add(COMPONENT_NAME);
        mUserState.mTouchExplorationGrantedServices.add(COMPONENT_NAME);
        mUserState.mAccessibilityShortcutKeyTargets.add(COMPONENT_NAME.flattenToString());
        mUserState.mAccessibilityButtonTargets.add(COMPONENT_NAME.flattenToString());
        mUserState.setTouchExplorationEnabledLocked(true);
        mUserState.setDisplayMagnificationEnabledLocked(true);
        mUserState.setAutoclickEnabledLocked(true);
        mUserState.setUserNonInteractiveUiTimeoutLocked(30);
        mUserState.setUserInteractiveUiTimeoutLocked(30);

        mUserState.onSwitchToAnotherUserLocked();

        verify(mMockConnection).unbindLocked();
        assertTrue(mUserState.getBoundServicesLocked().isEmpty());
        assertTrue(mUserState.getBindingServicesLocked().isEmpty());
        assertEquals(-1, mUserState.getLastSentClientStateLocked());
        assertEquals(0, mUserState.getNonInteractiveUiTimeoutLocked());
        assertEquals(0, mUserState.getInteractiveUiTimeoutLocked());
        assertTrue(mUserState.mEnabledServices.isEmpty());
        assertTrue(mUserState.mTouchExplorationGrantedServices.isEmpty());
        assertTrue(mUserState.mAccessibilityShortcutKeyTargets.isEmpty());
        assertTrue(mUserState.mAccessibilityButtonTargets.isEmpty());
        assertFalse(mUserState.isTouchExplorationEnabledLocked());
        assertFalse(mUserState.isDisplayMagnificationEnabledLocked());
        assertFalse(mUserState.isAutoclickEnabledLocked());
        assertEquals(0, mUserState.getUserNonInteractiveUiTimeoutLocked());
        assertEquals(0, mUserState.getUserInteractiveUiTimeoutLocked());
    }

    @Test
    public void addService_connectionAlreadyAdded_notAddAgain() {
        mUserState.getBoundServicesLocked().add(mMockConnection);

        mUserState.addServiceLocked(mMockConnection);

        verify(mMockConnection, never()).onAdded();
    }

    @Test
    public void addService_connectionNotYetAddedToBoundService_addAndNotifyServices() {
        when(mMockConnection.getComponentName()).thenReturn(COMPONENT_NAME);

        mUserState.addServiceLocked(mMockConnection);

        verify(mMockConnection).onAdded();
        assertTrue(mUserState.getBoundServicesLocked().contains(mMockConnection));
        assertEquals(mMockConnection, mUserState.mComponentNameToServiceMap.get(COMPONENT_NAME));
        verify(mMockListener).onServiceInfoChangedLocked(eq(mUserState));
    }

    @Test
    public void reconcileSoftKeyboardMode_whenStateNotMatchSettings_setBothDefault() {
        // When soft kb show mode is hidden in settings and is auto in state.
        putSecureIntForUser(Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE,
                SHOW_MODE_HIDDEN, USER_ID);

        mUserState.reconcileSoftKeyboardModeWithSettingsLocked();

        assertEquals(SHOW_MODE_AUTO, mUserState.getSoftKeyboardShowModeLocked());
        assertEquals(SHOW_MODE_AUTO, getSecureIntForUser(
                Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, USER_ID));
        assertNull(mUserState.getServiceChangingSoftKeyboardModeLocked());
    }

    @Test
    public void
            reconcileSoftKeyboardMode_stateIgnoreHardKb_settingsShowImeHardKb_setAutoOverride() {
        // When show mode is ignore hard kb without original hard kb value
        // and show ime with hard kb is hide
        putSecureIntForUser(Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE,
                SHOW_MODE_IGNORE_HARD_KEYBOARD, USER_ID);
        mUserState.setSoftKeyboardModeLocked(SHOW_MODE_IGNORE_HARD_KEYBOARD, COMPONENT_NAME);
        putSecureIntForUser(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD,
                STATE_HIDE_IME, USER_ID);

        mUserState.reconcileSoftKeyboardModeWithSettingsLocked();

        assertEquals(SHOW_MODE_AUTO | SHOW_MODE_HARD_KEYBOARD_OVERRIDDEN,
                getSecureIntForUser(Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, USER_ID));
        assertNull(mUserState.getServiceChangingSoftKeyboardModeLocked());
    }

    @Test
    public void removeService_serviceChangingSoftKeyboardMode_removeAndSetSoftKbModeAuto() {
        mUserState.setServiceChangingSoftKeyboardModeLocked(COMPONENT_NAME);
        mUserState.mComponentNameToServiceMap.put(COMPONENT_NAME, mMockConnection);
        mUserState.setSoftKeyboardModeLocked(SHOW_MODE_HIDDEN, COMPONENT_NAME);

        mUserState.removeServiceLocked(mMockConnection);

        assertFalse(mUserState.getBoundServicesLocked().contains(mMockConnection));
        verify(mMockConnection).onRemoved();
        assertEquals(SHOW_MODE_AUTO, mUserState.getSoftKeyboardShowModeLocked());
        assertNull(mUserState.mComponentNameToServiceMap.get(COMPONENT_NAME));
        verify(mMockListener).onServiceInfoChangedLocked(eq(mUserState));
    }

    @Test
    public void serviceDisconnected_removeServiceAndAddToCrashed() {
        when(mMockConnection.getComponentName()).thenReturn(COMPONENT_NAME);
        mUserState.addServiceLocked(mMockConnection);

        mUserState.serviceDisconnectedLocked(mMockConnection);

        assertFalse(mUserState.getBoundServicesLocked().contains(mMockConnection));
        assertTrue(mUserState.getCrashedServicesLocked().contains(COMPONENT_NAME));
    }

    @Test
    public void setSoftKeyboardMode_withInvalidShowMode_shouldKeepDefaultAuto() {
        final int invalidShowMode = SHOW_MODE_HIDDEN | SHOW_MODE_HARD_KEYBOARD_ORIGINAL_VALUE;

        assertFalse(mUserState.setSoftKeyboardModeLocked(invalidShowMode, null));

        assertEquals(SHOW_MODE_AUTO, mUserState.getSoftKeyboardShowModeLocked());
    }

    @Test
    public void setSoftKeyboardMode_newModeSameWithCurrentState_returnTrue() {
        when(mMockConnection.getComponentName()).thenReturn(COMPONENT_NAME);
        mUserState.addServiceLocked(mMockConnection);

        assertTrue(mUserState.setSoftKeyboardModeLocked(SHOW_MODE_AUTO, null));
    }

    @Test
    public void setSoftKeyboardMode_withIgnoreHardKb_whenHardKbOverridden_returnFalseAdNoChange() {
        putSecureIntForUser(Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE,
                SHOW_MODE_AUTO | SHOW_MODE_HARD_KEYBOARD_OVERRIDDEN, USER_ID);

        assertFalse(mUserState.setSoftKeyboardModeLocked(SHOW_MODE_IGNORE_HARD_KEYBOARD, null));

        assertEquals(SHOW_MODE_AUTO, mUserState.getSoftKeyboardShowModeLocked());
    }

    @Test
    public void
            setSoftKeyboardMode_withIgnoreHardKb_whenShowImeWithHardKb_setOriginalHardKbValue() {
        putSecureIntForUser(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, STATE_SHOW_IME, USER_ID);

        assertTrue(mUserState.setSoftKeyboardModeLocked(SHOW_MODE_IGNORE_HARD_KEYBOARD, null));

        assertEquals(SHOW_MODE_IGNORE_HARD_KEYBOARD | SHOW_MODE_HARD_KEYBOARD_ORIGINAL_VALUE,
                getSecureIntForUser(Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, USER_ID));
    }

    @Test
    public void setSoftKeyboardMode_whenCurrentIgnoreHardKb_shouldSetShowImeWithHardKbValue() {
        mUserState.setSoftKeyboardModeLocked(SHOW_MODE_IGNORE_HARD_KEYBOARD, COMPONENT_NAME);
        putSecureIntForUser(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, STATE_HIDE_IME, USER_ID);
        putSecureIntForUser(Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE,
                SHOW_MODE_IGNORE_HARD_KEYBOARD | SHOW_MODE_HARD_KEYBOARD_ORIGINAL_VALUE, USER_ID);

        assertTrue(mUserState.setSoftKeyboardModeLocked(SHOW_MODE_AUTO, null));

        assertEquals(STATE_SHOW_IME, getSecureIntForUser(
                Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, USER_ID));
    }

    @Test
    public void setSoftKeyboardMode_withRequester_shouldUpdateInternalStateAndSettingsAsIs() {
        assertTrue(mUserState.setSoftKeyboardModeLocked(SHOW_MODE_HIDDEN, COMPONENT_NAME));

        assertEquals(SHOW_MODE_HIDDEN, mUserState.getSoftKeyboardShowModeLocked());
        assertEquals(SHOW_MODE_HIDDEN, getSecureIntForUser(
                Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, USER_ID));
        assertEquals(COMPONENT_NAME, mUserState.getServiceChangingSoftKeyboardModeLocked());
    }

    @Test
    public void setSoftKeyboardMode_shouldNotifyBoundService() {
        mUserState.addServiceLocked(mMockConnection);

        assertTrue(mUserState.setSoftKeyboardModeLocked(SHOW_MODE_HIDDEN, COMPONENT_NAME));

        verify(mMockConnection).notifySoftKeyboardShowModeChangedLocked(eq(SHOW_MODE_HIDDEN));
    }

    @Test
    public void isShortcutTargetInstalledLocked_returnTrue() {
        mUserState.mInstalledServices.add(mMockServiceInfo);
        assertTrue(mUserState.isShortcutTargetInstalledLocked(COMPONENT_NAME.flattenToString()));
    }

    @Test
    public void isShortcutTargetInstalledLocked_invalidTarget_returnFalse() {
        final ComponentName invalidTarget =
                new ComponentName("com.android.server.accessibility", "InvalidTarget");
        assertFalse(mUserState.isShortcutTargetInstalledLocked(invalidTarget.flattenToString()));
    }

    private int getSecureIntForUser(String key, int userId) {
        return Settings.Secure.getIntForUser(mMockResolver, key, -1, userId);
    }

    private void putSecureIntForUser(String key, int value, int userId) {
        Settings.Secure.putIntForUser(mMockResolver, key, value, userId);
    }
}
