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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user