blob: bfac4fce92ebdeee4792e04973459e5dcc759bcf [file] [log] [blame]
chaviw4ce1ddf2019-10-25 13:46:47 -07001/*
2 * Copyright (C) 2019 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.accessibility;
18
Jacky Kao15b51052019-12-20 08:17:52 +080019import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
20
chaviw4ce1ddf2019-10-25 13:46:47 -070021import android.content.Context;
22import android.content.res.Resources;
23import android.graphics.PixelFormat;
24import android.graphics.Point;
25import android.graphics.PointF;
26import android.graphics.Rect;
27import android.os.Binder;
28import android.os.Handler;
29import android.view.Display;
30import android.view.Gravity;
31import android.view.LayoutInflater;
32import android.view.MotionEvent;
33import android.view.Surface;
34import android.view.SurfaceControl;
35import android.view.SurfaceHolder;
36import android.view.SurfaceView;
37import android.view.View;
38import android.view.ViewTreeObserver;
39import android.view.WindowManager;
40
41import com.android.systemui.R;
42import com.android.systemui.shared.system.WindowManagerWrapper;
43
44/**
45 * Class to handle adding and removing a window magnification.
46 */
47public class WindowMagnificationController implements View.OnClickListener,
48 View.OnLongClickListener, View.OnTouchListener, SurfaceHolder.Callback {
49 private final int mBorderSize;
50 private final int mMoveFrameAmountShort;
51 private final int mMoveFrameAmountLong;
52
53 private final Context mContext;
54 private final Point mDisplaySize = new Point();
55 private final int mDisplayId;
56 private final Handler mHandler;
57 private final Rect mMagnificationFrame = new Rect();
58 private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
59
60 private final WindowManager mWm;
61
62 private float mScale;
63
64 private final Rect mTmpRect = new Rect();
65
66 // The root of the mirrored content
67 private SurfaceControl mMirrorSurface;
68
69 private boolean mIsPressedDown;
70
71 private View mLeftControl;
72 private View mUpControl;
73 private View mRightControl;
74 private View mBottomControl;
75
76 private View mDragView;
77 private View mLeftDrag;
78 private View mTopDrag;
79 private View mRightDrag;
80 private View mBottomDrag;
81
82 private final PointF mLastDrag = new PointF();
83 private final Point mMoveWindowOffset = new Point();
84
85 private View mMirrorView;
86 private SurfaceView mMirrorSurfaceView;
87 private View mControlsView;
88 private View mOverlayView;
89
90 private MoveMirrorRunnable mMoveMirrorRunnable = new MoveMirrorRunnable();
91
92 WindowMagnificationController(Context context, Handler handler) {
93 mContext = context;
94 mHandler = handler;
95 Display display = mContext.getDisplay();
96 display.getSize(mDisplaySize);
97 mDisplayId = mContext.getDisplayId();
98
99 mWm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
100
101 Resources r = context.getResources();
102 mBorderSize = (int) r.getDimension(R.dimen.magnification_border_size);
103 mMoveFrameAmountShort = (int) r.getDimension(R.dimen.magnification_frame_move_short);
104 mMoveFrameAmountLong = (int) r.getDimension(R.dimen.magnification_frame_move_long);
105
106 mScale = r.getInteger(R.integer.magnification_default_scale);
107 }
108
109 /**
110 * Creates a magnification window if it doesn't already exist.
111 */
112 void createWindowMagnification() {
113 if (mMirrorView != null) {
114 return;
115 }
116 createOverlayWindow();
117 }
118
119 private void createOverlayWindow() {
120 WindowManager.LayoutParams params = new WindowManager.LayoutParams(
121 WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT,
122 WindowManager.LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY,
123 WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
124 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
125 PixelFormat.TRANSPARENT);
126 params.gravity = Gravity.TOP | Gravity.LEFT;
127 params.token = new Binder();
128 params.setTitle(mContext.getString(R.string.magnification_overlay_title));
129
130 mOverlayView = new View(mContext);
131 mOverlayView.getViewTreeObserver().addOnWindowAttachListener(
132 new ViewTreeObserver.OnWindowAttachListener() {
133 @Override
134 public void onWindowAttached() {
135 mOverlayView.getViewTreeObserver().removeOnWindowAttachListener(this);
136 createMirrorWindow();
137 createControls();
138 }
139
140 @Override
141 public void onWindowDetached() {
142
143 }
144 });
145
146 mOverlayView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
147 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
148 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
149 | View.SYSTEM_UI_FLAG_FULLSCREEN
150 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
151 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
152
153 mWm.addView(mOverlayView, params);
154 }
155
156 /**
157 * Deletes the magnification window.
158 */
159 void deleteWindowMagnification() {
160 if (mMirrorSurface != null) {
161 mTransaction.remove(mMirrorSurface).apply();
162 mMirrorSurface = null;
163 }
164
165 if (mOverlayView != null) {
166 mWm.removeView(mOverlayView);
167 mOverlayView = null;
168 }
169
170 if (mMirrorView != null) {
171 mWm.removeView(mMirrorView);
172 mMirrorView = null;
173 }
174
175 if (mControlsView != null) {
176 mWm.removeView(mControlsView);
177 mControlsView = null;
178 }
179 }
180
181 private void createMirrorWindow() {
182 setInitialStartBounds();
183
184 // The window should be the size the mirrored surface will be but also add room for the
185 // border and the drag handle.
186 int dragViewHeight = (int) mContext.getResources().getDimension(
187 R.dimen.magnification_drag_view_height);
188 int windowWidth = mMagnificationFrame.width() + 2 * mBorderSize;
189 int windowHeight = mMagnificationFrame.height() + dragViewHeight + 2 * mBorderSize;
190
191 WindowManager.LayoutParams params = new WindowManager.LayoutParams(
192 windowWidth, windowHeight,
193 WindowManager.LayoutParams.TYPE_APPLICATION_PANEL,
194 WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
195 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
196 PixelFormat.TRANSPARENT);
197 params.gravity = Gravity.TOP | Gravity.LEFT;
198 params.token = mOverlayView.getWindowToken();
199 params.x = mMagnificationFrame.left;
200 params.y = mMagnificationFrame.top;
Jacky Kao15b51052019-12-20 08:17:52 +0800201 params.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
chaviw4ce1ddf2019-10-25 13:46:47 -0700202 params.setTitle(mContext.getString(R.string.magnification_window_title));
203
204 mMirrorView = LayoutInflater.from(mContext).inflate(R.layout.window_magnifier_view, null);
205 mMirrorSurfaceView = mMirrorView.findViewById(R.id.surface_view);
206 // This places the SurfaceView's SurfaceControl above the ViewRootImpl's SurfaceControl to
207 // ensure the mirrored area can get touch instead of going to the window
208 mMirrorSurfaceView.setZOrderOnTop(true);
209
210 mMirrorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
211 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
212 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
213 | View.SYSTEM_UI_FLAG_FULLSCREEN
214 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
215 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
216 mWm.addView(mMirrorView, params);
217
218 SurfaceHolder holder = mMirrorSurfaceView.getHolder();
219 holder.addCallback(this);
220 holder.setFormat(PixelFormat.RGBA_8888);
221
222 addDragTouchListeners();
223 }
224
225 private void createControls() {
226 int controlsSize = (int) mContext.getResources().getDimension(
227 R.dimen.magnification_controls_size);
228
229 WindowManager.LayoutParams lp = new WindowManager.LayoutParams(controlsSize, controlsSize,
230 WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL,
231 WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
232 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
233 PixelFormat.RGBA_8888);
234 lp.gravity = Gravity.BOTTOM | Gravity.RIGHT;
235 lp.token = mOverlayView.getWindowToken();
236 lp.setTitle(mContext.getString(R.string.magnification_controls_title));
237
238 mControlsView = LayoutInflater.from(mContext).inflate(R.layout.magnifier_controllers, null);
239 mWm.addView(mControlsView, lp);
240
241 mLeftControl = mControlsView.findViewById(R.id.left_control);
242 mUpControl = mControlsView.findViewById(R.id.up_control);
243 mRightControl = mControlsView.findViewById(R.id.right_control);
244 mBottomControl = mControlsView.findViewById(R.id.down_control);
245
246 mLeftControl.setOnClickListener(this);
247 mUpControl.setOnClickListener(this);
248 mRightControl.setOnClickListener(this);
249 mBottomControl.setOnClickListener(this);
250
251 mLeftControl.setOnLongClickListener(this);
252 mUpControl.setOnLongClickListener(this);
253 mRightControl.setOnLongClickListener(this);
254 mBottomControl.setOnLongClickListener(this);
255
256 mLeftControl.setOnTouchListener(this);
257 mUpControl.setOnTouchListener(this);
258 mRightControl.setOnTouchListener(this);
259 mBottomControl.setOnTouchListener(this);
260 }
261
262 private void setInitialStartBounds() {
263 // Sets the initial frame area for the mirror and places it in the center of the display.
264 int initSize = Math.min(mDisplaySize.x, mDisplaySize.y) / 2;
265 int initX = mDisplaySize.x / 2 - initSize / 2;
266 int initY = mDisplaySize.y / 2 - initSize / 2;
267 mMagnificationFrame.set(initX, initY, initX + initSize, initY + initSize);
268 }
269
270 /**
271 * This is called once the surfaceView is created so the mirrored content can be placed as a
272 * child of the surfaceView.
273 */
274 private void createMirror() {
275 mMirrorSurface = WindowManagerWrapper.getInstance().mirrorDisplay(mDisplayId);
276 if (!mMirrorSurface.isValid()) {
277 return;
278 }
279 mTransaction.show(mMirrorSurface)
280 .reparent(mMirrorSurface, mMirrorSurfaceView.getSurfaceControl());
281
282 modifyWindowMagnification(mTransaction);
283 mTransaction.apply();
284 }
285
286 private void addDragTouchListeners() {
287 mDragView = mMirrorView.findViewById(R.id.drag_handle);
288 mLeftDrag = mMirrorView.findViewById(R.id.left_handle);
289 mTopDrag = mMirrorView.findViewById(R.id.top_handle);
290 mRightDrag = mMirrorView.findViewById(R.id.right_handle);
291 mBottomDrag = mMirrorView.findViewById(R.id.bottom_handle);
292
293 mDragView.setOnTouchListener(this);
294 mLeftDrag.setOnTouchListener(this);
295 mTopDrag.setOnTouchListener(this);
296 mRightDrag.setOnTouchListener(this);
297 mBottomDrag.setOnTouchListener(this);
298 }
299
300 /**
301 * Modifies the placement of the mirrored content.
302 */
303 private void modifyWindowMagnification(SurfaceControl.Transaction t) {
304 Rect sourceBounds = getSourceBounds(mMagnificationFrame, mScale);
305 // The final destination for the magnification surface should be at 0,0 since the
306 // ViewRootImpl's position will change
307 mTmpRect.set(0, 0, mMagnificationFrame.width(), mMagnificationFrame.height());
308
309 WindowManager.LayoutParams params =
310 (WindowManager.LayoutParams) mMirrorView.getLayoutParams();
311 params.x = mMagnificationFrame.left;
312 params.y = mMagnificationFrame.top;
313 mWm.updateViewLayout(mMirrorView, params);
314
315 t.setGeometry(mMirrorSurface, sourceBounds, mTmpRect, Surface.ROTATION_0);
316 }
317
318 @Override
319 public void onClick(View v) {
320 setMoveOffset(v, mMoveFrameAmountShort);
321 moveMirrorFromControls();
322 }
323
324 @Override
325 public boolean onLongClick(View v) {
326 mIsPressedDown = true;
327 setMoveOffset(v, mMoveFrameAmountLong);
328 mHandler.post(mMoveMirrorRunnable);
329 return true;
330 }
331
332 @Override
333 public boolean onTouch(View v, MotionEvent event) {
334 if (v == mLeftControl || v == mUpControl || v == mRightControl || v == mBottomControl) {
335 return handleControlTouchEvent(event);
336 } else if (v == mDragView || v == mLeftDrag || v == mTopDrag || v == mRightDrag
337 || v == mBottomDrag) {
338 return handleDragTouchEvent(event);
339 }
340 return false;
341 }
342
343 private boolean handleControlTouchEvent(MotionEvent event) {
344 switch (event.getAction()) {
345 case MotionEvent.ACTION_UP:
346 case MotionEvent.ACTION_CANCEL:
347 mIsPressedDown = false;
348 break;
349 }
350 return false;
351 }
352
353 private boolean handleDragTouchEvent(MotionEvent event) {
354 switch (event.getAction()) {
355 case MotionEvent.ACTION_DOWN:
356 mLastDrag.set(event.getRawX(), event.getRawY());
357 return true;
358 case MotionEvent.ACTION_MOVE:
359 int xDiff = (int) (event.getRawX() - mLastDrag.x);
360 int yDiff = (int) (event.getRawY() - mLastDrag.y);
361 mMagnificationFrame.offset(xDiff, yDiff);
362 mLastDrag.set(event.getRawX(), event.getRawY());
363 modifyWindowMagnification(mTransaction);
364 mTransaction.apply();
365 return true;
366 }
367 return false;
368 }
369
370 private void setMoveOffset(View v, int moveFrameAmount) {
371 mMoveWindowOffset.set(0, 0);
372
373 if (v == mLeftControl) {
374 mMoveWindowOffset.x = -moveFrameAmount;
375 } else if (v == mUpControl) {
376 mMoveWindowOffset.y = -moveFrameAmount;
377 } else if (v == mRightControl) {
378 mMoveWindowOffset.x = moveFrameAmount;
379 } else if (v == mBottomControl) {
380 mMoveWindowOffset.y = moveFrameAmount;
381 }
382 }
383
384 private void moveMirrorFromControls() {
385 mMagnificationFrame.offset(mMoveWindowOffset.x, mMoveWindowOffset.y);
386
387 modifyWindowMagnification(mTransaction);
388 mTransaction.apply();
389 }
390
391 /**
392 * Calculates the desired source bounds. This will be the area under from the center of the
393 * displayFrame, factoring in scale.
394 */
395 private Rect getSourceBounds(Rect displayFrame, float scale) {
396 int halfWidth = displayFrame.width() / 2;
397 int halfHeight = displayFrame.height() / 2;
398 int left = displayFrame.left + (halfWidth - (int) (halfWidth / scale));
399 int right = displayFrame.right - (halfWidth - (int) (halfWidth / scale));
400 int top = displayFrame.top + (halfHeight - (int) (halfHeight / scale));
401 int bottom = displayFrame.bottom - (halfHeight - (int) (halfHeight / scale));
402 return new Rect(left, top, right, bottom);
403 }
404
405 @Override
406 public void surfaceCreated(SurfaceHolder holder) {
407 createMirror();
408 }
409
410 @Override
411 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
412 }
413
414 @Override
415 public void surfaceDestroyed(SurfaceHolder holder) {
416 }
417
418 class MoveMirrorRunnable implements Runnable {
419 @Override
420 public void run() {
421 if (mIsPressedDown) {
422 moveMirrorFromControls();
423 mHandler.postDelayed(mMoveMirrorRunnable, 100);
424 }
425 }
426 }
427}