1 /*
2  * Copyright (C) 2021 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.systemui.accessibility.floatingmenu;
18 
19 import static android.util.TypedValue.COMPLEX_UNIT_PX;
20 import static android.view.View.MeasureSpec.AT_MOST;
21 import static android.view.View.MeasureSpec.UNSPECIFIED;
22 
23 import android.annotation.UiContext;
24 import android.content.Context;
25 import android.content.res.Configuration;
26 import android.content.res.Resources;
27 import android.graphics.CornerPathEffect;
28 import android.graphics.Paint;
29 import android.graphics.PixelFormat;
30 import android.graphics.Rect;
31 import android.graphics.drawable.GradientDrawable;
32 import android.graphics.drawable.ShapeDrawable;
33 import android.os.Bundle;
34 import android.text.method.MovementMethod;
35 import android.util.DisplayMetrics;
36 import android.view.Gravity;
37 import android.view.LayoutInflater;
38 import android.view.MotionEvent;
39 import android.view.View;
40 import android.view.ViewGroup;
41 import android.view.WindowManager;
42 import android.view.accessibility.AccessibilityNodeInfo;
43 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
44 import android.widget.FrameLayout;
45 import android.widget.LinearLayout;
46 import android.widget.TextView;
47 
48 import com.android.settingslib.Utils;
49 import com.android.systemui.R;
50 import com.android.systemui.recents.TriangleShape;
51 
52 /**
53  * Base tooltip view that shows the information about the operation of the
54  * Accessibility floating menu. In addition, the anchor view is only for {@link
55  * AccessibilityFloatingMenuView}, it should be more suited for displaying one-off menus to avoid
56  * the performance hit for the extra window.
57  */
58 class BaseTooltipView extends FrameLayout {
59     private int mFontSize;
60     private int mTextViewMargin;
61     private int mTextViewPadding;
62     private int mTextViewCornerRadius;
63     private int mArrowMargin;
64     private int mArrowWidth;
65     private int mArrowHeight;
66     private int mArrowCornerRadius;
67     private int mScreenWidth;
68     private boolean mIsShowing;
69     private TextView mTextView;
70     private final WindowManager.LayoutParams mCurrentLayoutParams;
71     private final WindowManager mWindowManager;
72     private final AccessibilityFloatingMenuView mAnchorView;
73 
BaseTooltipView(@iContext Context context, AccessibilityFloatingMenuView anchorView)74     BaseTooltipView(@UiContext Context context, AccessibilityFloatingMenuView anchorView) {
75         super(context);
76         mWindowManager = context.getSystemService(WindowManager.class);
77         mAnchorView = anchorView;
78         mCurrentLayoutParams = createDefaultLayoutParams();
79 
80         initViews();
81     }
82 
83     @Override
onConfigurationChanged(Configuration newConfig)84     protected void onConfigurationChanged(Configuration newConfig) {
85         super.onConfigurationChanged(newConfig);
86 
87         mAnchorView.onConfigurationChanged(newConfig);
88         updateTooltipView();
89 
90         mWindowManager.updateViewLayout(this, mCurrentLayoutParams);
91     }
92 
93     @Override
onTouchEvent(MotionEvent event)94     public boolean onTouchEvent(MotionEvent event) {
95         if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
96             hide();
97         }
98 
99         return super.onTouchEvent(event);
100     }
101 
102     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)103     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
104         super.onInitializeAccessibilityNodeInfo(info);
105 
106         info.addAction(AccessibilityAction.ACTION_DISMISS);
107     }
108 
109     @Override
performAccessibilityAction(int action, Bundle arguments)110     public boolean performAccessibilityAction(int action, Bundle arguments) {
111         if (action == AccessibilityAction.ACTION_DISMISS.getId()) {
112             hide();
113             return true;
114         }
115 
116         return super.performAccessibilityAction(action, arguments);
117     }
118 
show()119     void show() {
120         if (isShowing()) {
121             return;
122         }
123 
124         mIsShowing = true;
125         updateTooltipView();
126 
127         mWindowManager.addView(this, mCurrentLayoutParams);
128     }
129 
hide()130     void hide() {
131         if (!isShowing()) {
132             return;
133         }
134 
135         mIsShowing = false;
136         mWindowManager.removeView(this);
137     }
138 
setDescription(CharSequence text)139     void setDescription(CharSequence text) {
140         mTextView.setText(text);
141     }
142 
setMovementMethod(MovementMethod movement)143     void setMovementMethod(MovementMethod movement) {
144         mTextView.setMovementMethod(movement);
145     }
146 
isShowing()147     private boolean isShowing() {
148         return mIsShowing;
149     }
150 
initViews()151     private void initViews() {
152         final View contentView =
153                 LayoutInflater.from(getContext()).inflate(
154                         R.layout.accessibility_floating_menu_tooltip, this, false);
155 
156         mTextView = contentView.findViewById(R.id.text);
157 
158         addView(contentView);
159     }
160 
createDefaultLayoutParams()161     private static WindowManager.LayoutParams createDefaultLayoutParams() {
162         final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
163                 WindowManager.LayoutParams.WRAP_CONTENT,
164                 WindowManager.LayoutParams.WRAP_CONTENT,
165                 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
166                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
167                         | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
168                 PixelFormat.TRANSLUCENT);
169         params.windowAnimations = android.R.style.Animation_Translucent;
170         params.gravity = Gravity.START | Gravity.TOP;
171 
172         return params;
173     }
174 
updateDimensions()175     private void updateDimensions() {
176         final Resources res = getResources();
177         final DisplayMetrics dm = res.getDisplayMetrics();
178         mScreenWidth = dm.widthPixels;
179         mArrowWidth =
180                 res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_arrow_width);
181         mArrowHeight =
182                 res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_arrow_height);
183         mArrowMargin =
184                 res.getDimensionPixelSize(
185                         R.dimen.accessibility_floating_tooltip_arrow_margin);
186         mArrowCornerRadius =
187                 res.getDimensionPixelSize(
188                         R.dimen.accessibility_floating_tooltip_arrow_corner_radius);
189         mFontSize =
190                 res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_font_size);
191         mTextViewMargin =
192                 res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_margin);
193         mTextViewPadding =
194                 res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_padding);
195         mTextViewCornerRadius =
196                 res.getDimensionPixelSize(
197                         R.dimen.accessibility_floating_tooltip_text_corner_radius);
198     }
199 
updateTooltipView()200     private void updateTooltipView() {
201         updateDimensions();
202         updateTextView();
203 
204         final Rect anchorViewLocation = mAnchorView.getWindowLocationOnScreen();
205         updateArrowWith(anchorViewLocation);
206         updateWidthWith(anchorViewLocation);
207         updateLocationWith(anchorViewLocation);
208     }
209 
updateTextView()210     private void updateTextView() {
211         mTextView.setTextSize(COMPLEX_UNIT_PX, mFontSize);
212         mTextView.setPadding(mTextViewPadding, mTextViewPadding, mTextViewPadding,
213                 mTextViewPadding);
214 
215         final GradientDrawable gradientDrawable = (GradientDrawable) mTextView.getBackground();
216         gradientDrawable.setCornerRadius(mTextViewCornerRadius);
217         gradientDrawable.setColor(Utils.getColorAttrDefaultColor(getContext(),
218                 com.android.internal.R.attr.colorAccentPrimary));
219     }
220 
updateArrowWith(Rect anchorViewLocation)221     private void updateArrowWith(Rect anchorViewLocation) {
222         final boolean isAnchorViewOnLeft = isAnchorViewOnLeft(anchorViewLocation);
223         final View arrowView = findViewById(isAnchorViewOnLeft
224                 ? R.id.arrow_left
225                 : R.id.arrow_right);
226         arrowView.setVisibility(VISIBLE);
227         drawArrow(arrowView, isAnchorViewOnLeft);
228 
229         final LinearLayout.LayoutParams layoutParams =
230                 (LinearLayout.LayoutParams) arrowView.getLayoutParams();
231         layoutParams.width = mArrowWidth;
232         layoutParams.height = mArrowHeight;
233 
234         final int leftMargin = isAnchorViewOnLeft ? 0 : mArrowMargin;
235         final int rightMargin = isAnchorViewOnLeft ? mArrowMargin : 0;
236         layoutParams.setMargins(leftMargin, 0, rightMargin, 0);
237         arrowView.setLayoutParams(layoutParams);
238     }
239 
updateWidthWith(Rect anchorViewLocation)240     private void updateWidthWith(Rect anchorViewLocation) {
241         final ViewGroup.LayoutParams layoutParams = mTextView.getLayoutParams();
242         layoutParams.width = getTextWidthWith(anchorViewLocation);
243         mTextView.setLayoutParams(layoutParams);
244     }
245 
updateLocationWith(Rect anchorViewLocation)246     private void updateLocationWith(Rect anchorViewLocation) {
247         mCurrentLayoutParams.x = isAnchorViewOnLeft(anchorViewLocation)
248                 ? anchorViewLocation.width()
249                 : mScreenWidth - getWindowWidthWith(anchorViewLocation)
250                         - anchorViewLocation.width();
251         mCurrentLayoutParams.y =
252                 anchorViewLocation.centerY() - (getTextHeightWith(anchorViewLocation) / 2);
253     }
254 
drawArrow(View view, boolean isPointingLeft)255     private void drawArrow(View view, boolean isPointingLeft) {
256         final ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
257         final TriangleShape triangleShape =
258                 TriangleShape.createHorizontal(layoutParams.width, layoutParams.height,
259                         isPointingLeft);
260         final ShapeDrawable arrowDrawable = new ShapeDrawable(triangleShape);
261         final Paint arrowPaint = arrowDrawable.getPaint();
262         arrowPaint.setColor(Utils.getColorAttrDefaultColor(getContext(),
263                 com.android.internal.R.attr.colorAccentPrimary));
264         final CornerPathEffect effect = new CornerPathEffect(mArrowCornerRadius);
265         arrowPaint.setPathEffect(effect);
266         view.setBackground(arrowDrawable);
267     }
268 
isAnchorViewOnLeft(Rect anchorViewLocation)269     private boolean isAnchorViewOnLeft(Rect anchorViewLocation) {
270         return anchorViewLocation.left < (mScreenWidth / 2);
271     }
272 
getTextWidthWith(Rect anchorViewLocation)273     private int getTextWidthWith(Rect anchorViewLocation) {
274         final int widthSpec =
275                 MeasureSpec.makeMeasureSpec(getAvailableTextWidthWith(anchorViewLocation), AT_MOST);
276         final int heightSpec =
277                 MeasureSpec.makeMeasureSpec(0, UNSPECIFIED);
278         mTextView.measure(widthSpec, heightSpec);
279         return mTextView.getMeasuredWidth();
280     }
281 
getTextHeightWith(Rect anchorViewLocation)282     private int getTextHeightWith(Rect anchorViewLocation) {
283         final int widthSpec =
284                 MeasureSpec.makeMeasureSpec(getAvailableTextWidthWith(anchorViewLocation), AT_MOST);
285         final int heightSpec =
286                 MeasureSpec.makeMeasureSpec(0, UNSPECIFIED);
287         mTextView.measure(widthSpec, heightSpec);
288         return mTextView.getMeasuredHeight();
289     }
290 
getAvailableTextWidthWith(Rect anchorViewLocation)291     private int getAvailableTextWidthWith(Rect anchorViewLocation) {
292         return mScreenWidth - anchorViewLocation.width() - mArrowWidth - mArrowMargin
293                 - mTextViewMargin;
294     }
295 
getWindowWidthWith(Rect anchorViewLocation)296     private int getWindowWidthWith(Rect anchorViewLocation) {
297         return getTextWidthWith(anchorViewLocation) + mArrowWidth + mArrowMargin;
298     }
299 }
300