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