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