OverlayDisplayAdapter.java revision bd6e1500aedc5461e832f69e76341bff0e55fa2b
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 android.content.Context;
20import android.database.ContentObserver;
21import android.graphics.SurfaceTexture;
22import android.hardware.display.DisplayManager;
23import android.os.IBinder;
24import android.provider.Settings;
25import android.util.DisplayMetrics;
26import android.util.Slog;
27import android.view.Display;
28import android.view.DisplayInfo;
29import android.view.Gravity;
30import android.view.LayoutInflater;
31import android.view.MotionEvent;
32import android.view.ScaleGestureDetector;
33import android.view.Surface;
34import android.view.TextureView;
35import android.view.TextureView.SurfaceTextureListener;
36import android.view.View;
37import android.view.WindowManager;
38import android.widget.TextView;
39
40import java.io.PrintWriter;
41import java.util.ArrayList;
42import java.util.regex.Matcher;
43import java.util.regex.Pattern;
44
45/**
46 * A display adapter that uses overlay windows to simulate secondary displays
47 * for development purposes.  Use Development Settings to enable one or more
48 * overlay displays.
49 * <p>
50 * Display adapters are not thread-safe and must only be accessed
51 * on the display manager service's handler thread.
52 * </p>
53 */
54public final class OverlayDisplayAdapter extends DisplayAdapter {
55    private static final String TAG = "OverlayDisplayAdapter";
56
57    private static final int MIN_WIDTH = 100;
58    private static final int MIN_HEIGHT = 100;
59    private static final int MAX_WIDTH = 4096;
60    private static final int MAX_HEIGHT = 4096;
61
62    private static final Pattern SETTING_PATTERN =
63            Pattern.compile("(\\d+)x(\\d+)/(\\d+)");
64
65    private final ArrayList<Overlay> mOverlays = new ArrayList<Overlay>();
66    private String mCurrentOverlaySetting = "";
67
68    public OverlayDisplayAdapter(Context context) {
69        super(context, TAG);
70    }
71
72    @Override
73    public void dump(PrintWriter pw) {
74        pw.println("mCurrentOverlaySetting=" + mCurrentOverlaySetting);
75        pw.println("mOverlays: size=" + mOverlays.size());
76        for (Overlay overlay : mOverlays) {
77            overlay.dump(pw);
78        }
79    }
80
81    @Override
82    protected void onRegister() {
83        getContext().getContentResolver().registerContentObserver(
84                Settings.System.getUriFor(Settings.Secure.OVERLAY_DISPLAY_DEVICES), true,
85                new ContentObserver(getHandler()) {
86                    @Override
87                    public void onChange(boolean selfChange) {
88                        updateOverlayDisplayDevices();
89                    }
90                });
91        updateOverlayDisplayDevices();
92    }
93
94    private void updateOverlayDisplayDevices() {
95        String value = Settings.System.getString(getContext().getContentResolver(),
96                Settings.Secure.OVERLAY_DISPLAY_DEVICES);
97        if (value == null) {
98            value = "";
99        }
100
101        if (value.equals(mCurrentOverlaySetting)) {
102            return;
103        }
104        mCurrentOverlaySetting = value;
105
106        if (!mOverlays.isEmpty()) {
107            Slog.i(TAG, "Dismissing all overlay display devices.");
108            for (Overlay overlay : mOverlays) {
109                overlay.dismiss();
110            }
111            mOverlays.clear();
112        }
113
114        int number = 1;
115        for (String part : value.split(";")) {
116            if (number > 4) {
117                Slog.w(TAG, "Too many overlay display devices.");
118            }
119            Matcher matcher = SETTING_PATTERN.matcher(part);
120            if (matcher.matches()) {
121                try {
122                    int width = Integer.parseInt(matcher.group(1), 10);
123                    int height = Integer.parseInt(matcher.group(2), 10);
124                    int densityDpi = Integer.parseInt(matcher.group(3), 10);
125                    if (width >= MIN_WIDTH && width <= MAX_WIDTH
126                            && height >= MIN_HEIGHT && height <= MAX_HEIGHT
127                            && densityDpi >= DisplayMetrics.DENSITY_LOW
128                            && densityDpi <= DisplayMetrics.DENSITY_XXHIGH) {
129                        Slog.i(TAG, "Showing overlay display device #" + number
130                                + ": width=" + width + ", height=" + height
131                                + ", densityDpi=" + densityDpi);
132                        mOverlays.add(new Overlay(number++, width, height, densityDpi));
133                        continue;
134                    }
135                } catch (NumberFormatException ex) {
136                }
137            } else if (part.isEmpty()) {
138                continue;
139            }
140            Slog.w(TAG, "Malformed overlay display devices setting: \"" + value + "\"");
141        }
142
143        for (Overlay overlay : mOverlays) {
144            overlay.show();
145        }
146    }
147
148    // Manages an overlay window.
149    private final class Overlay {
150        private final float INITIAL_SCALE = 0.5f;
151        private final float MIN_SCALE = 0.3f;
152        private final float MAX_SCALE = 1.0f;
153        private final float WINDOW_ALPHA = 0.8f;
154
155        // When true, disables support for moving and resizing the overlay.
156        // The window is made non-touchable, which makes it possible to
157        // directly interact with the content underneath.
158        private final boolean DISABLE_MOVE_AND_RESIZE = false;
159
160        private final DisplayManager mDisplayManager;
161        private final WindowManager mWindowManager;
162
163        private final int mNumber;
164        private final int mWidth;
165        private final int mHeight;
166        private final int mDensityDpi;
167
168        private final String mName;
169        private final String mTitle;
170
171        private final Display mDefaultDisplay;
172        private final DisplayInfo mDefaultDisplayInfo = new DisplayInfo();
173        private final IBinder mDisplayToken;
174        private final OverlayDisplayDevice mDisplayDevice;
175
176        private View mWindowContent;
177        private WindowManager.LayoutParams mWindowParams;
178        private TextureView mTextureView;
179        private TextView mTitleTextView;
180        private ScaleGestureDetector mScaleGestureDetector;
181
182        private boolean mWindowVisible;
183        private int mWindowX;
184        private int mWindowY;
185        private float mWindowScale;
186
187        private int mLiveTranslationX;
188        private int mLiveTranslationY;
189        private float mLiveScale = 1.0f;
190
191        private int mDragPointerId;
192        private float mDragTouchX;
193        private float mDragTouchY;
194
195        public Overlay(int number, int width, int height, int densityDpi) {
196            Context context = getContext();
197            mDisplayManager = (DisplayManager)context.getSystemService(
198                    Context.DISPLAY_SERVICE);
199            mWindowManager = (WindowManager)context.getSystemService(
200                    Context.WINDOW_SERVICE);
201
202            mNumber = number;
203            mWidth = width;
204            mHeight = height;
205            mDensityDpi = densityDpi;
206
207            mName = context.getResources().getString(
208                    com.android.internal.R.string.display_manager_overlay_display_name, number);
209            mTitle = context.getResources().getString(
210                    com.android.internal.R.string.display_manager_overlay_display_title,
211                    mNumber, mWidth, mHeight, mDensityDpi);
212
213            mDefaultDisplay = mWindowManager.getDefaultDisplay();
214            updateDefaultDisplayInfo();
215
216            mDisplayToken = Surface.createDisplay(mName);
217            mDisplayDevice = new OverlayDisplayDevice(mDisplayToken, mName,
218                    mDefaultDisplayInfo.refreshRate, mDensityDpi);
219
220            createWindow();
221        }
222
223        public void show() {
224            if (!mWindowVisible) {
225                mDisplayManager.registerDisplayListener(mDisplayListener, null);
226                if (!updateDefaultDisplayInfo()) {
227                    mDisplayManager.unregisterDisplayListener(mDisplayListener);
228                    return;
229                }
230
231                clearLiveState();
232                updateWindowParams();
233                mWindowManager.addView(mWindowContent, mWindowParams);
234                mWindowVisible = true;
235            }
236        }
237
238        public void dismiss() {
239            if (mWindowVisible) {
240                mDisplayManager.unregisterDisplayListener(mDisplayListener);
241                mWindowManager.removeView(mWindowContent);
242                mWindowVisible = false;
243            }
244        }
245
246        public void relayout() {
247            if (mWindowVisible) {
248                updateWindowParams();
249                mWindowManager.updateViewLayout(mWindowContent, mWindowParams);
250            }
251        }
252
253        public void dump(PrintWriter pw) {
254            pw.println("  #" + mNumber + ": "
255                    + mWidth + "x" + mHeight + ", " + mDensityDpi + " dpi");
256            pw.println("    mName=" + mName);
257            pw.println("    mWindowVisible=" + mWindowVisible);
258            pw.println("    mWindowX=" + mWindowX);
259            pw.println("    mWindowY=" + mWindowY);
260            pw.println("    mWindowScale=" + mWindowScale);
261            pw.println("    mWindowParams=" + mWindowParams);
262            pw.println("    mLiveTranslationX=" + mLiveTranslationX);
263            pw.println("    mLiveTranslationY=" + mLiveTranslationY);
264            pw.println("    mLiveScale=" + mLiveScale);
265        }
266
267        private boolean updateDefaultDisplayInfo() {
268            if (!mDefaultDisplay.getDisplayInfo(mDefaultDisplayInfo)) {
269                Slog.w(TAG, "Cannot show overlay display because there is no "
270                        + "default display upon which to show it.");
271                return false;
272            }
273            return true;
274        }
275
276        private void createWindow() {
277            Context context = getContext();
278            LayoutInflater inflater = LayoutInflater.from(context);
279
280            mWindowContent = inflater.inflate(
281                    com.android.internal.R.layout.overlay_display_window, null);
282            mWindowContent.setOnTouchListener(mOnTouchListener);
283
284            mTextureView = (TextureView)mWindowContent.findViewById(
285                    com.android.internal.R.id.overlay_display_window_texture);
286            mTextureView.setPivotX(0);
287            mTextureView.setPivotY(0);
288            mTextureView.getLayoutParams().width = mWidth;
289            mTextureView.getLayoutParams().height = mHeight;
290            mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
291
292            mTitleTextView = (TextView)mWindowContent.findViewById(
293                    com.android.internal.R.id.overlay_display_window_title);
294            mTitleTextView.setText(mTitle);
295
296            mWindowParams = new WindowManager.LayoutParams(
297                    WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY);
298            mWindowParams.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
299                    | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
300                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
301                    | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
302                    | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
303            if (DISABLE_MOVE_AND_RESIZE) {
304                mWindowParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
305            }
306            mWindowParams.privateFlags |=
307                    WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED;
308            mWindowParams.alpha = WINDOW_ALPHA;
309            mWindowParams.gravity = Gravity.TOP | Gravity.LEFT;
310            mWindowParams.setTitle(mTitle);
311
312            mScaleGestureDetector = new ScaleGestureDetector(context, mOnScaleGestureListener);
313
314            // By default, arrange the displays in the four corners.
315            mWindowVisible = false;
316            mWindowScale = INITIAL_SCALE;
317            if (mNumber == 2 || mNumber == 3) {
318                mWindowX = mDefaultDisplayInfo.logicalWidth;
319            } else {
320                mWindowX = 0;
321            }
322            if (mNumber == 2 || mNumber == 4) {
323                mWindowY = mDefaultDisplayInfo.logicalHeight;
324            } else {
325                mWindowY = 0;
326            }
327        }
328
329        private void updateWindowParams() {
330            float scale = mWindowScale * mLiveScale;
331            if (mWidth * scale > mDefaultDisplayInfo.logicalWidth) {
332                scale = mDefaultDisplayInfo.logicalWidth / mWidth;
333            }
334            if (mHeight * scale > mDefaultDisplayInfo.logicalHeight) {
335                scale = mDefaultDisplayInfo.logicalHeight / mHeight;
336            }
337            scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale));
338
339            float offsetScale = (scale / mWindowScale - 1.0f) * 0.5f;
340            int width = (int)(mWidth * scale);
341            int height = (int)(mHeight * scale);
342            int x = mWindowX + mLiveTranslationX - (int)(width * offsetScale);
343            int y = mWindowY + mLiveTranslationY - (int)(height * offsetScale);
344            x = Math.max(0, Math.min(x, mDefaultDisplayInfo.logicalWidth - width));
345            y = Math.max(0, Math.min(y, mDefaultDisplayInfo.logicalHeight - height));
346
347            mTextureView.setScaleX(scale);
348            mTextureView.setScaleY(scale);
349
350            mWindowParams.x = x;
351            mWindowParams.y = y;
352            mWindowParams.width = width;
353            mWindowParams.height = height;
354        }
355
356        private void saveWindowParams() {
357            mWindowX = mWindowParams.x;
358            mWindowY = mWindowParams.y;
359            mWindowScale = mTextureView.getScaleX();
360            clearLiveState();
361        }
362
363        private void clearLiveState() {
364            mLiveTranslationX = 0;
365            mLiveTranslationY = 0;
366            mLiveScale = 1.0f;
367        }
368
369        private final DisplayManager.DisplayListener mDisplayListener =
370                new DisplayManager.DisplayListener() {
371            @Override
372            public void onDisplayAdded(int displayId) {
373            }
374
375            @Override
376            public void onDisplayChanged(int displayId) {
377                if (displayId == mDefaultDisplay.getDisplayId()) {
378                    if (updateDefaultDisplayInfo()) {
379                        relayout();
380                    } else {
381                        dismiss();
382                    }
383                }
384            }
385
386            @Override
387            public void onDisplayRemoved(int displayId) {
388                if (displayId == mDefaultDisplay.getDisplayId()) {
389                    dismiss();
390                }
391            }
392        };
393
394        private final SurfaceTextureListener mSurfaceTextureListener =
395                new SurfaceTextureListener() {
396            @Override
397            public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
398                Surface.openTransaction();
399                try {
400                    Surface.setDisplaySurface(mDisplayToken, surface);
401                } finally {
402                    Surface.closeTransaction();
403                }
404
405                mDisplayDevice.setSize(width, height);
406                sendDisplayDeviceEvent(mDisplayDevice, DISPLAY_DEVICE_EVENT_ADDED);
407            }
408
409            @Override
410            public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
411                sendDisplayDeviceEvent(mDisplayDevice, DISPLAY_DEVICE_EVENT_REMOVED);
412
413                Surface.openTransaction();
414                try {
415                    Surface.setDisplaySurface(mDisplayToken, null);
416                } finally {
417                    Surface.closeTransaction();
418                }
419                return true;
420            }
421
422            @Override
423            public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
424                mDisplayDevice.setSize(width, height);
425                sendDisplayDeviceEvent(mDisplayDevice, DISPLAY_DEVICE_EVENT_CHANGED);
426            }
427
428            @Override
429            public void onSurfaceTextureUpdated(SurfaceTexture surface) {
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                mScaleGestureDetector.onTouchEvent(event);
442
443                switch (event.getActionMasked()) {
444                    case MotionEvent.ACTION_DOWN:
445                        resetDrag(event);
446                        break;
447
448                    case MotionEvent.ACTION_MOVE:
449                        if (event.getPointerCount() == 1) {
450                            int index = event.findPointerIndex(mDragPointerId);
451                            if (index < 0) {
452                                resetDrag(event);
453                            } else {
454                                mLiveTranslationX = (int)(event.getX(index) - mDragTouchX);
455                                mLiveTranslationY = (int)(event.getY(index) - mDragTouchY);
456                                relayout();
457                            }
458                        }
459                        break;
460
461                    case MotionEvent.ACTION_UP:
462                    case MotionEvent.ACTION_CANCEL:
463                        saveWindowParams();
464                        break;
465                }
466
467                // Revert to window coordinates.
468                event.setLocation(oldX, oldY);
469                return true;
470            }
471
472            private void resetDrag(MotionEvent event) {
473                saveWindowParams();
474                mDragPointerId = event.getPointerId(0);
475                mDragTouchX = event.getX();
476                mDragTouchY = event.getY();
477            }
478        };
479
480        private final ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener =
481                new ScaleGestureDetector.SimpleOnScaleGestureListener() {
482            @Override
483            public boolean onScaleBegin(ScaleGestureDetector detector) {
484                saveWindowParams();
485                mDragPointerId = -1; // cause drag to be reset
486                return true;
487            }
488
489            @Override
490            public boolean onScale(ScaleGestureDetector detector) {
491                mLiveScale = detector.getScaleFactor();
492                relayout();
493                return false;
494            }
495        };
496    }
497
498    private final class OverlayDisplayDevice extends DisplayDevice {
499        private final String mName;
500        private final float mRefreshRate;
501        private final int mDensityDpi;
502        private int mWidth;
503        private int mHeight;
504
505        public OverlayDisplayDevice(IBinder displayToken, String name,
506                float refreshRate, int densityDpi) {
507            super(OverlayDisplayAdapter.this, displayToken);
508            mName = name;
509            mRefreshRate = refreshRate;
510            mDensityDpi = densityDpi;
511        }
512
513        public void setSize(int width, int height) {
514            mWidth = width;
515            mHeight = height;
516        }
517
518        @Override
519        public void getInfo(DisplayDeviceInfo outInfo) {
520            outInfo.name = mName;
521            outInfo.width = mWidth;
522            outInfo.height = mHeight;
523            outInfo.refreshRate = mRefreshRate;
524            outInfo.densityDpi = mDensityDpi;
525            outInfo.xDpi = mDensityDpi;
526            outInfo.yDpi = mDensityDpi;
527            outInfo.flags = DisplayDeviceInfo.FLAG_SECURE;
528        }
529    }
530}
531