diff --git a/README.md b/README.md index 1f615532..ea999ffb 100755 --- a/README.md +++ b/README.md @@ -19,6 +19,16 @@ Requires - Apache ANT - Android SDK [http://developer.android.com](http://developer.android.com) + +Building +--- + +To create your phonegap.jar, run in the framework directory: + + android update project -p . -t android-15 + ant jar + + Cordova Android Developer Tools --- diff --git a/framework/res/xml/phonegap.xml b/framework/res/xml/phonegap.xml index f57d9ab5..69e48763 100644 --- a/framework/res/xml/phonegap.xml +++ b/framework/res/xml/phonegap.xml @@ -1,6 +1,19 @@ - + + + + + + + + + + + diff --git a/framework/res/xml/plugins.xml b/framework/res/xml/plugins.xml index 73169370..4d84f599 100644 --- a/framework/res/xml/plugins.xml +++ b/framework/res/xml/plugins.xml @@ -16,4 +16,5 @@ + diff --git a/framework/src/com/phonegap/CordovaChromeClient.java b/framework/src/com/phonegap/CordovaChromeClient.java new file mode 100644 index 00000000..54f7b811 --- /dev/null +++ b/framework/src/com/phonegap/CordovaChromeClient.java @@ -0,0 +1,294 @@ +package com.phonegap; +import org.json.JSONArray; +import org.json.JSONException; + +import com.phonegap.api.LOG; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.view.KeyEvent; +import android.view.View; +import android.webkit.ConsoleMessage; +import android.webkit.JsPromptResult; +import android.webkit.JsResult; +import android.webkit.WebChromeClient; +import android.webkit.WebStorage; +import android.webkit.WebView; +import android.webkit.GeolocationPermissions.Callback; +import android.widget.EditText; + + +public class CordovaChromeClient extends WebChromeClient { + + + private String TAG = "PhoneGapLog"; + private long MAX_QUOTA = 100 * 1024 * 1024; + private DroidGap ctx; + + /** + * Constructor. + * + * @param ctx + */ + public CordovaChromeClient(Context ctx) { + this.ctx = (DroidGap) ctx; + } + + /** + * Tell the client to display a javascript alert dialog. + * + * @param view + * @param url + * @param message + * @param result + */ + @Override + public boolean onJsAlert(WebView view, String url, String message, final JsResult result) { + AlertDialog.Builder dlg = new AlertDialog.Builder(this.ctx); + dlg.setMessage(message); + dlg.setTitle("Alert"); + //Don't let alerts break the back button + dlg.setCancelable(true); + dlg.setPositiveButton(android.R.string.ok, + new AlertDialog.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + result.confirm(); + } + }); + dlg.setOnCancelListener( + new DialogInterface.OnCancelListener() { + public void onCancel(DialogInterface dialog) { + result.confirm(); + } + }); + dlg.setOnKeyListener(new DialogInterface.OnKeyListener() { + //DO NOTHING + public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { + if(keyCode == KeyEvent.KEYCODE_BACK) + { + result.confirm(); + return false; + } + else + return true; + } + }); + dlg.create(); + dlg.show(); + return true; + } + + /** + * Tell the client to display a confirm dialog to the user. + * + * @param view + * @param url + * @param message + * @param result + */ + @Override + public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) { + AlertDialog.Builder dlg = new AlertDialog.Builder(this.ctx); + dlg.setMessage(message); + dlg.setTitle("Confirm"); + dlg.setCancelable(true); + dlg.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + result.confirm(); + } + }); + dlg.setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + result.cancel(); + } + }); + dlg.setOnCancelListener( + new DialogInterface.OnCancelListener() { + public void onCancel(DialogInterface dialog) { + result.cancel(); + } + }); + dlg.setOnKeyListener(new DialogInterface.OnKeyListener() { + //DO NOTHING + public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { + if(keyCode == KeyEvent.KEYCODE_BACK) + { + result.cancel(); + return false; + } + else + return true; + } + }); + dlg.create(); + dlg.show(); + return true; + } + + /** + * Tell the client to display a prompt dialog to the user. + * If the client returns true, WebView will assume that the client will + * handle the prompt dialog and call the appropriate JsPromptResult method. + * + * Since we are hacking prompts for our own purposes, we should not be using them for + * this purpose, perhaps we should hack console.log to do this instead! + * + * @param view + * @param url + * @param message + * @param defaultValue + * @param result + */ + @Override + public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { + + // Security check to make sure any requests are coming from the page initially + // loaded in webview and not another loaded in an iframe. + boolean reqOk = false; + if (url.startsWith("file://") || url.indexOf(this.ctx.baseUrl) == 0 || ctx.isUrlWhiteListed(url)) { + reqOk = true; + } + + // Calling PluginManager.exec() to call a native service using + // prompt(this.stringify(args), "gap:"+this.stringify([service, action, callbackId, true])); + if (reqOk && defaultValue != null && defaultValue.length() > 3 && defaultValue.substring(0, 4).equals("gap:")) { + JSONArray array; + try { + array = new JSONArray(defaultValue.substring(4)); + String service = array.getString(0); + String action = array.getString(1); + String callbackId = array.getString(2); + boolean async = array.getBoolean(3); + String r = ctx.pluginManager.exec(service, action, callbackId, message, async); + result.confirm(r); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + // Polling for JavaScript messages + else if (reqOk && defaultValue != null && defaultValue.equals("gap_poll:")) { + String r = ctx.callbackServer.getJavascript(); + result.confirm(r); + } + + // Calling into CallbackServer + else if (reqOk && defaultValue != null && defaultValue.equals("gap_callbackServer:")) { + String r = ""; + if (message.equals("usePolling")) { + r = ""+ ctx.callbackServer.usePolling(); + } + else if (message.equals("restartServer")) { + ctx.callbackServer.restartServer(); + } + else if (message.equals("getPort")) { + r = Integer.toString(ctx.callbackServer.getPort()); + } + else if (message.equals("getToken")) { + r = ctx.callbackServer.getToken(); + } + result.confirm(r); + } + + // PhoneGap JS has initialized, so show webview + // (This solves white flash seen when rendering HTML) + else if (reqOk && defaultValue != null && defaultValue.equals("gap_init:")) { + ctx.appView.setVisibility(View.VISIBLE); + ctx.spinnerStop(); + result.confirm("OK"); + } + + // Show dialog + else { + final JsPromptResult res = result; + AlertDialog.Builder dlg = new AlertDialog.Builder(this.ctx); + dlg.setMessage(message); + final EditText input = new EditText(this.ctx); + if (defaultValue != null) { + input.setText(defaultValue); + } + dlg.setView(input); + dlg.setCancelable(false); + dlg.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + String usertext = input.getText().toString(); + res.confirm(usertext); + } + }); + dlg.setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + res.cancel(); + } + }); + dlg.create(); + dlg.show(); + } + return true; + } + + /** + * Handle database quota exceeded notification. + * + * @param url + * @param databaseIdentifier + * @param currentQuota + * @param estimatedSize + * @param totalUsedQuota + * @param quotaUpdater + */ + @Override + public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize, + long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) + { + LOG.d(TAG, "DroidGap: onExceededDatabaseQuota estimatedSize: %d currentQuota: %d totalUsedQuota: %d", estimatedSize, currentQuota, totalUsedQuota); + + if( estimatedSize < MAX_QUOTA) + { + //increase for 1Mb + long newQuota = estimatedSize; + LOG.d(TAG, "calling quotaUpdater.updateQuota newQuota: %d", newQuota); + quotaUpdater.updateQuota(newQuota); + } + else + { + // Set the quota to whatever it is and force an error + // TODO: get docs on how to handle this properly + quotaUpdater.updateQuota(currentQuota); + } + } + + // console.log in api level 7: http://developer.android.com/guide/developing/debug-tasks.html + @Override + public void onConsoleMessage(String message, int lineNumber, String sourceID) + { + LOG.d(TAG, "%s: Line %d : %s", sourceID, lineNumber, message); + super.onConsoleMessage(message, lineNumber, sourceID); + } + + @Override + public boolean onConsoleMessage(ConsoleMessage consoleMessage) + { + if(consoleMessage.message() != null) + LOG.d(TAG, consoleMessage.message()); + return super.onConsoleMessage(consoleMessage); + } + + @Override + /** + * Instructs the client to show a prompt to ask the user to set the Geolocation permission state for the specified origin. + * + * @param origin + * @param callback + */ + public void onGeolocationPermissionsShowPrompt(String origin, Callback callback) { + super.onGeolocationPermissionsShowPrompt(origin, callback); + callback.invoke(origin, true, false); + } + + +} diff --git a/framework/src/com/phonegap/CordovaWebViewClient.java b/framework/src/com/phonegap/CordovaWebViewClient.java new file mode 100644 index 00000000..c7121fa9 --- /dev/null +++ b/framework/src/com/phonegap/CordovaWebViewClient.java @@ -0,0 +1,261 @@ +package com.phonegap; + +import com.phonegap.api.LOG; + +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.Bitmap; +import android.net.Uri; +import android.net.http.SslError; +import android.view.View; +import android.webkit.HttpAuthHandler; +import android.webkit.SslErrorHandler; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +public class CordovaWebViewClient extends WebViewClient { + + private static final String TAG = "Cordova"; + DroidGap ctx; + + /** + * Constructor. + * + * @param ctx + */ + public CordovaWebViewClient(DroidGap ctx) { + this.ctx = ctx; + } + + /** + * Give the host application a chance to take over the control when a new url + * is about to be loaded in the current WebView. + * + * @param view The WebView that is initiating the callback. + * @param url The url to be loaded. + * @return true to override, false for default behavior + */ + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + + // First give any plugins the chance to handle the url themselves + if (this.ctx.pluginManager.onOverrideUrlLoading(url)) { + } + + // If dialing phone (tel:5551212) + else if (url.startsWith(WebView.SCHEME_TEL)) { + try { + Intent intent = new Intent(Intent.ACTION_DIAL); + intent.setData(Uri.parse(url)); + ctx.startActivity(intent); + } catch (android.content.ActivityNotFoundException e) { + LOG.e(TAG, "Error dialing "+url+": "+ e.toString()); + } + } + + // If displaying map (geo:0,0?q=address) + else if (url.startsWith("geo:")) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + ctx.startActivity(intent); + } catch (android.content.ActivityNotFoundException e) { + LOG.e(TAG, "Error showing map "+url+": "+ e.toString()); + } + } + + // If sending email (mailto:abc@corp.com) + else if (url.startsWith(WebView.SCHEME_MAILTO)) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + ctx.startActivity(intent); + } catch (android.content.ActivityNotFoundException e) { + LOG.e(TAG, "Error sending email "+url+": "+ e.toString()); + } + } + + // If sms:5551212?body=This is the message + else if (url.startsWith("sms:")) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + + // Get address + String address = null; + int parmIndex = url.indexOf('?'); + if (parmIndex == -1) { + address = url.substring(4); + } + else { + address = url.substring(4, parmIndex); + + // If body, then set sms body + Uri uri = Uri.parse(url); + String query = uri.getQuery(); + if (query != null) { + if (query.startsWith("body=")) { + intent.putExtra("sms_body", query.substring(5)); + } + } + } + intent.setData(Uri.parse("sms:"+address)); + intent.putExtra("address", address); + intent.setType("vnd.android-dir/mms-sms"); + ctx.startActivity(intent); + } catch (android.content.ActivityNotFoundException e) { + LOG.e(TAG, "Error sending sms "+url+":"+ e.toString()); + } + } + + // All else + else { + + // 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 (url.startsWith("file://") || url.indexOf(this.ctx.baseUrl) == 0 || ctx.isUrlWhiteListed(url)) { + this.ctx.loadUrl(url); + } + + // If not our application, let default viewer handle + else { + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + ctx.startActivity(intent); + } catch (android.content.ActivityNotFoundException e) { + LOG.e(TAG, "Error loading url "+url, e); + } + } + } + return true; + } + + /** + * On received http auth request. + * The method reacts on all registered authentication tokens. There is one and only one authentication token for any host + realm combination + * + * @param view + * the view + * @param handler + * the handler + * @param host + * the host + * @param realm + * the realm + */ + @Override + public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, + String realm) { + + // get the authentication token + AuthenticationToken token = ctx.getAuthenticationToken(host,realm); + + if(token != null) { + handler.proceed(token.getUserName(), token.getPassword()); + } + } + + + @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. + * + * @param view The webview initiating the callback. + * @param url The url of the page. + */ + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + + // Clear timeout flag + this.ctx.loadUrlTimeout++; + + // Try firing the onNativeReady event in JS. If it fails because the JS is + // not loaded yet then just set a flag so that the onNativeReady can be fired + // from the JS side when the JS gets to that code. + if (!url.equals("about:blank")) { + ctx.appView.loadUrl("javascript:try{ PhoneGap.onNativeReady.fire();}catch(e){_nativeReady = true;}"); + } + + // Make app visible after 2 sec in case there was a JS error and PhoneGap JS never initialized correctly + if (ctx.appView.getVisibility() == View.INVISIBLE) { + Thread t = new Thread(new Runnable() { + public void run() { + try { + Thread.sleep(2000); + ctx.runOnUiThread(new Runnable() { + public void run() { + ctx.appView.setVisibility(View.VISIBLE); + ctx.spinnerStop(); + } + }); + } catch (InterruptedException e) { + } + } + }); + t.start(); + } + + + // Shutdown if blank loaded + if (url.equals("about:blank")) { + if (this.ctx.callbackServer != null) { + this.ctx.callbackServer.destroy(); + } + this.ctx.endActivity(); + } + } + + /** + * Report an error to the host application. These errors are unrecoverable (i.e. the main resource is unavailable). + * The errorCode parameter corresponds to one of the ERROR_* constants. + * + * @param view The WebView that is initiating the callback. + * @param errorCode The error code corresponding to an ERROR_* value. + * @param description A String describing the error. + * @param failingUrl The url that failed to load. + */ + @Override + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + LOG.d(TAG, "DroidGap: GapViewClient.onReceivedError: Error code=%s Description=%s URL=%s", errorCode, description, failingUrl); + + // Clear timeout flag + this.ctx.loadUrlTimeout++; + + // Stop "app loading" spinner if showing + this.ctx.spinnerStop(); + + // Handle error + this.ctx.onReceivedError(errorCode, description, failingUrl); + } + + public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { + + final String packageName = this.ctx.getPackageName(); + final PackageManager pm = this.ctx.getPackageManager(); + ApplicationInfo appInfo; + try { + appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA); + if ((appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) { + // debug = true + handler.proceed(); + return; + } else { + // debug = false + super.onReceivedSslError(view, handler, error); + } + } catch (NameNotFoundException e) { + // When it doubt, lock it out! + super.onReceivedSslError(view, handler, error); + } + } +} diff --git a/framework/src/com/phonegap/DroidGap.java b/framework/src/com/phonegap/DroidGap.java index 59e4a15d..edfb4d34 100755 --- a/framework/src/com/phonegap/DroidGap.java +++ b/framework/src/com/phonegap/DroidGap.java @@ -191,14 +191,14 @@ public class DroidGap extends PhonegapActivity { // The base of the initial URL for our app. // Does not include file name. Ends with / // ie http://server/path/ - private String baseUrl = null; + String baseUrl = null; // Plugin to call when activity result is received protected IPlugin activityResultCallback = null; protected boolean activityResultKeepRunning; // Flag indicates that a loadUrl timeout occurred - private int loadUrlTimeout = 0; + int loadUrlTimeout = 0; // Default background color for activity // (this is not the color for the webview, which is set in HTML) @@ -375,8 +375,8 @@ public class DroidGap extends PhonegapActivity { ViewGroup.LayoutParams.FILL_PARENT, 1.0F)); - this.appView.setWebChromeClient(new GapClient(DroidGap.this)); - this.setWebViewClient(this.appView, new GapViewClient(this)); + this.appView.setWebChromeClient(new CordovaChromeClient(DroidGap.this)); + this.setWebViewClient(this.appView, new CordovaWebViewClient(this)); //14 is Ice Cream Sandwich! if(android.os.Build.VERSION.SDK_INT < 14 && this.classicRender) @@ -1066,527 +1066,6 @@ public class DroidGap extends PhonegapActivity { this.spinnerDialog = null; } } - - /** - * Set the chrome handler. - */ - public class GapClient extends WebChromeClient { - - private String TAG = "PhoneGapLog"; - private long MAX_QUOTA = 100 * 1024 * 1024; - private DroidGap ctx; - - /** - * Constructor. - * - * @param ctx - */ - public GapClient(Context ctx) { - this.ctx = (DroidGap)ctx; - } - - /** - * Tell the client to display a javascript alert dialog. - * - * @param view - * @param url - * @param message - * @param result - */ - @Override - public boolean onJsAlert(WebView view, String url, String message, final JsResult result) { - AlertDialog.Builder dlg = new AlertDialog.Builder(this.ctx); - dlg.setMessage(message); - dlg.setTitle("Alert"); - //Don't let alerts break the back button - dlg.setCancelable(true); - dlg.setPositiveButton(android.R.string.ok, - new AlertDialog.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - result.confirm(); - } - }); - dlg.setOnCancelListener( - new DialogInterface.OnCancelListener() { - public void onCancel(DialogInterface dialog) { - result.confirm(); - } - }); - dlg.setOnKeyListener(new DialogInterface.OnKeyListener() { - //DO NOTHING - public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { - if(keyCode == KeyEvent.KEYCODE_BACK) - { - result.confirm(); - return false; - } - else - return true; - } - }); - dlg.create(); - dlg.show(); - return true; - } - - /** - * Tell the client to display a confirm dialog to the user. - * - * @param view - * @param url - * @param message - * @param result - */ - @Override - public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) { - AlertDialog.Builder dlg = new AlertDialog.Builder(this.ctx); - dlg.setMessage(message); - dlg.setTitle("Confirm"); - dlg.setCancelable(true); - dlg.setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - result.confirm(); - } - }); - dlg.setNegativeButton(android.R.string.cancel, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - result.cancel(); - } - }); - dlg.setOnCancelListener( - new DialogInterface.OnCancelListener() { - public void onCancel(DialogInterface dialog) { - result.cancel(); - } - }); - dlg.setOnKeyListener(new DialogInterface.OnKeyListener() { - //DO NOTHING - public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { - if(keyCode == KeyEvent.KEYCODE_BACK) - { - result.cancel(); - return false; - } - else - return true; - } - }); - dlg.create(); - dlg.show(); - return true; - } - - /** - * Tell the client to display a prompt dialog to the user. - * If the client returns true, WebView will assume that the client will - * handle the prompt dialog and call the appropriate JsPromptResult method. - * - * Since we are hacking prompts for our own purposes, we should not be using them for - * this purpose, perhaps we should hack console.log to do this instead! - * - * @param view - * @param url - * @param message - * @param defaultValue - * @param result - */ - @Override - public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { - - // Security check to make sure any requests are coming from the page initially - // loaded in webview and not another loaded in an iframe. - boolean reqOk = false; - if (url.startsWith("file://") || url.indexOf(this.ctx.baseUrl) == 0 || isUrlWhiteListed(url)) { - reqOk = true; - } - - // Calling PluginManager.exec() to call a native service using - // prompt(this.stringify(args), "gap:"+this.stringify([service, action, callbackId, true])); - if (reqOk && defaultValue != null && defaultValue.length() > 3 && defaultValue.substring(0, 4).equals("gap:")) { - JSONArray array; - try { - array = new JSONArray(defaultValue.substring(4)); - String service = array.getString(0); - String action = array.getString(1); - String callbackId = array.getString(2); - boolean async = array.getBoolean(3); - String r = pluginManager.exec(service, action, callbackId, message, async); - result.confirm(r); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - // Polling for JavaScript messages - else if (reqOk && defaultValue != null && defaultValue.equals("gap_poll:")) { - String r = callbackServer.getJavascript(); - result.confirm(r); - } - - // Calling into CallbackServer - else if (reqOk && defaultValue != null && defaultValue.equals("gap_callbackServer:")) { - String r = ""; - if (message.equals("usePolling")) { - r = ""+callbackServer.usePolling(); - } - else if (message.equals("restartServer")) { - callbackServer.restartServer(); - } - else if (message.equals("getPort")) { - r = Integer.toString(callbackServer.getPort()); - } - else if (message.equals("getToken")) { - r = callbackServer.getToken(); - } - result.confirm(r); - } - - // PhoneGap JS has initialized, so show webview - // (This solves white flash seen when rendering HTML) - else if (reqOk && defaultValue != null && defaultValue.equals("gap_init:")) { - appView.setVisibility(View.VISIBLE); - ctx.spinnerStop(); - result.confirm("OK"); - } - - // Show dialog - else { - final JsPromptResult res = result; - AlertDialog.Builder dlg = new AlertDialog.Builder(this.ctx); - dlg.setMessage(message); - final EditText input = new EditText(this.ctx); - if (defaultValue != null) { - input.setText(defaultValue); - } - dlg.setView(input); - dlg.setCancelable(false); - dlg.setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - String usertext = input.getText().toString(); - res.confirm(usertext); - } - }); - dlg.setNegativeButton(android.R.string.cancel, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - res.cancel(); - } - }); - dlg.create(); - dlg.show(); - } - return true; - } - - /** - * Handle database quota exceeded notification. - * - * @param url - * @param databaseIdentifier - * @param currentQuota - * @param estimatedSize - * @param totalUsedQuota - * @param quotaUpdater - */ - @Override - public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize, - long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) - { - LOG.d(TAG, "DroidGap: onExceededDatabaseQuota estimatedSize: %d currentQuota: %d totalUsedQuota: %d", estimatedSize, currentQuota, totalUsedQuota); - - if( estimatedSize < MAX_QUOTA) - { - //increase for 1Mb - long newQuota = estimatedSize; - LOG.d(TAG, "calling quotaUpdater.updateQuota newQuota: %d", newQuota); - quotaUpdater.updateQuota(newQuota); - } - else - { - // Set the quota to whatever it is and force an error - // TODO: get docs on how to handle this properly - quotaUpdater.updateQuota(currentQuota); - } - } - - // console.log in api level 7: http://developer.android.com/guide/developing/debug-tasks.html - @Override - public void onConsoleMessage(String message, int lineNumber, String sourceID) - { - LOG.d(TAG, "%s: Line %d : %s", sourceID, lineNumber, message); - super.onConsoleMessage(message, lineNumber, sourceID); - } - - @Override - public boolean onConsoleMessage(ConsoleMessage consoleMessage) - { - LOG.d(TAG, consoleMessage.message()); - return super.onConsoleMessage(consoleMessage); - } - - @Override - /** - * Instructs the client to show a prompt to ask the user to set the Geolocation permission state for the specified origin. - * - * @param origin - * @param callback - */ - public void onGeolocationPermissionsShowPrompt(String origin, Callback callback) { - super.onGeolocationPermissionsShowPrompt(origin, callback); - callback.invoke(origin, true, false); - } - - } - - /** - * The webview client receives notifications about appView - */ - public class GapViewClient extends WebViewClient { - - DroidGap ctx; - - /** - * Constructor. - * - * @param ctx - */ - public GapViewClient(DroidGap ctx) { - this.ctx = ctx; - } - - /** - * Give the host application a chance to take over the control when a new url - * is about to be loaded in the current WebView. - * - * @param view The WebView that is initiating the callback. - * @param url The url to be loaded. - * @return true to override, false for default behavior - */ - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - - // First give any plugins the chance to handle the url themselves - if (this.ctx.pluginManager.onOverrideUrlLoading(url)) { - } - - // If dialing phone (tel:5551212) - else if (url.startsWith(WebView.SCHEME_TEL)) { - try { - Intent intent = new Intent(Intent.ACTION_DIAL); - intent.setData(Uri.parse(url)); - startActivity(intent); - } catch (android.content.ActivityNotFoundException e) { - LOG.e(TAG, "Error dialing "+url+": "+ e.toString()); - } - } - - // If displaying map (geo:0,0?q=address) - else if (url.startsWith("geo:")) { - try { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(url)); - startActivity(intent); - } catch (android.content.ActivityNotFoundException e) { - LOG.e(TAG, "Error showing map "+url+": "+ e.toString()); - } - } - - // If sending email (mailto:abc@corp.com) - else if (url.startsWith(WebView.SCHEME_MAILTO)) { - try { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(url)); - startActivity(intent); - } catch (android.content.ActivityNotFoundException e) { - LOG.e(TAG, "Error sending email "+url+": "+ e.toString()); - } - } - - // If sms:5551212?body=This is the message - else if (url.startsWith("sms:")) { - try { - Intent intent = new Intent(Intent.ACTION_VIEW); - - // Get address - String address = null; - int parmIndex = url.indexOf('?'); - if (parmIndex == -1) { - address = url.substring(4); - } - else { - address = url.substring(4, parmIndex); - - // If body, then set sms body - Uri uri = Uri.parse(url); - String query = uri.getQuery(); - if (query != null) { - if (query.startsWith("body=")) { - intent.putExtra("sms_body", query.substring(5)); - } - } - } - intent.setData(Uri.parse("sms:"+address)); - intent.putExtra("address", address); - intent.setType("vnd.android-dir/mms-sms"); - startActivity(intent); - } catch (android.content.ActivityNotFoundException e) { - LOG.e(TAG, "Error sending sms "+url+":"+ e.toString()); - } - } - - // All else - else { - - // 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 (url.startsWith("file://") || url.indexOf(this.ctx.baseUrl) == 0 || isUrlWhiteListed(url)) { - this.ctx.loadUrl(url); - } - - // If not our application, let default viewer handle - else { - try { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(url)); - startActivity(intent); - } catch (android.content.ActivityNotFoundException e) { - LOG.e(TAG, "Error loading url "+url, e); - } - } - } - return true; - } - - /** - * On received http auth request. - * The method reacts on all registered authentication tokens. There is one and only one authentication token for any host + realm combination - * - * @param view - * the view - * @param handler - * the handler - * @param host - * the host - * @param realm - * the realm - */ - @Override - public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, - String realm) { - - // get the authentication token - AuthenticationToken token = getAuthenticationToken(host,realm); - - if(token != null) { - handler.proceed(token.getUserName(), token.getPassword()); - } - } - - - @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. - * - * @param view The webview initiating the callback. - * @param url The url of the page. - */ - @Override - public void onPageFinished(WebView view, String url) { - super.onPageFinished(view, url); - - // Clear timeout flag - this.ctx.loadUrlTimeout++; - - // Try firing the onNativeReady event in JS. If it fails because the JS is - // not loaded yet then just set a flag so that the onNativeReady can be fired - // from the JS side when the JS gets to that code. - if (!url.equals("about:blank")) { - appView.loadUrl("javascript:try{ PhoneGap.onNativeReady.fire();}catch(e){_nativeReady = true;}"); - } - - // Make app visible after 2 sec in case there was a JS error and PhoneGap JS never initialized correctly - if (appView.getVisibility() == View.INVISIBLE) { - Thread t = new Thread(new Runnable() { - public void run() { - try { - Thread.sleep(2000); - ctx.runOnUiThread(new Runnable() { - public void run() { - appView.setVisibility(View.VISIBLE); - ctx.spinnerStop(); - } - }); - } catch (InterruptedException e) { - } - } - }); - t.start(); - } - - - // Shutdown if blank loaded - if (url.equals("about:blank")) { - if (this.ctx.callbackServer != null) { - this.ctx.callbackServer.destroy(); - } - this.ctx.endActivity(); - } - } - - /** - * Report an error to the host application. These errors are unrecoverable (i.e. the main resource is unavailable). - * The errorCode parameter corresponds to one of the ERROR_* constants. - * - * @param view The WebView that is initiating the callback. - * @param errorCode The error code corresponding to an ERROR_* value. - * @param description A String describing the error. - * @param failingUrl The url that failed to load. - */ - @Override - public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { - LOG.d(TAG, "DroidGap: GapViewClient.onReceivedError: Error code=%s Description=%s URL=%s", errorCode, description, failingUrl); - - // Clear timeout flag - this.ctx.loadUrlTimeout++; - - // Stop "app loading" spinner if showing - this.ctx.spinnerStop(); - - // Handle error - this.ctx.onReceivedError(errorCode, description, failingUrl); - } - - public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { - - final String packageName = this.ctx.getPackageName(); - final PackageManager pm = this.ctx.getPackageManager(); - ApplicationInfo appInfo; - try { - appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA); - if ((appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) { - // debug = true - handler.proceed(); - return; - } else { - // debug = false - super.onReceivedSslError(view, handler, error); - } - } catch (NameNotFoundException e) { - // When it doubt, lock it out! - super.onReceivedSslError(view, handler, error); - } - } - } /** * End this activity by calling finish for activity @@ -1774,88 +1253,6 @@ public class DroidGap extends PhonegapActivity { }); } - /** - * We are providing this class to detect when the soft keyboard is shown - * and hidden in the web view. - */ - class LinearLayoutSoftKeyboardDetect extends LinearLayout { - - private static final String TAG = "SoftKeyboardDetect"; - - private int oldHeight = 0; // Need to save the old height as not to send redundant events - private int oldWidth = 0; // Need to save old width for orientation change - private int screenWidth = 0; - private int screenHeight = 0; - - public LinearLayoutSoftKeyboardDetect(Context context, int width, int height) { - super(context); - screenWidth = width; - screenHeight = height; - } - - @Override - /** - * Start listening to new measurement events. Fire events when the height - * gets smaller fire a show keyboard event and when height gets bigger fire - * a hide keyboard event. - * - * Note: We are using callbackServer.sendJavascript() instead of - * this.appView.loadUrl() as changing the URL of the app would cause the - * soft keyboard to go away. - * - * @param widthMeasureSpec - * @param heightMeasureSpec - */ - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - LOG.v(TAG, "We are in our onMeasure method"); - - // Get the current height of the visible part of the screen. - // This height will not included the status bar. - int height = MeasureSpec.getSize(heightMeasureSpec); - int width = MeasureSpec.getSize(widthMeasureSpec); - - LOG.v(TAG, "Old Height = %d", oldHeight); - LOG.v(TAG, "Height = %d", height); - LOG.v(TAG, "Old Width = %d", oldWidth); - LOG.v(TAG, "Width = %d", width); - - // If the oldHeight = 0 then this is the first measure event as the app starts up. - // If oldHeight == height then we got a measurement change that doesn't affect us. - if (oldHeight == 0 || oldHeight == height) { - LOG.d(TAG, "Ignore this event"); - } - // Account for orientation change and ignore this event/Fire orientation change - else if(screenHeight == width) - { - int tmp_var = screenHeight; - screenHeight = screenWidth; - screenWidth = tmp_var; - LOG.v(TAG, "Orientation Change"); - } - // If the height as gotten bigger then we will assume the soft keyboard has - // gone away. - else if (height > oldHeight) { - if (callbackServer != null) { - LOG.v(TAG, "Throw hide keyboard event"); - callbackServer.sendJavascript("PhoneGap.fireDocumentEvent('hidekeyboard');"); - } - } - // If the height as gotten smaller then we will assume the soft keyboard has - // been displayed. - else if (height < oldHeight) { - if (callbackServer != null) { - LOG.v(TAG, "Throw show keyboard event"); - callbackServer.sendJavascript("PhoneGap.fireDocumentEvent('showkeyboard');"); - } - } - - // Update the old height for the next event - oldHeight = height; - oldWidth = width; - } - } /** * Load PhoneGap configuration from res/xml/phonegap.xml. @@ -1963,7 +1360,7 @@ public class DroidGap extends PhonegapActivity { * @param url * @return */ - private boolean isUrlWhiteListed(String url) { + boolean isUrlWhiteListed(String url) { // Check to see if we have matched url previously if (whiteListCache.get(url) != null) { diff --git a/framework/src/com/phonegap/GapView.java b/framework/src/com/phonegap/GapView.java deleted file mode 100644 index 6ac3b20c..00000000 --- a/framework/src/com/phonegap/GapView.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.phonegap; - -import android.content.Context; -import android.view.View; -import android.view.ViewGroup; -import android.webkit.WebView; -import android.widget.EditText; - -public class GapView extends WebView { - - public GapView(Context context) { - super(context); - // TODO Auto-generated constructor stub - } - - @Override - protected void attachViewToParent(View child, int index, ViewGroup.LayoutParams params) - { - if(child.getClass() != EditText.class) - super.attachViewToParent(child, index, params); - else - { - super.attachViewToParent(child, index, params); - } - } - - @Override - protected boolean addViewInLayout(View child, int index, ViewGroup.LayoutParams params) - { - return super.addViewInLayout(child, index, params); - } - - @Override - protected boolean addViewInLayout(View child, int index, ViewGroup.LayoutParams params, boolean preventRequestLayout) - { - return super.addViewInLayout(child, index, params); - } -} diff --git a/framework/src/com/phonegap/KeyboardHandler.java b/framework/src/com/phonegap/KeyboardHandler.java new file mode 100644 index 00000000..d10988cf --- /dev/null +++ b/framework/src/com/phonegap/KeyboardHandler.java @@ -0,0 +1,33 @@ +package com.phonegap; + +import org.json.JSONArray; + +import com.phonegap.api.Plugin; +import com.phonegap.api.PluginResult; + +public class KeyboardHandler extends Plugin { + + + /* + * This will never be called! + * (non-Javadoc) + * @see com.phonegap.api.Plugin#execute(java.lang.String, org.json.JSONArray, java.lang.String) + */ + @Override + public PluginResult execute(String action, JSONArray args, String callbackId) { + // TODO Auto-generated method stub + return null; + } + + public void onMessage(String id, Object data) + { + if(id.equals("keyboardHidden")) + { + super.sendJavascript("PhoneGap.fireDocumentEvent('hidekeyboard');"); + } + else if(id.equals("keyboardVisible")) + { + super.sendJavascript("PhoneGap.fireDocumentEvent('showkeyboard');"); + } + } +} diff --git a/framework/src/com/phonegap/LinearLayoutSoftKeyboardDetect.java b/framework/src/com/phonegap/LinearLayoutSoftKeyboardDetect.java new file mode 100644 index 00000000..1ef6001b --- /dev/null +++ b/framework/src/com/phonegap/LinearLayoutSoftKeyboardDetect.java @@ -0,0 +1,84 @@ +package com.phonegap; +import com.phonegap.api.LOG; + +import android.content.Context; +import android.view.View.MeasureSpec; +import android.widget.LinearLayout; + + +public class LinearLayoutSoftKeyboardDetect extends LinearLayout { + + private static final String TAG = "SoftKeyboardDetect"; + + private int oldHeight = 0; // Need to save the old height as not to send redundant events + private int oldWidth = 0; // Need to save old width for orientation change + private int screenWidth = 0; + private int screenHeight = 0; + private DroidGap app = null; + + public LinearLayoutSoftKeyboardDetect(Context context, int width, int height) { + super(context); + screenWidth = width; + screenHeight = height; + app = (DroidGap) context; + } + + @Override + /** + * Start listening to new measurement events. Fire events when the height + * gets smaller fire a show keyboard event and when height gets bigger fire + * a hide keyboard event. + * + * Note: We are using app.postMessage so that this is more compatible with the API + * + * @param widthMeasureSpec + * @param heightMeasureSpec + */ + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + LOG.v(TAG, "We are in our onMeasure method"); + + // Get the current height of the visible part of the screen. + // This height will not included the status bar.\ + int width, height; + + height = MeasureSpec.getSize(heightMeasureSpec); + width = MeasureSpec.getSize(widthMeasureSpec); + LOG.v(TAG, "Old Height = %d", oldHeight); + LOG.v(TAG, "Height = %d", height); + LOG.v(TAG, "Old Width = %d", oldWidth); + LOG.v(TAG, "Width = %d", width); + + // If the oldHeight = 0 then this is the first measure event as the app starts up. + // If oldHeight == height then we got a measurement change that doesn't affect us. + if (oldHeight == 0 || oldHeight == height) { + LOG.d(TAG, "Ignore this event"); + } + // Account for orientation change and ignore this event/Fire orientation change + else if(screenHeight == width) + { + int tmp_var = screenHeight; + screenHeight = screenWidth; + screenWidth = tmp_var; + LOG.v(TAG, "Orientation Change"); + } + // If the height as gotten bigger then we will assume the soft keyboard has + // gone away. + else if (height > oldHeight) { + if(app != null) + app.sendJavascript("PhoneGap.fireDocumentEvent('hidekeyboard');"); + } + // If the height as gotten smaller then we will assume the soft keyboard has + // been displayed. + else if (height < oldHeight) { + if(app != null) + app.sendJavascript("PhoneGap.fireDocumentEvent('showkeyboard');"); + } + + // Update the old height for the next event + oldHeight = height; + oldWidth = width; + } + +}