03775ae4ffef33373b1c7a70e484e5b1117408d1
[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.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;
17
18 import java.io.IOException;
19 import java.io.StringReader;
20 import java.io.StringWriter;
21 import java.lang.ref.WeakReference;
22 import java.net.URI;
23 import java.net.URISyntaxException;
24
25 import org.chromium.base.ActivityState;
26 import org.chromium.base.ThreadUtils;
27
28 import org.xwalk.core.internal.extension.api.XWalkDisplayManager;
29 import org.xwalk.core.internal.extension.XWalkExtensionWithActivityStateListener;
30
31 /**
32  * A XWalk extension for Presentation API implementation on Android.
33  */
34 public class PresentationExtension extends XWalkExtensionWithActivityStateListener {
35     public final static String JS_API_PATH = "jsapi/presentation_api.js";
36
37     private final static String NAME = "navigator.presentation";
38     private final static String TAG = "PresentationExtension";
39
40     // Tags:
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";
46
47     // Command messages:
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";
53
54     // Error messages:
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";
60
61     private XWalkDisplayManager mDisplayManager;
62
63     // The number of available presentation displays on the system.
64     private int mAvailableDisplayCount = 0;
65
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;
73
74     /**
75      * Listens for the secondary display arrival and removal.
76      *
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.
81      *
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.
85      */
86     private final XWalkDisplayManager.DisplayListener mDisplayListener =
87             new XWalkDisplayManager.DisplayListener() {
88         @Override
89         public void onDisplayAdded(int displayId) {
90             ++mAvailableDisplayCount;
91
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);
95         }
96
97         @Override
98         public void onDisplayRemoved(int displayId) {
99             --mAvailableDisplayCount;
100
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
106                 // any more.
107                 closePresentationContent();
108             }
109         }
110
111         @Override
112         public void onDisplayChanged(int displayId) {
113             // TODO(hmin): Figure out the behaviour when the display is changed.
114         }
115     };
116
117     public PresentationExtension(String jsApi, Activity activity) {
118         super(NAME, jsApi, activity);
119
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;
125     }
126
127     private Display getPreferredDisplay() {
128         Display[] displays = mDisplayManager.getPresentationDisplays();
129         if (displays.length > 0) return displays[0];
130         else return null;
131     }
132
133     private void notifyAvailabilityChanged(boolean isAvailable) {
134         StringWriter contents = new StringWriter();
135         JsonWriter writer = new JsonWriter(contents);
136
137         try {
138             writer.beginObject();
139             writer.name(TAG_CMD).value(CMD_DISPLAY_AVAILABLE_CHANGE);
140             writer.name(TAG_DATA).value(isAvailable);
141             writer.endObject();
142             writer.close();
143
144             broadcastMessage(contents.toString());
145         } catch (IOException e) {
146             Log.e(TAG, "Error: " + e.toString());
147         }
148     }
149
150     private void notifyRequestShowSucceed(int instanceId, int requestId, int presentationId) {
151         StringWriter contents = new StringWriter();
152         JsonWriter writer = new JsonWriter(contents);
153
154         try {
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);
159             writer.endObject();
160             writer.close();
161
162             postMessage(instanceId, contents.toString());
163         } catch (IOException e) {
164             Log.e(TAG, "Error: " + e.toString());
165         }
166     }
167
168     private void notifyRequestShowFail(int instanceId, int requestId, String errorMessage) {
169         StringWriter contents = new StringWriter();
170         JsonWriter writer = new JsonWriter(contents);
171
172         try {
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);
177             writer.endObject();
178             writer.close();
179
180             postMessage(instanceId, contents.toString());
181         } catch (IOException e) {
182             Log.e(TAG, "Error: " + e.toString());
183         }
184     }
185
186     @Override
187     public void onMessage(int instanceId, String message) {
188         StringReader contents = new StringReader(message);
189         JsonReader reader = new JsonReader(contents);
190
191         int requestId = -1;
192         String cmd = null;
193         String url = null;
194         String baseUrl = null;
195         try {
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();
207                 } else {
208                     reader.skipValue();
209                 }
210             }
211             reader.endObject();
212             reader.close();
213
214             if (cmd != null && cmd.equals(CMD_REQUEST_SHOW) && requestId >= 0) {
215                 handleRequestShow(instanceId, requestId, url, baseUrl);
216             }
217         } catch (IOException e) {
218             Log.d(TAG, "Error: " + e);
219         }
220     }
221
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);
226             return;
227         }
228
229         if (mAvailableDisplayCount == 0) {
230             Log.d(TAG, "No available presentation display is found.");
231             notifyRequestShowFail(instanceId, requestId, ERROR_NOT_FOUND);
232             return;
233         }
234
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() {
238             @Override
239             public void run() {
240                 Display preferredDisplay = getPreferredDisplay();
241
242                 // No availble display is found.
243                 if (preferredDisplay == null) {
244                     notifyRequestShowFail(instanceId, requestId, ERROR_NOT_FOUND);
245                     return;
246                 }
247
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);
252                     return;
253                 }
254
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;
260                 try {
261                     targetUri = new URI(url);
262                     if (!targetUri.isAbsolute()) {
263                         URI baseUri = new URI(baseUrl);
264                         targetUrl = baseUri.resolve(targetUri).toString();
265                     }
266                 } catch (URISyntaxException e) {
267                     Log.e(TAG, "Invalid url passed to requestShow");
268                     notifyRequestShowFail(instanceId, requestId, ERROR_INVALID_PARAMETER);
269                     return;
270                 }
271
272                 mPresentationContent = new XWalkPresentationContent(
273                         mContext,
274                         mActivity,
275                         new XWalkPresentationContent.PresentationDelegate() {
276                     @Override
277                     public void onContentLoaded(XWalkPresentationContent content) {
278                         notifyRequestShowSucceed(instanceId, requestId, content.getPresentationId());
279                     }
280
281                     @Override
282                     public void onContentClosed(XWalkPresentationContent content) {
283                         if (content == mPresentationContent) {
284                             closePresentationContent();
285                             if (mPresentationView != null) mPresentationView.cancel();
286                         }
287                     }
288                 });
289
290                 // Start to load the content from the target url.
291                 mPresentationContent.load(targetUrl);
292
293                 // Update the presentation view in order that the content could be presented
294                 // on the preferred display.
295                 updatePresentationView(preferredDisplay);
296             }
297         });
298     }
299
300     @Override
301     public String onSyncMessage(int instanceId, String message) {
302         if (message.equals(CMD_QUERY_DISPLAY_AVAILABILITY)) {
303             return mAvailableDisplayCount != 0 ? "true" : "false";
304         } else {
305             Log.e(TAG, "Unexpected sync message received: " + message);
306             return "";
307         }
308     }
309
310     public void onResume() {
311         Display[] displays = mDisplayManager.getPresentationDisplays();
312
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();
319         }
320
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
323         // count.
324         if (displays.length > 0 && mAvailableDisplayCount == 0) {
325             notifyAvailabilityChanged(true);
326             mAvailableDisplayCount = displays.length;
327         }
328
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;
333         }
334
335         if (mPresentationContent != null) {
336             mPresentationContent.onResume();
337         }
338
339         updatePresentationView(getPreferredDisplay());
340
341         // Register the listener to display manager.
342         mDisplayManager.registerDisplayListener(mDisplayListener);
343     }
344
345     private void updatePresentationView(Display preferredDisplay) {
346         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 ||
347                 preferredDisplay == null) {
348             return;
349         }
350
351         // No presentation content is ready for use.
352         if (mPresentationView == null && mPresentationContent == null) {
353             return;
354         }
355
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();
360         }
361
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());
369
370             mPresentationView = PresentationView.createInstance(mContext, preferredDisplay);
371             mPresentationView.setContentView(mPresentationContent.getContentView());
372             mPresentationView.setPresentationListener(new PresentationView.PresentationListener() {
373                 @Override
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
377                     // and JS execution.
378                     if (view == mPresentationView) {
379                         if (mPresentationContent != null) mPresentationContent.onPause();
380                         mPresentationView = null;
381                     }
382                 }
383
384                 @Override
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();
390                     }
391                 }
392             });
393         }
394
395         mPresentationView.show();
396     }
397
398     private void dismissPresentationView() {
399         if (mPresentationView == null) return;
400
401         mPresentationView.dismiss();
402         mPresentationView = null;
403     }
404
405     private void closePresentationContent() {
406         if (mPresentationContent == null) return;
407
408         mPresentationContent.close();
409         mPresentationContent = null;
410     }
411
412     @Override
413     public void onActivityStateChange(Activity activity, int newState) {
414         switch (newState) {
415             case ActivityState.RESUMED:
416                 onResume();
417                 break;
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);
423                 break;
424             case ActivityState.DESTROYED:
425                 // close the presentation content if have.
426                 closePresentationContent();
427                 break;
428             default:
429                 break;
430         }
431     }
432 }