Merge "Fix accessibility settings page did not update the preference state" into sc-dev am: e8063609eb

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Settings/+/14029862

Change-Id: Ide502e3d52249f7af468814420e5e20c6b6f488e
diff --git a/src/com/android/settings/accessibility/AccessibilitySettings.java b/src/com/android/settings/accessibility/AccessibilitySettings.java
index fb1564c..af8bf47 100644
--- a/src/com/android/settings/accessibility/AccessibilitySettings.java
+++ b/src/com/android/settings/accessibility/AccessibilitySettings.java
@@ -111,7 +111,7 @@
         @Override
         public void run() {
             if (getActivity() != null) {
-                updateServicePreferences();
+                onContentChanged();
             }
         }
     };
@@ -142,7 +142,8 @@
         }
     };
 
-    private final SettingsContentObserver mSettingsContentObserver;
+    @VisibleForTesting
+    final SettingsContentObserver mSettingsContentObserver;
 
     private final Map<String, PreferenceCategory> mCategoryToPrefCategoryMap =
             new ArrayMap<>();
@@ -151,6 +152,9 @@
     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 @@
         mSettingsContentObserver = new SettingsContentObserver(mHandler, shortcutFeatureKeys) {
             @Override
             public void onChange(boolean selfChange, Uri uri) {
-                updateAllPreferences();
+                onContentChanged();
             }
         };
     }
@@ -182,13 +186,6 @@
     }
 
     @Override
-    public void onCreate(Bundle icicle) {
-        super.onCreate(icicle);
-        initializeAllPreferences();
-        updateAllPreferences();
-    }
-
-    @Override
     public void onAttach(Context context) {
         super.onAttach(context);
         use(AccessibilityHearingAidPreferenceController.class)
@@ -196,21 +193,36 @@
     }
 
     @Override
-    public void onStart() {
-        super.onStart();
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        initializeAllPreferences();
+        updateAllPreferences();
+        registerContentMonitors();
+    }
 
-        mSettingsPackageMonitor.register(getActivity(), getActivity().getMainLooper(), false);
-        mSettingsContentObserver.register(getContentResolver());
+    @Override
+    public void onStart() {
+        if (mNeedPreferencesUpdate) {
+            updateAllPreferences();
+            mNeedPreferencesUpdate = false;
+        }
+        mIsForeground = true;
+        super.onStart();
     }
 
     @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 @@
                 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 @@
         }
     }
 
-    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
diff --git a/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java b/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java
index c2cc609..8c9d6b6 100644
--- a/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java
@@ -19,35 +19,55 @@
 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 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 @@
     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 @@
 
     @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 @@
 
     @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 @@
         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 @@
         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();
+    }
 }