diff --git a/res/drawable/accessibility_qs_tooltips_background.xml b/res/drawable/accessibility_qs_tooltips_background.xml
new file mode 100644
index 00000000000..a704b2e25c2
--- /dev/null
+++ b/res/drawable/accessibility_qs_tooltips_background.xml
@@ -0,0 +1,39 @@
+
+
+
+
The popup window shows the information about the operation of the quick settings. In + * addition, the arrow is pointing to the top center of the device to display one-off menu within + * {@code mCloseDelayTimeMillis} time.
+ */ +public class AccessibilityQuickSettingsTooltipWindow extends PopupWindow { + + private final Context mContext; + private Handler mHandler; + private long mCloseDelayTimeMillis; + + public AccessibilityQuickSettingsTooltipWindow(Context context) { + super(context); + this.mContext = context; + } + + /** + * Sets up {@link #AccessibilityQuickSettingsTooltipWindow}'s layout and content. + * + * @param text text to be displayed + */ + public void setup(String text) { + this.setup(text, /* closeDelayTimeMillis= */ 0); + } + + /** + * Sets up {@link #AccessibilityQuickSettingsTooltipWindow}'s layout and content. + * + *The system will attempt to close popup window to the target duration of the threads if + * close delay time is positive number.
+ * + * @param text text to be displayed + * @param closeDelayTimeMillis how long the popup window be auto-closed + */ + public void setup(String text, long closeDelayTimeMillis) { + this.mCloseDelayTimeMillis = closeDelayTimeMillis; + + setBackgroundDrawable(new ColorDrawable(mContext.getColor(android.R.color.transparent))); + final LayoutInflater inflater = mContext.getSystemService(LayoutInflater.class); + final View popupView = + inflater.inflate(R.layout.accessibility_qs_tooltips, /* root= */ null); + setContentView(popupView); + final TextView textView = getContentView().findViewById(R.id.qs_content); + textView.setText(text); + + setWidth(getWindowWidthWith(textView)); + setHeight(LinearLayout.LayoutParams.WRAP_CONTENT); + setFocusable(/* focusable= */ true); + } + + /** + * Displays the content view in a popup window at the top and center position. + * + * @param targetView a target view to get the {@link View#getWindowToken()} token from. + */ + public void showAtTopCenter(View targetView) { + showAtLocation(targetView, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, 0); + } + + /** + * Disposes of the popup window. + * + *Remove any pending posts of callbacks and sent messages for closing popup window.
+ */ + @Override + public void dismiss() { + super.dismiss(); + if (mHandler != null) { + mHandler.removeCallbacksAndMessages(/* token= */ null); + } + } + + /** + * Displays the content view in a popup window at the specified location. + * + *The system will attempt to close popup window to the target duration of the threads if + * close delay time is positive number.
+ * + * @param parent a parent view to get the {@link android.view.View#getWindowToken()} token from + * @param gravity the gravity which controls the placement of the popup window + * @param x the popup's x location offset + * @param y the popup's y location offset + */ + @Override + public void showAtLocation(View parent, int gravity, int x, int y) { + super.showAtLocation(parent, gravity, x, y); + scheduleAutoCloseAction(); + } + + private void scheduleAutoCloseAction() { + if (mCloseDelayTimeMillis <= 0) { + return; + } + + if (mHandler == null) { + mHandler = new Handler(mContext.getMainLooper()); + } + mHandler.removeCallbacksAndMessages(/* token= */ null); + mHandler.postDelayed(this::dismiss, mCloseDelayTimeMillis); + } + + private int getWindowWidthWith(TextView textView) { + final int availableWindowWidth = getAvailableWindowWidth(); + final int widthSpec = + View.MeasureSpec.makeMeasureSpec(availableWindowWidth, View.MeasureSpec.AT_MOST); + final int heightSpec = + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + textView.measure(widthSpec, heightSpec); + return textView.getMeasuredWidth(); + } + + @VisibleForTesting + int getAvailableWindowWidth() { + final Resources res = mContext.getResources(); + final int padding = res.getDimensionPixelSize(R.dimen.accessibility_qs_tooltips_margin); + final int screenWidth = res.getDisplayMetrics().widthPixels; + return screenWidth - padding * 2; + } +} diff --git a/tests/robotests/src/com/android/settings/accessibility/AccessibilityQuickSettingsTooltipWindowTest.java b/tests/robotests/src/com/android/settings/accessibility/AccessibilityQuickSettingsTooltipWindowTest.java new file mode 100644 index 00000000000..f1e1121b78e --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/AccessibilityQuickSettingsTooltipWindowTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.accessibility; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.view.View; +import android.widget.PopupWindow; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowApplication; +import org.robolectric.shadows.ShadowLooper; + +/** Tests for {@link AccessibilityQuickSettingsTooltipWindow}. */ +@RunWith(RobolectricTestRunner.class) +public class AccessibilityQuickSettingsTooltipWindowTest { + + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Mock + private PopupWindow.OnDismissListener mMockOnDismissListener; + + private static final String TEST_PACKAGE_NAME = "com.test.package"; + private final Context mContext = ApplicationProvider.getApplicationContext(); + private AccessibilityQuickSettingsTooltipWindow mToolTipView; + private View mView; + + @Before + public void setUp() { + mToolTipView = new AccessibilityQuickSettingsTooltipWindow(mContext); + mView = new View(RuntimeEnvironment.application); + } + + @Test + public void initToolTipView_atMostAvailableTextWidth() { + final String quickSettingsTooltipsContent = mContext.getString( + R.string.accessibility_service_quick_settings_tooltips_content, TEST_PACKAGE_NAME); + mToolTipView.setup(quickSettingsTooltipsContent); + + final int getMaxWidth = mToolTipView.getAvailableWindowWidth(); + assertThat(mToolTipView.getWidth()).isAtMost(getMaxWidth); + } + + @Test + public void showToolTipView_success() { + mToolTipView.setup(TEST_PACKAGE_NAME); + assertThat(getLatestPopupWindow()).isNull(); + + mToolTipView.showAtTopCenter(mView); + + assertThat(getLatestPopupWindow()).isSameInstanceAs(mToolTipView); + } + + @Test + public void dismiss_toolTipViewShown_shouldInvokeCallbackAndNotShowing() { + mToolTipView.setup(TEST_PACKAGE_NAME); + mToolTipView.setOnDismissListener(mMockOnDismissListener); + mToolTipView.showAtTopCenter(mView); + + mToolTipView.dismiss(); + + verify(mMockOnDismissListener).onDismiss(); + assertThat(getLatestPopupWindow().isShowing()).isFalse(); + } + + @Test + public void waitAutoCloseDelayTime_toolTipViewShown_shouldInvokeCallbackAndNotShowing() { + mToolTipView.setup(TEST_PACKAGE_NAME, /* closeDelayTimeMillis= */ 1); + mToolTipView.setOnDismissListener(mMockOnDismissListener); + mToolTipView.showAtTopCenter(mView); + + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + verify(mMockOnDismissListener).onDismiss(); + assertThat(getLatestPopupWindow().isShowing()).isFalse(); + } + + private static PopupWindow getLatestPopupWindow() { + final ShadowApplication shadowApplication = Shadow.extract(RuntimeEnvironment.application); + return shadowApplication.getLatestPopupWindow(); + } +}