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 = new WindowManager.LayoutParams(
130                        WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
131                params.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
132                        | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
133                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
134                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
135                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
136                params.alpha = WINDOW_ALPHA;
137                params.gravity = Gravity.LEFT | Gravity.BOTTOM;
138                params.setTitle(mName);
139
140                int width = (int)(display.getWidth() * INITIAL_SCALE);
141                int height = (int)(display.getHeight() * INITIAL_SCALE);
142                if (mWidth > mHeight) {
143                    height = mHeight * width / mWidth;
144                } else {
145                    width = mWidth * height / mHeight;
146                }
147                params.width = width;
148                params.height = height;
149
150                mWindowManager.addView(mSurfaceView, params);
151                mWindowVisible = true;
152
153                SurfaceHolder holder = mSurfaceView.getHolder();
154                holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
155                mListener.onWindowCreated(holder);
156            }
157        }
158
159        @Override
160        public void dismiss() {
161            if (mWindowVisible) {
162                mListener.onWindowDestroyed();
163
164                mWindowManager.removeView(mSurfaceView);
165                mWindowVisible = false;
166            }
167        }
168
169        @Override
170        public void updateAspectRatio(int width, int height) {
171        }
172
173        @Override
174        public Bitmap getSnapshot() {
175            return null;
176        }
177    }
178
179    /**
180     * Implementation for API version 17+.
181     */
182    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
183    private static final class JellybeanMr1Impl extends OverlayDisplayWindow {
184        // When true, disables support for moving and resizing the overlay.
185        // The window is made non-touchable, which makes it possible to
186        // directly interact with the content underneath.
187        private static final boolean DISABLE_MOVE_AND_RESIZE = false;
188
189        private final DisplayManager mDisplayManager;
190        private final WindowManager mWindowManager;
191
192        private final Display mDefaultDisplay;
193        private final DisplayMetrics mDefaultDisplayMetrics = new DisplayMetrics();
194
195        private View mWindowContent;
196        private WindowManager.LayoutParams mWindowParams;
197        private TextureView mTextureView;
198        private TextView mNameTextView;
199
200        private GestureDetector mGestureDetector;
201        private ScaleGestureDetector mScaleGestureDetector;
202
203        private boolean mWindowVisible;
204        private int mWindowX;
205        private int mWindowY;
206        private float mWindowScale;
207
208        private float mLiveTranslationX;
209        private float mLiveTranslationY;
210        private float mLiveScale = 1.0f;
211
212        public JellybeanMr1Impl(Context context, String name,
213                int width, int height, int gravity) {
214            super(context, name, width, height, gravity);
215
216            mDisplayManager = (DisplayManager)context.getSystemService(
217                    Context.DISPLAY_SERVICE);
218            mWindowManager = (WindowManager)context.getSystemService(
219                    Context.WINDOW_SERVICE);
220
221            mDefaultDisplay = mWindowManager.getDefaultDisplay();
222            updateDefaultDisplayInfo();
223
224            createWindow();
225        }
226
227        @Override
228        public void show() {
229            if (!mWindowVisible) {
230                mDisplayManager.registerDisplayListener(mDisplayListener, null);
231                if (!updateDefaultDisplayInfo()) {
232                    mDisplayManager.unregisterDisplayListener(mDisplayListener);
233                    return;
234                }
235
236                clearLiveState();
237                updateWindowParams();
238                mWindowManager.addView(mWindowContent, mWindowParams);
239                mWindowVisible = true;
240            }
241        }
242
243        @Override
244        public void dismiss() {
245            if (mWindowVisible) {
246                mDisplayManager.unregisterDisplayListener(mDisplayListener);
247                mWindowManager.removeView(mWindowContent);
248                mWindowVisible = false;
249            }
250        }
251
252        @Override
253        public void updateAspectRatio(int width, int height) {
254            if (mWidth * height < mHeight * width) {
255                mTextureView.getLayoutParams().width = mWidth;
256                mTextureView.getLayoutParams().height = mWidth * height / width;
257            } else {
258                mTextureView.getLayoutParams().width = mHeight * width / height;
259                mTextureView.getLayoutParams().height = mHeight;
260            }
261            relayout();
262        }
263
264        @Override
265        public Bitmap getSnapshot() {
266            return mTextureView.getBitmap();
267        }
268
269        private void relayout() {
270            if (mWindowVisible) {
271                updateWindowParams();
272                mWindowManager.updateViewLayout(mWindowContent, mWindowParams);
273            }
274        }
275
276        private boolean updateDefaultDisplayInfo() {
277            mDefaultDisplay.getMetrics(mDefaultDisplayMetrics);
278            return true;
279        }
280
281        private void createWindow() {
282            LayoutInflater inflater = LayoutInflater.from(mContext);
283
284            mWindowContent = inflater.inflate(
285                    R.layout.overlay_display_window, null);
286            mWindowContent.setOnTouchListener(mOnTouchListener);
287
288            mTextureView = (TextureView)mWindowContent.findViewById(
289                    R.id.overlay_display_window_texture);
290            mTextureView.setPivotX(0);
291            mTextureView.setPivotY(0);
292            mTextureView.getLayoutParams().width = mWidth;
293            mTextureView.getLayoutParams().height = mHeight;
294            mTextureView.setOpaque(false);
295            mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
296
297            mNameTextView = (TextView)mWindowContent.findViewById(
298                    R.id.overlay_display_window_title);
299            mNameTextView.setText(mName);
300
301            mWindowParams = new WindowManager.LayoutParams(
302                    WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
303            mWindowParams.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
304                    | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
305                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
306                    | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
307                    | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
308            if (DISABLE_MOVE_AND_RESIZE) {
309                mWindowParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
310            }
311            mWindowParams.alpha = WINDOW_ALPHA;
312            mWindowParams.gravity = Gravity.TOP | Gravity.LEFT;
313            mWindowParams.setTitle(mName);
314
315            mGestureDetector = new GestureDetector(mContext, mOnGestureListener);
316            mScaleGestureDetector = new ScaleGestureDetector(mContext, mOnScaleGestureListener);
317
318            // Set the initial position and scale.
319            // The position and scale will be clamped when the display is first shown.
320            mWindowX = (mGravity & Gravity.LEFT) == Gravity.LEFT ?
321                    0 : mDefaultDisplayMetrics.widthPixels;
322            mWindowY = (mGravity & Gravity.TOP) == Gravity.TOP ?
323                    0 : mDefaultDisplayMetrics.heightPixels;
324            Log.d(TAG, mDefaultDisplayMetrics.toString());
325            mWindowScale = INITIAL_SCALE;
326
327            // calculate and save initial settings
328            updateWindowParams();
329            saveWindowParams();
330        }
331
332        private void updateWindowParams() {
333            float scale = mWindowScale * mLiveScale;
334            scale = Math.min(scale, (float)mDefaultDisplayMetrics.widthPixels / mWidth);
335            scale = Math.min(scale, (float)mDefaultDisplayMetrics.heightPixels / mHeight);
336            scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale));
337
338            float offsetScale = (scale / mWindowScale - 1.0f) * 0.5f;
339            int width = (int)(mWidth * scale);
340            int height = (int)(mHeight * scale);
341            int x = (int)(mWindowX + mLiveTranslationX - width * offsetScale);
342            int y = (int)(mWindowY + mLiveTranslationY - height * offsetScale);
343            x = Math.max(0, Math.min(x, mDefaultDisplayMetrics.widthPixels - width));
344            y = Math.max(0, Math.min(y, mDefaultDisplayMetrics.heightPixels - height));
345
346            if (DEBUG) {
347                Log.d(TAG, "updateWindowParams: scale=" + scale
348                        + ", offsetScale=" + offsetScale
349                        + ", x=" + x + ", y=" + y
350                        + ", width=" + width + ", height=" + height);
351            }
352
353            mTextureView.setScaleX(scale);
354            mTextureView.setScaleY(scale);
355
356            mTextureView.setTranslationX(
357                    (mWidth - mTextureView.getLayoutParams().width) * scale / 2);
358            mTextureView.setTranslationY(
359                    (mHeight - mTextureView.getLayoutParams().height) * scale / 2);
360
361            mWindowParams.x = x;
362            mWindowParams.y = y;
363            mWindowParams.width = width;
364            mWindowParams.height = height;
365        }
366
367        private void saveWindowParams() {
368            mWindowX = mWindowParams.x;
369            mWindowY = mWindowParams.y;
370            mWindowScale = mTextureView.getScaleX();
371            clearLiveState();
372        }
373
374        private void clearLiveState() {
375            mLiveTranslationX = 0f;
376            mLiveTranslationY = 0f;
377            mLiveScale = 1.0f;
378        }
379
380        private final DisplayManager.DisplayListener mDisplayListener =
381                new DisplayManager.DisplayListener() {
382            @Override
383            public void onDisplayAdded(int displayId) {
384            }
385
386            @Override
387            public void onDisplayChanged(int displayId) {
388                if (displayId == mDefaultDisplay.getDisplayId()) {
389                    if (updateDefaultDisplayInfo()) {
390                        relayout();
391                    } else {
392                        dismiss();
393                    }
394                }
395            }
396
397            @Override
398            public void onDisplayRemoved(int displayId) {
399                if (displayId == mDefaultDisplay.getDisplayId()) {
400                    dismiss();
401                }
402            }
403        };
404
405        private final SurfaceTextureListener mSurfaceTextureListener =
406                new SurfaceTextureListener() {
407            @Override
408            public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture,
409                    int width, int height) {
410                if (mListener != null) {
411                    mListener.onWindowCreated(new Surface(surfaceTexture));
412                }
413            }
414
415            @Override
416            public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
417                if (mListener != null) {
418                    mListener.onWindowDestroyed();
419                }
420                return true;
421            }
422
423            @Override
424            public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture,
425                    int width, int height) {
426            }
427
428            @Override
429            public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
430            }
431        };
432
433        private final View.OnTouchListener mOnTouchListener = new View.OnTouchListener() {
434            @Override
435            public boolean onTouch(View view, MotionEvent event) {
436                // Work in screen coordinates.
437                final float oldX = event.getX();
438                final float oldY = event.getY();
439                event.setLocation(event.getRawX(), event.getRawY());
440
441                mGestureDetector.onTouchEvent(event);
442                mScaleGestureDetector.onTouchEvent(event);
443
444                switch (event.getActionMasked()) {
445                    case MotionEvent.ACTION_UP:
446                    case MotionEvent.ACTION_CANCEL:
447                        saveWindowParams();
448                        break;
449                }
450
451                // Revert to window coordinates.
452                event.setLocation(oldX, oldY);
453                return true;
454            }
455        };
456
457        private final GestureDetector.OnGestureListener mOnGestureListener =
458                new GestureDetector.SimpleOnGestureListener() {
459            @Override
460            public boolean onScroll(MotionEvent e1, MotionEvent e2,
461                    float distanceX, float distanceY) {
462                mLiveTranslationX -= distanceX;
463                mLiveTranslationY -= distanceY;
464                relayout();
465                return true;
466            }
467        };
468
469        private final ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener =
470                new ScaleGestureDetector.SimpleOnScaleGestureListener() {
471            @Override
472            public boolean onScale(ScaleGestureDetector detector) {
473                mLiveScale *= detector.getScaleFactor();
474                relayout();
475                return true;
476            }
477        };
478    }
479}
480