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 android.app;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.hardware.display.DisplayManager;
22import android.hardware.display.DisplayManager.DisplayListener;
23import android.view.ContextThemeWrapper;
24import android.view.Display;
25import android.view.Gravity;
26import android.view.WindowManagerImpl;
27import android.os.Handler;
28import android.os.Message;
29import android.util.DisplayMetrics;
30import android.util.Log;
31import android.util.TypedValue;
32
33/**
34 * Base class for presentations.
35 * <p>
36 * A presentation is a special kind of dialog whose purpose is to present
37 * content on a secondary display.  A {@link Presentation} is associated with
38 * the target {@link Display} at creation time and configures its context and
39 * resource configuration according to the display's metrics.
40 * </p><p>
41 * Notably, the {@link Context} of a presentation is different from the context
42 * of its containing {@link Activity}.  It is important to inflate the layout
43 * of a presentation and load other resources using the presentation's own context
44 * to ensure that assets of the correct size and density for the target display
45 * are loaded.
46 * </p><p>
47 * A presentation is automatically canceled (see {@link Dialog#cancel()}) when
48 * the display to which it is attached is removed.  An activity should take
49 * care of pausing and resuming whatever content is playing within the presentation
50 * whenever the activity itself is paused or resumed.
51 * </p>
52 *
53 * <h3>Choosing a presentation display</h3>
54 * <p>
55 * Before showing a {@link Presentation} it's important to choose the {@link Display}
56 * on which it will appear.  Choosing a presentation display is sometimes difficult
57 * because there may be multiple displays attached.  Rather than trying to guess
58 * which display is best, an application should let the system choose a suitable
59 * presentation display.
60 * </p><p>
61 * There are two main ways to choose a {@link Display}.
62 * </p>
63 *
64 * <h4>Using the media router to choose a presentation display</h4>
65 * <p>
66 * The easiest way to choose a presentation display is to use the
67 * {@link android.media.MediaRouter MediaRouter} API.  The media router service keeps
68 * track of which audio and video routes are available on the system.
69 * The media router sends notifications whenever routes are selected or unselected
70 * or when the preferred presentation display of a route changes.
71 * So an application can simply watch for these notifications and show or dismiss
72 * a presentation on the preferred presentation display automatically.
73 * </p><p>
74 * The preferred presentation display is the display that the media router recommends
75 * that the application should use if it wants to show content on the secondary display.
76 * Sometimes there may not be a preferred presentation display in which
77 * case the application should show its content locally without using a presentation.
78 * </p><p>
79 * Here's how to use the media router to create and show a presentation on the preferred
80 * presentation display using {@link android.media.MediaRouter.RouteInfo#getPresentationDisplay()}.
81 * </p>
82 * <pre>
83 * MediaRouter mediaRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
84 * MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute();
85 * if (route != null) {
86 *     Display presentationDisplay = route.getPresentationDisplay();
87 *     if (presentationDisplay != null) {
88 *         Presentation presentation = new MyPresentation(context, presentationDisplay);
89 *         presentation.show();
90 *     }
91 * }</pre>
92 * <p>
93 * The following sample code from <code>ApiDemos</code> demonstrates how to use the media
94 * router to automatically switch between showing content in the main activity and showing
95 * the content in a presentation when a presentation display is available.
96 * </p>
97 * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/PresentationWithMediaRouterActivity.java
98 *      activity}
99 *
100 * <h4>Using the display manager to choose a presentation display</h4>
101 * <p>
102 * Another way to choose a presentation display is to use the {@link DisplayManager} API
103 * directly.  The display manager service provides functions to enumerate and describe all
104 * displays that are attached to the system including displays that may be used
105 * for presentations.
106 * </p><p>
107 * The display manager keeps track of all displays in the system.  However, not all
108 * displays are appropriate for showing presentations.  For example, if an activity
109 * attempted to show a presentation on the main display it might obscure its own content
110 * (it's like opening a dialog on top of your activity).
111 * </p><p>
112 * Here's how to identify suitable displays for showing presentations using
113 * {@link DisplayManager#getDisplays(String)} and the
114 * {@link DisplayManager#DISPLAY_CATEGORY_PRESENTATION} category.
115 * </p>
116 * <pre>
117 * DisplayManager displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
118 * Display[] presentationDisplays = displayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
119 * if (presentationDisplays.length > 0) {
120 *     // If there is more than one suitable presentation display, then we could consider
121 *     // giving the user a choice.  For this example, we simply choose the first display
122 *     // which is the one the system recommends as the preferred presentation display.
123 *     Display display = presentationDisplays[0];
124 *     Presentation presentation = new MyPresentation(context, presentationDisplay);
125 *     presentation.show();
126 * }</pre>
127 * <p>
128 * The following sample code from <code>ApiDemos</code> demonstrates how to use the display
129 * manager to enumerate displays and show content on multiple presentation displays
130 * simultaneously.
131 * </p>
132 * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/PresentationActivity.java
133 *      activity}
134 *
135 * @see android.media.MediaRouter#ROUTE_TYPE_LIVE_VIDEO for information on about live
136 * video routes and how to obtain the preferred presentation display for the
137 * current media route.
138 * @see DisplayManager for information on how to enumerate displays and receive
139 * notifications when displays are added or removed.
140 */
141public class Presentation extends Dialog {
142    private static final String TAG = "Presentation";
143
144    private static final int MSG_CANCEL = 1;
145
146    private final Display mDisplay;
147    private final DisplayManager mDisplayManager;
148
149    /**
150     * Creates a new presentation that is attached to the specified display
151     * using the default theme.
152     *
153     * @param outerContext The context of the application that is showing the presentation.
154     * The presentation will create its own context (see {@link #getContext()}) based
155     * on this context and information about the associated display.
156     * @param display The display to which the presentation should be attached.
157     */
158    public Presentation(Context outerContext, Display display) {
159        this(outerContext, display, 0);
160    }
161
162    /**
163     * Creates a new presentation that is attached to the specified display
164     * using the optionally specified theme.
165     *
166     * @param outerContext The context of the application that is showing the presentation.
167     * The presentation will create its own context (see {@link #getContext()}) based
168     * on this context and information about the associated display.
169     * @param display The display to which the presentation should be attached.
170     * @param theme A style resource describing the theme to use for the window.
171     * See <a href="{@docRoot}guide/topics/resources/available-resources.html#stylesandthemes">
172     * Style and Theme Resources</a> for more information about defining and using
173     * styles.  This theme is applied on top of the current theme in
174     * <var>outerContext</var>.  If 0, the default presentation theme will be used.
175     */
176    public Presentation(Context outerContext, Display display, int theme) {
177        super(createPresentationContext(outerContext, display, theme), theme, false);
178
179        mDisplay = display;
180        mDisplayManager = (DisplayManager)getContext().getSystemService(Context.DISPLAY_SERVICE);
181
182        getWindow().setGravity(Gravity.FILL);
183        setCanceledOnTouchOutside(false);
184    }
185
186    /**
187     * Gets the {@link Display} that this presentation appears on.
188     *
189     * @return The display.
190     */
191    public Display getDisplay() {
192        return mDisplay;
193    }
194
195    /**
196     * Gets the {@link Resources} that should be used to inflate the layout of this presentation.
197     * This resources object has been configured according to the metrics of the
198     * display that the presentation appears on.
199     *
200     * @return The presentation resources object.
201     */
202    public Resources getResources() {
203        return getContext().getResources();
204    }
205
206    @Override
207    protected void onStart() {
208        super.onStart();
209        mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
210
211        // Since we were not watching for display changes until just now, there is a
212        // chance that the display metrics have changed.  If so, we will need to
213        // dismiss the presentation immediately.  This case is expected
214        // to be rare but surprising, so we'll write a log message about it.
215        if (!isConfigurationStillValid()) {
216            Log.i(TAG, "Presentation is being dismissed because the "
217                    + "display metrics have changed since it was created.");
218            mHandler.sendEmptyMessage(MSG_CANCEL);
219        }
220    }
221
222    @Override
223    protected void onStop() {
224        mDisplayManager.unregisterDisplayListener(mDisplayListener);
225        super.onStop();
226    }
227
228    /**
229     * Inherited from {@link Dialog#show}. Will throw
230     * {@link android.view.WindowManager.InvalidDisplayException} if the specified secondary
231     * {@link Display} can't be found.
232     */
233    @Override
234    public void show() {
235        super.show();
236    }
237
238    /**
239     * Called by the system when the {@link Display} to which the presentation
240     * is attached has been removed.
241     *
242     * The system automatically calls {@link #cancel} to dismiss the presentation
243     * after sending this event.
244     *
245     * @see #getDisplay
246     */
247    public void onDisplayRemoved() {
248    }
249
250    /**
251     * Called by the system when the properties of the {@link Display} to which
252     * the presentation is attached have changed.
253     *
254     * If the display metrics have changed (for example, if the display has been
255     * resized or rotated), then the system automatically calls
256     * {@link #cancel} to dismiss the presentation.
257     *
258     * @see #getDisplay
259     */
260    public void onDisplayChanged() {
261    }
262
263    private void handleDisplayRemoved() {
264        onDisplayRemoved();
265        cancel();
266    }
267
268    private void handleDisplayChanged() {
269        onDisplayChanged();
270
271        // We currently do not support configuration changes for presentations
272        // (although we could add that feature with a bit more work).
273        // If the display metrics have changed in any way then the current configuration
274        // is invalid and the application must recreate the presentation to get
275        // a new context.
276        if (!isConfigurationStillValid()) {
277            Log.i(TAG, "Presentation is being dismissed because the "
278                    + "display metrics have changed since it was created.");
279            cancel();
280        }
281    }
282
283    private boolean isConfigurationStillValid() {
284        DisplayMetrics dm = new DisplayMetrics();
285        mDisplay.getMetrics(dm);
286        return dm.equalsPhysical(getResources().getDisplayMetrics());
287    }
288
289    private static Context createPresentationContext(
290            Context outerContext, Display display, int theme) {
291        if (outerContext == null) {
292            throw new IllegalArgumentException("outerContext must not be null");
293        }
294        if (display == null) {
295            throw new IllegalArgumentException("display must not be null");
296        }
297
298        Context displayContext = outerContext.createDisplayContext(display);
299        if (theme == 0) {
300            TypedValue outValue = new TypedValue();
301            displayContext.getTheme().resolveAttribute(
302                    com.android.internal.R.attr.presentationTheme, outValue, true);
303            theme = outValue.resourceId;
304        }
305
306        // Derive the display's window manager from the outer window manager.
307        // We do this because the outer window manager have some extra information
308        // such as the parent window, which is important if the presentation uses
309        // an application window type.
310        final WindowManagerImpl outerWindowManager =
311                (WindowManagerImpl)outerContext.getSystemService(Context.WINDOW_SERVICE);
312        final WindowManagerImpl displayWindowManager =
313                outerWindowManager.createPresentationWindowManager(display);
314        return new ContextThemeWrapper(displayContext, theme) {
315            @Override
316            public Object getSystemService(String name) {
317                if (Context.WINDOW_SERVICE.equals(name)) {
318                    return displayWindowManager;
319                }
320                return super.getSystemService(name);
321            }
322        };
323    }
324
325    private final DisplayListener mDisplayListener = new DisplayListener() {
326        @Override
327        public void onDisplayAdded(int displayId) {
328        }
329
330        @Override
331        public void onDisplayRemoved(int displayId) {
332            if (displayId == mDisplay.getDisplayId()) {
333                handleDisplayRemoved();
334            }
335        }
336
337        @Override
338        public void onDisplayChanged(int displayId) {
339            if (displayId == mDisplay.getDisplayId()) {
340                handleDisplayChanged();
341            }
342        }
343    };
344
345    private final Handler mHandler = new Handler() {
346        @Override
347        public void handleMessage(Message msg) {
348            switch (msg.what) {
349                case MSG_CANCEL:
350                    cancel();
351                    break;
352            }
353        }
354    };
355}
356