From bd7ed19b525ef1740fce3e43e7e11dd6ee9e1a2c Mon Sep 17 00:00:00 2001 From: Bryce Curtis Date: Fri, 21 Oct 2011 16:29:55 -0500 Subject: [PATCH] Load multi-page apps in same webview and update pause/resume for consistency. 1. Make handling of multi-page apps consistent with iOS and Blackberry to load into same webview (instead of starting a new activity). 2. Make lifecycle consistent. pause is called when going into background, resume is called when coming into foreground. It is no longer called when loading or leaving an HTML page. Use window.onload/onunload to get these notifications. --- framework/assets/js/app.js | 11 +- framework/assets/js/phonegap.js.base | 9 +- framework/src/com/phonegap/App.java | 35 ++- .../src/com/phonegap/CallbackServer.java | 14 + framework/src/com/phonegap/DroidGap.java | 255 ++++++++++-------- .../src/com/phonegap/api/PluginManager.java | 11 + 6 files changed, 188 insertions(+), 147 deletions(-) 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 */