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.
This commit is contained in:
Bryce Curtis 2011-10-21 16:29:55 -05:00
parent 75f3651376
commit bd7ed19b52
6 changed files with 188 additions and 147 deletions

View File

@ -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]);

View File

@ -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') {

View File

@ -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<String, Object> params = new HashMap<String, Object>();
@ -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();
}

View File

@ -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<String>();
// 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.
*

View File

@ -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<String> urls = new Stack<String>();
// 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<String, Object> 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<Entry<String,Object>> s = params.entrySet();
java.util.Iterator<Entry<String,Object>> it = s.iterator();
while(it.hasNext()) {
Entry<String,Object> 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<String, Object> 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<String, Object> params = new HashMap<String, Object>();
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);

View File

@ -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<String, IPlugin>();
}
/**
* Load plugins from res/xml/plugins.xml
*/