1 // Copyright (c) 2013 Intel Corporation. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.xwalk.core.internal.extension.api.presentation;
7 import android.content.Context;
8 import android.os.Build;
9 import android.os.Bundle;
10 import android.util.JsonReader;
11 import android.util.JsonWriter;
12 import android.util.Log;
13 import android.view.Display;
14 import android.view.ViewGroup;
16 import java.io.IOException;
17 import java.io.StringReader;
18 import java.io.StringWriter;
20 import java.net.URISyntaxException;
22 import org.chromium.base.ThreadUtils;
24 import org.xwalk.core.internal.extension.api.XWalkDisplayManager;
25 import org.xwalk.core.internal.extension.XWalkExtension;
26 import org.xwalk.core.internal.extension.XWalkExtensionContext;
29 * A XWalk extension for Presentation API implementation on Android.
31 public class PresentationExtension extends XWalkExtension {
32 public final static String JS_API_PATH = "jsapi/presentation_api.js";
34 private final static String NAME = "navigator.presentation";
35 private final static String TAG = "PresentationExtension";
38 private final static String TAG_BASE_URL = "baseUrl";
39 private final static String TAG_CMD = "cmd";
40 private final static String TAG_DATA = "data";
41 private final static String TAG_REQUEST_ID = "requestId";
42 private final static String TAG_URL = "url";
45 private final static String CMD_DISPLAY_AVAILABLE_CHANGE = "DisplayAvailableChange";
46 private final static String CMD_QUERY_DISPLAY_AVAILABILITY = "QueryDisplayAvailability";
47 private final static String CMD_REQUEST_SHOW = "RequestShow";
48 private final static String CMD_SHOW_SUCCEEDED = "ShowSucceeded";
49 private final static String CMD_SHOW_FAILED = "ShowFailed";
52 private final static String ERROR_INVALID_ACCESS = "InvalidAccessError";
53 private final static String ERROR_INVALID_PARAMETER = "InvalidParameterError";
54 private final static String ERROR_INVALID_STATE = "InvalidStateError";
55 private final static String ERROR_NOT_FOUND = "NotFoundError";
56 private final static String ERROR_NOT_SUPPORTED = "NotSupportedError";
58 private XWalkDisplayManager mDisplayManager;
60 // The number of available presentation displays on the system.
61 private int mAvailableDisplayCount = 0;
63 // The presentation content and view to be showed on the secondary display.
64 // Currently, only one presentation is allowed to show at the same time.
65 private XWalkPresentationContent mPresentationContent;
66 private XWalkPresentationContent.PresentationDelegate mPresentationDelegate;
67 private PresentationView mPresentationView;
70 * Listens for the secondary display arrival and removal.
72 * We rely on onDisplayAdded/onDisplayRemoved callback to trigger the display
73 * availability change event. The presentation display becomes available if
74 * the first secondary display is arrived, and becomes unavailable if one
75 * of the last secondary display is removed.
77 * Note the display id is a system-wide unique number for each physical connection.
78 * It means that for the same display device, the display id assigned by the system
79 * would be different if it is re-connected again.
81 private final XWalkDisplayManager.DisplayListener mDisplayListener =
82 new XWalkDisplayManager.DisplayListener() {
84 public void onDisplayAdded(int displayId) {
85 ++mAvailableDisplayCount;
87 // Notify that the secondary display for presentation show becomes
88 // available now if the first one is added.
89 if (mAvailableDisplayCount == 1) notifyAvailabilityChanged(true);
93 public void onDisplayRemoved(int displayId) {
94 --mAvailableDisplayCount;
96 // Notify that the secondary display for presentation show becomes
97 // unavailable now if the last one is removed already.
98 if (mAvailableDisplayCount == 0) {
99 notifyAvailabilityChanged(false);
100 // Destroy the presentation content if there is no available secondary display
102 closePresentationContent();
107 public void onDisplayChanged(int displayId) {
108 // TODO(hmin): Figure out the behaviour when the display is changed.
112 public PresentationExtension(String jsApi, XWalkExtensionContext context) {
113 super(NAME, jsApi, context);
115 mDisplayManager = XWalkDisplayManager.getInstance(context.getContext());
116 Display[] displays = mDisplayManager.getPresentationDisplays();
117 mAvailableDisplayCount = displays.length;
120 private Display getPreferredDisplay() {
121 Display[] displays = mDisplayManager.getPresentationDisplays();
122 if (displays.length > 0) return displays[0];
126 private void notifyAvailabilityChanged(boolean isAvailable) {
127 StringWriter contents = new StringWriter();
128 JsonWriter writer = new JsonWriter(contents);
131 writer.beginObject();
132 writer.name(TAG_CMD).value(CMD_DISPLAY_AVAILABLE_CHANGE);
133 writer.name(TAG_DATA).value(isAvailable);
137 broadcastMessage(contents.toString());
138 } catch (IOException e) {
139 Log.e(TAG, "Error: " + e.toString());
143 private void notifyRequestShowSucceed(int instanceId, int requestId, int presentationId) {
144 StringWriter contents = new StringWriter();
145 JsonWriter writer = new JsonWriter(contents);
148 writer.beginObject();
149 writer.name(TAG_CMD).value(CMD_SHOW_SUCCEEDED);
150 writer.name(TAG_REQUEST_ID).value(requestId);
151 writer.name(TAG_DATA).value(presentationId);
155 postMessage(instanceId, contents.toString());
156 } catch (IOException e) {
157 Log.e(TAG, "Error: " + e.toString());
161 private void notifyRequestShowFail(int instanceId, int requestId, String errorMessage) {
162 StringWriter contents = new StringWriter();
163 JsonWriter writer = new JsonWriter(contents);
166 writer.beginObject();
167 writer.name(TAG_CMD).value(CMD_SHOW_FAILED);
168 writer.name(TAG_REQUEST_ID).value(requestId);
169 writer.name(TAG_DATA).value(errorMessage);
173 postMessage(instanceId, contents.toString());
174 } catch (IOException e) {
175 Log.e(TAG, "Error: " + e.toString());
180 public void onMessage(int instanceId, String message) {
181 StringReader contents = new StringReader(message);
182 JsonReader reader = new JsonReader(contents);
187 String baseUrl = null;
189 reader.beginObject();
190 while (reader.hasNext()) {
191 String name = reader.nextName();
192 if (name.equals(TAG_CMD)) {
193 cmd = reader.nextString();
194 } else if (name.equals(TAG_REQUEST_ID)) {
195 requestId = reader.nextInt();
196 } else if (name.equals(TAG_URL)) {
197 url = reader.nextString();
198 } else if (name.equals(TAG_BASE_URL)) {
199 baseUrl = reader.nextString();
207 if (cmd != null && cmd.equals(CMD_REQUEST_SHOW) && requestId >= 0) {
208 handleRequestShow(instanceId, requestId, url, baseUrl);
210 } catch (IOException e) {
211 Log.d(TAG, "Error: " + e);
215 private void handleRequestShow(final int instanceId, final int requestId,
216 final String url, final String baseUrl) {
217 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
218 notifyRequestShowFail(instanceId, requestId, ERROR_NOT_SUPPORTED);
222 if (mAvailableDisplayCount == 0) {
223 Log.d(TAG, "No available presentation display is found.");
224 notifyRequestShowFail(instanceId, requestId, ERROR_NOT_FOUND);
228 // We have to post the runnable task for presentation view creation to
229 // UI thread since extension API is not running on UI thread.
230 ThreadUtils.runOnUiThread(new Runnable() {
233 Display preferredDisplay = getPreferredDisplay();
235 // No availble display is found.
236 if (preferredDisplay == null) {
237 notifyRequestShowFail(instanceId, requestId, ERROR_NOT_FOUND);
241 // Only one presentation is allowed to show on the presentation display. Notify
242 // the JS side that an error occurs if there is already one presentation showed.
243 if (mPresentationContent != null) {
244 notifyRequestShowFail(instanceId, requestId, ERROR_INVALID_ACCESS);
248 // Check the url passed to requestShow.
249 // If it's relative, combine it with baseUrl to make it abslute.
250 // If the url is invalid, notify the JS side ERROR_INVALID_PARAMETER exception.
251 String targetUrl = url;
252 URI targetUri = null;
254 targetUri = new URI(url);
255 if (!targetUri.isAbsolute()) {
256 URI baseUri = new URI(baseUrl);
257 targetUrl = baseUri.resolve(targetUri).toString();
259 } catch (URISyntaxException e) {
260 Log.e(TAG, "Invalid url passed to requestShow");
261 notifyRequestShowFail(instanceId, requestId, ERROR_INVALID_PARAMETER);
265 mPresentationContent = new XWalkPresentationContent(
266 mExtensionContext.getContext(),
267 mExtensionContext.getActivity(),
268 new XWalkPresentationContent.PresentationDelegate() {
270 public void onContentLoaded(XWalkPresentationContent content) {
271 notifyRequestShowSucceed(instanceId, requestId, content.getPresentationId());
275 public void onContentClosed(XWalkPresentationContent content) {
276 if (content == mPresentationContent) {
277 closePresentationContent();
278 if (mPresentationView != null) mPresentationView.cancel();
283 // Start to load the content from the target url.
284 mPresentationContent.load(targetUrl);
286 // Update the presentation view in order that the content could be presented
287 // on the preferred display.
288 updatePresentationView(preferredDisplay);
294 public String onSyncMessage(int instanceId, String message) {
295 if (message.equals(CMD_QUERY_DISPLAY_AVAILABILITY)) {
296 return mAvailableDisplayCount != 0 ? "true" : "false";
298 Log.e(TAG, "Unexpected sync message received: " + message);
304 public void onResume() {
305 Display[] displays = mDisplayManager.getPresentationDisplays();
307 // If there was available displays but right now no one is available for presentation,
308 // we need to notify the display availability changes and reset the display count.
309 if (displays.length == 0 && mAvailableDisplayCount > 0) {
310 notifyAvailabilityChanged(false);
311 mAvailableDisplayCount = 0;
312 closePresentationContent();
315 // If there was no available display but right now there is at least one available
316 // display, we need to notify the display availability changes and update the display
318 if (displays.length > 0 && mAvailableDisplayCount == 0) {
319 notifyAvailabilityChanged(true);
320 mAvailableDisplayCount = displays.length;
323 // If there was available displays and right now there is also at least one
324 // available display, we only need to update the display count.
325 if (displays.length > 0 && mAvailableDisplayCount > 0) {
326 mAvailableDisplayCount = displays.length;
329 if (mPresentationContent != null) {
330 mPresentationContent.onResume();
333 updatePresentationView(getPreferredDisplay());
335 // Register the listener to display manager.
336 mDisplayManager.registerDisplayListener(mDisplayListener);
339 private void updatePresentationView(Display preferredDisplay) {
340 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 ||
341 preferredDisplay == null) {
345 // No presentation content is ready for use.
346 if (mPresentationView == null && mPresentationContent == null) {
350 // If the presentation view is showed on another display, we need to dismiss it
351 // and re-create a new one.
352 if (mPresentationView != null && mPresentationView.getDisplay() != preferredDisplay) {
353 dismissPresentationView();
356 // If the presentation view is not NULL and its associated display is not changed,
357 // the displaying system will automatically restore the content on the view once
358 // the Activity gets resumed.
359 if (mPresentationView == null && mPresentationContent != null) {
360 // Remove the content view from its previous view hierarchy if have.
361 ViewGroup parent = (ViewGroup)mPresentationContent.getContentView().getParent();
362 if (parent != null) parent.removeView(mPresentationContent.getContentView());
364 mPresentationView = PresentationView.createInstance(mExtensionContext.getContext(), preferredDisplay);
365 mPresentationView.setContentView(mPresentationContent.getContentView());
366 mPresentationView.setPresentationListener(new PresentationView.PresentationListener() {
368 public void onDismiss(PresentationView view) {
369 // We need to pause the content if the view is dismissed from the screen
370 // to avoid unnecessary overhead to update the content, e.g. stop animation
372 if (view == mPresentationView) {
373 if (mPresentationContent != null) mPresentationContent.onPause();
374 mPresentationView = null;
379 public void onShow(PresentationView view) {
380 // The presentation content may be paused due to the presentation view was
381 // dismissed, we need to resume it when the new view is showed.
382 if (view == mPresentationView && mPresentationContent != null) {
383 mPresentationContent.onResume();
389 mPresentationView.show();
393 public void onPause() {
394 dismissPresentationView();
396 if (mPresentationContent != null) mPresentationContent.onPause();
398 // No need to listen display changes when the activity is paused.
399 mDisplayManager.unregisterDisplayListener(mDisplayListener);
403 public void onDestroy() {
404 // close the presentation content if have.
405 closePresentationContent();
408 private void dismissPresentationView() {
409 if (mPresentationView == null) return;
411 mPresentationView.dismiss();
412 mPresentationView = null;
415 private void closePresentationContent() {
416 if (mPresentationContent == null) return;
418 mPresentationContent.close();
419 mPresentationContent = null;