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.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.extension.api.XWalkDisplayManager;
25 import org.xwalk.core.extension.XWalkExtension;
26 import org.xwalk.core.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 if (mPresentationContent != null) {
102 mPresentationContent.close();
103 mPresentationContent = null;
109 public void onDisplayChanged(int displayId) {
110 // TODO(hmin): Figure out the behaviour when the display is changed.
114 public PresentationExtension(String name, String jsApi, XWalkExtensionContext context) {
115 super(name, jsApi, context);
117 mDisplayManager = XWalkDisplayManager.getInstance(context.getContext());
118 Display[] displays = mDisplayManager.getPresentationDisplays();
119 mAvailableDisplayCount = displays.length;
122 private Display getPreferredDisplay() {
123 Display[] displays = mDisplayManager.getPresentationDisplays();
124 if (displays.length > 0) return displays[0];
128 private void notifyAvailabilityChanged(boolean isAvailable) {
129 StringWriter contents = new StringWriter();
130 JsonWriter writer = new JsonWriter(contents);
133 writer.beginObject();
134 writer.name(TAG_CMD).value(CMD_DISPLAY_AVAILABLE_CHANGE);
135 writer.name(TAG_DATA).value(isAvailable);
139 broadcastMessage(contents.toString());
140 } catch (IOException e) {
141 Log.e(TAG, "Error: " + e.toString());
145 private void notifyRequestShowSucceed(int instanceId, int requestId, int presentationId) {
146 StringWriter contents = new StringWriter();
147 JsonWriter writer = new JsonWriter(contents);
150 writer.beginObject();
151 writer.name(TAG_CMD).value(CMD_SHOW_SUCCEEDED);
152 writer.name(TAG_REQUEST_ID).value(requestId);
153 writer.name(TAG_DATA).value(presentationId);
157 postMessage(instanceId, contents.toString());
158 } catch (IOException e) {
159 Log.e(TAG, "Error: " + e.toString());
163 private void notifyRequestShowFail(int instanceId, int requestId, String errorMessage) {
164 StringWriter contents = new StringWriter();
165 JsonWriter writer = new JsonWriter(contents);
168 writer.beginObject();
169 writer.name(TAG_CMD).value(CMD_SHOW_FAILED);
170 writer.name(TAG_REQUEST_ID).value(requestId);
171 writer.name(TAG_DATA).value(errorMessage);
175 postMessage(instanceId, contents.toString());
176 } catch (IOException e) {
177 Log.e(TAG, "Error: " + e.toString());
182 public void onMessage(int instanceId, String message) {
183 StringReader contents = new StringReader(message);
184 JsonReader reader = new JsonReader(contents);
189 String baseUrl = null;
191 reader.beginObject();
192 while (reader.hasNext()) {
193 String name = reader.nextName();
194 if (name.equals(TAG_CMD)) {
195 cmd = reader.nextString();
196 } else if (name.equals(TAG_REQUEST_ID)) {
197 requestId = reader.nextInt();
198 } else if (name.equals(TAG_URL)) {
199 url = reader.nextString();
200 } else if (name.equals(TAG_BASE_URL)) {
201 baseUrl = reader.nextString();
209 if (cmd != null && cmd.equals(CMD_REQUEST_SHOW) && requestId >= 0) {
210 handleRequestShow(instanceId, requestId, url, baseUrl);
212 } catch (IOException e) {
213 Log.d(TAG, "Error: " + e);
217 private void handleRequestShow(final int instanceId, final int requestId,
218 final String url, final String baseUrl) {
219 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
220 notifyRequestShowFail(instanceId, requestId, ERROR_NOT_SUPPORTED);
224 if (mAvailableDisplayCount == 0) {
225 Log.d(TAG, "No available presentation display is found.");
226 notifyRequestShowFail(instanceId, requestId, ERROR_NOT_FOUND);
230 // We have to post the runnable task for presentation view creation to
231 // UI thread since extension API is not running on UI thread.
232 ThreadUtils.runOnUiThread(new Runnable() {
235 Display preferredDisplay = getPreferredDisplay();
237 // No availble display is found.
238 if (preferredDisplay == null) {
239 notifyRequestShowFail(instanceId, requestId, ERROR_NOT_FOUND);
243 // Only one presentation is allowed to show on the presentation display. Notify
244 // the JS side that an error occurs if there is already one presentation showed.
245 if (mPresentationContent != null) {
246 notifyRequestShowFail(instanceId, requestId, ERROR_INVALID_ACCESS);
250 // Check the url passed to requestShow.
251 // If it's relative, combine it with baseUrl to make it abslute.
252 // If the url is invalid, notify the JS side ERROR_INVALID_PARAMETER exception.
253 String targetUrl = url;
254 URI targetUri = null;
256 targetUri = new URI(url);
257 if (!targetUri.isAbsolute()) {
258 URI baseUri = new URI(baseUrl);
259 targetUrl = baseUri.resolve(targetUri).toString();
261 } catch (URISyntaxException e) {
262 Log.e(TAG, "Invalid url passed to requestShow");
263 notifyRequestShowFail(instanceId, requestId, ERROR_INVALID_PARAMETER);
267 mPresentationContent = new XWalkPresentationContent(
268 mExtensionContext.getContext(),
269 mExtensionContext.getActivity(),
270 new XWalkPresentationContent.PresentationDelegate() {
272 public void onContentLoaded(XWalkPresentationContent content) {
273 notifyRequestShowSucceed(instanceId, requestId, content.getPresentationId());
277 public void onContentClosed(XWalkPresentationContent content) {
278 if (content == mPresentationContent) {
279 mPresentationContent.close();
280 mPresentationContent = null;
281 if (mPresentationView != null) mPresentationView.cancel();
286 // Start to load the content from the target url.
287 mPresentationContent.load(targetUrl);
289 // Update the presentation view in order that the content could be presented
290 // on the preferred display.
291 updatePresentationView(preferredDisplay);
297 public String onSyncMessage(int instanceId, String message) {
298 if (message.equals(CMD_QUERY_DISPLAY_AVAILABILITY)) {
299 return mAvailableDisplayCount != 0 ? "true" : "false";
301 Log.e(TAG, "Unexpected sync message received: " + message);
307 public void onResume() {
308 Display[] displays = mDisplayManager.getPresentationDisplays();
310 // If there was available displays but right now no one is available for presentation,
311 // we need to notify the display availability changes and reset the display count.
312 if (displays.length == 0 && mAvailableDisplayCount > 0) {
313 notifyAvailabilityChanged(false);
314 mAvailableDisplayCount = 0;
315 if (mPresentationContent != null) {
316 mPresentationContent.close();
317 mPresentationContent = null;
321 // If there was no available display but right now there is at least one available
322 // display, we need to notify the display availability changes and update the display
324 if (displays.length > 0 && mAvailableDisplayCount == 0) {
325 notifyAvailabilityChanged(true);
326 mAvailableDisplayCount = displays.length;
329 // If there was available displays and right now there is also at least one
330 // available display, we only need to update the display count.
331 if (displays.length > 0 && mAvailableDisplayCount > 0) {
332 mAvailableDisplayCount = displays.length;
335 if (mPresentationContent != null) {
336 mPresentationContent.onResume();
339 updatePresentationView(getPreferredDisplay());
341 // Register the listener to display manager.
342 mDisplayManager.registerDisplayListener(mDisplayListener);
345 private void updatePresentationView(Display preferredDisplay) {
346 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 ||
347 preferredDisplay == null) {
351 // No presentation content is ready for use.
352 if (mPresentationView == null && mPresentationContent == null) {
356 // If the presentation view is showed on another display, we need to dismiss it
357 // and re-create a new one.
358 if (mPresentationView != null && mPresentationView.getDisplay() != preferredDisplay) {
359 mPresentationView.dismiss();
360 mPresentationView = null;
363 // If the presentation view is not NULL and its associated display is not changed,
364 // the displaying system will automatically restore the content on the view once
365 // the Activity gets resumed.
366 if (mPresentationView == null && mPresentationContent != null) {
367 // Remove the content view from its previous view hierarchy if have.
368 ViewGroup parent = (ViewGroup)mPresentationContent.getContentView().getParent();
369 if (parent != null) parent.removeView(mPresentationContent.getContentView());
371 mPresentationView = PresentationView.createInstance(mExtensionContext.getContext(), preferredDisplay);
372 mPresentationView.setContentView(mPresentationContent.getContentView());
373 mPresentationView.setPresentationListener(new PresentationView.PresentationListener() {
375 public void onDismiss(PresentationView view) {
376 // We need to pause the content if the view is dismissed from the screen
377 // to avoid unnecessary overhead to update the content, e.g. stop animation
379 if (view == mPresentationView) {
380 if (mPresentationContent != null) mPresentationContent.onPause();
381 mPresentationView = null;
386 public void onShow(PresentationView view) {
387 // The presentation content may be paused due to the presentation view was
388 // dismissed, we need to resume it when the new view is showed.
389 if (view == mPresentationView && mPresentationContent != null) {
390 mPresentationContent.onResume();
396 mPresentationView.show();
400 public void onPause() {
401 if (mPresentationView != null) {
402 mPresentationView.dismiss();
403 mPresentationView = null;
406 if (mPresentationContent != null) mPresentationContent.onPause();
408 // No need to listen display changes when the activity is paused.
409 mDisplayManager.unregisterDisplayListener(mDisplayListener);
413 public void onDestroy() {