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

Commit 15df635d authored by jasonwshsu's avatar jasonwshsu
Browse files

Fix accessibility settings page did not update the preference state

Root Cause: Keys in ContentObserver registered after the keys change

Solution: Keys in ContentObserver need to be observed during the whole lifecycle to update the preferences including summary text

Fix: 183157677
Fix: 172469017
Fix: 183459237
Fix: 183459376
Test: atest AccessibilitySettingsTest
Change-Id: I3b22773965f1878c499a0f9cbd8bd0f3c9c6fae9
parent 818c5396
Loading
Loading
Loading
Loading
+53 −16
Original line number Diff line number Diff line
@@ -111,7 +111,7 @@ public class AccessibilitySettings extends DashboardFragment {
        @Override
        public void run() {
            if (getActivity() != null) {
                updateServicePreferences();
                onContentChanged();
            }
        }
    };
@@ -142,7 +142,8 @@ public class AccessibilitySettings extends DashboardFragment {
        }
    };

    private final SettingsContentObserver mSettingsContentObserver;
    @VisibleForTesting
    final SettingsContentObserver mSettingsContentObserver;

    private final Map<String, PreferenceCategory> mCategoryToPrefCategoryMap =
            new ArrayMap<>();
@@ -151,6 +152,9 @@ public class AccessibilitySettings extends DashboardFragment {
    private final Map<ComponentName, PreferenceCategory> mPreBundledServiceComponentToCategoryMap =
            new ArrayMap<>();

    private boolean mNeedPreferencesUpdate = false;
    private boolean mIsForeground = true;

    public AccessibilitySettings() {
        // Observe changes to anything that the shortcut can toggle, so we can reflect updates
        final Collection<AccessibilityShortcutController.ToggleableFrameworkFeatureInfo> features =
@@ -166,7 +170,7 @@ public class AccessibilitySettings extends DashboardFragment {
        mSettingsContentObserver = new SettingsContentObserver(mHandler, shortcutFeatureKeys) {
            @Override
            public void onChange(boolean selfChange, Uri uri) {
                updateAllPreferences();
                onContentChanged();
            }
        };
    }
@@ -181,13 +185,6 @@ public class AccessibilitySettings extends DashboardFragment {
        return R.string.help_uri_accessibility;
    }

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        initializeAllPreferences();
        updateAllPreferences();
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
@@ -195,21 +192,36 @@ public class AccessibilitySettings extends DashboardFragment {
                .setFragmentManager(getFragmentManager());
    }

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        initializeAllPreferences();
        updateAllPreferences();
        registerContentMonitors();
    }

    @Override
    public void onStart() {
        if (mNeedPreferencesUpdate) {
            updateAllPreferences();
            mNeedPreferencesUpdate = false;
        }
        mIsForeground = true;
        super.onStart();

        mSettingsPackageMonitor.register(getActivity(), getActivity().getMainLooper(), false);
        mSettingsContentObserver.register(getContentResolver());
    }

    @Override
    public void onStop() {
        mSettingsPackageMonitor.unregister();
        mSettingsContentObserver.unregister(getContentResolver());
        mIsForeground = false;
        super.onStop();
    }

    @Override
    public void onDestroy() {
        unregisterContentMonitors();
        super.onDestroy();
    }

    @Override
    protected int getPreferenceScreenResId() {
        return R.xml.accessibility_settings;
@@ -283,6 +295,17 @@ public class AccessibilitySettings extends DashboardFragment {
                context.getContentResolver(), Settings.Global.APPLY_RAMPING_RINGER, 0) == 1;
    }

    @VisibleForTesting
    void onContentChanged() {
        // If the fragment is visible then update preferences immediately, else set the flag then
        // wait for the fragment to show up to update preferences.
        if (mIsForeground) {
            updateAllPreferences();
        } else {
            mNeedPreferencesUpdate = true;
        }
    }

    private void initializeAllPreferences() {
        for (int i = 0; i < CATEGORIES.length; i++) {
            PreferenceCategory prefCategory = findPreference(CATEGORIES[i]);
@@ -290,11 +313,25 @@ public class AccessibilitySettings extends DashboardFragment {
        }
    }

    private void updateAllPreferences() {
    @VisibleForTesting
    void updateAllPreferences() {
        updateSystemPreferences();
        updateServicePreferences();
    }

    private void registerContentMonitors() {
        final Context context = getActivity();

        mSettingsPackageMonitor.register(context, context.getMainLooper(), /* externalStorage= */
                false);
        mSettingsContentObserver.register(getContentResolver());
    }

    private void unregisterContentMonitors() {
        mSettingsPackageMonitor.unregister();
        mSettingsContentObserver.unregister(getContentResolver());
    }

    protected void updateServicePreferences() {
        // Since services category is auto generated we have to do a pass
        // to generate it since services can come and go and then based on
+140 −30
Original line number Diff line number Diff line
@@ -19,35 +19,55 @@ package com.android.settings.accessibility;
import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import static java.util.Collections.singletonList;

import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.AccessibilityShortcutInfo;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.accessibility.AccessibilityManager;

import androidx.fragment.app.FragmentActivity;
import androidx.preference.PreferenceManager;
import androidx.test.core.app.ApplicationProvider;

import com.android.internal.content.PackageMonitor;
import com.android.settings.R;
import com.android.settings.testutils.XmlTestUtils;
import com.android.settings.testutils.shadow.ShadowDeviceConfig;
import com.android.settings.testutils.shadow.ShadowFragment;
import com.android.settings.testutils.shadow.ShadowUserManager;
import com.android.settingslib.RestrictedPreference;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowAccessibilityManager;
@@ -55,15 +75,14 @@ import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@RunWith(RobolectricTestRunner.class)
public class AccessibilitySettingsTest {
    private static final String DUMMY_PACKAGE_NAME = "com.mock.example";
    private static final String DUMMY_CLASS_NAME = DUMMY_PACKAGE_NAME + ".mock_a11y_service";
    private static final ComponentName DUMMY_COMPONENT_NAME = new ComponentName(DUMMY_PACKAGE_NAME,
            DUMMY_CLASS_NAME);
    private static final String PACKAGE_NAME = "com.android.test";
    private static final String CLASS_NAME = PACKAGE_NAME + ".test_a11y_service";
    private static final ComponentName COMPONENT_NAME = new ComponentName(PACKAGE_NAME,
            CLASS_NAME);
    private static final int ON = 1;
    private static final int OFF = 0;
    private static final String EMPTY_STRING = "";
@@ -72,24 +91,35 @@ public class AccessibilitySettingsTest {
    private static final String DEFAULT_LABEL = "default label";
    private static final Boolean SERVICE_ENABLED = true;
    private static final Boolean SERVICE_DISABLED = false;

    private Context mContext;
    private AccessibilitySettings mSettings;
    private ShadowAccessibilityManager mShadowAccessibilityManager;
    private AccessibilityServiceInfo mServiceInfo;
    @Rule
    public final MockitoRule mocks = MockitoJUnit.rule();
    @Spy
    private final Context mContext = ApplicationProvider.getApplicationContext();
    @Spy
    private final AccessibilityServiceInfo mServiceInfo = getMockAccessibilityServiceInfo(
            PACKAGE_NAME, CLASS_NAME);
    @Spy
    private final AccessibilitySettings mFragment = new AccessibilitySettings();
    @Mock
    private AccessibilityShortcutInfo mShortcutInfo;
    @Mock
    private FragmentActivity mActivity;
    @Mock
    private ContentResolver mContentResolver;
    @Mock
    private PreferenceManager mPreferenceManager;
    private ShadowAccessibilityManager mShadowAccessibilityManager;

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

        mContext = spy(RuntimeEnvironment.application);
        mSettings = spy(new AccessibilitySettings());
        mServiceInfo = spy(getMockAccessibilityServiceInfo());
        mShadowAccessibilityManager = Shadow.extract(AccessibilityManager.getInstance(mContext));
        mShadowAccessibilityManager.setInstalledAccessibilityServiceList(new ArrayList<>());
        doReturn(mContext).when(mSettings).getContext();
        when(mFragment.getContext()).thenReturn(mContext);
        when(mFragment.getActivity()).thenReturn(mActivity);
        when(mActivity.getContentResolver()).thenReturn(mContentResolver);
        when(mFragment.getPreferenceManager()).thenReturn(mPreferenceManager);
        when(mFragment.getPreferenceManager().getContext()).thenReturn(mContext);
        mContext.setTheme(R.style.Theme_AppCompat);
    }

    @Test
@@ -216,11 +246,11 @@ public class AccessibilitySettingsTest {

    @Test
    public void createAccessibilityServicePreferenceList_hasOneInfo_containsSameKey() {
        final String key = DUMMY_COMPONENT_NAME.flattenToString();
        final String key = COMPONENT_NAME.flattenToString();
        final AccessibilitySettings.RestrictedPreferenceHelper helper =
                new AccessibilitySettings.RestrictedPreferenceHelper(mContext);
        final List<AccessibilityServiceInfo> infoList = new ArrayList<>(
                Collections.singletonList(mServiceInfo));
                singletonList(mServiceInfo));

        final List<RestrictedPreference> preferenceList =
                helper.createAccessibilityServicePreferenceList(infoList);
@@ -231,12 +261,12 @@ public class AccessibilitySettingsTest {

    @Test
    public void createAccessibilityActivityPreferenceList_hasOneInfo_containsSameKey() {
        final String key = DUMMY_COMPONENT_NAME.flattenToString();
        final String key = COMPONENT_NAME.flattenToString();
        final AccessibilitySettings.RestrictedPreferenceHelper helper =
                new AccessibilitySettings.RestrictedPreferenceHelper(mContext);
        setMockAccessibilityShortcutInfo(mShortcutInfo);
        final List<AccessibilityShortcutInfo> infoList = new ArrayList<>(
                Collections.singletonList(mShortcutInfo));
                singletonList(mShortcutInfo));

        final List<RestrictedPreference> preferenceList =
                helper.createAccessibilityActivityPreferenceList(infoList);
@@ -245,21 +275,94 @@ public class AccessibilitySettingsTest {
        assertThat(preference.getKey()).isEqualTo(key);
    }

    private AccessibilityServiceInfo getMockAccessibilityServiceInfo() {
    @Test
    @Config(shadows = {ShadowFragment.class, ShadowUserManager.class})
    public void onCreate_haveRegisterToSpecificUrisAndActions() {
        final ArgumentCaptor<IntentFilter> captor = ArgumentCaptor.forClass(IntentFilter.class);
        final IntentFilter intentFilter;
        mFragment.onAttach(mContext);

        mFragment.onCreate(Bundle.EMPTY);

        verify(mContentResolver).registerContentObserver(
                eq(Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS)),
                anyBoolean(),
                any(SettingsContentObserver.class));
        verify(mContentResolver).registerContentObserver(eq(Settings.Secure.getUriFor(
                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE)), anyBoolean(),
                any(SettingsContentObserver.class));
        verify(mActivity, atLeast(1)).registerReceiver(any(PackageMonitor.class), captor.capture(),
                isNull(), any());
        intentFilter = captor.getAllValues().get(/* first time */ 0);
        assertThat(intentFilter.hasAction(Intent.ACTION_PACKAGE_ADDED)).isTrue();
        assertThat(intentFilter.hasAction(Intent.ACTION_PACKAGE_REMOVED)).isTrue();
    }

    @Test
    @Config(shadows = {ShadowFragment.class, ShadowUserManager.class})
    public void onDestroy_unregisterObserverAndReceiver() {
        setupFragment();
        mFragment.onPause();
        mFragment.onStop();

        mFragment.onDestroy();

        verify(mContentResolver).unregisterContentObserver(any(SettingsContentObserver.class));
        verify(mActivity).unregisterReceiver(any(PackageMonitor.class));

    }

    @Test
    @Config(shadows = {ShadowFragment.class, ShadowUserManager.class})
    public void onContentChanged_updatePreferenceInForeground_preferenceUpdated() {
        setupFragment();
        mShadowAccessibilityManager.setInstalledAccessibilityServiceList(
                singletonList(mServiceInfo));

        mFragment.onContentChanged();

        RestrictedPreference preference = mFragment.getPreferenceScreen().findPreference(
                COMPONENT_NAME.flattenToString());

        assertThat(preference).isNotNull();

    }

    @Test
    @Config(shadows = {ShadowFragment.class, ShadowUserManager.class})
    public void onContentChanged_updatePreferenceInBackground_preferenceUpdated() {
        setupFragment();
        mFragment.onPause();
        mFragment.onStop();

        mShadowAccessibilityManager.setInstalledAccessibilityServiceList(
                singletonList(mServiceInfo));

        mFragment.onContentChanged();
        mFragment.onStart();

        RestrictedPreference preference = mFragment.getPreferenceScreen().findPreference(
                COMPONENT_NAME.flattenToString());

        assertThat(preference).isNotNull();

    }

    private AccessibilityServiceInfo getMockAccessibilityServiceInfo(String packageName,
            String className) {
        final ApplicationInfo applicationInfo = new ApplicationInfo();
        final ServiceInfo serviceInfo = new ServiceInfo();
        applicationInfo.packageName = DUMMY_PACKAGE_NAME;
        serviceInfo.packageName = DUMMY_PACKAGE_NAME;
        serviceInfo.name = DUMMY_CLASS_NAME;
        applicationInfo.packageName = packageName;
        serviceInfo.packageName = packageName;
        serviceInfo.name = className;
        serviceInfo.applicationInfo = applicationInfo;

        final ResolveInfo resolveInfo = new ResolveInfo();
        resolveInfo.serviceInfo = serviceInfo;

        try {
            final AccessibilityServiceInfo info = new AccessibilityServiceInfo(resolveInfo,
                    mContext);
            info.setComponentName(DUMMY_COMPONENT_NAME);
            info.setComponentName(new ComponentName(PACKAGE_NAME, CLASS_NAME));
            return info;
        } catch (XmlPullParserException | IOException e) {
            // Do nothing
@@ -274,11 +377,18 @@ public class AccessibilitySettingsTest {
        when(activityInfo.loadLabel(any())).thenReturn(DEFAULT_LABEL);
        when(mockInfo.loadSummary(any())).thenReturn(DEFAULT_SUMMARY);
        when(mockInfo.loadDescription(any())).thenReturn(DEFAULT_DESCRIPTION);
        when(mockInfo.getComponentName()).thenReturn(DUMMY_COMPONENT_NAME);
        when(mockInfo.getComponentName()).thenReturn(COMPONENT_NAME);
    }

    private void setInvisibleToggleFragmentType(AccessibilityServiceInfo info) {
        info.getResolveInfo().serviceInfo.applicationInfo.targetSdkVersion = Build.VERSION_CODES.R;
        info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON;
    }

    private void setupFragment() {
        mFragment.onAttach(mContext);
        mFragment.onCreate(Bundle.EMPTY);
        mFragment.onStart();
        mFragment.onResume();
    }
}