Merge "Create tooltip for notifying auto-adding the font scaling tile" into udc-dev
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1f1ecc8..6062046 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -4595,6 +4595,8 @@
     <string name="accessibility_one_handed_mode_auto_added_qs_tooltip_content">One-handed mode added to Quick Settings. Swipe down to turn it on or off anytime.</string>
     <!-- Used in the One-hand mode settings to show quick settings tooltip. [CHAR LIMIT=NONE] -->
     <string name="accessibility_one_handed_mode_qs_tooltip_content">You can also add one-handed mode to Quick Settings from the top of your screen</string>
+    <!-- Used in the font size settings to show quick settings tooltip for auto-added feature. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_font_scaling_auto_added_qs_tooltip_content">Font size added to Quick Settings. Swipe down to change the font size anytime.</string>
     <!-- Used in the accessibility action for accessibility quick settings tooltip to dismiss. [CHAR LIMIT=NONE] -->
     <string name="accessibility_quick_settings_tooltip_dismiss">Dismiss</string>
     <!-- Used in the Color correction settings screen to control turning on/off the feature entirely [CHAR LIMIT=60] -->
diff --git a/src/com/android/settings/accessibility/PreviewSizeSeekBarController.java b/src/com/android/settings/accessibility/PreviewSizeSeekBarController.java
index 851797e..089dc7b 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 @@
  * 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<? extends Number> mSizeData;
+    private static final String KEY_SAVED_QS_TOOLTIP_RESHOW = "qs_tooltip_reshow";
     private boolean mSeekByTouch;
     private Optional<ProgressInteractionListener> 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 @@
 
                     if (!mSeekByTouch) {
                         interactionListener.onProgressChanged();
+                        onProgressFinalized();
                     }
                 }
 
@@ -67,6 +83,7 @@
                     mSeekByTouch = false;
 
                     mInteractionListener.ifPresent(ProgressInteractionListener::onEndTrackingTouch);
+                    onProgressFinalized();
                 }
             };
 
@@ -74,6 +91,30 @@
             @NonNull PreviewSizeData<? extends Number> 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 @@
 
         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 @@
         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 b35a5fe..97a9071 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 @@
         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 5d10116..6300bd3 100644
--- a/src/com/android/settings/widget/LabeledSeekBarPreference.java
+++ b/src/com/android/settings/widget/LabeledSeekBarPreference.java
@@ -63,6 +63,8 @@
     private OnPreferenceChangeListener mStopListener;
     private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener;
 
+    private SeekBar mSeekBar;
+
     public LabeledSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr,
             int defStyleRes) {
 
@@ -104,6 +106,10 @@
                 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 @@
         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 52ccb37..6b0f5c0 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 @@
 @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 @@
 
         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;
+        }
+    }
 }