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