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