com.phonegap.PhoneGap.java Source code

Java tutorial

Introduction

Here is the source code for com.phonegap.PhoneGap.java

Source

/**
 * The MIT License
 * -------------------------------------------------------------
 * Copyright (c) 2008, Rob Ellis, Brock Whitten, Brian Leroux, Joe Bowser, Dave Johnson, Nitobi
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.phonegap;

import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;

import javax.microedition.io.HttpConnection;

import net.rim.device.api.browser.field.BrowserContent;
import net.rim.device.api.browser.field.BrowserContentChangedEvent;
import net.rim.device.api.browser.field.Event;
import net.rim.device.api.browser.field.RedirectEvent;
import net.rim.device.api.browser.field.RenderingApplication;
import net.rim.device.api.browser.field.RenderingException;
import net.rim.device.api.browser.field.RenderingOptions;
import net.rim.device.api.browser.field.RenderingSession;
import net.rim.device.api.browser.field.RequestedResource;
import net.rim.device.api.browser.field.SetHttpCookieEvent;
import net.rim.device.api.browser.field.UrlRequestedEvent;
import net.rim.device.api.io.http.HttpHeaders;
import net.rim.device.api.system.Application;
import net.rim.device.api.system.Characters;
import net.rim.device.api.system.Display;
import net.rim.device.api.system.EncodedImage;
import net.rim.device.api.system.KeyListener;
import net.rim.device.api.ui.Field;
import net.rim.device.api.ui.UiApplication;
import net.rim.device.api.ui.component.BitmapField;
import net.rim.device.api.ui.component.Status;
import net.rim.device.api.ui.container.MainScreen;

import com.phonegap.api.CommandManager;
import com.phonegap.api.CommandResult;
import com.phonegap.io.ConnectionManager;
import com.phonegap.io.PrimaryResourceFetchThread;
import com.phonegap.io.SecondaryResourceFetchThread;

import org.json.me.*;

/**
 * Bridges HTML/JS/CSS to a native Blackberry application.
 * @author Jose Noheda
 * @author Fil Maj
 * @author Dave Johnson
 */
public class PhoneGap extends UiApplication implements RenderingApplication {
    public static final String APPLICATION_UID = "%PLACEHOLDER%"; // Gets replaced at build-time with the app name; used as a key to reference offline storage. CAVEAT: if you change your app name, you will lose any existing offline data.
    public static final String PHONEGAP_PROTOCOL = "PhoneGap=";
    private static final String DEFAULT_INITIAL_URL = "data:///www/index.html";
    private static final String LOADING_IMAGE = "www/Default.png";
    private static final String REFERER = "referer";
    private static final String REDIRECT_MSG = "You are being redirected to a different page...";
    public Vector pendingResponses = new Vector();
    private CommandManager commandManager;
    private RenderingSession _renderingSession;
    public HttpConnection _currentConnection;
    private MainScreen _mainScreen;
    private EncodedImage loadingImage;
    private BitmapField loadingField = new BitmapField();
    private MainScreen loadingScreen = new MainScreen();
    private Timer refreshTimer;

    /**
     * Launches the application. Accepts up to one parameter, a URL to the index page. 
     */
    public static void main(String[] args) {
        PhoneGap bridge = args.length > 0 ? new PhoneGap(args[0]) : new PhoneGap();
        bridge.enterEventDispatcher();
    }

    public PhoneGap() {
        init(DEFAULT_INITIAL_URL);
    }

    /**
     * Launches the application with a custom index page.
     *
     * @param url a http:// or data:// string
     */
    public PhoneGap(final String url) {
        if ((url != null) && (url.trim().length() > 0)) {
            init(url);
        } else {
            init(DEFAULT_INITIAL_URL);
        }
    }

    private void init(final String url) {
        commandManager = new CommandManager(this);
        _mainScreen = new MainScreen();
        pushScreen(_mainScreen);
        // Add loading screen and display ASAP
        loadingImage = EncodedImage.getEncodedImageResource(LOADING_IMAGE);
        if (loadingImage != null) {
            // If a loading image exists, add it to the loading field and push it onto the screen stack.
            loadingField.setImage(loadingImage);
            loadingScreen.add(loadingField);
            pushScreen(loadingScreen);
        }
        _mainScreen.addKeyListener(new PhoneGapKeyListener(this));

        // Set up the browser/renderer.
        _renderingSession = RenderingSession.getNewInstance();
        _renderingSession.getRenderingOptions().setProperty(RenderingOptions.CORE_OPTIONS_GUID,
                RenderingOptions.JAVASCRIPT_ENABLED, true);
        _renderingSession.getRenderingOptions().setProperty(RenderingOptions.CORE_OPTIONS_GUID,
                RenderingOptions.JAVASCRIPT_LOCATION_ENABLED, true);
        // Enable nice-looking BlackBerry browser field.
        _renderingSession.getRenderingOptions().setProperty(RenderingOptions.CORE_OPTIONS_GUID, 17000, true);
        PrimaryResourceFetchThread thread = new PrimaryResourceFetchThread(url, null, null, null, this);
        thread.start();
        refreshTimer = new Timer();
        refreshTimer.scheduleAtFixedRate(new TimerRefresh(), 500, 500);
    }

    public Object eventOccurred(final Event event) {
        int eventId = event.getUID();
        switch (eventId) {
        case Event.EVENT_REDIRECT: {
            RedirectEvent e = (RedirectEvent) event;
            String referrer = e.getSourceURL();
            switch (e.getType()) {
            case RedirectEvent.TYPE_SINGLE_FRAME_REDIRECT:
                // Show redirect message.
                Application.getApplication().invokeAndWait(new Runnable() {
                    public void run() {
                        Status.show(REDIRECT_MSG);
                    }
                });
                break;

            case RedirectEvent.TYPE_JAVASCRIPT:
                break;

            case RedirectEvent.TYPE_META:
                // MSIE and Mozilla don't send a Referer for META Refresh.
                referrer = null;
                break;

            case RedirectEvent.TYPE_300_REDIRECT:
                // MSIE, Mozilla, and Opera all send the original request's Referer as the Referer for the new request.
                Object eventSource = e.getSource();
                if (eventSource instanceof HttpConnection) {
                    referrer = ((HttpConnection) eventSource).getRequestProperty(REFERER);
                }
                eventSource = null;
                break;
            }
            this.showLoadingScreen();
            // Create the request, populate header with referrer and fire off the request.
            HttpHeaders requestHeaders = new HttpHeaders();
            requestHeaders.setProperty(REFERER, referrer);
            PrimaryResourceFetchThread thread = new PrimaryResourceFetchThread(e.getLocation(), requestHeaders,
                    null, event, this);
            thread.start();
            e = null;
            referrer = null;
            requestHeaders = null;
            break;
        }
        case Event.EVENT_URL_REQUESTED: {
            this.showLoadingScreen();
            UrlRequestedEvent urlRequestedEvent = (UrlRequestedEvent) event;
            String url = urlRequestedEvent.getURL();
            HttpHeaders header = urlRequestedEvent.getHeaders();
            PrimaryResourceFetchThread thread = new PrimaryResourceFetchThread(url, header,
                    urlRequestedEvent.getPostData(), event, this);
            thread.start();
            urlRequestedEvent = null;
            url = null;
            header = null;
            break;
        }
        case Event.EVENT_BROWSER_CONTENT_CHANGED: {
            // Browser field title might have changed update title.
            BrowserContentChangedEvent browserContentChangedEvent = (BrowserContentChangedEvent) event;
            if (browserContentChangedEvent.getSource() instanceof BrowserContent) {
                BrowserContent browserField = (BrowserContent) browserContentChangedEvent.getSource();
                String newTitle = browserField.getTitle();
                if (newTitle != null) {
                    synchronized (getAppEventLock()) {
                        _mainScreen.setTitle(newTitle);
                    }
                }
                browserField = null;
                newTitle = null;
            }
            browserContentChangedEvent = null;
            break;
        }
        case Event.EVENT_CLOSE:
            // TODO: close the application
            break;

        case Event.EVENT_SET_HEADER: // No cache support.
        case Event.EVENT_SET_HTTP_COOKIE:
            String cookie = ((SetHttpCookieEvent) event).getCookie();

            // Support the old protocol still
            if (cookie.startsWith(PHONEGAP_PROTOCOL)) {
                String response = commandManager.processInstruction(cookie);
                if ((response != null) && (response.trim().length() > 0)) {
                    pendingResponses.addElement(response);
                }
                response = null;
            } else {
                // cookie = {clazz:'com.example.Foo', action:'bar', callbackId:'1', args:{...}, async:true};
                try {
                    JSONObject o = new JSONObject(cookie);

                    JSONArray args = o.getJSONArray("args");
                    String clazz = o.getString("clazz");
                    String action = o.getString("action");
                    String callbackId = o.getString("callbackId");
                    boolean async = o.getBoolean("async");

                    String response = commandManager.exec(clazz, action, callbackId, args, async);
                    if ((response != null) && (response.trim().length() > 0)) {
                        pendingResponses.addElement(response);
                    }
                    response = null;
                } catch (JSONException e) {
                    pendingResponses
                            .addElement(new CommandResult(CommandResult.Status.JSON_EXCEPTION, "").toErrorString());
                }
            }
            cookie = null;
            break;
        case Event.EVENT_HISTORY: // TODO: No history support.. but we added our own history stack implementation in ConnectionManager. Can we hook it up - then we'd have access to window.history :o
        case Event.EVENT_EXECUTING_SCRIPT: // No progress bar is supported.
        case Event.EVENT_FULL_WINDOW: // No full window support.
        case Event.EVENT_STOP: // No stop loading support.
        default:
        }

        return null;
    }

    /**
     * Catch the 'get' cookie event, aggregate PhoneGap API responses that haven't been flushed and return.
     **/
    public String getHTTPCookie(String url) {
        StringBuffer responseCode = new StringBuffer();
        synchronized (pendingResponses) {
            for (int index = 0; index < pendingResponses.size(); index++)
                responseCode.append(pendingResponses.elementAt(index));
            pendingResponses.removeAllElements();
        }
        return responseCode.toString();
    }

    public int getAvailableHeight(BrowserContent browserContent) {
        return Display.getHeight();
    }

    public int getAvailableWidth(BrowserContent browserContent) {
        return Display.getWidth();
    }

    public int getHistoryPosition(BrowserContent browserContent) {
        return 0; // TODO: No support... but should try hooking it up to our own implementation of the history stack and see what happens.
    }

    public HttpConnection getResource(RequestedResource resource, BrowserContent referrer) {
        if ((resource != null) && (resource.getUrl() != null) && !resource.isCacheOnly()) {
            String url = resource.getUrl().trim();
            if ((referrer == null) || (ConnectionManager.isInternal(url, resource))) {
                return ConnectionManager.getUnmanagedConnection(url, resource.getRequestHeaders(), null);
            } else {
                SecondaryResourceFetchThread.enqueue(resource, referrer);
            }
            url = null;
        }
        return null;
    }

    public void showLoadingScreen() {
        synchronized (Application.getEventLock()) {
            pushScreen(this.loadingScreen);
        }
    }

    public void hideLoadingScreen() {
        synchronized (Application.getEventLock()) {
            try {
                popScreen(this.loadingScreen);
            } catch (Exception e) {
            }
        }
    }

    /**
     * Processes a new HttpConnection object to instantiate a new browser Field (aka WebView) object, and then resets the screen to the newly-created Field.
     * @param connection
     * @param e
     */
    public void processConnection(HttpConnection connection, Event e) {
        // Cancel previous request.
        if (_currentConnection != null) {
            try {
                _currentConnection.close();
            } catch (IOException e1) {
            }
        }
        // Clear out pending responses.
        synchronized (pendingResponses) {
            pendingResponses.removeAllElements();
        }
        // Cancel any XHRs happening.
        commandManager.stopXHR();
        _currentConnection = connection;
        BrowserContent browserContent = null;
        Field field = null;
        try {
            browserContent = _renderingSession.getBrowserContent(connection, this, e);
            if (browserContent != null) {
                field = browserContent.getDisplayableContent();
                if (field != null) {
                    synchronized (Application.getEventLock()) {
                        // The deleteAll call will remove the loading screen if exists.
                        _mainScreen.deleteAll();
                        _mainScreen.add(field);
                    }
                }
                browserContent.finishLoading();
            }
        } catch (RenderingException re) {
        } finally {
            browserContent = null;
            field = null;
            this.hideLoadingScreen();
            // Manually call the garbage collector to clean up all of leftover objects and free up the nulled object handles.
            System.gc();
        }
    }

    /**
     * Required for implementing RenderingApplication ... but not used. Use invokeLater instead.
     */
    public void invokeRunnable(Runnable runnable) {
        (new Thread(runnable)).start();
    }

    /**
     * This is essentially to make the API more like Android.
     * 
     * @param javascript
     */
    public void loadUrl(String javascript) {
        System.out.println(javascript);
        pendingResponses.addElement(javascript);
    }

    /**
     * An analogous function to String.replaceAll from J2SE, but unavailable on Micro. Courtesy of Jijo from http://www.itgalary.com/forum_posts.asp?TID=871
     * @param _text
     * @return 
     */
    public static final String replace(String _text, String _searchStr, String _replacementStr) {
        StringBuffer sb = new StringBuffer();
        int searchStringPos = _text.indexOf(_searchStr);
        int startPos = 0;
        int searchStringLength = _searchStr.length();
        while (searchStringPos != -1) {
            sb.append(_text.substring(startPos, searchStringPos)).append(_replacementStr);
            startPos = searchStringPos + searchStringLength;
            searchStringPos = _text.indexOf(_searchStr, startPos);
        }
        sb.append(_text.substring(startPos, _text.length()));
        return sb.toString();
    }

    public static final String[] splitString(final String data, final char splitChar, final boolean allowEmpty) {
        Vector v = new Vector();

        int indexStart = 0;
        int indexEnd = data.indexOf(splitChar);
        if (indexEnd != -1) {
            while (indexEnd != -1) {
                String s = data.substring(indexStart, indexEnd);
                if (allowEmpty || s.length() > 0) {
                    v.addElement(s);
                }
                s = null;
                indexStart = indexEnd + 1;
                indexEnd = data.indexOf(splitChar, indexStart);
            }

            if (indexStart != data.length()) {
                // Add the rest of the string
                String s = data.substring(indexStart);
                if (allowEmpty || s.length() > 0) {
                    v.addElement(s);
                }
                s = null;
            }
        } else {
            if (allowEmpty || data.length() > 0) {
                v.addElement(data);
            }
        }
        String[] result = new String[v.size()];
        v.copyInto(result);
        v = null;
        return result;
    }

    private class TimerRefresh extends TimerTask {
        public void run() {
            UiApplication.getUiApplication().invokeLater(new Runnable() {
                public void run() {
                    int numFields = _mainScreen.getFieldCount();
                    for (int i = 0; i < numFields; i++) {
                        Field field = _mainScreen.getField(i);
                        field.getManager().invalidate();
                        field = null;
                    }
                    _mainScreen.doPaint();
                }
            });
        }
    }

    private static class PhoneGapKeyListener implements KeyListener {
        private PhoneGap phoneGap;

        public PhoneGapKeyListener(PhoneGap pg) {
            phoneGap = pg;
        }

        public boolean keyChar(char arg0, int arg1, int arg2) {
            // Catch BlackBerry's back key, pop history URL stack and initiate HTTP request to it.
            if (ConnectionManager.history.size() > 1 && arg0 == Characters.ESCAPE) {
                phoneGap.showLoadingScreen();
                ConnectionManager.history.pop();
                PrimaryResourceFetchThread thread = new PrimaryResourceFetchThread(
                        (String) ConnectionManager.history.pop(), null, null, null, this.phoneGap);
                thread.start();
                return true;
            }
            return false;
        }

        public boolean keyDown(int keycode, int time) {
            return false;
        }

        public boolean keyRepeat(int keycode, int time) {
            return false;
        }

        public boolean keyStatus(int keycode, int time) {
            return false;
        }

        public boolean keyUp(int keycode, int time) {
            return false;
        }
    }
}