e958fd60d51a8fca8d10b2abbba4a8483935d0c1
[platform/framework/web/crosswalk.git] / src / xwalk / runtime / android / core_internal / src / org / xwalk / core / internal / extension / api / presentation / PresentationExtension.java
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.
4
5 package org.xwalk.core.internal.extension.api.presentation;
6
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;
15
16 import java.io.IOException;
17 import java.io.StringReader;
18 import java.io.StringWriter;
19 import java.net.URI;
20 import java.net.URISyntaxException;
21
22 import org.chromium.base.ThreadUtils;
23
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;
27
28 /**
29  * A XWalk extension for Presentation API implementation on Android.
30  */
31 public class PresentationExtension extends XWalkExtension {
32     public final static String JS_API_PATH = "jsapi/presentation_api.js";
33
34     private final static String NAME = "navigator.presentation";
35     private final static String TAG = "PresentationExtension";
36
37     // Tags:
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";
43
44     // Command messages:
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";
50
51     // Error messages:
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";
57
58     private XWalkDisplayManager mDisplayManager;
59
60     // The number of available presentation displays on the system.
61     private int mAvailableDisplayCount = 0;
62
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;
68
69     /**
70      * Listens for the secondary display arrival and removal.
71      *
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.
76      *
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.
80      */
81     private final XWalkDisplayManager.DisplayListener mDisplayListener =
82             new XWalkDisplayManager.DisplayListener() {
83         @Override
84         public void onDisplayAdded(int displayId) {
85             ++mAvailableDisplayCount;
86
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);
90         }
91
92         @Override
93         public void onDisplayRemoved(int displayId) {
94             --mAvailableDisplayCount;
95
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
101                 // any more.
102                 closePresentationContent();
103             }
104         }
105
106         @Override
107         public void onDisplayChanged(int displayId) {
108             // TODO(hmin): Figure out the behaviour when the display is changed.
109         }
110     };
111
112     public PresentationExtension(String jsApi, XWalkExtensionContext context) {
113         super(NAME, jsApi, context);
114
115         mDisplayManager = XWalkDisplayManager.getInstance(context.getContext());
116         Display[] displays = mDisplayManager.getPresentationDisplays();
117         mAvailableDisplayCount = displays.length;
118     }
119
120     private Display getPreferredDisplay() {
121         Display[] displays = mDisplayManager.getPresentationDisplays();
122         if (displays.length > 0) return displays[0];
123         else return null;
124     }
125
126     private void notifyAvailabilityChanged(boolean isAvailable) {
127         StringWriter contents = new StringWriter();
128         JsonWriter writer = new JsonWriter(contents);
129
130         try {
131             writer.beginObject();
132             writer.name(TAG_CMD).value(CMD_DISPLAY_AVAILABLE_CHANGE);
133             writer.name(TAG_DATA).value(isAvailable);
134             writer.endObject();
135             writer.close();
136
137             broadcastMessage(contents.toString());
138         } catch (IOException e) {
139             Log.e(TAG, "Error: " + e.toString());
140         }
141     }
142
143     private void notifyRequestShowSucceed(int instanceId, int requestId, int presentationId) {
144         StringWriter contents = new StringWriter();
145         JsonWriter writer = new JsonWriter(contents);
146
147         try {
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);
152             writer.endObject();
153             writer.close();
154
155             postMessage(instanceId, contents.toString());
156         } catch (IOException e) {
157             Log.e(TAG, "Error: " + e.toString());
158         }
159     }
160
161     private void notifyRequestShowFail(int instanceId, int requestId, String errorMessage) {
162         StringWriter contents = new StringWriter();
163         JsonWriter writer = new JsonWriter(contents);
164
165         try {
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);
170             writer.endObject();
171             writer.close();
172
173             postMessage(instanceId, contents.toString());
174         } catch (IOException e) {
175             Log.e(TAG, "Error: " + e.toString());
176         }
177     }
178
179     @Override
180     public void onMessage(int instanceId, String message) {
181         StringReader contents = new StringReader(message);
182         JsonReader reader = new JsonReader(contents);
183
184         int requestId = -1;
185         String cmd = null;
186         String url = null;
187         String baseUrl = null;
188         try {
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();
200                 } else {
201                     reader.skipValue();
202                 }
203             }
204             reader.endObject();
205             reader.close();
206
207             if (cmd != null && cmd.equals(CMD_REQUEST_SHOW) && requestId >= 0) {
208                 handleRequestShow(instanceId, requestId, url, baseUrl);
209             }
210         } catch (IOException e) {
211             Log.d(TAG, "Error: " + e);
212         }
213     }
214
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);
219             return;
220         }
221
222         if (mAvailableDisplayCount == 0) {
223             Log.d(TAG, "No available presentation display is found.");
224             notifyRequestShowFail(instanceId, requestId, ERROR_NOT_FOUND);
225             return;
226         }
227
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() {
231             @Override
232             public void run() {
233                 Display preferredDisplay = getPreferredDisplay();
234
235                 // No availble display is found.
236                 if (preferredDisplay == null) {
237                     notifyRequestShowFail(instanceId, requestId, ERROR_NOT_FOUND);
238                     return;
239                 }
240
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);
245                     return;
246                 }
247
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;
253                 try {
254                     targetUri = new URI(url);
255                     if (!targetUri.isAbsolute()) {
256                         URI baseUri = new URI(baseUrl);
257                         targetUrl = baseUri.resolve(targetUri).toString();
258                     }
259                 } catch (URISyntaxException e) {
260                     Log.e(TAG, "Invalid url passed to requestShow");
261                     notifyRequestShowFail(instanceId, requestId, ERROR_INVALID_PARAMETER);
262                     return;
263                 }
264
265                 mPresentationContent = new XWalkPresentationContent(
266                         mExtensionContext.getContext(),
267                         mExtensionContext.getActivity(),
268                         new XWalkPresentationContent.PresentationDelegate() {
269                     @Override
270                     public void onContentLoaded(XWalkPresentationContent content) {
271                         notifyRequestShowSucceed(instanceId, requestId, content.getPresentationId());
272                     }
273
274                     @Override
275                     public void onContentClosed(XWalkPresentationContent content) {
276                         if (content == mPresentationContent) {
277                             closePresentationContent();
278                             if (mPresentationView != null) mPresentationView.cancel();
279                         }
280                     }
281                 });
282
283                 // Start to load the content from the target url.
284                 mPresentationContent.load(targetUrl);
285
286                 // Update the presentation view in order that the content could be presented
287                 // on the preferred display.
288                 updatePresentationView(preferredDisplay);
289             }
290         });
291     }
292
293     @Override
294     public String onSyncMessage(int instanceId, String message) {
295         if (message.equals(CMD_QUERY_DISPLAY_AVAILABILITY)) {
296             return mAvailableDisplayCount != 0 ? "true" : "false";
297         } else {
298             Log.e(TAG, "Unexpected sync message received: " + message);
299             return "";
300         }
301     }
302
303     @Override
304     public void onResume() {
305         Display[] displays = mDisplayManager.getPresentationDisplays();
306
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();
313         }
314
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
317         // count.
318         if (displays.length > 0 && mAvailableDisplayCount == 0) {
319             notifyAvailabilityChanged(true);
320             mAvailableDisplayCount = displays.length;
321         }
322
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;
327         }
328
329         if (mPresentationContent != null) {
330             mPresentationContent.onResume();
331         }
332
333         updatePresentationView(getPreferredDisplay());
334
335         // Register the listener to display manager.
336         mDisplayManager.registerDisplayListener(mDisplayListener);
337     }
338
339     private void updatePresentationView(Display preferredDisplay) {
340         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 ||
341                 preferredDisplay == null) {
342             return;
343         }
344
345         // No presentation content is ready for use.
346         if (mPresentationView == null && mPresentationContent == null) {
347             return;
348         }
349
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();
354         }
355
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());
363
364             mPresentationView = PresentationView.createInstance(mExtensionContext.getContext(), preferredDisplay);
365             mPresentationView.setContentView(mPresentationContent.getContentView());
366             mPresentationView.setPresentationListener(new PresentationView.PresentationListener() {
367                 @Override
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
371                     // and JS execution.
372                     if (view == mPresentationView) {
373                         if (mPresentationContent != null) mPresentationContent.onPause();
374                         mPresentationView = null;
375                     }
376                 }
377
378                 @Override
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();
384                     }
385                 }
386             });
387         }
388
389         mPresentationView.show();
390     }
391
392     @Override
393     public void onPause() {
394         dismissPresentationView();
395
396         if (mPresentationContent != null) mPresentationContent.onPause();
397
398         // No need to listen display changes when the activity is paused.
399         mDisplayManager.unregisterDisplayListener(mDisplayListener);
400     }
401
402     @Override
403     public void onDestroy() {
404         // close the presentation content if have.
405         closePresentationContent();
406     }
407
408     private void dismissPresentationView() {
409         if (mPresentationView == null) return;
410
411         mPresentationView.dismiss();
412         mPresentationView = null;
413     }
414
415     private void closePresentationContent() {
416         if (mPresentationContent == null) return;
417
418         mPresentationContent.close();
419         mPresentationContent = null;
420     }
421 }