1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.accessibility;
18 
19 import android.content.Context;
20 import android.content.res.Configuration;
21 import android.content.res.TypedArray;
22 import android.os.SystemClock;
23 import android.util.Log;
24 import android.view.Choreographer;
25 import android.view.View;
26 
27 import androidx.annotation.NonNull;
28 import androidx.annotation.VisibleForTesting;
29 import androidx.preference.PreferenceScreen;
30 
31 import com.android.settings.R;
32 import com.android.settings.accessibility.TextReadingPreferenceFragment.EntryPoint;
33 import com.android.settings.core.BasePreferenceController;
34 import com.android.settings.core.instrumentation.SettingsStatsLog;
35 import com.android.settings.display.PreviewPagerAdapter;
36 import com.android.settings.widget.LabeledSeekBarPreference;
37 
38 import java.util.Objects;
39 
40 /**
41  * A {@link BasePreferenceController} for controlling the preview pager of the text and reading
42  * options.
43  */
44 class TextReadingPreviewController extends BasePreferenceController implements
45         PreviewSizeSeekBarController.ProgressInteractionListener {
46     private static final String TAG = "TextReadingPreviewCtrl";
47     private static final int LAYER_INITIAL_INDEX = 0;
48     private static final int FRAME_INITIAL_INDEX = 0;
49     private static final String PREVIEW_KEY = "preview";
50     private static final String FONT_SIZE_KEY = "font_size";
51     private static final String DISPLAY_SIZE_KEY = "display_size";
52     private static final long MIN_COMMIT_INTERVAL_MS = 800;
53     private static final long CHANGE_BY_SEEKBAR_DELAY_MS = 100;
54     private static final long CHANGE_BY_BUTTON_DELAY_MS = 300;
55     private final FontSizeData mFontSizeData;
56     private final DisplaySizeData mDisplaySizeData;
57     private int mLastFontProgress;
58     private int mLastDisplayProgress;
59     private long mLastCommitTime;
60     private TextReadingPreviewPreference mPreviewPreference;
61     private LabeledSeekBarPreference mFontSizePreference;
62     private LabeledSeekBarPreference mDisplaySizePreference;
63 
64     @EntryPoint
65     private int mEntryPoint;
66 
67     private final Choreographer.FrameCallback mCommit = f -> {
68         tryCommitFontSizeConfig();
69         tryCommitDisplaySizeConfig();
70 
71         mLastCommitTime = SystemClock.elapsedRealtime();
72     };
73 
TextReadingPreviewController(Context context, String preferenceKey, @NonNull FontSizeData fontSizeData, @NonNull DisplaySizeData displaySizeData)74     TextReadingPreviewController(Context context, String preferenceKey,
75             @NonNull FontSizeData fontSizeData, @NonNull DisplaySizeData displaySizeData) {
76         super(context, preferenceKey);
77         mFontSizeData = fontSizeData;
78         mDisplaySizeData = displaySizeData;
79     }
80 
81     @Override
getAvailabilityStatus()82     public int getAvailabilityStatus() {
83         return AVAILABLE;
84     }
85 
86     @Override
displayPreference(PreferenceScreen screen)87     public void displayPreference(PreferenceScreen screen) {
88         super.displayPreference(screen);
89 
90         mPreviewPreference = screen.findPreference(PREVIEW_KEY);
91 
92         mFontSizePreference = screen.findPreference(FONT_SIZE_KEY);
93         mDisplaySizePreference = screen.findPreference(DISPLAY_SIZE_KEY);
94         Objects.requireNonNull(mFontSizePreference,
95                 /* message= */ "Font size preference is null, the preview controller "
96                         + "couldn't get the info");
97         Objects.requireNonNull(mDisplaySizePreference,
98                 /* message= */ "Display size preference is null, the preview controller"
99                         + " couldn't get the info");
100 
101         mLastFontProgress = mFontSizeData.getInitialIndex();
102         mLastDisplayProgress = mDisplaySizeData.getInitialIndex();
103 
104         final Configuration origConfig = mContext.getResources().getConfiguration();
105         final boolean isLayoutRtl =
106                 origConfig.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
107         final int[] previewSamples = getPreviewSampleLayouts(mContext);
108         final PreviewPagerAdapter pagerAdapter = new PreviewPagerAdapter(mContext, isLayoutRtl,
109                 previewSamples, createConfig(origConfig));
110         mPreviewPreference.setPreviewAdapter(pagerAdapter);
111         mPreviewPreference.setCurrentItem(
112                 isLayoutRtl ? previewSamples.length - 1 : FRAME_INITIAL_INDEX);
113 
114         final int initialPagerIndex =
115                 mLastFontProgress * mDisplaySizeData.getValues().size() + mLastDisplayProgress;
116         mPreviewPreference.setLastLayerIndex(initialPagerIndex);
117         pagerAdapter.setPreviewLayer(initialPagerIndex, LAYER_INITIAL_INDEX,
118                 FRAME_INITIAL_INDEX, /* animate= */ false);
119     }
120 
121     @Override
notifyPreferenceChanged()122     public void notifyPreferenceChanged() {
123         mPreviewPreference.notifyPreviewPagerChanged(getPagerIndex());
124     }
125 
126     @Override
onProgressChanged()127     public void onProgressChanged() {
128         postCommitDelayed(CHANGE_BY_BUTTON_DELAY_MS);
129     }
130 
131     @Override
onEndTrackingTouch()132     public void onEndTrackingTouch() {
133         postCommitDelayed(CHANGE_BY_SEEKBAR_DELAY_MS);
134     }
135 
setCurrentItem(int index)136     void setCurrentItem(int index) {
137         mPreviewPreference.setCurrentItem(index);
138     }
139 
getCurrentItem()140     int getCurrentItem() {
141         return mPreviewPreference.getCurrentItem();
142     }
143 
144     /**
145      * The entry point is used for logging.
146      *
147      * @param entryPoint from which settings page
148      */
setEntryPoint(@ntryPoint int entryPoint)149     void setEntryPoint(@EntryPoint int entryPoint) {
150         mEntryPoint = entryPoint;
151     }
152 
153     /**
154      * Avoids the flicker when switching to the previous or next level.
155      *
156      * <p><br>[Flickering problem steps] commit()-> snapshot in framework(old screenshot) ->
157      * app update the preview -> snapshot(old screen) fade out</p>
158      *
159      * <p><br>To prevent flickering problem, we make sure that we update the local preview
160      * first and then we do the commit later. </p>
161      *
162      * <p><br><b>Note:</b> It doesn't matter that we use
163      * Choreographer or main thread handler since the delay time is longer
164      * than 1 frame. Use Choreographer to let developer understand it's a
165      * window update.</p>
166      *
167      * @param commitDelayMs the interval time after a action.
168      */
postCommitDelayed(long commitDelayMs)169     void postCommitDelayed(long commitDelayMs) {
170         if (SystemClock.elapsedRealtime() - mLastCommitTime < MIN_COMMIT_INTERVAL_MS) {
171             commitDelayMs += MIN_COMMIT_INTERVAL_MS;
172         }
173 
174         final Choreographer choreographer = Choreographer.getInstance();
175         choreographer.removeFrameCallback(mCommit);
176         choreographer.postFrameCallbackDelayed(mCommit, commitDelayMs);
177     }
178 
179     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
getPreviewSampleLayouts(Context context)180     static int[] getPreviewSampleLayouts(Context context) {
181         TypedArray previews = context.getResources().obtainTypedArray(
182                 R.array.config_text_reading_preview_samples);
183         int previewCount = previews.length();
184         int[] previewSamples = new int[previewCount];
185         for (int i = 0; i < previewCount; i++) {
186             previewSamples[i] = previews.getResourceId(i, R.layout.screen_zoom_preview_1);
187         }
188         previews.recycle();
189         return previewSamples;
190     }
191 
getPagerIndex()192     private int getPagerIndex() {
193         final int displayDataSize = mDisplaySizeData.getValues().size();
194         final int fontSizeProgress = mFontSizePreference.getProgress();
195         final int displaySizeProgress = mDisplaySizePreference.getProgress();
196 
197         // To be consistent with the {@link PreviewPagerAdapter#setPreviewLayer(int, int, int,
198         // boolean)} behavior, here also needs the same design. In addition, please also refer to
199         // the {@link #createConfig(Configuration)}.
200         return fontSizeProgress * displayDataSize + displaySizeProgress;
201     }
202 
tryCommitFontSizeConfig()203     private void tryCommitFontSizeConfig() {
204         final int fontProgress = mFontSizePreference.getProgress();
205         if (fontProgress != mLastFontProgress) {
206             mFontSizeData.commit(fontProgress);
207             mLastFontProgress = fontProgress;
208 
209             if (Log.isLoggable(TAG, Log.DEBUG)) {
210                 Log.d(TAG, "Font size: " + fontProgress);
211             }
212 
213             SettingsStatsLog.write(
214                     SettingsStatsLog.ACCESSIBILITY_TEXT_READING_OPTIONS_CHANGED,
215                     AccessibilityStatsLogUtils.convertToItemKeyName(mFontSizePreference.getKey()),
216                     fontProgress,
217                     AccessibilityStatsLogUtils.convertToEntryPoint(mEntryPoint));
218         }
219     }
220 
tryCommitDisplaySizeConfig()221     private void tryCommitDisplaySizeConfig() {
222         final int displayProgress = mDisplaySizePreference.getProgress();
223         if (displayProgress != mLastDisplayProgress) {
224             mDisplaySizeData.commit(displayProgress);
225             mLastDisplayProgress = displayProgress;
226 
227             if (Log.isLoggable(TAG, Log.DEBUG)) {
228                 Log.d(TAG, "Display size: " + displayProgress);
229             }
230 
231             SettingsStatsLog.write(
232                     SettingsStatsLog.ACCESSIBILITY_TEXT_READING_OPTIONS_CHANGED,
233                     AccessibilityStatsLogUtils.convertToItemKeyName(
234                             mDisplaySizePreference.getKey()),
235                     displayProgress,
236                     AccessibilityStatsLogUtils.convertToEntryPoint(mEntryPoint));
237         }
238     }
239 
createConfig(Configuration origConfig)240     private Configuration[] createConfig(Configuration origConfig) {
241         final int fontDataSize = mFontSizeData.getValues().size();
242         final int displayDataSize = mDisplaySizeData.getValues().size();
243         final int totalNum = fontDataSize * displayDataSize;
244         final Configuration[] configurations = new Configuration[totalNum];
245 
246         for (int i = 0; i < fontDataSize; ++i) {
247             for (int j = 0; j < displayDataSize; ++j) {
248                 final Configuration config = new Configuration(origConfig);
249                 config.fontScale = mFontSizeData.getValues().get(i);
250                 config.densityDpi = mDisplaySizeData.getValues().get(j);
251 
252                 configurations[i * displayDataSize + j] = config;
253             }
254         }
255 
256         return configurations;
257     }
258 }
259