blob: 581cf7a2fbef827bb8ac336b4c24d2f81d911c2b [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;
Jacky Kaod5ad33d2020-01-30 16:03:57 +080089 // The boundary of magnification frame.
90 private final Rect mMagnificationFrameBoundary = new Rect();
chaviw4ce1ddf2019-10-25 13:46:47 -070091
92 private MoveMirrorRunnable mMoveMirrorRunnable = new MoveMirrorRunnable();
93
94 WindowMagnificationController(Context context, Handler handler) {
95 mContext = context;
96 mHandler = handler;
97 Display display = mContext.getDisplay();
Jacky Kaod5ad33d2020-01-30 16:03:57 +080098 display.getRealSize(mDisplaySize);
chaviw4ce1ddf2019-10-25 13:46:47 -070099 mDisplayId = mContext.getDisplayId();
100
101 mWm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
102
103 Resources r = context.getResources();
104 mBorderSize = (int) r.getDimension(R.dimen.magnification_border_size);
105 mMoveFrameAmountShort = (int) r.getDimension(R.dimen.magnification_frame_move_short);
106 mMoveFrameAmountLong = (int) r.getDimension(R.dimen.magnification_frame_move_long);
107
108 mScale = r.getInteger(R.integer.magnification_default_scale);
109 }
110
111 /**
112 * Creates a magnification window if it doesn't already exist.
113 */
114 void createWindowMagnification() {
115 if (mMirrorView != null) {
116 return;
117 }
mincheli5fbfc532019-12-24 21:02:06 +0800118 setInitialStartBounds();
Jacky Kaod5ad33d2020-01-30 16:03:57 +0800119 setMagnificationFrameBoundary();
chaviw4ce1ddf2019-10-25 13:46:47 -0700120 createOverlayWindow();
121 }
122
123 private void createOverlayWindow() {
124 WindowManager.LayoutParams params = new WindowManager.LayoutParams(
125 WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT,
126 WindowManager.LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY,
127 WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
128 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
129 PixelFormat.TRANSPARENT);
130 params.gravity = Gravity.TOP | Gravity.LEFT;
131 params.token = new Binder();
132 params.setTitle(mContext.getString(R.string.magnification_overlay_title));
133
134 mOverlayView = new View(mContext);
135 mOverlayView.getViewTreeObserver().addOnWindowAttachListener(
136 new ViewTreeObserver.OnWindowAttachListener() {
137 @Override
138 public void onWindowAttached() {
139 mOverlayView.getViewTreeObserver().removeOnWindowAttachListener(this);
140 createMirrorWindow();
141 createControls();
142 }
143
144 @Override
145 public void onWindowDetached() {
146
147 }
148 });
149
150 mOverlayView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
151 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
152 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
153 | View.SYSTEM_UI_FLAG_FULLSCREEN
154 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
155 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
156
157 mWm.addView(mOverlayView, params);
158 }
159
160 /**
161 * Deletes the magnification window.
162 */
163 void deleteWindowMagnification() {
164 if (mMirrorSurface != null) {
165 mTransaction.remove(mMirrorSurface).apply();
166 mMirrorSurface = null;
167 }
168
169 if (mOverlayView != null) {
170 mWm.removeView(mOverlayView);
171 mOverlayView = null;
172 }
173
174 if (mMirrorView != null) {
175 mWm.removeView(mMirrorView);
176 mMirrorView = null;
177 }
178
179 if (mControlsView != null) {
180 mWm.removeView(mControlsView);
181 mControlsView = null;
182 }
183 }
184
mincheli5fbfc532019-12-24 21:02:06 +0800185 /**
186 * Called when the configuration has changed, and it updates window magnification UI.
187 *
188 * @param configDiff a bit mask of the differences between the configurations
189 */
190 void onConfigurationChanged(int configDiff) {
191 // TODO(b/145780606): update toggle button UI.
192 if (mMirrorView != null) {
193 mWm.removeView(mMirrorView);
194 createMirrorWindow();
195 }
196 }
chaviw4ce1ddf2019-10-25 13:46:47 -0700197
mincheli5fbfc532019-12-24 21:02:06 +0800198 private void createMirrorWindow() {
chaviw4ce1ddf2019-10-25 13:46:47 -0700199 // The window should be the size the mirrored surface will be but also add room for the
200 // border and the drag handle.
201 int dragViewHeight = (int) mContext.getResources().getDimension(
202 R.dimen.magnification_drag_view_height);
203 int windowWidth = mMagnificationFrame.width() + 2 * mBorderSize;
204 int windowHeight = mMagnificationFrame.height() + dragViewHeight + 2 * mBorderSize;
205
206 WindowManager.LayoutParams params = new WindowManager.LayoutParams(
207 windowWidth, windowHeight,
208 WindowManager.LayoutParams.TYPE_APPLICATION_PANEL,
209 WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
210 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
211 PixelFormat.TRANSPARENT);
212 params.gravity = Gravity.TOP | Gravity.LEFT;
213 params.token = mOverlayView.getWindowToken();
214 params.x = mMagnificationFrame.left;
215 params.y = mMagnificationFrame.top;
Jacky Kao15b51052019-12-20 08:17:52 +0800216 params.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
chaviw4ce1ddf2019-10-25 13:46:47 -0700217 params.setTitle(mContext.getString(R.string.magnification_window_title));
218
219 mMirrorView = LayoutInflater.from(mContext).inflate(R.layout.window_magnifier_view, null);
220 mMirrorSurfaceView = mMirrorView.findViewById(R.id.surface_view);
221 // This places the SurfaceView's SurfaceControl above the ViewRootImpl's SurfaceControl to
222 // ensure the mirrored area can get touch instead of going to the window
223 mMirrorSurfaceView.setZOrderOnTop(true);
224
225 mMirrorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
226 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
227 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
228 | View.SYSTEM_UI_FLAG_FULLSCREEN
229 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
230 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
231 mWm.addView(mMirrorView, params);
232
233 SurfaceHolder holder = mMirrorSurfaceView.getHolder();
234 holder.addCallback(this);
235 holder.setFormat(PixelFormat.RGBA_8888);
236
237 addDragTouchListeners();
238 }
239
240 private void createControls() {
241 int controlsSize = (int) mContext.getResources().getDimension(
242 R.dimen.magnification_controls_size);
243
244 WindowManager.LayoutParams lp = new WindowManager.LayoutParams(controlsSize, controlsSize,
245 WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL,
246 WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
247 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
248 PixelFormat.RGBA_8888);
249 lp.gravity = Gravity.BOTTOM | Gravity.RIGHT;
250 lp.token = mOverlayView.getWindowToken();
251 lp.setTitle(mContext.getString(R.string.magnification_controls_title));
252
253 mControlsView = LayoutInflater.from(mContext).inflate(R.layout.magnifier_controllers, null);
254 mWm.addView(mControlsView, lp);
255
256 mLeftControl = mControlsView.findViewById(R.id.left_control);
257 mUpControl = mControlsView.findViewById(R.id.up_control);
258 mRightControl = mControlsView.findViewById(R.id.right_control);
259 mBottomControl = mControlsView.findViewById(R.id.down_control);
260
261 mLeftControl.setOnClickListener(this);
262 mUpControl.setOnClickListener(this);
263 mRightControl.setOnClickListener(this);
264 mBottomControl.setOnClickListener(this);
265
266 mLeftControl.setOnLongClickListener(this);
267 mUpControl.setOnLongClickListener(this);
268 mRightControl.setOnLongClickListener(this);
269 mBottomControl.setOnLongClickListener(this);
270
271 mLeftControl.setOnTouchListener(this);
272 mUpControl.setOnTouchListener(this);
273 mRightControl.setOnTouchListener(this);
274 mBottomControl.setOnTouchListener(this);
275 }
276
277 private void setInitialStartBounds() {
278 // Sets the initial frame area for the mirror and places it in the center of the display.
279 int initSize = Math.min(mDisplaySize.x, mDisplaySize.y) / 2;
280 int initX = mDisplaySize.x / 2 - initSize / 2;
281 int initY = mDisplaySize.y / 2 - initSize / 2;
282 mMagnificationFrame.set(initX, initY, initX + initSize, initY + initSize);
283 }
284
285 /**
286 * This is called once the surfaceView is created so the mirrored content can be placed as a
287 * child of the surfaceView.
288 */
289 private void createMirror() {
290 mMirrorSurface = WindowManagerWrapper.getInstance().mirrorDisplay(mDisplayId);
291 if (!mMirrorSurface.isValid()) {
292 return;
293 }
294 mTransaction.show(mMirrorSurface)
295 .reparent(mMirrorSurface, mMirrorSurfaceView.getSurfaceControl());
296
297 modifyWindowMagnification(mTransaction);
298 mTransaction.apply();
299 }
300
301 private void addDragTouchListeners() {
302 mDragView = mMirrorView.findViewById(R.id.drag_handle);
303 mLeftDrag = mMirrorView.findViewById(R.id.left_handle);
304 mTopDrag = mMirrorView.findViewById(R.id.top_handle);
305 mRightDrag = mMirrorView.findViewById(R.id.right_handle);
306 mBottomDrag = mMirrorView.findViewById(R.id.bottom_handle);
307
308 mDragView.setOnTouchListener(this);
309 mLeftDrag.setOnTouchListener(this);
310 mTopDrag.setOnTouchListener(this);
311 mRightDrag.setOnTouchListener(this);
312 mBottomDrag.setOnTouchListener(this);
313 }
314
315 /**
316 * Modifies the placement of the mirrored content.
317 */
318 private void modifyWindowMagnification(SurfaceControl.Transaction t) {
319 Rect sourceBounds = getSourceBounds(mMagnificationFrame, mScale);
320 // The final destination for the magnification surface should be at 0,0 since the
321 // ViewRootImpl's position will change
322 mTmpRect.set(0, 0, mMagnificationFrame.width(), mMagnificationFrame.height());
323
324 WindowManager.LayoutParams params =
325 (WindowManager.LayoutParams) mMirrorView.getLayoutParams();
326 params.x = mMagnificationFrame.left;
327 params.y = mMagnificationFrame.top;
328 mWm.updateViewLayout(mMirrorView, params);
329
330 t.setGeometry(mMirrorSurface, sourceBounds, mTmpRect, Surface.ROTATION_0);
331 }
332
333 @Override
334 public void onClick(View v) {
335 setMoveOffset(v, mMoveFrameAmountShort);
Jacky Kaod5ad33d2020-01-30 16:03:57 +0800336 moveMirrorWindow(mMoveWindowOffset.x, mMoveWindowOffset.y);
chaviw4ce1ddf2019-10-25 13:46:47 -0700337 }
338
339 @Override
340 public boolean onLongClick(View v) {
341 mIsPressedDown = true;
342 setMoveOffset(v, mMoveFrameAmountLong);
343 mHandler.post(mMoveMirrorRunnable);
344 return true;
345 }
346
347 @Override
348 public boolean onTouch(View v, MotionEvent event) {
349 if (v == mLeftControl || v == mUpControl || v == mRightControl || v == mBottomControl) {
350 return handleControlTouchEvent(event);
351 } else if (v == mDragView || v == mLeftDrag || v == mTopDrag || v == mRightDrag
352 || v == mBottomDrag) {
353 return handleDragTouchEvent(event);
354 }
355 return false;
356 }
357
358 private boolean handleControlTouchEvent(MotionEvent event) {
359 switch (event.getAction()) {
360 case MotionEvent.ACTION_UP:
361 case MotionEvent.ACTION_CANCEL:
362 mIsPressedDown = false;
363 break;
364 }
365 return false;
366 }
367
368 private boolean handleDragTouchEvent(MotionEvent event) {
369 switch (event.getAction()) {
370 case MotionEvent.ACTION_DOWN:
371 mLastDrag.set(event.getRawX(), event.getRawY());
372 return true;
373 case MotionEvent.ACTION_MOVE:
374 int xDiff = (int) (event.getRawX() - mLastDrag.x);
375 int yDiff = (int) (event.getRawY() - mLastDrag.y);
Jacky Kaod5ad33d2020-01-30 16:03:57 +0800376 moveMirrorWindow(xDiff, yDiff);
chaviw4ce1ddf2019-10-25 13:46:47 -0700377 mLastDrag.set(event.getRawX(), event.getRawY());
chaviw4ce1ddf2019-10-25 13:46:47 -0700378 return true;
379 }
380 return false;
381 }
382
383 private void setMoveOffset(View v, int moveFrameAmount) {
384 mMoveWindowOffset.set(0, 0);
385
386 if (v == mLeftControl) {
387 mMoveWindowOffset.x = -moveFrameAmount;
388 } else if (v == mUpControl) {
389 mMoveWindowOffset.y = -moveFrameAmount;
390 } else if (v == mRightControl) {
391 mMoveWindowOffset.x = moveFrameAmount;
392 } else if (v == mBottomControl) {
393 mMoveWindowOffset.y = moveFrameAmount;
394 }
395 }
396
Jacky Kaod5ad33d2020-01-30 16:03:57 +0800397 private void moveMirrorWindow(int xOffset, int yOffset) {
398 if (updateMagnificationFramePosition(xOffset, yOffset)) {
399 modifyWindowMagnification(mTransaction);
400 mTransaction.apply();
401 }
chaviw4ce1ddf2019-10-25 13:46:47 -0700402 }
403
404 /**
405 * Calculates the desired source bounds. This will be the area under from the center of the
406 * displayFrame, factoring in scale.
407 */
408 private Rect getSourceBounds(Rect displayFrame, float scale) {
409 int halfWidth = displayFrame.width() / 2;
410 int halfHeight = displayFrame.height() / 2;
411 int left = displayFrame.left + (halfWidth - (int) (halfWidth / scale));
412 int right = displayFrame.right - (halfWidth - (int) (halfWidth / scale));
413 int top = displayFrame.top + (halfHeight - (int) (halfHeight / scale));
414 int bottom = displayFrame.bottom - (halfHeight - (int) (halfHeight / scale));
415 return new Rect(left, top, right, bottom);
416 }
417
Jacky Kaod5ad33d2020-01-30 16:03:57 +0800418 private void setMagnificationFrameBoundary() {
419 // Calculates width and height for magnification frame could exceed out the screen.
420 // TODO : re-calculating again when scale is changed.
421 // The half width of magnification frame.
422 final int halfWidth = mMagnificationFrame.width() / 2;
423 // The half height of magnification frame.
424 final int halfHeight = mMagnificationFrame.height() / 2;
425 // The scaled half width of magnified region.
426 final int scaledWidth = (int) (halfWidth / mScale);
427 // The scaled half height of magnified region.
428 final int scaledHeight = (int) (halfHeight / mScale);
429 final int exceededWidth = halfWidth - scaledWidth;
430 final int exceededHeight = halfHeight - scaledHeight;
431
432 mMagnificationFrameBoundary.set(-exceededWidth, -exceededHeight,
433 mDisplaySize.x + exceededWidth, mDisplaySize.y + exceededHeight);
434 }
435
436 /**
437 * Calculates and sets the real position of magnification frame based on the magnified region
438 * should be limited by the region of the display.
439 */
440 private boolean updateMagnificationFramePosition(int xOffset, int yOffset) {
441 mTmpRect.set(mMagnificationFrame);
442 mTmpRect.offset(xOffset, yOffset);
443
444 if (mTmpRect.left < mMagnificationFrameBoundary.left) {
445 mTmpRect.offsetTo(mMagnificationFrameBoundary.left, mTmpRect.top);
446 } else if (mTmpRect.right > mMagnificationFrameBoundary.right) {
447 final int leftOffset = mMagnificationFrameBoundary.right - mMagnificationFrame.width();
448 mTmpRect.offsetTo(leftOffset, mTmpRect.top);
449 }
450
451 if (mTmpRect.top < mMagnificationFrameBoundary.top) {
452 mTmpRect.offsetTo(mTmpRect.left, mMagnificationFrameBoundary.top);
453 } else if (mTmpRect.bottom > mMagnificationFrameBoundary.bottom) {
454 final int topOffset = mMagnificationFrameBoundary.bottom - mMagnificationFrame.height();
455 mTmpRect.offsetTo(mTmpRect.left, topOffset);
456 }
457
458 if (!mTmpRect.equals(mMagnificationFrame)) {
459 mMagnificationFrame.set(mTmpRect);
460 return true;
461 }
462 return false;
463 }
chaviw4ce1ddf2019-10-25 13:46:47 -0700464 @Override
465 public void surfaceCreated(SurfaceHolder holder) {
466 createMirror();
467 }
468
469 @Override
470 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
471 }
472
473 @Override
474 public void surfaceDestroyed(SurfaceHolder holder) {
475 }
476
477 class MoveMirrorRunnable implements Runnable {
478 @Override
479 public void run() {
480 if (mIsPressedDown) {
Jacky Kaod5ad33d2020-01-30 16:03:57 +0800481 moveMirrorWindow(mMoveWindowOffset.x, mMoveWindowOffset.y);
chaviw4ce1ddf2019-10-25 13:46:47 -0700482 mHandler.postDelayed(mMoveMirrorRunnable, 100);
483 }
484 }
485 }
486}