Refactor CaptionAppearanceFragment to improve maintainability (5/n)

Root cause: There is a bunch of different logic of preferences in CaptionAppearanceFragment. It’s hard to implement new features and hard to maintain and hard to be testable.
Solution: Move out custom perference visiblity logic of CaptionAppearanceFragment into controllers to reduce the complexity of the relationship between preference and fragment.

Bug: 197695932
Test: make RunSettingsRoboTests ROBOTEST_FILTER=com.android.settings.accessibility
Change-Id: I53d4ca3809031587f8d6853b427774fd322a71d8
diff --git a/res/xml/captioning_appearance.xml b/res/xml/captioning_appearance.xml
index b29d5a7..fa574d1 100644
--- a/res/xml/captioning_appearance.xml
+++ b/res/xml/captioning_appearance.xml
@@ -43,7 +43,8 @@
 
     <PreferenceCategory
         android:key="custom"
-        android:title="@string/captioning_custom_options_title">
+        android:title="@string/captioning_custom_options_title"
+        settings:controller="com.android.settings.accessibility.CaptionCustomController">
 
         <ListPreference
             android:entries="@array/captioning_typeface_selector_titles"
diff --git a/src/com/android/settings/accessibility/CaptionAppearanceFragment.java b/src/com/android/settings/accessibility/CaptionAppearanceFragment.java
index b0f9138..7230cbc 100644
--- a/src/com/android/settings/accessibility/CaptionAppearanceFragment.java
+++ b/src/com/android/settings/accessibility/CaptionAppearanceFragment.java
@@ -17,41 +17,17 @@
 package com.android.settings.accessibility;
 
 import android.app.settings.SettingsEnums;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.provider.Settings;
-import android.view.accessibility.CaptioningManager;
-import android.view.accessibility.CaptioningManager.CaptionStyle;
-
-import androidx.annotation.VisibleForTesting;
-import androidx.preference.PreferenceCategory;
 
 import com.android.settings.R;
 import com.android.settings.dashboard.DashboardFragment;
 import com.android.settings.search.BaseSearchIndexProvider;
 import com.android.settingslib.search.SearchIndexable;
 
-import java.util.Arrays;
-import java.util.List;
-
 /** Settings fragment containing font style of captioning properties. */
 @SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC)
 public class CaptionAppearanceFragment extends DashboardFragment {
 
     private static final String TAG = "CaptionAppearanceFragment";
-    @VisibleForTesting
-    static final String PREF_CUSTOM = "custom";
-    @VisibleForTesting
-    static final List<String> CAPTIONING_FEATURE_KEYS = Arrays.asList(
-            Settings.Secure.ACCESSIBILITY_CAPTIONING_PRESET
-    );
-
-    private final Handler mHandler = new Handler(Looper.getMainLooper());
-    @VisibleForTesting
-    AccessibilitySettingsContentObserver mSettingsContentObserver;
-    private CaptioningManager mCaptioningManager;
-    private PreferenceCategory mCustom;
 
     @Override
     public int getMetricsCategory() {
@@ -59,29 +35,6 @@
     }
 
     @Override
-    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
-        super.onCreatePreferences(savedInstanceState, rootKey);
-        mCaptioningManager = getContext().getSystemService(CaptioningManager.class);
-        mSettingsContentObserver = new AccessibilitySettingsContentObserver(mHandler);
-        mSettingsContentObserver.registerKeysToObserverCallback(CAPTIONING_FEATURE_KEYS,
-                key -> refreshShowingCustom());
-        mCustom = findPreference(PREF_CUSTOM);
-        refreshShowingCustom();
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-        mSettingsContentObserver.register(getContext().getContentResolver());
-    }
-
-    @Override
-    public void onStop() {
-        super.onStop();
-        getContext().getContentResolver().unregisterContentObserver(mSettingsContentObserver);
-    }
-
-    @Override
     protected int getPreferenceScreenResId() {
         return R.xml.captioning_appearance;
     }
@@ -96,12 +49,6 @@
         return R.string.help_url_caption;
     }
 
-    private void refreshShowingCustom() {
-        final boolean isCustomPreset =
-                mCaptioningManager.getRawUserStyle() == CaptionStyle.PRESET_CUSTOM;
-        mCustom.setVisible(isCustomPreset);
-    }
-
     public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
             new BaseSearchIndexProvider(R.xml.captioning_appearance);
 }
diff --git a/src/com/android/settings/accessibility/CaptionCustomController.java b/src/com/android/settings/accessibility/CaptionCustomController.java
new file mode 100644
index 0000000..e1674a2
--- /dev/null
+++ b/src/com/android/settings/accessibility/CaptionCustomController.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2022 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.settings.accessibility;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.view.accessibility.CaptioningManager;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.core.BasePreferenceController;
+import com.android.settingslib.core.lifecycle.LifecycleObserver;
+import com.android.settingslib.core.lifecycle.events.OnStart;
+import com.android.settingslib.core.lifecycle.events.OnStop;
+
+import java.util.Arrays;
+import java.util.List;
+
+/** Preference controller for caption custom visibility. */
+public class CaptionCustomController extends BasePreferenceController
+        implements LifecycleObserver, OnStart, OnStop {
+
+    private Preference mCustom;
+    private final CaptionHelper mCaptionHelper;
+    private final ContentResolver mContentResolver;
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    @VisibleForTesting
+    AccessibilitySettingsContentObserver mSettingsContentObserver;
+    @VisibleForTesting
+    static final List<String> CAPTIONING_FEATURE_KEYS = Arrays.asList(
+            Settings.Secure.ACCESSIBILITY_CAPTIONING_PRESET
+    );
+
+    public CaptionCustomController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+        mCaptionHelper = new CaptionHelper(context);
+        mContentResolver = context.getContentResolver();
+        mSettingsContentObserver = new AccessibilitySettingsContentObserver(mHandler);
+        mSettingsContentObserver.registerKeysToObserverCallback(CAPTIONING_FEATURE_KEYS,
+                key -> refreshShowingCustom());
+    }
+
+    @VisibleForTesting
+    CaptionCustomController(Context context, String preferenceKey,
+            AccessibilitySettingsContentObserver contentObserver) {
+        this(context, preferenceKey);
+        mSettingsContentObserver = contentObserver;
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AVAILABLE;
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+        mCustom = screen.findPreference(getPreferenceKey());
+        refreshShowingCustom();
+    }
+
+    @Override
+    public void onStart() {
+        mSettingsContentObserver.register(mContentResolver);
+    }
+
+    @Override
+    public void onStop() {
+        mSettingsContentObserver.unregister(mContentResolver);
+    }
+
+    private void refreshShowingCustom() {
+        final boolean isCustomPreset =
+                mCaptionHelper.getRawUserStyle() == CaptioningManager.CaptionStyle.PRESET_CUSTOM;
+        mCustom.setVisible(isCustomPreset);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/accessibility/CaptionAppearanceFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/CaptionAppearanceFragmentTest.java
index 3c320e2..b180f22 100644
--- a/tests/robotests/src/com/android/settings/accessibility/CaptionAppearanceFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/CaptionAppearanceFragmentTest.java
@@ -18,37 +18,18 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-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 android.app.settings.SettingsEnums;
-import android.content.ContentResolver;
 import android.content.Context;
-import android.os.Bundle;
-import android.provider.Settings;
-import android.view.accessibility.CaptioningManager.CaptionStyle;
 
-import androidx.preference.PreferenceCategory;
-import androidx.preference.PreferenceManager;
-import androidx.preference.PreferenceScreen;
 import androidx.test.core.app.ApplicationProvider;
 
 import com.android.settings.R;
-import com.android.settings.SettingsActivity;
 import com.android.settings.testutils.XmlTestUtils;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.Spy;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
 import org.robolectric.RobolectricTestRunner;
-import org.robolectric.util.ReflectionHelpers;
 
 import java.util.List;
 
@@ -56,76 +37,12 @@
 @RunWith(RobolectricTestRunner.class)
 public class CaptionAppearanceFragmentTest {
 
-    @Rule
-    public MockitoRule mMockitoRule = MockitoJUnit.rule();
-    @Mock
-    private SettingsActivity mActivity;
-    @Mock
-    private PreferenceScreen mScreen;
-    @Mock
-    private PreferenceManager mPreferenceManager;
-    @Mock
-    private ContentResolver mContentResolver;
-    @Mock
-    private PreferenceCategory mCustomPref;
-    @Spy
-    private Context mContext = ApplicationProvider.getApplicationContext();
-    private TestCaptionAppearanceFragment mFragment;
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private CaptionAppearanceFragment mFragment;
 
     @Before
     public void setUp() {
-        mFragment = spy(new TestCaptionAppearanceFragment());
-        doReturn(mActivity).when(mFragment).getActivity();
-        doReturn(mContext).when(mFragment).getContext();
-        doReturn(mCustomPref).when(mFragment).findPreference(mFragment.PREF_CUSTOM);
-        when(mPreferenceManager.getPreferenceScreen()).thenReturn(mScreen);
-        ReflectionHelpers.setField(mFragment, "mPreferenceManager", mPreferenceManager);
-    }
-
-    @Test
-    public void onCreatePreferences_shouldPreferenceIsInvisible() {
-        mFragment.onAttach(mContext);
-
-        mFragment.onCreatePreferences(Bundle.EMPTY, /* rootKey */ null);
-
-        verify(mCustomPref).setVisible(false);
-    }
-
-    @Test
-    public void onCreatePreferences_customValue_shouldPreferenceIsVisible() {
-        Settings.Secure.putInt(mContext.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_CAPTIONING_PRESET, CaptionStyle.PRESET_CUSTOM);
-        mFragment.onAttach(mContext);
-
-        mFragment.onCreatePreferences(Bundle.EMPTY, /* rootKey */ null);
-
-        verify(mCustomPref).setVisible(true);
-    }
-
-    @Test
-    public void onStart_registerSpecificContentObserverForSpecificKeys() {
-        when(mContext.getContentResolver()).thenReturn(mContentResolver);
-        mFragment.onAttach(mContext);
-        mFragment.onCreatePreferences(Bundle.EMPTY, /* rootKey */ null);
-
-        mFragment.onStart();
-
-        for (String key : mFragment.CAPTIONING_FEATURE_KEYS) {
-            verify(mContentResolver).registerContentObserver(Settings.Secure.getUriFor(key),
-                    /* notifyForDescendants= */ false, mFragment.mSettingsContentObserver);
-        }
-    }
-
-    @Test
-    public void onStop_unregisterContentObserver() {
-        when(mContext.getContentResolver()).thenReturn(mContentResolver);
-        mFragment.onAttach(mContext);
-        mFragment.onCreatePreferences(Bundle.EMPTY, /* rootKey */ null);
-        mFragment.onStart();
-
-        mFragment.onStop();
-
-        verify(mContentResolver).unregisterContentObserver(mFragment.mSettingsContentObserver);
+        mFragment = new CaptionAppearanceFragment();
     }
 
     @Test
@@ -135,11 +52,21 @@
     }
 
     @Test
+    public void getPreferenceScreenResId_returnsCorrectXml() {
+        assertThat(mFragment.getPreferenceScreenResId()).isEqualTo(R.xml.captioning_appearance);
+    }
+
+    @Test
     public void getLogTag_returnsCorrectTag() {
         assertThat(mFragment.getLogTag()).isEqualTo("CaptionAppearanceFragment");
     }
 
     @Test
+    public void getHelpResource_returnsCorrectHelpResource() {
+        assertThat(mFragment.getHelpResource()).isEqualTo(R.string.help_url_caption);
+    }
+
+    @Test
     public void getNonIndexableKeys_existInXmlLayout() {
         final List<String> niks = CaptionAppearanceFragment.SEARCH_INDEX_DATA_PROVIDER
                 .getNonIndexableKeys(mContext);
@@ -148,12 +75,4 @@
 
         assertThat(keys).containsAtLeastElementsIn(niks);
     }
-
-    private static class TestCaptionAppearanceFragment extends CaptionAppearanceFragment {
-
-        @Override
-        public int getPreferenceScreenResId() {
-            return R.xml.placeholder_prefs;
-        }
-    }
 }
diff --git a/tests/robotests/src/com/android/settings/accessibility/CaptionCustomControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/CaptionCustomControllerTest.java
new file mode 100644
index 0000000..783a9fc
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/accessibility/CaptionCustomControllerTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2022 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.settings.accessibility;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.provider.Settings;
+import android.view.accessibility.CaptioningManager.CaptionStyle;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.core.BasePreferenceController;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for {@link CaptionCustomController}. */
+@RunWith(RobolectricTestRunner.class)
+public class CaptionCustomControllerTest {
+
+    private static final String PREF_KEY = "custom";
+
+    @Rule
+    public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Mock
+    private PreferenceScreen mScreen;
+    @Mock
+    private AccessibilitySettingsContentObserver mAccessibilitySettingsContentObserver;
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private ContentResolver mContentResolver;
+    private CaptionCustomController mController;
+    private Preference mPreference;
+
+    @Before
+    public void setUp() {
+        mContentResolver = mContext.getContentResolver();
+        mController = new CaptionCustomController(mContext, PREF_KEY,
+                mAccessibilitySettingsContentObserver);
+        mPreference = new Preference(mContext);
+        when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference);
+    }
+
+    @Test
+    public void getAvailabilityStatus_shouldReturnAvailable() {
+        assertThat(mController.getAvailabilityStatus())
+                .isEqualTo(BasePreferenceController.AVAILABLE);
+    }
+
+
+    @Test
+    public void displayPreference_byDefault_shouldIsInvisible() {
+        mController.displayPreference(mScreen);
+
+        assertThat(mPreference.isVisible()).isFalse();
+    }
+
+    @Test
+    public void displayPreference_customValue_shouldIsVisible() {
+        Settings.Secure.putInt(mContext.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_CAPTIONING_PRESET, CaptionStyle.PRESET_CUSTOM);
+
+        mController.displayPreference(mScreen);
+
+        assertThat(mPreference.isVisible()).isTrue();
+    }
+
+    @Test
+    public void onStart_registerSpecificContentObserverForSpecificKeys() {
+        mController.onStart();
+
+        verify(mAccessibilitySettingsContentObserver).register(mContentResolver);
+    }
+
+    @Test
+    public void onStop_unregisterContentObserver() {
+        mController.onStop();
+
+        verify(mAccessibilitySettingsContentObserver).unregister(mContentResolver);
+    }
+}