1 /* 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 17 package com.google.android.test.mirrorsurface; 18 19 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; 20 21 import android.app.Activity; 22 import android.graphics.Canvas; 23 import android.graphics.Color; 24 import android.graphics.PixelFormat; 25 import android.graphics.Point; 26 import android.graphics.Rect; 27 import android.os.Bundle; 28 import android.os.Handler; 29 import android.os.RemoteException; 30 import android.view.Gravity; 31 import android.view.IWindowManager; 32 import android.view.MotionEvent; 33 import android.view.Surface; 34 import android.view.SurfaceControl; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.view.WindowManager; 38 import android.view.WindowManagerGlobal; 39 import android.widget.EditText; 40 import android.widget.LinearLayout; 41 import android.widget.TextView; 42 import android.window.WindowMetricsHelper; 43 44 public class MirrorSurfaceActivity extends Activity implements View.OnClickListener, 45 View.OnLongClickListener, View.OnTouchListener { 46 private static final int BORDER_SIZE = 10; 47 private static final int DEFAULT_SCALE = 2; 48 private static final int DEFAULT_BORDER_COLOR = Color.argb(255, 255, 153, 0); 49 private static final int MOVE_FRAME_AMOUNT = 20; 50 51 private IWindowManager mIWm; 52 // An instance of WindowManager that is adjusted for adding windows with type 53 // TYPE_APPLICATION_OVERLAY. 54 private WindowManager mWm; 55 56 private SurfaceControl mSurfaceControl = new SurfaceControl(); 57 private SurfaceControl mBorderSc; 58 59 private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); 60 private View mOverlayView; 61 private View mArrowOverlay; 62 63 private Rect mWindowBounds = new Rect(); 64 65 private EditText mScaleText; 66 private EditText mDisplayFrameText; 67 private TextView mSourcePositionText; 68 69 private Rect mTmpRect = new Rect(); 70 private final Surface mTmpSurface = new Surface(); 71 72 private boolean mHasMirror; 73 74 private Rect mCurrFrame = new Rect(); 75 private float mCurrScale = DEFAULT_SCALE; 76 77 private final Handler mHandler = new Handler(); 78 79 private MoveMirrorRunnable mMoveMirrorRunnable = new MoveMirrorRunnable(); 80 private boolean mIsPressedDown = false; 81 82 private int mDisplayId; 83 84 @Override onCreate(Bundle savedInstanceState)85 protected void onCreate(Bundle savedInstanceState) { 86 super.onCreate(savedInstanceState); 87 88 setContentView(R.layout.activity_mirror_surface); 89 mWm = createWindowContext(TYPE_APPLICATION_OVERLAY, null /* options */) 90 .getSystemService(WindowManager.class); 91 mIWm = WindowManagerGlobal.getWindowManagerService(); 92 93 Rect windowBounds = WindowMetricsHelper.getBoundsExcludingNavigationBarAndCutout( 94 mWm.getCurrentWindowMetrics()); 95 mWindowBounds.set(0, 0, windowBounds.width(), windowBounds.height()); 96 97 mScaleText = findViewById(R.id.scale); 98 mDisplayFrameText = findViewById(R.id.displayFrame); 99 mSourcePositionText = findViewById(R.id.sourcePosition); 100 101 mCurrFrame.set(0, 0, mWindowBounds.width() / 2, mWindowBounds.height() / 2); 102 mCurrScale = DEFAULT_SCALE; 103 104 mDisplayId = getDisplay().getDisplayId(); 105 updateEditTexts(); 106 107 findViewById(R.id.mirror_button).setOnClickListener(view -> { 108 if (mArrowOverlay == null) { 109 createArrowOverlay(); 110 } 111 createOrUpdateMirror(); 112 }); 113 114 findViewById(R.id.remove_mirror_button).setOnClickListener(v -> { 115 removeMirror(); 116 removeArrowOverlay(); 117 }); 118 119 createMirrorOverlay(); 120 } 121 updateEditTexts()122 private void updateEditTexts() { 123 mDisplayFrameText.setText( 124 String.format("%s, %s, %s, %s", mCurrFrame.left, mCurrFrame.top, mCurrFrame.right, 125 mCurrFrame.bottom)); 126 mScaleText.setText(String.valueOf(mCurrScale)); 127 } 128 129 @Override onDestroy()130 protected void onDestroy() { 131 super.onDestroy(); 132 if (mOverlayView != null) { 133 removeMirror(); 134 mWm.removeView(mOverlayView); 135 mOverlayView = null; 136 } 137 removeArrowOverlay(); 138 } 139 createArrowOverlay()140 private void createArrowOverlay() { 141 mArrowOverlay = getLayoutInflater().inflate(R.layout.move_view, null); 142 WindowManager.LayoutParams arrowParams = new WindowManager.LayoutParams( 143 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 144 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, 145 WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 146 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH 147 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, 148 PixelFormat.RGBA_8888); 149 arrowParams.gravity = Gravity.RIGHT | Gravity.BOTTOM; 150 mWm.addView(mArrowOverlay, arrowParams); 151 152 View leftArrow = mArrowOverlay.findViewById(R.id.left_arrow); 153 View topArrow = mArrowOverlay.findViewById(R.id.up_arrow); 154 View rightArrow = mArrowOverlay.findViewById(R.id.right_arrow); 155 View bottomArrow = mArrowOverlay.findViewById(R.id.down_arrow); 156 157 leftArrow.setOnClickListener(this); 158 topArrow.setOnClickListener(this); 159 rightArrow.setOnClickListener(this); 160 bottomArrow.setOnClickListener(this); 161 162 leftArrow.setOnLongClickListener(this); 163 topArrow.setOnLongClickListener(this); 164 rightArrow.setOnLongClickListener(this); 165 bottomArrow.setOnLongClickListener(this); 166 167 leftArrow.setOnTouchListener(this); 168 topArrow.setOnTouchListener(this); 169 rightArrow.setOnTouchListener(this); 170 bottomArrow.setOnTouchListener(this); 171 172 mArrowOverlay.findViewById(R.id.zoom_in_button).setOnClickListener(v -> { 173 if (mCurrScale <= 1) { 174 mCurrScale *= 2; 175 } else { 176 mCurrScale += 0.5; 177 } 178 179 updateMirror(mCurrFrame, mCurrScale); 180 }); 181 mArrowOverlay.findViewById(R.id.zoom_out_button).setOnClickListener(v -> { 182 if (mCurrScale <= 1) { 183 mCurrScale /= 2; 184 } else { 185 mCurrScale -= 0.5; 186 } 187 188 updateMirror(mCurrFrame, mCurrScale); 189 }); 190 } 191 removeArrowOverlay()192 private void removeArrowOverlay() { 193 if (mArrowOverlay != null) { 194 mWm.removeView(mArrowOverlay); 195 mArrowOverlay = null; 196 } 197 } 198 createMirrorOverlay()199 private void createMirrorOverlay() { 200 mOverlayView = new LinearLayout(this); 201 WindowManager.LayoutParams params = new WindowManager.LayoutParams(mWindowBounds.width(), 202 mWindowBounds.height(), 203 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, 204 WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE 205 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, 206 PixelFormat.RGBA_8888); 207 params.gravity = Gravity.LEFT | Gravity.TOP; 208 params.setTitle("Mirror Overlay"); 209 mOverlayView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE 210 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 211 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 212 | View.SYSTEM_UI_FLAG_FULLSCREEN 213 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY 214 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); 215 216 mWm.addView(mOverlayView, params); 217 218 } 219 removeMirror()220 private void removeMirror() { 221 if (mSurfaceControl.isValid()) { 222 mTransaction.remove(mSurfaceControl).apply(); 223 } 224 mHasMirror = false; 225 } 226 createOrUpdateMirror()227 private void createOrUpdateMirror() { 228 if (mHasMirror) { 229 updateMirror(getDisplayFrame(), getScale()); 230 } else { 231 createMirror(getDisplayFrame(), getScale()); 232 } 233 234 } 235 getDisplayFrame()236 private Rect getDisplayFrame() { 237 mTmpRect.setEmpty(); 238 String[] frameVals = mDisplayFrameText.getText().toString().split("\\s*,\\s*"); 239 if (frameVals.length != 4) { 240 return mTmpRect; 241 } 242 243 try { 244 mTmpRect.set(Integer.parseInt(frameVals[0]), Integer.parseInt(frameVals[1]), 245 Integer.parseInt(frameVals[2]), Integer.parseInt(frameVals[3])); 246 } catch (Exception e) { 247 mTmpRect.setEmpty(); 248 } 249 250 return mTmpRect; 251 } 252 getScale()253 private float getScale() { 254 try { 255 return Float.parseFloat(mScaleText.getText().toString()); 256 } catch (Exception e) { 257 return -1; 258 } 259 } 260 createMirror(Rect displayFrame, float scale)261 private void createMirror(Rect displayFrame, float scale) { 262 boolean success = false; 263 try { 264 success = mIWm.mirrorDisplay(mDisplayId, mSurfaceControl); 265 } catch (RemoteException e) { 266 } 267 268 if (!success) { 269 return; 270 } 271 272 if (!mSurfaceControl.isValid()) { 273 return; 274 } 275 276 mHasMirror = true; 277 278 mBorderSc = new SurfaceControl.Builder() 279 .setName("Mirror Border") 280 .setBufferSize(1, 1) 281 .setFormat(PixelFormat.TRANSLUCENT) 282 .build(); 283 284 updateMirror(displayFrame, scale); 285 286 mTransaction 287 .show(mSurfaceControl) 288 .reparent(mSurfaceControl, mOverlayView.getViewRootImpl().getSurfaceControl()) 289 .setLayer(mBorderSc, 1) 290 .show(mBorderSc) 291 .reparent(mBorderSc, mSurfaceControl) 292 .apply(); 293 } 294 updateMirror(Rect displayFrame, float scale)295 private void updateMirror(Rect displayFrame, float scale) { 296 if (displayFrame.isEmpty()) { 297 Rect bounds = mWindowBounds; 298 int defaultCropW = bounds.width() / 2; 299 int defaultCropH = bounds.height() / 2; 300 displayFrame.set(0, 0, defaultCropW, defaultCropH); 301 } 302 303 if (scale <= 0) { 304 scale = DEFAULT_SCALE; 305 } 306 307 mCurrFrame.set(displayFrame); 308 mCurrScale = scale; 309 310 int width = (int) Math.ceil(displayFrame.width() / scale); 311 int height = (int) Math.ceil(displayFrame.height() / scale); 312 313 Rect sourceBounds = getSourceBounds(displayFrame, scale); 314 315 mTransaction.setGeometry(mSurfaceControl, sourceBounds, displayFrame, Surface.ROTATION_0) 316 .setPosition(mBorderSc, sourceBounds.left, sourceBounds.top) 317 .setBufferSize(mBorderSc, width, height) 318 .apply(); 319 320 drawBorder(mBorderSc, width, height, (int) Math.ceil(BORDER_SIZE / scale)); 321 322 mSourcePositionText.setText(sourceBounds.left + ", " + sourceBounds.top); 323 mDisplayFrameText.setText( 324 String.format("%s, %s, %s, %s", mCurrFrame.left, mCurrFrame.top, mCurrFrame.right, 325 mCurrFrame.bottom)); 326 mScaleText.setText(String.valueOf(mCurrScale)); 327 } 328 drawBorder(SurfaceControl borderSc, int width, int height, int borderSize)329 private void drawBorder(SurfaceControl borderSc, int width, int height, int borderSize) { 330 mTmpSurface.copyFrom(borderSc); 331 332 Canvas c = null; 333 try { 334 c = mTmpSurface.lockCanvas(null); 335 } catch (IllegalArgumentException | Surface.OutOfResourcesException e) { 336 } 337 if (c == null) { 338 return; 339 } 340 341 // Top 342 c.save(); 343 c.clipRect(new Rect(0, 0, width, borderSize)); 344 c.drawColor(DEFAULT_BORDER_COLOR); 345 c.restore(); 346 // Left 347 c.save(); 348 c.clipRect(new Rect(0, 0, borderSize, height)); 349 c.drawColor(DEFAULT_BORDER_COLOR); 350 c.restore(); 351 // Right 352 c.save(); 353 c.clipRect(new Rect(width - borderSize, 0, width, height)); 354 c.drawColor(DEFAULT_BORDER_COLOR); 355 c.restore(); 356 // Bottom 357 c.save(); 358 c.clipRect(new Rect(0, height - borderSize, width, height)); 359 c.drawColor(DEFAULT_BORDER_COLOR); 360 c.restore(); 361 362 mTmpSurface.unlockCanvasAndPost(c); 363 } 364 365 @Override onClick(View v)366 public void onClick(View v) { 367 Point offset = findOffset(v); 368 moveMirrorForArrows(offset.x, offset.y); 369 } 370 371 @Override onLongClick(View v)372 public boolean onLongClick(View v) { 373 mIsPressedDown = true; 374 Point point = findOffset(v); 375 mMoveMirrorRunnable.mXOffset = point.x; 376 mMoveMirrorRunnable.mYOffset = point.y; 377 mHandler.post(mMoveMirrorRunnable); 378 return false; 379 } 380 381 @Override onTouch(View v, MotionEvent event)382 public boolean onTouch(View v, MotionEvent event) { 383 switch (event.getAction()) { 384 case MotionEvent.ACTION_UP: 385 case MotionEvent.ACTION_CANCEL: 386 mIsPressedDown = false; 387 break; 388 } 389 return false; 390 } 391 findOffset(View v)392 private Point findOffset(View v) { 393 Point offset = new Point(0, 0); 394 395 switch (v.getId()) { 396 case R.id.up_arrow: 397 offset.y = -MOVE_FRAME_AMOUNT; 398 break; 399 case R.id.down_arrow: 400 offset.y = MOVE_FRAME_AMOUNT; 401 break; 402 case R.id.right_arrow: 403 offset.x = -MOVE_FRAME_AMOUNT; 404 break; 405 case R.id.left_arrow: 406 offset.x = MOVE_FRAME_AMOUNT; 407 break; 408 } 409 410 return offset; 411 } 412 moveMirrorForArrows(int xOffset, int yOffset)413 private void moveMirrorForArrows(int xOffset, int yOffset) { 414 mCurrFrame.offset(xOffset, yOffset); 415 416 updateMirror(mCurrFrame, mCurrScale); 417 } 418 419 /** 420 * Calculates the desired source bounds. This will be the area under from the center of the 421 * displayFrame, factoring in scale. 422 */ getSourceBounds(Rect displayFrame, float scale)423 private Rect getSourceBounds(Rect displayFrame, float scale) { 424 int halfWidth = displayFrame.width() / 2; 425 int halfHeight = displayFrame.height() / 2; 426 int left = displayFrame.left + (halfWidth - (int) (halfWidth / scale)); 427 int right = displayFrame.right - (halfWidth - (int) (halfWidth / scale)); 428 int top = displayFrame.top + (halfHeight - (int) (halfHeight / scale)); 429 int bottom = displayFrame.bottom - (halfHeight - (int) (halfHeight / scale)); 430 return new Rect(left, top, right, bottom); 431 } 432 433 class MoveMirrorRunnable implements Runnable { 434 int mXOffset = 0; 435 int mYOffset = 0; 436 437 @Override run()438 public void run() { 439 if (mIsPressedDown) { 440 moveMirrorForArrows(mXOffset, mYOffset); 441 mHandler.postDelayed(mMoveMirrorRunnable, 150); 442 } 443 } 444 } 445 } 446