Merge changes from topic "accessibility_floating_menu_widget" into sc-dev

* changes:
  Add new floating action menu for Accessibility targets. (5/n)
  Add new floating action menu for Accessibility targets. (4/n)
  Add new floating action menu for Accessibility targets. (3/n)
  Add new floating action menu for Accessibility targets. (2/n)
  Add new floating action menu for Accessibility targets. (1/n)
This commit is contained in:
TreeHugger Robot
2021-03-19 05:26:47 +00:00
committed by Android (Google) Code Review
14 changed files with 1709 additions and 3 deletions

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2021 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:bottomLeftRadius="@dimen/accessibility_floating_menu_small_single_radius"
android:bottomRightRadius="0dp"
android:topLeftRadius="@dimen/accessibility_floating_menu_small_single_radius"
android:topRightRadius="0dp"/>
<solid
android:color="@color/accessibility_floating_menu_background"/>
</shape>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2021 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="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/accessibility_floating_menu_padding"
android:paddingEnd="@dimen/accessibility_floating_menu_padding"
android:orientation="vertical"
android:gravity="center">
<View
android:id="@+id/icon_view"
android:layout_width="@dimen/accessibility_floating_menu_small_width_height"
android:layout_height="@dimen/accessibility_floating_menu_small_width_height"/>
<View
android:id="@+id/transparent_divider"
android:layout_width="match_parent"
android:layout_height="@dimen/accessibility_floating_menu_padding"/>
</LinearLayout>

View File

@@ -102,4 +102,6 @@
<color name="privacy_circle_camera">#81C995</color> <!-- g300 -->
<color name="privacy_circle_microphone_location">#FCAD70</color> <!--o300 -->
<!-- Accessibility floating menu -->
<color name="accessibility_floating_menu_background">#B3000000</color> <!-- 70% -->
</resources>

View File

@@ -274,4 +274,8 @@
<!-- TODO(b/178093014) Colors for privacy dialog. These should be changed to the new palette -->
<color name="privacy_circle_camera">#1E8E3E</color> <!-- g600 -->
<color name="privacy_circle_microphone_location">#E8710A</color> <!--o600 -->
<!-- Accessibility floating menu -->
<color name="accessibility_floating_menu_background">#CCFFFFFF</color> <!-- 80% -->
<color name="accessibility_floating_menu_stroke_dark">#26FFFFFF</color> <!-- 15% -->
</resources>

View File

@@ -1356,6 +1356,19 @@
<dimen name="people_space_image_radius">20dp</dimen>
<dimen name="people_space_widget_background_padding">6dp</dimen>
<!-- Accessibility floating menu -->
<dimen name="accessibility_floating_menu_elevation">5dp</dimen>
<dimen name="accessibility_floating_menu_stroke_width">1dp</dimen>
<dimen name="accessibility_floating_menu_stroke_inset">-2dp</dimen>
<dimen name="accessibility_floating_menu_margin">16dp</dimen>
<dimen name="accessibility_floating_menu_padding">6dp</dimen>
<dimen name="accessibility_floating_menu_small_width_height">36dp</dimen>
<dimen name="accessibility_floating_menu_small_single_radius">25dp</dimen>
<dimen name="accessibility_floating_menu_small_multiple_radius">20dp</dimen>
<dimen name="accessibility_floating_menu_large_width_height">56dp</dimen>
<dimen name="accessibility_floating_menu_large_single_radius">33dp</dimen>
<dimen name="accessibility_floating_menu_large_multiple_radius">35dp</dimen>
<dimen name="rounded_slider_height">44dp</dimen>
<!-- rounded_slider_height / 2 -->
<dimen name="rounded_slider_corner_radius">22dp</dimen>

View File

@@ -0,0 +1,163 @@
/*
* Copyright (C) 2021 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.systemui.accessibility.floatingmenu;
import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED;
import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_ICON_TYPE;
import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY;
import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE;
import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets;
import static com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.ShapeType;
import static com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.SizeType;
import android.content.Context;
import android.database.ContentObserver;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
import android.provider.Settings;
import com.android.internal.annotations.VisibleForTesting;
/**
* Contains logic for an accessibility floating menu view.
*/
public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu {
private static final int DEFAULT_FADE_EFFECT_ENABLED = 1;
private static final float DEFAULT_OPACITY_VALUE = 0.55f;
private final Context mContext;
private final AccessibilityFloatingMenuView mMenuView;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final ContentObserver mContentObserver =
new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
mMenuView.onTargetsChanged(getTargets(mContext, ACCESSIBILITY_BUTTON));
}
};
private final ContentObserver mSizeContentObserver =
new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
mMenuView.setSizeType(getSizeType(mContext));
}
};
private final ContentObserver mFadeOutContentObserver =
new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
mMenuView.updateOpacityWith(isFadeEffectEnabled(mContext),
getOpacityValue(mContext));
}
};
public AccessibilityFloatingMenu(Context context) {
mContext = context;
mMenuView = new AccessibilityFloatingMenuView(context);
}
@VisibleForTesting
AccessibilityFloatingMenu(Context context, AccessibilityFloatingMenuView menuView) {
mContext = context;
mMenuView = menuView;
}
@Override
public boolean isShowing() {
return mMenuView.isShowing();
}
@Override
public void show() {
if (isShowing()) {
return;
}
mMenuView.show();
mMenuView.onTargetsChanged(getTargets(mContext, ACCESSIBILITY_BUTTON));
mMenuView.updateOpacityWith(isFadeEffectEnabled(mContext),
getOpacityValue(mContext));
mMenuView.setSizeType(getSizeType(mContext));
mMenuView.setShapeType(getShapeType(mContext));
registerContentObservers();
}
@Override
public void hide() {
if (!isShowing()) {
return;
}
mMenuView.hide();
unregisterContentObservers();
}
private static boolean isFadeEffectEnabled(Context context) {
return Settings.Secure.getInt(
context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED,
DEFAULT_FADE_EFFECT_ENABLED) == /* enable */ 1;
}
private static float getOpacityValue(Context context) {
return Settings.Secure.getFloat(
context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_OPACITY,
DEFAULT_OPACITY_VALUE);
}
private static int getSizeType(Context context) {
return Settings.Secure.getInt(
context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_SIZE, SizeType.SMALL);
}
private static int getShapeType(Context context) {
return Settings.Secure.getInt(
context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_ICON_TYPE,
ShapeType.OVAL);
}
private void registerContentObservers() {
mContext.getContentResolver().registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS),
/* notifyForDescendants */ false, mContentObserver,
UserHandle.USER_CURRENT);
mContext.getContentResolver().registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE),
/* notifyForDescendants */ false, mSizeContentObserver,
UserHandle.USER_CURRENT);
mContext.getContentResolver().registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED),
/* notifyForDescendants */ false, mFadeOutContentObserver,
UserHandle.USER_CURRENT);
mContext.getContentResolver().registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY),
/* notifyForDescendants */ false, mFadeOutContentObserver,
UserHandle.USER_CURRENT);
}
private void unregisterContentObservers() {
mContext.getContentResolver().unregisterContentObserver(mContentObserver);
mContext.getContentResolver().unregisterContentObserver(mSizeContentObserver);
mContext.getContentResolver().unregisterContentObserver(mFadeOutContentObserver);
}
}

View File

@@ -0,0 +1,690 @@
/*
* Copyright (C) 2021 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.systemui.accessibility.floatingmenu;
import static android.util.MathUtils.constrain;
import static android.util.MathUtils.sq;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Handler;
import android.os.Looper;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.OvershootInterpolator;
import android.widget.FrameLayout;
import androidx.annotation.DimenRes;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.android.internal.accessibility.dialog.AccessibilityTarget;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Accessibility floating menu is used for the actions of accessibility features, it's also the
* action set.
*
* <p>The number of items would depend on strings key
* {@link android.provider.Settings.Secure#ACCESSIBILITY_BUTTON_TARGETS}.
*/
public class AccessibilityFloatingMenuView extends FrameLayout
implements RecyclerView.OnItemTouchListener {
private static final int INDEX_MENU_ITEM = 0;
private static final int FADE_OUT_DURATION_MS = 1000;
private static final int FADE_EFFECT_DURATION_MS = 3000;
private static final int SNAP_TO_LOCATION_DURATION_MS = 150;
private static final int MIN_WINDOW_X = 0;
private static final int MIN_WINDOW_Y = 0;
private static final float LOCATION_Y_PERCENTAGE = 0.8f;
private boolean mIsFadeEffectEnabled;
private boolean mIsShowing;
private boolean mIsDownInEnlargedTouchArea;
private boolean mIsDragging = false;
@Alignment
private int mAlignment = Alignment.RIGHT;
@SizeType
private int mSizeType = SizeType.SMALL;
@VisibleForTesting
@ShapeType
int mShapeType = ShapeType.OVAL;
private int mTemporaryShapeType;
@RadiusType
private int mRadiusType = RadiusType.LEFT_HALF_OVAL;
private int mMargin;
private int mPadding;
private int mScreenHeight;
private int mScreenWidth;
private int mIconWidth;
private int mIconHeight;
private int mInset;
private int mDownX;
private int mDownY;
private int mRelativeToPointerDownX;
private int mRelativeToPointerDownY;
private float mRadius;
private float mPercentageY = LOCATION_Y_PERCENTAGE;
private float mSquareScaledTouchSlop;
private final RecyclerView mListView;
private final AccessibilityTargetAdapter mAdapter;
private float mFadeOutValue;
private final ValueAnimator mFadeOutAnimator;
@VisibleForTesting
final ValueAnimator mDragAnimator;
private final Handler mUiHandler;
@VisibleForTesting
final WindowManager.LayoutParams mCurrentLayoutParams;
private final WindowManager mWindowManager;
private final List<AccessibilityTarget> mTargets = new ArrayList<>();
@IntDef({
SizeType.SMALL,
SizeType.LARGE
})
@Retention(RetentionPolicy.SOURCE)
@interface SizeType {
int SMALL = 0;
int LARGE = 1;
}
@IntDef({
ShapeType.OVAL,
ShapeType.HALF_OVAL
})
@Retention(RetentionPolicy.SOURCE)
@interface ShapeType {
int OVAL = 0;
int HALF_OVAL = 1;
}
@IntDef({
RadiusType.LEFT_HALF_OVAL,
RadiusType.OVAL,
RadiusType.RIGHT_HALF_OVAL
})
@Retention(RetentionPolicy.SOURCE)
@interface RadiusType {
int LEFT_HALF_OVAL = 0;
int OVAL = 1;
int RIGHT_HALF_OVAL = 2;
}
@IntDef({
Alignment.LEFT,
Alignment.RIGHT
})
@Retention(RetentionPolicy.SOURCE)
@interface Alignment {
int LEFT = 0;
int RIGHT = 1;
}
public AccessibilityFloatingMenuView(Context context) {
this(context, new RecyclerView(context));
}
@VisibleForTesting
AccessibilityFloatingMenuView(Context context,
RecyclerView listView) {
super(context);
mListView = listView;
mWindowManager = context.getSystemService(WindowManager.class);
mCurrentLayoutParams = createDefaultLayoutParams();
mAdapter = new AccessibilityTargetAdapter(mTargets);
mUiHandler = createUiHandler();
mFadeOutAnimator = ValueAnimator.ofFloat(1.0f, mFadeOutValue);
mFadeOutAnimator.setDuration(FADE_OUT_DURATION_MS);
mFadeOutAnimator.addUpdateListener(
(animation) -> setAlpha((float) animation.getAnimatedValue()));
mDragAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
mDragAnimator.setDuration(SNAP_TO_LOCATION_DURATION_MS);
mDragAnimator.setInterpolator(new OvershootInterpolator());
mDragAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mAlignment = calculateCurrentAlignment();
mPercentageY = calculateCurrentPercentageY();
updateLocationWith(mAlignment, mPercentageY);
updateMarginsWith(mAlignment);
updateInsetWith(getResources().getConfiguration().uiMode, mAlignment);
mRadiusType = (mAlignment == Alignment.RIGHT)
? RadiusType.LEFT_HALF_OVAL
: RadiusType.RIGHT_HALF_OVAL;
updateRadiusWith(mSizeType, mRadiusType, mTargets.size());
fadeOut();
}
});
updateDimensions();
initListView();
updateStrokeWith(getResources().getConfiguration().uiMode, mAlignment);
}
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
@NonNull MotionEvent event) {
final int currentRawX = (int) event.getRawX();
final int currentRawY = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
fadeIn();
mDownX = currentRawX;
mDownY = currentRawY;
mRelativeToPointerDownX = mCurrentLayoutParams.x - mDownX;
mRelativeToPointerDownY = mCurrentLayoutParams.y - mDownY;
mListView.animate().translationX(0);
break;
case MotionEvent.ACTION_MOVE:
if (mIsDragging
|| hasExceededTouchSlop(mDownX, mDownY, currentRawX, currentRawY)) {
if (!mIsDragging) {
mIsDragging = true;
setRadius(mRadius, RadiusType.OVAL);
setInset(0, 0);
}
mTemporaryShapeType =
isMovingTowardsScreenEdge(mAlignment, currentRawX, mDownX)
? ShapeType.HALF_OVAL
: ShapeType.OVAL;
final int newWindowX = currentRawX + mRelativeToPointerDownX;
final int newWindowY = currentRawY + mRelativeToPointerDownY;
mCurrentLayoutParams.x = constrain(newWindowX, MIN_WINDOW_X, getMaxWindowX());
mCurrentLayoutParams.y = constrain(newWindowY, MIN_WINDOW_Y, getMaxWindowY());
mWindowManager.updateViewLayout(this, mCurrentLayoutParams);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mIsDragging) {
mIsDragging = false;
final int maxX = getMaxWindowX();
final int endX = mCurrentLayoutParams.x > ((MIN_WINDOW_X + maxX) / 2)
? maxX : MIN_WINDOW_X;
final int endY = mCurrentLayoutParams.y;
snapToLocation(endX, endY);
setShapeType(mTemporaryShapeType);
// Avoid triggering the listener of the item.
return true;
}
// Must switch the oval shape type before tapping the corresponding item in the
// list view, otherwise it can't work on it.
if (mShapeType == ShapeType.HALF_OVAL) {
setShapeType(ShapeType.OVAL);
return true;
}
fadeOut();
break;
default: // Do nothing
}
// not consume all the events here because keeping the scroll behavior of list view.
return false;
}
@Override
public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
// Do Nothing
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean b) {
// Do Nothing
}
void show() {
if (isShowing()) {
return;
}
mIsShowing = true;
mWindowManager.addView(this, mCurrentLayoutParams);
setSystemGestureExclusion();
}
void hide() {
if (!isShowing()) {
return;
}
mIsShowing = false;
mWindowManager.removeView(this);
setSystemGestureExclusion();
}
boolean isShowing() {
return mIsShowing;
}
void onTargetsChanged(List<AccessibilityTarget> newTargets) {
fadeIn();
mTargets.clear();
mTargets.addAll(newTargets);
mAdapter.notifyDataSetChanged();
updateRadiusWith(mSizeType, mRadiusType, mTargets.size());
setSystemGestureExclusion();
fadeOut();
}
void setSizeType(@SizeType int newSizeType) {
fadeIn();
mSizeType = newSizeType;
updateIconSizeWith(newSizeType);
updateRadiusWith(newSizeType, mRadiusType, mTargets.size());
// When the icon sized changed, the menu size and location will be impacted.
updateLocationWith(mAlignment, mPercentageY);
setSystemGestureExclusion();
fadeOut();
}
void setShapeType(@ShapeType int newShapeType) {
fadeIn();
mShapeType = newShapeType;
updateOffsetWith(newShapeType, mAlignment);
setOnTouchListener(
newShapeType == ShapeType.OVAL
? null
: (view, event) -> onTouched(event));
fadeOut();
}
void updateOpacityWith(boolean isFadeEffectEnabled, float newOpacityValue) {
mIsFadeEffectEnabled = isFadeEffectEnabled;
mFadeOutValue = newOpacityValue;
mFadeOutAnimator.cancel();
mFadeOutAnimator.setFloatValues(1.0f, mFadeOutValue);
setAlpha(mIsFadeEffectEnabled ? mFadeOutValue : /* completely opaque */ 1.0f);
}
@VisibleForTesting
void fadeIn() {
if (!mIsFadeEffectEnabled) {
return;
}
mFadeOutAnimator.cancel();
mUiHandler.removeCallbacksAndMessages(null);
mUiHandler.post(() -> setAlpha(/* completely opaque */ 1.0f));
}
@VisibleForTesting
void fadeOut() {
if (!mIsFadeEffectEnabled) {
return;
}
mUiHandler.postDelayed(() -> mFadeOutAnimator.start(), FADE_EFFECT_DURATION_MS);
}
private boolean onTouched(MotionEvent event) {
final int action = event.getAction();
final int currentX = (int) event.getX();
final int currentY = (int) event.getY();
final int menuHalfWidth = getLayoutWidth() / 2;
final Rect touchDelegateBounds =
new Rect(mMargin, mMargin, mMargin + menuHalfWidth, mMargin + getLayoutHeight());
if (action == MotionEvent.ACTION_DOWN
&& touchDelegateBounds.contains(currentX, currentY)) {
mIsDownInEnlargedTouchArea = true;
}
if (!mIsDownInEnlargedTouchArea) {
return false;
}
if (action == MotionEvent.ACTION_UP
|| action == MotionEvent.ACTION_CANCEL) {
mIsDownInEnlargedTouchArea = false;
}
// In order to correspond to the correct item of list view.
event.setLocation(currentX - mMargin, currentY - mMargin);
return mListView.dispatchTouchEvent(event);
}
private boolean isMovingTowardsScreenEdge(@Alignment int side, int currentRawX, int downX) {
return (side == Alignment.RIGHT && currentRawX > downX)
|| (side == Alignment.LEFT && downX > currentRawX);
}
private boolean hasExceededTouchSlop(int startX, int startY, int endX, int endY) {
return (sq(endX - startX) + sq(endY - startY)) > mSquareScaledTouchSlop;
}
private void setRadius(float radius, @RadiusType int type) {
getMenuGradientDrawable().setCornerRadii(createRadii(radius, type));
}
private float[] createRadii(float radius, @RadiusType int type) {
if (type == RadiusType.LEFT_HALF_OVAL) {
return new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius};
}
if (type == RadiusType.RIGHT_HALF_OVAL) {
return new float[]{0.0f, 0.0f, radius, radius, radius, radius, 0.0f, 0.0f};
}
return new float[]{radius, radius, radius, radius, radius, radius, radius, radius};
}
private Handler createUiHandler() {
final Looper looper = Looper.myLooper();
if (looper == null) {
throw new IllegalArgumentException("looper must not be null");
}
return new Handler(looper);
}
private void updateDimensions() {
final Resources res = getResources();
final DisplayMetrics dm = res.getDisplayMetrics();
mScreenWidth = dm.widthPixels;
mScreenHeight = dm.heightPixels;
mMargin =
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin);
mPadding =
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_padding);
mInset =
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_inset);
mSquareScaledTouchSlop =
sq(ViewConfiguration.get(getContext()).getScaledTouchSlop());
}
private void updateIconSizeWith(@SizeType int sizeType) {
final Resources res = getResources();
final int iconResId =
sizeType == SizeType.SMALL
? R.dimen.accessibility_floating_menu_small_width_height
: R.dimen.accessibility_floating_menu_large_width_height;
mIconWidth = res.getDimensionPixelSize(iconResId);
mIconHeight = mIconWidth;
mAdapter.setIconWidthHeight(mIconWidth);
mAdapter.notifyDataSetChanged();
}
private void initListView() {
final Drawable background =
getContext().getDrawable(R.drawable.accessibility_floating_menu_background);
final LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
final LayoutParams layoutParams =
new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
mListView.setLayoutParams(layoutParams);
final InstantInsetLayerDrawable layerDrawable =
new InstantInsetLayerDrawable(new Drawable[]{background});
mListView.setBackground(layerDrawable);
mListView.setAdapter(mAdapter);
mListView.setLayoutManager(layoutManager);
mListView.addOnItemTouchListener(this);
mListView.animate().setInterpolator(new OvershootInterpolator());
updateListView();
addView(mListView);
}
private void updateListView() {
final int elevation =
getResources().getDimensionPixelSize(R.dimen.accessibility_floating_menu_elevation);
mListView.setElevation(elevation);
updateMarginsWith(mAlignment);
}
private WindowManager.LayoutParams createDefaultLayoutParams() {
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
params.windowAnimations = android.R.style.Animation_Translucent;
params.gravity = Gravity.START | Gravity.TOP;
params.x = getMaxWindowX();
params.y = (int) (getMaxWindowY() * mPercentageY);
return params;
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
updateDimensions();
updateListView();
updateIconSizeWith(mSizeType);
updateColor();
updateStrokeWith(newConfig.uiMode, mAlignment);
updateLocationWith(mAlignment, mPercentageY);
}
private void snapToLocation(int endX, int endY) {
mDragAnimator.cancel();
mDragAnimator.removeAllUpdateListeners();
mDragAnimator.addUpdateListener(anim -> onDragAnimationUpdate(anim, endX, endY));
mDragAnimator.start();
}
private void onDragAnimationUpdate(ValueAnimator animator, int endX, int endY) {
float value = (float) animator.getAnimatedValue();
final int newX = (int) (((1 - value) * mCurrentLayoutParams.x) + (value * endX));
final int newY = (int) (((1 - value) * mCurrentLayoutParams.y) + (value * endY));
mCurrentLayoutParams.x = newX;
mCurrentLayoutParams.y = newY;
mWindowManager.updateViewLayout(this, mCurrentLayoutParams);
}
private int getMaxWindowX() {
return mScreenWidth - mMargin - getLayoutWidth();
}
private int getMaxWindowY() {
return mScreenHeight - getWindowHeight();
}
private InstantInsetLayerDrawable getMenuLayerDrawable() {
return (InstantInsetLayerDrawable) mListView.getBackground();
}
private GradientDrawable getMenuGradientDrawable() {
return (GradientDrawable) getMenuLayerDrawable().getDrawable(INDEX_MENU_ITEM);
}
/**
* Updates the floating menu to be fixed at the side of the screen.
*/
private void updateLocationWith(@Alignment int side, float percentageCurrentY) {
mCurrentLayoutParams.x = (side == Alignment.RIGHT) ? getMaxWindowX() : MIN_WINDOW_X;
mCurrentLayoutParams.y = (int) (percentageCurrentY * getMaxWindowY());
mWindowManager.updateViewLayout(this, mCurrentLayoutParams);
}
private void updateOffsetWith(@ShapeType int shapeType, @Alignment int side) {
final float halfWidth = getLayoutWidth() / 2.0f;
final float offset = (shapeType == ShapeType.OVAL) ? 0 : halfWidth;
mListView.animate().translationX(side == Alignment.RIGHT ? offset : -offset);
}
private void updateMarginsWith(@Alignment int side) {
final LayoutParams layoutParams = (LayoutParams) mListView.getLayoutParams();
final int marginLeft = (side == Alignment.LEFT) ? 0 : mMargin;
final int marginRight = (side == Alignment.RIGHT) ? 0 : mMargin;
if (marginLeft == layoutParams.leftMargin
&& marginRight == layoutParams.rightMargin) {
return;
}
layoutParams.setMargins(marginLeft, mMargin, marginRight, mMargin);
mListView.setLayoutParams(layoutParams);
}
private void updateColor() {
final int menuColorResId = R.color.accessibility_floating_menu_background;
getMenuGradientDrawable().setColor(getResources().getColor(menuColorResId));
}
private void updateStrokeWith(int uiMode, @Alignment int side) {
updateInsetWith(uiMode, side);
final boolean isNightMode =
(uiMode & Configuration.UI_MODE_NIGHT_MASK)
== Configuration.UI_MODE_NIGHT_YES;
final Resources res = getResources();
final int width =
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_width);
final int strokeWidth = isNightMode ? width : 0;
final int strokeColor = res.getColor(R.color.accessibility_floating_menu_stroke_dark);
getMenuGradientDrawable().setStroke(strokeWidth, strokeColor);
}
private void updateRadiusWith(@SizeType int sizeType, @RadiusType int radiusType,
int itemCount) {
mRadius =
getResources().getDimensionPixelSize(getRadiusResId(sizeType, itemCount));
setRadius(mRadius, radiusType);
}
private void updateInsetWith(int uiMode, @Alignment int side) {
final boolean isNightMode =
(uiMode & Configuration.UI_MODE_NIGHT_MASK)
== Configuration.UI_MODE_NIGHT_YES;
final int layerInset = isNightMode ? mInset : 0;
final int insetLeft = (side == Alignment.LEFT) ? layerInset : 0;
final int insetRight = (side == Alignment.RIGHT) ? layerInset : 0;
setInset(insetLeft, insetRight);
}
private void setInset(int left, int right) {
final LayerDrawable layerDrawable = getMenuLayerDrawable();
if (layerDrawable.getLayerInsetLeft(INDEX_MENU_ITEM) == left
&& layerDrawable.getLayerInsetRight(INDEX_MENU_ITEM) == right) {
return;
}
layerDrawable.setLayerInset(INDEX_MENU_ITEM, left, 0, right, 0);
}
@Alignment
private int calculateCurrentAlignment() {
return mCurrentLayoutParams.x >= ((MIN_WINDOW_X + getMaxWindowX()) / 2)
? Alignment.RIGHT
: Alignment.LEFT;
}
private float calculateCurrentPercentageY() {
return mCurrentLayoutParams.y / (float) getMaxWindowY();
}
private @DimenRes int getRadiusResId(@SizeType int sizeType, int itemCount) {
return sizeType == SizeType.SMALL
? getSmallSizeResIdWith(itemCount)
: getLargeSizeResIdWith(itemCount);
}
private int getSmallSizeResIdWith(int itemCount) {
return itemCount > 1
? R.dimen.accessibility_floating_menu_small_multiple_radius
: R.dimen.accessibility_floating_menu_small_single_radius;
}
private int getLargeSizeResIdWith(int itemCount) {
return itemCount > 1
? R.dimen.accessibility_floating_menu_large_multiple_radius
: R.dimen.accessibility_floating_menu_large_single_radius;
}
private int getLayoutWidth() {
return mPadding * 2 + mIconWidth;
}
private int getLayoutHeight() {
return Math.min(mScreenHeight - mMargin * 2,
(mPadding + mIconHeight) * mTargets.size() + mPadding);
}
private int getWindowWidth() {
return mMargin + getLayoutWidth();
}
private int getWindowHeight() {
return Math.min(mScreenHeight, mMargin * 2 + getLayoutHeight());
}
private void setSystemGestureExclusion() {
final Rect excludeZone =
new Rect(0, 0, getWindowWidth(), getWindowHeight());
post(() -> setSystemGestureExclusionRects(
mIsShowing
? Collections.singletonList(excludeZone)
: Collections.emptyList()));
}
}

View File

@@ -0,0 +1,150 @@
/*
* Copyright (C) 2020 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.systemui.accessibility.floatingmenu;
import static android.view.View.GONE;
import static com.android.systemui.accessibility.floatingmenu.AccessibilityTargetAdapter.ItemType.FIRST_ITEM;
import static com.android.systemui.accessibility.floatingmenu.AccessibilityTargetAdapter.ItemType.LAST_ITEM;
import static com.android.systemui.accessibility.floatingmenu.AccessibilityTargetAdapter.ItemType.REGULAR_ITEM;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.Adapter;
import com.android.internal.accessibility.dialog.AccessibilityTarget;
import com.android.systemui.R;
import com.android.systemui.accessibility.floatingmenu.AccessibilityTargetAdapter.ViewHolder;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
/**
* An adapter which shows the set of accessibility targets that can be performed.
*/
public class AccessibilityTargetAdapter extends Adapter<ViewHolder> {
private int mIconWidthHeight;
private final List<AccessibilityTarget> mTargets;
@IntDef({
FIRST_ITEM,
REGULAR_ITEM,
LAST_ITEM
})
@Retention(RetentionPolicy.SOURCE)
@interface ItemType {
int FIRST_ITEM = 0;
int REGULAR_ITEM = 1;
int LAST_ITEM = 2;
}
public AccessibilityTargetAdapter(List<AccessibilityTarget> targets) {
mTargets = targets;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, @ItemType int itemType) {
final View root = LayoutInflater.from(parent.getContext()).inflate(
R.layout.accessibility_floating_menu_item, parent,
/* attachToRoot= */ false);
if (itemType == FIRST_ITEM) {
return new TopViewHolder(root);
}
if (itemType == LAST_ITEM) {
return new BottomViewHolder(root);
}
return new ViewHolder(root);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.mIconView.setBackground(mTargets.get(position).getIcon());
holder.updateIconWidthHeight(mIconWidthHeight);
holder.itemView.setOnClickListener((v) -> mTargets.get(position).onSelected());
}
@ItemType
@Override
public int getItemViewType(int position) {
if (position == 0) {
return FIRST_ITEM;
}
if (position == (getItemCount() - 1)) {
return LAST_ITEM;
}
return REGULAR_ITEM;
}
@Override
public int getItemCount() {
return mTargets.size();
}
public void setIconWidthHeight(int iconWidthHeight) {
mIconWidthHeight = iconWidthHeight;
}
static class ViewHolder extends RecyclerView.ViewHolder {
final View mIconView;
final View mDivider;
ViewHolder(View itemView) {
super(itemView);
mIconView = itemView.findViewById(R.id.icon_view);
mDivider = itemView.findViewById(R.id.transparent_divider);
}
void updateIconWidthHeight(int newValue) {
final ViewGroup.LayoutParams layoutParams = mIconView.getLayoutParams();
if (layoutParams.width == newValue) {
return;
}
layoutParams.width = newValue;
layoutParams.height = newValue;
mIconView.setLayoutParams(layoutParams);
}
}
static class TopViewHolder extends ViewHolder {
TopViewHolder(View itemView) {
super(itemView);
final int padding = itemView.getPaddingStart();
itemView.setPaddingRelative(padding, padding, padding, 0);
}
}
static class BottomViewHolder extends ViewHolder {
BottomViewHolder(View itemView) {
super(itemView);
mDivider.setVisibility(GONE);
final int padding = itemView.getPaddingStart();
itemView.setPaddingRelative(padding, 0, padding, padding);
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (C) 2021 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.systemui.accessibility.floatingmenu;
/**
* Interface for managing the accessibility targets menu component.
*/
public interface IAccessibilityFloatingMenu {
/**
* Checks if the menu was shown.
*/
boolean isShowing();
/**
* Shows the accessibility targets menu.
*/
void show();
/**
* Hides the accessibility targets menu.
*/
void hide();
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2021 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.systemui.accessibility.floatingmenu;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
/**
* A drawable that forces to update the bounds {@link #onBoundsChange(Rect)} immediately after
* {@link #setLayerInset} dynamically.
*/
public class InstantInsetLayerDrawable extends LayerDrawable {
public InstantInsetLayerDrawable(Drawable[] layers) {
super(layers);
}
@Override
public void setLayerInset(int index, int l, int t, int r, int b) {
super.setLayerInset(index, l, t, r, b);
onBoundsChange(getBounds());
}
}

View File

@@ -23,11 +23,11 @@ import com.android.internal.annotations.GuardedBy;
import java.util.ArrayList;
import java.util.List;
class MotionEventHelper {
public class MotionEventHelper {
@GuardedBy("this")
private final List<MotionEvent> mMotionEvents = new ArrayList<>();
void recycleEvents() {
public void recycleEvents() {
for (MotionEvent event:mMotionEvents) {
event.recycle();
}
@@ -36,7 +36,7 @@ class MotionEventHelper {
}
}
MotionEvent obtainMotionEvent(long downTime, long eventTime, int action, float x,
public MotionEvent obtainMotionEvent(long downTime, long eventTime, int action, float x,
float y) {
MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, x, y, 0);
synchronized (this) {

View File

@@ -0,0 +1,95 @@
/*
* Copyright (C) 2021 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.systemui.accessibility.floatingmenu;
import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import android.content.Context;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.accessibility.AccessibilityManager;
import androidx.test.filters.SmallTest;
import com.android.internal.accessibility.dialog.AccessibilityTarget;
import com.android.systemui.SysuiTestCase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.List;
/** Tests for {@link AccessibilityFloatingMenu}. */
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class AccessibilityFloatingMenuTest extends SysuiTestCase {
@Mock
private AccessibilityManager mAccessibilityManager;
private AccessibilityFloatingMenuView mMenuView;
private AccessibilityFloatingMenu mMenu;
@Before
public void initMenu() {
MockitoAnnotations.initMocks(this);
final List<AccessibilityTarget> mTargets = new ArrayList<>();
mTargets.add(mock(AccessibilityTarget.class));
final List<String> assignedTargets = new ArrayList<>();
mContext.addMockSystemService(Context.ACCESSIBILITY_SERVICE, mAccessibilityManager);
assignedTargets.add(MAGNIFICATION_CONTROLLER_NAME);
doReturn(assignedTargets).when(mAccessibilityManager).getAccessibilityShortcutTargets(
anyInt());
mMenuView = new AccessibilityFloatingMenuView(mContext);
mMenu = new AccessibilityFloatingMenu(mContext, mMenuView);
}
@Test
public void showMenuView_success() {
mMenu.show();
assertThat(mMenuView.isShowing()).isTrue();
}
@Test
public void hideMenuView_success() {
mMenu.show();
mMenu.hide();
assertThat(mMenuView.isShowing()).isFalse();
}
@After
public void tearDown() {
mMenu.hide();
}
}

View File

@@ -0,0 +1,365 @@
/*
* Copyright (C) 2021 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.systemui.accessibility.floatingmenu;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.LayerDrawable;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewPropertyAnimator;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.filters.SmallTest;
import com.android.internal.accessibility.dialog.AccessibilityTarget;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.accessibility.MotionEventHelper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.List;
/** Tests for {@link AccessibilityFloatingMenuView}. */
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class AccessibilityFloatingMenuViewTest extends SysuiTestCase {
private AccessibilityFloatingMenuView mMenuView;
@Mock
private WindowManager mWindowManager;
@Mock
private ViewPropertyAnimator mAnimator;
private MotionEvent mInterceptMotionEvent;
private RecyclerView mListView;
private int mMenuHalfWidth;
private int mMenuHalfHeight;
private int mScreenHalfWidth;
private int mScreenHalfHeight;
private int mMaxWindowX;
private final MotionEventHelper mMotionEventHelper = new MotionEventHelper();
private final List<AccessibilityTarget> mTargets = new ArrayList<>();
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
final WindowManager wm = mContext.getSystemService(WindowManager.class);
doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
mWindowManager).getMaximumWindowMetrics();
mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
mTargets.add(mock(AccessibilityTarget.class));
mListView = new RecyclerView(mContext);
mMenuView = new AccessibilityFloatingMenuView(mContext, mListView);
final Resources res = mContext.getResources();
final int margin =
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin);
final int padding =
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_padding);
final int iconWidthHeight =
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_width_height);
final int menuWidth = padding * 2 + iconWidthHeight;
final int menuHeight = (padding + iconWidthHeight) * mTargets.size() + padding;
final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels;
final int screenHeight = mContext.getResources().getDisplayMetrics().heightPixels;
mMenuHalfWidth = menuWidth / 2;
mMenuHalfHeight = menuHeight / 2;
mScreenHalfWidth = screenWidth / 2;
mScreenHalfHeight = screenHeight / 2;
mMaxWindowX = screenWidth - margin - menuWidth;
}
@Test
public void initListView_success() {
assertThat(mMenuView.getChildCount()).isEqualTo(1);
}
@Test
public void showMenuView_success() {
mMenuView.show();
assertThat(mMenuView.isShowing()).isTrue();
verify(mWindowManager).addView(eq(mMenuView), any(WindowManager.LayoutParams.class));
}
@Test
public void showMenuView_showTwice_addViewOnce() {
mMenuView.show();
mMenuView.show();
assertThat(mMenuView.isShowing()).isTrue();
verify(mWindowManager, times(1)).addView(eq(mMenuView),
any(WindowManager.LayoutParams.class));
}
@Test
public void hideMenuView_success() {
mMenuView.show();
mMenuView.hide();
assertThat(mMenuView.isShowing()).isFalse();
verify(mWindowManager).removeView(eq(mMenuView));
}
@Test
public void hideMenuView_hideTwice_removeViewOnce() {
mMenuView.show();
mMenuView.hide();
mMenuView.hide();
assertThat(mMenuView.isShowing()).isFalse();
verify(mWindowManager, times(1)).removeView(eq(mMenuView));
}
@Test
public void updateListViewRadius_singleTarget_matchResult() {
final float radius =
getContext().getResources().getDimensionPixelSize(
R.dimen.accessibility_floating_menu_small_single_radius);
final float[] expectedRadii =
new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius};
mMenuView.onTargetsChanged(mTargets);
final View view = mMenuView.getChildAt(0);
final LayerDrawable layerDrawable = (LayerDrawable) view.getBackground();
final GradientDrawable gradientDrawable =
(GradientDrawable) layerDrawable.getDrawable(0);
final float[] actualRadii = gradientDrawable.getCornerRadii();
assertThat(actualRadii).isEqualTo(expectedRadii);
}
@Test
public void setSizeType_largeSize_matchResult() {
final int shapeType = 2;
final float radius = getContext().getResources().getDimensionPixelSize(
R.dimen.accessibility_floating_menu_large_single_radius);
final float[] expectedRadii =
new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius};
final Drawable listViewBackground =
mContext.getDrawable(R.drawable.accessibility_floating_menu_background);
mListView = spy(new RecyclerView(mContext));
mListView.setBackground(listViewBackground);
mMenuView = new AccessibilityFloatingMenuView(mContext, mListView);
mMenuView.setSizeType(shapeType);
final LayerDrawable layerDrawable =
(LayerDrawable) mListView.getBackground();
final GradientDrawable gradientDrawable =
(GradientDrawable) layerDrawable.getDrawable(0);
assertThat(gradientDrawable.getCornerRadii()).isEqualTo(expectedRadii);
}
@Test
public void setShapeType_halfCircle_translationX() {
final RecyclerView listView = spy(new RecyclerView(mContext));
final AccessibilityFloatingMenuView menuView =
new AccessibilityFloatingMenuView(mContext, listView);
final int shapeType = 2;
doReturn(mAnimator).when(listView).animate();
menuView.setShapeType(shapeType);
verify(mAnimator).translationX(anyFloat());
}
@Test
public void onTargetsChanged_fadeInOut() {
final AccessibilityFloatingMenuView menuView = spy(mMenuView);
final InOrder inOrderMenuView = inOrder(menuView);
menuView.onTargetsChanged(mTargets);
inOrderMenuView.verify(menuView).fadeIn();
inOrderMenuView.verify(menuView).fadeOut();
}
@Test
public void setSizeType_fadeInOut() {
final AccessibilityFloatingMenuView menuView = spy(mMenuView);
final InOrder inOrderMenuView = inOrder(menuView);
final int smallSize = 0;
menuView.setSizeType(smallSize);
inOrderMenuView.verify(menuView).fadeIn();
inOrderMenuView.verify(menuView).fadeOut();
}
@Test
public void tapOnAndDragMenu_interceptUpEvent() {
final RecyclerView listView = new RecyclerView(mContext);
final TestAccessibilityFloatingMenu menuView =
new TestAccessibilityFloatingMenu(mContext, listView);
menuView.show();
menuView.onTargetsChanged(mTargets);
menuView.setSizeType(0);
menuView.setShapeType(0);
final int currentWindowX = mMenuView.mCurrentLayoutParams.x;
final int currentWindowY = mMenuView.mCurrentLayoutParams.y;
final MotionEvent downEvent =
mMotionEventHelper.obtainMotionEvent(0, 1,
MotionEvent.ACTION_DOWN,
currentWindowX + /* offsetXToMenuCenterX */ mMenuHalfWidth,
currentWindowY + /* offsetYToMenuCenterY */ mMenuHalfHeight);
final MotionEvent moveEvent =
mMotionEventHelper.obtainMotionEvent(2, 3,
MotionEvent.ACTION_MOVE,
/* screenCenterX */mScreenHalfWidth
- /* offsetXToScreenLeftHalfRegion */ 10,
/* screenCenterY */ mScreenHalfHeight);
final MotionEvent upEvent =
mMotionEventHelper.obtainMotionEvent(4, 5,
MotionEvent.ACTION_UP,
/* screenCenterX */ mScreenHalfWidth
- /* offsetXToScreenLeftHalfRegion */ 10,
/* screenCenterY */ mScreenHalfHeight);
listView.dispatchTouchEvent(downEvent);
listView.dispatchTouchEvent(moveEvent);
listView.dispatchTouchEvent(upEvent);
assertThat(mInterceptMotionEvent.getAction()).isEqualTo(MotionEvent.ACTION_UP);
}
@Test
public void tapOnAndDragMenu_matchLocation() {
mMenuView.show();
mMenuView.onTargetsChanged(mTargets);
mMenuView.setSizeType(0);
mMenuView.setShapeType(0);
final int currentWindowX = mMenuView.mCurrentLayoutParams.x;
final int currentWindowY = mMenuView.mCurrentLayoutParams.y;
final MotionEvent downEvent =
mMotionEventHelper.obtainMotionEvent(0, 1,
MotionEvent.ACTION_DOWN,
currentWindowX + /* offsetXToMenuCenterX */ mMenuHalfWidth,
currentWindowY + /* offsetYToMenuCenterY */ mMenuHalfHeight);
final MotionEvent moveEvent =
mMotionEventHelper.obtainMotionEvent(2, 3,
MotionEvent.ACTION_MOVE,
/* screenCenterX */mScreenHalfWidth
+ /* offsetXToScreenRightHalfRegion */ 10,
/* screenCenterY */ mScreenHalfHeight);
final MotionEvent upEvent =
mMotionEventHelper.obtainMotionEvent(4, 5,
MotionEvent.ACTION_UP,
/* screenCenterX */ mScreenHalfWidth
+ /* offsetXToScreenRightHalfRegion */ 10,
/* screenCenterY */ mScreenHalfHeight);
mListView.dispatchTouchEvent(downEvent);
mListView.dispatchTouchEvent(moveEvent);
mListView.dispatchTouchEvent(upEvent);
mMenuView.mDragAnimator.end();
assertThat(mMenuView.mCurrentLayoutParams.x).isEqualTo(mMaxWindowX);
assertThat(mMenuView.mCurrentLayoutParams.y).isEqualTo(
/* newWindowY = screenCenterY - offsetY */ mScreenHalfHeight - mMenuHalfHeight);
}
@Test
public void tapOnAndDragMenuToScreenSide_transformShapeHalfOval() {
mMenuView.show();
mMenuView.onTargetsChanged(mTargets);
mMenuView.setSizeType(0);
mMenuView.setShapeType(/* oval */ 0);
final int currentWindowX = mMenuView.mCurrentLayoutParams.x;
final int currentWindowY = mMenuView.mCurrentLayoutParams.y;
final MotionEvent downEvent =
mMotionEventHelper.obtainMotionEvent(0, 1,
MotionEvent.ACTION_DOWN,
currentWindowX + /* offsetXToMenuCenterX */ mMenuHalfWidth,
currentWindowY + /* offsetYToMenuCenterY */ mMenuHalfHeight);
final MotionEvent moveEvent =
mMotionEventHelper.obtainMotionEvent(2, 3,
MotionEvent.ACTION_MOVE,
/* downX */(currentWindowX + mMenuHalfWidth)
+ /* offsetXToScreenRightSide */ mMenuHalfWidth,
/* downY */ (currentWindowY + mMenuHalfHeight));
final MotionEvent upEvent =
mMotionEventHelper.obtainMotionEvent(4, 5,
MotionEvent.ACTION_UP,
/* downX */(currentWindowX + mMenuHalfWidth)
+ /* offsetXToScreenRightSide */ mMenuHalfWidth,
/* downY */ (currentWindowY + mMenuHalfHeight));
mListView.dispatchTouchEvent(downEvent);
mListView.dispatchTouchEvent(moveEvent);
mListView.dispatchTouchEvent(upEvent);
assertThat(mMenuView.mShapeType).isEqualTo(/* halfOval */ 1);
}
@After
public void tearDown() {
mInterceptMotionEvent = null;
mMotionEventHelper.recycleEvents();
}
private class TestAccessibilityFloatingMenu extends AccessibilityFloatingMenuView {
TestAccessibilityFloatingMenu(Context context, RecyclerView listView) {
super(context, listView);
}
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
@NonNull MotionEvent event) {
final boolean intercept = super.onInterceptTouchEvent(recyclerView, event);
if (intercept) {
mInterceptMotionEvent = event;
}
return intercept;
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) 2021 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.systemui.accessibility.floatingmenu;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.when;
import android.graphics.drawable.Drawable;
import android.testing.AndroidTestingRunner;
import android.view.LayoutInflater;
import android.view.View;
import androidx.test.filters.SmallTest;
import com.android.internal.accessibility.dialog.AccessibilityTarget;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.accessibility.floatingmenu.AccessibilityTargetAdapter.ViewHolder;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.List;
/** Tests for {@link AccessibilityTargetAdapter}. */
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class AccessibilityTargetAdapterTest extends SysuiTestCase {
@Mock
private AccessibilityTarget mAccessibilityTarget;
@Mock
private Drawable mIcon;
@Mock
private Drawable.ConstantState mConstantState;
private ViewHolder mViewHolder;
private AccessibilityTargetAdapter mAdapter;
private final List<AccessibilityTarget> mTargets = new ArrayList<>();
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mTargets.add(mAccessibilityTarget);
mAdapter = new AccessibilityTargetAdapter(mTargets);
final View root = LayoutInflater.from(mContext).inflate(
R.layout.accessibility_floating_menu_item, null);
mViewHolder = new ViewHolder(root);
when(mAccessibilityTarget.getIcon()).thenReturn(mIcon);
when(mIcon.getConstantState()).thenReturn(mConstantState);
}
@Test
public void onBindViewHolder_setIconWidthHeight_matchResult() {
final int iconWidthHeight = 50;
mAdapter.setIconWidthHeight(iconWidthHeight);
mAdapter.onBindViewHolder(mViewHolder, 0);
final int actualIconWith = mViewHolder.mIconView.getLayoutParams().width;
assertThat(actualIconWith).isEqualTo(iconWidthHeight);
}
}