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.android.server.display;
18
19import com.android.internal.util.DumpUtils;
20
21import android.content.Context;
22import android.graphics.SurfaceTexture;
23import android.hardware.display.DisplayManager;
24import android.util.Slog;
25import android.view.Display;
26import android.view.DisplayInfo;
27import android.view.GestureDetector;
28import android.view.Gravity;
29import android.view.LayoutInflater;
30import android.view.MotionEvent;
31import android.view.ScaleGestureDetector;
32import android.view.TextureView;
33import android.view.View;
34import android.view.WindowManager;
35import android.view.TextureView.SurfaceTextureListener;
36import android.widget.TextView;
37
38import java.io.PrintWriter;
39
40/**
41 * Manages an overlay window on behalf of {@link OverlayDisplayAdapter}.
42 * <p>
43 * This object must only be accessed on the UI thread.
44 * No locks are held by this object and locks must not be held while making called into it.
45 * </p>
46 */
47final class OverlayDisplayWindow implements DumpUtils.Dump {
48    private static final String TAG = "OverlayDisplayWindow";
49    private static final boolean DEBUG = false;
50
51    private final float INITIAL_SCALE = 0.5f;
52    private final float MIN_SCALE = 0.3f;
53    private final float MAX_SCALE = 1.0f;
54    private final float WINDOW_ALPHA = 0.8f;
55
56    // When true, disables support for moving and resizing the overlay.
57    // The window is made non-touchable, which makes it possible to
58    // directly interact with the content underneath.
59    private final boolean DISABLE_MOVE_AND_RESIZE = false;
60
61    private final Context mContext;
62    private final String mName;
63    private final int mWidth;
64    private final int mHeight;
65    private final int mDensityDpi;
66    private final int mGravity;
67    private final Listener mListener;
68    private final String mTitle;
69
70    private final DisplayManager mDisplayManager;
71    private final WindowManager mWindowManager;
72
73
74    private final Display mDefaultDisplay;
75    private final DisplayInfo mDefaultDisplayInfo = new DisplayInfo();
76
77    private View mWindowContent;
78    private WindowManager.LayoutParams mWindowParams;
79    private TextureView mTextureView;
80    private TextView mTitleTextView;
81
82    private GestureDetector mGestureDetector;
83    private ScaleGestureDetector mScaleGestureDetector;
84
85    private boolean mWindowVisible;
86    private int mWindowX;
87    private int mWindowY;
88    private float mWindowScale;
89
90    private float mLiveTranslationX;
91    private float mLiveTranslationY;
92    private float mLiveScale = 1.0f;
93
94    public OverlayDisplayWindow(Context context, String name,
95            int width, int height, int densityDpi, int gravity, Listener listener) {
96        mContext = context;
97        mName = name;
98        mWidth = width;
99        mHeight = height;
100        mDensityDpi = densityDpi;
101        mGravity = gravity;
102        mListener = listener;
103        mTitle = context.getResources().getString(
104                com.android.internal.R.string.display_manager_overlay_display_title,
105                mName, mWidth, mHeight, mDensityDpi);
106
107        mDisplayManager = (DisplayManager)context.getSystemService(
108                Context.DISPLAY_SERVICE);
109        mWindowManager = (WindowManager)context.getSystemService(
110                Context.WINDOW_SERVICE);
111
112        mDefaultDisplay = mWindowManager.getDefaultDisplay();
113        updateDefaultDisplayInfo();
114
115        createWindow();
116    }
117
118    public void show() {
119        if (!mWindowVisible) {
120            mDisplayManager.registerDisplayListener(mDisplayListener, null);
121            if (!updateDefaultDisplayInfo()) {
122                mDisplayManager.unregisterDisplayListener(mDisplayListener);
123                return;
124            }
125
126            clearLiveState();
127            updateWindowParams();
128            mWindowManager.addView(mWindowContent, mWindowParams);
129            mWindowVisible = true;
130        }
131    }
132
133    public void dismiss() {
134        if (mWindowVisible) {
135            mDisplayManager.unregisterDisplayListener(mDisplayListener);
136            mWindowManager.removeView(mWindowContent);
137            mWindowVisible = false;
138        }
139    }
140
141    public void relayout() {
142        if (mWindowVisible) {
143            updateWindowParams();
144            mWindowManager.updateViewLayout(mWindowContent, mWindowParams);
145        }
146    }
147
148    @Override
149    public void dump(PrintWriter pw) {
150        pw.println("mWindowVisible=" + mWindowVisible);
151        pw.println("mWindowX=" + mWindowX);
152        pw.println("mWindowY=" + mWindowY);
153        pw.println("mWindowScale=" + mWindowScale);
154        pw.println("mWindowParams=" + mWindowParams);
155        if (mTextureView != null) {
156            pw.println("mTextureView.getScaleX()=" + mTextureView.getScaleX());
157            pw.println("mTextureView.getScaleY()=" + mTextureView.getScaleY());
158        }
159        pw.println("mLiveTranslationX=" + mLiveTranslationX);
160        pw.println("mLiveTranslationY=" + mLiveTranslationY);
161        pw.println("mLiveScale=" + mLiveScale);
162    }
163
164    private boolean updateDefaultDisplayInfo() {
165        if (!mDefaultDisplay.getDisplayInfo(mDefaultDisplayInfo)) {
166            Slog.w(TAG, "Cannot show overlay display because there is no "
167                    + "default display upon which to show it.");
168            return false;
169        }
170        return true;
171    }
172
173    private void createWindow() {
174        LayoutInflater inflater = LayoutInflater.from(mContext);
175
176        mWindowContent = inflater.inflate(
177                com.android.internal.R.layout.overlay_display_window, null);
178        mWindowContent.setOnTouchListener(mOnTouchListener);
179
180        mTextureView = (TextureView)mWindowContent.findViewById(
181                com.android.internal.R.id.overlay_display_window_texture);
182        mTextureView.setPivotX(0);
183        mTextureView.setPivotY(0);
184        mTextureView.getLayoutParams().width = mWidth;
185        mTextureView.getLayoutParams().height = mHeight;
186        mTextureView.setOpaque(false);
187        mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
188
189        mTitleTextView = (TextView)mWindowContent.findViewById(
190                com.android.internal.R.id.overlay_display_window_title);
191        mTitleTextView.setText(mTitle);
192
193        mWindowParams = new WindowManager.LayoutParams(
194                WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY);
195        mWindowParams.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
196                | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
197                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
198                | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
199                | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
200        if (DISABLE_MOVE_AND_RESIZE) {
201            mWindowParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
202        }
203        mWindowParams.privateFlags |=
204                WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED;
205        mWindowParams.alpha = WINDOW_ALPHA;
206        mWindowParams.gravity = Gravity.TOP | Gravity.LEFT;
207        mWindowParams.setTitle(mTitle);
208
209        mGestureDetector = new GestureDetector(mContext, mOnGestureListener);
210        mScaleGestureDetector = new ScaleGestureDetector(mContext, mOnScaleGestureListener);
211
212        // Set the initial position and scale.
213        // The position and scale will be clamped when the display is first shown.
214        mWindowX = (mGravity & Gravity.LEFT) == Gravity.LEFT ?
215                0 : mDefaultDisplayInfo.logicalWidth;
216        mWindowY = (mGravity & Gravity.TOP) == Gravity.TOP ?
217                0 : mDefaultDisplayInfo.logicalHeight;
218        mWindowScale = INITIAL_SCALE;
219    }
220
221    private void updateWindowParams() {
222        float scale = mWindowScale * mLiveScale;
223        scale = Math.min(scale, (float)mDefaultDisplayInfo.logicalWidth / mWidth);
224        scale = Math.min(scale, (float)mDefaultDisplayInfo.logicalHeight / mHeight);
225        scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale));
226
227        float offsetScale = (scale / mWindowScale - 1.0f) * 0.5f;
228        int width = (int)(mWidth * scale);
229        int height = (int)(mHeight * scale);
230        int x = (int)(mWindowX + mLiveTranslationX - width * offsetScale);
231        int y = (int)(mWindowY + mLiveTranslationY - height * offsetScale);
232        x = Math.max(0, Math.min(x, mDefaultDisplayInfo.logicalWidth - width));
233        y = Math.max(0, Math.min(y, mDefaultDisplayInfo.logicalHeight - height));
234
235        if (DEBUG) {
236            Slog.d(TAG, "updateWindowParams: scale=" + scale
237                    + ", offsetScale=" + offsetScale
238                    + ", x=" + x + ", y=" + y
239                    + ", width=" + width + ", height=" + height);
240        }
241
242        mTextureView.setScaleX(scale);
243        mTextureView.setScaleY(scale);
244
245        mWindowParams.x = x;
246        mWindowParams.y = y;
247        mWindowParams.width = width;
248        mWindowParams.height = height;
249    }
250
251    private void saveWindowParams() {
252        mWindowX = mWindowParams.x;
253        mWindowY = mWindowParams.y;
254        mWindowScale = mTextureView.getScaleX();
255        clearLiveState();
256    }
257
258    private void clearLiveState() {
259        mLiveTranslationX = 0f;
260        mLiveTranslationY = 0f;
261        mLiveScale = 1.0f;
262    }
263
264    private final DisplayManager.DisplayListener mDisplayListener =
265            new DisplayManager.DisplayListener() {
266        @Override
267        public void onDisplayAdded(int displayId) {
268        }
269
270        @Override
271        public void onDisplayChanged(int displayId) {
272            if (displayId == mDefaultDisplay.getDisplayId()) {
273                if (updateDefaultDisplayInfo()) {
274                    relayout();
275                } else {
276                    dismiss();
277                }
278            }
279        }
280
281        @Override
282        public void onDisplayRemoved(int displayId) {
283            if (displayId == mDefaultDisplay.getDisplayId()) {
284                dismiss();
285            }
286        }
287    };
288
289    private final SurfaceTextureListener mSurfaceTextureListener =
290            new SurfaceTextureListener() {
291        @Override
292        public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture,
293                int width, int height) {
294            mListener.onWindowCreated(surfaceTexture, mDefaultDisplayInfo.refreshRate);
295        }
296
297        @Override
298        public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
299            mListener.onWindowDestroyed();
300            return true;
301        }
302
303        @Override
304        public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture,
305                int width, int height) {
306        }
307
308        @Override
309        public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
310        }
311    };
312
313    private final View.OnTouchListener mOnTouchListener = new View.OnTouchListener() {
314        @Override
315        public boolean onTouch(View view, MotionEvent event) {
316            // Work in screen coordinates.
317            final float oldX = event.getX();
318            final float oldY = event.getY();
319            event.setLocation(event.getRawX(), event.getRawY());
320
321            mGestureDetector.onTouchEvent(event);
322            mScaleGestureDetector.onTouchEvent(event);
323
324            switch (event.getActionMasked()) {
325                case MotionEvent.ACTION_UP:
326                case MotionEvent.ACTION_CANCEL:
327                    saveWindowParams();
328                    break;
329            }
330
331            // Revert to window coordinates.
332            event.setLocation(oldX, oldY);
333            return true;
334        }
335    };
336
337    private final GestureDetector.OnGestureListener mOnGestureListener =
338            new GestureDetector.SimpleOnGestureListener() {
339        @Override
340        public boolean onScroll(MotionEvent e1, MotionEvent e2,
341                float distanceX, float distanceY) {
342            mLiveTranslationX -= distanceX;
343            mLiveTranslationY -= distanceY;
344            relayout();
345            return true;
346        }
347    };
348
349    private final ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener =
350            new ScaleGestureDetector.SimpleOnScaleGestureListener() {
351        @Override
352        public boolean onScale(ScaleGestureDetector detector) {
353            mLiveScale *= detector.getScaleFactor();
354            relayout();
355            return true;
356        }
357    };
358
359    /**
360     * Watches for significant changes in the overlay display window lifecycle.
361     */
362    public interface Listener {
363        public void onWindowCreated(SurfaceTexture surfaceTexture, float refreshRate);
364        public void onWindowDestroyed();
365    }
366}