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.app.Activity;
8 import android.content.Context;
9 import android.os.Build;
10 import android.os.Bundle;
11 import android.provider.ContactsContract;
12 import android.util.JsonReader;
13 import android.util.JsonWriter;
14 import android.util.Log;
15 import android.view.Display;
16 import android.view.ViewGroup;
18 import java.io.IOException;
19 import java.io.StringReader;
20 import java.io.StringWriter;
21 import java.lang.ref.WeakReference;
23 import java.net.URISyntaxException;
25 import org.chromium.base.ActivityState;
26 import org.chromium.base.ThreadUtils;
28 import org.xwalk.core.internal.extension.api.XWalkDisplayManager;
29 import org.xwalk.core.internal.extension.XWalkExtensionWithActivityStateListener;
32 * A XWalk extension for Presentation API implementation on Android.
34 public class PresentationExtension extends XWalkExtensionWithActivityStateListener {
35 public final static String JS_API_PATH = "jsapi/presentation_api.js";
37 private final static String NAME = "navigator.presentation";
38 private final static String TAG = "PresentationExtension";
41 private final static String TAG_BASE_URL = "baseUrl";
42 private final static String TAG_CMD = "cmd";
43 private final static String TAG_DATA = "data";
44 private final static String TAG_REQUEST_ID = "requestId";
45 private final static String TAG_URL = "url";
48 private final static String CMD_DISPLAY_AVAILABLE_CHANGE = "DisplayAvailableChange";
49 private final static String CMD_QUERY_DISPLAY_AVAILABILITY = "QueryDisplayAvailability";
50 private final static String CMD_REQUEST_SHOW = "RequestShow";
51 private final static String CMD_SHOW_SUCCEEDED = "ShowSucceeded";
52 private final static String CMD_SHOW_FAILED = "ShowFailed";
55 private final static String ERROR_INVALID_ACCESS = "InvalidAccessError";
56 private final static String ERROR_INVALID_PARAMETER = "InvalidParameterError";
57 private final static String ERROR_INVALID_STATE = "InvalidStateError";
58 private final static String ERROR_NOT_FOUND = "NotFoundError";
59 private final static String ERROR_NOT_SUPPORTED = "NotSupportedError";
61 private XWalkDisplayManager mDisplayManager;
63 // The number of available presentation displays on the system.
64 private int mAvailableDisplayCount = 0;
66 // The presentation content and view to be showed on the secondary display.
67 // Currently, only one presentation is allowed to show at the same time.
68 private XWalkPresentationContent mPresentationContent;
69 private XWalkPresentationContent.PresentationDelegate mPresentationDelegate;
70 private PresentationView mPresentationView;
71 private Context mContext;
72 private WeakReference<Activity> mActivity;
75 * Listens for the secondary display arrival and removal.
77 * We rely on onDisplayAdded/onDisplayRemoved callback to trigger the display
78 * availability change event. The presentation display becomes available if
79 * the first secondary display is arrived, and becomes unavailable if one
80 * of the last secondary display is removed.
82 * Note the display id is a system-wide unique number for each physical connection.
83 * It means that for the same display device, the display id assigned by the system
84 * would be different if it is re-connected again.
86 private final XWalkDisplayManager.DisplayListener mDisplayListener =
87 new XWalkDisplayManager.DisplayListener() {
89 public void onDisplayAdded(int displayId) {
90 ++mAvailableDisplayCount;
92 // Notify that the secondary display for presentation show becomes
93 // available now if the first one is added.
94 if (mAvailableDisplayCount == 1) notifyAvailabilityChanged(true);
98 public void onDisplayRemoved(int displayId) {
99 --mAvailableDisplayCount;
101 // Notify that the secondary display for presentation show becomes
102 // unavailable now if the last one is removed already.
103 if (mAvailableDisplayCount == 0) {
104 notifyAvailabilityChanged(false);
105 // Destroy the presentation content if there is no available secondary display
107 closePresentationContent();
112 public void onDisplayChanged(int displayId) {
113 // TODO(hmin): Figure out the behaviour when the display is changed.
117 public PresentationExtension(String jsApi, Activity activity) {
118 super(NAME, jsApi, activity);
120 mContext = activity.getApplicationContext();
121 mActivity = new WeakReference<Activity>(activity);
122 mDisplayManager = XWalkDisplayManager.getInstance(activity.getApplicationContext());
123 Display[] displays = mDisplayManager.getPresentationDisplays();
124 mAvailableDisplayCount = displays.length;
127 private Display getPreferredDisplay() {
128 Display[] displays = mDisplayManager.getPresentationDisplays();
129 if (displays.length > 0) return displays[0];
133 private void notifyAvailabilityChanged(boolean isAvailable) {
134 StringWriter contents = new StringWriter();
135 JsonWriter writer = new JsonWriter(contents);
138 writer.beginObject();
139 writer.name(TAG_CMD).value(CMD_DISPLAY_AVAILABLE_CHANGE);
140 writer.name(TAG_DATA).value(isAvailable);
144 broadcastMessage(contents.toString());
145 } catch (IOException e) {
146 Log.e(TAG, "Error: " + e.toString());
150 private void notifyRequestShowSucceed(int instanceId, int requestId, int presentationId) {
151 StringWriter contents = new StringWriter();
152 JsonWriter writer = new JsonWriter(contents);
155 writer.beginObject();
156 writer.name(TAG_CMD).value(CMD_SHOW_SUCCEEDED);
157 writer.name(TAG_REQUEST_ID).value(requestId);
158 writer.name(TAG_DATA).value(presentationId);
162 postMessage(instanceId, contents.toString());
163 } catch (IOException e) {
164 Log.e(TAG, "Error: " + e.toString());
168 private void notifyRequestShowFail(int instanceId, int requestId, String errorMessage) {
169 StringWriter contents = new StringWriter();
170 JsonWriter writer = new JsonWriter(contents);
173 writer.beginObject();
174 writer.name(TAG_CMD).value(CMD_SHOW_FAILED);
175 writer.name(TAG_REQUEST_ID).value(requestId);
176 writer.name(TAG_DATA).value(errorMessage);
180 postMessage(instanceId, contents.toString());
181 } catch (IOException e) {
182 Log.e(TAG, "Error: " + e.toString());
187 public void onMessage(int instanceId, String message) {
188 StringReader contents = new StringReader(message);
189 JsonReader reader = new JsonReader(contents);
194 String baseUrl = null;
196 reader.beginObject();
197 while (reader.hasNext()) {
198 String name = reader.nextName();
199 if (name.equals(TAG_CMD)) {
200 cmd = reader.nextString();
201 } else if (name.equals(TAG_REQUEST_ID)) {
202 requestId = reader.nextInt();
203 } else if (name.equals(TAG_URL)) {
204 url = reader.nextString();
205 } else if (name.equals(TAG_BASE_URL)) {
206 baseUrl = reader.nextString();
214 if (cmd != null && cmd.equals(CMD_REQUEST_SHOW) && requestId >= 0) {
215 handleRequestShow(instanceId, requestId, url, baseUrl);
217 } catch (IOException e) {
218 Log.d(TAG, "Error: " + e);
222 private void handleRequestShow(final int instanceId, final int requestId,
223 final String url, final String baseUrl) {
224 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
225 notifyRequestShowFail(instanceId, requestId, ERROR_NOT_SUPPORTED);
229 if (mAvailableDisplayCount == 0) {
230 Log.d(TAG, "No available presentation display is found.");
231 notifyRequestShowFail(instanceId, requestId, ERROR_NOT_FOUND);
235 // We have to post the runnable task for presentation view creation to
236 // UI thread since extension API is not running on UI thread.
237 ThreadUtils.runOnUiThread(new Runnable() {
240 Display preferredDisplay = getPreferredDisplay();
242 // No availble display is found.
243 if (preferredDisplay == null) {
244 notifyRequestShowFail(instanceId, requestId, ERROR_NOT_FOUND);
248 // Only one presentation is allowed to show on the presentation display. Notify
249 // the JS side that an error occurs if there is already one presentation showed.
250 if (mPresentationContent != null) {
251 notifyRequestShowFail(instanceId, requestId, ERROR_INVALID_ACCESS);
255 // Check the url passed to requestShow.
256 // If it's relative, combine it with baseUrl to make it abslute.
257 // If the url is invalid, notify the JS side ERROR_INVALID_PARAMETER exception.
258 String targetUrl = url;
259 URI targetUri = null;
261 targetUri = new URI(url);
262 if (!targetUri.isAbsolute()) {
263 URI baseUri = new URI(baseUrl);
264 targetUrl = baseUri.resolve(targetUri).toString();
266 } catch (URISyntaxException e) {
267 Log.e(TAG, "Invalid url passed to requestShow");
268 notifyRequestShowFail(instanceId, requestId, ERROR_INVALID_PARAMETER);
272 mPresentationContent = new XWalkPresentationContent(
275 new XWalkPresentationContent.PresentationDelegate() {
277 public void onContentLoaded(XWalkPresentationContent content) {
278 notifyRequestShowSucceed(instanceId, requestId, content.getPresentationId());
282 public void onContentClosed(XWalkPresentationContent content) {
283 if (content == mPresentationContent) {
284 closePresentationContent();
285 if (mPresentationView != null) mPresentationView.cancel();
290 // Start to load the content from the target url.
291 mPresentationContent.load(targetUrl);
293 // Update the presentation view in order that the content could be presented
294 // on the preferred display.
295 updatePresentationView(preferredDisplay);
301 public String onSyncMessage(int instanceId, String message) {
302 if (message.equals(CMD_QUERY_DISPLAY_AVAILABILITY)) {
303 return mAvailableDisplayCount != 0 ? "true" : "false";
305 Log.e(TAG, "Unexpected sync message received: " + message);
310 public void onResume() {
311 Display[] displays = mDisplayManager.getPresentationDisplays();
313 // If there was available displays but right now no one is available for presentation,
314 // we need to notify the display availability changes and reset the display count.
315 if (displays.length == 0 && mAvailableDisplayCount > 0) {
316 notifyAvailabilityChanged(false);
317 mAvailableDisplayCount = 0;
318 closePresentationContent();
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 dismissPresentationView();
362 // If the presentation view is not NULL and its associated display is not changed,
363 // the displaying system will automatically restore the content on the view once
364 // the Activity gets resumed.
365 if (mPresentationView == null && mPresentationContent != null) {
366 // Remove the content view from its previous view hierarchy if have.
367 ViewGroup parent = (ViewGroup)mPresentationContent.getContentView().getParent();
368 if (parent != null) parent.removeView(mPresentationContent.getContentView());
370 mPresentationView = PresentationView.createInstance(mContext, preferredDisplay);
371 mPresentationView.setContentView(mPresentationContent.getContentView());
372 mPresentationView.setPresentationListener(new PresentationView.PresentationListener() {
374 public void onDismiss(PresentationView view) {
375 // We need to pause the content if the view is dismissed from the screen
376 // to avoid unnecessary overhead to update the content, e.g. stop animation
378 if (view == mPresentationView) {
379 if (mPresentationContent != null) mPresentationContent.onPause();
380 mPresentationView = null;
385 public void onShow(PresentationView view) {
386 // The presentation content may be paused due to the presentation view was
387 // dismissed, we need to resume it when the new view is showed.
388 if (view == mPresentationView && mPresentationContent != null) {
389 mPresentationContent.onResume();
395 mPresentationView.show();
398 private void dismissPresentationView() {
399 if (mPresentationView == null) return;
401 mPresentationView.dismiss();
402 mPresentationView = null;
405 private void closePresentationContent() {
406 if (mPresentationContent == null) return;
408 mPresentationContent.close();
409 mPresentationContent = null;
413 public void onActivityStateChange(Activity activity, int newState) {
415 case ActivityState.RESUMED:
418 case ActivityState.PAUSED:
419 dismissPresentationView();
420 if (mPresentationContent != null) mPresentationContent.onPause();
421 // No need to listen display changes when the activity is paused.
422 mDisplayManager.unregisterDisplayListener(mDisplayListener);
424 case ActivityState.DESTROYED:
425 // close the presentation content if have.
426 closePresentationContent();