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