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 TAG = "PresentationExtension";
33 public final static String JS_API_PATH = "jsapi/presentation_api.js";
34 public final static String NAME = "navigator.presentation";
37 private final static String TAG_BASE_URL = "baseUrl";
38 private final static String TAG_CMD = "cmd";
39 private final static String TAG_DATA = "data";
40 private final static String TAG_REQUEST_ID = "requestId";
41 private final static String TAG_URL = "url";
44 private final static String CMD_DISPLAY_AVAILABLE_CHANGE = "DisplayAvailableChange";
45 private final static String CMD_QUERY_DISPLAY_AVAILABILITY = "QueryDisplayAvailability";
46 private final static String CMD_REQUEST_SHOW = "RequestShow";
47 private final static String CMD_SHOW_SUCCEEDED = "ShowSucceeded";
48 private final static String CMD_SHOW_FAILED = "ShowFailed";
51 private final static String ERROR_INVALID_ACCESS = "InvalidAccessError";
52 private final static String ERROR_INVALID_PARAMETER = "InvalidParameterError";
53 private final static String ERROR_INVALID_STATE = "InvalidStateError";
54 private final static String ERROR_NOT_FOUND = "NotFoundError";
55 private final static String ERROR_NOT_SUPPORTED = "NotSupportedError";
57 private XWalkDisplayManager mDisplayManager;
59 // The number of available presentation displays on the system.
60 private int mAvailableDisplayCount = 0;
62 // The presentation content and view to be showed on the secondary display.
63 // Currently, only one presentation is allowed to show at the same time.
64 private XWalkPresentationContent mPresentationContent;
65 private XWalkPresentationContent.PresentationDelegate mPresentationDelegate;
66 private PresentationView mPresentationView;
69 * Listens for the secondary display arrival and removal.
71 * We rely on onDisplayAdded/onDisplayRemoved callback to trigger the display
72 * availability change event. The presentation display becomes available if
73 * the first secondary display is arrived, and becomes unavailable if one
74 * of the last secondary display is removed.
76 * Note the display id is a system-wide unique number for each physical connection.
77 * It means that for the same display device, the display id assigned by the system
78 * would be different if it is re-connected again.
80 private final XWalkDisplayManager.DisplayListener mDisplayListener =
81 new XWalkDisplayManager.DisplayListener() {
83 public void onDisplayAdded(int displayId) {
84 ++mAvailableDisplayCount;
86 // Notify that the secondary display for presentation show becomes
87 // available now if the first one is added.
88 if (mAvailableDisplayCount == 1) notifyAvailabilityChanged(true);
92 public void onDisplayRemoved(int displayId) {
93 --mAvailableDisplayCount;
95 // Notify that the secondary display for presentation show becomes
96 // unavailable now if the last one is removed already.
97 if (mAvailableDisplayCount == 0) {
98 notifyAvailabilityChanged(false);
99 // Destroy the presentation content if there is no available secondary display
101 closePresentationContent();
106 public void onDisplayChanged(int displayId) {
107 // TODO(hmin): Figure out the behaviour when the display is changed.
111 public PresentationExtension(String name, String jsApi, XWalkExtensionContext context) {
112 super(name, jsApi, context);
114 mDisplayManager = XWalkDisplayManager.getInstance(context.getContext());
115 Display[] displays = mDisplayManager.getPresentationDisplays();
116 mAvailableDisplayCount = displays.length;
119 private Display getPreferredDisplay() {
120 Display[] displays = mDisplayManager.getPresentationDisplays();
121 if (displays.length > 0) return displays[0];
125 private void notifyAvailabilityChanged(boolean isAvailable) {
126 StringWriter contents = new StringWriter();
127 JsonWriter writer = new JsonWriter(contents);
130 writer.beginObject();
131 writer.name(TAG_CMD).value(CMD_DISPLAY_AVAILABLE_CHANGE);
132 writer.name(TAG_DATA).value(isAvailable);
136 broadcastMessage(contents.toString());
137 } catch (IOException e) {
138 Log.e(TAG, "Error: " + e.toString());
142 private void notifyRequestShowSucceed(int instanceId, int requestId, int presentationId) {
143 StringWriter contents = new StringWriter();
144 JsonWriter writer = new JsonWriter(contents);
147 writer.beginObject();
148 writer.name(TAG_CMD).value(CMD_SHOW_SUCCEEDED);
149 writer.name(TAG_REQUEST_ID).value(requestId);
150 writer.name(TAG_DATA).value(presentationId);
154 postMessage(instanceId, contents.toString());
155 } catch (IOException e) {
156 Log.e(TAG, "Error: " + e.toString());
160 private void notifyRequestShowFail(int instanceId, int requestId, String errorMessage) {
161 StringWriter contents = new StringWriter();
162 JsonWriter writer = new JsonWriter(contents);
165 writer.beginObject();
166 writer.name(TAG_CMD).value(CMD_SHOW_FAILED);
167 writer.name(TAG_REQUEST_ID).value(requestId);
168 writer.name(TAG_DATA).value(errorMessage);
172 postMessage(instanceId, contents.toString());
173 } catch (IOException e) {
174 Log.e(TAG, "Error: " + e.toString());
179 public void onMessage(int instanceId, String message) {
180 StringReader contents = new StringReader(message);
181 JsonReader reader = new JsonReader(contents);
186 String baseUrl = null;
188 reader.beginObject();
189 while (reader.hasNext()) {
190 String name = reader.nextName();
191 if (name.equals(TAG_CMD)) {
192 cmd = reader.nextString();
193 } else if (name.equals(TAG_REQUEST_ID)) {
194 requestId = reader.nextInt();
195 } else if (name.equals(TAG_URL)) {
196 url = reader.nextString();
197 } else if (name.equals(TAG_BASE_URL)) {
198 baseUrl = reader.nextString();
206 if (cmd != null && cmd.equals(CMD_REQUEST_SHOW) && requestId >= 0) {
207 handleRequestShow(instanceId, requestId, url, baseUrl);
209 } catch (IOException e) {
210 Log.d(TAG, "Error: " + e);
214 private void handleRequestShow(final int instanceId, final int requestId,
215 final String url, final String baseUrl) {
216 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
217 notifyRequestShowFail(instanceId, requestId, ERROR_NOT_SUPPORTED);
221 if (mAvailableDisplayCount == 0) {
222 Log.d(TAG, "No available presentation display is found.");
223 notifyRequestShowFail(instanceId, requestId, ERROR_NOT_FOUND);
227 // We have to post the runnable task for presentation view creation to
228 // UI thread since extension API is not running on UI thread.
229 ThreadUtils.runOnUiThread(new Runnable() {
232 Display preferredDisplay = getPreferredDisplay();
234 // No availble display is found.
235 if (preferredDisplay == null) {
236 notifyRequestShowFail(instanceId, requestId, ERROR_NOT_FOUND);
240 // Only one presentation is allowed to show on the presentation display. Notify
241 // the JS side that an error occurs if there is already one presentation showed.
242 if (mPresentationContent != null) {
243 notifyRequestShowFail(instanceId, requestId, ERROR_INVALID_ACCESS);
247 // Check the url passed to requestShow.
248 // If it's relative, combine it with baseUrl to make it abslute.
249 // If the url is invalid, notify the JS side ERROR_INVALID_PARAMETER exception.
250 String targetUrl = url;
251 URI targetUri = null;
253 targetUri = new URI(url);
254 if (!targetUri.isAbsolute()) {
255 URI baseUri = new URI(baseUrl);
256 targetUrl = baseUri.resolve(targetUri).toString();
258 } catch (URISyntaxException e) {
259 Log.e(TAG, "Invalid url passed to requestShow");
260 notifyRequestShowFail(instanceId, requestId, ERROR_INVALID_PARAMETER);
264 mPresentationContent = new XWalkPresentationContent(
265 mExtensionContext.getContext(),
266 mExtensionContext.getActivity(),
267 new XWalkPresentationContent.PresentationDelegate() {
269 public void onContentLoaded(XWalkPresentationContent content) {
270 notifyRequestShowSucceed(instanceId, requestId, content.getPresentationId());
274 public void onContentClosed(XWalkPresentationContent content) {
275 if (content == mPresentationContent) {
276 closePresentationContent();
277 if (mPresentationView != null) mPresentationView.cancel();
282 // Start to load the content from the target url.
283 mPresentationContent.load(targetUrl);
285 // Update the presentation view in order that the content could be presented
286 // on the preferred display.
287 updatePresentationView(preferredDisplay);
293 public String onSyncMessage(int instanceId, String message) {
294 if (message.equals(CMD_QUERY_DISPLAY_AVAILABILITY)) {
295 return mAvailableDisplayCount != 0 ? "true" : "false";
297 Log.e(TAG, "Unexpected sync message received: " + message);
303 public void onResume() {
304 Display[] displays = mDisplayManager.getPresentationDisplays();
306 // If there was available displays but right now no one is available for presentation,
307 // we need to notify the display availability changes and reset the display count.
308 if (displays.length == 0 && mAvailableDisplayCount > 0) {
309 notifyAvailabilityChanged(false);
310 mAvailableDisplayCount = 0;
311 closePresentationContent();
314 // If there was no available display but right now there is at least one available
315 // display, we need to notify the display availability changes and update the display
317 if (displays.length > 0 && mAvailableDisplayCount == 0) {
318 notifyAvailabilityChanged(true);
319 mAvailableDisplayCount = displays.length;
322 // If there was available displays and right now there is also at least one
323 // available display, we only need to update the display count.
324 if (displays.length > 0 && mAvailableDisplayCount > 0) {
325 mAvailableDisplayCount = displays.length;
328 if (mPresentationContent != null) {
329 mPresentationContent.onResume();
332 updatePresentationView(getPreferredDisplay());
334 // Register the listener to display manager.
335 mDisplayManager.registerDisplayListener(mDisplayListener);
338 private void updatePresentationView(Display preferredDisplay) {
339 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 ||
340 preferredDisplay == null) {
344 // No presentation content is ready for use.
345 if (mPresentationView == null && mPresentationContent == null) {
349 // If the presentation view is showed on another display, we need to dismiss it
350 // and re-create a new one.
351 if (mPresentationView != null && mPresentationView.getDisplay() != preferredDisplay) {
352 dismissPresentationView();
355 // If the presentation view is not NULL and its associated display is not changed,
356 // the displaying system will automatically restore the content on the view once
357 // the Activity gets resumed.
358 if (mPresentationView == null && mPresentationContent != null) {
359 // Remove the content view from its previous view hierarchy if have.
360 ViewGroup parent = (ViewGroup)mPresentationContent.getContentView().getParent();
361 if (parent != null) parent.removeView(mPresentationContent.getContentView());
363 mPresentationView = PresentationView.createInstance(mExtensionContext.getContext(), preferredDisplay);
364 mPresentationView.setContentView(mPresentationContent.getContentView());
365 mPresentationView.setPresentationListener(new PresentationView.PresentationListener() {
367 public void onDismiss(PresentationView view) {
368 // We need to pause the content if the view is dismissed from the screen
369 // to avoid unnecessary overhead to update the content, e.g. stop animation
371 if (view == mPresentationView) {
372 if (mPresentationContent != null) mPresentationContent.onPause();
373 mPresentationView = null;
378 public void onShow(PresentationView view) {
379 // The presentation content may be paused due to the presentation view was
380 // dismissed, we need to resume it when the new view is showed.
381 if (view == mPresentationView && mPresentationContent != null) {
382 mPresentationContent.onResume();
388 mPresentationView.show();
392 public void onPause() {
393 dismissPresentationView();
395 if (mPresentationContent != null) mPresentationContent.onPause();
397 // No need to listen display changes when the activity is paused.
398 mDisplayManager.unregisterDisplayListener(mDisplayListener);
402 public void onDestroy() {
403 // close the presentation content if have.
404 closePresentationContent();
407 private void dismissPresentationView() {
408 if (mPresentationView == null) return;
410 mPresentationView.dismiss();
411 mPresentationView = null;
414 private void closePresentationContent() {
415 if (mPresentationContent == null) return;
417 mPresentationContent.close();
418 mPresentationContent = null;