Android Open Source - incubator-cordova-android Callback Server






From Project

Back to project page incubator-cordova-android.

License

The source code is released under:

Apache License

If you think the Android project incubator-cordova-android listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF licenses this file
       to you under the Apache License, Version 2.0 (the
       "License"); you may not use this file except in compliance
       with the License.  You may obtain a copy of the License at
/* ww w .  j  a v a 2  s .  c  om*/
         http://www.apache.org/licenses/LICENSE-2.0

       Unless required by applicable law or agreed to in writing,
       software distributed under the License is distributed on an
       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
       KIND, either express or implied.  See the License for the
       specific language governing permissions and limitations
       under the License.
*/
package org.apache.cordova;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.LinkedList;

/**
 * This class provides a way for Java to run JavaScript in the web page that has loaded Cordova.
 * The CallbackServer class implements an XHR server and a polling server with a list of JavaScript
 * statements that are to be executed on the web page.
 *
 * The process flow for XHR is:
 * 1. JavaScript makes an async XHR call.
 * 2. The server holds the connection open until data is available.
 * 3. The server writes the data to the client and closes the connection.
 * 4. The server immediately starts listening for the next XHR call.
 * 5. The client receives this XHR response, processes it.
 * 6. The client sends a new async XHR request.
 *
 * The CallbackServer class requires the following permission in Android manifest file
 *     <uses-permission android:name="android.permission.INTERNET" />
 *
 * If the device has a proxy set, then XHR cannot be used, so polling must be used instead.
 * This can be determined by the client by calling CallbackServer.usePolling().
 *
 * The process flow for polling is:
 * 1. The client calls CallbackServer.getJavascript() to retrieve next statement.
 * 2. If statement available, then client processes it.
 * 3. The client repeats #1 in loop.
 */
public class CallbackServer implements Runnable {

    @SuppressWarnings("unused")
    private static final String LOG_TAG = "CallbackServer";
    
    private ServerSocket waitSocket;
    /**
     * The list of JavaScript statements to be sent to JavaScript.
     */
    private LinkedList<String> javascript;

    /**
     * The port to listen on.
     */
    private int port;

    /**
     * The server thread.
     */
    private Thread serverThread;

    /**
     * Indicates the server is running.
     */
    private boolean active;

    /**
     * Indicates that the JavaScript statements list is empty
     */
    private boolean empty;

    /**
     * Indicates that polling should be used instead of XHR.
     */
    private boolean usePolling = true;

    /**
     * Security token to prevent other apps from accessing this callback server via XHR
     */
    private String token;

    /**
     * Constructor.
     */
    public CallbackServer() {
        //Log.d(LOG_TAG, "CallbackServer()");
        this.active = false;
        this.empty = true;
        this.port = 0;
        this.javascript = new LinkedList<String>();
    }

    /**
     * Init callback server and start XHR if running local app.
    *
     * If Cordova app is loaded from file://, then we can use XHR
     * otherwise we have to use polling due to cross-domain security restrictions.
     *
     * @param url      The URL of the Cordova app being loaded
     */
    public void init(String url) {
        //System.out.println("CallbackServer.start("+url+")");
        this.active = false;
        this.empty = true;
        this.port = 0;
        this.javascript = new LinkedList<String>();

        // Determine if XHR or polling is to be used
        if ((url != null) && !url.startsWith("file://")) {
            this.usePolling = true;
            this.stopServer();
        }
        else if (android.net.Proxy.getDefaultHost() != null) {
            this.usePolling = true;
            this.stopServer();
        }
        else {
            this.usePolling = false;
            this.startServer();
        }
    }

    /**
     * Re-init when loading a new HTML page into webview.
     *
     * @param url           The URL of the Cordova app being loaded
     */
    public void reinit(String url) {
        this.stopServer();
        this.init(url);
    }

    /**
     * Return if polling is being used instead of XHR.
     * @return
     */
    public boolean usePolling() {
        return this.usePolling;
    }

    /**
     * Get the port that this server is running on.
     * @return
     */
    public int getPort() {
        return this.port;
    }

    /**
     * Get the security token that this server requires when calling getJavascript().
     * @return
     */
    public String getToken() {
        return this.token;
    }

    /**
     * Start the server on a new thread.
     */
    public void startServer() {
        //Log.d(LOG_TAG, "CallbackServer.startServer()");
        this.active = false;

        // Start server on new thread
        this.serverThread = new Thread(this);
        this.serverThread.start();
    }

    /**
     * Restart the server on a new thread.
     */
    public void restartServer() {

        // Stop server
        this.stopServer();

        // Start server again
        this.startServer();
    }

    /**
     * Start running the server.  
     * This is called automatically when the server thread is started.
     */
    public void run() {

        // Start server
        try {
            this.active = true;
            String request;
            waitSocket = new ServerSocket(0);
            this.port = waitSocket.getLocalPort();
            //Log.d(LOG_TAG, "CallbackServer -- using port " +this.port);
            this.token = java.util.UUID.randomUUID().toString();
            //Log.d(LOG_TAG, "CallbackServer -- using token "+this.token);

            while (this.active) {
                //Log.d(LOG_TAG, "CallbackServer: Waiting for data on socket");
                Socket connection = waitSocket.accept();
                BufferedReader xhrReader = new BufferedReader(new InputStreamReader(connection.getInputStream()), 40);
                DataOutputStream output = new DataOutputStream(connection.getOutputStream());
                request = xhrReader.readLine();
                String response = "";
                //Log.d(LOG_TAG, "CallbackServerRequest="+request);
                if (this.active && (request != null)) {
                    if (request.contains("GET")) {

                        // Get requested file
                        String[] requestParts = request.split(" ");

                        // Must have security token
                        if ((requestParts.length == 3) && (requestParts[1].substring(1).equals(this.token))) {
                            //Log.d(LOG_TAG, "CallbackServer -- Processing GET request");

                            // Wait until there is some data to send, or send empty data every 10 sec 
                            // to prevent XHR timeout on the client 
                            synchronized (this) {
                                while (this.empty) {
                                    try {
                                        this.wait(10000); // prevent timeout from happening
                                        //Log.d(LOG_TAG, "CallbackServer>>> break <<<");
                                        break;
                                    } catch (Exception e) {
                                    }
                                }
                            }

                            // If server is still running
                            if (this.active) {

                                // If no data, then send 404 back to client before it times out
                                if (this.empty) {
                                    //Log.d(LOG_TAG, "CallbackServer -- sending data 0");
                                    response = "HTTP/1.1 404 NO DATA\r\n\r\n "; // need to send content otherwise some Android devices fail, so send space
                                }
                                else {
                                    //Log.d(LOG_TAG, "CallbackServer -- sending item");
                                    response = "HTTP/1.1 200 OK\r\n\r\n";
                                    String js = this.getJavascript();
                                    if (js != null) {
                                        response += encode(js, "UTF-8");
                                    }
                                }
                            }
                            else {
                                response = "HTTP/1.1 503 Service Unavailable\r\n\r\n ";
                            }
                        }
                        else {
                            response = "HTTP/1.1 403 Forbidden\r\n\r\n ";
                        }
                    }
                    else {
                        response = "HTTP/1.1 400 Bad Request\r\n\r\n ";
                    }
                    //Log.d(LOG_TAG, "CallbackServer: response="+response);
                    //Log.d(LOG_TAG, "CallbackServer: closing output");
                    output.writeBytes(response);
                    output.flush();
                }
                output.close();
                xhrReader.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        this.active = false;
        //Log.d(LOG_TAG, "CallbackServer.startServer() - EXIT");
    }

    /**
     * Stop server.  
     * This stops the thread that the server is running on.
     */
    public void stopServer() {
        //Log.d(LOG_TAG, "CallbackServer.stopServer()");
        if (this.active) {
            this.active = false;
            
            try { waitSocket.close(); } catch (IOException ignore) {}
            
            // Break out of server wait
            synchronized (this) {
                this.notify();
            }
        }
    }

    /**
     * Destroy
     */
    public void destroy() {
        this.stopServer();
    }

    /**
     * Get the number of JavaScript statements.
     * 
     * @return int
     */
    public int getSize() {
        synchronized (this) {
            int size = this.javascript.size();
            return size;
        }
    }

    /**
     * Get the next JavaScript statement and remove from list.
     *  
     * @return String
     */
    public String getJavascript() {
        synchronized (this) {
            if (this.javascript.size() == 0) {
                return null;
            }
            String statement = this.javascript.remove(0);
            if (this.javascript.size() == 0) {
                this.empty = true;
            }
            return statement;
        }
    }

    /**
     * Add a JavaScript statement to the list.
     * 
     * @param statement
     */
    public void sendJavascript(String statement) {
        synchronized (this) {
            this.javascript.add(statement);
            this.empty = false;
            this.notify();
        }
    }

    /* The Following code has been modified from original implementation of URLEncoder */

    /* start */

    /*
     *  Licensed to the Apache Software Foundation (ASF) under one or more
     *  contributor license agreements.  See the NOTICE file distributed with
     *  this work for additional information regarding copyright ownership.
     *  The ASF licenses this file to You under the Apache License, Version 2.0
     *  (the "License"); you may not use this file except in compliance with
     *  the License.  You may obtain a copy of the License at
     *
     *     http://www.apache.org/licenses/LICENSE-2.0
     *
     *  Unless required by applicable law or agreed to in writing, software
     *  distributed under the License is distributed on an "AS IS" BASIS,
     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     *  See the License for the specific language governing permissions and
     *  limitations under the License.
     */
    static final String digits = "0123456789ABCDEF";

    /**
     * This will encode the return value to JavaScript.  We revert the encoding for
     * common characters that don't require encoding to reduce the size of the string
     * being passed to JavaScript.
     *
     * @param s to be encoded
     * @param enc encoding type
     * @return encoded string
     */
    public static String encode(String s, String enc) throws UnsupportedEncodingException {
        if (s == null || enc == null) {
            throw new NullPointerException();
        }
        // check for UnsupportedEncodingException
        "".getBytes(enc);

        // Guess a bit bigger for encoded form
        StringBuilder buf = new StringBuilder(s.length() + 16);
        int start = -1;
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
                    || (ch >= '0' && ch <= '9')
                    || " .-*_'(),<>=?@[]{}:~\"\\/;!".indexOf(ch) > -1) {
                if (start >= 0) {
                    convert(s.substring(start, i), buf, enc);
                    start = -1;
                }
                if (ch != ' ') {
                    buf.append(ch);
                } else {
                    buf.append(' ');
                }
            } else {
                if (start < 0) {
                    start = i;
                }
            }
        }
        if (start >= 0) {
            convert(s.substring(start, s.length()), buf, enc);
        }
        return buf.toString();
    }

    private static void convert(String s, StringBuilder buf, String enc) throws UnsupportedEncodingException {
        byte[] bytes = s.getBytes(enc);
        for (int j = 0; j < bytes.length; j++) {
            buf.append('%');
            buf.append(digits.charAt((bytes[j] & 0xf0) >> 4));
            buf.append(digits.charAt(bytes[j] & 0xf));
        }
    }

    /* end */
}




Java Source Code List

com.phonegap.api.IPlugin.java
com.phonegap.api.LOG.java
com.phonegap.api.PhonegapActivity.java
com.phonegap.api.PluginManager.java
com.phonegap.api.PluginResult.java
com.phonegap.api.Plugin.java
org.apache.cordova.AccelListener.java
org.apache.cordova.App.java
org.apache.cordova.AudioHandler.java
org.apache.cordova.AudioPlayer.java
org.apache.cordova.AuthenticationToken.java
org.apache.cordova.BatteryListener.java
org.apache.cordova.CallbackServer.java
org.apache.cordova.CameraLauncher.java
org.apache.cordova.Capture.java
org.apache.cordova.CompassListener.java
org.apache.cordova.ContactAccessorSdk5.java
org.apache.cordova.ContactAccessor.java
org.apache.cordova.ContactManager.java
org.apache.cordova.CordovaChromeClient.java
org.apache.cordova.CordovaLocationListener.java
org.apache.cordova.CordovaWebViewClient.java
org.apache.cordova.CordovaWebView.java
org.apache.cordova.Device.java
org.apache.cordova.DirectoryManager.java
org.apache.cordova.DroidGap.java
org.apache.cordova.ExifHelper.java
org.apache.cordova.FileTransfer.java
org.apache.cordova.FileUploadResult.java
org.apache.cordova.FileUtils.java
org.apache.cordova.GPSListener.java
org.apache.cordova.GeoBroker.java
org.apache.cordova.HttpHandler.java
org.apache.cordova.LinearLayoutSoftKeyboardDetect.java
org.apache.cordova.NetworkListener.java
org.apache.cordova.NetworkManager.java
org.apache.cordova.Notification.java
org.apache.cordova.SplashScreen.java
org.apache.cordova.StandAlone.java
org.apache.cordova.Storage.java
org.apache.cordova.TempListener.java
org.apache.cordova.api.CordovaInterface.java
org.apache.cordova.api.IPlugin.java
org.apache.cordova.api.LOG.java
org.apache.cordova.api.LegacyContext.java
org.apache.cordova.api.PluginEntry.java
org.apache.cordova.api.PluginManager.java
org.apache.cordova.api.PluginResult.java
org.apache.cordova.api.Plugin.java
org.apache.cordova.file.EncodingException.java
org.apache.cordova.file.FileExistsException.java
org.apache.cordova.file.InvalidModificationException.java
org.apache.cordova.file.NoModificationAllowedException.java
org.apache.cordova.file.TypeMismatchException.java