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