/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.accessibility; import static android.view.WindowManager.LayoutParams; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; import android.util.Log; import android.util.MathUtils; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; import com.android.systemui.R; /** * Contains a movable control UI to manipulate mirrored window's position, size and scale. The * window type of the UI is {@link LayoutParams#TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY} to * ensure it won't be magnified. It is not movable to the navigation bar. */ public abstract class MirrorWindowControl { private static final String TAG = "MirrorWindowControl"; private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG) | false; /** * A delegate handling a mirrored window's offset. */ public interface MirrorWindowDelegate { /** * Moves the window with specified offset. * * @param xOffset the amount in pixels to offset the window in the X coordinate, in current * display pixels. * @param yOffset the amount in pixels to offset the window in the Y coordinate, in current * display pixels. */ void move(int xOffset, int yOffset); } protected final Context mContext; private final Rect mDraggableBound = new Rect(); final Point mTmpPoint = new Point(); @Nullable protected MirrorWindowDelegate mMirrorWindowDelegate; protected View mControlsView; /** * The left top position of the control UI. Initialized when the control UI is visible. * * @see #setDefaultPosition(LayoutParams) */ private final Point mControlPosition = new Point(); private final WindowManager mWindowManager; MirrorWindowControl(Context context) { mContext = context; mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); } public void setWindowDelegate(@Nullable MirrorWindowDelegate windowDelegate) { mMirrorWindowDelegate = windowDelegate; } /** * Shows the control UI. * * {@link LayoutParams#TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY} window. */ public final void showControl() { if (mControlsView != null) { Log.w(TAG, "control view is visible"); return; } final Point viewSize = mTmpPoint; mControlsView = onCreateView(LayoutInflater.from(mContext), viewSize); final LayoutParams lp = new LayoutParams(); final int defaultSize = mContext.getResources().getDimensionPixelSize( R.dimen.magnification_controls_size); lp.width = viewSize.x <= 0 ? defaultSize : viewSize.x; lp.height = viewSize.y <= 0 ? defaultSize : viewSize.y; setDefaultParams(lp); setDefaultPosition(lp); mWindowManager.addView(mControlsView, lp); updateDraggableBound(lp.width, lp.height); } private void setDefaultParams(LayoutParams lp) { lp.gravity = Gravity.TOP | Gravity.LEFT; lp.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL | LayoutParams.FLAG_NOT_FOCUSABLE; lp.type = LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY; lp.format = PixelFormat.RGBA_8888; lp.setTitle(getWindowTitle()); } private void setDefaultPosition(LayoutParams layoutParams) { final Point displaySize = mTmpPoint; mContext.getDisplay().getSize(displaySize); layoutParams.x = displaySize.x - layoutParams.width; layoutParams.y = displaySize.y - layoutParams.height; mControlPosition.set(layoutParams.x, layoutParams.y); } /** * Removes the UI from the scene. */ public final void destroyControl() { if (mControlsView != null) { mWindowManager.removeView(mControlsView); mControlsView = null; } } /** * Moves the control view with specified offset. * * @param xOffset the amount in pixels to offset the UI in the X coordinate, in current * display pixels. * @param yOffset the amount in pixels to offset the UI in the Y coordinate, in current * display pixels. */ public void move(int xOffset, int yOffset) { if (mControlsView == null) { Log.w(TAG, "control view is not available yet or destroyed"); return; } final Point nextPosition = mTmpPoint; nextPosition.set(mControlPosition.x, mControlPosition.y); mTmpPoint.offset(xOffset, yOffset); setPosition(mTmpPoint); } private void setPosition(Point point) { constrainFrameToDraggableBound(point); if (point.equals(mControlPosition)) { return; } mControlPosition.set(point.x, point.y); LayoutParams lp = (LayoutParams) mControlsView.getLayoutParams(); lp.x = mControlPosition.x; lp.y = mControlPosition.y; mWindowManager.updateViewLayout(mControlsView, lp); } private void constrainFrameToDraggableBound(Point point) { point.x = MathUtils.constrain(point.x, mDraggableBound.left, mDraggableBound.right); point.y = MathUtils.constrain(point.y, mDraggableBound.top, mDraggableBound.bottom); } private void updateDraggableBound(int viewWidth, int viewHeight) { final Point size = mTmpPoint; mContext.getDisplay().getSize(size); mDraggableBound.set(0, 0, size.x - viewWidth, size.y - viewHeight); if (DBG) { Log.d(TAG, "updateDraggableBound :" + mDraggableBound); } } abstract String getWindowTitle(); /** * Called when the UI is going to show. * * @param inflater The LayoutInflater object used to inflate the view. * @param viewSize The {@link Point} to specify view's width with {@link Point#x)} and height * with {@link Point#y)} .The value should be greater than 0, otherwise will * fall back to the default size. * @return the View for the control's UI. */ @NonNull abstract View onCreateView(@NonNull LayoutInflater inflater, @NonNull Point viewSize); }