New feature “Text and reading options” for SetupWizard, Wallpaper, and Settings (12/n).

- Link-up between the preview, font size, and display size preferences.
- Add the preview preference and entry.

Bug: 211503117
Test: make RunSettingsRoboTests ROBOTEST_FILTER=TextReadingPreviewControllerTest
Test: make RunSettingsRoboTests ROBOTEST_FILTER=TextReadingPreviewPreferenceTest
Change-Id: Ic6c48861a0051670fd78b13dca5488711de30cb8
diff --git a/res/xml/accessibility_text_reading_options.xml b/res/xml/accessibility_text_reading_options.xml
index f46c24e..167886b 100644
--- a/res/xml/accessibility_text_reading_options.xml
+++ b/res/xml/accessibility_text_reading_options.xml
@@ -21,6 +21,10 @@
     android:persistent="false"
     android:title="@string/accessibility_text_reading_options_title">
 
+    <com.android.settings.accessibility.TextReadingPreviewPreference
+        android:key="preview"
+        android:selectable="false"/>
+
     <com.android.settings.widget.LabeledSeekBarPreference
         android:key="font_size"
         android:selectable="false"
diff --git a/src/com/android/settings/accessibility/TextReadingPreferenceFragment.java b/src/com/android/settings/accessibility/TextReadingPreferenceFragment.java
index c7950e4..8066195 100644
--- a/src/com/android/settings/accessibility/TextReadingPreferenceFragment.java
+++ b/src/com/android/settings/accessibility/TextReadingPreferenceFragment.java
@@ -37,6 +37,7 @@
     private static final String TAG = "TextReadingPreferenceFragment";
     private static final String FONT_SIZE_KEY = "font_size";
     private static final String DISPLAY_SIZE_KEY = "display_size";
+    private static final String PREVIEW_KEY = "preview";
 
     @Override
     protected int getPreferenceScreenResId() {
@@ -58,9 +59,20 @@
         final List<AbstractPreferenceController> controllers = new ArrayList<>();
         final FontSizeData fontSizeData = new FontSizeData(context);
         final DisplaySizeData displaySizeData = new DisplaySizeData(context);
-        controllers.add(new PreviewSizeSeekBarController(context, FONT_SIZE_KEY, fontSizeData));
-        controllers.add(
-                new PreviewSizeSeekBarController(context, DISPLAY_SIZE_KEY, displaySizeData));
+
+        final TextReadingPreviewController previewController = new TextReadingPreviewController(
+                context, PREVIEW_KEY, fontSizeData, displaySizeData);
+        controllers.add(previewController);
+
+        final PreviewSizeSeekBarController fontSizeController = new PreviewSizeSeekBarController(
+                context, FONT_SIZE_KEY, fontSizeData);
+        fontSizeController.setInteractionListener(previewController);
+        controllers.add(fontSizeController);
+
+        final PreviewSizeSeekBarController displaySizeController = new PreviewSizeSeekBarController(
+                context, DISPLAY_SIZE_KEY, displaySizeData);
+        displaySizeController.setInteractionListener(previewController);
+        controllers.add(displaySizeController);
 
         return controllers;
     }
diff --git a/src/com/android/settings/accessibility/TextReadingPreviewController.java b/src/com/android/settings/accessibility/TextReadingPreviewController.java
new file mode 100644
index 0000000..cef20aa
--- /dev/null
+++ b/src/com/android/settings/accessibility/TextReadingPreviewController.java
@@ -0,0 +1,194 @@
+/*
+ * 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.Context;
+import android.content.res.Configuration;
+import android.os.SystemClock;
+import android.view.Choreographer;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.display.PreviewPagerAdapter;
+import com.android.settings.widget.LabeledSeekBarPreference;
+
+import java.util.Objects;
+
+/**
+ * A {@link BasePreferenceController} for controlling the preview pager of the text and reading
+ * options.
+ */
+class TextReadingPreviewController extends BasePreferenceController implements
+        PreviewSizeSeekBarController.ProgressInteractionListener {
+    static final int[] PREVIEW_SAMPLE_RES_IDS = new int[]{
+            R.layout.accessibility_text_reading_preview_app_grid,
+            R.layout.screen_zoom_preview_1,
+            R.layout.accessibility_text_reading_preview_mail_content};
+
+    private static final String PREVIEW_KEY = "preview";
+    private static final String FONT_SIZE_KEY = "font_size";
+    private static final String DISPLAY_SIZE_KEY = "display_size";
+    private static final long MIN_COMMIT_INTERVAL_MS = 800;
+    private static final long CHANGE_BY_SEEKBAR_DELAY_MS = 100;
+    private static final long CHANGE_BY_BUTTON_DELAY_MS = 300;
+    private final FontSizeData mFontSizeData;
+    private final DisplaySizeData mDisplaySizeData;
+    private int mLastFontProgress;
+    private int mLastDisplayProgress;
+    private long mLastCommitTime;
+    private TextReadingPreviewPreference mPreviewPreference;
+    private LabeledSeekBarPreference mFontSizePreference;
+    private LabeledSeekBarPreference mDisplaySizePreference;
+
+    private final Choreographer.FrameCallback mCommit = f -> {
+        tryCommitFontSizeConfig();
+        tryCommitDisplaySizeConfig();
+
+        mLastCommitTime = SystemClock.elapsedRealtime();
+    };
+
+    TextReadingPreviewController(Context context, String preferenceKey,
+            @NonNull FontSizeData fontSizeData, @NonNull DisplaySizeData displaySizeData) {
+        super(context, preferenceKey);
+        mFontSizeData = fontSizeData;
+        mDisplaySizeData = displaySizeData;
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AVAILABLE;
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+
+        mPreviewPreference = screen.findPreference(PREVIEW_KEY);
+
+        mFontSizePreference = screen.findPreference(FONT_SIZE_KEY);
+        mDisplaySizePreference = screen.findPreference(DISPLAY_SIZE_KEY);
+        Objects.requireNonNull(mFontSizePreference,
+                /* message= */ "Font size preference is null, the preview controller "
+                        + "couldn't get the info");
+        Objects.requireNonNull(mDisplaySizePreference,
+                /* message= */ "Display size preference is null, the preview controller"
+                        + " couldn't get the info");
+
+        mLastFontProgress = mFontSizePreference.getProgress();
+        mLastDisplayProgress = mDisplaySizePreference.getProgress();
+
+        final Configuration origConfig = mContext.getResources().getConfiguration();
+        final boolean isLayoutRtl =
+                origConfig.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+        final PreviewPagerAdapter pagerAdapter = new PreviewPagerAdapter(mContext, isLayoutRtl,
+                PREVIEW_SAMPLE_RES_IDS, createConfig(origConfig));
+        mPreviewPreference.setPreviewAdapter(pagerAdapter);
+        pagerAdapter.setPreviewLayer(/* newLayerIndex= */ 0,
+                /* currentLayerIndex= */ 0,
+                /* currentFrameIndex= */ 0, /* animate= */ false);
+    }
+
+    @Override
+    public void notifyPreferenceChanged() {
+        final int displayDataSize = mDisplaySizeData.getValues().size();
+        final int fontSizeProgress = mFontSizePreference.getProgress();
+        final int displaySizeProgress = mDisplaySizePreference.getProgress();
+
+        // To be consistent with the
+        // {@link PreviewPagerAdapter#setPreviewLayer(int, int, int, boolean)} behavior,
+        // here also needs the same design. In addition, please also refer to
+        // the {@link #createConfig(Configuration)}.
+        final int pagerIndex = fontSizeProgress * displayDataSize + displaySizeProgress;
+
+        mPreviewPreference.notifyPreviewPagerChanged(pagerIndex);
+    }
+
+    @Override
+    public void onProgressChanged() {
+        postCommitDelayed(CHANGE_BY_BUTTON_DELAY_MS);
+    }
+
+    @Override
+    public void onEndTrackingTouch() {
+        postCommitDelayed(CHANGE_BY_SEEKBAR_DELAY_MS);
+    }
+
+    /**
+     * Avoids the flicker when switching to the previous or next level.
+     *
+     * <p><br>[Flickering problem steps] commit()-> snapshot in framework(old screenshot) ->
+     * app update the preview -> snapshot(old screen) fade out</p>
+     *
+     * <p><br>To prevent flickering problem, we make sure that we update the local preview
+     * first and then we do the commit later. </p>
+     *
+     * <p><br><b>Note:</b> It doesn't matter that we use
+     * Choreographer or main thread handler since the delay time is longer
+     * than 1 frame. Use Choreographer to let developer understand it's a
+     * window update.</p>
+     *
+     * @param commitDelayMs the interval time after a action.
+     */
+    void postCommitDelayed(long commitDelayMs) {
+        if (SystemClock.elapsedRealtime() - mLastCommitTime < MIN_COMMIT_INTERVAL_MS) {
+            commitDelayMs += MIN_COMMIT_INTERVAL_MS;
+        }
+
+        final Choreographer choreographer = Choreographer.getInstance();
+        choreographer.removeFrameCallback(mCommit);
+        choreographer.postFrameCallbackDelayed(mCommit, commitDelayMs);
+    }
+
+    private void tryCommitFontSizeConfig() {
+        final int fontProgress = mFontSizePreference.getProgress();
+        if (fontProgress != mLastFontProgress) {
+            mFontSizeData.commit(fontProgress);
+            mLastFontProgress = fontProgress;
+        }
+    }
+
+    private void tryCommitDisplaySizeConfig() {
+        final int displayProgress = mDisplaySizePreference.getProgress();
+        if (displayProgress != mLastDisplayProgress) {
+            mDisplaySizeData.commit(displayProgress);
+            mLastDisplayProgress = displayProgress;
+        }
+    }
+
+    private Configuration[] createConfig(Configuration origConfig) {
+        final int fontDataSize = mFontSizeData.getValues().size();
+        final int displayDataSize = mDisplaySizeData.getValues().size();
+        final int totalNum = fontDataSize * displayDataSize;
+        final Configuration[] configurations = new Configuration[totalNum];
+
+        for (int i = 0; i < fontDataSize; ++i) {
+            for (int j = 0; j < displayDataSize; ++j) {
+                final Configuration config = new Configuration(origConfig);
+                config.fontScale = mFontSizeData.getValues().get(i);
+                config.densityDpi = mDisplaySizeData.getValues().get(j);
+
+                configurations[i * displayDataSize + j] = config;
+            }
+        }
+
+        return configurations;
+    }
+}
diff --git a/src/com/android/settings/accessibility/TextReadingPreviewPreference.java b/src/com/android/settings/accessibility/TextReadingPreviewPreference.java
index 1b9cc4b..4b8ca39 100644
--- a/src/com/android/settings/accessibility/TextReadingPreviewPreference.java
+++ b/src/com/android/settings/accessibility/TextReadingPreviewPreference.java
@@ -32,8 +32,9 @@
 /**
  * A {@link Preference} that could show the preview related to the text and reading options.
  */
-final class TextReadingPreviewPreference extends Preference {
+public class TextReadingPreviewPreference extends Preference {
     private int mCurrentItem;
+    private int mLastLayerIndex;
     private PreviewPagerAdapter mPreviewAdapter;
 
     TextReadingPreviewPreference(Context context) {
@@ -41,7 +42,7 @@
         init();
     }
 
-    TextReadingPreviewPreference(Context context, AttributeSet attrs) {
+    public TextReadingPreviewPreference(Context context, AttributeSet attrs) {
         super(context, attrs);
         init();
     }
@@ -120,4 +121,16 @@
     private void init() {
         setLayoutResource(R.layout.accessibility_text_reading_preview);
     }
+
+    void notifyPreviewPagerChanged(int pagerIndex) {
+        Preconditions.checkNotNull(mPreviewAdapter,
+                "Preview adapter is null, you should init the preview adapter first");
+
+        if (pagerIndex != mLastLayerIndex) {
+            mPreviewAdapter.setPreviewLayer(pagerIndex, mLastLayerIndex, getCurrentItem(),
+                    /* animate= */ false);
+        }
+
+        mLastLayerIndex = pagerIndex;
+    }
 }
diff --git a/src/com/android/settings/display/PreviewPagerAdapter.java b/src/com/android/settings/display/PreviewPagerAdapter.java
index 018be32..693d574 100644
--- a/src/com/android/settings/display/PreviewPagerAdapter.java
+++ b/src/com/android/settings/display/PreviewPagerAdapter.java
@@ -117,7 +117,15 @@
         mAnimationEndAction = action;
     }
 
-    void setPreviewLayer(int newLayerIndex, int currentLayerIndex, int currentFrameIndex,
+    /**
+     * Switches the sample layouts for the preview pager.
+     *
+     * @param newLayerIndex the new layer index
+     * @param currentLayerIndex the current layer index
+     * @param currentFrameIndex the current frame index
+     * @param animate whether to enable the animation
+     */
+    public void setPreviewLayer(int newLayerIndex, int currentLayerIndex, int currentFrameIndex,
             final boolean animate) {
         for (FrameLayout previewFrame : mPreviewFrames) {
             if (currentLayerIndex >= 0) {
diff --git a/tests/robotests/src/com/android/settings/accessibility/TextReadingPreviewControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/TextReadingPreviewControllerTest.java
new file mode 100644
index 0000000..b630509
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/accessibility/TextReadingPreviewControllerTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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 org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.display.PreviewPagerAdapter;
+import com.android.settings.widget.LabeledSeekBarPreference;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowChoreographer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for {@link TextReadingPreviewController}.
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = ShadowChoreographer.class)
+public class TextReadingPreviewControllerTest {
+    private static final String PREVIEW_KEY = "preview";
+    private static final String FONT_SIZE_KEY = "font_size";
+    private static final String DISPLAY_SIZE_KEY = "display_size";
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private TextReadingPreviewController mPreviewController;
+    private TextReadingPreviewPreference mPreviewPreference;
+    private LabeledSeekBarPreference mFontSizePreference;
+    private LabeledSeekBarPreference mDisplaySizePreference;
+
+    @Mock
+    private DisplaySizeData mDisplaySizeData;
+
+    @Mock
+    private PreferenceScreen mPreferenceScreen;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        final FontSizeData fontSizeData = new FontSizeData(mContext);
+        final List<Integer> displayData = createFakeDisplayData();
+        when(mDisplaySizeData.getValues()).thenReturn(displayData);
+        mPreviewPreference = spy(new TextReadingPreviewPreference(mContext, /* attr= */ null));
+        mPreviewController = new TextReadingPreviewController(mContext, PREVIEW_KEY, fontSizeData,
+                mDisplaySizeData);
+        mFontSizePreference = new LabeledSeekBarPreference(mContext, /* attr= */ null);
+        mDisplaySizePreference = new LabeledSeekBarPreference(mContext, /* attr= */ null);
+    }
+
+    @Test
+    public void initPreviewerAdapter_verifyAction() {
+        when(mPreferenceScreen.findPreference(PREVIEW_KEY)).thenReturn(mPreviewPreference);
+        when(mPreferenceScreen.findPreference(FONT_SIZE_KEY)).thenReturn(mFontSizePreference);
+        when(mPreferenceScreen.findPreference(DISPLAY_SIZE_KEY)).thenReturn(mDisplaySizePreference);
+
+        mPreviewController.displayPreference(mPreferenceScreen);
+
+        verify(mPreviewPreference).setPreviewAdapter(any(PreviewPagerAdapter.class));
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void initPreviewerAdapterWithoutDisplaySizePreference_throwNPE() {
+        when(mPreferenceScreen.findPreference(PREVIEW_KEY)).thenReturn(mPreviewPreference);
+        when(mPreferenceScreen.findPreference(DISPLAY_SIZE_KEY)).thenReturn(mDisplaySizePreference);
+
+        mPreviewController.displayPreference(mPreferenceScreen);
+
+        verify(mPreviewPreference).setPreviewAdapter(any(PreviewPagerAdapter.class));
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void initPreviewerAdapterWithoutFontSizePreference_throwNPE() {
+        when(mPreferenceScreen.findPreference(PREVIEW_KEY)).thenReturn(mPreviewPreference);
+        when(mPreferenceScreen.findPreference(FONT_SIZE_KEY)).thenReturn(mFontSizePreference);
+
+        mPreviewController.displayPreference(mPreferenceScreen);
+
+        verify(mPreviewPreference).setPreviewAdapter(any(PreviewPagerAdapter.class));
+    }
+
+    private List<Integer> createFakeDisplayData() {
+        final List<Integer> list = new ArrayList<>();
+        list.add(1);
+        list.add(2);
+        list.add(3);
+        list.add(4);
+
+        return list;
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/accessibility/TextReadingPreviewPreferenceTest.java b/tests/robotests/src/com/android/settings/accessibility/TextReadingPreviewPreferenceTest.java
index 6b9395a..3dc82da 100644
--- a/tests/robotests/src/com/android/settings/accessibility/TextReadingPreviewPreferenceTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/TextReadingPreviewPreferenceTest.java
@@ -16,8 +16,16 @@
 
 package com.android.settings.accessibility;
 
+import static com.android.settings.accessibility.TextReadingPreviewController.PREVIEW_SAMPLE_RES_IDS;
+
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
 import android.content.Context;
 import android.content.res.Configuration;
 import android.view.LayoutInflater;
@@ -50,12 +58,11 @@
     @Before
     public void setUp() {
         final Context context = ApplicationProvider.getApplicationContext();
-        final int[] sampleResIds = new int[]{1, 2, 3, 4, 5, 6};
-        final Configuration[] configurations = createConfigurations(6);
+        final Configuration[] configurations = createConfigurations(PREVIEW_SAMPLE_RES_IDS.length);
         mTextReadingPreviewPreference = new TextReadingPreviewPreference(context);
         mPreviewPagerAdapter =
-                new PreviewPagerAdapter(context, /* isLayoutRtl= */ false, sampleResIds,
-                        configurations);
+                spy(new PreviewPagerAdapter(context, /* isLayoutRtl= */ false,
+                        PREVIEW_SAMPLE_RES_IDS, configurations));
         final LayoutInflater inflater = LayoutInflater.from(context);
         final View view =
                 inflater.inflate(mTextReadingPreviewPreference.getLayoutResource(),
@@ -87,7 +94,7 @@
 
     @Test
     public void setCurrentItem_success() {
-        final int currentItem = 3;
+        final int currentItem = 1;
         mTextReadingPreviewPreference.setPreviewAdapter(mPreviewPagerAdapter);
         mTextReadingPreviewPreference.onBindViewHolder(mHolder);
 
@@ -104,6 +111,24 @@
         mTextReadingPreviewPreference.setCurrentItem(currentItem);
     }
 
+    @Test(expected = NullPointerException.class)
+    public void updatePagerWithoutPreviewAdapter_throwNPE() {
+        final int index = 1;
+
+        mTextReadingPreviewPreference.notifyPreviewPagerChanged(index);
+    }
+
+    @Test
+    public void notifyPreviewPager_setPreviewLayer() {
+        final int index = 2;
+        mTextReadingPreviewPreference.setPreviewAdapter(mPreviewPagerAdapter);
+        mTextReadingPreviewPreference.onBindViewHolder(mHolder);
+
+        mTextReadingPreviewPreference.notifyPreviewPagerChanged(index);
+
+        verify(mPreviewPagerAdapter).setPreviewLayer(eq(index), anyInt(), anyInt(), anyBoolean());
+    }
+
     private static Configuration[] createConfigurations(int count) {
         final Configuration[] configurations = new Configuration[count];
         for (int i = 0; i < count; i++) {