Implement Tooltips widget for QS panel

- Show the illustration and QS tile label text
- Dynamic update popup window width by content
- Support the auto close timer API

Bug: 210353709
Test: make RunSettingsRoboTests ROBOTEST_FILTER=AccessibilityQuickSettingsTooltipWindowTest
Change-Id: I8e0d3ff4ef6a48a54ef1e80724002d2cd28d7ac3
This commit is contained in:
menghanli
2022-01-10 11:51:22 +08:00
committed by Menghan Li
parent 0fd5007620
commit e9f9985e95
7 changed files with 406 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:top="8dp">
<shape
android:shape="rectangle">
<solid android:color="#BCEDDF"/>
<corners android:radius="28dp" />
<padding android:top="8dp" />
</shape>
</item>
<item
android:width="12dp"
android:height="12dp"
android:gravity="top|center_horizontal"
android:top="-6dp">
<rotate android:fromDegrees="45">
<shape android:shape="rectangle">
<solid android:color="#BCEDDF"/>
</shape>
</rotate>
</item>
</layer-list>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="181dp"
android:height="86dp"
android:viewportWidth="181"
android:viewportHeight="86">
<group>
<clip-path
android:pathData="M0,0h181v86h-181z"/>
<path
android:pathData="M17.388,4.007L162.381,4.007A13,13 0,0 1,175.381 17.007L175.381,323.989A13,13 0,0 1,162.381 336.989L17.388,336.989A13,13 0,0 1,4.388 323.989L4.388,17.007A13,13 0,0 1,17.388 4.007z"
android:strokeWidth="6"
android:fillColor="#ffffff"
android:strokeColor="#EDEDED"/>
</group>
<path
android:pathData="M21.548,23.014L158.221,23.014A3.078,3.078 0,0 1,161.299 26.092L161.299,26.092A3.078,3.078 0,0 1,158.221 29.17L21.548,29.17A3.078,3.078 0,0 1,18.469 26.092L18.469,26.092A3.078,3.078 0,0 1,21.548 23.014z"
android:fillColor="#EDEDED"/>
<path
android:pathData="M24.469,43.946L78.959,43.946A6,6 0,0 1,84.959 49.946L84.959,68.728A6,6 0,0 1,78.959 74.728L24.469,74.728A6,6 0,0 1,18.469 68.728L18.469,49.946A6,6 0,0 1,24.469 43.946z"
android:fillColor="#EDEDED"/>
<path
android:pathData="M100.81,43.946L155.299,43.946A6,6 0,0 1,161.299 49.946L161.299,68.728A6,6 0,0 1,155.299 74.728L100.81,74.728A6,6 0,0 1,94.81 68.728L94.81,49.946A6,6 0,0 1,100.81 43.946z"
android:fillColor="#E0DCDC"/>
<path
android:pathData="M100.81,43.946L155.299,43.946A6,6 0,0 1,161.299 49.946L161.299,68.728A6,6 0,0 1,155.299 74.728L100.81,74.728A6,6 0,0 1,94.81 68.728L94.81,49.946A6,6 0,0 1,100.81 43.946z"
android:fillColor="#797272"/>
<path
android:pathData="M104.197,55.027L110.047,55.027A2,2 0,0 1,112.047 57.027L112.047,62.878A2,2 0,0 1,110.047 64.878L104.197,64.878A2,2 0,0 1,102.197 62.878L102.197,57.027A2,2 0,0 1,104.197 55.027z"
android:fillColor="#ffffff"/>
<path
android:pathData="M117.588,53.796L139.751,53.796A1.847,1.847 0,0 1,141.598 55.643L141.598,55.643A1.847,1.847 0,0 1,139.751 57.49L117.588,57.49A1.847,1.847 0,0 1,115.741 55.643L115.741,55.643A1.847,1.847 0,0 1,117.588 53.796z"
android:fillColor="#ffffff"/>
<path
android:pathData="M117.588,62.415L152.064,62.415A1.847,1.847 0,0 1,153.911 64.262L153.911,64.262A1.847,1.847 0,0 1,152.064 66.109L117.588,66.109A1.847,1.847 0,0 1,115.741 64.262L115.741,64.262A1.847,1.847 0,0 1,117.588 62.415z"
android:fillColor="#C8C5C5"/>
</vector>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:background="@drawable/accessibility_qs_tooltips_background">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="@dimen/accessibility_qs_tooltips_margin_top"
android:src="@drawable/accessibility_qs_tooltips_illustration"
android:layout_gravity="center_horizontal"
android:contentDescription="@null" />
<TextView
android:id="@+id/qs_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/accessibility_qs_tooltips_margin"
android:textColor="@android:color/black"
android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
android:textSize="16sp" />
</LinearLayout>

View File

@@ -296,6 +296,10 @@
<dimen name="accessibility_icon_size">32dp</dimen>
<dimen name="accessibility_icon_foreground_size">18dp</dimen>
<!-- Accessibility quick settings tooltips -->
<dimen name="accessibility_qs_tooltips_margin">20dp</dimen>
<dimen name="accessibility_qs_tooltips_margin_top">27dp</dimen>
<!-- Restricted icon in switch bar -->
<dimen name="restricted_icon_margin_end">16dp</dimen>
<!-- Restricted icon size in switch bar -->

View File

@@ -5581,6 +5581,8 @@
<string name="accessibility_service_primary_switch_title">Use <xliff:g id="accessibility_app_name" example="TalkBack">%1$s</xliff:g></string>
<!-- Used in the accessibility service settings to open the activity. [CHAR LIMIT=NONE] -->
<string name="accessibility_service_primary_open_title">Open <xliff:g id="accessibility_app_name" example="TalkBack">%1$s</xliff:g></string>
<!-- Used in the accessibility service settings to show quick settings tooltips. [CHAR LIMIT=NONE] -->
<string name="accessibility_service_quick_settings_tooltips_content">Swipe down to quickly turn <xliff:g id="accessibility_app_name" example="TalkBack">%1$s</xliff:g> on or off in quick settings</string>
<!-- Used in the Color correction settings screen to control turning on/off the feature entirely [CHAR LIMIT=60] -->
<string name="accessibility_daltonizer_primary_switch_title">Use color correction</string>
<!-- Title for accessibility shortcut preference for color correction. [CHAR LIMIT=60] -->

View File

@@ -0,0 +1,154 @@
/*
* 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 android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.ColorDrawable;
import android.os.Handler;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.TextView;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
/**
* UI container for the accessibility quick settings tooltip.
*
* <p> 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.</p>
*/
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.
*
* <p> The system will attempt to close popup window to the target duration of the threads if
* close delay time is positive number. </p>
*
* @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.
*
* <p> Remove any pending posts of callbacks and sent messages for closing popup window. </p>
*/
@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.
*
* <p> The system will attempt to close popup window to the target duration of the threads if
* close delay time is positive number. </p>
*
* @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;
}
}

View File

@@ -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();
}
}