diff --git a/res/xml/accessibility_text_reading_options.xml b/res/xml/accessibility_text_reading_options.xml index f46c24e996f..167886bdd89 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"> + + 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 00000000000..cef20aac546 --- /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. + * + *


[Flickering problem steps] commit()-> snapshot in framework(old screenshot) -> + * app update the preview -> snapshot(old screen) fade out

+ * + *


To prevent flickering problem, we make sure that we update the local preview + * first and then we do the commit later.

+ * + *


Note: 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.

+ * + * @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 1b9cc4b5171..4b8ca39ed18 100644 --- a/src/com/android/settings/accessibility/TextReadingPreviewPreference.java +++ b/src/com/android/settings/accessibility/TextReadingPreviewPreference.java @@ -32,8 +32,9 @@ import com.android.settings.widget.DotsPageIndicator; /** * 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 @@ final class TextReadingPreviewPreference extends Preference { init(); } - TextReadingPreviewPreference(Context context, AttributeSet attrs) { + public TextReadingPreviewPreference(Context context, AttributeSet attrs) { super(context, attrs); init(); } @@ -120,4 +121,16 @@ final class TextReadingPreviewPreference extends Preference { 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 018be326d1b..693d574ff83 100644 --- a/src/com/android/settings/display/PreviewPagerAdapter.java +++ b/src/com/android/settings/display/PreviewPagerAdapter.java @@ -117,7 +117,15 @@ public class PreviewPagerAdapter extends PagerAdapter { 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 00000000000..b6305099c68 --- /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 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 createFakeDisplayData() { + final List 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 6b9395a335c..3dc82da5add 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 @@ public class TextReadingPreviewPreferenceTest { @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 @@ public class TextReadingPreviewPreferenceTest { @Test public void setCurrentItem_success() { - final int currentItem = 3; + final int currentItem = 1; mTextReadingPreviewPreference.setPreviewAdapter(mPreviewPagerAdapter); mTextReadingPreviewPreference.onBindViewHolder(mHolder); @@ -104,6 +111,24 @@ public class TextReadingPreviewPreferenceTest { 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++) {