From b132ada169da08202b31a32edb672cae15cb3937 Mon Sep 17 00:00:00 2001 From: Candice Lo Date: Mon, 20 Mar 2023 08:17:26 +0000 Subject: [PATCH] Create tooltip for notifying auto-adding the font scaling tile 1. Add string for the content of tooltip. 2. Show the tooltip if needed: the tooltip will only be shown once when users change the font size from the Settings page for the first time. 3. Since the layout shown on the screen will be recreated after font size changes, we need to save the state of the tooltip popup window to check if we need to reshow it in displayPreference. Bug: 269679768 Test: Manually - attach videos to the bug Test: make RunSettingsRoboTests ROBOTEST_FILTER=PreviewSizeSeekBarControllerTest Change-Id: I1b6c5fdbd74c7a868cf42bd21d2cdb1052c0bbe6 --- res/values/strings.xml | 2 + .../PreviewSizeSeekBarController.java | 87 +++++++++++- .../TextReadingPreferenceFragment.java | 28 +++- .../widget/LabeledSeekBarPreference.java | 14 +- .../PreviewSizeSeekBarControllerTest.java | 125 +++++++++++++++++- 5 files changed, 242 insertions(+), 14 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 3e96e1de6e5..4f6faf12d16 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4567,6 +4567,8 @@ One-handed mode added to Quick Settings. Swipe down to turn it on or off anytime. You can also add one-handed mode to Quick Settings from the top of your screen + + Font size added to Quick Settings. Swipe down to change the font size anytime. Dismiss diff --git a/src/com/android/settings/accessibility/PreviewSizeSeekBarController.java b/src/com/android/settings/accessibility/PreviewSizeSeekBarController.java index 851797ec2a9..089dc7be535 100644 --- a/src/com/android/settings/accessibility/PreviewSizeSeekBarController.java +++ b/src/com/android/settings/accessibility/PreviewSizeSeekBarController.java @@ -16,14 +16,22 @@ package com.android.settings.accessibility; +import android.content.ComponentName; import android.content.Context; +import android.os.Bundle; +import android.os.Handler; import android.widget.SeekBar; import androidx.annotation.NonNull; import androidx.preference.PreferenceScreen; +import com.android.settings.R; import com.android.settings.core.BasePreferenceController; import com.android.settings.widget.LabeledSeekBarPreference; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnCreate; +import com.android.settingslib.core.lifecycle.events.OnDestroy; +import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; import java.util.Optional; @@ -31,12 +39,19 @@ import java.util.Optional; * The controller of {@link LabeledSeekBarPreference} that listens to display size and font size * settings changes and updates preview size threshold smoothly. */ -class PreviewSizeSeekBarController extends BasePreferenceController implements - TextReadingResetController.ResetStateListener { +abstract class PreviewSizeSeekBarController extends BasePreferenceController implements + TextReadingResetController.ResetStateListener, LifecycleObserver, OnCreate, + OnDestroy, OnSaveInstanceState { private final PreviewSizeData mSizeData; + private static final String KEY_SAVED_QS_TOOLTIP_RESHOW = "qs_tooltip_reshow"; private boolean mSeekByTouch; private Optional mInteractionListener = Optional.empty(); private LabeledSeekBarPreference mSeekBarPreference; + private int mLastProgress; + private boolean mNeedsQSTooltipReshow = false; + private AccessibilityQuickSettingsTooltipWindow mTooltipWindow; + private final Handler mHandler; + private final SeekBar.OnSeekBarChangeListener mSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() { @@ -54,6 +69,7 @@ class PreviewSizeSeekBarController extends BasePreferenceController implements if (!mSeekByTouch) { interactionListener.onProgressChanged(); + onProgressFinalized(); } } @@ -67,6 +83,7 @@ class PreviewSizeSeekBarController extends BasePreferenceController implements mSeekByTouch = false; mInteractionListener.ifPresent(ProgressInteractionListener::onEndTrackingTouch); + onProgressFinalized(); } }; @@ -74,6 +91,30 @@ class PreviewSizeSeekBarController extends BasePreferenceController implements @NonNull PreviewSizeData sizeData) { super(context, preferenceKey); mSizeData = sizeData; + mHandler = new Handler(context.getMainLooper()); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + // Restore the tooltip. + if (savedInstanceState != null + && savedInstanceState.containsKey(KEY_SAVED_QS_TOOLTIP_RESHOW)) { + mNeedsQSTooltipReshow = savedInstanceState.getBoolean(KEY_SAVED_QS_TOOLTIP_RESHOW); + } + } + + @Override + public void onDestroy() { + // remove runnables in the queue. + mHandler.removeCallbacksAndMessages(null); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + final boolean isTooltipWindowShowing = mTooltipWindow != null && mTooltipWindow.isShowing(); + if (mNeedsQSTooltipReshow || isTooltipWindowShowing) { + outState.putBoolean(KEY_SAVED_QS_TOOLTIP_RESHOW, /* value= */ true); + } } void setInteractionListener(ProgressInteractionListener interactionListener) { @@ -91,11 +132,15 @@ class PreviewSizeSeekBarController extends BasePreferenceController implements final int dataSize = mSizeData.getValues().size(); final int initialIndex = mSizeData.getInitialIndex(); + mLastProgress = initialIndex; mSeekBarPreference = screen.findPreference(getPreferenceKey()); mSeekBarPreference.setMax(dataSize - 1); mSeekBarPreference.setProgress(initialIndex); mSeekBarPreference.setContinuousUpdates(true); mSeekBarPreference.setOnSeekBarChangeListener(mSeekBarChangeListener); + if (mNeedsQSTooltipReshow) { + mHandler.post(this::showQuickSettingsTooltipIfNeeded); + } } @Override @@ -108,6 +153,44 @@ class PreviewSizeSeekBarController extends BasePreferenceController implements mInteractionListener.ifPresent(ProgressInteractionListener::onProgressChanged); } + private void onProgressFinalized() { + // Using progress in SeekBarPreference since the progresses in + // SeekBarPreference and seekbar are not always the same. + // See {@link androidx.preference.Preference#callChangeListener(Object)} + int seekBarPreferenceProgress = mSeekBarPreference.getProgress(); + if (seekBarPreferenceProgress != mLastProgress) { + showQuickSettingsTooltipIfNeeded(); + mLastProgress = seekBarPreferenceProgress; + } + } + + private void showQuickSettingsTooltipIfNeeded() { + final ComponentName tileComponentName = getTileComponentName(); + if (tileComponentName == null) { + // Returns if no tile service assigned. + return; + } + + if (!mNeedsQSTooltipReshow && AccessibilityQuickSettingUtils.hasValueInSharedPreferences( + mContext, tileComponentName)) { + // Returns if quick settings tooltip only show once. + return; + } + + mTooltipWindow = new AccessibilityQuickSettingsTooltipWindow(mContext); + mTooltipWindow.setup(getTileTooltipContent(), + R.drawable.accessibility_auto_added_qs_tooltip_illustration); + mTooltipWindow.showAtTopCenter(mSeekBarPreference.getSeekbar()); + AccessibilityQuickSettingUtils.optInValueToSharedPreferences(mContext, tileComponentName); + mNeedsQSTooltipReshow = false; + } + + /** Returns the accessibility Quick Settings tile component name. */ + abstract ComponentName getTileComponentName(); + + /** Returns accessibility Quick Settings tile tooltip content. */ + abstract CharSequence getTileTooltipContent(); + /** * Interface for callbacks when users interact with the seek bar. diff --git a/src/com/android/settings/accessibility/TextReadingPreferenceFragment.java b/src/com/android/settings/accessibility/TextReadingPreferenceFragment.java index b35a5fe5a24..97a9071066e 100644 --- a/src/com/android/settings/accessibility/TextReadingPreferenceFragment.java +++ b/src/com/android/settings/accessibility/TextReadingPreferenceFragment.java @@ -16,11 +16,13 @@ package com.android.settings.accessibility; +import static com.android.internal.accessibility.AccessibilityShortcutController.FONT_SIZE_COMPONENT_NAME; import static com.android.settings.accessibility.TextReadingResetController.ResetStateListener; import android.app.Activity; import android.app.Dialog; import android.app.settings.SettingsEnums; +import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; @@ -156,12 +158,34 @@ public class TextReadingPreferenceFragment extends DashboardFragment { controllers.add(mPreviewController); final PreviewSizeSeekBarController fontSizeController = new PreviewSizeSeekBarController( - context, FONT_SIZE_KEY, fontSizeData); + context, FONT_SIZE_KEY, fontSizeData) { + @Override + ComponentName getTileComponentName() { + return FONT_SIZE_COMPONENT_NAME; + } + + @Override + CharSequence getTileTooltipContent() { + return context.getText( + R.string.accessibility_font_scaling_auto_added_qs_tooltip_content); + } + }; fontSizeController.setInteractionListener(mPreviewController); + getSettingsLifecycle().addObserver(fontSizeController); controllers.add(fontSizeController); final PreviewSizeSeekBarController displaySizeController = new PreviewSizeSeekBarController( - context, DISPLAY_SIZE_KEY, displaySizeData); + context, DISPLAY_SIZE_KEY, displaySizeData) { + @Override + ComponentName getTileComponentName() { + return null; + } + + @Override + CharSequence getTileTooltipContent() { + return null; + } + }; displaySizeController.setInteractionListener(mPreviewController); controllers.add(displaySizeController); diff --git a/src/com/android/settings/widget/LabeledSeekBarPreference.java b/src/com/android/settings/widget/LabeledSeekBarPreference.java index 5d1011634c7..6300bd3b318 100644 --- a/src/com/android/settings/widget/LabeledSeekBarPreference.java +++ b/src/com/android/settings/widget/LabeledSeekBarPreference.java @@ -63,6 +63,8 @@ public class LabeledSeekBarPreference extends SeekBarPreference { private OnPreferenceChangeListener mStopListener; private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener; + private SeekBar mSeekBar; + public LabeledSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { @@ -104,6 +106,10 @@ public class LabeledSeekBarPreference extends SeekBarPreference { com.android.internal.R.attr.seekBarPreferenceStyle), 0); } + public SeekBar getSeekbar() { + return mSeekBar; + } + @Override public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); @@ -133,19 +139,19 @@ public class LabeledSeekBarPreference extends SeekBarPreference { final boolean isValidTextResIdExist = mTextStartId > 0 || mTextEndId > 0; labelFrame.setVisibility(isValidTextResIdExist ? View.VISIBLE : View.GONE); - final SeekBar seekBar = (SeekBar) holder.findViewById(com.android.internal.R.id.seekbar); + mSeekBar = (SeekBar) holder.findViewById(com.android.internal.R.id.seekbar); if (mTickMarkId != 0) { final Drawable tickMark = getContext().getDrawable(mTickMarkId); - seekBar.setTickMark(tickMark); + mSeekBar.setTickMark(tickMark); } final ViewGroup iconStartFrame = (ViewGroup) holder.findViewById(R.id.icon_start_frame); final ImageView iconStartView = (ImageView) holder.findViewById(R.id.icon_start); - updateIconStartIfNeeded(iconStartFrame, iconStartView, seekBar); + updateIconStartIfNeeded(iconStartFrame, iconStartView, mSeekBar); final ViewGroup iconEndFrame = (ViewGroup) holder.findViewById(R.id.icon_end_frame); final ImageView iconEndView = (ImageView) holder.findViewById(R.id.icon_end); - updateIconEndIfNeeded(iconEndFrame, iconEndView, seekBar); + updateIconEndIfNeeded(iconEndFrame, iconEndView, mSeekBar); } public void setOnPreferenceChangeStopListener(OnPreferenceChangeListener listener) { diff --git a/tests/robotests/src/com/android/settings/accessibility/PreviewSizeSeekBarControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/PreviewSizeSeekBarControllerTest.java index 52ccb374ca6..6b0f5c00413 100644 --- a/tests/robotests/src/com/android/settings/accessibility/PreviewSizeSeekBarControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/PreviewSizeSeekBarControllerTest.java @@ -16,29 +16,45 @@ package com.android.settings.accessibility; +import static com.android.internal.accessibility.AccessibilityShortcutController.FONT_SIZE_COMPONENT_NAME; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.content.ComponentName; import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.widget.PopupWindow; import android.widget.SeekBar; +import androidx.preference.PreferenceManager; import androidx.preference.PreferenceScreen; +import androidx.preference.PreferenceViewHolder; import androidx.test.core.app.ApplicationProvider; +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.testutils.shadow.ShadowFragment; import com.android.settings.testutils.shadow.ShadowInteractionJankMonitor; import com.android.settings.widget.LabeledSeekBarPreference; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Answers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.Spy; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowApplication; /** * Tests for {@link PreviewSizeSeekBarController}. @@ -47,30 +63,67 @@ import org.robolectric.annotation.Config; @Config(shadows = {ShadowInteractionJankMonitor.class}) public class PreviewSizeSeekBarControllerTest { private static final String FONT_SIZE_KEY = "font_size"; + private static final String KEY_SAVED_QS_TOOLTIP_RESHOW = "qs_tooltip_reshow"; + @Spy private final Context mContext = ApplicationProvider.getApplicationContext(); private PreviewSizeSeekBarController mSeekBarController; private FontSizeData mFontSizeData; private LabeledSeekBarPreference mSeekBarPreference; - @Mock private PreferenceScreen mPreferenceScreen; + private TestFragment mFragment; + private PreferenceViewHolder mHolder; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private PreferenceManager mPreferenceManager; @Mock private PreviewSizeSeekBarController.ProgressInteractionListener mInteractionListener; + private static PopupWindow getLatestPopupWindow() { + final ShadowApplication shadowApplication = + Shadow.extract(ApplicationProvider.getApplicationContext()); + return shadowApplication.getLatestPopupWindow(); + } + @Before public void setUp() { MockitoAnnotations.initMocks(this); - mFontSizeData = new FontSizeData(mContext); - - mSeekBarController = - new PreviewSizeSeekBarController(mContext, FONT_SIZE_KEY, mFontSizeData); - + mContext.setTheme(R.style.Theme_AppCompat); + mFragment = spy(new TestFragment()); + when(mFragment.getPreferenceManager()).thenReturn(mPreferenceManager); + when(mFragment.getPreferenceManager().getContext()).thenReturn(mContext); + when(mFragment.getContext()).thenReturn(mContext); + mPreferenceScreen = spy(new PreferenceScreen(mContext, /* attrs= */ null)); + when(mPreferenceScreen.getPreferenceManager()).thenReturn(mPreferenceManager); + doReturn(mPreferenceScreen).when(mFragment).getPreferenceScreen(); mSeekBarPreference = spy(new LabeledSeekBarPreference(mContext, /* attrs= */ null)); + mSeekBarPreference.setKey(FONT_SIZE_KEY); + + LayoutInflater inflater = LayoutInflater.from(mContext); + mHolder = PreferenceViewHolder.createInstanceForTests(inflater.inflate( + R.layout.preference_labeled_slider, null)); + mSeekBarPreference.onBindViewHolder(mHolder); + when(mPreferenceScreen.findPreference(anyString())).thenReturn(mSeekBarPreference); + mFontSizeData = new FontSizeData(mContext); + mSeekBarController = + new PreviewSizeSeekBarController(mContext, FONT_SIZE_KEY, mFontSizeData) { + @Override + ComponentName getTileComponentName() { + return FONT_SIZE_COMPONENT_NAME; + } + + @Override + CharSequence getTileTooltipContent() { + return mContext.getText( + R.string.accessibility_font_scaling_auto_added_qs_tooltip_content); + } + }; mSeekBarController.setInteractionListener(mInteractionListener); + when(mPreferenceScreen.findPreference(mSeekBarController.getPreferenceKey())).thenReturn( + mSeekBarPreference); } @Test @@ -123,4 +176,64 @@ public class PreviewSizeSeekBarControllerTest { verify(mInteractionListener).notifyPreferenceChanged(); } + + @Test + public void onProgressChanged_showTooltipView() { + mSeekBarController.displayPreference(mPreferenceScreen); + + // Simulate changing the progress for the first time + int newProgress = (mSeekBarPreference.getProgress() != 0) ? 0 : mSeekBarPreference.getMax(); + mSeekBarPreference.setProgress(newProgress); + mSeekBarPreference.onProgressChanged(new SeekBar(mContext), + newProgress, + /* fromUser= */ false); + + assertThat(getLatestPopupWindow().isShowing()).isTrue(); + } + + @Test + public void onProgressChanged_tooltipViewHasBeenShown_notShowTooltipView() { + mSeekBarController.displayPreference(mPreferenceScreen); + // Simulate changing the progress for the first time + int newProgress = (mSeekBarPreference.getProgress() != 0) ? 0 : mSeekBarPreference.getMax(); + mSeekBarPreference.setProgress(newProgress); + mSeekBarPreference.onProgressChanged(new SeekBar(mContext), + newProgress, + /* fromUser= */ false); + getLatestPopupWindow().dismiss(); + + // Simulate progress changing for the second time + newProgress = (mSeekBarPreference.getProgress() != 0) ? 0 : mSeekBarPreference.getMax(); + mSeekBarPreference.setProgress(newProgress); + mSeekBarPreference.onProgressChanged(new SeekBar(mContext), + newProgress, + /* fromUser= */ false); + + assertThat(getLatestPopupWindow().isShowing()).isFalse(); + } + + @Test + @Config(shadows = ShadowFragment.class) + public void restoreValueFromSavedInstanceState_showTooltipView() { + final Bundle savedInstanceState = new Bundle(); + savedInstanceState.putBoolean(KEY_SAVED_QS_TOOLTIP_RESHOW, /* value= */ true); + mSeekBarController.onCreate(savedInstanceState); + + mSeekBarController.displayPreference(mPreferenceScreen); + + assertThat(getLatestPopupWindow().isShowing()).isTrue(); + } + + private static class TestFragment extends SettingsPreferenceFragment { + + @Override + protected boolean shouldSkipForInitialSUW() { + return false; + } + + @Override + public int getMetricsCategory() { + return 0; + } + } }