diff --git a/framework/assets/js/app.js b/framework/assets/js/app.js index 9565bbe8..06ecac4e 100755 --- a/framework/assets/js/app.js +++ b/framework/assets/js/app.js @@ -24,21 +24,18 @@ App.prototype.clearCache = function() { }; /** - * Load the url into the webview. + * Load the url into the webview or into new browser instance. * * @param url The URL to load * @param props Properties that can be passed in to the activity: * wait: int => wait msec before loading URL * loadingDialog: "Title,Message" => display a native loading dialog - * hideLoadingDialogOnPage: boolean => hide loadingDialog when page loaded instead of when deviceready event occurs. - * loadInWebView: boolean => cause all links on web page to be loaded into existing web view, instead of being loaded into new browser. * loadUrlTimeoutValue: int => time in msec to wait before triggering a timeout error - * errorUrl: URL => URL to load if there's an error loading specified URL with loadUrl(). Should be a local URL such as file:///android_asset/www/error.html"); - * keepRunning: boolean => enable app to keep running in background + * clearHistory: boolean => clear webview history (default=false) + * openExternal: boolean => open in a new browser (default=false) * * Example: - * App app = new App(); - * app.loadUrl("http://server/myapp/index.html", {wait:2000, loadingDialog:"Wait,Loading App", loadUrlTimeoutValue: 60000}); + * navigator.app.loadUrl("http://server/myapp/index.html", {wait:2000, loadingDialog:"Wait,Loading App", loadUrlTimeoutValue: 60000}); */ App.prototype.loadUrl = function(url, props) { PhoneGap.exec(null, null, "App", "loadUrl", [url, props]); diff --git a/framework/assets/js/phonegap.js.base b/framework/assets/js/phonegap.js.base index f8f64025..1e913c9c 100755 --- a/framework/assets/js/phonegap.js.base +++ b/framework/assets/js/phonegap.js.base @@ -23,13 +23,18 @@ if (typeof PhoneGap === "undefined") { * onDestroy Internal event fired when app is being destroyed (User should use window.onunload event, not this one). * * The only PhoneGap events that user code should register for are: - * onDeviceReady - * onResume + * deviceready PhoneGap native code is initialized and PhoneGap APIs can be called from JavaScript + * pause App has moved to background + * resume App has returned to foreground * * Listeners can be registered as: * document.addEventListener("deviceready", myDeviceReadyListener, false); * document.addEventListener("resume", myResumeListener, false); * document.addEventListener("pause", myPauseListener, false); + * + * The DOM lifecycle events should be used for saving and restoring state + * window.onload + * window.onunload */ if (typeof(DeviceInfo) !== 'object') { diff --git a/framework/src/com/phonegap/App.java b/framework/src/com/phonegap/App.java index 28b0b6fe..811f6c1c 100755 --- a/framework/src/com/phonegap/App.java +++ b/framework/src/com/phonegap/App.java @@ -78,18 +78,18 @@ public class App extends Plugin { ((DroidGap)this.ctx).clearCache(); } - /** - * Load the url into the webview. - * - * @param url - * @param props Properties that can be passed in to the DroidGap activity (i.e. loadingDialog, wait, ...) - * @throws JSONException - */ + /** + * Load the url into the webview. + * + * @param url + * @param props Properties that can be passed in to the DroidGap activity (i.e. loadingDialog, wait, ...) + * @throws JSONException + */ public void loadUrl(String url, JSONObject props) throws JSONException { System.out.println("App.loadUrl("+url+","+props+")"); int wait = 0; - boolean usePhoneGap = true; - boolean clearPrev = false; + boolean openExternal = false; + boolean clearHistory = false; // If there are properties, then set them on the Activity HashMap params = new HashMap(); @@ -100,11 +100,11 @@ public class App extends Plugin { if (key.equals("wait")) { wait = props.getInt(key); } - else if (key.equalsIgnoreCase("usephonegap")) { - usePhoneGap = props.getBoolean(key); + else if (key.equalsIgnoreCase("openexternal")) { + openExternal = props.getBoolean(key); } - else if (key.equalsIgnoreCase("clearprev")) { - clearPrev = props.getBoolean(key); + else if (key.equalsIgnoreCase("clearhistory")) { + clearHistory = props.getBoolean(key); } else { Object value = props.get(key); @@ -135,7 +135,7 @@ public class App extends Plugin { e.printStackTrace(); } } - ((DroidGap)this.ctx).showWebPage(url, usePhoneGap, clearPrev, params); + ((DroidGap)this.ctx).showWebPage(url, openExternal, clearHistory, params); } /** @@ -146,12 +146,10 @@ public class App extends Plugin { } /** - * Clear web history in this web view. - * This does not have any effect since each page has its own activity. + * Clear page history for the app. */ public void clearHistory() { ((DroidGap)this.ctx).clearHistory(); - // TODO: Kill previous activities? } /** @@ -159,7 +157,7 @@ public class App extends Plugin { * This is the same as pressing the backbutton on Android device. */ public void backHistory() { - ((DroidGap)this.ctx).endActivity(); + ((DroidGap)this.ctx).backHistory(); } /** @@ -186,7 +184,6 @@ public class App extends Plugin { * Exit the Android application. */ public void exitApp() { - ((DroidGap)this.ctx).setResult(Activity.RESULT_OK); ((DroidGap)this.ctx).endActivity(); } diff --git a/framework/src/com/phonegap/CallbackServer.java b/framework/src/com/phonegap/CallbackServer.java index 2c2a9c84..0c2b1506 100755 --- a/framework/src/com/phonegap/CallbackServer.java +++ b/framework/src/com/phonegap/CallbackServer.java @@ -103,6 +103,10 @@ public class CallbackServer implements Runnable { */ public void init(String url) { //System.out.println("CallbackServer.start("+url+")"); + this.active = false; + this.empty = true; + this.port = 0; + this.javascript = new LinkedList(); // Determine if XHR or polling is to be used if ((url != null) && !url.startsWith("file://")) { @@ -119,6 +123,16 @@ public class CallbackServer implements Runnable { } } + /** + * 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. * diff --git a/framework/src/com/phonegap/DroidGap.java b/framework/src/com/phonegap/DroidGap.java index 09adc9d5..ad2c2983 100755 --- a/framework/src/com/phonegap/DroidGap.java +++ b/framework/src/com/phonegap/DroidGap.java @@ -10,6 +10,7 @@ package com.phonegap; import java.util.HashMap; import java.util.Map.Entry; import java.util.ArrayList; +import java.util.Stack; import java.util.regex.Pattern; import java.util.regex.Matcher; import java.util.Iterator; @@ -105,10 +106,6 @@ import org.xmlpull.v1.XmlPullParserException; * // (String - default=null) * super.setStringProperty("loadingPageDialog", "Loading page..."); * - * // Cause all links on web page to be loaded into existing web view, - * // instead of being loaded into new browser. (Boolean - default=false) - * super.setBooleanProperty("loadInWebView", true); - * * // Load a splash screen image from the resource drawable directory. * // (Integer - default=0) * super.setIntegerProperty("splashscreen", R.drawable.splash); @@ -161,13 +158,20 @@ public class DroidGap extends PhonegapActivity { public CallbackServer callbackServer; protected PluginManager pluginManager; protected boolean cancelLoadUrl = false; - protected boolean clearHistory = false; protected ProgressDialog spinnerDialog = null; // The initial URL for our app // ie http://server/path/index.html#abc?query - private String url; - private boolean firstPage = true; + private String url = null; + private Stack urls = new Stack(); + + // Url was specified from extras (activity was started programmatically) + private String initUrl = null; + + private static int ACTIVITY_STARTING = 0; + private static int ACTIVITY_RUNNING = 1; + private static int ACTIVITY_EXITING = 2; + private int activityState = 0; // 0=starting, 1=running (after 1st resume), 2=shutting down // The base of the initial URL for our app. // Does not include file name. Ends with / @@ -177,7 +181,6 @@ public class DroidGap extends PhonegapActivity { // Plugin to call when activity result is received protected IPlugin activityResultCallback = null; protected boolean activityResultKeepRunning; - private static int PG_REQUEST_CODE = 99; // Flag indicates that a loadUrl timeout occurred private int loadUrlTimeout = 0; @@ -190,10 +193,6 @@ public class DroidGap extends PhonegapActivity { * The variables below are used to cache some of the activity properties. */ - // Flag indicates that a URL navigated to from PhoneGap app should be loaded into same webview - // instead of being loaded into the web browser. - protected boolean loadInWebView = false; - // Draw a splash screen using an image located in the drawable resource directory. // This is not the same as calling super.loadSplashscreen(url) protected int splashscreen = 0; @@ -236,13 +235,11 @@ public class DroidGap extends PhonegapActivity { this.loadConfiguration(); // If url was passed in to intent, then init webview, which will load the url - this.firstPage = true; Bundle bundle = this.getIntent().getExtras(); if (bundle != null) { String url = bundle.getString("url"); if (url != null) { - this.url = url; - this.firstPage = false; + this.initUrl = url; } } // Setup the hardware volume controls to handle volume control @@ -293,10 +290,6 @@ public class DroidGap extends PhonegapActivity { // Enable built-in geolocation WebViewReflect.setGeolocationEnabled(settings, true); - // Create callback server and plugin manager - this.callbackServer = new CallbackServer(); - this.pluginManager = new PluginManager(this.appView, this); - // Add web view but make it invisible while loading URL this.appView.setVisibility(View.INVISIBLE); root.addView(this.appView); @@ -324,24 +317,16 @@ public class DroidGap extends PhonegapActivity { */ private void handleActivityParameters() { - // Init web view if not already done - if (this.appView == null) { - this.init(); - } - // If backgroundColor this.backgroundColor = this.getIntegerProperty("backgroundColor", Color.BLACK); this.root.setBackgroundColor(this.backgroundColor); // If spashscreen this.splashscreen = this.getIntegerProperty("splashscreen", 0); - if (this.firstPage && (this.splashscreen != 0)) { + if ((this.urls.size() == 0) && (this.splashscreen != 0)) { root.setBackgroundResource(this.splashscreen); } - // If loadInWebView - this.loadInWebView = this.getBooleanProperty("loadInWebView", false); - // If loadUrlTimeoutValue int timeout = this.getIntegerProperty("loadUrlTimeoutValue", 0); if (timeout > 0) { @@ -360,12 +345,12 @@ public class DroidGap extends PhonegapActivity { public void loadUrl(String url) { // If first page of app, then set URL to load to be the one passed in - if (this.firstPage) { + if (this.initUrl == null || (this.urls.size() > 0)) { this.loadUrlIntoView(url); } // Otherwise use the URL specified in the activity's extras bundle else { - this.loadUrlIntoView(this.url); + this.loadUrlIntoView(this.initUrl); } } @@ -378,6 +363,11 @@ public class DroidGap extends PhonegapActivity { if (!url.startsWith("javascript:")) { LOG.d(TAG, "DroidGap.loadUrl(%s)", url); } + + // Init web view if not already done + if (this.appView == null) { + this.init(); + } this.url = url; if (this.baseUrl == null) { @@ -401,12 +391,28 @@ public class DroidGap extends PhonegapActivity { // Handle activity parameters me.handleActivityParameters(); - // Initialize callback server - me.callbackServer.init(url); - + // Track URLs loaded instead of using appView history + me.urls.push(url); + me.appView.clearHistory(); + + // Create callback server and plugin manager + if (me.callbackServer == null) { + me.callbackServer = new CallbackServer(); + me.callbackServer.init(url); + } + else { + me.callbackServer.reinit(url); + } + if (me.pluginManager == null) { + me.pluginManager = new PluginManager(me.appView, me); + } + else { + me.pluginManager.reinit(); + } + // If loadingDialog property, then show the App loading dialog for first page of app String loading = null; - if (me.firstPage) { + if (me.urls.size() == 0) { loading = me.getStringProperty("loadingDialog", null); } else { @@ -446,6 +452,7 @@ public class DroidGap extends PhonegapActivity { // If timeout, then stop loading and handle error if (me.loadUrlTimeout == currentLoadUrlTimeout) { me.appView.stopLoading(); + LOG.e(TAG, "DroidGap: TIMEOUT ERROR! - calling webViewClient"); me.webViewClient.onReceivedError(me.appView, -6, "The connection to the server was unsuccessful.", url); } } @@ -467,12 +474,12 @@ public class DroidGap extends PhonegapActivity { public void loadUrl(final String url, int time) { // If first page of app, then set URL to load to be the one passed in - if (this.firstPage) { + if (this.initUrl == null || (this.urls.size() > 0)) { this.loadUrlIntoView(url, time); } // Otherwise use the URL specified in the activity's extras bundle else { - this.loadUrlIntoView(this.url); + this.loadUrlIntoView(this.initUrl); } } @@ -484,10 +491,13 @@ public class DroidGap extends PhonegapActivity { * @param time The number of ms to wait before loading webview */ private void loadUrlIntoView(final String url, final int time) { + + // Clear cancel flag + this.cancelLoadUrl = false; // If not first page of app, then load immediately - if (!this.firstPage) { - this.loadUrl(url); + if (this.urls.size() > 0) { + this.loadUrlIntoView(url); } if (!url.startsWith("javascript:")) { @@ -512,7 +522,7 @@ public class DroidGap extends PhonegapActivity { e.printStackTrace(); } if (!me.cancelLoadUrl) { - me.loadUrl(url); + me.loadUrlIntoView(url); } else{ me.cancelLoadUrl = false; @@ -545,9 +555,22 @@ public class DroidGap extends PhonegapActivity { * Clear web history in this web view. */ public void clearHistory() { - this.clearHistory = true; - if (this.appView != null) { - this.appView.clearHistory(); + this.urls.clear(); + + // Leave current url on history stack + if (this.url != null) { + this.urls.push(this.url); + } + } + + /** + * Go to previous page in history. (We manage our own history) + */ + public void backHistory() { + if (this.urls.size() > 1) { + this.urls.pop(); // Pop current url + String url = this.urls.pop(); // Pop prev url that we want to load + this.loadUrl(url); } } @@ -684,6 +707,12 @@ public class DroidGap extends PhonegapActivity { */ protected void onPause() { super.onPause(); + + // Don't process pause if shutting down, since onDestroy() will be called + if (this.activityState == ACTIVITY_EXITING) { + return; + } + if (this.appView == null) { return; } @@ -719,6 +748,12 @@ public class DroidGap extends PhonegapActivity { */ protected void onResume() { super.onResume(); + + if (this.activityState == ACTIVITY_STARTING) { + this.activityState = ACTIVITY_RUNNING; + return; + } + if (this.appView == null) { return; } @@ -752,8 +787,6 @@ public class DroidGap extends PhonegapActivity { if (this.appView != null) { - // Make sure pause event is sent if onPause hasn't been called before onDestroy - this.appView.loadUrl("javascript:try{PhoneGap.onPause.fire();}catch(e){};"); // Send destroy event to JavaScript this.appView.loadUrl("javascript:try{PhoneGap.onDestroy.fire();}catch(e){};"); @@ -790,62 +823,60 @@ public class DroidGap extends PhonegapActivity { } /** - * Display a new browser with the specified URL. + * Load the specified URL in the PhoneGap webview or a new browser instance. * - * NOTE: If usePhoneGap is set, only trusted PhoneGap URLs should be loaded, - * since any PhoneGap API can be called by the loaded HTML page. + * NOTE: If openExternal is false, only URLs listed in whitelist can be loaded. * * @param url The url to load. - * @param usePhoneGap Load url in PhoneGap webview. - * @param clearPrev Clear the activity stack, so new app becomes top of stack + * @param openExternal Load url in browser instead of PhoneGap webview. + * @param clearHistory Clear the history stack, so new page becomes top of history * @param params DroidGap parameters for new app - * @throws android.content.ActivityNotFoundException */ - public void showWebPage(String url, boolean usePhoneGap, boolean clearPrev, HashMap params) throws android.content.ActivityNotFoundException { - Intent intent = null; - if (usePhoneGap) { - try { - intent = new Intent().setClass(this, Class.forName(this.getComponentName().getClassName())); - intent.putExtra("url", url); - - // Add parameters - if (params != null) { - java.util.Set> s = params.entrySet(); - java.util.Iterator> it = s.iterator(); - while(it.hasNext()) { - Entry entry = it.next(); - String key = entry.getKey(); - Object value = entry.getValue(); - if (value == null) { - } - else if (value.getClass().equals(String.class)) { - intent.putExtra(key, (String)value); - } - else if (value.getClass().equals(Boolean.class)) { - intent.putExtra(key, (Boolean)value); - } - else if (value.getClass().equals(Integer.class)) { - intent.putExtra(key, (Integer)value); - } - } - } - super.startActivityForResult(intent, PG_REQUEST_CODE); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(url)); - this.startActivity(intent); - } - } - else { - intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(url)); - this.startActivity(intent); + public void showWebPage(String url, boolean openExternal, boolean clearHistory, HashMap params) { //throws android.content.ActivityNotFoundException { + LOG.d(TAG, "showWebPage(%s, %b, %b, HashMap", url, openExternal, clearHistory); + + // If clearing history + if (clearHistory) { + this.clearHistory(); } - // Finish current activity - if (clearPrev) { - this.endActivity(); + // If loading into our webview + if (!openExternal) { + + // Make sure url is in whitelist + if (url.startsWith("file://") || url.indexOf(this.baseUrl) == 0 || isUrlWhiteListed(url)) { + // TODO: What about params? + + // Clear out current url from history, since it will be replacing it + if (clearHistory) { + this.urls.clear(); + } + + // Load new URL + this.loadUrl(url); + } + // Load in default viewer if not + else { + LOG.w(TAG, "showWebPage: Cannot load URL into webview since it is not in white list. Loading into browser instead. (URL="+url+")"); + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + this.startActivity(intent); + } catch (android.content.ActivityNotFoundException e) { + LOG.e(TAG, "Error loading url "+url, e); + } + } + } + + // Load in default view intent + else { + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + this.startActivity(intent); + } catch (android.content.ActivityNotFoundException e) { + LOG.e(TAG, "Error loading url "+url, e); + } } } @@ -1231,14 +1262,8 @@ public class DroidGap extends PhonegapActivity { // If our app or file:, then load into a new phonegap webview container by starting a new instance of our activity. // Our app continues to run. When BACK is pressed, our app is redisplayed. - if (this.ctx.loadInWebView || url.startsWith("file://") || url.indexOf(this.ctx.baseUrl) == 0 || isUrlWhiteListed(url)) { - try { - // Init parameters to new DroidGap activity and propagate existing parameters - HashMap params = new HashMap(); - this.ctx.showWebPage(url, true, false, params); - } catch (android.content.ActivityNotFoundException e) { - LOG.e(TAG, "Error loading url into DroidGap - "+url, e); - } + if (url.startsWith("file://") || url.indexOf(this.ctx.baseUrl) == 0 || isUrlWhiteListed(url)) { + this.ctx.loadUrl(url); } // If not our application, let default viewer handle @@ -1255,6 +1280,14 @@ public class DroidGap extends PhonegapActivity { return true; } + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + + // Clear history so history.back() doesn't do anything. + // So we can reinit() native side CallbackServer & PluginManager. + view.clearHistory(); + } + /** * Notify the host application that a page has finished loading. * @@ -1294,11 +1327,6 @@ public class DroidGap extends PhonegapActivity { t.start(); } - // Clear history, so that previous screen isn't there when Back button is pressed - if (this.ctx.clearHistory) { - this.ctx.clearHistory = false; - this.ctx.appView.clearHistory(); - } // Shutdown if blank loaded if (url.equals("about:blank")) { @@ -1386,8 +1414,8 @@ public class DroidGap extends PhonegapActivity { else { // Go to previous page in webview if it is possible to go back - if (this.appView.canGoBack()) { - this.appView.goBack(); + if (this.urls.size() > 1) { + this.backHistory(); return true; } @@ -1463,17 +1491,6 @@ public class DroidGap extends PhonegapActivity { */ protected void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); - - // If a subsequent DroidGap activity is returning - if (requestCode == PG_REQUEST_CODE) { - // If terminating app, then shut down this activity too - if (resultCode == Activity.RESULT_OK) { - this.setResult(Activity.RESULT_OK); - this.endActivity(); - } - return; - } - IPlugin callback = this.activityResultCallback; if (callback != null) { callback.onActivityResult(requestCode, resultCode, intent); diff --git a/framework/src/com/phonegap/api/PluginManager.java b/framework/src/com/phonegap/api/PluginManager.java index a74822f3..11cabfbc 100755 --- a/framework/src/com/phonegap/api/PluginManager.java +++ b/framework/src/com/phonegap/api/PluginManager.java @@ -51,6 +51,17 @@ public final class PluginManager { this.loadPlugins(); } + /** + * Re-init when loading a new HTML page into webview. + */ + public void reinit() { + + // Stop plugins on current HTML page and discard + this.onPause(false); + this.onDestroy(); + this.plugins = new HashMap(); + } + /** * Load plugins from res/xml/plugins.xml */