1 /*
2  * Copyright 2022 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.example.androidx.mediarouting;
18 
19 import android.content.Context;
20 import android.graphics.Bitmap;
21 import android.graphics.SurfaceTexture;
22 import android.hardware.display.DisplayManager;
23 import android.os.Build;
24 import android.util.DisplayMetrics;
25 import android.util.Log;
26 import android.view.Display;
27 import android.view.GestureDetector;
28 import android.view.Gravity;
29 import android.view.LayoutInflater;
30 import android.view.MotionEvent;
31 import android.view.ScaleGestureDetector;
32 import android.view.Surface;
33 import android.view.SurfaceHolder;
34 import android.view.TextureView;
35 import android.view.TextureView.SurfaceTextureListener;
36 import android.view.View;
37 import android.view.WindowManager;
38 import android.widget.TextView;
39 
40 import org.jspecify.annotations.NonNull;
41 import org.jspecify.annotations.Nullable;
42 
43 /**
44  * Manages an overlay display window, used for simulating remote playback.
45  */
46 public abstract class OverlayDisplayWindow {
47     private static final String TAG = "OverlayDisplayWindow";
48     private static final boolean DEBUG = false;
49 
50     private static final float WINDOW_ALPHA = 0.8f;
51     private static final float INITIAL_SCALE = 0.5f;
52     private static final float MIN_SCALE = 0.3f;
53     private static final float MAX_SCALE = 1.0f;
54 
55     protected final Context mContext;
56     protected final String mName;
57     protected final int mWidth;
58     protected final int mHeight;
59     protected final int mGravity;
60     protected @Nullable OverlayWindowListener mListener;
61 
OverlayDisplayWindow(@onNull Context context, @NonNull String name, int width, int height, int gravity)62     protected OverlayDisplayWindow(@NonNull Context context, @NonNull String name, int width,
63             int height, int gravity) {
64         mContext = context;
65         mName = name;
66         mWidth = width;
67         mHeight = height;
68         mGravity = gravity;
69     }
70 
71     /**
72      * Factory methd to create the overlay window.
73      *
74      * @return the created overlay window.
75      */
create(@onNull Context context, @NonNull String name, int width, int height, int gravity)76     public static @NonNull OverlayDisplayWindow create(@NonNull Context context,
77             @NonNull String name, int width, int height, int gravity) {
78         return new JellybeanMr1Impl(context, name, width, height, gravity);
79     }
80 
setOverlayWindowListener(@onNull OverlayWindowListener listener)81     public void setOverlayWindowListener(@NonNull OverlayWindowListener listener) {
82         mListener = listener;
83     }
84 
getContext()85     public @NonNull Context getContext() {
86         return mContext;
87     }
88 
89     /**
90      * Shows the overlay window.
91      */
show()92     public abstract void show();
93 
94     /**
95      * Dismisses the overlay window.
96      */
dismiss()97     public abstract void dismiss();
98 
99     /**
100      * Change the view aspect ration to a new ratio.
101      */
updateAspectRatio(int width, int height)102     public abstract void updateAspectRatio(int width, int height);
103 
104     /**
105      * Gets a bitmap representing the snapshot of the window.
106      *
107      * @return a bitmap representing the snapshot of the window.
108      */
getSnapshot()109     public abstract @Nullable Bitmap getSnapshot();
110 
111     /**
112      * Watches for significant changes in the overlay display window lifecycle.
113      */
114     public interface OverlayWindowListener {
115         /**
116          * Called when the window is created.
117          */
onWindowCreated(@onNull Surface surface)118         void onWindowCreated(@NonNull Surface surface);
119 
120         /**
121          * Called when the window is created.
122          */
onWindowCreated(@onNull SurfaceHolder surfaceHolder)123         void onWindowCreated(@NonNull SurfaceHolder surfaceHolder);
124 
125         /**
126          * Called when the window is destroyed.
127          */
onWindowDestroyed()128         void onWindowDestroyed();
129     }
130 
131     /**
132      * Implementation for API version 17+.
133      */
134     private static final class JellybeanMr1Impl extends OverlayDisplayWindow {
135         // When true, disables support for moving and resizing the overlay.
136         // The window is made non-touchable, which makes it possible to
137         // directly interact with the content underneath.
138         private static final boolean DISABLE_MOVE_AND_RESIZE = false;
139 
140         private final DisplayManager mDisplayManager;
141         private final WindowManager mWindowManager;
142 
143         private final Display mDefaultDisplay;
144         private final DisplayMetrics mDefaultDisplayMetrics = new DisplayMetrics();
145 
146         private View mWindowContent;
147         private WindowManager.LayoutParams mWindowParams;
148         private TextureView mTextureView;
149         private TextView mNameTextView;
150 
151         private GestureDetector mGestureDetector;
152         private ScaleGestureDetector mScaleGestureDetector;
153 
154         private boolean mWindowVisible;
155         private int mWindowX;
156         private int mWindowY;
157         private float mWindowScale;
158 
159         private float mLiveTranslationX;
160         private float mLiveTranslationY;
161         private float mLiveScale = 1.0f;
162 
JellybeanMr1Impl( Context context, String name, int width, int height, int gravity)163         JellybeanMr1Impl(
164                 Context context, String name, int width, int height, int gravity) {
165             super(context, name, width, height, gravity);
166 
167             mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
168             mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
169 
170             mDefaultDisplay = mWindowManager.getDefaultDisplay();
171             updateDefaultDisplayInfo();
172 
173             createWindow();
174         }
175 
176         @Override
show()177         public void show() {
178             if (!mWindowVisible) {
179                 mDisplayManager.registerDisplayListener(mDisplayListener, null);
180                 if (!updateDefaultDisplayInfo()) {
181                     mDisplayManager.unregisterDisplayListener(mDisplayListener);
182                     return;
183                 }
184 
185                 clearLiveState();
186                 updateWindowParams();
187                 mWindowManager.addView(mWindowContent, mWindowParams);
188                 mWindowVisible = true;
189             }
190         }
191 
192         @Override
dismiss()193         public void dismiss() {
194             if (mWindowVisible) {
195                 mDisplayManager.unregisterDisplayListener(mDisplayListener);
196                 mWindowManager.removeView(mWindowContent);
197                 mWindowVisible = false;
198             }
199         }
200 
201         @Override
updateAspectRatio(int width, int height)202         public void updateAspectRatio(int width, int height) {
203             if (mWidth * height < mHeight * width) {
204                 mTextureView.getLayoutParams().width = mWidth;
205                 mTextureView.getLayoutParams().height = mWidth * height / width;
206             } else {
207                 mTextureView.getLayoutParams().width = mHeight * width / height;
208                 mTextureView.getLayoutParams().height = mHeight;
209             }
210             relayout();
211         }
212 
213         @Override
getSnapshot()214         public @NonNull Bitmap getSnapshot() {
215             return mTextureView.getBitmap();
216         }
217 
relayout()218         private void relayout() {
219             if (mWindowVisible) {
220                 updateWindowParams();
221                 mWindowManager.updateViewLayout(mWindowContent, mWindowParams);
222             }
223         }
224 
updateDefaultDisplayInfo()225         private boolean updateDefaultDisplayInfo() {
226             mDefaultDisplay.getMetrics(mDefaultDisplayMetrics);
227             return true;
228         }
229 
createWindow()230         private void createWindow() {
231             LayoutInflater inflater = LayoutInflater.from(mContext);
232 
233             mWindowContent = inflater.inflate(R.layout.overlay_display_window, null);
234             mWindowContent.setOnTouchListener(mOnTouchListener);
235 
236             mTextureView = (TextureView) mWindowContent.findViewById(
237                     R.id.overlay_display_window_texture);
238             mTextureView.setPivotX(0);
239             mTextureView.setPivotY(0);
240             mTextureView.getLayoutParams().width = mWidth;
241             mTextureView.getLayoutParams().height = mHeight;
242             mTextureView.setOpaque(false);
243             mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
244 
245             mNameTextView = (TextView) mWindowContent.findViewById(
246                     R.id.overlay_display_window_title);
247             mNameTextView.setText(mName);
248 
249             if (Build.VERSION.SDK_INT >= 26) {
250                 // TYPE_SYSTEM_ALERT is deprecated in android O.
251                 mWindowParams = new WindowManager.LayoutParams(
252                         WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
253             } else {
254                 mWindowParams = new WindowManager.LayoutParams(
255                         WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
256             }
257             mWindowParams.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
258                     | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
259                     | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
260                     | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
261                     | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
262             if (DISABLE_MOVE_AND_RESIZE) {
263                 mWindowParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
264             }
265             mWindowParams.alpha = WINDOW_ALPHA;
266             mWindowParams.gravity = Gravity.TOP | Gravity.LEFT;
267             mWindowParams.setTitle(mName);
268 
269             mGestureDetector = new GestureDetector(mContext, mOnGestureListener);
270             mScaleGestureDetector = new ScaleGestureDetector(mContext, mOnScaleGestureListener);
271 
272             // Set the initial position and scale.
273             // The position and scale will be clamped when the display is first shown.
274             mWindowX = (mGravity & Gravity.LEFT) == Gravity.LEFT
275                     ? 0 : mDefaultDisplayMetrics.widthPixels;
276             mWindowY = (mGravity & Gravity.TOP) == Gravity.TOP
277                     ? 0 : mDefaultDisplayMetrics.heightPixels;
278             Log.d(TAG, mDefaultDisplayMetrics.toString());
279             mWindowScale = INITIAL_SCALE;
280 
281             // calculate and save initial settings
282             updateWindowParams();
283             saveWindowParams();
284         }
285 
updateWindowParams()286         private void updateWindowParams() {
287             float scale = mWindowScale * mLiveScale;
288             scale = Math.min(scale, (float) mDefaultDisplayMetrics.widthPixels / mWidth);
289             scale = Math.min(scale, (float) mDefaultDisplayMetrics.heightPixels / mHeight);
290             scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale));
291 
292             float offsetScale = (scale / mWindowScale - 1.0f) * 0.5f;
293             int width = (int) (mWidth * scale);
294             int height = (int) (mHeight * scale);
295             int x = (int) (mWindowX + mLiveTranslationX - width * offsetScale);
296             int y = (int) (mWindowY + mLiveTranslationY - height * offsetScale);
297             x = Math.max(0, Math.min(x, mDefaultDisplayMetrics.widthPixels - width));
298             y = Math.max(0, Math.min(y, mDefaultDisplayMetrics.heightPixels - height));
299 
300             if (DEBUG) {
301                 Log.d(TAG, "updateWindowParams: scale=" + scale + ", offsetScale=" + offsetScale
302                         + ", x=" + x + ", y=" + y + ", width=" + width + ", height=" + height);
303             }
304 
305             mTextureView.setScaleX(scale);
306             mTextureView.setScaleY(scale);
307 
308             mTextureView.setTranslationX(
309                     (mWidth - mTextureView.getLayoutParams().width) * scale / 2);
310             mTextureView.setTranslationY(
311                     (mHeight - mTextureView.getLayoutParams().height) * scale / 2);
312 
313             mWindowParams.x = x;
314             mWindowParams.y = y;
315             mWindowParams.width = width;
316             mWindowParams.height = height;
317         }
318 
saveWindowParams()319         private void saveWindowParams() {
320             mWindowX = mWindowParams.x;
321             mWindowY = mWindowParams.y;
322             mWindowScale = mTextureView.getScaleX();
323             clearLiveState();
324         }
325 
clearLiveState()326         private void clearLiveState() {
327             mLiveTranslationX = 0f;
328             mLiveTranslationY = 0f;
329             mLiveScale = 1.0f;
330         }
331 
332         private final DisplayManager.DisplayListener mDisplayListener =
333                 new DisplayManager.DisplayListener() {
334                     @Override
335                     public void onDisplayAdded(int displayId) {
336                     }
337 
338                     @Override
339                     public void onDisplayChanged(int displayId) {
340                         if (displayId == mDefaultDisplay.getDisplayId()) {
341                             if (updateDefaultDisplayInfo()) {
342                                 relayout();
343                             } else {
344                                 dismiss();
345                             }
346                         }
347                     }
348 
349                     @Override
350                     public void onDisplayRemoved(int displayId) {
351                         if (displayId == mDefaultDisplay.getDisplayId()) {
352                             dismiss();
353                         }
354                     }
355                 };
356 
357         private final SurfaceTextureListener mSurfaceTextureListener =
358                 new SurfaceTextureListener() {
359                     @Override
360                     public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width,
361                             int height) {
362                         if (mListener != null) {
363                             mListener.onWindowCreated(new Surface(surfaceTexture));
364                         }
365                     }
366 
367                     @Override
368                     public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
369                         if (mListener != null) {
370                             mListener.onWindowDestroyed();
371                         }
372                         return true;
373                     }
374 
375                     @Override
376                     public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture,
377                             int width, int height) {
378                     }
379 
380                     @Override
381                     public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
382                     }
383                 };
384 
385         private final View.OnTouchListener mOnTouchListener = new View.OnTouchListener() {
386             @Override
387             public boolean onTouch(View view, MotionEvent event) {
388                 // Work in screen coordinates.
389                 final float oldX = event.getX();
390                 final float oldY = event.getY();
391                 event.setLocation(event.getRawX(), event.getRawY());
392 
393                 mGestureDetector.onTouchEvent(event);
394                 mScaleGestureDetector.onTouchEvent(event);
395 
396                 switch (event.getActionMasked()) {
397                     case MotionEvent.ACTION_UP:
398                     case MotionEvent.ACTION_CANCEL:
399                         saveWindowParams();
400                         break;
401                 }
402 
403                 // Revert to window coordinates.
404                 event.setLocation(oldX, oldY);
405                 return true;
406             }
407         };
408 
409         private final GestureDetector.OnGestureListener mOnGestureListener =
410                 new GestureDetector.SimpleOnGestureListener() {
411                     @Override
412                     public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
413                             float distanceY) {
414                         mLiveTranslationX -= distanceX;
415                         mLiveTranslationY -= distanceY;
416                         relayout();
417                         return true;
418                     }
419                 };
420 
421         private final ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener =
422                 new ScaleGestureDetector.SimpleOnScaleGestureListener() {
423                     @Override
424                     public boolean onScale(ScaleGestureDetector detector) {
425                         mLiveScale *= detector.getScaleFactor();
426                         relayout();
427                         return true;
428                     }
429                 };
430     }
431 }
432