Merge changes I92149aed,I01226212

* changes:
  Remove the legacy implementation of the accessibility floating menu.
  Remove the feature flag of the A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS.
This commit is contained in:
PETER LIANG
2023-02-01 12:20:42 +00:00
committed by Android (Google) Code Review
15 changed files with 18 additions and 2926 deletions

View File

@@ -1,277 +0,0 @@
/*
* 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_MIGRATION_TOOLTIP_PROMPT;
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.Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP;
import static com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.ShapeType;
import static com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.SizeType;
import android.annotation.FloatRange;
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 android.text.TextUtils;
import androidx.annotation.NonNull;
import com.android.internal.accessibility.dialog.AccessibilityTarget;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.Prefs;
import com.android.systemui.shared.system.SysUiStatsLog;
import com.android.systemui.util.settings.SecureSettings;
import java.util.List;
/**
* Contains logic for an accessibility floating menu view.
*/
public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu {
private static final int DEFAULT_FADE_EFFECT_IS_ENABLED = 1;
private static final int DEFAULT_MIGRATION_TOOLTIP_PROMPT_IS_DISABLED = 0;
@FloatRange(from = 0.0, to = 1.0)
private static final float DEFAULT_OPACITY_VALUE = 0.55f;
@FloatRange(from = 0.0, to = 1.0)
private static final float DEFAULT_POSITION_X_PERCENT = 1.0f;
@FloatRange(from = 0.0, to = 1.0)
private static final float DEFAULT_POSITION_Y_PERCENT = 0.77f;
private final Context mContext;
private final SecureSettings mSecureSettings;
private final AccessibilityFloatingMenuView mMenuView;
private final MigrationTooltipView mMigrationTooltipView;
private final DockTooltipView mDockTooltipView;
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());
}
};
private final ContentObserver mFadeOutContentObserver =
new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
mMenuView.updateOpacityWith(isFadeEffectEnabled(),
getOpacityValue());
}
};
private final ContentObserver mEnabledA11yServicesContentObserver =
new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
mMenuView.onEnabledFeaturesChanged();
}
};
public AccessibilityFloatingMenu(Context context, SecureSettings secureSettings) {
mContext = context;
mSecureSettings = secureSettings;
mMenuView = new AccessibilityFloatingMenuView(context, getPosition(context));
mMigrationTooltipView = new MigrationTooltipView(mContext, mMenuView);
mDockTooltipView = new DockTooltipView(mContext, mMenuView);
}
@VisibleForTesting
AccessibilityFloatingMenu(Context context, SecureSettings secureSettings,
AccessibilityFloatingMenuView menuView) {
mContext = context;
mSecureSettings = secureSettings;
mMenuView = menuView;
mMigrationTooltipView = new MigrationTooltipView(mContext, mMenuView);
mDockTooltipView = new DockTooltipView(mContext, mMenuView);
}
@Override
public boolean isShowing() {
return mMenuView.isShowing();
}
@Override
public void show() {
if (isShowing()) {
return;
}
final List<AccessibilityTarget> targetList = getTargets(mContext, ACCESSIBILITY_BUTTON);
if (targetList.isEmpty()) {
return;
}
mMenuView.show();
mMenuView.onTargetsChanged(targetList);
mMenuView.updateOpacityWith(isFadeEffectEnabled(),
getOpacityValue());
mMenuView.setSizeType(getSizeType());
mMenuView.setShapeType(getShapeType());
mMenuView.setOnDragEndListener(this::onDragEnd);
showMigrationTooltipIfNecessary();
registerContentObservers();
}
@Override
public void hide() {
if (!isShowing()) {
return;
}
mMenuView.hide();
mMenuView.setOnDragEndListener(null);
mMigrationTooltipView.hide();
mDockTooltipView.hide();
unregisterContentObservers();
}
@NonNull
private Position getPosition(Context context) {
final String absolutePositionString = Prefs.getString(context,
Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null);
if (TextUtils.isEmpty(absolutePositionString)) {
return new Position(DEFAULT_POSITION_X_PERCENT, DEFAULT_POSITION_Y_PERCENT);
} else {
return Position.fromString(absolutePositionString);
}
}
// Migration tooltip was the android S feature. It's just used on the Android version from R
// to S. In addition, it only shows once.
private void showMigrationTooltipIfNecessary() {
if (isMigrationTooltipPromptEnabled()) {
mMigrationTooltipView.show();
mSecureSettings.putInt(
ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT, /* disabled */ 0);
}
}
private boolean isMigrationTooltipPromptEnabled() {
return mSecureSettings.getInt(
ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT,
DEFAULT_MIGRATION_TOOLTIP_PROMPT_IS_DISABLED) == /* enabled */ 1;
}
private void onDragEnd(Position position) {
SysUiStatsLog.write(SysUiStatsLog.ACCESSIBILITY_FLOATING_MENU_UI_CHANGED,
position.getPercentageX(), position.getPercentageY(),
mContext.getResources().getConfiguration().orientation);
savePosition(mContext, position);
showDockTooltipIfNecessary(mContext);
}
private void savePosition(Context context, Position position) {
Prefs.putString(context, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION,
position.toString());
}
/**
* Shows tooltip when user drags accessibility floating menu for the first time.
*/
private void showDockTooltipIfNecessary(Context context) {
if (!Prefs.get(context).getBoolean(
HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, false)) {
// if the menu is an oval, the user has already dragged it out, so show the tooltip.
if (mMenuView.isOvalShape()) {
mDockTooltipView.show();
}
Prefs.putBoolean(context, HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, true);
}
}
private boolean isFadeEffectEnabled() {
return mSecureSettings.getInt(
ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED,
DEFAULT_FADE_EFFECT_IS_ENABLED) == /* enabled */ 1;
}
private float getOpacityValue() {
return mSecureSettings.getFloat(
ACCESSIBILITY_FLOATING_MENU_OPACITY,
DEFAULT_OPACITY_VALUE);
}
private int getSizeType() {
return mSecureSettings.getInt(
ACCESSIBILITY_FLOATING_MENU_SIZE, SizeType.SMALL);
}
private int getShapeType() {
return mSecureSettings.getInt(
ACCESSIBILITY_FLOATING_MENU_ICON_TYPE,
ShapeType.OVAL);
}
private void registerContentObservers() {
mSecureSettings.registerContentObserverForUser(
Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
/* notifyForDescendants */ false, mContentObserver,
UserHandle.USER_CURRENT);
mSecureSettings.registerContentObserverForUser(
Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
/* notifyForDescendants */ false, mContentObserver,
UserHandle.USER_CURRENT);
mSecureSettings.registerContentObserverForUser(
Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE,
/* notifyForDescendants */ false, mSizeContentObserver,
UserHandle.USER_CURRENT);
mSecureSettings.registerContentObserverForUser(
Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED,
/* notifyForDescendants */ false, mFadeOutContentObserver,
UserHandle.USER_CURRENT);
mSecureSettings.registerContentObserverForUser(
Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY,
/* notifyForDescendants */ false, mFadeOutContentObserver,
UserHandle.USER_CURRENT);
mSecureSettings.registerContentObserverForUser(
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
/* notifyForDescendants */ false,
mEnabledA11yServicesContentObserver, UserHandle.USER_CURRENT);
}
private void unregisterContentObservers() {
mSecureSettings.unregisterContentObserver(mContentObserver);
mSecureSettings.unregisterContentObserver(mSizeContentObserver);
mSecureSettings.unregisterContentObserver(mFadeOutContentObserver);
mSecureSettings.unregisterContentObserver(
mEnabledA11yServicesContentObserver);
}
}

View File

@@ -19,8 +19,6 @@ package com.android.systemui.accessibility.floatingmenu;
import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU;
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
import static com.android.systemui.flags.Flags.A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS;
import android.content.Context;
import android.hardware.display.DisplayManager;
import android.os.UserHandle;
@@ -38,7 +36,6 @@ import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
import com.android.systemui.accessibility.AccessibilityButtonModeObserver.AccessibilityButtonMode;
import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.util.settings.SecureSettings;
@@ -59,7 +56,7 @@ public class AccessibilityFloatingMenuController implements
private final WindowManager mWindowManager;
private final DisplayManager mDisplayManager;
private final AccessibilityManager mAccessibilityManager;
private final FeatureFlags mFeatureFlags;
private final SecureSettings mSecureSettings;
private final DisplayTracker mDisplayTracker;
@VisibleForTesting
@@ -105,7 +102,6 @@ public class AccessibilityFloatingMenuController implements
AccessibilityButtonTargetsObserver accessibilityButtonTargetsObserver,
AccessibilityButtonModeObserver accessibilityButtonModeObserver,
KeyguardUpdateMonitor keyguardUpdateMonitor,
FeatureFlags featureFlags,
SecureSettings secureSettings,
DisplayTracker displayTracker) {
mContext = context;
@@ -115,7 +111,6 @@ public class AccessibilityFloatingMenuController implements
mAccessibilityButtonTargetsObserver = accessibilityButtonTargetsObserver;
mAccessibilityButtonModeObserver = accessibilityButtonModeObserver;
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mFeatureFlags = featureFlags;
mSecureSettings = secureSettings;
mDisplayTracker = displayTracker;
@@ -162,7 +157,7 @@ public class AccessibilityFloatingMenuController implements
* Handles the accessibility floating menu visibility with the given values.
*
* @param keyguardVisible the keyguard visibility status. Not show the
* {@link AccessibilityFloatingMenu} when keyguard appears.
* {@link MenuView} when keyguard appears.
* @param mode accessibility button mode {@link AccessibilityButtonMode}
* @param targets accessibility button list; it should comes from
* {@link android.provider.Settings.Secure#ACCESSIBILITY_BUTTON_TARGETS}.
@@ -187,16 +182,12 @@ public class AccessibilityFloatingMenuController implements
private void showFloatingMenu() {
if (mFloatingMenu == null) {
if (mFeatureFlags.isEnabled(A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS)) {
final Display defaultDisplay = mDisplayManager.getDisplay(
mDisplayTracker.getDefaultDisplayId());
final Context windowContext = mContext.createWindowContext(defaultDisplay,
TYPE_NAVIGATION_BAR_PANEL, /* options= */ null);
mFloatingMenu = new MenuViewLayerController(windowContext, mWindowManager,
mAccessibilityManager, mSecureSettings);
} else {
mFloatingMenu = new AccessibilityFloatingMenu(mContext, mSecureSettings);
}
final Display defaultDisplay = mDisplayManager.getDisplay(
mDisplayTracker.getDefaultDisplayId());
final Context windowContext = mContext.createWindowContext(defaultDisplay,
TYPE_NAVIGATION_BAR_PANEL, /* options= */ null);
mFloatingMenu = new MenuViewLayerController(windowContext, mWindowManager,
mAccessibilityManager, mSecureSettings);
}
mFloatingMenu.show();

View File

@@ -1,921 +0,0 @@
/*
* 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.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.util.MathUtils.constrain;
import static android.util.MathUtils.sq;
import static android.view.WindowInsets.Type.displayCutout;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsets.Type.systemBars;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
import static java.util.Objects.requireNonNull;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Insets;
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.view.Gravity;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import android.view.animation.Animation;
import android.view.animation.OvershootInterpolator;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
import androidx.annotation.DimenRes;
import androidx.annotation.NonNull;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
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;
import java.util.Optional;
/**
* 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_Y = 0;
private static final int ANIMATION_START_OFFSET = 600;
private static final int ANIMATION_DURATION_MS = 600;
private static final float ANIMATION_TO_X_VALUE = 0.5f;
private boolean mIsFadeEffectEnabled;
private boolean mIsShowing;
private boolean mIsDownInEnlargedTouchArea;
private boolean mIsDragging = false;
@Alignment
private int mAlignment;
@SizeType
private int mSizeType = SizeType.SMALL;
@VisibleForTesting
@ShapeType
int mShapeType = ShapeType.OVAL;
private int mTemporaryShapeType;
@RadiusType
private int mRadiusType;
private int mMargin;
private int mPadding;
// The display width excludes the window insets of the system bar and display cutout.
private int mDisplayHeight;
// The display Height excludes the window insets of the system bar and display cutout.
private int mDisplayWidth;
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 final Rect mDisplayInsetsRect = new Rect();
private final Rect mImeInsetsRect = new Rect();
private final Position mPosition;
private float mSquareScaledTouchSlop;
private final Configuration mLastConfiguration;
private Optional<OnDragEndListener> mOnDragEndListener = Optional.empty();
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;
}
/**
* Interface for a callback to be invoked when the floating menu was dragging.
*/
interface OnDragEndListener {
/**
* Called when a drag is completed.
*
* @param position Stores information about the position
*/
void onDragEnd(Position position);
}
public AccessibilityFloatingMenuView(Context context, @NonNull Position position) {
this(context, position, new RecyclerView(context));
}
@VisibleForTesting
AccessibilityFloatingMenuView(Context context, @NonNull Position position,
RecyclerView listView) {
super(context);
mListView = listView;
mWindowManager = context.getSystemService(WindowManager.class);
mLastConfiguration = new Configuration(getResources().getConfiguration());
mAdapter = new AccessibilityTargetAdapter(mTargets);
mUiHandler = createUiHandler();
mPosition = position;
mAlignment = transformToAlignment(mPosition.getPercentageX());
mRadiusType = (mAlignment == Alignment.RIGHT)
? RadiusType.LEFT_HALF_OVAL
: RadiusType.RIGHT_HALF_OVAL;
updateDimensions();
mCurrentLayoutParams = createDefaultLayoutParams();
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) {
mPosition.update(transformCurrentPercentageXToEdge(),
calculateCurrentPercentageY());
mAlignment = transformToAlignment(mPosition.getPercentageX());
updateLocationWith(mPosition);
updateInsetWith(getResources().getConfiguration().uiMode, mAlignment);
mRadiusType = (mAlignment == Alignment.RIGHT)
? RadiusType.LEFT_HALF_OVAL
: RadiusType.RIGHT_HALF_OVAL;
updateRadiusWith(mSizeType, mRadiusType, mTargets.size());
fadeOut();
mOnDragEndListener.ifPresent(
onDragEndListener -> onDragEndListener.onDragEnd(mPosition));
}
});
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, getMinWindowX(), 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 minX = getMinWindowX();
final int maxX = getMaxWindowX();
final int endX = mCurrentLayoutParams.x > ((minX + maxX) / 2)
? maxX : minX;
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 (!isOvalShape()) {
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);
setOnApplyWindowInsetsListener((view, insets) -> onWindowInsetsApplied(insets));
setSystemGestureExclusion();
}
void hide() {
if (!isShowing()) {
return;
}
mIsShowing = false;
mDragAnimator.cancel();
mWindowManager.removeView(this);
setOnApplyWindowInsetsListener(null);
setSystemGestureExclusion();
}
boolean isShowing() {
return mIsShowing;
}
boolean isOvalShape() {
return mShapeType == ShapeType.OVAL;
}
void onTargetsChanged(List<AccessibilityTarget> newTargets) {
fadeIn();
mTargets.clear();
mTargets.addAll(newTargets);
onEnabledFeaturesChanged();
updateRadiusWith(mSizeType, mRadiusType, mTargets.size());
updateScrollModeWith(hasExceededMaxLayoutHeight());
setSystemGestureExclusion();
fadeOut();
}
void setSizeType(@SizeType int newSizeType) {
fadeIn();
mSizeType = newSizeType;
updateItemViewWith(newSizeType);
updateRadiusWith(newSizeType, mRadiusType, mTargets.size());
// When the icon sized changed, the menu size and location will be impacted.
updateLocationWith(mPosition);
updateScrollModeWith(hasExceededMaxLayoutHeight());
updateOffsetWith(mShapeType, mAlignment);
setSystemGestureExclusion();
fadeOut();
}
void setShapeType(@ShapeType int newShapeType) {
fadeIn();
mShapeType = newShapeType;
updateOffsetWith(newShapeType, mAlignment);
setOnTouchListener(
newShapeType == ShapeType.OVAL
? null
: (view, event) -> onTouched(event));
fadeOut();
}
public void setOnDragEndListener(OnDragEndListener onDragEndListener) {
mOnDragEndListener = Optional.ofNullable(onDragEndListener);
}
void startTranslateXAnimation() {
fadeIn();
final float toXValue = (mAlignment == Alignment.RIGHT)
? ANIMATION_TO_X_VALUE
: -ANIMATION_TO_X_VALUE;
final TranslateAnimation animation =
new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, toXValue,
Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, 0);
animation.setDuration(ANIMATION_DURATION_MS);
animation.setRepeatMode(Animation.REVERSE);
animation.setInterpolator(new OvershootInterpolator());
animation.setRepeatCount(Animation.INFINITE);
animation.setStartOffset(ANIMATION_START_OFFSET);
mListView.startAnimation(animation);
}
void stopTranslateXAnimation() {
mListView.clearAnimation();
fadeOut();
}
Rect getWindowLocationOnScreen() {
final int left = mCurrentLayoutParams.x;
final int top = mCurrentLayoutParams.y;
return new Rect(left, top, left + getWindowWidth(), top + getWindowHeight());
}
void updateOpacityWith(boolean isFadeEffectEnabled, float newOpacityValue) {
mIsFadeEffectEnabled = isFadeEffectEnabled;
mFadeOutValue = newOpacityValue;
mFadeOutAnimator.cancel();
mFadeOutAnimator.setFloatValues(1.0f, mFadeOutValue);
setAlpha(mIsFadeEffectEnabled ? mFadeOutValue : /* completely opaque */ 1.0f);
}
void onEnabledFeaturesChanged() {
mAdapter.notifyDataSetChanged();
}
@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 marginStartEnd = getMarginStartEndWith(mLastConfiguration);
final Rect touchDelegateBounds =
new Rect(marginStartEnd, mMargin, marginStartEnd + getLayoutWidth(),
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 WindowInsets onWindowInsetsApplied(WindowInsets insets) {
final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
final Rect displayWindowInsetsRect = getDisplayInsets(windowMetrics).toRect();
if (!displayWindowInsetsRect.equals(mDisplayInsetsRect)) {
updateDisplaySizeWith(windowMetrics);
updateLocationWith(mPosition);
}
final Rect imeInsetsRect = windowMetrics.getWindowInsets().getInsets(ime()).toRect();
if (!imeInsetsRect.equals(mImeInsetsRect)) {
if (isImeVisible(imeInsetsRect)) {
mImeInsetsRect.set(imeInsetsRect);
} else {
mImeInsetsRect.setEmpty();
}
updateLocationWith(mPosition);
}
return insets;
}
private boolean isMovingTowardsScreenEdge(@Alignment int side, int currentRawX, int downX) {
return (side == Alignment.RIGHT && currentRawX > downX)
|| (side == Alignment.LEFT && downX > currentRawX);
}
private boolean isImeVisible(Rect imeInsetsRect) {
return imeInsetsRect.left != 0 || imeInsetsRect.top != 0 || imeInsetsRect.right != 0
|| imeInsetsRect.bottom != 0;
}
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() {
return new Handler(requireNonNull(Looper.myLooper(), "looper must not be null"));
}
private void updateDimensions() {
final Resources res = getResources();
updateDisplaySizeWith(mWindowManager.getCurrentWindowMetrics());
mMargin =
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin);
mInset =
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_inset);
mSquareScaledTouchSlop =
sq(ViewConfiguration.get(getContext()).getScaledTouchSlop());
updateItemViewDimensionsWith(mSizeType);
}
private void updateDisplaySizeWith(WindowMetrics metrics) {
final Rect displayBounds = metrics.getBounds();
final Insets displayInsets = getDisplayInsets(metrics);
mDisplayInsetsRect.set(displayInsets.toRect());
displayBounds.inset(displayInsets);
mDisplayWidth = displayBounds.width();
mDisplayHeight = displayBounds.height();
}
private void updateItemViewDimensionsWith(@SizeType int sizeType) {
final Resources res = getResources();
final int paddingResId =
sizeType == SizeType.SMALL
? R.dimen.accessibility_floating_menu_small_padding
: R.dimen.accessibility_floating_menu_large_padding;
mPadding = res.getDimensionPixelSize(paddingResId);
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;
}
private void updateItemViewWith(@SizeType int sizeType) {
updateItemViewDimensionsWith(sizeType);
mAdapter.setItemPadding(mPadding);
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());
mListView.setAccessibilityDelegateCompat(new RecyclerViewAccessibilityDelegate(mListView) {
@NonNull
@Override
public AccessibilityDelegateCompat getItemDelegate() {
return new ItemDelegateCompat(this,
AccessibilityFloatingMenuView.this);
}
});
updateListViewWith(mLastConfiguration);
addView(mListView);
}
private void updateListViewWith(Configuration configuration) {
updateMarginWith(configuration);
final int elevation =
getResources().getDimensionPixelSize(R.dimen.accessibility_floating_menu_elevation);
mListView.setElevation(elevation);
}
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
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
PixelFormat.TRANSLUCENT);
params.receiveInsetsIgnoringZOrder = true;
params.privateFlags |= PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
params.windowAnimations = android.R.style.Animation_Translucent;
params.gravity = Gravity.START | Gravity.TOP;
params.x = (mAlignment == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX();
// params.y = (int) (mPosition.getPercentageY() * getMaxWindowY());
final int currentLayoutY = (int) (mPosition.getPercentageY() * getMaxWindowY());
params.y = Math.max(MIN_WINDOW_Y, currentLayoutY - getInterval());
updateAccessibilityTitle(params);
return params;
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mLastConfiguration.setTo(newConfig);
final int diff = newConfig.diff(mLastConfiguration);
if ((diff & ActivityInfo.CONFIG_LOCALE) != 0) {
updateAccessibilityTitle(mCurrentLayoutParams);
}
updateDimensions();
updateListViewWith(newConfig);
updateItemViewWith(mSizeType);
updateColor();
updateStrokeWith(newConfig.uiMode, mAlignment);
updateLocationWith(mPosition);
updateRadiusWith(mSizeType, mRadiusType, mTargets.size());
updateScrollModeWith(hasExceededMaxLayoutHeight());
setSystemGestureExclusion();
}
@VisibleForTesting
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 getMinWindowX() {
return -getMarginStartEndWith(mLastConfiguration);
}
private int getMaxWindowX() {
return mDisplayWidth - getMarginStartEndWith(mLastConfiguration) - getLayoutWidth();
}
private int getMaxWindowY() {
return mDisplayHeight - getWindowHeight();
}
private InstantInsetLayerDrawable getMenuLayerDrawable() {
return (InstantInsetLayerDrawable) mListView.getBackground();
}
private GradientDrawable getMenuGradientDrawable() {
return (GradientDrawable) getMenuLayerDrawable().getDrawable(INDEX_MENU_ITEM);
}
private Insets getDisplayInsets(WindowMetrics metrics) {
return metrics.getWindowInsets().getInsetsIgnoringVisibility(
systemBars() | displayCutout());
}
/**
* Updates the floating menu to be fixed at the side of the display.
*/
private void updateLocationWith(Position position) {
final @Alignment int alignment = transformToAlignment(position.getPercentageX());
mCurrentLayoutParams.x = (alignment == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX();
final int currentLayoutY = (int) (position.getPercentageY() * getMaxWindowY());
mCurrentLayoutParams.y = Math.max(MIN_WINDOW_Y, currentLayoutY - getInterval());
mWindowManager.updateViewLayout(this, mCurrentLayoutParams);
}
/**
* Gets the moving interval to not overlap between the keyboard and menu view.
*
* @return the moving interval if they overlap each other, otherwise 0.
*/
private int getInterval() {
final int currentLayoutY = (int) (mPosition.getPercentageY() * getMaxWindowY());
final int imeY = mDisplayHeight - mImeInsetsRect.bottom;
final int layoutBottomY = currentLayoutY + getWindowHeight();
return layoutBottomY > imeY ? (layoutBottomY - imeY) : 0;
}
private void updateMarginWith(Configuration configuration) {
// Avoid overlapping with system bars under landscape mode, update the margins of the menu
// to align the edge of system bars.
final int marginStartEnd = getMarginStartEndWith(configuration);
final LayoutParams layoutParams = (FrameLayout.LayoutParams) mListView.getLayoutParams();
layoutParams.setMargins(marginStartEnd, mMargin, marginStartEnd, mMargin);
mListView.setLayoutParams(layoutParams);
}
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 updateScrollModeWith(boolean hasExceededMaxLayoutHeight) {
mListView.setOverScrollMode(hasExceededMaxLayoutHeight
? OVER_SCROLL_ALWAYS
: OVER_SCROLL_NEVER);
}
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 updateAccessibilityTitle(WindowManager.LayoutParams params) {
params.accessibilityTitle = getResources().getString(
com.android.internal.R.string.accessibility_select_shortcut_menu_title);
}
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);
}
@VisibleForTesting
boolean hasExceededMaxLayoutHeight() {
return calculateActualLayoutHeight() > getMaxLayoutHeight();
}
@Alignment
private int transformToAlignment(@FloatRange(from = 0.0, to = 1.0) float percentageX) {
return (percentageX < 0.5f) ? Alignment.LEFT : Alignment.RIGHT;
}
private float transformCurrentPercentageXToEdge() {
final float percentageX = calculateCurrentPercentageX();
return (percentageX < 0.5) ? 0.0f : 1.0f;
}
private float calculateCurrentPercentageX() {
return mCurrentLayoutParams.x / (float) getMaxWindowX();
}
private float calculateCurrentPercentageY() {
return mCurrentLayoutParams.y / (float) getMaxWindowY();
}
private int calculateActualLayoutHeight() {
return (mPadding + mIconHeight) * mTargets.size() + mPadding;
}
private int getMarginStartEndWith(Configuration configuration) {
return configuration != null
&& configuration.orientation == ORIENTATION_PORTRAIT
? mMargin : 0;
}
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;
}
@VisibleForTesting
Rect getAvailableBounds() {
return new Rect(0, 0, mDisplayWidth - getWindowWidth(),
mDisplayHeight - getWindowHeight());
}
private int getMaxLayoutHeight() {
return mDisplayHeight - mMargin * 2;
}
private int getLayoutWidth() {
return mPadding * 2 + mIconWidth;
}
private int getLayoutHeight() {
return Math.min(getMaxLayoutHeight(), calculateActualLayoutHeight());
}
private int getWindowWidth() {
return getMarginStartEndWith(mLastConfiguration) * 2 + getLayoutWidth();
}
private int getWindowHeight() {
return Math.min(mDisplayHeight, 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

@@ -1,299 +0,0 @@
/*
* 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.TypedValue.COMPLEX_UNIT_PX;
import static android.view.View.MeasureSpec.AT_MOST;
import static android.view.View.MeasureSpec.UNSPECIFIED;
import android.annotation.UiContext;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.os.Bundle;
import android.text.method.MovementMethod;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.settingslib.Utils;
import com.android.systemui.R;
import com.android.systemui.recents.TriangleShape;
/**
* Base tooltip view that shows the information about the operation of the
* Accessibility floating menu. In addition, the anchor view is only for {@link
* AccessibilityFloatingMenuView}, it should be more suited for displaying one-off menus to avoid
* the performance hit for the extra window.
*/
class BaseTooltipView extends FrameLayout {
private int mFontSize;
private int mTextViewMargin;
private int mTextViewPadding;
private int mTextViewCornerRadius;
private int mArrowMargin;
private int mArrowWidth;
private int mArrowHeight;
private int mArrowCornerRadius;
private int mScreenWidth;
private boolean mIsShowing;
private TextView mTextView;
private final WindowManager.LayoutParams mCurrentLayoutParams;
private final WindowManager mWindowManager;
private final AccessibilityFloatingMenuView mAnchorView;
BaseTooltipView(@UiContext Context context, AccessibilityFloatingMenuView anchorView) {
super(context);
mWindowManager = context.getSystemService(WindowManager.class);
mAnchorView = anchorView;
mCurrentLayoutParams = createDefaultLayoutParams();
initViews();
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mAnchorView.onConfigurationChanged(newConfig);
updateTooltipView();
mWindowManager.updateViewLayout(this, mCurrentLayoutParams);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
hide();
}
return super.onTouchEvent(event);
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
info.addAction(AccessibilityAction.ACTION_DISMISS);
}
@Override
public boolean performAccessibilityAction(int action, Bundle arguments) {
if (action == AccessibilityAction.ACTION_DISMISS.getId()) {
hide();
return true;
}
return super.performAccessibilityAction(action, arguments);
}
void show() {
if (isShowing()) {
return;
}
mIsShowing = true;
updateTooltipView();
mWindowManager.addView(this, mCurrentLayoutParams);
}
void hide() {
if (!isShowing()) {
return;
}
mIsShowing = false;
mWindowManager.removeView(this);
}
void setDescription(CharSequence text) {
mTextView.setText(text);
}
void setMovementMethod(MovementMethod movement) {
mTextView.setMovementMethod(movement);
}
private boolean isShowing() {
return mIsShowing;
}
private void initViews() {
final View contentView =
LayoutInflater.from(getContext()).inflate(
R.layout.accessibility_floating_menu_tooltip, this, false);
mTextView = contentView.findViewById(R.id.text);
addView(contentView);
}
private static 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
| WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
PixelFormat.TRANSLUCENT);
params.windowAnimations = android.R.style.Animation_Translucent;
params.gravity = Gravity.START | Gravity.TOP;
return params;
}
private void updateDimensions() {
final Resources res = getResources();
final DisplayMetrics dm = res.getDisplayMetrics();
mScreenWidth = dm.widthPixels;
mArrowWidth =
res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_arrow_width);
mArrowHeight =
res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_arrow_height);
mArrowMargin =
res.getDimensionPixelSize(
R.dimen.accessibility_floating_tooltip_arrow_margin);
mArrowCornerRadius =
res.getDimensionPixelSize(
R.dimen.accessibility_floating_tooltip_arrow_corner_radius);
mFontSize =
res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_font_size);
mTextViewMargin =
res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_margin);
mTextViewPadding =
res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_padding);
mTextViewCornerRadius =
res.getDimensionPixelSize(
R.dimen.accessibility_floating_tooltip_text_corner_radius);
}
private void updateTooltipView() {
updateDimensions();
updateTextView();
final Rect anchorViewLocation = mAnchorView.getWindowLocationOnScreen();
updateArrowWith(anchorViewLocation);
updateWidthWith(anchorViewLocation);
updateLocationWith(anchorViewLocation);
}
private void updateTextView() {
mTextView.setTextSize(COMPLEX_UNIT_PX, mFontSize);
mTextView.setPadding(mTextViewPadding, mTextViewPadding, mTextViewPadding,
mTextViewPadding);
final GradientDrawable gradientDrawable = (GradientDrawable) mTextView.getBackground();
gradientDrawable.setCornerRadius(mTextViewCornerRadius);
gradientDrawable.setColor(Utils.getColorAttrDefaultColor(getContext(),
com.android.internal.R.attr.colorAccentPrimary));
}
private void updateArrowWith(Rect anchorViewLocation) {
final boolean isAnchorViewOnLeft = isAnchorViewOnLeft(anchorViewLocation);
final View arrowView = findViewById(isAnchorViewOnLeft
? R.id.arrow_left
: R.id.arrow_right);
arrowView.setVisibility(VISIBLE);
drawArrow(arrowView, isAnchorViewOnLeft);
final LinearLayout.LayoutParams layoutParams =
(LinearLayout.LayoutParams) arrowView.getLayoutParams();
layoutParams.width = mArrowWidth;
layoutParams.height = mArrowHeight;
final int leftMargin = isAnchorViewOnLeft ? 0 : mArrowMargin;
final int rightMargin = isAnchorViewOnLeft ? mArrowMargin : 0;
layoutParams.setMargins(leftMargin, 0, rightMargin, 0);
arrowView.setLayoutParams(layoutParams);
}
private void updateWidthWith(Rect anchorViewLocation) {
final ViewGroup.LayoutParams layoutParams = mTextView.getLayoutParams();
layoutParams.width = getTextWidthWith(anchorViewLocation);
mTextView.setLayoutParams(layoutParams);
}
private void updateLocationWith(Rect anchorViewLocation) {
mCurrentLayoutParams.x = isAnchorViewOnLeft(anchorViewLocation)
? anchorViewLocation.width()
: mScreenWidth - getWindowWidthWith(anchorViewLocation)
- anchorViewLocation.width();
mCurrentLayoutParams.y =
anchorViewLocation.centerY() - (getTextHeightWith(anchorViewLocation) / 2);
}
private void drawArrow(View view, boolean isPointingLeft) {
final ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
final TriangleShape triangleShape =
TriangleShape.createHorizontal(layoutParams.width, layoutParams.height,
isPointingLeft);
final ShapeDrawable arrowDrawable = new ShapeDrawable(triangleShape);
final Paint arrowPaint = arrowDrawable.getPaint();
arrowPaint.setColor(Utils.getColorAttrDefaultColor(getContext(),
com.android.internal.R.attr.colorAccentPrimary));
final CornerPathEffect effect = new CornerPathEffect(mArrowCornerRadius);
arrowPaint.setPathEffect(effect);
view.setBackground(arrowDrawable);
}
private boolean isAnchorViewOnLeft(Rect anchorViewLocation) {
return anchorViewLocation.left < (mScreenWidth / 2);
}
private int getTextWidthWith(Rect anchorViewLocation) {
final int widthSpec =
MeasureSpec.makeMeasureSpec(getAvailableTextWidthWith(anchorViewLocation), AT_MOST);
final int heightSpec =
MeasureSpec.makeMeasureSpec(0, UNSPECIFIED);
mTextView.measure(widthSpec, heightSpec);
return mTextView.getMeasuredWidth();
}
private int getTextHeightWith(Rect anchorViewLocation) {
final int widthSpec =
MeasureSpec.makeMeasureSpec(getAvailableTextWidthWith(anchorViewLocation), AT_MOST);
final int heightSpec =
MeasureSpec.makeMeasureSpec(0, UNSPECIFIED);
mTextView.measure(widthSpec, heightSpec);
return mTextView.getMeasuredHeight();
}
private int getAvailableTextWidthWith(Rect anchorViewLocation) {
return mScreenWidth - anchorViewLocation.width() - mArrowWidth - mArrowMargin
- mTextViewMargin;
}
private int getWindowWidthWith(Rect anchorViewLocation) {
return getTextWidthWith(anchorViewLocation) + mArrowWidth + mArrowMargin;
}
}

View File

@@ -1,50 +0,0 @@
/*
* 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.content.Context;
import com.android.systemui.R;
/**
* Dock tooltip view that shows the info about moving the Accessibility button to the edge to hide.
*/
class DockTooltipView extends BaseTooltipView {
private final AccessibilityFloatingMenuView mAnchorView;
DockTooltipView(Context context, AccessibilityFloatingMenuView anchorView) {
super(context, anchorView);
mAnchorView = anchorView;
setDescription(
getContext().getText(R.string.accessibility_floating_button_docking_tooltip));
}
@Override
void hide() {
super.hide();
mAnchorView.stopTranslateXAnimation();
}
@Override
void show() {
super.show();
mAnchorView.startTranslateXAnimation();
}
}

View File

@@ -1,141 +0,0 @@
/*
* 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.content.res.Resources;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
import com.android.systemui.R;
import com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.ShapeType;
import java.lang.ref.WeakReference;
/**
* An accessibility item delegate for the individual items of the list view
* {@link AccessibilityFloatingMenuView}.
*/
final class ItemDelegateCompat extends RecyclerViewAccessibilityDelegate.ItemDelegate {
private final WeakReference<AccessibilityFloatingMenuView> mMenuViewRef;
ItemDelegateCompat(@NonNull RecyclerViewAccessibilityDelegate recyclerViewDelegate,
AccessibilityFloatingMenuView menuView) {
super(recyclerViewDelegate);
this.mMenuViewRef = new WeakReference<>(menuView);
}
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
if (mMenuViewRef.get() == null) {
return;
}
final AccessibilityFloatingMenuView menuView = mMenuViewRef.get();
final Resources res = menuView.getResources();
final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveTopLeft =
new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.action_move_top_left,
res.getString(
R.string.accessibility_floating_button_action_move_top_left));
info.addAction(moveTopLeft);
final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveTopRight =
new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
R.id.action_move_top_right,
res.getString(
R.string.accessibility_floating_button_action_move_top_right));
info.addAction(moveTopRight);
final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveBottomLeft =
new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
R.id.action_move_bottom_left,
res.getString(
R.string.accessibility_floating_button_action_move_bottom_left));
info.addAction(moveBottomLeft);
final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveBottomRight =
new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
R.id.action_move_bottom_right,
res.getString(
R.string.accessibility_floating_button_action_move_bottom_right));
info.addAction(moveBottomRight);
final int moveEdgeId = menuView.isOvalShape()
? R.id.action_move_to_edge_and_hide
: R.id.action_move_out_edge_and_show;
final int moveEdgeTextResId = menuView.isOvalShape()
? R.string.accessibility_floating_button_action_move_to_edge_and_hide_to_half
: R.string.accessibility_floating_button_action_move_out_edge_and_show;
final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveToOrOutEdge =
new AccessibilityNodeInfoCompat.AccessibilityActionCompat(moveEdgeId,
res.getString(moveEdgeTextResId));
info.addAction(moveToOrOutEdge);
}
@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
if (mMenuViewRef.get() == null) {
return super.performAccessibilityAction(host, action, args);
}
final AccessibilityFloatingMenuView menuView = mMenuViewRef.get();
menuView.fadeIn();
final Rect bounds = menuView.getAvailableBounds();
if (action == R.id.action_move_top_left) {
menuView.setShapeType(ShapeType.OVAL);
menuView.snapToLocation(bounds.left, bounds.top);
return true;
}
if (action == R.id.action_move_top_right) {
menuView.setShapeType(ShapeType.OVAL);
menuView.snapToLocation(bounds.right, bounds.top);
return true;
}
if (action == R.id.action_move_bottom_left) {
menuView.setShapeType(ShapeType.OVAL);
menuView.snapToLocation(bounds.left, bounds.bottom);
return true;
}
if (action == R.id.action_move_bottom_right) {
menuView.setShapeType(ShapeType.OVAL);
menuView.snapToLocation(bounds.right, bounds.bottom);
return true;
}
if (action == R.id.action_move_to_edge_and_hide) {
menuView.setShapeType(ShapeType.HALF_OVAL);
return true;
}
if (action == R.id.action_move_out_edge_and_show) {
menuView.setShapeType(ShapeType.OVAL);
return true;
}
return super.performAccessibilityAction(host, action, args);
}
}

View File

@@ -1,52 +0,0 @@
/*
* 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.ACCESSIBILITY_BUTTON_COMPONENT_NAME;
import android.content.Context;
import android.content.Intent;
import android.provider.Settings;
import android.text.method.LinkMovementMethod;
import com.android.systemui.R;
/**
* Migration tooltip view that shows the information about the Accessibility button was replaced
* with the floating menu.
*/
class MigrationTooltipView extends BaseTooltipView {
MigrationTooltipView(Context context, AccessibilityFloatingMenuView anchorView) {
super(context, anchorView);
final Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_DETAILS_SETTINGS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Intent.EXTRA_COMPONENT_NAME,
ACCESSIBILITY_BUTTON_COMPONENT_NAME.flattenToShortString());
final AnnotationLinkSpan.LinkInfo linkInfo = new AnnotationLinkSpan.LinkInfo(
AnnotationLinkSpan.LinkInfo.DEFAULT_ANNOTATION,
v -> {
getContext().startActivity(intent);
hide();
});
final int textResId = R.string.accessibility_floating_button_migration_tooltip;
setDescription(AnnotationLinkSpan.linkify(getContext().getText(textResId), linkInfo));
setMovementMethod(LinkMovementMethod.getInstance());
}
}

View File

@@ -538,12 +538,6 @@ object Flags {
// TODO(b/266983474) Tracking Bug
val SHARESHEET_IMAGE_AND_TEXT_PREVIEW = unreleasedFlag(1503, "sharesheet_image_text_preview")
// 1600 - accessibility
// TODO(b/262224538): Tracking Bug
@JvmField
val A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS =
releasedFlag(1600, "a11y_floating_menu_fling_spring_animations")
// 1700 - clipboard
@JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior")

View File

@@ -19,8 +19,6 @@ package com.android.systemui.accessibility.floatingmenu;
import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU;
import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR;
import static com.android.systemui.flags.Flags.A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -46,7 +44,6 @@ import com.android.systemui.Dependency;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.util.settings.SecureSettings;
@@ -74,6 +71,7 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase {
public MockitoRule mockito = MockitoJUnit.rule();
private Context mContextWrapper;
private WindowManager mWindowManager;
private AccessibilityManager mAccessibilityManager;
private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
private AccessibilityFloatingMenuController mController;
@@ -97,6 +95,7 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase {
}
};
mWindowManager = mContext.getSystemService(WindowManager.class);
mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
mLastButtonTargets = Settings.Secure.getStringForUser(mContextWrapper.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, UserHandle.USER_CURRENT);
@@ -158,7 +157,8 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase {
public void onKeyguardVisibilityChanged_showing_destroyWidget() {
enableAccessibilityFloatingMenuConfig();
mController = setUpController();
mController.mFloatingMenu = new AccessibilityFloatingMenu(mContextWrapper, mSecureSettings);
mController.mFloatingMenu = new MenuViewLayerController(mContextWrapper, mWindowManager,
mAccessibilityManager, mSecureSettings);
captureKeyguardUpdateMonitorCallback();
mKeyguardCallback.onUserUnlocked();
@@ -184,7 +184,8 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase {
final int fakeUserId = 1;
enableAccessibilityFloatingMenuConfig();
mController = setUpController();
mController.mFloatingMenu = new AccessibilityFloatingMenu(mContextWrapper, mSecureSettings);
mController.mFloatingMenu = new MenuViewLayerController(mContextWrapper, mWindowManager,
mAccessibilityManager, mSecureSettings);
captureKeyguardUpdateMonitorCallback();
mKeyguardCallback.onUserSwitching(fakeUserId);
@@ -197,7 +198,8 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase {
final int fakeUserId = 1;
enableAccessibilityFloatingMenuConfig();
mController = setUpController();
mController.mFloatingMenu = new AccessibilityFloatingMenu(mContextWrapper, mSecureSettings);
mController.mFloatingMenu = new MenuViewLayerController(mContextWrapper, mWindowManager,
mAccessibilityManager, mSecureSettings);
captureKeyguardUpdateMonitorCallback();
mKeyguardCallback.onUserUnlocked();
mKeyguardCallback.onKeyguardVisibilityChanged(true);
@@ -317,41 +319,19 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase {
assertThat(mController.mFloatingMenu).isNull();
}
@Test
public void onTargetsChanged_flingSpringAnimationsDisabled_floatingMenuIsCreated() {
Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU,
UserHandle.USER_CURRENT);
final FakeFeatureFlags featureFlags = new FakeFeatureFlags();
featureFlags.set(A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS, false);
mController = setUpController();
mController.onAccessibilityButtonTargetsChanged(TEST_A11Y_BTN_TARGETS);
assertThat(mController.mFloatingMenu).isInstanceOf(AccessibilityFloatingMenu.class);
}
@Test
public void onTargetsChanged_isFloatingViewLayerControllerCreated() {
Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU,
UserHandle.USER_CURRENT);
final FakeFeatureFlags featureFlags = new FakeFeatureFlags();
featureFlags.set(A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS, true);
mController = setUpController(featureFlags);
mController = setUpController();
mController.onAccessibilityButtonTargetsChanged(TEST_A11Y_BTN_TARGETS);
assertThat(mController.mFloatingMenu).isInstanceOf(MenuViewLayerController.class);
}
private AccessibilityFloatingMenuController setUpController() {
final FakeFeatureFlags featureFlags = new FakeFeatureFlags();
featureFlags.set(A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS, false);
return setUpController(featureFlags);
}
private AccessibilityFloatingMenuController setUpController(FakeFeatureFlags featureFlags) {
final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
final FakeDisplayTracker displayTracker = new FakeDisplayTracker(mContext);
@@ -361,7 +341,7 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase {
final AccessibilityFloatingMenuController controller =
new AccessibilityFloatingMenuController(mContextWrapper, windowManager,
displayManager, mAccessibilityManager, mTargetsObserver, mModeObserver,
mKeyguardUpdateMonitor, featureFlags, mSecureSettings, displayTracker);
mKeyguardUpdateMonitor, mSecureSettings, displayTracker);
controller.init();
return controller;

View File

@@ -1,108 +0,0 @@
/*
* 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 android.content.Context;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.accessibility.AccessibilityManager;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.util.settings.SecureSettings;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.util.ArrayList;
import java.util.List;
/** Tests for {@link AccessibilityFloatingMenu}. */
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class AccessibilityFloatingMenuTest extends SysuiTestCase {
@Rule
public MockitoRule mockito = MockitoJUnit.rule();
@Mock
private AccessibilityManager mAccessibilityManager;
@Mock
private SecureSettings mSecureSettings;
private AccessibilityFloatingMenuView mMenuView;
private AccessibilityFloatingMenu mMenu;
@Before
public void initMenu() {
final List<String> assignedTargets = new ArrayList<>();
mContext.addMockSystemService(Context.ACCESSIBILITY_SERVICE, mAccessibilityManager);
assignedTargets.add(MAGNIFICATION_CONTROLLER_NAME);
doReturn(assignedTargets).when(mAccessibilityManager).getAccessibilityShortcutTargets(
anyInt());
final Position position = new Position(0, 0);
mMenuView = new AccessibilityFloatingMenuView(mContext, position);
mMenu = new AccessibilityFloatingMenu(mContext, mSecureSettings, 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();
}
@Test
public void showMenuView_emptyTarget_notShow() {
final List<String> emptyTargets = new ArrayList<>();
doReturn(emptyTargets).when(mAccessibilityManager).getAccessibilityShortcutTargets(
anyInt());
mMenu.show();
assertThat(mMenuView.isShowing()).isFalse();
}
@After
public void tearDown() {
mMenu.hide();
}
}

View File

@@ -1,527 +0,0 @@
/*
* 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.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.view.View.OVER_SCROLL_ALWAYS;
import static android.view.View.OVER_SCROLL_NEVER;
import static android.view.WindowInsets.Type.displayCutout;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsets.Type.systemBars;
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 static org.mockito.Mockito.when;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.Rect;
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.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
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.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** Tests for {@link AccessibilityFloatingMenuView}. */
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class AccessibilityFloatingMenuViewTest extends SysuiTestCase {
@Rule
public MockitoRule mockito = MockitoJUnit.rule();
private final MotionEventHelper mMotionEventHelper = new MotionEventHelper();
private final List<AccessibilityTarget> mTargets = new ArrayList<>(
Collections.singletonList(mock(AccessibilityTarget.class)));
private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);
@Mock
private WindowManager mWindowManager;
@Mock
private ViewPropertyAnimator mAnimator;
@Mock
private WindowMetrics mWindowMetrics;
private MotionEvent mInterceptMotionEvent;
private AccessibilityFloatingMenuView mMenuView;
private RecyclerView mListView = new RecyclerView(mContext);
private int mMenuWindowHeight;
private int mMenuHalfWidth;
private int mMenuHalfHeight;
private int mDisplayHalfWidth;
private int mDisplayHalfHeight;
private int mMaxWindowX;
private int mMaxWindowY;
private final int mDisplayWindowWidth = 1080;
private final int mDisplayWindowHeight = 2340;
@Before
public void initMenuView() {
final WindowManager wm = mContext.getSystemService(WindowManager.class);
doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
mWindowManager).getMaximumWindowMetrics();
mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
when(mWindowMetrics.getBounds()).thenReturn(new Rect(0, 0, mDisplayWindowWidth,
mDisplayWindowHeight));
when(mWindowMetrics.getWindowInsets()).thenReturn(fakeDisplayInsets());
mMenuView = spy(
new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition, mListView));
}
@Before
public void setUpMatrices() {
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_small_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;
mMenuHalfWidth = menuWidth / 2;
mMenuHalfHeight = menuHeight / 2;
mDisplayHalfWidth = mDisplayWindowWidth / 2;
mDisplayHalfHeight = mDisplayWindowHeight / 2;
int marginStartEnd =
mContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT
? margin : 0;
mMaxWindowX = mDisplayWindowWidth - marginStartEnd - menuWidth;
mMenuWindowHeight = menuHeight + margin * 2;
mMaxWindowY = mDisplayWindowHeight - mMenuWindowHeight;
}
@Test
public void initListView_success() {
assertThat(mListView.getCompatAccessibilityDelegate()).isNotNull();
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 hideMenuViewWhenStartingAnimation_animatorNotRunning() {
mMenuView.show();
mMenuView.mDragAnimator.start();
mMenuView.hide();
assertThat(mMenuView.mDragAnimator.isRunning()).isFalse();
}
@Test
public void onTargetsChanged_singleTarget_expectedRadii() {
final Position alignRightPosition = new Position(1.0f, 0.0f);
final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
alignRightPosition);
setupBasicMenuView(menuView);
menuView.onTargetsChanged(mTargets);
final View view = menuView.getChildAt(0);
final LayerDrawable layerDrawable = (LayerDrawable) view.getBackground();
final GradientDrawable gradientDrawable =
(GradientDrawable) layerDrawable.getDrawable(0);
final float smallRadius =
getContext().getResources().getDimensionPixelSize(
R.dimen.accessibility_floating_menu_small_single_radius);
final float[] expectedRadii =
new float[]{smallRadius, smallRadius, 0.0f, 0.0f, 0.0f, 0.0f, smallRadius,
smallRadius};
assertThat(gradientDrawable.getCornerRadii()).isEqualTo(expectedRadii);
}
@Test
public void setSizeType_alignRightAndLargeSize_expectedRadii() {
final RecyclerView listView = spy(new RecyclerView(mContext));
final Position alignRightPosition = new Position(1.0f, 0.0f);
final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
alignRightPosition, listView);
setupBasicMenuView(menuView);
menuView.setSizeType(/* largeSize */ 1);
final LayerDrawable layerDrawable =
(LayerDrawable) listView.getBackground();
final GradientDrawable gradientDrawable =
(GradientDrawable) layerDrawable.getDrawable(0);
final float largeRadius = getContext().getResources().getDimensionPixelSize(
R.dimen.accessibility_floating_menu_large_single_radius);
final float[] expectedRadii =
new float[] {largeRadius, largeRadius, 0.0f, 0.0f, 0.0f, 0.0f, largeRadius,
largeRadius};
assertThat(gradientDrawable.getCornerRadii()).isEqualTo(expectedRadii);
}
@Test
public void setShapeType_halfCircle_translationX() {
final RecyclerView listView = spy(new RecyclerView(mContext));
final AccessibilityFloatingMenuView menuView =
new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition, listView);
setupBasicMenuView(menuView);
doReturn(mAnimator).when(listView).animate();
menuView.setShapeType(/* halfOvalShape */ 1);
verify(mAnimator).translationX(anyFloat());
}
@Test
public void onTargetsChanged_fadeInOut() {
final InOrder inOrderMenuView = inOrder(mMenuView);
mMenuView.onTargetsChanged(mTargets);
inOrderMenuView.verify(mMenuView).fadeIn();
inOrderMenuView.verify(mMenuView).fadeOut();
}
@Test
public void setSizeType_fadeInOut() {
final InOrder inOrderMenuView = inOrder(mMenuView);
mMenuView.setSizeType(/* smallSize */ 0);
inOrderMenuView.verify(mMenuView).fadeIn();
inOrderMenuView.verify(mMenuView).fadeOut();
}
@Test
public void tapOnAndDragMenu_interceptUpEvent() {
final RecyclerView listView = new RecyclerView(mContext);
final TestAccessibilityFloatingMenu menuView =
new TestAccessibilityFloatingMenu(mContext, mPlaceholderPosition, listView);
setupBasicMenuView(menuView);
final int currentWindowX = menuView.mCurrentLayoutParams.x;
final int currentWindowY = menuView.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,
/* displayCenterX */mDisplayHalfWidth
- /* offsetXToDisplayLeftHalfRegion */ 10,
/* displayCenterY */ mDisplayHalfHeight);
final MotionEvent upEvent =
mMotionEventHelper.obtainMotionEvent(4, 5,
MotionEvent.ACTION_UP,
/* displayCenterX */ mDisplayHalfWidth
- /* offsetXToDisplayLeftHalfRegion */ 10,
/* displayCenterY */ mDisplayHalfHeight);
listView.dispatchTouchEvent(downEvent);
listView.dispatchTouchEvent(moveEvent);
listView.dispatchTouchEvent(upEvent);
assertThat(mInterceptMotionEvent.getAction()).isEqualTo(MotionEvent.ACTION_UP);
}
@Test
public void tapOnAndDragMenu_matchLocation() {
final float expectedX = 1.0f;
final float expectedY = 0.7f;
final Position position = new Position(expectedX, expectedY);
final RecyclerView listView = new RecyclerView(mContext);
final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
position, listView);
setupBasicMenuView(menuView);
final int currentWindowX = menuView.mCurrentLayoutParams.x;
final int currentWindowY = menuView.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,
/* displayCenterX */mDisplayHalfWidth
+ /* offsetXToDisplayRightHalfRegion */ 10,
/* displayCenterY */ mDisplayHalfHeight);
final MotionEvent upEvent =
mMotionEventHelper.obtainMotionEvent(4, 5,
MotionEvent.ACTION_UP,
/* displayCenterX */ mDisplayHalfWidth
+ /* offsetXToDisplayRightHalfRegion */ 10,
/* displayCenterY */ mDisplayHalfHeight);
listView.dispatchTouchEvent(downEvent);
listView.dispatchTouchEvent(moveEvent);
listView.dispatchTouchEvent(upEvent);
menuView.mDragAnimator.end();
assertThat((float) menuView.mCurrentLayoutParams.x).isWithin(1.0f).of(mMaxWindowX);
assertThat((float) menuView.mCurrentLayoutParams.y).isWithin(1.0f).of(
/* newWindowY = displayCenterY - offsetY */ mDisplayHalfHeight - mMenuHalfHeight);
}
@Test
public void tapOnAndDragMenuToDisplaySide_transformShapeHalfOval() {
final Position alignRightPosition = new Position(1.0f, 0.8f);
final RecyclerView listView = new RecyclerView(mContext);
final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
alignRightPosition, listView);
setupBasicMenuView(menuView);
final int currentWindowX = menuView.mCurrentLayoutParams.x;
final int currentWindowY = menuView.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)
+ /* offsetXToDisplayRightSide */ mMenuHalfWidth,
/* downY */ (currentWindowY + mMenuHalfHeight));
final MotionEvent upEvent =
mMotionEventHelper.obtainMotionEvent(4, 5,
MotionEvent.ACTION_UP,
/* downX */(currentWindowX + mMenuHalfWidth)
+ /* offsetXToDisplayRightSide */ mMenuHalfWidth,
/* downY */ (currentWindowY + mMenuHalfHeight));
listView.dispatchTouchEvent(downEvent);
listView.dispatchTouchEvent(moveEvent);
listView.dispatchTouchEvent(upEvent);
assertThat(menuView.mShapeType).isEqualTo(/* halfOval */ 1);
}
@Test
public void onTargetsChanged_exceedAvailableHeight_overScrollAlways() {
doReturn(true).when(mMenuView).hasExceededMaxLayoutHeight();
mMenuView.onTargetsChanged(mTargets);
assertThat(mListView.getOverScrollMode()).isEqualTo(OVER_SCROLL_ALWAYS);
}
@Test
public void onTargetsChanged_notExceedAvailableHeight_overScrollNever() {
doReturn(false).when(mMenuView).hasExceededMaxLayoutHeight();
mMenuView.onTargetsChanged(mTargets);
assertThat(mListView.getOverScrollMode()).isEqualTo(OVER_SCROLL_NEVER);
}
@Test
public void showMenuView_insetsListener_overlapWithIme_menuViewShifted() {
final int offset = 200;
final Position alignRightPosition = new Position(1.0f, 0.8f);
final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
alignRightPosition);
setupBasicMenuView(menuView);
final WindowInsets imeInset = fakeImeInsetWith(menuView, offset);
when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
when(mWindowMetrics.getWindowInsets()).thenReturn(imeInset);
final int expectedLayoutY = menuView.mCurrentLayoutParams.y - offset;
menuView.dispatchApplyWindowInsets(imeInset);
assertThat(menuView.mCurrentLayoutParams.y).isEqualTo(expectedLayoutY);
}
@Test
public void hideIme_onMenuViewShifted_menuViewMovedBack() {
final int offset = 200;
setupBasicMenuView(mMenuView);
final WindowInsets imeInset = fakeImeInsetWith(mMenuView, offset);
when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
when(mWindowMetrics.getWindowInsets()).thenReturn(imeInset);
final int expectedLayoutY = mMenuView.mCurrentLayoutParams.y;
mMenuView.dispatchApplyWindowInsets(imeInset);
mMenuView.dispatchApplyWindowInsets(
new WindowInsets.Builder().setVisible(ime(), false).build());
assertThat(mMenuView.mCurrentLayoutParams.y).isEqualTo(expectedLayoutY);
}
@Test
public void showMenuAndIme_withHigherIme_alignDisplayTopEdge() {
final int offset = 99999;
setupBasicMenuView(mMenuView);
final WindowInsets imeInset = fakeImeInsetWith(mMenuView, offset);
when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
when(mWindowMetrics.getWindowInsets()).thenReturn(imeInset);
mMenuView.dispatchApplyWindowInsets(imeInset);
assertThat(mMenuView.mCurrentLayoutParams.y).isEqualTo(0);
}
@Test
public void testConstructor_withPosition_expectedPosition() {
final float expectedX = 1.0f;
final float expectedY = 0.7f;
final Position position = new Position(expectedX, expectedY);
final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
position);
setupBasicMenuView(menuView);
assertThat((float) menuView.mCurrentLayoutParams.x).isWithin(1.0f).of(mMaxWindowX);
assertThat((float) menuView.mCurrentLayoutParams.y).isWithin(1.0f).of(
expectedY * mMaxWindowY);
}
@After
public void tearDown() {
mInterceptMotionEvent = null;
mMotionEventHelper.recycleEvents();
mListView = null;
}
private void setupBasicMenuView(AccessibilityFloatingMenuView menuView) {
menuView.show();
menuView.onTargetsChanged(mTargets);
menuView.setSizeType(0);
menuView.setShapeType(0);
}
/**
* Based on the current menu status, fake the ime inset component {@link WindowInsets} used
* for testing.
*
* @param menuView {@link AccessibilityFloatingMenuView} that needs to be changed
* @param offset is used for the y-axis position of ime higher than the y-axis position of menu
* @return the ime inset
*/
private WindowInsets fakeImeInsetWith(AccessibilityFloatingMenuView menuView, int offset) {
// Ensure the keyboard has overlapped on the menu view.
final int fakeImeHeight =
mDisplayWindowHeight - (menuView.mCurrentLayoutParams.y + mMenuWindowHeight)
+ offset;
return new WindowInsets.Builder()
.setVisible(ime(), true)
.setInsets(ime(), Insets.of(0, 0, 0, fakeImeHeight))
.build();
}
private WindowInsets fakeDisplayInsets() {
final int fakeStatusBarHeight = 75;
final int fakeNavigationBarHeight = 125;
return new WindowInsets.Builder()
.setVisible(systemBars() | displayCutout(), true)
.setInsets(systemBars() | displayCutout(),
Insets.of(0, fakeStatusBarHeight, 0, fakeNavigationBarHeight))
.build();
}
private class TestAccessibilityFloatingMenu extends AccessibilityFloatingMenuView {
TestAccessibilityFloatingMenu(Context context, Position position, RecyclerView listView) {
super(context, position, 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

@@ -1,133 +0,0 @@
/*
* 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.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.MotionEvent;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import android.view.accessibility.AccessibilityNodeInfo;
import androidx.test.filters.SmallTest;
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.Mock;
import org.mockito.MockitoAnnotations;
/** Tests for {@link BaseTooltipView}. */
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class BaseTooltipViewTest extends SysuiTestCase {
@Mock
private WindowManager mWindowManager;
@Mock
private WindowMetrics mWindowMetrics;
private AccessibilityFloatingMenuView mMenuView;
private BaseTooltipView mToolTipView;
private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);
private final MotionEventHelper mMotionEventHelper = new MotionEventHelper();
@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);
when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
when(mWindowMetrics.getBounds()).thenReturn(new Rect());
when(mWindowMetrics.getWindowInsets()).thenReturn(new WindowInsets.Builder().build());
mMenuView = new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition);
mToolTipView = new BaseTooltipView(mContext, mMenuView);
}
@Test
public void showToolTipView_success() {
mToolTipView.show();
verify(mWindowManager).addView(eq(mToolTipView), any(WindowManager.LayoutParams.class));
}
@Test
public void touchOutsideWhenToolTipViewShown_dismiss() {
final MotionEvent outsideEvent =
mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0,
/* eventTime= */1,
MotionEvent.ACTION_OUTSIDE,
/* x= */ 0,
/* y= */ 0);
mToolTipView.show();
mToolTipView.dispatchTouchEvent(outsideEvent);
verify(mWindowManager).removeView(mToolTipView);
}
@Test
public void getAccessibilityActionList_matchResult() {
final AccessibilityNodeInfo infos = new AccessibilityNodeInfo();
mToolTipView.onInitializeAccessibilityNodeInfo(infos);
assertThat(infos.getActionList().size()).isEqualTo(1);
}
@Test
public void accessibilityAction_dismiss_success() {
final BaseTooltipView tooltipView =
spy(new BaseTooltipView(mContext, mMenuView));
final boolean isActionPerformed =
tooltipView.performAccessibilityAction(
AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS.getId(),
/* arguments= */ null);
assertThat(isActionPerformed).isTrue();
verify(tooltipView).hide();
}
@After
public void tearDown() {
mToolTipView.hide();
mMotionEventHelper.recycleEvents();
}
}

View File

@@ -1,116 +0,0 @@
/*
* 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 org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.MotionEvent;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import androidx.test.filters.SmallTest;
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.Mock;
import org.mockito.MockitoAnnotations;
/** Tests for {@link DockTooltipView}. */
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class DockTooltipViewTest extends SysuiTestCase {
@Mock
private WindowManager mWindowManager;
@Mock
private WindowMetrics mWindowMetrics;
private AccessibilityFloatingMenuView mMenuView;
private DockTooltipView mDockTooltipView;
private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);
private final MotionEventHelper mMotionEventHelper = new MotionEventHelper();
@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);
when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
when(mWindowMetrics.getBounds()).thenReturn(new Rect());
when(mWindowMetrics.getWindowInsets()).thenReturn(new WindowInsets.Builder().build());
mMenuView = spy(new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition));
mDockTooltipView = new DockTooltipView(mContext, mMenuView);
}
@Test
public void showTooltip_success() {
mDockTooltipView.show();
verify(mMenuView).startTranslateXAnimation();
verify(mWindowManager).addView(eq(mDockTooltipView), any(WindowManager.LayoutParams.class));
}
@Test
public void hideTooltip_success() {
mDockTooltipView.show();
mDockTooltipView.hide();
verify(mMenuView).stopTranslateXAnimation();
verify(mWindowManager).removeView(mDockTooltipView);
}
@Test
public void touchOutsideWhenToolTipViewShown_stopAnimation() {
final MotionEvent outsideEvent =
mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0,
/* eventTime= */ 1,
MotionEvent.ACTION_OUTSIDE,
/* x= */ 0,
/* y= */ 0);
mDockTooltipView.show();
mDockTooltipView.dispatchTouchEvent(outsideEvent);
verify(mMenuView).stopTranslateXAnimation();
}
@After
public void tearDown() {
mMotionEventHelper.recycleEvents();
}
}

View File

@@ -1,178 +0,0 @@
/*
* 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.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import android.view.accessibility.AccessibilityNodeInfo;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
import androidx.test.filters.SmallTest;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
/** Tests for {@link ItemDelegateCompat}. */
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class ItemDelegateCompatTest extends SysuiTestCase {
@Rule
public MockitoRule mockito = MockitoJUnit.rule();
@Mock
private WindowManager mWindowManager;
@Mock
private WindowMetrics mWindowMetrics;
private RecyclerView mListView;
private AccessibilityFloatingMenuView mMenuView;
private ItemDelegateCompat mItemDelegateCompat;
private final Rect mAvailableBounds = new Rect(100, 200, 300, 400);
private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);
@Before
public void setUp() {
final WindowManager wm = mContext.getSystemService(WindowManager.class);
doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
mWindowManager).getMaximumWindowMetrics();
mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
when(mWindowMetrics.getBounds()).thenReturn(new Rect());
when(mWindowMetrics.getWindowInsets()).thenReturn(new WindowInsets.Builder().build());
mListView = new RecyclerView(mContext);
mMenuView =
spy(new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition, mListView));
mItemDelegateCompat =
new ItemDelegateCompat(new RecyclerViewAccessibilityDelegate(mListView), mMenuView);
}
@Test
public void getAccessibilityActionList_matchResult() {
final AccessibilityNodeInfoCompat info =
new AccessibilityNodeInfoCompat(new AccessibilityNodeInfo());
mItemDelegateCompat.onInitializeAccessibilityNodeInfo(mListView, info);
assertThat(info.getActionList().size()).isEqualTo(5);
}
@Test
public void performAccessibilityMoveTopLeftAction_halfOval_success() {
doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
mMenuView.setShapeType(/* halfOvalShape */ 1);
final boolean moveTopLeftAction =
mItemDelegateCompat.performAccessibilityAction(mListView, R.id.action_move_top_left,
null);
assertThat(moveTopLeftAction).isTrue();
assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
verify(mMenuView).snapToLocation(mAvailableBounds.left, mAvailableBounds.top);
}
@Test
public void performAccessibilityMoveTopRightAction_halfOval_success() {
doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
mMenuView.setShapeType(/* halfOvalShape */ 1);
final boolean moveTopRightAction =
mItemDelegateCompat.performAccessibilityAction(mListView,
R.id.action_move_top_right, null);
assertThat(moveTopRightAction).isTrue();
assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
verify(mMenuView).snapToLocation(mAvailableBounds.right, mAvailableBounds.top);
}
@Test
public void performAccessibilityMoveBottomLeftAction_halfOval_success() {
doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
mMenuView.setShapeType(/* halfOvalShape */ 1);
final boolean moveBottomLeftAction =
mItemDelegateCompat.performAccessibilityAction(mListView,
R.id.action_move_bottom_left, null);
assertThat(moveBottomLeftAction).isTrue();
assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
verify(mMenuView).snapToLocation(mAvailableBounds.left, mAvailableBounds.bottom);
}
@Test
public void performAccessibilityMoveBottomRightAction_halfOval_success() {
doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
mMenuView.setShapeType(/* halfOvalShape */ 1);
final boolean moveBottomRightAction =
mItemDelegateCompat.performAccessibilityAction(mListView,
R.id.action_move_bottom_right, null);
assertThat(moveBottomRightAction).isTrue();
assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
verify(mMenuView).snapToLocation(mAvailableBounds.right, mAvailableBounds.bottom);
}
@Test
public void performAccessibilityMoveOutEdgeAction_halfOval_success() {
doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
mMenuView.setShapeType(/* halfOvalShape */ 1);
final boolean moveOutEdgeAndShowAction =
mItemDelegateCompat.performAccessibilityAction(mListView,
R.id.action_move_out_edge_and_show, null);
assertThat(moveOutEdgeAndShowAction).isTrue();
assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
}
@Test
public void setupAccessibilityActions_oval_hasActionMoveToEdgeAndHide() {
final AccessibilityNodeInfoCompat info =
new AccessibilityNodeInfoCompat(new AccessibilityNodeInfo());
mMenuView.setShapeType(/* ovalShape */ 0);
mItemDelegateCompat.onInitializeAccessibilityNodeInfo(mListView, info);
assertThat(info.getActionList().stream().anyMatch(
action -> action.getId() == R.id.action_move_to_edge_and_hide)).isTrue();
}
}

View File

@@ -1,71 +0,0 @@
/*
* 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 android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.widget.TextView;
import androidx.test.filters.SmallTest;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link MigrationTooltipView}. */
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class MigrationTooltipViewTest extends SysuiTestCase {
private TextView mTextView;
private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);
@Before
public void setUp() {
final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
mPlaceholderPosition);
final MigrationTooltipView toolTipView = new MigrationTooltipView(mContext, menuView);
mTextView = toolTipView.findViewById(R.id.text);
}
@Test
public void onCreate_setLinkMovementMethod() {
assertThat(mTextView.getMovementMethod()).isInstanceOf(LinkMovementMethod.class);
}
@Test
public void onCreate_setDescription_matchTextAndSpanNum() {
final CharSequence expectedTextWithoutSpan =
AnnotationLinkSpan.linkify(mContext.getText(
R.string.accessibility_floating_button_migration_tooltip)).toString();
final SpannableString spannableString = (SpannableString) mTextView.getText();
final int AnnotationLinkSpanNum =
spannableString.getSpans(/* queryStart= */ 0, spannableString.length(),
AnnotationLinkSpan.class).length;
assertThat(AnnotationLinkSpanNum).isEqualTo(1);
assertThat(mTextView.getText().toString().contentEquals(expectedTextWithoutSpan)).isTrue();
}
}