diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fb0a709c99..304403b8ac 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -20,6 +20,40 @@ --> ## Release Notes for Cordova (Android) ## +### Release 3.7.2 (May 2015) ### + +* Removed Intent Functionality from Preferences - Preferences can no longer be set by intents + +### Release 3.7.1 (January 2015) ### +* CB-8411 Initialize plugins only after `createViews()` is called (regression in 3.7.0) + +### Release 3.7.0 (January 2015) ### + +* CB-8328 Allow plugins to handle certificate challenges (close #150) +* CB-8201 Add support for auth dialogs into Cordova Android +* CB-8017 Add support for `` for Lollipop +* CB-8143 Loads of gradle improvements (try it with cordova/build --gradle) +* CB-8329 Cancel outstanding ActivityResult requests when a new startActivityForResult occurs +* CB-8026 Bumping up Android Version and setting it up to allow third-party cookies. This might change later. +* CB-8210 Use PluginResult for various events from native so that content-security-policy can be used +* CB-8168 Add support for `cordova/run --list` (closes #139) +* CB-8176 Vastly better auto-detection of SDK & JDK locations +* CB-8079 Use activity class package name, but fallback to application package name when looking for splash screen drawable +* CB-8147 Have corodva/build warn about unrecognized flags rather than fail +* CB-7881 Android tooling shouldn't lock application directory +* CB-8112 Turn off mediaPlaybackRequiresUserGesture +* CB-6153 Add a preference for controlling hardware button audio stream (DefaultVolumeStream) +* CB-8031 Fix race condition that shows as ConcurrentModificationException +* CB-7974 Cancel timeout timer if view is destroyed +* CB-7940 Disable exec bridge if bridgeSecret is wrong +* CB-7758 Allow content-url-hosted pages to access the bridge +* CB-6511 Fixes build for android when app name contains unicode characters. +* CB-7707 Added multipart PluginResult +* CB-6837 Fix leaked window when hitting back button while alert being rendered +* CB-7674 Move preference activation back into onCreate() +* CB-7499 Support RTL text direction +* CB-7330 Don't run check_reqs for bin/create. + ### 3.6.4 (Sept 30, 2014) ### * Set VERSION to 3.6.4 (via coho) diff --git a/VERSION b/VERSION index a4ce38ebae..0b2eb36f50 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.7.0-dev +3.7.2 diff --git a/bin/lib/create.js b/bin/lib/create.js index c0d0c50f06..5471bb4da3 100755 --- a/bin/lib/create.js +++ b/bin/lib/create.js @@ -72,6 +72,7 @@ function copyJsAndLibrary(projectPath, shared, projectName) { shell.cp('-f', path.join(ROOT, 'framework', 'AndroidManifest.xml'), nestedCordovaLibPath); shell.cp('-f', path.join(ROOT, 'framework', 'project.properties'), nestedCordovaLibPath); shell.cp('-f', path.join(ROOT, 'framework', 'build.gradle'), nestedCordovaLibPath); + shell.cp('-f', path.join(ROOT, 'framework', 'cordova.gradle'), nestedCordovaLibPath); shell.cp('-r', path.join(ROOT, 'framework', 'src'), nestedCordovaLibPath); // Create an eclipse project file and set the name of it to something unique. // Without this, you can't import multiple CordovaLib projects into the same workspace. @@ -122,7 +123,6 @@ function copyBuildRules(projectPath) { shell.cp('-f', path.join(srcDir, 'custom_rules.xml'), projectPath); shell.cp('-f', path.join(srcDir, 'build.gradle'), projectPath); - shell.cp('-f', path.join(srcDir, 'cordova.gradle'), projectPath); } function copyScripts(projectPath) { diff --git a/bin/templates/cordova/lib/build.js b/bin/templates/cordova/lib/build.js index 5ad3e76f0e..e441caa711 100644 --- a/bin/templates/cordova/lib/build.js +++ b/bin/templates/cordova/lib/build.js @@ -69,11 +69,13 @@ function findOutputApksHelper(dir, build_type, arch) { if (ret.length === 0) { return ret; } + // Assume arch-specific build if newest api has -x86 or -arm. var archSpecific = !!/-x86|-arm/.exec(ret[0]); + // And show only arch-specific ones (or non-arch-specific) ret = ret.filter(function(p) { return !!/-x86|-arm/.exec(p) == archSpecific; }); - if (arch) { + if (arch && ret.length > 1) { ret = ret.filter(function(p) { return p.indexOf('-' + arch) != -1; }); @@ -175,20 +177,16 @@ var builders = { }, gradle: { getArgs: function(cmd, arch, extraArgs) { - if (arch == 'arm' && cmd == 'debug') { - cmd = 'assembleArmv7Debug'; - } else if (arch == 'arm' && cmd == 'release') { - cmd = 'assembleArmv7Release'; - } else if (arch == 'x86' && cmd == 'debug') { - cmd = 'assembleX86Debug'; - } else if (arch == 'x86' && cmd == 'release') { - cmd = 'assembleX86Release'; + if (cmd == 'release') { + cmd = 'cdvBuildRelease'; } else if (cmd == 'debug') { - cmd = 'assembleDebug'; - } else if (cmd == 'release') { - cmd = 'assembleRelease'; + cmd = 'cdvBuildDebug'; } var args = [cmd, '-b', path.join(ROOT, 'build.gradle')]; + if (arch) { + args.push('-PcdvBuildArch=' + arch); + } + // 10 seconds -> 6 seconds args.push('-Dorg.gradle.daemon=true'); args.push.apply(args, extraArgs); @@ -260,7 +258,7 @@ var builders = { var wrapper = path.join(ROOT, 'gradlew'); var args = this.getArgs(build_type == 'debug' ? 'debug' : 'release', arch, extraArgs); return Q().then(function() { - console.log('Running: ' + wrapper + ' ' + extraArgs.join(' ')); + console.log('Running: ' + wrapper + ' ' + args.concat(extraArgs).join(' ')); return spawn(wrapper, args); }); }, @@ -270,7 +268,7 @@ var builders = { var wrapper = path.join(ROOT, 'gradlew'); var args = builder.getArgs('clean', null, extraArgs); return Q().then(function() { - console.log('Running: ' + wrapper + ' ' + extraArgs.join(' ')); + console.log('Running: ' + wrapper + ' ' + args.concat(extraArgs).join(' ')); return spawn(wrapper, args); }); }, @@ -319,8 +317,8 @@ function parseOpts(options, resolvedTarget) { for (var i=0; options && (i < options.length); ++i) { if (/^--/.exec(options[i])) { var keyValue = options[i].substring(2).split('='); - var flagName = keyValue[0]; - var flagValue = keyValue[1]; + var flagName = keyValue.shift(); + var flagValue = keyValue.join('='); if (multiValueArgs[flagName] && !flagValue) { flagValue = options[i + 1]; ++i; @@ -359,10 +357,7 @@ function parseOpts(options, resolvedTarget) { } } - var multiApk = ret.buildMethod == 'gradle' && process.env['BUILD_MULTIPLE_APKS']; - if (multiApk && !/0|false|no/i.exec(multiApk)) { - ret.arch = resolvedTarget && resolvedTarget.arch; - } + ret.arch = resolvedTarget && resolvedTarget.arch; return ret; } diff --git a/bin/templates/cordova/lib/run.js b/bin/templates/cordova/lib/run.js index db783d9126..ced23a09fc 100644 --- a/bin/templates/cordova/lib/run.js +++ b/bin/templates/cordova/lib/run.js @@ -39,7 +39,7 @@ var path = require('path'), var list = false; for (var i=2; i props.load(reader) } - def storeFile = new File(ensureValueExists(propsFilePath, props, 'storeFile')) + def storeFile = new File(privateHelpers.ensureValueExists(propsFilePath, props, 'storeFile')) if (!storeFile.isAbsolute()) { storeFile = RelativePath.parse(true, storeFile.toString()).getFile(propsFile.getParentFile()) } if (!storeFile.exists()) { throw new FileNotFoundException('Keystore file does not exist: ' + storeFile.getAbsolutePath()) } - signingConfig.keyAlias = ensureValueExists(propsFilePath, props, 'keyAlias') + signingConfig.keyAlias = privateHelpers.ensureValueExists(propsFilePath, props, 'keyAlias') signingConfig.keyPassword = props.get('keyPassword', signingConfig.keyPassword) signingConfig.storeFile = storeFile signingConfig.storePassword = props.get('storePassword', signingConfig.storePassword) diff --git a/framework/assets/www/cordova.js b/framework/assets/www/cordova.js index 2fcf2469b9..07c7c6ee3c 100644 --- a/framework/assets/www/cordova.js +++ b/framework/assets/www/cordova.js @@ -1,5 +1,5 @@ // Platform: android -// ee7b91f28e3780afb44222a2d950ccc1bebd0b87 +// 24ab6855470f2dc0662624b597c98585e56a1666 /* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file @@ -19,7 +19,7 @@ under the License. */ ;(function() { -var PLATFORM_VERSION_BUILD_LABEL = '3.7.0-dev'; +var PLATFORM_VERSION_BUILD_LABEL = '3.7.2'; // file: src/scripts/require.js /*jshint -W079 */ @@ -1977,4 +1977,4 @@ window.cordova = require('cordova'); require('cordova/init'); -})(); \ No newline at end of file +})(); diff --git a/framework/build.gradle b/framework/build.gradle index cace204294..2565633ffe 100644 --- a/framework/build.gradle +++ b/framework/build.gradle @@ -23,23 +23,21 @@ buildscript { mavenCentral() } - dependencies { - // Switch the Android Gradle plugin version requirement depending on the - // installed version of Gradle. This dependency is documented at - // http://tools.android.com/tech-docs/new-build-system/version-compatibility - // and https://issues.apache.org/jira/browse/CB-8143 - if (gradle.gradleVersion >= "2.2") { - dependencies { - classpath 'com.android.tools.build:gradle:1.0.0+' - } - } else if (gradle.gradleVersion >= "2.1") { - dependencies { - classpath 'com.android.tools.build:gradle:0.14.0+' - } - } else { - dependencies { - classpath 'com.android.tools.build:gradle:0.12.0+' - } + // Switch the Android Gradle plugin version requirement depending on the + // installed version of Gradle. This dependency is documented at + // http://tools.android.com/tech-docs/new-build-system/version-compatibility + // and https://issues.apache.org/jira/browse/CB-8143 + if (gradle.gradleVersion >= "2.2") { + dependencies { + classpath 'com.android.tools.build:gradle:1.0.0+' + } + } else if (gradle.gradleVersion >= "2.1") { + dependencies { + classpath 'com.android.tools.build:gradle:0.14.0+' + } + } else { + dependencies { + classpath 'com.android.tools.build:gradle:0.12.0+' } } } diff --git a/bin/templates/project/cordova.gradle b/framework/cordova.gradle similarity index 84% rename from bin/templates/project/cordova.gradle rename to framework/cordova.gradle index 8b371bf99b..5ce93f8ff4 100644 --- a/bin/templates/project/cordova.gradle +++ b/framework/cordova.gradle @@ -20,16 +20,19 @@ import java.util.regex.Pattern import groovy.swing.SwingBuilder +String doEnsureValueExists(filePath, props, key) { + if (props.get(key) == null) { + throw new GradleException(filePath + ': Missing key required "' + key + '"') + } + return props.get(key) +} -String getProjectTarget(String defaultTarget) { - def manifestFile = file("project.properties") - def pattern = Pattern.compile("target\\s*=\\s*(.*)") - def matcher = pattern.matcher(manifestFile.getText()) - if (matcher.find()) { - matcher.group(1) - } else { - defaultTarget +String doGetProjectTarget() { + def props = new Properties() + file('project.properties').withReader { reader -> + props.load(reader) } + return doEnsureValueExists('project.properties', props, 'target') } String[] getAvailableBuildTools() { @@ -39,7 +42,7 @@ String[] getAvailableBuildTools() { .sort { a, b -> compareVersions(b, a) } } -String findLatestInstalledBuildTools(String minBuildToolsVersion) { +String doFindLatestInstalledBuildTools(String minBuildToolsVersion) { def availableBuildToolsVersions try { availableBuildToolsVersions = getAvailableBuildTools() @@ -117,7 +120,7 @@ String getAndroidSdkDir() { androidSdkDir } -def extractIntFromManifest(name) { +def doExtractIntFromManifest(name) { def manifestFile = file(android.sourceSets.main.manifest.srcFile) def pattern = Pattern.compile(name + "=\"(\\d+)\"") def matcher = pattern.matcher(manifestFile.getText()) @@ -125,7 +128,7 @@ def extractIntFromManifest(name) { return Integer.parseInt(matcher.group(1)) } -def promptForPassword(msg) { +def doPromptForPassword(msg) { if (System.console() == null) { def ret = null new SwingBuilder().edt { @@ -153,9 +156,10 @@ def promptForPassword(msg) { ext { // These helpers are shared, but are not guaranteed to be stable / unchanged. privateHelpers = {} - privateHelpers.getProjectTarget = { defaultValue -> getProjectTarget(defaultValue) } - privateHelpers.findLatestInstalledBuildTools = { defaultValue -> findLatestInstalledBuildTools(defaultValue) } - privateHelpers.extractIntFromManifest = { name -> extractIntFromManifest(name) } - privateHelpers.promptForPassword = { msg -> promptForPassword(msg) } + privateHelpers.getProjectTarget = { doGetProjectTarget() } + privateHelpers.findLatestInstalledBuildTools = { doFindLatestInstalledBuildTools('19.1.0') } + privateHelpers.extractIntFromManifest = { name -> doExtractIntFromManifest(name) } + privateHelpers.promptForPassword = { msg -> doPromptForPassword(msg) } + privateHelpers.ensureValueExists = { filePath, props, key -> doEnsureValueExists(filePath, props, key) } } diff --git a/framework/src/org/apache/cordova/Config.java b/framework/src/org/apache/cordova/Config.java index f13292c3f1..53c6871278 100644 --- a/framework/src/org/apache/cordova/Config.java +++ b/framework/src/org/apache/cordova/Config.java @@ -37,7 +37,6 @@ public static void init(Activity action) { parser = new ConfigXmlParser(); parser.parse(action); parser.getPreferences().setPreferencesBundle(action.getIntent().getExtras()); - parser.getPreferences().copyIntoIntentExtras(action); } // Intended to be used for testing only; creates an empty configuration. diff --git a/framework/src/org/apache/cordova/CordovaActivity.java b/framework/src/org/apache/cordova/CordovaActivity.java index b61fa98707..4d6a40374c 100755 --- a/framework/src/org/apache/cordova/CordovaActivity.java +++ b/framework/src/org/apache/cordova/CordovaActivity.java @@ -36,7 +36,6 @@ Licensed to the Apache Software Foundation (ASF) under one import android.content.Intent; import android.graphics.Color; import android.media.AudioManager; -import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.Display; @@ -48,7 +47,6 @@ Licensed to the Apache Software Foundation (ASF) under one import android.view.ViewParent; import android.view.Window; import android.view.WindowManager; -import android.webkit.ValueCallback; import android.webkit.WebViewClient; import android.widget.LinearLayout; @@ -103,7 +101,8 @@ public class CordovaActivity extends Activity implements CordovaInterface { private int activityState = 0; // 0=starting, 1=running (after 1st resume), 2=shutting down // Plugin to call when activity result is received - protected CordovaPlugin activityResultCallback = null; + protected int activityResultRequestCode; + protected CordovaPlugin activityResultCallback; protected boolean activityResultKeepRunning; /* @@ -233,7 +232,6 @@ protected void loadConfig() { parser.parse(this); preferences = parser.getPreferences(); preferences.setPreferencesBundle(getIntent().getExtras()); - preferences.copyIntoIntentExtras(this); internalWhitelist = parser.getInternalWhitelist(); externalWhitelist = parser.getExternalWhitelist(); launchUrl = parser.getLaunchUrl(); @@ -334,11 +332,6 @@ public void init(CordovaWebView webView, CordovaWebViewClient webViewClient, Cor } appView = webView != null ? webView : makeWebView(); - if (appView.pluginManager == null) { - appView.init(this, webViewClient != null ? webViewClient : makeWebViewClient(appView), - webChromeClient != null ? webChromeClient : makeChromeClient(appView), - pluginEntries, internalWhitelist, externalWhitelist, preferences); - } // TODO: Have the views set this themselves. if (preferences.getBoolean("DisallowOverscroll", false)) { @@ -346,6 +339,13 @@ public void init(CordovaWebView webView, CordovaWebViewClient webViewClient, Cor } createViews(); + // Init plugins only after creating views + if (appView.pluginManager == null) { + appView.init(this, webViewClient != null ? webViewClient : makeWebViewClient(appView), + webChromeClient != null ? webChromeClient : makeChromeClient(appView), + pluginEntries, internalWhitelist, externalWhitelist, preferences); + } + // Wire the hardware volume controls to control media if desired. String volumePref = preferences.getString("DefaultVolumeStream", ""); if ("media".equals(volumePref.toLowerCase(Locale.ENGLISH))) { @@ -666,7 +666,7 @@ public void endActivity() { * @param requestCode The request code that is passed to callback to identify the activity */ public void startActivityForResult(CordovaPlugin command, Intent intent, int requestCode) { - this.activityResultCallback = command; + setActivityResultCallback(command); this.activityResultKeepRunning = this.keepRunning; // If multitasking turned on, then disable it for activities that return results @@ -674,8 +674,19 @@ public void startActivityForResult(CordovaPlugin command, Intent intent, int req this.keepRunning = false; } - // Start activity - super.startActivityForResult(intent, requestCode); + try { + startActivityForResult(intent, requestCode); + } catch (RuntimeException e) { // E.g.: ActivityNotFoundException + activityResultCallback = null; + throw e; + } + } + + @Override + public void startActivityForResult(Intent intent, int requestCode, Bundle options) { + // Capture requestCode here so that it is captured in the setActivityResultCallback() case. + activityResultRequestCode = requestCode; + super.startActivityForResult(intent, requestCode, options); } /** @@ -685,37 +696,34 @@ public void startActivityForResult(CordovaPlugin command, Intent intent, int req * @param requestCode The request code originally supplied to startActivityForResult(), * allowing you to identify who this result came from. * @param resultCode The integer result code returned by the child activity through its setResult(). - * @param data An Intent, which can return result data to the caller (various data can be attached to Intent "extras"). + * @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras"). */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { - LOG.d(TAG, "Incoming Result"); + LOG.d(TAG, "Incoming Result. Request code = " + requestCode); super.onActivityResult(requestCode, resultCode, intent); - Log.d(TAG, "Request code = " + requestCode); - if (appView != null && requestCode == CordovaChromeClient.FILECHOOSER_RESULTCODE) { - ValueCallback mUploadMessage = this.appView.getWebChromeClient().getValueCallback(); - Log.d(TAG, "did we get here?"); - if (null == mUploadMessage) - return; - Uri result = intent == null || resultCode != Activity.RESULT_OK ? null : intent.getData(); - Log.d(TAG, "result = " + result); - mUploadMessage.onReceiveValue(result); - mUploadMessage = null; - } CordovaPlugin callback = this.activityResultCallback; if(callback == null && initCallbackClass != null) { // The application was restarted, but had defined an initial callback // before being shut down. - this.activityResultCallback = appView.pluginManager.getPlugin(initCallbackClass); - callback = this.activityResultCallback; + callback = appView.pluginManager.getPlugin(initCallbackClass); } - if(callback != null) { + initCallbackClass = null; + activityResultCallback = null; + + if (callback != null) { LOG.d(TAG, "We have a callback to send this result to"); callback.onActivityResult(requestCode, resultCode, intent); + } else { + LOG.w(TAG, "Got an activity result, but no plugin was registered to receive it."); } } public void setActivityResultCallback(CordovaPlugin plugin) { + // Cancel any previously pending activity. + if (activityResultCallback != null) { + activityResultCallback.onActivityResult(activityResultRequestCode, Activity.RESULT_CANCELED, null); + } this.activityResultCallback = plugin; } diff --git a/framework/src/org/apache/cordova/CordovaChromeClient.java b/framework/src/org/apache/cordova/CordovaChromeClient.java index ff0c0f2f78..31759d0aa5 100755 --- a/framework/src/org/apache/cordova/CordovaChromeClient.java +++ b/framework/src/org/apache/cordova/CordovaChromeClient.java @@ -22,10 +22,14 @@ Licensed to the Apache Software Foundation (ASF) under one import org.apache.cordova.LOG; import android.annotation.TargetApi; +import android.app.Activity; import android.app.AlertDialog; +import android.content.ActivityNotFoundException; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; +import android.os.Build; +import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; @@ -67,10 +71,7 @@ public class CordovaChromeClient extends WebChromeClient { //Keep track of last AlertDialog showed private AlertDialog lastHandledDialog; - - // File Chooser - public ValueCallback mUploadMessage; - + @Deprecated public CordovaChromeClient(CordovaInterface cordova) { this.cordova = cordova; @@ -309,7 +310,10 @@ public View getVideoLoadingProgressView() { } return mVideoProgressView; } - + + // support: + // openFileChooser() is for pre KitKat and in KitKat mr1 (it's known broken in KitKat). + // For Lollipop, we use onShowFileChooser(). public void openFileChooser(ValueCallback uploadMsg) { this.openFileChooser(uploadMsg, "*/*"); } @@ -318,20 +322,41 @@ public void openFileChooser( ValueCallback uploadMsg, String acceptType ) { this.openFileChooser(uploadMsg, acceptType, null); } - public void openFileChooser(ValueCallback uploadMsg, String acceptType, String capture) + public void openFileChooser(final ValueCallback uploadMsg, String acceptType, String capture) { - mUploadMessage = uploadMsg; - Intent i = new Intent(Intent.ACTION_GET_CONTENT); - i.addCategory(Intent.CATEGORY_OPENABLE); - i.setType("*/*"); - this.cordova.getActivity().startActivityForResult(Intent.createChooser(i, "File Browser"), - FILECHOOSER_RESULTCODE); + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + cordova.startActivityForResult(new CordovaPlugin() { + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + Uri result = intent == null || resultCode != Activity.RESULT_OK ? null : intent.getData(); + Log.d(TAG, "Receive file chooser URL: " + result); + uploadMsg.onReceiveValue(result); + } + }, intent, FILECHOOSER_RESULTCODE); } - - public ValueCallback getValueCallback() { - return this.mUploadMessage; + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean onShowFileChooser(WebView webView, final ValueCallback filePathsCallback, final WebChromeClient.FileChooserParams fileChooserParams) { + Intent intent = fileChooserParams.createIntent(); + try { + cordova.startActivityForResult(new CordovaPlugin() { + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + Uri[] result = WebChromeClient.FileChooserParams.parseResult(resultCode, intent); + Log.d(TAG, "Receive file chooser URL: " + result); + filePathsCallback.onReceiveValue(result); + } + }, intent, FILECHOOSER_RESULTCODE); + } catch (ActivityNotFoundException e) { + Log.w("No activity found to handle file chooser intent.", e); + filePathsCallback.onReceiveValue(null); + } + return true; } - + public void destroyLastDialog(){ if(lastHandledDialog != null){ lastHandledDialog.cancel(); diff --git a/framework/src/org/apache/cordova/CordovaClientCertRequest.java b/framework/src/org/apache/cordova/CordovaClientCertRequest.java new file mode 100644 index 0000000000..5dd0ecaec5 --- /dev/null +++ b/framework/src/org/apache/cordova/CordovaClientCertRequest.java @@ -0,0 +1,96 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova; + +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +import android.webkit.ClientCertRequest; + +/** + * Implementation of the ICordovaClientCertRequest for Android WebView. + */ +public class CordovaClientCertRequest implements ICordovaClientCertRequest { + + private final ClientCertRequest request; + + public CordovaClientCertRequest(ClientCertRequest request) { + this.request = request; + } + + /** + * Cancel this request + */ + public void cancel() + { + request.cancel(); + } + + /* + * Returns the host name of the server requesting the certificate. + */ + public String getHost() + { + return request.getHost(); + } + + /* + * Returns the acceptable types of asymmetric keys (can be null). + */ + public String[] getKeyTypes() + { + return request.getKeyTypes(); + } + + /* + * Returns the port number of the server requesting the certificate. + */ + public int getPort() + { + return request.getPort(); + } + + /* + * Returns the acceptable certificate issuers for the certificate matching the private key (can be null). + */ + public Principal[] getPrincipals() + { + return request.getPrincipals(); + } + + /* + * Ignore the request for now. Do not remember user's choice. + */ + public void ignore() + { + request.ignore(); + } + + /* + * Proceed with the specified private key and client certificate chain. Remember the user's positive choice and use it for future requests. + * + * @param privateKey The privateKey + * @param chain The certificate chain + */ + public void proceed(PrivateKey privateKey, X509Certificate[] chain) + { + request.proceed(privateKey, chain); + } +} diff --git a/framework/src/org/apache/cordova/CordovaHttpAuthHandler.java b/framework/src/org/apache/cordova/CordovaHttpAuthHandler.java new file mode 100644 index 0000000000..724381e2c1 --- /dev/null +++ b/framework/src/org/apache/cordova/CordovaHttpAuthHandler.java @@ -0,0 +1,51 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova; + +import android.webkit.HttpAuthHandler; + +/** + * Specifies interface for HTTP auth handler object which is used to handle auth requests and + * specifying user credentials. + */ +public class CordovaHttpAuthHandler implements ICordovaHttpAuthHandler { + + private final HttpAuthHandler handler; + + public CordovaHttpAuthHandler(HttpAuthHandler handler) { + this.handler = handler; + } + + /** + * Instructs the WebView to cancel the authentication request. + */ + public void cancel () { + this.handler.cancel(); + } + + /** + * Instructs the WebView to proceed with the authentication with the given credentials. + * + * @param username + * @param password + */ + public void proceed (String username, String password) { + this.handler.proceed(username, password); + } +} diff --git a/framework/src/org/apache/cordova/CordovaPlugin.java b/framework/src/org/apache/cordova/CordovaPlugin.java index a68d3d7eb8..1748407ab1 100644 --- a/framework/src/org/apache/cordova/CordovaPlugin.java +++ b/framework/src/org/apache/cordova/CordovaPlugin.java @@ -198,4 +198,34 @@ public Uri remapUri(Uri uri) { */ public void onReset() { } + + /** + * Called when the system received an HTTP authentication request. Plugin can use + * the supplied HttpAuthHandler to process this auth challenge. + * + * @param view The WebView that is initiating the callback + * @param handler The HttpAuthHandler used to set the WebView's response + * @param host The host requiring authentication + * @param realm The realm for which authentication is required + * + * @return Returns True if plugin will resolve this auth challenge, otherwise False + * + */ + public boolean onReceivedHttpAuthRequest(CordovaWebView view, ICordovaHttpAuthHandler handler, String host, String realm) { + return false; + } + + /** + * Called when he system received an SSL client certificate request. Plugin can use + * the supplied ClientCertRequest to process this certificate challenge. + * + * @param view The WebView that is initiating the callback + * @param request The client certificate request + * + * @return Returns True if plugin will resolve this auth challenge, otherwise False + * + */ + public boolean onReceivedClientCertRequest(CordovaWebView view, ICordovaClientCertRequest request) { + return false; + } } diff --git a/framework/src/org/apache/cordova/CordovaPreferences.java b/framework/src/org/apache/cordova/CordovaPreferences.java index ed0b9b8951..27fb82682f 100644 --- a/framework/src/org/apache/cordova/CordovaPreferences.java +++ b/framework/src/org/apache/cordova/CordovaPreferences.java @@ -61,13 +61,6 @@ public boolean getBoolean(String name, boolean defaultValue) { String value = prefs.get(name); if (value != null) { return Boolean.parseBoolean(value); - } else if (preferencesBundleExtras != null) { - Object bundleValue = preferencesBundleExtras.get(name); - if (bundleValue instanceof String) { - return "true".equals(bundleValue); - } - // Gives a nice warning if type is wrong. - return preferencesBundleExtras.getBoolean(name, defaultValue); } return defaultValue; } @@ -78,13 +71,6 @@ public int getInteger(String name, int defaultValue) { if (value != null) { // Use Integer.decode() can't handle it if the highest bit is set. return (int)(long)Long.decode(value); - } else if (preferencesBundleExtras != null) { - Object bundleValue = preferencesBundleExtras.get(name); - if (bundleValue instanceof String) { - return Integer.valueOf((String)bundleValue); - } - // Gives a nice warning if type is wrong. - return preferencesBundleExtras.getInt(name, defaultValue); } return defaultValue; } @@ -94,14 +80,7 @@ public double getDouble(String name, double defaultValue) { String value = prefs.get(name); if (value != null) { return Double.valueOf(value); - } else if (preferencesBundleExtras != null) { - Object bundleValue = preferencesBundleExtras.get(name); - if (bundleValue instanceof String) { - return Double.valueOf((String)bundleValue); - } - // Gives a nice warning if type is wrong. - return preferencesBundleExtras.getDouble(name, defaultValue); - } + } return defaultValue; } @@ -110,69 +89,8 @@ public String getString(String name, String defaultValue) { String value = prefs.get(name); if (value != null) { return value; - } else if (preferencesBundleExtras != null && !"errorurl".equals(name)) { - Object bundleValue = preferencesBundleExtras.get(name); - if (bundleValue != null) { - return bundleValue.toString(); - } - } + } return defaultValue; } - // Plugins should not rely on values within the intent since this does not work - // for apps with multiple webviews. Instead, they should retrieve prefs from the - // Config object associated with their webview. - public void copyIntoIntentExtras(Activity action) { - for (String name : prefs.keySet()) { - String value = prefs.get(name); - if (value == null) { - continue; - } - if (name.equals("loglevel")) { - LOG.setLogLevel(value); - } else if (name.equals("splashscreen")) { - // Note: We should probably pass in the classname for the variable splash on splashscreen! - int resource = action.getResources().getIdentifier(value, "drawable", action.getClass().getPackage().getName()); - if(resource == 0) { - resource = action.getResources().getIdentifier(value, "drawable", action.getPackageName()); - } - action.getIntent().putExtra(name, resource); - } - else if(name.equals("backgroundcolor")) { - int asInt = (int)(long)Long.decode(value); - action.getIntent().putExtra(name, asInt); - } - else if(name.equals("loadurltimeoutvalue")) { - int asInt = Integer.decode(value); - action.getIntent().putExtra(name, asInt); - } - else if(name.equals("splashscreendelay")) { - int asInt = Integer.decode(value); - action.getIntent().putExtra(name, asInt); - } - else if(name.equals("keeprunning")) - { - boolean asBool = Boolean.parseBoolean(value); - action.getIntent().putExtra(name, asBool); - } - else if(name.equals("inappbrowserstorageenabled")) - { - boolean asBool = Boolean.parseBoolean(value); - action.getIntent().putExtra(name, asBool); - } - else if(name.equals("disallowoverscroll")) - { - boolean asBool = Boolean.parseBoolean(value); - action.getIntent().putExtra(name, asBool); - } - else - { - action.getIntent().putExtra(name, value); - } - } - // In the normal case, the intent extras are null until the first call to putExtra(). - if (preferencesBundleExtras == null) { - preferencesBundleExtras = action.getIntent().getExtras(); - } - } } diff --git a/framework/src/org/apache/cordova/CordovaWebView.java b/framework/src/org/apache/cordova/CordovaWebView.java index 8c6e47121b..14919c4b79 100755 --- a/framework/src/org/apache/cordova/CordovaWebView.java +++ b/framework/src/org/apache/cordova/CordovaWebView.java @@ -62,7 +62,7 @@ Licensed to the Apache Software Foundation (ASF) under one public class CordovaWebView extends WebView { public static final String TAG = "CordovaWebView"; - public static final String CORDOVA_VERSION = "3.7.0-dev"; + public static final String CORDOVA_VERSION = "3.7.2"; private HashSet boundKeyCodes = new HashSet(); @@ -405,7 +405,7 @@ public void loadUrlIntoView(final String url, boolean recreatePlugins) { // Create a timeout timer for loadUrl final CordovaWebView me = this; final int currentLoadUrlTimeout = me.loadUrlTimeout; - final int loadUrlTimeoutValue = Integer.parseInt(this.getProperty("LoadUrlTimeoutValue", "20000")); + final int loadUrlTimeoutValue = preferences.getInteger("LoadUrlTimeoutValue", 20000); // Timeout error method final Runnable loadError = new Runnable() { @@ -454,7 +454,7 @@ void loadUrlNow(String url) { if (LOG.isLoggable(LOG.DEBUG) && !url.startsWith("javascript:")) { LOG.d(TAG, ">>> loadUrlNow()"); } - if (url.startsWith("file://") || url.startsWith("javascript:") || internalWhitelist.isUrlWhiteListed(url)) { + if (url.startsWith("file://") || url.startsWith("javascript:") || url.startsWith("about:") || internalWhitelist.isUrlWhiteListed(url)) { super.loadUrl(url); } } @@ -609,26 +609,6 @@ public void showWebPage(String url, boolean openExternal, boolean clearHistory, } } - /** - * Get string property for activity. - * - * @param name - * @param defaultValue - * @return the String value for the named property - */ - public String getProperty(String name, String defaultValue) { - Bundle bundle = this.cordova.getActivity().getIntent().getExtras(); - if (bundle == null) { - return defaultValue; - } - name = name.toLowerCase(Locale.getDefault()); - Object p = bundle.get(name); - if (p == null) { - return defaultValue; - } - return p.toString(); - } - @Override public boolean onKeyDown(int keyCode, KeyEvent event) { diff --git a/framework/src/org/apache/cordova/CordovaWebViewClient.java b/framework/src/org/apache/cordova/CordovaWebViewClient.java index f65a9760fe..6b3b46085a 100755 --- a/framework/src/org/apache/cordova/CordovaWebViewClient.java +++ b/framework/src/org/apache/cordova/CordovaWebViewClient.java @@ -21,7 +21,6 @@ Licensed to the Apache Software Foundation (ASF) under one import java.util.Hashtable; import org.apache.cordova.CordovaInterface; - import org.apache.cordova.LOG; import org.json.JSONException; import org.json.JSONObject; @@ -33,6 +32,7 @@ Licensed to the Apache Software Foundation (ASF) under one import android.graphics.Bitmap; import android.net.http.SslError; import android.view.View; +import android.webkit.ClientCertRequest; import android.webkit.HttpAuthHandler; import android.webkit.SslErrorHandler; import android.webkit.WebView; @@ -115,15 +115,45 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { @Override public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { - // Get the authentication token + // Get the authentication token (if specified) AuthenticationToken token = this.getAuthenticationToken(host, realm); if (token != null) { handler.proceed(token.getUserName(), token.getPassword()); + return; + } + + // Check if there is some plugin which can resolve this auth challenge + PluginManager pluginManager = this.appView.pluginManager; + if (pluginManager != null && pluginManager.onReceivedHttpAuthRequest(this.appView, new CordovaHttpAuthHandler(handler), host, realm)) { + this.appView.loadUrlTimeout++; + return; } - else { - // Handle 401 like we'd normally do! - super.onReceivedHttpAuthRequest(view, handler, host, realm); + + // By default handle 401 like we'd normally do! + super.onReceivedHttpAuthRequest(view, handler, host, realm); + } + + /** + * On received client cert request. + * The method forwards the request to any running plugins before using the default implementation. + * + * @param view + * @param request + */ + @Override + @TargetApi(21) + public void onReceivedClientCertRequest (WebView view, ClientCertRequest request) + { + + // Check if there is some plugin which can resolve this certificate request + PluginManager pluginManager = this.appView.pluginManager; + if (pluginManager != null && pluginManager.onReceivedClientCertRequest(this.appView, new CordovaClientCertRequest(request))) { + this.appView.loadUrlTimeout++; + return; } + + // By default pass to WebViewClient + super.onReceivedClientCertRequest(view, request); } /** @@ -163,8 +193,8 @@ public void onPageStarted(WebView view, String url, Bitmap favicon) { @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); - // Ignore excessive calls. - if (!isCurrentlyLoading) { + // Ignore excessive calls, if url is not about:blank (CB-8317). + if (!isCurrentlyLoading && !url.startsWith("about:")) { return; } isCurrentlyLoading = false; diff --git a/framework/src/org/apache/cordova/ICordovaClientCertRequest.java b/framework/src/org/apache/cordova/ICordovaClientCertRequest.java new file mode 100644 index 0000000000..455d2f9d35 --- /dev/null +++ b/framework/src/org/apache/cordova/ICordovaClientCertRequest.java @@ -0,0 +1,66 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova; + +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +/** + * Specifies interface for handling certificate requests. + */ +public interface ICordovaClientCertRequest { + /** + * Cancel this request + */ + public void cancel(); + + /* + * Returns the host name of the server requesting the certificate. + */ + public String getHost(); + + /* + * Returns the acceptable types of asymmetric keys (can be null). + */ + public String[] getKeyTypes(); + + /* + * Returns the port number of the server requesting the certificate. + */ + public int getPort(); + + /* + * Returns the acceptable certificate issuers for the certificate matching the private key (can be null). + */ + public Principal[] getPrincipals(); + + /* + * Ignore the request for now. Do not remember user's choice. + */ + public void ignore(); + + /* + * Proceed with the specified private key and client certificate chain. Remember the user's positive choice and use it for future requests. + * + * @param privateKey The privateKey + * @param chain The certificate chain + */ + public void proceed(PrivateKey privateKey, X509Certificate[] chain); +} \ No newline at end of file diff --git a/framework/src/org/apache/cordova/ICordovaHttpAuthHandler.java b/framework/src/org/apache/cordova/ICordovaHttpAuthHandler.java new file mode 100644 index 0000000000..c55818ac6d --- /dev/null +++ b/framework/src/org/apache/cordova/ICordovaHttpAuthHandler.java @@ -0,0 +1,38 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova; + +/** + * Specifies interface for HTTP auth handler object which is used to handle auth requests and + * specifying user credentials. + */ + public interface ICordovaHttpAuthHandler { + /** + * Instructs the WebView to cancel the authentication request. + */ + public void cancel (); + + /** + * Instructs the WebView to proceed with the authentication with the given credentials. + * + * @param username The user name + * @param password The password + */ + public void proceed (String username, String password); +} \ No newline at end of file diff --git a/framework/src/org/apache/cordova/PluginManager.java b/framework/src/org/apache/cordova/PluginManager.java index 7ddf300599..2fb6b2b896 100755 --- a/framework/src/org/apache/cordova/PluginManager.java +++ b/framework/src/org/apache/cordova/PluginManager.java @@ -242,6 +242,46 @@ public void onPause(boolean multitasking) { } } + /** + * Called when the system received an HTTP authentication request. Plugins can use + * the supplied HttpAuthHandler to process this auth challenge. + * + * @param view The WebView that is initiating the callback + * @param handler The HttpAuthHandler used to set the WebView's response + * @param host The host requiring authentication + * @param realm The realm for which authentication is required + * + * @return Returns True if there is a plugin which will resolve this auth challenge, otherwise False + * + */ + public boolean onReceivedHttpAuthRequest(CordovaWebView view, ICordovaHttpAuthHandler handler, String host, String realm) { + for (CordovaPlugin plugin : this.pluginMap.values()) { + if (plugin != null && plugin.onReceivedHttpAuthRequest(view, handler, host, realm)) { + return true; + } + } + return false; + } + + /** + * Called when he system received an SSL client certificate request. Plugin can use + * the supplied ClientCertRequest to process this certificate challenge. + * + * @param view The WebView that is initiating the callback + * @param request The client certificate request + * + * @return Returns True if plugin will resolve this auth challenge, otherwise False + * + */ + public boolean onReceivedClientCertRequest(CordovaWebView view, ICordovaClientCertRequest request) { + for (CordovaPlugin plugin : this.pluginMap.values()) { + if (plugin != null && plugin.onReceivedClientCertRequest(view, request)) { + return true; + } + } + return false; + } + /** * Called when the activity will start interacting with the user. * diff --git a/framework/src/org/apache/cordova/SplashScreenInternal.java b/framework/src/org/apache/cordova/SplashScreenInternal.java index 715e418275..e1154a53fc 100644 --- a/framework/src/org/apache/cordova/SplashScreenInternal.java +++ b/framework/src/org/apache/cordova/SplashScreenInternal.java @@ -63,7 +63,7 @@ protected void pluginInitialize() { firstShow = false; loadSpinner(); - showSplashScreen(); + showSplashScreen(true); } @Override @@ -115,7 +115,7 @@ public Object onMessage(String id, Object data) { if ("hide".equals(data.toString())) { this.removeSplashScreen(); } else { - this.showSplashScreen(); + this.showSplashScreen(false); } } else if ("spinner".equals(id)) { if ("stop".equals(data.toString())) { @@ -143,7 +143,7 @@ public void run() { * Shows the splash screen over the full Activity */ @SuppressWarnings("deprecation") - private void showSplashScreen() { + private void showSplashScreen(final boolean hideAfterDelay) { final int splashscreenTime = preferences.getInteger("SplashScreenDelay", 3000); final int drawableId = preferences.getInteger("SplashDrawableId", 0); @@ -151,7 +151,7 @@ private void showSplashScreen() { if (this.splashDialog != null && splashDialog.isShowing()) { return; } - if (drawableId == 0 || splashscreenTime <= 0) { + if (drawableId == 0 || (splashscreenTime <= 0 && hideAfterDelay)) { return; } @@ -187,12 +187,14 @@ public void run() { splashDialog.show(); // Set Runnable to remove splash screen just in case - final Handler handler = new Handler(); - handler.postDelayed(new Runnable() { - public void run() { - removeSplashScreen(); - } - }, splashscreenTime); + if (hideAfterDelay) { + final Handler handler = new Handler(); + handler.postDelayed(new Runnable() { + public void run() { + removeSplashScreen(); + } + }, splashscreenTime); + } } }); } diff --git a/package.json b/package.json index a152ceb696..a2d7722cdc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-android", - "version": "3.7.0-dev", + "version": "3.7.2", "description": "cordova-android release", "main": "bin/create", "repository": { @@ -26,4 +26,4 @@ "jasmine-node": "~1", "promise-matchers": "~0" } -} \ No newline at end of file +}