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