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