• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.app.Activity;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.os.Handler;
23 import android.widget.SeekBar;
24 
25 import androidx.annotation.NonNull;
26 import androidx.preference.PreferenceScreen;
27 
28 import com.android.settings.R;
29 import com.android.settings.core.BasePreferenceController;
30 import com.android.settingslib.core.lifecycle.LifecycleObserver;
31 import com.android.settingslib.core.lifecycle.events.OnDestroy;
32 import com.android.settingslib.core.lifecycle.events.OnStart;
33 import com.android.settingslib.core.lifecycle.events.OnStop;
34 
35 import com.google.android.setupcompat.util.WizardManagerHelper;
36 
37 import java.util.Optional;
38 
39 /**
40  * The controller of {@link AccessibilitySeekBarPreference} that listens to display size and font
41  * size settings changes and updates preview size threshold smoothly.
42  */
43 abstract class PreviewSizeSeekBarController extends BasePreferenceController implements
44         TextReadingResetController.ResetStateListener, LifecycleObserver, OnStart, OnStop,
45         OnDestroy {
46     private final PreviewSizeData<? extends Number> mSizeData;
47     private boolean mSeekByTouch;
48     private Optional<ProgressInteractionListener> mInteractionListener = Optional.empty();
49     private AccessibilitySeekBarPreference mSeekBarPreference;
50     private int mLastProgress;
51     private final Handler mHandler;
52 
53     private String[] mStateLabels = null;
54 
55     private final SeekBar.OnSeekBarChangeListener mSeekBarChangeListener =
56             new SeekBar.OnSeekBarChangeListener() {
57                 @Override
58                 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
59                     setSeekbarStateDescription(progress);
60 
61                     if (mInteractionListener.isEmpty()) {
62                         return;
63                     }
64 
65                     final ProgressInteractionListener interactionListener =
66                             mInteractionListener.get();
67                     // Avoid timing issues to update the corresponding preview fail when clicking
68                     // the increase/decrease button.
69                     seekBar.post(interactionListener::notifyPreferenceChanged);
70 
71                     if (!mSeekByTouch) {
72                         interactionListener.onProgressChanged();
73                         onProgressFinalized();
74                     }
75                 }
76 
77                 @Override
78                 public void onStartTrackingTouch(SeekBar seekBar) {
79                     mSeekByTouch = true;
80                 }
81 
82                 @Override
83                 public void onStopTrackingTouch(SeekBar seekBar) {
84                     mSeekByTouch = false;
85 
86                     mInteractionListener.ifPresent(ProgressInteractionListener::onEndTrackingTouch);
87                     onProgressFinalized();
88                 }
89             };
90 
PreviewSizeSeekBarController(Context context, String preferenceKey, @NonNull PreviewSizeData<? extends Number> sizeData)91     PreviewSizeSeekBarController(Context context, String preferenceKey,
92             @NonNull PreviewSizeData<? extends Number> sizeData) {
93         super(context, preferenceKey);
94         mSizeData = sizeData;
95         mHandler = new Handler(context.getMainLooper());
96     }
97 
98     @Override
onStart()99     public void onStart() {
100         if (mSeekBarPreference.getNeedsQSTooltipReshow()) {
101             mHandler.post(this::showQuickSettingsTooltipIfNeeded);
102         }
103     }
104 
105     @Override
onStop()106     public void onStop() {
107         // all the messages/callbacks will be removed.
108         mHandler.removeCallbacksAndMessages(null);
109     }
110 
111     @Override
onDestroy()112     public void onDestroy() {
113         mSeekBarPreference.dismissTooltip();
114     }
115 
setInteractionListener(ProgressInteractionListener interactionListener)116     void setInteractionListener(ProgressInteractionListener interactionListener) {
117         mInteractionListener = Optional.ofNullable(interactionListener);
118     }
119 
120     @Override
getAvailabilityStatus()121     public int getAvailabilityStatus() {
122         return AVAILABLE;
123     }
124 
125     @Override
displayPreference(PreferenceScreen screen)126     public void displayPreference(PreferenceScreen screen) {
127         super.displayPreference(screen);
128 
129         final int dataSize = mSizeData.getValues().size();
130         final int initialIndex = mSizeData.getInitialIndex();
131         mLastProgress = initialIndex;
132         mSeekBarPreference = screen.findPreference(getPreferenceKey());
133         mSeekBarPreference.setMax(dataSize - 1);
134         mSeekBarPreference.setProgress(initialIndex);
135         mSeekBarPreference.setContinuousUpdates(true);
136         mSeekBarPreference.setOnSeekBarChangeListener(mSeekBarChangeListener);
137         setSeekbarStateDescription(mSeekBarPreference.getProgress());
138     }
139 
140     @Override
resetState()141     public void resetState() {
142         final int defaultProgress = mSizeData.getValues().indexOf(mSizeData.getDefaultValue());
143         mSeekBarPreference.setProgress(defaultProgress);
144 
145         // Immediately take the effect of updating the progress to avoid waiting for receiving
146         // the event to delay update.
147         mInteractionListener.ifPresent(ProgressInteractionListener::onProgressChanged);
148     }
149 
150     /**
151      * Stores the String array we would like to use for describing the state of seekbar progress
152      * and updates the state description with current progress.
153      *
154      * @param labels The state descriptions to be announced for each progress.
155      */
setProgressStateLabels(String[] labels)156     public void setProgressStateLabels(String[] labels) {
157         mStateLabels = labels;
158         if (mStateLabels == null) {
159             return;
160         }
161         updateState(mSeekBarPreference);
162     }
163 
164     /**
165      * Sets the state of seekbar based on current progress. The progress of seekbar is
166      * corresponding to the index of the string array. If the progress is larger than or equals
167      * to the length of the array, the state description is set to an empty string.
168      */
setSeekbarStateDescription(int index)169     private void setSeekbarStateDescription(int index) {
170         if (mStateLabels == null) {
171             return;
172         }
173         mSeekBarPreference.setSeekBarStateDescription(
174                 (index < mStateLabels.length)
175                         ? mStateLabels[index] : "");
176     }
177 
178     private void onProgressFinalized() {
179         // Using progress in SeekBarPreference since the progresses in
180         // SeekBarPreference and seekbar are not always the same.
181         // See {@link androidx.preference.Preference#callChangeListener(Object)}
182         int seekBarPreferenceProgress = mSeekBarPreference.getProgress();
183         if (seekBarPreferenceProgress != mLastProgress) {
184             showQuickSettingsTooltipIfNeeded();
185             mLastProgress = seekBarPreferenceProgress;
186         }
187     }
188 
189     private void showQuickSettingsTooltipIfNeeded() {
190         final ComponentName tileComponentName = getTileComponentName();
191         if (tileComponentName == null) {
192             // Returns if no tile service assigned.
193             return;
194         }
195 
196         if (mContext instanceof Activity
197                 && WizardManagerHelper.isAnySetupWizard(((Activity) mContext).getIntent())) {
198             // Don't show QuickSettingsTooltip in Setup Wizard
199             return;
200         }
201 
202         if (!mSeekBarPreference.getNeedsQSTooltipReshow()
203                 && AccessibilityQuickSettingUtils.hasValueInSharedPreferences(
204                 mContext, tileComponentName)) {
205             // Returns if quick settings tooltip only show once.
206             return;
207         }
208 
209         // TODO (287728819): Move tooltip showing to SystemUI
210         // Since the lifecycle of controller is independent of that of the preference, doing
211         // null check on seekbar is a temporary solution for the case that seekbar view
212         // is not ready when we would like to show the tooltip.  If the seekbar is not ready,
213         // we give up showing the tooltip and also do not reshow it in the future.
214         if (mSeekBarPreference.getSeekbar() != null) {
215             final AccessibilityQuickSettingsTooltipWindow tooltipWindow =
216                     mSeekBarPreference.createTooltipWindow();
217             tooltipWindow.setup(getTileTooltipContent(),
218                     R.drawable.accessibility_auto_added_qs_tooltip_illustration);
219             tooltipWindow.showAtTopCenter(mSeekBarPreference.getSeekbar());
220         }
221         AccessibilityQuickSettingUtils.optInValueToSharedPreferences(mContext,
222                 tileComponentName);
223         mSeekBarPreference.setNeedsQSTooltipReshow(false);
224     }
225 
226     /** Returns the accessibility Quick Settings tile component name. */
227     abstract ComponentName getTileComponentName();
228 
229     /** Returns accessibility Quick Settings tile tooltip content. */
230     abstract CharSequence getTileTooltipContent();
231 
232 
233     /**
234      * Interface for callbacks when users interact with the seek bar.
235      */
236     interface ProgressInteractionListener {
237 
238         /**
239          * Called when the progress is changed.
240          */
241         void notifyPreferenceChanged();
242 
243         /**
244          * Called when the progress is changed without tracking touch.
245          */
246         void onProgressChanged();
247 
248         /**
249          * Called when the seek bar is end tracking.
250          */
251         void onEndTrackingTouch();
252     }
253 }
254