| /* |
| * Copyright (C) 2019 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.google.android.test.mirrorsurface; |
| |
| |
| import android.app.Activity; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.PixelFormat; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.RemoteException; |
| import android.util.DisplayMetrics; |
| import android.view.Gravity; |
| import android.view.IWindowManager; |
| import android.view.MotionEvent; |
| import android.view.Surface; |
| import android.view.SurfaceControl; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.WindowManager; |
| import android.view.WindowManagerGlobal; |
| import android.widget.EditText; |
| import android.widget.LinearLayout; |
| import android.widget.TextView; |
| |
| public class MirrorSurfaceActivity extends Activity implements View.OnClickListener, |
| View.OnLongClickListener, View.OnTouchListener { |
| private static final int BORDER_SIZE = 10; |
| private static final int DEFAULT_SCALE = 2; |
| private static final int DEFAULT_BORDER_COLOR = Color.argb(255, 255, 153, 0); |
| private static final int MOVE_FRAME_AMOUNT = 20; |
| |
| private IWindowManager mIWm; |
| private WindowManager mWm; |
| |
| private SurfaceControl mSurfaceControl = new SurfaceControl(); |
| private SurfaceControl mBorderSc; |
| |
| private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); |
| private View mOverlayView; |
| private View mArrowOverlay; |
| |
| private Rect mDisplayBounds = new Rect(); |
| |
| private EditText mScaleText; |
| private EditText mDisplayFrameText; |
| private TextView mSourcePositionText; |
| |
| private Rect mTmpRect = new Rect(); |
| private final Surface mTmpSurface = new Surface(); |
| |
| private boolean mHasMirror; |
| |
| private Rect mCurrFrame = new Rect(); |
| private float mCurrScale = DEFAULT_SCALE; |
| |
| private final Handler mHandler = new Handler(); |
| |
| private MoveMirrorRunnable mMoveMirrorRunnable = new MoveMirrorRunnable(); |
| private boolean mIsPressedDown = false; |
| |
| private int mDisplayId; |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| setContentView(R.layout.activity_mirror_surface); |
| mWm = (WindowManager) getSystemService(WINDOW_SERVICE); |
| mIWm = WindowManagerGlobal.getWindowManagerService(); |
| |
| DisplayMetrics displayMetrics = new DisplayMetrics(); |
| getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); |
| mDisplayBounds.set(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels); |
| |
| mScaleText = findViewById(R.id.scale); |
| mDisplayFrameText = findViewById(R.id.displayFrame); |
| mSourcePositionText = findViewById(R.id.sourcePosition); |
| |
| mCurrFrame.set(0, 0, mDisplayBounds.width() / 2, mDisplayBounds.height() / 2); |
| mCurrScale = DEFAULT_SCALE; |
| |
| mDisplayId = getWindowManager().getDefaultDisplay().getDisplayId(); |
| updateEditTexts(); |
| |
| findViewById(R.id.mirror_button).setOnClickListener(view -> { |
| if (mArrowOverlay == null) { |
| createArrowOverlay(); |
| } |
| createOrUpdateMirror(); |
| }); |
| |
| findViewById(R.id.remove_mirror_button).setOnClickListener(v -> { |
| removeMirror(); |
| removeArrowOverlay(); |
| }); |
| |
| createMirrorOverlay(); |
| } |
| |
| private void updateEditTexts() { |
| mDisplayFrameText.setText( |
| String.format("%s, %s, %s, %s", mCurrFrame.left, mCurrFrame.top, mCurrFrame.right, |
| mCurrFrame.bottom)); |
| mScaleText.setText(String.valueOf(mCurrScale)); |
| } |
| |
| @Override |
| protected void onDestroy() { |
| super.onDestroy(); |
| if (mOverlayView != null) { |
| removeMirror(); |
| mWm.removeView(mOverlayView); |
| mOverlayView = null; |
| } |
| removeArrowOverlay(); |
| } |
| |
| private void createArrowOverlay() { |
| mArrowOverlay = getLayoutInflater().inflate(R.layout.move_view, null); |
| WindowManager.LayoutParams arrowParams = new WindowManager.LayoutParams( |
| ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, |
| WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, |
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
| | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH |
| | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, |
| PixelFormat.RGBA_8888); |
| arrowParams.gravity = Gravity.RIGHT | Gravity.BOTTOM; |
| mWm.addView(mArrowOverlay, arrowParams); |
| |
| View leftArrow = mArrowOverlay.findViewById(R.id.left_arrow); |
| View topArrow = mArrowOverlay.findViewById(R.id.up_arrow); |
| View rightArrow = mArrowOverlay.findViewById(R.id.right_arrow); |
| View bottomArrow = mArrowOverlay.findViewById(R.id.down_arrow); |
| |
| leftArrow.setOnClickListener(this); |
| topArrow.setOnClickListener(this); |
| rightArrow.setOnClickListener(this); |
| bottomArrow.setOnClickListener(this); |
| |
| leftArrow.setOnLongClickListener(this); |
| topArrow.setOnLongClickListener(this); |
| rightArrow.setOnLongClickListener(this); |
| bottomArrow.setOnLongClickListener(this); |
| |
| leftArrow.setOnTouchListener(this); |
| topArrow.setOnTouchListener(this); |
| rightArrow.setOnTouchListener(this); |
| bottomArrow.setOnTouchListener(this); |
| |
| mArrowOverlay.findViewById(R.id.zoom_in_button).setOnClickListener(v -> { |
| if (mCurrScale <= 1) { |
| mCurrScale *= 2; |
| } else { |
| mCurrScale += 0.5; |
| } |
| |
| updateMirror(mCurrFrame, mCurrScale); |
| }); |
| mArrowOverlay.findViewById(R.id.zoom_out_button).setOnClickListener(v -> { |
| if (mCurrScale <= 1) { |
| mCurrScale /= 2; |
| } else { |
| mCurrScale -= 0.5; |
| } |
| |
| updateMirror(mCurrFrame, mCurrScale); |
| }); |
| } |
| |
| private void removeArrowOverlay() { |
| if (mArrowOverlay != null) { |
| mWm.removeView(mArrowOverlay); |
| mArrowOverlay = null; |
| } |
| } |
| |
| private void createMirrorOverlay() { |
| mOverlayView = new LinearLayout(this); |
| WindowManager.LayoutParams params = new WindowManager.LayoutParams(mDisplayBounds.width(), |
| mDisplayBounds.height(), |
| WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, |
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
| | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, |
| PixelFormat.RGBA_8888); |
| params.gravity = Gravity.LEFT | Gravity.TOP; |
| params.setTitle("Mirror Overlay"); |
| mOverlayView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
| | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
| | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
| | View.SYSTEM_UI_FLAG_FULLSCREEN |
| | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
| | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); |
| |
| mWm.addView(mOverlayView, params); |
| |
| } |
| |
| private void removeMirror() { |
| if (mSurfaceControl.isValid()) { |
| mTransaction.remove(mSurfaceControl).apply(); |
| } |
| mHasMirror = false; |
| } |
| |
| private void createOrUpdateMirror() { |
| if (mHasMirror) { |
| updateMirror(getDisplayFrame(), getScale()); |
| } else { |
| createMirror(getDisplayFrame(), getScale()); |
| } |
| |
| } |
| |
| private Rect getDisplayFrame() { |
| mTmpRect.setEmpty(); |
| String[] frameVals = mDisplayFrameText.getText().toString().split("\\s*,\\s*"); |
| if (frameVals.length != 4) { |
| return mTmpRect; |
| } |
| |
| try { |
| mTmpRect.set(Integer.parseInt(frameVals[0]), Integer.parseInt(frameVals[1]), |
| Integer.parseInt(frameVals[2]), Integer.parseInt(frameVals[3])); |
| } catch (Exception e) { |
| mTmpRect.setEmpty(); |
| } |
| |
| return mTmpRect; |
| } |
| |
| private float getScale() { |
| try { |
| return Float.parseFloat(mScaleText.getText().toString()); |
| } catch (Exception e) { |
| return -1; |
| } |
| } |
| |
| private void createMirror(Rect displayFrame, float scale) { |
| boolean success = false; |
| try { |
| success = mIWm.mirrorDisplay(mDisplayId, mSurfaceControl); |
| } catch (RemoteException e) { |
| } |
| |
| if (!success) { |
| return; |
| } |
| |
| if (!mSurfaceControl.isValid()) { |
| return; |
| } |
| |
| mHasMirror = true; |
| |
| mBorderSc = new SurfaceControl.Builder() |
| .setName("Mirror Border") |
| .setBufferSize(1, 1) |
| .setFormat(PixelFormat.TRANSLUCENT) |
| .build(); |
| |
| updateMirror(displayFrame, scale); |
| |
| mTransaction |
| .show(mSurfaceControl) |
| .reparent(mSurfaceControl, mOverlayView.getViewRootImpl().getSurfaceControl()) |
| .setLayer(mBorderSc, 1) |
| .show(mBorderSc) |
| .reparent(mBorderSc, mSurfaceControl) |
| .apply(); |
| } |
| |
| private void updateMirror(Rect displayFrame, float scale) { |
| if (displayFrame.isEmpty()) { |
| Rect bounds = mDisplayBounds; |
| int defaultCropW = Math.round(bounds.width() / 2); |
| int defaultCropH = Math.round(bounds.height() / 2); |
| displayFrame.set(0, 0, defaultCropW, defaultCropH); |
| } |
| |
| if (scale <= 0) { |
| scale = DEFAULT_SCALE; |
| } |
| |
| mCurrFrame.set(displayFrame); |
| mCurrScale = scale; |
| |
| int width = (int) Math.ceil(displayFrame.width() / scale); |
| int height = (int) Math.ceil(displayFrame.height() / scale); |
| |
| Rect sourceBounds = getSourceBounds(displayFrame, scale); |
| |
| mTransaction.setGeometry(mSurfaceControl, sourceBounds, displayFrame, Surface.ROTATION_0) |
| .setPosition(mBorderSc, sourceBounds.left, sourceBounds.top) |
| .setBufferSize(mBorderSc, width, height) |
| .apply(); |
| |
| drawBorder(mBorderSc, width, height, (int) Math.ceil(BORDER_SIZE / scale)); |
| |
| mSourcePositionText.setText(sourceBounds.left + ", " + sourceBounds.top); |
| mDisplayFrameText.setText( |
| String.format("%s, %s, %s, %s", mCurrFrame.left, mCurrFrame.top, mCurrFrame.right, |
| mCurrFrame.bottom)); |
| mScaleText.setText(String.valueOf(mCurrScale)); |
| } |
| |
| private void drawBorder(SurfaceControl borderSc, int width, int height, int borderSize) { |
| mTmpSurface.copyFrom(borderSc); |
| |
| Canvas c = null; |
| try { |
| c = mTmpSurface.lockCanvas(null); |
| } catch (IllegalArgumentException | Surface.OutOfResourcesException e) { |
| } |
| if (c == null) { |
| return; |
| } |
| |
| // Top |
| c.save(); |
| c.clipRect(new Rect(0, 0, width, borderSize)); |
| c.drawColor(DEFAULT_BORDER_COLOR); |
| c.restore(); |
| // Left |
| c.save(); |
| c.clipRect(new Rect(0, 0, borderSize, height)); |
| c.drawColor(DEFAULT_BORDER_COLOR); |
| c.restore(); |
| // Right |
| c.save(); |
| c.clipRect(new Rect(width - borderSize, 0, width, height)); |
| c.drawColor(DEFAULT_BORDER_COLOR); |
| c.restore(); |
| // Bottom |
| c.save(); |
| c.clipRect(new Rect(0, height - borderSize, width, height)); |
| c.drawColor(DEFAULT_BORDER_COLOR); |
| c.restore(); |
| |
| mTmpSurface.unlockCanvasAndPost(c); |
| } |
| |
| @Override |
| public void onClick(View v) { |
| Point offset = findOffset(v); |
| moveMirrorForArrows(offset.x, offset.y); |
| } |
| |
| @Override |
| public boolean onLongClick(View v) { |
| mIsPressedDown = true; |
| Point point = findOffset(v); |
| mMoveMirrorRunnable.mXOffset = point.x; |
| mMoveMirrorRunnable.mYOffset = point.y; |
| mHandler.post(mMoveMirrorRunnable); |
| return false; |
| } |
| |
| @Override |
| public boolean onTouch(View v, MotionEvent event) { |
| switch (event.getAction()) { |
| case MotionEvent.ACTION_UP: |
| case MotionEvent.ACTION_CANCEL: |
| mIsPressedDown = false; |
| break; |
| } |
| return false; |
| } |
| |
| private Point findOffset(View v) { |
| Point offset = new Point(0, 0); |
| |
| switch (v.getId()) { |
| case R.id.up_arrow: |
| offset.y = -MOVE_FRAME_AMOUNT; |
| break; |
| case R.id.down_arrow: |
| offset.y = MOVE_FRAME_AMOUNT; |
| break; |
| case R.id.right_arrow: |
| offset.x = -MOVE_FRAME_AMOUNT; |
| break; |
| case R.id.left_arrow: |
| offset.x = MOVE_FRAME_AMOUNT; |
| break; |
| } |
| |
| return offset; |
| } |
| |
| private void moveMirrorForArrows(int xOffset, int yOffset) { |
| mCurrFrame.offset(xOffset, yOffset); |
| |
| updateMirror(mCurrFrame, mCurrScale); |
| } |
| |
| /** |
| * Calculates the desired source bounds. This will be the area under from the center of the |
| * displayFrame, factoring in scale. |
| */ |
| private Rect getSourceBounds(Rect displayFrame, float scale) { |
| int halfWidth = displayFrame.width() / 2; |
| int halfHeight = displayFrame.height() / 2; |
| int left = displayFrame.left + (halfWidth - (int) (halfWidth / scale)); |
| int right = displayFrame.right - (halfWidth - (int) (halfWidth / scale)); |
| int top = displayFrame.top + (halfHeight - (int) (halfHeight / scale)); |
| int bottom = displayFrame.bottom - (halfHeight - (int) (halfHeight / scale)); |
| return new Rect(left, top, right, bottom); |
| } |
| |
| class MoveMirrorRunnable implements Runnable { |
| int mXOffset = 0; |
| int mYOffset = 0; |
| |
| @Override |
| public void run() { |
| if (mIsPressedDown) { |
| moveMirrorForArrows(mXOffset, mYOffset); |
| mHandler.postDelayed(mMoveMirrorRunnable, 150); |
| } |
| } |
| } |
| } |