blob: 527c12493b3f1db72a1cd4b19960837613ba4616 [file] [log] [blame]
/*
* Copyright (C) 2022 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;
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import android.annotation.IntDef;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.graphics.Insets;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.MathUtils;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.AccessibilityDelegate;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.view.WindowMetrics;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.Switch;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
import com.android.systemui.R;
import com.android.systemui.util.settings.SecureSettings;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
/**
* Class to set value about WindowManificationSettings.
*/
class WindowMagnificationSettings implements MagnificationGestureDetector.OnGestureListener {
private static final String TAG = "WindowMagnificationSettings";
private final Context mContext;
private final AccessibilityManager mAccessibilityManager;
private final WindowManager mWindowManager;
private final SecureSettings mSecureSettings;
private final Runnable mWindowInsetChangeRunnable;
private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider;
private final LayoutParams mParams;
@VisibleForTesting
final Rect mDraggableWindowBounds = new Rect();
private boolean mIsVisible = false;
private final MagnificationGestureDetector mGestureDetector;
private boolean mSingleTapDetected = false;
private SeekBar mZoomSeekbar;
private Switch mAllowDiagonalScrollingSwitch;
private LinearLayout mPanelView;
private LinearLayout mSettingView;
private LinearLayout mButtonView;
private ImageButton mSmallButton;
private ImageButton mMediumButton;
private ImageButton mLargeButton;
private Button mDoneButton;
private Button mEditButton;
private ImageButton mChangeModeButton;
private boolean mAllowDiagonalScrolling = false;
private static final float A11Y_CHANGE_SCALE_DIFFERENCE = 1.0f;
private static final float A11Y_SCALE_MIN_VALUE = 2.0f;
private WindowMagnificationSettingsCallback mCallback;
@Retention(RetentionPolicy.SOURCE)
@IntDef({
MagnificationSize.NONE,
MagnificationSize.SMALL,
MagnificationSize.MEDIUM,
MagnificationSize.LARGE,
})
/** Denotes the Magnification size type. */
public @interface MagnificationSize {
int NONE = 0;
int SMALL = 1;
int MEDIUM = 2;
int LARGE = 3;
}
@VisibleForTesting
WindowMagnificationSettings(Context context, WindowMagnificationSettingsCallback callback,
SfVsyncFrameCallbackProvider sfVsyncFrameProvider, SecureSettings secureSettings) {
mContext = context;
mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
mWindowManager = mContext.getSystemService(WindowManager.class);
mSfVsyncFrameProvider = sfVsyncFrameProvider;
mCallback = callback;
mSecureSettings = secureSettings;
mAllowDiagonalScrolling = mSecureSettings.getIntForUser(
Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING, 0,
UserHandle.USER_CURRENT) == 1;
inflateView();
mParams = createLayoutParams(context);
mWindowInsetChangeRunnable = this::onWindowInsetChanged;
mGestureDetector = new MagnificationGestureDetector(context,
context.getMainThreadHandler(), this);
}
private class ZoomSeekbarChangeListener implements SeekBar.OnSeekBarChangeListener {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
float scale = progress * A11Y_CHANGE_SCALE_DIFFERENCE + A11Y_SCALE_MIN_VALUE;
mSecureSettings.putFloatForUser(
Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale,
UserHandle.USER_CURRENT);
mCallback.onMagnifierScale(scale);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// Do nothing
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// Do nothing
}
}
private CharSequence formatContentDescription(int viewId) {
if (viewId == R.id.magnifier_small_button) {
return mContext.getResources().getString(
R.string.accessibility_magnification_small);
} else if (viewId == R.id.magnifier_medium_button) {
return mContext.getResources().getString(
R.string.accessibility_magnification_medium);
} else if (viewId == R.id.magnifier_large_button) {
return mContext.getResources().getString(
R.string.accessibility_magnification_large);
} else if (viewId == R.id.magnifier_done_button) {
return mContext.getResources().getString(
R.string.accessibility_magnification_done);
} else if (viewId == R.id.magnifier_edit_button) {
return mContext.getResources().getString(
R.string.accessibility_resize);
} else {
return mContext.getResources().getString(
R.string.magnification_mode_switch_description);
}
}
private final AccessibilityDelegate mButtonDelegate = new AccessibilityDelegate() {
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(host, info);
info.setContentDescription(formatContentDescription(host.getId()));
final AccessibilityAction clickAction = new AccessibilityAction(
AccessibilityAction.ACTION_CLICK.getId(), mContext.getResources().getString(
R.string.magnification_mode_switch_click_label));
info.addAction(clickAction);
info.setClickable(true);
info.addAction(new AccessibilityAction(R.id.accessibility_action_move_up,
mContext.getString(R.string.accessibility_control_move_up)));
info.addAction(new AccessibilityAction(R.id.accessibility_action_move_down,
mContext.getString(R.string.accessibility_control_move_down)));
info.addAction(new AccessibilityAction(R.id.accessibility_action_move_left,
mContext.getString(R.string.accessibility_control_move_left)));
info.addAction(new AccessibilityAction(R.id.accessibility_action_move_right,
mContext.getString(R.string.accessibility_control_move_right)));
}
@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
if (performA11yAction(host, action)) {
return true;
}
return super.performAccessibilityAction(host, action, args);
}
private boolean performA11yAction(View view, int action) {
final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
if (action == AccessibilityAction.ACTION_CLICK.getId()) {
handleSingleTap(view);
} else if (action == R.id.accessibility_action_move_up) {
moveButton(0, -windowBounds.height());
} else if (action == R.id.accessibility_action_move_down) {
moveButton(0, windowBounds.height());
} else if (action == R.id.accessibility_action_move_left) {
moveButton(-windowBounds.width(), 0);
} else if (action == R.id.accessibility_action_move_right) {
moveButton(windowBounds.width(), 0);
} else {
return false;
}
return true;
}
};
private void applyResourcesValuesWithDensityChanged() {
if (mIsVisible) {
// Reset button to make its window layer always above the mirror window.
hideSettingPanel();
showSettingPanel(false);
}
}
private boolean onTouch(View v, MotionEvent event) {
if (!mIsVisible) {
return false;
}
return mGestureDetector.onTouch(v, event);
}
private View.OnClickListener mButtonClickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
int id = view.getId();
if (id == R.id.magnifier_small_button) {
setMagnifierSize(MagnificationSize.SMALL);
} else if (id == R.id.magnifier_medium_button) {
setMagnifierSize(MagnificationSize.MEDIUM);
} else if (id == R.id.magnifier_large_button) {
setMagnifierSize(MagnificationSize.LARGE);
} else if (id == R.id.magnifier_edit_button) {
editMagnifierSizeMode(true);
} else if (id == R.id.magnifier_done_button) {
hideSettingPanel();
} else if (id == R.id.magnifier_full_button) {
hideSettingPanel();
toggleMagnificationMode();
} else {
hideSettingPanel();
}
}
};
@Override
public boolean onSingleTap(View view) {
mSingleTapDetected = true;
handleSingleTap(view);
return true;
}
@Override
public boolean onDrag(View v, float offsetX, float offsetY) {
moveButton(offsetX, offsetY);
return true;
}
@Override
public boolean onStart(float x, float y) {
return true;
}
@Override
public boolean onFinish(float xOffset, float yOffset) {
if (!mSingleTapDetected) {
showSettingPanel();
}
mSingleTapDetected = false;
return true;
}
@VisibleForTesting
public ViewGroup getSettingView() {
return mSettingView;
}
private void moveButton(float offsetX, float offsetY) {
mSfVsyncFrameProvider.postFrameCallback(l -> {
mParams.x += offsetX;
mParams.y += offsetY;
updateButtonViewLayoutIfNeeded();
});
}
public void hideSettingPanel() {
hideSettingPanel(true);
}
public void hideSettingPanel(boolean resetPosition) {
if (!mIsVisible) {
return;
}
// Reset button status.
mWindowManager.removeView(mSettingView);
mIsVisible = false;
if (resetPosition) {
mParams.x = 0;
mParams.y = 0;
}
mContext.unregisterReceiver(mScreenOffReceiver);
}
public void showSettingPanel() {
showSettingPanel(true);
}
public void setScaleSeekbar(float scale) {
setSeekbarProgress(scale);
}
private void toggleMagnificationMode() {
mCallback.onModeSwitch(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
}
/**
* Shows magnification panel for set window magnification.
* When the panel is going to be visible by calling this method, the layout position can be
* reset depending on the flag.
*
* @param resetPosition if the button position needs be reset
*/
private void showSettingPanel(boolean resetPosition) {
if (!mIsVisible) {
if (resetPosition) {
mDraggableWindowBounds.set(getDraggableWindowBounds());
mParams.x = mDraggableWindowBounds.right;
mParams.y = mDraggableWindowBounds.bottom;
}
mWindowManager.addView(mSettingView, mParams);
// Exclude magnification switch button from system gesture area.
setSystemGestureExclusion();
mIsVisible = true;
}
mContext.registerReceiver(mScreenOffReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
}
private final BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
hideSettingPanel();
}
};
private void setSeekbarProgress(float scale) {
int index = (int) ((scale - A11Y_SCALE_MIN_VALUE) / A11Y_CHANGE_SCALE_DIFFERENCE);
if (index < 0) {
index = 0;
}
mZoomSeekbar.setProgress(index);
}
void inflateView() {
mSettingView = (LinearLayout) View.inflate(mContext,
R.layout.window_magnification_settings_view, null);
mSettingView.setClickable(true);
mSettingView.setFocusable(true);
mSettingView.setOnTouchListener(this::onTouch);
mPanelView = mSettingView.findViewById(R.id.magnifier_panel_view);
mSmallButton = mSettingView.findViewById(R.id.magnifier_small_button);
mMediumButton = mSettingView.findViewById(R.id.magnifier_medium_button);
mLargeButton = mSettingView.findViewById(R.id.magnifier_large_button);
mDoneButton = mSettingView.findViewById(R.id.magnifier_done_button);
mEditButton = mSettingView.findViewById(R.id.magnifier_edit_button);
mChangeModeButton = mSettingView.findViewById(R.id.magnifier_full_button);
mZoomSeekbar = mSettingView.findViewById(R.id.magnifier_zoom_seekbar);
mZoomSeekbar.setOnSeekBarChangeListener(new ZoomSeekbarChangeListener());
float scale = mSecureSettings.getFloatForUser(
Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 0,
UserHandle.USER_CURRENT);
setSeekbarProgress(scale);
mAllowDiagonalScrollingSwitch =
(Switch) mSettingView.findViewById(R.id.magnifier_horizontal_lock_switch);
mAllowDiagonalScrollingSwitch.setChecked(mAllowDiagonalScrolling);
mAllowDiagonalScrollingSwitch.setOnCheckedChangeListener((view, checked) -> {
toggleDiagonalScrolling();
});
mSmallButton.setAccessibilityDelegate(mButtonDelegate);
mSmallButton.setOnClickListener(mButtonClickListener);
mMediumButton.setAccessibilityDelegate(mButtonDelegate);
mMediumButton.setOnClickListener(mButtonClickListener);
mLargeButton.setAccessibilityDelegate(mButtonDelegate);
mLargeButton.setOnClickListener(mButtonClickListener);
mDoneButton.setAccessibilityDelegate(mButtonDelegate);
mDoneButton.setOnClickListener(mButtonClickListener);
mChangeModeButton.setAccessibilityDelegate(mButtonDelegate);
mChangeModeButton.setOnClickListener(mButtonClickListener);
mEditButton.setAccessibilityDelegate(mButtonDelegate);
mEditButton.setOnClickListener(mButtonClickListener);
mSettingView.setOnApplyWindowInsetsListener((v, insets) -> {
// Adds a pending post check to avoiding redundant calculation because this callback
// is sent frequently when the switch icon window dragged by the users.
if (!mSettingView.getHandler().hasCallbacks(mWindowInsetChangeRunnable)) {
mSettingView.getHandler().post(mWindowInsetChangeRunnable);
}
return v.onApplyWindowInsets(insets);
});
}
void onConfigurationChanged(int configDiff) {
if ((configDiff & ActivityInfo.CONFIG_UI_MODE) != 0
|| (configDiff & ActivityInfo.CONFIG_ASSETS_PATHS) != 0) {
boolean showSettingPanelAfterThemeChange = mIsVisible;
hideSettingPanel(/* resetPosition= */ false);
inflateView();
if (showSettingPanelAfterThemeChange) {
showSettingPanel(/* resetPosition= */ false);
}
return;
}
if ((configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0) {
final Rect previousDraggableBounds = new Rect(mDraggableWindowBounds);
mDraggableWindowBounds.set(getDraggableWindowBounds());
// Keep the Y position with the same height ratio before the window bounds and
// draggable bounds are changed.
final float windowHeightFraction = (float) (mParams.y - previousDraggableBounds.top)
/ previousDraggableBounds.height();
mParams.y = (int) (windowHeightFraction * mDraggableWindowBounds.height())
+ mDraggableWindowBounds.top;
return;
}
if ((configDiff & ActivityInfo.CONFIG_DENSITY) != 0) {
applyResourcesValuesWithDensityChanged();
return;
}
if ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0) {
updateAccessibilityWindowTitle();
return;
}
}
private void onWindowInsetChanged() {
final Rect newBounds = getDraggableWindowBounds();
if (mDraggableWindowBounds.equals(newBounds)) {
return;
}
mDraggableWindowBounds.set(newBounds);
}
private void updateButtonViewLayoutIfNeeded() {
if (mIsVisible) {
mParams.x = MathUtils.constrain(mParams.x, mDraggableWindowBounds.left,
mDraggableWindowBounds.right);
mParams.y = MathUtils.constrain(mParams.y, mDraggableWindowBounds.top,
mDraggableWindowBounds.bottom);
mWindowManager.updateViewLayout(mSettingView, mParams);
}
}
private void updateAccessibilityWindowTitle() {
mParams.accessibilityTitle = getAccessibilityWindowTitle(mContext);
if (mIsVisible) {
mWindowManager.updateViewLayout(mSettingView, mParams);
}
}
private void handleSingleTap(View view) {
int id = view.getId();
if (id == R.id.magnifier_small_button) {
setMagnifierSize(MagnificationSize.SMALL);
} else if (id == R.id.magnifier_medium_button) {
setMagnifierSize(MagnificationSize.MEDIUM);
} else if (id == R.id.magnifier_large_button) {
setMagnifierSize(MagnificationSize.LARGE);
} else if (id == R.id.magnifier_full_button) {
hideSettingPanel();
toggleMagnificationMode();
} else {
hideSettingPanel();
}
}
public void editMagnifierSizeMode(boolean enable) {
setEditMagnifierSizeMode(enable);
hideSettingPanel();
}
private void setMagnifierSize(@MagnificationSize int index) {
mCallback.onSetMagnifierSize(index);
}
private void toggleDiagonalScrolling() {
boolean enabled = mSecureSettings.getIntForUser(
Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING, 0,
UserHandle.USER_CURRENT) == 1;
mSecureSettings.putIntForUser(
Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING, enabled ? 0 : 1,
UserHandle.USER_CURRENT);
mCallback.onSetDiagonalScrolling(!enabled);
}
private void setEditMagnifierSizeMode(boolean enable) {
mCallback.onEditMagnifierSizeMode(enable);
}
private static LayoutParams createLayoutParams(Context context) {
final LayoutParams params = new LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT,
LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY,
LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSPARENT);
params.gravity = Gravity.TOP | Gravity.START;
params.accessibilityTitle = getAccessibilityWindowTitle(context);
params.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
return params;
}
private Rect getDraggableWindowBounds() {
final int layoutMargin = mContext.getResources().getDimensionPixelSize(
R.dimen.magnification_switch_button_margin);
final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
final Insets windowInsets = windowMetrics.getWindowInsets().getInsetsIgnoringVisibility(
WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
final Rect boundRect = new Rect(windowMetrics.getBounds());
boundRect.offsetTo(0, 0);
boundRect.inset(0, 0, mParams.width, mParams.height);
boundRect.inset(windowInsets);
boundRect.inset(layoutMargin, layoutMargin);
return boundRect;
}
private static String getAccessibilityWindowTitle(Context context) {
return context.getString(com.android.internal.R.string.android_system_label);
}
private void setSystemGestureExclusion() {
mSettingView.post(() -> {
mSettingView.setSystemGestureExclusionRects(
Collections.singletonList(
new Rect(0, 0, mSettingView.getWidth(), mSettingView.getHeight())));
});
}
}