Android Open Source - phonegap-lite-android Callback Server






From Project

Back to project page phonegap-lite-android.

License

The source code is released under:

MIT License

If you think the Android project phonegap-lite-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

/*
 * PhoneGap is available under *either* the terms of the modified BSD license *or* the
 * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
 * //w w  w.  j ava  2  s  . c  o m
 * Copyright (c) 2005-2010, Nitobi Software Inc.
 * Copyright (c) 2010, IBM Corporation
 */
package com.phonegap;

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.net.URLEncoder;
import java.util.LinkedList;

import android.util.Log;

/**
 * This class provides a way for Java to run JavaScript in the web page that has loaded PhoneGap.
 * 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 {
  
  private static final String LOG_TAG = "CallbackServer";

  /**
   * 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() {
    //System.out.println("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 PhoneGap 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 PhoneGap 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 PhoneGap 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() {
    //System.out.println("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;
      ServerSocket waitSocket = new ServerSocket(0);
      this.port = waitSocket.getLocalPort();
      //System.out.println("CallbackServer -- using port " +this.port);
      this.token = java.util.UUID.randomUUID().toString();
      //System.out.println("CallbackServer -- using token "+this.token);

       while (this.active) {
         //System.out.println("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 = "";
         //System.out.println("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))) {
               //System.out.println("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
                     //System.out.println("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) {
                   //System.out.println("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 {
                   //System.out.println("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 ";
           }
           //System.out.println("CallbackServer: response="+response);
           //System.out.println("CallbackServer: closing output");
           output.writeBytes(response);
           output.flush();
         }
         output.close();
         xhrReader.close();
       }
     } catch (IOException e) {
       e.printStackTrace();
     }
     this.active = false;
     //System.out.println("CallbackServer.startServer() - EXIT");
  }
    
  /**
   * Stop server.  
   * This stops the thread that the server is running on.
   */
  public void stopServer() {
    //System.out.println("CallbackServer.stopServer()");
    if (this.active) {
      this.active = false;

      // 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() {
    int size = this.javascript.size();
    //System.out.println("getSize() = " + size);
    return size;
  }
  
  /**
   * Get the next JavaScript statement and remove from list.
   *  
   * @return String
   */
  public String getJavascript() {
    if (this.javascript.size() == 0) {
      return null;
    }
    String statement = this.javascript.remove(0);
    //System.out.println("CallbackServer.getJavascript() = " + statement);
    if (this.javascript.size() == 0) {
      synchronized (this) { 
        this.empty = true;
      }
    }
    return statement;
  }
  
  /**
   * Add a JavaScript statement to the list.
   * 
   * @param statement
   */
  public void sendJavascript(String statement) {
    //System.out.println("CallbackServer.sendJavascript("+statement+")");
    this.javascript.add(statement);
    synchronized (this) { 
      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

__ID__.Activity.java
com.phonegap.App.java
com.phonegap.CallbackServer.java
com.phonegap.Device.java
com.phonegap.DroidGap.java
com.phonegap.HttpHandler.java
com.phonegap.StandAlone.java
com.phonegap.TempListener.java
com.phonegap.WebViewReflect.java
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
com.phonegap.file.EncodingException.java
com.phonegap.plugin.sqlitePlugin.SQLitePlugin.java