1 /*
2  * Copyright (C) 2022 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.view.ViewGroup.LayoutParams.WRAP_CONTENT;
20 
21 import android.annotation.SuppressLint;
22 import android.content.ComponentCallbacks;
23 import android.content.Context;
24 import android.content.res.Configuration;
25 import android.graphics.PointF;
26 import android.graphics.Rect;
27 import android.graphics.drawable.GradientDrawable;
28 import android.view.ViewGroup;
29 import android.view.ViewTreeObserver;
30 import android.widget.FrameLayout;
31 
32 import androidx.annotation.NonNull;
33 import androidx.lifecycle.Observer;
34 import androidx.recyclerview.widget.DiffUtil;
35 import androidx.recyclerview.widget.LinearLayoutManager;
36 import androidx.recyclerview.widget.RecyclerView;
37 
38 import com.android.internal.accessibility.dialog.AccessibilityTarget;
39 import com.android.modules.expresslog.Counter;
40 import com.android.systemui.Flags;
41 import com.android.systemui.util.settings.SecureSettings;
42 
43 import java.util.ArrayList;
44 import java.util.Collections;
45 import java.util.List;
46 
47 /**
48  * The container view displays the accessibility features.
49  */
50 @SuppressLint("ViewConstructor")
51 class MenuView extends FrameLayout implements
52         ViewTreeObserver.OnComputeInternalInsetsListener, ComponentCallbacks {
53     private static final int INDEX_MENU_ITEM = 0;
54     private final List<AccessibilityTarget> mTargetFeatures = new ArrayList<>();
55     private final AccessibilityTargetAdapter mAdapter;
56     private final MenuViewModel mMenuViewModel;
57     private final Rect mBoundsInParent = new Rect();
58     private final RecyclerView mTargetFeaturesView;
59     private final ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater =
60             this::updateSystemGestureExcludeRects;
61     private final Observer<MenuFadeEffectInfo> mFadeEffectInfoObserver =
62             this::onMenuFadeEffectInfoChanged;
63     private final Observer<Boolean> mMoveToTuckedObserver = this::onMoveToTucked;
64     private final Observer<Position> mPercentagePositionObserver = this::onPercentagePosition;
65     private final Observer<Integer> mSizeTypeObserver = this::onSizeTypeChanged;
66     private final Observer<List<AccessibilityTarget>> mTargetFeaturesObserver =
67             this::onTargetFeaturesChanged;
68     private final MenuViewAppearance mMenuViewAppearance;
69 
70     private boolean mIsMoveToTucked;
71 
72     private final MenuAnimationController mMenuAnimationController;
73     private OnTargetFeaturesChangeListener mFeaturesChangeListener;
74     private OnMoveToTuckedListener mMoveToTuckedListener;
75     private SecureSettings mSecureSettings;
76 
MenuView(Context context, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance, SecureSettings secureSettings)77     MenuView(Context context, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance,
78             SecureSettings secureSettings) {
79         super(context);
80 
81         mMenuViewModel = menuViewModel;
82         mMenuViewAppearance = menuViewAppearance;
83         mSecureSettings = secureSettings;
84         mMenuAnimationController = new MenuAnimationController(this, menuViewAppearance);
85         mAdapter = new AccessibilityTargetAdapter(mTargetFeatures);
86         mTargetFeaturesView = new RecyclerView(context);
87         mTargetFeaturesView.setAdapter(mAdapter);
88         mTargetFeaturesView.setLayoutManager(new LinearLayoutManager(context));
89         setLayoutParams(new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
90         // Avoid drawing out of bounds of the parent view
91         setClipToOutline(true);
92 
93         loadLayoutResources();
94 
95         addView(mTargetFeaturesView);
96 
97         setClickable(false);
98         setFocusable(false);
99         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
100     }
101 
102     @Override
onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo)103     public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
104         inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
105         if (getVisibility() == VISIBLE) {
106             inoutInfo.touchableRegion.union(mBoundsInParent);
107         }
108     }
109 
110     @Override
onConfigurationChanged(@onNull Configuration newConfig)111     public void onConfigurationChanged(@NonNull Configuration newConfig) {
112         loadLayoutResources();
113 
114         mTargetFeaturesView.setOverScrollMode(mMenuViewAppearance.getMenuScrollMode());
115     }
116 
117     @Override
onLowMemory()118     public void onLowMemory() {
119         // Do nothing.
120     }
121 
122     @Override
onAttachedToWindow()123     protected void onAttachedToWindow() {
124         super.onAttachedToWindow();
125 
126         getContext().registerComponentCallbacks(this);
127     }
128 
129     @Override
onDetachedFromWindow()130     protected void onDetachedFromWindow() {
131         super.onDetachedFromWindow();
132 
133         getContext().unregisterComponentCallbacks(this);
134     }
135 
setOnTargetFeaturesChangeListener(OnTargetFeaturesChangeListener listener)136     void setOnTargetFeaturesChangeListener(OnTargetFeaturesChangeListener listener) {
137         mFeaturesChangeListener = listener;
138     }
139 
setMoveToTuckedListener(OnMoveToTuckedListener listener)140     void setMoveToTuckedListener(OnMoveToTuckedListener listener) {
141         mMoveToTuckedListener = listener;
142     }
143 
addOnItemTouchListenerToList(RecyclerView.OnItemTouchListener listener)144     void addOnItemTouchListenerToList(RecyclerView.OnItemTouchListener listener) {
145         mTargetFeaturesView.addOnItemTouchListener(listener);
146     }
147 
getMenuAnimationController()148     MenuAnimationController getMenuAnimationController() {
149         return mMenuAnimationController;
150     }
151 
152     @SuppressLint("NotifyDataSetChanged")
onItemSizeChanged()153     private void onItemSizeChanged() {
154         mAdapter.setItemPadding(mMenuViewAppearance.getMenuPadding());
155         mAdapter.setIconWidthHeight(mMenuViewAppearance.getMenuIconSize());
156         mAdapter.notifyDataSetChanged();
157     }
158 
onSizeChanged()159     private void onSizeChanged() {
160         mBoundsInParent.set(mBoundsInParent.left, mBoundsInParent.top,
161                 mBoundsInParent.left + mMenuViewAppearance.getMenuWidth(),
162                 mBoundsInParent.top + mMenuViewAppearance.getMenuHeight());
163 
164         final FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams();
165         layoutParams.height = mMenuViewAppearance.getMenuHeight();
166         setLayoutParams(layoutParams);
167     }
168 
onEdgeChangedIfNeeded()169     void onEdgeChangedIfNeeded() {
170         final Rect draggableBounds = mMenuViewAppearance.getMenuDraggableBounds();
171         if (getTranslationX() != draggableBounds.left
172                 && getTranslationX() != draggableBounds.right) {
173             return;
174         }
175 
176         onEdgeChanged();
177     }
178 
onEdgeChanged()179     void onEdgeChanged() {
180         final int[] insets = mMenuViewAppearance.getMenuInsets();
181         getContainerViewInsetLayer().setLayerInset(INDEX_MENU_ITEM, insets[0], insets[1], insets[2],
182                 insets[3]);
183 
184         final GradientDrawable gradientDrawable = getContainerViewGradient();
185         gradientDrawable.setStroke(mMenuViewAppearance.getMenuStrokeWidth(),
186                 mMenuViewAppearance.getMenuStrokeColor());
187         mMenuAnimationController.startRadiiAnimation(mMenuViewAppearance.getMenuRadii());
188     }
189 
setRadii(float[] radii)190     void setRadii(float[] radii) {
191         getContainerViewGradient().setCornerRadii(radii);
192     }
193 
onMoveToTucked(boolean isMoveToTucked)194     private void onMoveToTucked(boolean isMoveToTucked) {
195         mIsMoveToTucked = isMoveToTucked;
196 
197         onPositionChanged();
198     }
199 
onPercentagePosition(Position percentagePosition)200     private void onPercentagePosition(Position percentagePosition) {
201         mMenuViewAppearance.setPercentagePosition(percentagePosition);
202 
203         onPositionChanged();
204     }
205 
onPositionChanged()206     void onPositionChanged() {
207         onPositionChanged(/* animateMovement = */ false);
208     }
209 
onPositionChanged(boolean animateMovement)210     void onPositionChanged(boolean animateMovement) {
211         final PointF position;
212         if (isMoveToTucked()) {
213             position = mMenuAnimationController.getTuckedMenuPosition();
214         } else {
215             position = getMenuPosition();
216         }
217 
218         // We can skip animating if FAB is not visible
219         if (animateMovement && getVisibility() == VISIBLE) {
220             mMenuAnimationController.moveToPosition(position, /* animateMovement = */ true);
221             // onArrivalAtPosition() is called at the end of the animation.
222         } else {
223             mMenuAnimationController.moveToPosition(position);
224             onArrivalAtPosition(true); // no animation, so we call this immediately.
225         }
226     }
227 
onArrivalAtPosition(boolean moveToEdgeIfTucked)228     void onArrivalAtPosition(boolean moveToEdgeIfTucked) {
229         final PointF position = getMenuPosition();
230         onBoundsInParentChanged((int) position.x, (int) position.y);
231 
232         if (isMoveToTucked() && moveToEdgeIfTucked) {
233             mMenuAnimationController.moveToEdgeAndHide();
234         }
235     }
236 
237     @SuppressLint("NotifyDataSetChanged")
onSizeTypeChanged(int newSizeType)238     private void onSizeTypeChanged(int newSizeType) {
239         mMenuAnimationController.fadeInNowIfEnabled();
240 
241         mMenuViewAppearance.setSizeType(newSizeType);
242 
243         mAdapter.setItemPadding(mMenuViewAppearance.getMenuPadding());
244         mAdapter.setIconWidthHeight(mMenuViewAppearance.getMenuIconSize());
245         mAdapter.notifyDataSetChanged();
246 
247         onSizeChanged();
248         onEdgeChanged();
249         onPositionChanged();
250 
251         mMenuAnimationController.fadeOutIfEnabled();
252     }
253 
onTargetFeaturesChanged(List<AccessibilityTarget> newTargetFeatures)254     private void onTargetFeaturesChanged(List<AccessibilityTarget> newTargetFeatures) {
255         mMenuAnimationController.fadeInNowIfEnabled();
256 
257         final List<AccessibilityTarget> targetFeatures =
258                 Collections.unmodifiableList(mTargetFeatures.stream().toList());
259         mTargetFeatures.clear();
260         mTargetFeatures.addAll(newTargetFeatures);
261         mMenuViewAppearance.setTargetFeaturesSize(newTargetFeatures.size());
262         mTargetFeaturesView.setOverScrollMode(mMenuViewAppearance.getMenuScrollMode());
263         DiffUtil.calculateDiff(
264                 new MenuTargetsCallback(targetFeatures, newTargetFeatures)).dispatchUpdatesTo(
265                 mAdapter);
266 
267         onSizeChanged();
268         onEdgeChanged();
269         onPositionChanged();
270 
271         if (mFeaturesChangeListener != null) {
272             mFeaturesChangeListener.onChange(newTargetFeatures);
273         }
274 
275         mMenuAnimationController.fadeOutIfEnabled();
276     }
277 
onMenuFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo)278     private void onMenuFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo) {
279         mMenuAnimationController.updateOpacityWith(fadeEffectInfo.isFadeEffectEnabled(),
280                 fadeEffectInfo.getOpacity());
281     }
282 
getMenuDraggableBounds()283     Rect getMenuDraggableBounds() {
284         return mMenuViewAppearance.getMenuDraggableBounds();
285     }
286 
getMenuDraggableBoundsExcludeIme()287     Rect getMenuDraggableBoundsExcludeIme() {
288         return mMenuViewAppearance.getMenuDraggableBoundsExcludeIme();
289     }
290 
getMenuHeight()291     int getMenuHeight() {
292         return mMenuViewAppearance.getMenuHeight();
293     }
294 
getMenuWidth()295     int getMenuWidth() {
296         return mMenuViewAppearance.getMenuWidth();
297     }
298 
getMenuPosition()299     PointF getMenuPosition() {
300         return mMenuViewAppearance.getMenuPosition();
301     }
302 
getTargetFeaturesView()303     RecyclerView getTargetFeaturesView() {
304         return mTargetFeaturesView;
305     }
306 
persistPositionAndUpdateEdge(Position percentagePosition)307     void persistPositionAndUpdateEdge(Position percentagePosition) {
308         mMenuViewModel.updateMenuSavingPosition(percentagePosition);
309         mMenuViewAppearance.setPercentagePosition(percentagePosition);
310 
311         onEdgeChangedIfNeeded();
312     }
313 
isMoveToTucked()314     boolean isMoveToTucked() {
315         return mIsMoveToTucked;
316     }
317 
updateMenuMoveToTucked(boolean isMoveToTucked)318     void updateMenuMoveToTucked(boolean isMoveToTucked) {
319         mIsMoveToTucked = isMoveToTucked;
320         mMenuViewModel.updateMenuMoveToTucked(isMoveToTucked);
321         if (mMoveToTuckedListener != null) {
322             mMoveToTuckedListener.onMoveToTuckedChanged(isMoveToTucked);
323         }
324     }
325 
326 
327     /**
328      * Uses the touch events from the parent view to identify if users clicked the extra
329      * space of the menu view. If yes, will use the percentage position and update the
330      * translations of the menu view to meet the effect of moving out from the edge. It’s only
331      * used when the menu view is hidden to the screen edge.
332      *
333      * @param x the current x of the touch event from the parent {@link MenuViewLayer} of the
334      * {@link MenuView}.
335      * @param y the current y of the touch event from the parent {@link MenuViewLayer} of the
336      * {@link MenuView}.
337      * @return true if consume the touch event, otherwise false.
338      */
maybeMoveOutEdgeAndShow(int x, int y)339     boolean maybeMoveOutEdgeAndShow(int x, int y) {
340         // Utilizes the touch region of the parent view to implement that users could tap extra
341         // the space region to show the menu from the edge.
342         if (!isMoveToTucked() || !mBoundsInParent.contains(x, y)) {
343             return false;
344         }
345 
346         mMenuAnimationController.fadeInNowIfEnabled();
347 
348         mMenuAnimationController.moveOutEdgeAndShow();
349 
350         mMenuAnimationController.fadeOutIfEnabled();
351         return true;
352     }
353 
show()354     void show() {
355         mMenuViewModel.getPercentagePositionData().observeForever(mPercentagePositionObserver);
356         mMenuViewModel.getFadeEffectInfoData().observeForever(mFadeEffectInfoObserver);
357         mMenuViewModel.getTargetFeaturesData().observeForever(mTargetFeaturesObserver);
358         mMenuViewModel.getSizeTypeData().observeForever(mSizeTypeObserver);
359         mMenuViewModel.getMoveToTuckedData().observeForever(mMoveToTuckedObserver);
360         setVisibility(VISIBLE);
361         mMenuViewModel.registerObserversAndCallbacks();
362         getViewTreeObserver().addOnComputeInternalInsetsListener(this);
363         getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater);
364     }
365 
hide()366     void hide() {
367         setVisibility(GONE);
368         mBoundsInParent.setEmpty();
369         mMenuViewModel.getPercentagePositionData().removeObserver(mPercentagePositionObserver);
370         mMenuViewModel.getFadeEffectInfoData().removeObserver(mFadeEffectInfoObserver);
371         mMenuViewModel.getTargetFeaturesData().removeObserver(mTargetFeaturesObserver);
372         mMenuViewModel.getSizeTypeData().removeObserver(mSizeTypeObserver);
373         mMenuViewModel.getMoveToTuckedData().removeObserver(mMoveToTuckedObserver);
374         mMenuViewModel.unregisterObserversAndCallbacks();
375         getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
376         getViewTreeObserver().removeOnDrawListener(mSystemGestureExcludeUpdater);
377     }
378 
onDraggingStart()379     void onDraggingStart() {
380         final int[] insets = mMenuViewAppearance.getMenuMovingStateInsets();
381         getContainerViewInsetLayer().setLayerInset(INDEX_MENU_ITEM, insets[0], insets[1], insets[2],
382                 insets[3]);
383 
384         mMenuAnimationController.startRadiiAnimation(
385                 mMenuViewAppearance.getMenuMovingStateRadii());
386     }
387 
onBoundsInParentChanged(int newLeft, int newTop)388     void onBoundsInParentChanged(int newLeft, int newTop) {
389         mBoundsInParent.offsetTo(newLeft, newTop);
390     }
391 
loadLayoutResources()392     void loadLayoutResources() {
393         mMenuViewAppearance.update();
394 
395         mTargetFeaturesView.setContentDescription(mMenuViewAppearance.getContentDescription());
396         setBackground(mMenuViewAppearance.getMenuBackground());
397         setElevation(mMenuViewAppearance.getMenuElevation());
398         onItemSizeChanged();
399         onSizeChanged();
400         onEdgeChanged();
401         onPositionChanged();
402     }
403 
incrementTexMetric(String metric)404     void incrementTexMetric(String metric) {
405         if (!Flags.floatingMenuDragToEdit()) {
406             return;
407         }
408         Counter.logIncrement(metric);
409     }
410 
getContainerViewInsetLayer()411     private InstantInsetLayerDrawable getContainerViewInsetLayer() {
412         return (InstantInsetLayerDrawable) getBackground();
413     }
414 
getContainerViewGradient()415     private GradientDrawable getContainerViewGradient() {
416         return (GradientDrawable) getContainerViewInsetLayer().getDrawable(INDEX_MENU_ITEM);
417     }
418 
updateSystemGestureExcludeRects()419     private void updateSystemGestureExcludeRects() {
420         final ViewGroup parentView = (ViewGroup) getParent();
421         parentView.setSystemGestureExclusionRects(Collections.singletonList(mBoundsInParent));
422     }
423 
424     /**
425      * Interface definition for the {@link AccessibilityTarget} list changes.
426      */
427     interface OnTargetFeaturesChangeListener {
428         /**
429          * Called when the list of accessibility target features was updated. This will be
430          * invoked when the end of {@code onTargetFeaturesChanged}.
431          *
432          * @param newTargetFeatures the list related to the current accessibility features.
433          */
onChange(List<AccessibilityTarget> newTargetFeatures)434         void onChange(List<AccessibilityTarget> newTargetFeatures);
435     }
436 
437     /**
438      * Interface containing a callback for when MoveToTucked changes.
439      */
440     interface OnMoveToTuckedListener {
onMoveToTuckedChanged(boolean moveToTucked)441         void onMoveToTuckedChanged(boolean moveToTucked);
442     }
443 }
444