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