Compare commits

..

89 Commits

Author SHA1 Message Date
Andrew Grieve
f4b315961b CB-8415 updated RELEASENOTES 2015-02-03 20:50:17 -05:00
Andrew Grieve
3bc755de7c Set VERSION to 3.7.1 (via coho) 2015-02-03 20:46:29 -05:00
Andrew Grieve
c5ffdff49b Prune 3.7.0 RELEASENOTES to a more glanceable list 2015-02-03 16:04:35 -05:00
Andrew Grieve
2829ed77cc CB-8411 Initialize plugins only after createViews() is called 2015-02-03 16:03:47 -05:00
Joe Bowser
f3885692d9 Updating RELASENOTES.md, this is in a weird spot, since you need the branch to exist to generate the notes 2015-01-21 08:25:37 -08:00
shingotoda
d7fc37d365 CB-8317 Make it work to load about:blank and to dispatch exit message (close #149) 2015-01-20 19:46:20 -05:00
Andrew Grieve
c3bdebdb83 CB-8026 Remove default target value from gradle file
Wasn't being used anyways, and it still referenced android-19
This also switches to using a Properties object rather than a RegEx
for parsing project.properties
2015-01-20 15:04:49 -05:00
Andrew Grieve
9514a3ed94 Move cordova.gradle from project template to CordovaLib
Make it easier to share with tests project.
Also, one less file in the project template is a good thing.
2015-01-20 15:04:46 -05:00
Andrew Grieve
050dd088cf gradle: Fix incorrect buildTools dependencies in framework's build.gradle
(although it didn't seem to hurt anything?)
2015-01-20 10:50:38 -05:00
Marcus Pridham
26f0401fda CB-8328 Allow plugins to handle certificate challenges (close #150)
This is a new API for Lollipop
2015-01-19 22:21:10 -05:00
sgrebnov
e4e04927b6 CB-8201 Add support for auth dialogs into Cordova Android 2015-01-19 22:21:05 -05:00
Andrew Grieve
6b3ef11715 Adds cdvPrintProps gradle task: dumps out all cdv properties
Useful for debugging.
2015-01-19 22:05:30 -05:00
Andrew Grieve
cf0fdf5ac0 CB-8255 Pass arch to gradle regardless of cdvBuildMultipleApks
This also pushes the "which target to build" logic into gradle, since
build.js doesn't actually know the value of `cdvBuildMultipleApks`.
2015-01-19 22:05:26 -05:00
Andrew Grieve
33614d1273 CB-8255 Fix cordova/build --gradleVar=--foo=bar stripping off =bar 2015-01-19 22:05:24 -05:00
Andrew Grieve
1f735935b9 Fix cordova/build not printing out all gradle args in console message 2015-01-19 22:05:23 -05:00
Andrew Grieve
8c14e33bb6 Fix cordova/run not finding apk when multi-arch is specified but only arch-independent apk exists 2015-01-19 22:05:22 -05:00
Andrew Grieve
7375154228 Fix exception for unknown flag in cordova/run 2015-01-19 22:05:21 -05:00
Andrew Grieve
1427c13504 Allow --ant, --gradle for cordova/run 2015-01-19 22:05:18 -05:00
Andrew Grieve
5538a231a8 CB-8017 Add support for <input type=file> for Lollipop
Also refactors a bit to remove related special-case code from CordovaActivity
2015-01-19 16:23:20 -05:00
Andrew Grieve
9be110683b CB-8329 Cancel outstanding ActivityResult requests when a new startActivityForResult occurs 2015-01-19 16:23:19 -05:00
Andrew Grieve
cfc36140bc CB-8280 android: Don't apply SplashScreenDelay when .show() is called explicitly 2015-01-19 13:42:35 -05:00
Murat Sutunc
72e947e5c7 CB-4914 Fix build whitespace issue 2015-01-13 10:00:04 -05:00
Joe Bowser
7320ce8ea7 Set VERSION to 3.7.0 (via coho) 2015-01-12 14:55:48 -08:00
Joe Bowser
3e07f26bc4 Update JS snapshot to version 3.7.0 (via coho) 2015-01-12 14:55:48 -08:00
Jason Chase
5415440829 CB-8210 Remove unused onDestroy channel (close #146)
- Channel was defined as internal event and fired by javascript eval()
- Rather than change firing of event, simpler to remove as was not used
2015-01-12 10:50:36 -05:00
Joe Bowser
15e19489e3 CB-8026: Bumping up Android Version and setting it up to allow third-party cookies. This might change later. 2015-01-09 11:31:29 -08:00
Andrew Grieve
c3610aa43c CB-8255 Use properties rather than environment variables for gradle settings 2015-01-08 15:26:24 -05:00
Andrew Grieve
c1ac3aa483 CB-8210 Drop events from native that occur before start-up 2015-01-05 16:32:39 -05:00
Jason Chase
291f111913 CB-8210 Use PluginResult for various events from native (close #144)
- Change to send events via plugin message channel: various buttons, pause/resume
2015-01-05 16:15:17 -05:00
Murat Sutunc
c2a6dcb6bd CB-8168 Add support for cordova/run --list (closes #139) 2015-01-03 21:05:52 -05:00
Jason Chase
3439746645 CB-8210 Use PluginResult instead of sendJavascript() for keyboard events (close #142)
- Initialize a message channel for native -> Javascript in the core App plugin
- Change keyboard detection to send events via plugin message channel, instead
  using eval() (i.e. webView.sendJavascript())
2014-12-30 23:25:56 -05:00
Andrew Grieve
b10fe465ab Closing stale pull request: close #114 2014-12-30 23:20:52 -05:00
Andrew Grieve
480af2644c CB-8228 Gradle: Allow plugins to use Maven dependencies 2014-12-30 22:57:53 -05:00
Andrew Grieve
ecd2e06883 CB-8229 Gradle: Add CordovaLib as a dependency to all plugin sub-projects 2014-12-30 22:56:43 -05:00
Andrew Grieve
7cfb33d0ef CB-7980 Add --minSdkVersion and --versionCode flags to cordova/build command
These are also exposed via environment variables: ANDROID_VERSION_CODE, ANDROID_MIN_SDK_VERSION
This also fixes build.gradle modifying the value set by ANDROID_VERSION_CODE when multi-apk is enabled (override should never be modified)
2014-12-23 16:26:43 -05:00
Mark Koudritsky
9224ab1592 CB-7980: Add 9 to versionCode for minSdk 20+ if not multiarch 2014-12-23 15:29:40 -05:00
fujunwei
931a996dab Allow plugins to set ext.multiarch to enable multiple APK building
The xwalk webView need build multiple apks by default after install
cordova-crosswalk-engine plugin, we can set ext.multiarch=true to open
the flag in plugin, it don't necessary set system environment BUILD_MULTIPLE_APKS
manually.

This closes #141
2014-12-22 23:19:27 -05:00
Ian Clelland
98fe46757f CB-8204: Reinstate link tasks to avoid gradle build failures 2014-12-22 13:24:58 -05:00
Andrew Grieve
6b6e887c2f CB-8143 Use gradle 2.2.1 instead of 1.12 to appease Android Studio 1.0 warning-on-startup 2014-12-22 11:37:06 -05:00
Andrew Grieve
b92303b1c9 CB-8143 Use gradle plugin 1.0.0 for Android Studio 1.0.0 2014-12-22 11:19:21 -05:00
Andrew Grieve
731a36d3a0 CB-8202 Fix gradle build signing when passwords provided interactively 2014-12-22 10:21:17 -05:00
Andrew Grieve
342bbaa3ae CB-8176 Update Android SDK search path for Android Studio 1.0 2014-12-16 14:17:55 -05:00
Daniel Toplak
56a3ee5fe6 CB-8079 Use activity class package name, but fallback to application
package name when looking for splash screen drawable

Close #136
2014-12-10 21:19:28 -05:00
Andrew Grieve
d80d532a2a Fix syntax error in 3aca14d530 2014-12-10 21:16:54 -05:00
Andrew Grieve
3aca14d530 CB-8147 Have corodva/build warn about unrecognized flags rather than fail
Close #127
2014-12-10 21:02:57 -05:00
Andrew Grieve
aa2d3962bf Close #126 (not-a-problem) 2014-12-10 21:02:57 -05:00
Andrew Grieve
f7c717e393 Close #137 (already merged). 2014-12-10 21:02:57 -05:00
sgrebnov
268fea58ee CB-7881 Android tooling shouldn't lock application directory
Close #130
2014-12-10 21:02:48 -05:00
fujunwei
ba140a8a84 Add a section for plugin extensions
The build.gradle will apply gradle srcipte from plugin extension
When install the plugin with "gradleReference" framework.
The gradle can set ext.multiarch=true to support multiple APKs by
default, so add this section in here.
2014-12-10 15:44:39 -05:00
Andrew Grieve
27f1181d53 CB-3679 Move splashscreen logic into splashscreen plugin
Tried as hard as possible for this not to be a breaking change (all
symbols were preserved). Planning to remove delegating symbols in 4.0.x
though.

Also for backwards compatability - a copy of the plugin is bundled. It
will likewise be removed in 4.0.x
2014-12-10 15:40:03 -05:00
Ian Clelland
f953e6adb8 CB-8143: Use the correct Android Gradle plugin for the installed Gradle version 2014-12-10 10:07:05 -05:00
Brian Geppert
ffd14fe7d9 Revert Gradle distributionUrlRegex cleanup.
This reverts commit 75a0a6752a.
2014-12-09 14:23:38 -05:00
Andrew Grieve
66fa12a091 CB-8119 Restart adb when we detect it's hung 2014-12-04 10:00:26 -05:00
Andrew Grieve
132650df28 CB-8112 Turn off mediaPlaybackRequiresUserGesture 2014-12-03 10:04:54 -05:00
Andrew Grieve
81a77949fc CB-6153 Add a preference for controlling hardware button audio stream (DefaultVolumeStream)
This, along with the commit to the audio plugin, makes it so that by
default apps control the ringer volume, but when any audio players are
active, the media volume is controlled.
2014-11-27 10:52:19 -05:00
Andrew Grieve
7fbb2b195f CB-8081 Allow gradle builds to use Java 6 instead of requiring 7 2014-11-26 11:44:49 -05:00
Andrew Grieve
1feaa7fed7 CB-8031 Fix race condition that shows as ConcurrentModificationException 2014-11-17 22:11:21 -08:00
Andrew Grieve
ac284fd39c CB-7976 Use webView's context rather than Activity's context for intent receiver 2014-11-06 16:23:32 -05:00
Andrew Grieve
e78db000c6 CB-7974 Cancel timeout timer if view is destroyed 2014-11-06 15:33:10 -05:00
Andrew Grieve
032ea8a8d3 CB-7940 Disable exec bridge if bridgeSecret is wrong 2014-11-04 15:57:51 -05:00
Ian Clelland
fc63f66e89 CB-7758: Allow content-url-hosted pages to access the bridge
This allows e.g. jsHybugger to create pages with access to Cordova APIs.
We restrict access to content provider URLs which are at subdomains of the application itself, ie, begin with "content://com.your.package.id."
2014-10-27 15:26:38 -04:00
Chris Alfano
832e626573 CB-7726 fix typo in gitignore: ant-built -> ant-build
github: close #131
2014-10-27 12:33:09 -04:00
Andrew Grieve
ce5d9a2ee8 gradle: Allow storeType to be set (allows using .p12 files) 2014-10-21 12:59:34 -04:00
Andrew Grieve
77c51d3ae7 gradle: Allow absolute paths to keystore files 2014-10-21 12:43:30 -04:00
Joe Bowser
53dae45430 Fixed the SecureRandom so it only returns positive values 2014-10-17 15:30:28 -07:00
Joe Bowser
16343ffe70 Undoing change to Math.random() for now, this creates a weird bug 2014-10-17 13:52:33 -07:00
Joe Bowser
b37498d5f6 Replacing Math.random() with something a little more random. 2014-10-14 10:11:09 -07:00
Vladimir Kotikov
9f41906895 CB-6511 Fixes build for android when app name contains unicode characters.
github: close #124
2014-10-07 15:24:12 -04:00
Rui Zhao
fbeb379f1b CB-7707 Added multipart PluginResult (close #125)
Corresponds to cordova-js commit: a1f866606b3
2014-10-07 15:17:56 -04:00
Andrew Grieve
2dcd50c11b CB-7714 Teach check_reqs about brew's install location for android SDK 2014-10-06 10:33:31 -04:00
Andrew Grieve
30681eb772 Fix --shared flag of create script (broke in recent gradle changes) 2014-10-04 15:14:51 -04:00
Andrew Grieve
52e575e1e7 Update .gitignore to ignore /framework/build and /node_modules 2014-10-04 15:14:45 -04:00
Martin Gonzalez
890e12c306 CB-6837 Fix leaked window when hitting back button while alert being rendered
Keep track of the last AlertDialog showed.
The last dialog showed that is rendered while hitting back button it
causes a leaked window.
Instead of perform a full track of all dialogs created, only destroy the
last one showed, this fixes the problem.

close #122
2014-10-04 14:44:06 -04:00
Joe Bowser
6cbf6b7875 CB-7674: Added sleep to avoid null error after most recent change to not break API 2014-09-30 17:57:42 -07:00
Marcel Kinard
c255a84941 CB-7674 move preference activation back into onCreate()
The preference creation actually needs to be before
super.onCreate(savedInstance) in order to avoid the exception
"requestFeature() must be called before adding content". Also ran into an
issue in the native tests "Whitelist" and "User WebView/Client/Chrome" where
it would throw an exception that the CordovaWebView appView already had
a parent and needed to be removed from that parent before the invocation
to root.addView(appView). So I conditionally remove the wrong parent.
Also made a change to the native tests so the menus test would work.
I also put super.init() back into the template, though invoking it is optional
as loadUrl will call it automatically if needed.
2014-09-30 19:38:34 -04:00
Steven Gill
ce7d6d69d9 updated release notes 2014-09-30 13:10:16 -07:00
Steven Gill
d5538b7076 updated .gitignore to include npm-debug.log 2014-09-29 23:49:12 -07:00
Steven Gill
cdfa13b265 Update JS snapshot to version 3.7.0-dev (via coho) 2014-09-29 14:59:07 -07:00
Andrew Grieve
e31c911c30 CB-7634 Detect JAVA_HOME properly on Ubuntu 2014-09-29 10:16:22 -04:00
Marcel Kinard
a658ea1573 CB-7410 update the docs to match the actual title 2014-09-25 11:25:09 -04:00
Max Woghiren
a986e72338 Added gradle distribution URL updating. (commit fix-up) 2014-09-24 16:18:18 -04:00
Andrew Grieve
162d9b6c2e gradle: Build only the active architecture when applicable 2014-09-24 16:16:59 -04:00
Andrew Grieve
9e3ccf4b3e gradle: Fix warning about dynamic properties being deprecated 2014-09-23 21:04:04 -04:00
Andrew Grieve
6b71c2f392 gradle: Have project's build.gradle look for a build-extras.gradle 2014-09-23 21:03:17 -04:00
Andrew Grieve
0d313a3964 gradle: Write sub-project list explicitly to make Android Studio happy 2014-09-23 21:03:00 -04:00
Max Woghiren
ddac192c4a Added gradle distribution URL updating.
Conflicts:
	bin/templates/cordova/lib/build.js
	framework/build.gradle
2014-09-22 22:27:43 -04:00
Andrew Grieve
69a03c2e16 CB-7512 Use a standard build.gradle for all plugins
Plugins can extend it by providing a "build-extras.gradle"
2014-09-22 22:27:43 -04:00
Andrew Grieve
2b128b85f7 CB-7512 Make gradle build only a single config for sub-libraries (release vs debug) 2014-09-22 20:54:28 -04:00
Andrew Grieve
879da03438 CB-7579 Fix run script's ability to use non-arch-specific APKs 2014-09-22 14:23:30 -04:00
73 changed files with 4203 additions and 1874 deletions

3
.gitignore vendored
View File

@@ -37,3 +37,6 @@ Desktop.ini
# IntelliJ IDEA files
*.iml
.idea
npm-debug.log
/node_modules
/framework/build

View File

@@ -25,8 +25,8 @@ Anyone can contribute to Cordova. And we need your contributions.
There are multiple ways to contribute: report bugs, improve the docs, and
contribute code.
For instructions on this, start with the
For instructions on this, start with the
[contribution overview](http://cordova.apache.org/#contribute).
The details are explained there, but the important items are:
@@ -35,3 +35,4 @@ The details are explained there, but the important items are:
- Run the tests so your patch doesn't break existing functionality.
We look forward to your contributions!

View File

@@ -20,6 +20,47 @@
-->
## Release Notes for Cordova (Android) ##
### 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 `<input type=file>` 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 <meta> 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)
* Update JS snapshot to version 3.6.4 (via coho)
* CB-7634 Detect JAVA_HOME properly on Ubuntu
* CB-7579 Fix run script's ability to use non-arch-specific APKs
* CB-6511 Fixes build for android when app name contains unicode characters.
* CB-7463: Adding licences. I don't know what the gradle syntax is for comments, that still needs to be done.
* CB-7463: Looked at the Apache BigTop git, gradle uses C-style comments
* CB-7460: Fixing bug with KitKat where the background colour would override the CSS colours on the application
### 3.6.0 (Sept 2014) ###
* Set VERSION to 3.6.0 (via coho)

View File

@@ -1 +1 @@
4.0.0-dev
3.7.1

View File

@@ -31,7 +31,7 @@ var isWindows = process.platform == 'win32';
function forgivingWhichSync(cmd) {
try {
return which.sync(cmd);
return fs.realpathSync(which.sync(cmd));
} catch (e) {
return '';
}
@@ -101,6 +101,7 @@ module.exports.check_java = function() {
});
} else {
// See if we can derive it from javac's location.
// fs.realpathSync is require on Ubuntu, which symplinks from /usr/bin -> JDK
var maybeJavaHome = path.dirname(path.dirname(javacPath));
if (fs.existsSync(path.join(maybeJavaHome, 'lib', 'tools.jar'))) {
process.env['JAVA_HOME'] = maybeJavaHome;
@@ -155,20 +156,26 @@ module.exports.check_android = function() {
}
if (!hasAndroidHome && !androidCmdPath) {
if (isWindows) {
// Android Studio installer.
// Android Studio 1.0 installer
maybeSetAndroidHome(path.join(process.env['LOCALAPPDATA'], 'Android', 'sdk'));
maybeSetAndroidHome(path.join(process.env['ProgramFiles'], 'Android', 'sdk'));
// Android Studio pre-1.0 installer
maybeSetAndroidHome(path.join(process.env['LOCALAPPDATA'], 'Android', 'android-studio', 'sdk'));
maybeSetAndroidHome(path.join(process.env['ProgramFiles'], 'Android', 'android-studio', 'sdk'));
// Stand-alone installer.
// Stand-alone installer
maybeSetAndroidHome(path.join(process.env['LOCALAPPDATA'], 'Android', 'android-sdk'));
maybeSetAndroidHome(path.join(process.env['ProgramFiles'], 'Android', 'android-sdk'));
} else if (process.platform == 'darwin') {
// Android Studio 1.0 installer
maybeSetAndroidHome(path.join(process.env['HOME'], 'Library', 'Android', 'sdk'));
// Android Studio pre-1.0 installer
maybeSetAndroidHome('/Applications/Android Studio.app/sdk');
// Stand-alone zip file that user might think to put under /Applications
maybeSetAndroidHome('/Applications/android-sdk-macosx');
maybeSetAndroidHome('/Applications/android-sdk');
}
if (process.env['HOME']) {
// or their HOME directory.
// Stand-alone zip file that user might think to put under their home directory
maybeSetAndroidHome(path.join(process.env['HOME'], 'android-sdk-macosx'));
maybeSetAndroidHome(path.join(process.env['HOME'], 'android-sdk'));
}
@@ -178,9 +185,15 @@ module.exports.check_android = function() {
}
if (androidCmdPath && !hasAndroidHome) {
var parentDir = path.dirname(androidCmdPath);
var grandParentDir = path.dirname(parentDir);
if (path.basename(parentDir) == 'tools') {
process.env['ANDROID_HOME'] = path.dirname(parentDir);
hasAndroidHome = true;
} else if (fs.existsSync(path.join(grandParentDir, 'tools', 'android'))) {
process.env['ANDROID_HOME'] = grandParentDir;
hasAndroidHome = true;
} else {
throw new Error('ANDROID_HOME is not set and no "tools" directory found at ' + parentDir);
}
}
if (hasAndroidHome && !adbInPath) {

View File

@@ -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.
@@ -119,10 +120,9 @@ function writeProjectProperties(projectPath, target_api, shared) {
function copyBuildRules(projectPath) {
var srcDir = path.join(ROOT, 'bin', 'templates', 'project');
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, 'settings.gradle'), projectPath);
shell.cp('-f', path.join(srcDir, 'cordova.gradle'), projectPath);
}
function copyScripts(projectPath) {
@@ -210,9 +210,11 @@ exports.createProject = function(project_path, package_name, project_name, proje
project_template_dir :
path.join(ROOT, 'bin', 'templates', 'project');
var safe_activity_name = project_name.replace(/\W/g, '');
var package_as_path = package_name.replace(/\./g, path.sep);
var activity_dir = path.join(project_path, 'src', package_as_path);
// safe_activity_name is being hardcoded to avoid issues with unicode app name (https://issues.apache.org/jira/browse/CB-6511)
// TODO: provide option to specify activity name via CLI (proposal: https://issues.apache.org/jira/browse/CB-7231)
var safe_activity_name = 'MainActivity';
var activity_path = path.join(activity_dir, safe_activity_name + '.java');
var target_api = check_reqs.get_target();
var manifest_path = path.join(project_path, 'AndroidManifest.xml');
@@ -276,7 +278,7 @@ exports.createProject = function(project_path, package_name, project_name, proje
copyBuildRules(project_path);
});
// Link it to local android install.
writeProjectProperties(project_path, target_api);
writeProjectProperties(project_path, target_api, use_shared_project);
}).then(function() {
console.log('Project successfully created.');
});

View File

@@ -24,6 +24,7 @@ var shell = require('shelljs'),
Q = require('q'),
path = require('path'),
fs = require('fs'),
os = require('os'),
ROOT = path.join(__dirname, '..', '..');
var check_reqs = require('./check_reqs');
var exec = require('./exec');
@@ -32,23 +33,54 @@ var LOCAL_PROPERTIES_TEMPLATE =
'# This file is automatically generated.\n' +
'# Do not modify this file -- YOUR CHANGES WILL BE ERASED!\n';
function find_files(directory, predicate) {
function findApks(directory) {
var ret = [];
if (fs.existsSync(directory)) {
var candidates = fs.readdirSync(directory).filter(predicate).map(function(p) {
p = path.join(directory, p);
return { p: p, t: fs.statSync(p).mtime };
}).sort(function(a,b) {
var timeDiff = b.t - a.t;
if (timeDiff === 0) {
return a.p.length - b.p.length;
fs.readdirSync(directory).forEach(function(p) {
if (path.extname(p) == '.apk') {
ret.push(path.join(directory, p));
}
return timeDiff;
}).map(function(p) { return p.p; });
return candidates;
} else {
console.error('ERROR : unable to find project ' + directory + ' directory, could not locate .apk');
process.exit(2);
});
}
return ret;
}
function sortFilesByDate(files) {
return files.map(function(p) {
return { p: p, t: fs.statSync(p).mtime };
}).sort(function(a, b) {
var timeDiff = b.t - a.t;
return timeDiff === 0 ? a.p.length - b.p.length : timeDiff;
}).map(function(p) { return p.p; });
}
function findOutputApksHelper(dir, build_type, arch) {
var ret = findApks(dir).filter(function(candidate) {
// Need to choose between release and debug .apk.
if (build_type === 'debug') {
return /-debug/.exec(candidate) && !/-unaligned|-unsigned/.exec(candidate);
}
if (build_type === 'release') {
return /-release/.exec(candidate) && !/-unaligned/.exec(candidate);
}
return true;
});
ret = sortFilesByDate(ret);
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 && ret.length > 1) {
ret = ret.filter(function(p) {
return p.indexOf('-' + arch) != -1;
});
}
return ret;
}
function hasCustomRules() {
@@ -127,8 +159,6 @@ var builders = {
return check_reqs.check_ant()
.then(function() {
return spawn('ant', args);
}).then(function() {
return builder.getOutputFiles();
});
},
@@ -140,57 +170,26 @@ var builders = {
});
},
// Find the recently-generated output APK files
// Ant only generates one output file; return it.
getOutputFiles: function() {
var binDir;
if(hasCustomRules()) {
binDir = path.join(ROOT, 'ant-build');
} else {
binDir = path.join(ROOT, 'bin');
}
var candidates = find_files(binDir, function(candidate) { return path.extname(candidate) == '.apk'; });
if (candidates.length === 0) {
console.error('ERROR : No .apk found in ' + binDir + ' directory');
process.exit(2);
}
var ret = candidates[0];
return [ret];
findOutputApks: function(build_type) {
var binDir = path.join(ROOT, hasCustomRules() ? 'ant-build' : 'bin');
return findOutputApksHelper(binDir, build_type, null);
}
},
gradle: {
getArgs: function(cmd) {
var lintSteps;
if (process.env['BUILD_MULTIPLE_APKS']) {
lintSteps = [
'lint',
'lintVitalX86Release',
'lintVitalArmv7Release',
'compileLint',
'copyReleaseLint',
'copyDebugLint'
];
} else {
lintSteps = [
'lint',
'lintVitalRelease',
'compileLint',
'copyReleaseLint',
'copyDebugLint'
];
}
if (cmd == 'debug') {
cmd = 'assembleDebug';
} else if (cmd == 'release') {
cmd = 'assembleRelease';
getArgs: function(cmd, arch, extraArgs) {
if (cmd == 'release') {
cmd = 'cdvBuildRelease';
} else if (cmd == 'debug') {
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');
// Excluding lint: 6s-> 1.6s
for (var i = 0; i < lintSteps.length; ++i) {
args.push('-x', lintSteps[i]);
}
args.push.apply(args, extraArgs);
// Shaves another 100ms, but produces a "try at own risk" warning. Not worth it (yet):
// args.push('-Dorg.gradle.parallel=true');
return args;
@@ -215,13 +214,38 @@ var builders = {
shell.mkdir('-p', path.join(projectPath, 'gradle'));
shell.cp('-r', path.join(wrapperDir, 'gradle', 'wrapper'), path.join(projectPath, 'gradle'));
// If the gradle distribution URL is set, make sure it points to version 1.12.
// If the gradle distribution URL is set, make sure it points to version we want.
// If it's not set, do nothing, assuming that we're using a future version of gradle that we don't want to mess with.
// For some reason, using ^ and $ don't work. This does the job, though.
var distributionUrlRegex = /distributionUrl.*zip/;
var distributionUrl = 'distributionUrl=http\\://services.gradle.org/distributions/gradle-1.12-all.zip';
var distributionUrl = 'distributionUrl=http\\://services.gradle.org/distributions/gradle-2.2.1-all.zip';
var gradleWrapperPropertiesPath = path.join(projectPath, 'gradle', 'wrapper', 'gradle-wrapper.properties');
shell.sed('-i', distributionUrlRegex, distributionUrl, gradleWrapperPropertiesPath);
// Update the version of build.gradle in each dependent library.
var pluginBuildGradle = path.join(projectPath, 'cordova', 'lib', 'plugin-build.gradle');
var subProjects = extractSubProjectPaths();
for (var i = 0; i < subProjects.length; ++i) {
if (subProjects[i] !== 'CordovaLib') {
shell.cp('-f', pluginBuildGradle, path.join(ROOT, subProjects[i], 'build.gradle'));
}
}
var subProjectsAsGradlePaths = subProjects.map(function(p) { return ':' + p.replace(/[/\\]/g, ':') });
// Write the settings.gradle file.
fs.writeFileSync(path.join(projectPath, 'settings.gradle'),
'// GENERATED FILE - DO NOT EDIT\n' +
'include ":"\n' +
'include "' + subProjectsAsGradlePaths.join('"\ninclude "') + '"\n');
// Update dependencies within build.gradle.
var buildGradle = fs.readFileSync(path.join(projectPath, 'build.gradle'), 'utf8');
var depsList = '';
subProjectsAsGradlePaths.forEach(function(p) {
depsList += ' debugCompile project(path: "' + p + '", configuration: "debug")\n';
depsList += ' releaseCompile project(path: "' + p + '", configuration: "release")\n';
});
buildGradle = buildGradle.replace(/(SUB-PROJECT DEPENDENCIES START)[\s\S]*(\/\/ SUB-PROJECT DEPENDENCIES END)/, '$1\n' + depsList + ' $2');
fs.writeFileSync(path.join(projectPath, 'build.gradle'), buildGradle);
});
},
@@ -229,41 +253,29 @@ var builders = {
* Builds the project with gradle.
* Returns a promise.
*/
build: function(build_type) {
build: function(build_type, arch, extraArgs) {
var builder = this;
var wrapper = path.join(ROOT, 'gradlew');
var args = this.getArgs(build_type == 'debug' ? 'debug' : 'release');
return Q().then(function() {
return spawn(wrapper, args);
}).then(function() {
return builder.getOutputFiles(build_type);
});
},
clean: function() {
var builder = this;
var wrapper = path.join(ROOT, 'gradlew');
var args = builder.getArgs('clean');
var args = this.getArgs(build_type == 'debug' ? 'debug' : 'release', arch, extraArgs);
return Q().then(function() {
console.log('Running: ' + wrapper + ' ' + args.concat(extraArgs).join(' '));
return spawn(wrapper, args);
});
},
// Find the recently-generated output APK files
// Gradle can generate multiple output files; return all of them.
getOutputFiles: function(build_type) {
clean: function(extraArgs) {
var builder = this;
var wrapper = path.join(ROOT, 'gradlew');
var args = builder.getArgs('clean', null, extraArgs);
return Q().then(function() {
console.log('Running: ' + wrapper + ' ' + args.concat(extraArgs).join(' '));
return spawn(wrapper, args);
});
},
findOutputApks: function(build_type, arch) {
var binDir = path.join(ROOT, 'build', 'outputs', 'apk');
var candidates = find_files(binDir, function(candidate) {
// Need to choose between release and debug .apk.
if (build_type === 'debug') {
return (path.extname(candidate) == '.apk' && candidate.indexOf('-debug') >= 0);
}
if (build_type === 'release') {
return (path.extname(candidate) == '.apk' && candidate.indexOf('-release') >= 0);
}
return path.extname(candidate) == '.apk';
});
return candidates;
return findOutputApksHelper(binDir, build_type, arch);
}
},
@@ -273,46 +285,80 @@ var builders = {
},
build: function() {
console.log('Skipping build...');
return Q();
return Q(null);
},
clean: function() {
return Q();
},
findOutputApks: function(build_type, arch) {
return sortFilesByDate(builders.ant.findOutputApks(build_type, arch).concat(builders.gradle.findOutputApks(build_type, arch)));
}
}
};
function parseOpts(options) {
function parseOpts(options, resolvedTarget) {
// Backwards-compatibility: Allow a single string argument
if (typeof options == "string") options = [options];
var ret = {
buildType: 'debug',
buildMethod: process.env['ANDROID_BUILD'] || 'ant'
buildMethod: process.env['ANDROID_BUILD'] || 'ant',
arch: null,
extraArgs: []
};
var multiValueArgs = {
'versionCode': true,
'minSdkVersion': true,
'gradleArg': true
};
// Iterate through command line options
for (var i=0; options && (i < options.length); ++i) {
if (options[i].substring && options[i].substring(0,2) == "--") {
var option = options[i].substring(2);
switch(option) {
if (/^--/.exec(options[i])) {
var keyValue = options[i].substring(2).split('=');
var flagName = keyValue.shift();
var flagValue = keyValue.join('=');
if (multiValueArgs[flagName] && !flagValue) {
flagValue = options[i + 1];
++i;
}
switch(flagName) {
case 'debug':
case 'release':
ret.buildType = option;
ret.buildType = flagName;
break;
case 'ant':
case 'gradle':
ret.buildMethod = option;
ret.buildMethod = flagName;
break;
case 'device':
case 'emulator':
// Don't need to do anything special to when building for device vs emulator.
// iOS uses this flag to switch on architecture.
break;
case 'nobuild' :
ret.buildMethod = 'none';
break;
case 'versionCode':
ret.extraArgs.push('-PcdvVersionCode=' + flagValue);
break;
case 'minSdkVersion':
ret.extraArgs.push('-PcdvMinSdkVersion=' + flagValue);
break;
case 'gradleArg':
ret.extraArgs.push(flagValue);
break;
default :
return Q.reject('Build option \'' + options[i] + '\' not recognized.');
console.warn('Build option --\'' + flagName + '\' not recognized (ignoring).');
}
} else {
return Q.reject('Build option \'' + options[i] + '\' not recognized.');
console.warn('Build option \'' + options[i] + '\' not recognized (ignoring).');
}
}
ret.arch = resolvedTarget && resolvedTarget.arch;
return ret;
}
@@ -325,7 +371,7 @@ module.exports.runClean = function(options) {
var builder = builders[opts.buildMethod];
return builder.prepEnv()
.then(function() {
return builder.clean();
return builder.clean(opts.extraArgs);
}).then(function() {
shell.rm('-rf', path.join(ROOT, 'out'));
});
@@ -335,27 +381,21 @@ module.exports.runClean = function(options) {
* Builds the project with the specifed options
* Returns a promise.
*/
module.exports.run = function(options) {
var opts = parseOpts(options);
module.exports.run = function(options, optResolvedTarget) {
var opts = parseOpts(options, optResolvedTarget);
var builder = builders[opts.buildMethod];
return builder.prepEnv()
.then(function() {
return builder.build(opts.buildType);
}).then(function(apkFiles) {
// TODO: Rather than copy apks to out, it might be better to
// just write out what the last .apk build was. These files
// are used by get_apk().
var outputDir = path.join(ROOT, 'out');
shell.mkdir('-p', outputDir);
var builtApks = [];
for (var i=0; i < apkFiles.length; ++i) {
var dst = path.join(outputDir, path.basename(apkFiles[i]));
builtApks.push(dst);
shell.cp('-f', apkFiles[i], dst);
}
console.log('Built the following APKs:\n' + builtApks.join('\n'));
return builtApks;
return builder.build(opts.buildType, opts.arch, opts.extraArgs);
}).then(function() {
var apkPaths = builder.findOutputApks(opts.buildType, opts.arch);
console.log('Built the following apk(s):');
console.log(' ' + apkPaths.join('\n '));
return {
apkPaths: apkPaths,
buildType: opts.buildType,
buildMethod: opts.buildMethod
};
});
};
@@ -364,39 +404,78 @@ module.exports.run = function(options) {
* Returns "arm" or "x86".
*/
module.exports.detectArchitecture = function(target) {
return exec('adb -s ' + target + ' shell cat /proc/cpuinfo')
.then(function(output) {
if (/intel/i.exec(output)) {
return 'x86';
function helper() {
return exec('adb -s ' + target + ' shell cat /proc/cpuinfo', os.tmpdir())
.then(function(output) {
if (/intel/i.exec(output)) {
return 'x86';
}
return 'arm';
});
}
// It sometimes happens (at least on OS X), that this command will hang forever.
// To fix it, either unplug & replug device, or restart adb server.
return helper().timeout(1000, 'Device communication timed out. Try unplugging & replugging the device.')
.then(null, function(err) {
if (/timed out/.exec('' + err)) {
// adb kill-server doesn't seem to do the trick.
// Could probably find a x-platform version of killall, but I'm not actually
// sure that this scenario even happens on non-OSX machines.
return exec('killall adb')
.then(function() {
console.log('adb seems hung. retrying.');
return helper()
.then(null, function() {
// The double kill is sadly often necessary, at least on mac.
console.log('Now device not found... restarting adb again.');
return exec('killall adb')
.then(function() {
return helper()
.then(null, function() {
return Q.reject('USB is flakey. Try unplugging & replugging the device.');
});
});
});
}, function() {
// For non-killall OS's.
return Q.reject(err);
})
}
return 'arm';
throw err;
});
};
/*
* Gets the path to the apk file, if not such file exists then
* the script will error out. (should we error or just return undefined?)
* This is called by the run script to install the apk to the device
*/
module.exports.get_apk = function(build_type, architecture) {
var outputDir = path.join(ROOT, 'out');
var candidates = find_files(outputDir, function(filename) { return (!architecture) || filename.indexOf(architecture) >= 0; });
if (candidates.length === 0) {
console.error('ERROR : No .apk found in ' + outputDir + ' directory');
process.exit(2);
module.exports.findBestApkForArchitecture = function(buildResults, arch) {
var paths = buildResults.apkPaths.filter(function(p) {
if (buildResults.buildType == 'debug') {
return /-debug/.exec(p);
}
return !/-debug/.exec(p);
});
var archPattern = new RegExp('-' + arch);
var hasArchPattern = /-x86|-arm/;
for (var i = 0; i < paths.length; ++i) {
if (hasArchPattern.exec(paths[i])) {
if (archPattern.exec(paths[i])) {
return paths[i];
}
} else {
return paths[i];
}
}
// TODO: Use build_type here.
console.log('Using apk: ' + candidates[0]);
return candidates[0];
throw new Error('Could not find apk architecture: ' + arch + ' build-type: ' + buildResults.buildType);
};
module.exports.help = function() {
console.log('Usage: ' + path.relative(process.cwd(), path.join(ROOT, 'cordova', 'build')) + ' [build_type]');
console.log('Build Types : ');
console.log(' \'--debug\': Default build, will build project in debug mode');
console.log('Usage: ' + path.relative(process.cwd(), path.join(ROOT, 'cordova', 'build')) + ' [flags]');
console.log('Flags:');
console.log(' \'--debug\': will build project in debug mode (default)');
console.log(' \'--release\': will build project for release');
console.log(' \'--ant\': Default build, will build project with ant');
console.log(' \'--ant\': will build project with ant (default)');
console.log(' \'--gradle\': will build project with gradle');
console.log(' \'--nobuild\': will skip build process (can be used with run command)');
console.log(' \'--nobuild\': will skip build process (useful when using run command)');
console.log(' \'--versionCode=#\': Override versionCode for this build. Useful for uploading multiple APKs. Requires --gradle.');
console.log(' \'--minSdkVersion=#\': Override minSdkVersion for this build. Useful for uploading multiple APKs. Requires --gradle.');
console.log(' \'--gradleArg=<gradle command line arg>\': Extra args to pass to the gradle command. Use one flag per arg. Ex. --gradleArg=-PcdvBuildMultipleApks=true');
process.exit(0);
};

View File

@@ -22,41 +22,56 @@
var exec = require('./exec'),
Q = require('q'),
path = require('path'),
os = require('os'),
build = require('./build'),
appinfo = require('./appinfo'),
ROOT = path.join(__dirname, '..', '..');
/**
* Returns a promise for the list of the device ID's found
* @param lookHarder When true, try restarting adb if no devices are found.
*/
module.exports.list = function() {
return exec('adb devices')
.then(function(output) {
var response = output.split('\n');
var device_list = [];
for (var i = 1; i < response.length; i++) {
if (response[i].match(/\w+\tdevice/) && !response[i].match(/emulator/)) {
device_list.push(response[i].replace(/\tdevice/, '').replace('\r', ''));
module.exports.list = function(lookHarder) {
function helper() {
return exec('adb devices', os.tmpdir())
.then(function(output) {
var response = output.split('\n');
var device_list = [];
for (var i = 1; i < response.length; i++) {
if (response[i].match(/\w+\tdevice/) && !response[i].match(/emulator/)) {
device_list.push(response[i].replace(/\tdevice/, '').replace('\r', ''));
}
}
return device_list;
});
}
return helper()
.then(function(list) {
if (list.length === 0 && lookHarder) {
// adb kill-server doesn't seem to do the trick.
// Could probably find a x-platform version of killall, but I'm not actually
// sure that this scenario even happens on non-OSX machines.
return exec('killall adb')
.then(function() {
console.log('Restarting adb to see if more devices are detected.');
return helper();
}, function() {
// For non-killall OS's.
return list;
});
}
return device_list;
return list;
});
}
/*
* Installs a previously built application on the device
* and launches it.
* Returns a promise.
*/
module.exports.install = function(target) {
var launchName;
return this.list()
module.exports.resolveTarget = function(target) {
return this.list(true)
.then(function(device_list) {
if (!device_list || !device_list.length)
if (!device_list || !device_list.length) {
return Q.reject('ERROR: Failed to deploy to device, no devices found.');
}
// default device
target = typeof target !== 'undefined' ? target : device_list[0];
target = target || device_list[0];
if (device_list.indexOf(target) < 0) {
return Q.reject('ERROR: Unable to find target \'' + target + '\'.');
@@ -64,27 +79,45 @@ module.exports.install = function(target) {
return build.detectArchitecture(target)
.then(function(arch) {
var apk_path = build.get_apk(null, arch);
launchName = appinfo.getActivityName();
console.log('Installing app on device...');
var cmd = 'adb -s ' + target + ' install -r "' + apk_path + '"';
return exec(cmd);
return { target: target, arch: arch, isEmulator: false };
});
}).then(function(output) {
if (output.match(/Failure/)) return Q.reject('ERROR: Failed to install apk to device: ' + output);
});
};
//unlock screen
var cmd = 'adb -s ' + target + ' shell input keyevent 82';
return exec(cmd);
}, function(err) { return Q.reject('ERROR: Failed to install apk to device: ' + err); })
.then(function() {
// launch the application
console.log('Launching application...');
var cmd = 'adb -s ' + target + ' shell am start -W -a android.intent.action.MAIN -n ' + launchName;
return exec(cmd);
}).then(function() {
console.log('LAUNCH SUCCESS');
}, function(err) {
return Q.reject('ERROR: Failed to launch application on device: ' + err);
/*
* Installs a previously built application on the device
* and launches it.
* Returns a promise.
*/
module.exports.install = function(target, buildResults) {
return Q().then(function() {
if (target && typeof target == 'object') {
return target;
}
return module.exports.resolveTarget(target);
}).then(function(resolvedTarget) {
var apk_path = build.findBestApkForArchitecture(buildResults, resolvedTarget.arch);
var launchName = appinfo.getActivityName();
console.log('Using apk: ' + apk_path);
console.log('Installing app on device...');
var cmd = 'adb -s ' + resolvedTarget.target + ' install -r "' + apk_path + '"';
return exec(cmd, os.tmpdir())
.then(function(output) {
if (output.match(/Failure/)) return Q.reject('ERROR: Failed to install apk to device: ' + output);
//unlock screen
var cmd = 'adb -s ' + resolvedTarget.target + ' shell input keyevent 82';
return exec(cmd, os.tmpdir());
}, function(err) { return Q.reject('ERROR: Failed to install apk to device: ' + err); })
.then(function() {
// launch the application
console.log('Launching application...');
var cmd = 'adb -s ' + resolvedTarget.target + ' shell am start -W -a android.intent.action.MAIN -n ' + launchName;
return exec(cmd, os.tmpdir());
}).then(function() {
console.log('LAUNCH SUCCESS');
}, function(err) {
return Q.reject('ERROR: Failed to launch application on device: ' + err);
});
});
}

View File

@@ -23,6 +23,7 @@ var shell = require('shelljs'),
exec = require('./exec'),
Q = require('q'),
path = require('path'),
os = require('os'),
appinfo = require('./appinfo'),
build = require('./build'),
ROOT = path.join(__dirname, '..', '..'),
@@ -108,7 +109,7 @@ module.exports.best_image = function() {
// Returns a promise.
module.exports.list_started = function() {
return exec('adb devices')
return exec('adb devices', os.tmpdir())
.then(function(output) {
var response = output.split('\n');
var started_emulator_list = [];
@@ -123,7 +124,7 @@ module.exports.list_started = function() {
// Returns a promise.
module.exports.list_targets = function() {
return exec('android list targets')
return exec('android list targets', os.tmpdir())
.then(function(output) {
var target_out = output.split('\n');
var targets = [];
@@ -201,7 +202,7 @@ module.exports.start = function(emulator_ID) {
console.log('BOOT COMPLETE');
//unlock screen
return exec('adb -s ' + emulator_id + ' shell input keyevent 82');
return exec('adb -s ' + emulator_id + ' shell input keyevent 82', os.tmpdir());
}).then(function() {
//return the new emulator id for the started emulators
return emulator_id;
@@ -231,7 +232,7 @@ module.exports.wait_for_emulator = function(num_running) {
*/
module.exports.wait_for_boot = function(emulator_id) {
var self = this;
return exec('adb -s ' + emulator_id + ' shell getprop init.svc.bootanim')
return exec('adb -s ' + emulator_id + ' shell getprop init.svc.bootanim', os.tmpdir())
.then(function(output) {
if (output.match(/stopped/)) {
return;
@@ -273,14 +274,7 @@ module.exports.create_image = function(name, target) {
}
}
/*
* Installs a previously built application on the emulator and launches it.
* If no target is specified, then it picks one.
* If no started emulators are found, error out.
* Returns a promise.
*/
module.exports.install = function(target) {
var self = this;
module.exports.resolveTarget = function(target) {
return this.list_started()
.then(function(emulator_list) {
if (emulator_list.length < 1) {
@@ -288,36 +282,55 @@ module.exports.install = function(target) {
}
// default emulator
target = typeof target !== 'undefined' ? target : emulator_list[0];
target = target || emulator_list[0];
if (emulator_list.indexOf(target) < 0) {
return Q.reject('Unable to find target \'' + target + '\'. Failed to deploy to emulator.');
}
return build.detectArchitecture(target)
.then(function(arch) {
var apk_path = build.get_apk(null, arch);
console.log('Installing app on emulator...');
return exec('adb -s ' + target + ' install -r "' + apk_path + '"');
return {target:target, arch:arch, isEmulator:true};
});
}).then(function(output) {
if (output.match(/Failure/)) {
return Q.reject('Failed to install apk to emulator: ' + output);
});
};
/*
* Installs a previously built application on the emulator and launches it.
* If no target is specified, then it picks one.
* If no started emulators are found, error out.
* Returns a promise.
*/
module.exports.install = function(target, buildResults) {
return Q().then(function() {
if (target && typeof target == 'object') {
return target;
}
return Q();
}, function(err) {
return Q.reject('Failed to install apk to emulator: ' + err);
}).then(function() {
//unlock screen
return exec('adb -s ' + target + ' shell input keyevent 82');
}).then(function() {
// launch the application
console.log('Launching application...');
var launchName = appinfo.getActivityName();
cmd = 'adb -s ' + target + ' shell am start -W -a android.intent.action.MAIN -n ' + launchName;
return exec(cmd);
}).then(function(output) {
console.log('LAUNCH SUCCESS');
}, function(err) {
return Q.reject('Failed to launch app on emulator: ' + err);
return module.exports.resolveTarget(target);
}).then(function(resolvedTarget) {
var apk_path = build.findBestApkForArchitecture(buildResults, resolvedTarget.arch);
console.log('Installing app on emulator...');
console.log('Using apk: ' + apk_path);
return exec('adb -s ' + resolvedTarget.target + ' install -r "' + apk_path + '"', os.tmpdir())
.then(function(output) {
if (output.match(/Failure/)) {
return Q.reject('Failed to install apk to emulator: ' + output);
}
return Q();
}, function(err) {
return Q.reject('Failed to install apk to emulator: ' + err);
}).then(function() {
//unlock screen
return exec('adb -s ' + resolvedTarget.target + ' shell input keyevent 82', os.tmpdir());
}).then(function() {
// launch the application
console.log('Launching application...');
var launchName = appinfo.getActivityName();
cmd = 'adb -s ' + resolvedTarget.target + ' shell am start -W -a android.intent.action.MAIN -n ' + launchName;
return exec(cmd, os.tmpdir());
}).then(function(output) {
console.log('LAUNCH SUCCESS');
}, function(err) {
return Q.reject('Failed to launch app on emulator: ' + err);
});
});
}

View File

@@ -21,6 +21,7 @@
var shell = require('shelljs'),
path = require('path'),
os = require('os'),
Q = require('q'),
child_process = require('child_process'),
ROOT = path.join(__dirname, '..', '..');
@@ -32,7 +33,7 @@ var shell = require('shelljs'),
module.exports.run = function() {
var cmd = 'adb logcat | grep -v nativeGetEnabledTags';
var d = Q.defer();
var adb = child_process.spawn('adb', ['logcat']);
var adb = child_process.spawn('adb', ['logcat'], {cwd: os.tmpdir()});
adb.stdout.on('data', function(data) {
var lines = data ? data.toString().split('\n') : [];

View File

@@ -0,0 +1,79 @@
/* 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.
*/
// GENERATED FILE! DO NOT EDIT!
buildscript {
repositories {
mavenCentral()
}
// 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+'
}
}
}
apply plugin: 'android-library'
dependencies {
compile fileTree(dir: 'libs', include: '*.jar')
debugCompile project(path: ":CordovaLib", configuration: "debug")
releaseCompile project(path: ":CordovaLib", configuration: "release")
}
android {
compileSdkVersion cdvCompileSdkVersion
buildToolsVersion cdvBuildToolsVersion
publishNonDefault true
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_6
targetCompatibility JavaVersion.VERSION_1_6
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
resources.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
jniLibs.srcDirs = ['libs']
}
}
}
if (file('build-extras.gradle').exists()) {
apply from: 'build-extras.gradle'
}

View File

@@ -23,105 +23,124 @@ var path = require('path'),
build = require('./build'),
emulator = require('./emulator'),
device = require('./device'),
shell = require('shelljs'),
Q = require('q');
/*
* Runs the application on a device if available.
* If not device is found, it will use a started emulator.
* If no device is found, it will use a started emulator.
* If no started emulators are found it will attempt to start an avd.
* If no avds are found it will error out.
* Returns a promise.
*/
module.exports.run = function(args) {
var build_type;
var buildFlags = [];
var install_target;
var list = false;
for (var i=2; i<args.length; i++) {
if (args[i] == '--debug') {
build_type = '--debug';
} else if (args[i] == '--release') {
build_type = '--release';
} else if (args[i] == '--nobuild') {
build_type = '--nobuild';
if (/^--(debug|release|ant|gradle|nobuild|versionCode=|minSdkVersion=|gradleArg=)/.exec(args[i])) {
buildFlags.push(args[i]);
} else if (args[i] == '--device') {
install_target = '--device';
} else if (args[i] == '--emulator') {
install_target = '--emulator';
} else if (args[i].substring(0, 9) == '--target=') {
} else if (/^--target=/.exec(args[i])) {
install_target = args[i].substring(9, args[i].length);
} else if (args[i] == '--list') {
list = true;
} else {
console.error('ERROR : Run option \'' + args[i] + '\' not recognized.');
process.exit(2);
console.warn('Option \'' + args[i] + '\' not recognized (ignoring).');
}
}
return build.run(build_type).then(function() {
if (install_target == '--device') {
return device.install();
if (list) {
var output = '';
var temp = '';
if (!install_target) {
output += 'Available Android Devices:\n';
temp = shell.exec(path.join(__dirname, 'list-devices'), {silent:true}).output;
temp = temp.replace(/^(?=[^\s])/gm, '\t');
output += temp;
output += 'Available Android Virtual Devices:\n';
temp = shell.exec(path.join(__dirname, 'list-emulator-images'), {silent:true}).output;
temp = temp.replace(/^(?=[^\s])/gm, '\t');
output += temp;
} else if (install_target == '--emulator') {
return emulator.list_started().then(function(started) {
var p = started && started.length > 0 ? Q() : emulator.start();
return p.then(function() { emulator.install(); });
});
} else if (install_target) {
var devices, started_emulators, avds;
return device.list()
.then(function(res) {
devices = res;
return emulator.list_started();
}).then(function(res) {
started_emulators = res;
return emulator.list_images();
}).then(function(res) {
avds = res;
if (devices.indexOf(install_target) > -1) {
return device.install(install_target);
} else if (started_emulators.indexOf(install_target) > -1) {
return emulator.install(install_target);
} else {
// if target emulator isn't started, then start it.
var emulator_ID;
for(avd in avds) {
if(avds[avd].name == install_target) {
return emulator.start(install_target)
.then(function() { emulator.install(emulator_ID); });
}
}
return Q.reject('Target \'' + install_target + '\' not found, unable to run project');
}
});
} else {
output += 'Available Android Virtual Devices:\n';
temp = shell.exec(path.join(__dirname, 'list-emulator-images'), {silent:true}).output;
temp = temp.replace(/^(?=[^\s])/gm, '\t');
output += temp;
} else if (install_target == '--device') {
output += 'Available Android Devices:\n';
temp = shell.exec(path.join(__dirname, 'list-devices'), {silent:true}).output;
temp = temp.replace(/^(?=[^\s])/gm, '\t');
output += temp;
}
console.log(output);
return;
}
return Q()
.then(function() {
if (!install_target) {
// no target given, deploy to device if available, otherwise use the emulator.
return device.list()
.then(function(device_list) {
if (device_list.length > 0) {
console.log('WARNING : No target specified, deploying to device \'' + device_list[0] + '\'.');
return device.install(device_list[0]);
install_target = device_list[0];
} else {
return emulator.list_started()
.then(function(emulator_list) {
if (emulator_list.length > 0) {
console.log('WARNING : No target specified, deploying to emulator \'' + emulator_list[0] + '\'.');
return emulator.install(emulator_list[0]);
} else {
console.log('WARNING : No started emulators found, starting an emulator.');
return emulator.best_image()
.then(function(best_avd) {
if(best_avd) {
return emulator.start(best_avd.name)
.then(function(emulator_ID) {
console.log('WARNING : No target specified, deploying to emulator \'' + emulator_ID + '\'.');
return emulator.install(emulator_ID);
});
} else {
return emulator.start();
}
});
}
});
console.log('WARNING : No target specified, deploying to emulator');
install_target = '--emulator';
}
});
}
}).then(function() {
if (install_target == '--device') {
return device.resolveTarget(null);
} else if (install_target == '--emulator') {
// Give preference to any already started emulators. Else, start one.
return emulator.list_started()
.then(function(started) {
return started && started.length > 0 ? started[0] : emulator.start();
}).then(function(emulatorId) {
return emulator.resolveTarget(emulatorId);
});
}
// They specified a specific device/emulator ID.
return device.list()
.then(function(devices) {
if (devices.indexOf(install_target) > -1) {
return device.resolveTarget(install_target);
}
return emulator.list_started()
.then(function(started_emulators) {
if (started_emulators.indexOf(install_target) > -1) {
return emulator.resolveTarget(install_target);
}
return emulator.list_images()
.then(function(avds) {
// if target emulator isn't started, then start it.
for (avd in avds) {
if (avds[avd].name == install_target) {
return emulator.start(install_target)
.then(function(emulatorId) {
return emulator.resolveTarget(emulatorId);
});
}
}
return Q.reject('Target \'' + install_target + '\' not found, unable to run project');
});
});
});
}).then(function(resolvedTarget) {
return build.run(buildFlags, resolvedTarget).then(function(buildResults) {
if (resolvedTarget.isEmulator) {
return emulator.install(resolvedTarget, buildResults);
}
return device.install(resolvedTarget, buildResults);
});
});
}

View File

@@ -26,13 +26,15 @@ var isWindows = process.platform.slice(0, 3) == 'win';
// Takes a command and optional current working directory.
module.exports = function(cmd, args, opt_cwd) {
var d = Q.defer();
var opts = { cwd: opt_cwd, stdio: 'inherit' };
try {
// Work around spawn not being able to find .bat files.
if (isWindows) {
args.unshift('/s', '/c', cmd);
cmd = 'cmd';
args = [['/s', '/c', '"' + [cmd].concat(args).map(function(a){if (/^[^"].* .*[^"]/.test(a)) return '"' + a + '"'; return a;}).join(' ')+'"'].join(' ')];
cmd = 'cmd';
opts.windowsVerbatimArguments = true;
}
var child = child_process.spawn(cmd, args, {cwd: opt_cwd, stdio: 'inherit'});
var child = child_process.spawn(cmd, args, opts);
child.on('exit', function(code) {
if (code) {
d.reject('Error code ' + code + ' for command: ' + cmd + ' with args: ' + args);
@@ -45,5 +47,4 @@ module.exports = function(cmd, args, opt_cwd) {
d.reject(e);
}
return d.promise;
}
};

View File

@@ -20,6 +20,6 @@
*/
// Coho updates this line:
var VERSION = "4.0.0-dev";
var VERSION = "3.7.1";
console.log(VERSION);

View File

@@ -28,6 +28,7 @@ public class __ACTIVITY__ extends CordovaActivity
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
super.init();
// Set by <content src="index.html" /> in config.xml
loadUrl(launchUrl);
}

View File

@@ -29,8 +29,6 @@
/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<application android:icon="@drawable/icon" android:label="@string/app_name"
android:hardwareAccelerated="true" android:supportsRtl="true">

View File

@@ -17,11 +17,8 @@
under the License.
*/
import java.util.regex.Pattern
import groovy.swing.SwingBuilder
// GENERATED FILE! DO NOT EDIT!
ext.cordova = {}
apply from: 'cordova.gradle', to: ext.cordova
apply plugin: 'android'
buildscript {
@@ -29,20 +26,118 @@ buildscript {
mavenCentral()
}
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+'
}
}
}
ext.multiarch=false
// Allow plugins to declare Maven dependencies via build-extras.gradle.
repositories {
mavenCentral()
}
dependencies {
compile fileTree(dir: 'libs', include: '*.jar')
for (subproject in getProjectList()) {
compile project(subproject)
task wrapper(type: Wrapper) {
gradleVersion = '2.2.1'
}
// Configuration properties. Set these via environment variables, build-extras.gradle, or gradle.properties.
// Refer to: http://www.gradle.org/docs/current/userguide/tutorial_this_and_that.html
ext {
apply from: 'CordovaLib/cordova.gradle'
// The value for android.compileSdkVersion.
if (!project.hasProperty('cdvCompileSdkVersion')) {
cdvCompileSdkVersion = privateHelpers.getProjectTarget()
}
// The value for android.buildToolsVersion.
if (!project.hasProperty('cdvBuildToolsVersion')) {
cdvBuildToolsVersion = privateHelpers.findLatestInstalledBuildTools()
}
// Sets the versionCode to the given value.
if (!project.hasProperty('cdvVersionCode')) {
cdvVersionCode = null
}
// Sets the minSdkVersion to the given value.
if (!project.hasProperty('cdvMinSdkVersion')) {
cdvMinSdkVersion = null
}
// Whether to build architecture-specific APKs.
if (!project.hasProperty('cdvBuildMultipleApks')) {
cdvBuildMultipleApks = false
}
// .properties files to use for release signing.
if (!project.hasProperty('cdvReleaseSigningPropertiesFile')) {
cdvReleaseSigningPropertiesFile = null
}
// .properties files to use for debug signing.
if (!project.hasProperty('cdvDebugSigningPropertiesFile')) {
cdvDebugSigningPropertiesFile = null
}
// Set by build.js script.
if (!project.hasProperty('cdvBuildArch')) {
cdvBuildArch = null
}
}
def hasBuildExtras = file('build-extras.gradle').exists()
if (hasBuildExtras) {
apply from: 'build-extras.gradle'
}
def computeBuildTargetName(debugBuild) {
def ret = 'assemble'
if (cdvBuildMultipleApks && cdvBuildArch) {
def arch = cdvBuildArch == 'arm' ? 'armv7' : cdvBuildArch
ret += '' + arch.toUpperCase().charAt(0) + arch.substring(1);
}
return ret + (debugBuild ? 'Debug' : 'Release')
}
// Make cdvBuild a task that depends on the debug/arch-sepecific task.
task cdvBuildDebug
cdvBuildDebug.dependsOn {
return computeBuildTargetName(true)
}
task cdvBuildRelease
cdvBuildRelease.dependsOn {
return computeBuildTargetName(false)
}
task cdvPrintProps << {
println('cdvCompileSdkVersion=' + cdvCompileSdkVersion)
println('cdvBuildToolsVersion=' + cdvBuildToolsVersion)
println('cdvVersionCode=' + cdvVersionCode)
println('cdvMinSdkVersion=' + cdvMinSdkVersion)
println('cdvBuildMultipleApks=' + cdvBuildMultipleApks)
println('cdvReleaseSigningPropertiesFile=' + cdvReleaseSigningPropertiesFile)
println('cdvDebugSigningPropertiesFile=' + cdvDebugSigningPropertiesFile)
println('cdvBuildArch=' + cdvBuildArch)
println('computedVersionCode=' + android.defaultConfig.versionCode)
if (android.productFlavors.has('armv7')) {
println('computedArmv7VersionCode=' + android.productFlavors.armv7.versionCode)
}
if (android.productFlavors.has('x86')) {
println('computedx86VersionCode=' + android.productFlavors.x86.versionCode)
}
}
// PLUGIN GRADLE EXTENSIONS START
// PLUGIN GRADLE EXTENSIONS END
android {
sourceSets {
main {
@@ -56,23 +151,29 @@ android {
}
}
def versionCodeOverride = cdvVersionCode ? Integer.parseInt(cdvVersionCode) : null
def minSdkVersionOverride = cdvMinSdkVersion ? Integer.parseInt(cdvMinSdkVersion) : null
defaultConfig {
versionCode Integer.parseInt(System.env.ANDROID_VERSION_CODE ?: ("" + getVersionCodeFromManifest() + "0"))
versionCode versionCodeOverride ?: Integer.parseInt("" + privateHelpers.extractIntFromManifest("versionCode") + "0")
if (minSdkVersionOverride != null) {
minSdkVersion minSdkVersionOverride
}
}
compileSdkVersion cordova.cordovaSdkVersion
buildToolsVersion cordova.cordovaBuildToolsVersion
compileSdkVersion cdvCompileSdkVersion
buildToolsVersion cdvBuildToolsVersion
if (multiarch || System.env.BUILD_MULTIPLE_APKS) {
if (Boolean.valueOf(cdvBuildMultipleApks)) {
productFlavors {
armv7 {
versionCode defaultConfig.versionCode + 2
versionCode versionCodeOverride ?: defaultConfig.versionCode + 2
ndk {
abiFilters "armeabi-v7a", ""
}
}
x86 {
versionCode defaultConfig.versionCode + 4
versionCode versionCodeOverride ?: defaultConfig.versionCode + 4
ndk {
abiFilters "x86", ""
}
@@ -83,21 +184,31 @@ android {
}
}
}
} else if (!versionCodeOverride) {
def minSdkVersion = minSdkVersionOverride ?: privateHelpers.extractIntFromManifest("minSdkVersion")
// Vary versionCode by the two most common API levels:
// 14 is ICS, which is the lowest API level for many apps.
// 20 is Lollipop, which is the lowest API level for the updatable system webview.
if (minSdkVersion >= 20) {
defaultConfig.versionCode += 9
} else if (minSdkVersion >= 14) {
defaultConfig.versionCode += 8
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
sourceCompatibility JavaVersion.VERSION_1_6
targetCompatibility JavaVersion.VERSION_1_6
}
if (System.env.RELEASE_SIGNING_PROPERTIES_FILE) {
if (cdvReleaseSigningPropertiesFile) {
signingConfigs {
release {
// These must be set or Gradle will complain (even if they are overridden).
keyAlias = ""
keyPassword = ""
keyPassword = "__unset" // And these must be set to non-empty in order to have the signing step added to the task graph.
storeFile = null
storePassword = ""
storePassword = "__unset"
}
}
buildTypes {
@@ -105,51 +216,28 @@ android {
signingConfig signingConfigs.release
}
}
addSigningProps(System.env.RELEASE_SIGNING_PROPERTIES_FILE, signingConfigs.release)
addSigningProps(cdvReleaseSigningPropertiesFile, signingConfigs.release)
}
if (System.env.DEBUG_SIGNING_PROPERTIES_FILE) {
addSigningProps(System.env.DEBUG_SIGNING_PROPERTIES_FILE, signingConfigs.debug)
if (cdvDebugSigningPropertiesFile) {
addSigningProps(cdvDebugSigningPropertiesFile, signingConfigs.debug)
}
}
task wrapper(type: Wrapper) {
gradleVersion = '1.12'
}
def promptForPassword(msg) {
if (System.console() == null) {
def ret = null
new SwingBuilder().edt {
dialog(modal: true, title: 'Enter password', alwaysOnTop: true, resizable: false, locationRelativeTo: null, pack: true, show: true) {
vbox {
label(text: msg)
def input = passwordField()
button(defaultButton: true, text: 'OK', actionPerformed: {
ret = input.password;
dispose();
})
}
}
}
if (!ret) {
throw new GradleException('User canceled build')
}
return new String(ret)
} else {
return System.console().readPassword('\n' + msg);
}
dependencies {
compile fileTree(dir: 'libs', include: '*.jar')
// SUB-PROJECT DEPENDENCIES START
// SUB-PROJECT DEPENDENCIES END
}
def promptForReleaseKeyPassword() {
if (!System.env.RELEASE_SIGNING_PROPERTIES_FILE) {
if (!cdvReleaseSigningPropertiesFile) {
return;
}
if (!android.signingConfigs.release.storePassword) {
android.signingConfigs.release.storePassword = promptForPassword('Enter key store password: ')
println('set to:' + android.signingConfigs.release.storePassword)
if ('__unset'.equals(android.signingConfigs.release.storePassword)) {
android.signingConfigs.release.storePassword = privateHelpers.promptForPassword('Enter key store password: ')
}
if (!android.signingConfigs.release.keyPassword) {
android.signingConfigs.release.keyPassword = promptForPassword('Enter key password: ');
if ('__unset'.equals(android.signingConfigs.release.keyPassword)) {
android.signingConfigs.release.keyPassword = privateHelpers.promptForPassword('Enter key password: ');
}
}
@@ -161,41 +249,37 @@ gradle.taskGraph.whenReady { taskGraph ->
}
}
def getVersionCodeFromManifest() {
def manifestFile = file(android.sourceSets.main.manifest.srcFile)
def pattern = Pattern.compile("versionCode=\"(\\d+)\"")
def matcher = pattern.matcher(manifestFile.getText())
matcher.find()
return Integer.parseInt(matcher.group(1))
}
def getProjectList() {
def manifestFile = file("project.properties")
def pattern = Pattern.compile("android.library.reference.(\\d+)\\s*=\\s*(.*)")
def matcher = pattern.matcher(manifestFile.getText())
def projects = []
while (matcher.find()) {
projects.add(":" + matcher.group(2).replace("/",":"))
}
return projects
}
def ensureValueExists(filePath, props, key) {
if (props.get(key) == null) {
throw new GradleException(filePath + ': Missing key required "' + key + '"')
}
return props.get(key)
}
def addSigningProps(propsFilePath, signingConfig) {
def propsFile = file(propsFilePath)
def props = new Properties()
propsFile.withReader { reader ->
def props = new Properties()
props.load(reader)
signingConfig.keyAlias = ensureValueExists(propsFilePath, props, 'keyAlias')
signingConfig.keyPassword = props.get('keyPassword')
signingConfig.storeFile = RelativePath.parse(true, ensureValueExists(propsFilePath, props, 'storeFile')).getFile(propsFile.getParentFile())
signingConfig.storePassword = props.get('storePassword')
}
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 = privateHelpers.ensureValueExists(propsFilePath, props, 'keyAlias')
signingConfig.keyPassword = props.get('keyPassword', signingConfig.keyPassword)
signingConfig.storeFile = storeFile
signingConfig.storePassword = props.get('storePassword', signingConfig.storePassword)
def storeType = props.get('storeType')
if (!storeType) {
def filename = storeFile.getName().toLowerCase();
if (filename.endsWith('.p12') || filename.endsWith('.pfx')) {
storeType = 'pkcs12'
}
}
if (storeType) {
signingConfig.storeType = storeType
}
}
// This can be defined within build-extras.gradle as:
// ext.postBuildExtras = { ... code here ... }
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project>
<target name="-pre-compile">
<!-- Fix library references due to bug in build.xml: See: https://groups.google.com/forum/#!topic/android-developers/0ivH-YqCjzg -->
<pathconvert property="fixedJarsPath" refid="project.all.jars.path">
<filtermapper>
<replacestring from="/bin/" to="/ant-build/"/>
<replacestring from="\bin\" to="\ant-build\"/>
</filtermapper>
</pathconvert>
<path id="project.all.jars.path">
<pathelement path="${fixedJarsPath}"/>
</path>
<echo message="Set jars path to: ${toString:project.all.jars.path}"/>
</target>
<!-- Rename AndroidManifest.xml so that Eclipse's import wizard doesn't detect ant-build as a project -->
<target name="-post-build">
<move file="ant-build/AndroidManifest.xml" tofile="ant-build/AndroidManifest.cordova.xml" failonerror="false" overwrite="true" />
<move file="CordovaLib/ant-build/AndroidManifest.xml" tofile="CordovaLib/ant-build/AndroidManifest.cordova.xml" failonerror="false" overwrite="true" />
</target>
</project>

View File

@@ -5,7 +5,7 @@ local.properties
/gradlew.bat
/gradle
# Ant builds
ant-built
ant-build
ant-gen
# Eclipse builds
gen

View File

@@ -1,18 +0,0 @@
import java.util.regex.Pattern
def getProjectList() {
def manifestFile = file("project.properties")
def pattern = Pattern.compile("android.library.reference.(\\d+)\\s*=\\s*(.*)")
def matcher = pattern.matcher(manifestFile.getText())
def projects = []
while (matcher.find()) {
projects.add(":" + matcher.group(2).replace("/",":"))
}
return projects
}
for (subproject in getProjectList()) {
include subproject
}
include ':'

View File

@@ -1,5 +1,5 @@
// Platform: android
// 3.7.0-dev-1258511
// 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 CORDOVA_JS_BUILD_LABEL = '3.7.0-dev-1258511';
var PLATFORM_VERSION_BUILD_LABEL = '3.7.1';
// file: src/scripts/require.js
/*jshint -W079 */
@@ -175,7 +175,8 @@ function createEvent(type, data) {
var cordova = {
define:define,
require:require,
version:CORDOVA_JS_BUILD_LABEL,
version:PLATFORM_VERSION_BUILD_LABEL,
platformVersion:PLATFORM_VERSION_BUILD_LABEL,
platformId:platform.id,
/**
* Methods to add/remove your own addEventListener hijacking on document + window.
@@ -262,11 +263,7 @@ var cordova = {
* Called by native code when returning successful result from an action.
*/
callbackSuccess: function(callbackId, args) {
try {
cordova.callbackFromNative(callbackId, true, args.status, [args.message], args.keepCallback);
} catch (e) {
console.log("Error in success callback: " + callbackId + " = "+e);
}
cordova.callbackFromNative(callbackId, true, args.status, [args.message], args.keepCallback);
},
/**
@@ -275,30 +272,40 @@ var cordova = {
callbackError: function(callbackId, args) {
// TODO: Deprecate callbackSuccess and callbackError in favour of callbackFromNative.
// Derive success from status.
try {
cordova.callbackFromNative(callbackId, false, args.status, [args.message], args.keepCallback);
} catch (e) {
console.log("Error in error callback: " + callbackId + " = "+e);
}
cordova.callbackFromNative(callbackId, false, args.status, [args.message], args.keepCallback);
},
/**
* Called by native code when returning the result from an action.
*/
callbackFromNative: function(callbackId, success, status, args, keepCallback) {
var callback = cordova.callbacks[callbackId];
if (callback) {
if (success && status == cordova.callbackStatus.OK) {
callback.success && callback.success.apply(null, args);
} else if (!success) {
callback.fail && callback.fail.apply(null, args);
}
// Clear callback if not expecting any more results
if (!keepCallback) {
delete cordova.callbacks[callbackId];
callbackFromNative: function(callbackId, isSuccess, status, args, keepCallback) {
try {
var callback = cordova.callbacks[callbackId];
if (callback) {
if (isSuccess && status == cordova.callbackStatus.OK) {
callback.success && callback.success.apply(null, args);
} else if (!isSuccess) {
callback.fail && callback.fail.apply(null, args);
}
/*
else
Note, this case is intentionally not caught.
this can happen if isSuccess is true, but callbackStatus is NO_RESULT
which is used to remove a callback from the list without calling the callbacks
typically keepCallback is false in this case
*/
// Clear callback if not expecting any more results
if (!keepCallback) {
delete cordova.callbacks[callbackId];
}
}
}
catch (err) {
var msg = "Error in " + (isSuccess ? "Success" : "Error") + " callbackId: " + callbackId + " : " + err;
console && console.log && console.log(msg);
cordova.fireWindowEvent("cordovacallbackerror", { 'message': msg });
throw err;
}
},
addConstructor: function(func) {
channel.onCordovaReady.subscribe(function() {
@@ -508,9 +515,14 @@ function each(objects, func, context) {
function clobber(obj, key, value) {
exports.replaceHookForTesting(obj, key);
obj[key] = value;
var needsProperty = false;
try {
obj[key] = value;
} catch (e) {
needsProperty = true;
}
// Getters can only be overridden by getters.
if (obj[key] !== value) {
if (needsProperty || obj[key] !== value) {
utils.defineGetter(obj, key, function() {
return value;
});
@@ -625,7 +637,6 @@ var utils = require('cordova/utils'),
* onDeviceReady* User event fired to indicate that Cordova is ready
* onResume User event fired to indicate a start/resume lifecycle event
* onPause User event fired to indicate a pause lifecycle event
* onDestroy* Internal event fired when app is being destroyed (User should use window.onunload event, not this one).
*
* The events marked with an * are sticky. Once they have fired, they will stay in the fired state.
* All listeners that subscribe after the event is fired will be executed right away.
@@ -837,9 +848,6 @@ channel.create('onResume');
// Event to indicate a pause lifecycle event
channel.create('onPause');
// Event to indicate a destroy lifecycle event
channel.createSticky('onDestroy');
// Channels that must fire before "deviceready" is fired.
channel.waitForInitialization('onCordovaReady');
channel.waitForInitialization('onDOMContentLoaded');
@@ -1012,6 +1020,37 @@ androidExec.setNativeToJsBridgeMode = function(mode) {
}
};
function buildPayload(payload, message) {
var payloadKind = message.charAt(0);
if (payloadKind == 's') {
payload.push(message.slice(1));
} else if (payloadKind == 't') {
payload.push(true);
} else if (payloadKind == 'f') {
payload.push(false);
} else if (payloadKind == 'N') {
payload.push(null);
} else if (payloadKind == 'n') {
payload.push(+message.slice(1));
} else if (payloadKind == 'A') {
var data = message.slice(1);
payload.push(base64.toArrayBuffer(data));
} else if (payloadKind == 'S') {
payload.push(window.atob(message.slice(1)));
} else if (payloadKind == 'M') {
var multipartMessages = message.slice(1);
while (multipartMessages !== "") {
var spaceIdx = multipartMessages.indexOf(' ');
var msgLen = +multipartMessages.slice(0, spaceIdx);
var multipartMessage = multipartMessages.substr(spaceIdx + 1, msgLen);
multipartMessages = multipartMessages.slice(spaceIdx + msgLen + 1);
buildPayload(payload, multipartMessage);
}
} else {
payload.push(JSON.parse(message));
}
}
// Processes a single message, as encoded by NativeToJsMessageQueue.java.
function processMessage(message) {
try {
@@ -1025,32 +1064,10 @@ function processMessage(message) {
var status = +message.slice(2, spaceIdx);
var nextSpaceIdx = message.indexOf(' ', spaceIdx + 1);
var callbackId = message.slice(spaceIdx + 1, nextSpaceIdx);
var payloadKind = message.charAt(nextSpaceIdx + 1);
var payload;
if (payloadKind == 's') {
payload = message.slice(nextSpaceIdx + 2);
} else if (payloadKind == 't') {
payload = true;
} else if (payloadKind == 'f') {
payload = false;
} else if (payloadKind == 'N') {
payload = null;
} else if (payloadKind == 'n') {
payload = +message.slice(nextSpaceIdx + 2);
} else if (payloadKind == 'A') {
var data = message.slice(nextSpaceIdx + 2);
var bytes = window.atob(data);
var arraybuffer = new Uint8Array(bytes.length);
for (var i = 0; i < bytes.length; i++) {
arraybuffer[i] = bytes.charCodeAt(i);
}
payload = arraybuffer.buffer;
} else if (payloadKind == 'S') {
payload = window.atob(message.slice(nextSpaceIdx + 2));
} else {
payload = JSON.parse(message.slice(nextSpaceIdx + 1));
}
cordova.callbackFromNative(callbackId, success, status, [payload], keepCallback);
var payloadMessage = message.slice(nextSpaceIdx + 1);
var payload = [];
buildPayload(payload, payloadMessage);
cordova.callbackFromNative(callbackId, success, status, payload, keepCallback);
} else {
console.log("processMessage failed: invalid message: " + JSON.stringify(message));
}
@@ -1152,6 +1169,7 @@ var cordova = require('cordova');
var modulemapper = require('cordova/modulemapper');
var platform = require('cordova/platform');
var pluginloader = require('cordova/pluginloader');
var utils = require('cordova/utils');
var platformInitChannelsArray = [channel.onNativeReady, channel.onPluginsReady];
@@ -1184,10 +1202,18 @@ function replaceNavigator(origNavigator) {
if (typeof origNavigator[key] == 'function') {
newNavigator[key] = origNavigator[key].bind(origNavigator);
}
else {
(function(k) {
utils.defineGetterSetter(newNavigator,key,function() {
return origNavigator[k];
});
})(key);
}
}
}
return newNavigator;
}
if (window.navigator) {
window.navigator = replaceNavigator(window.navigator);
}
@@ -1268,6 +1294,7 @@ define("cordova/init_b", function(require, exports, module) {
var channel = require('cordova/channel');
var cordova = require('cordova');
var platform = require('cordova/platform');
var utils = require('cordova/utils');
var platformInitChannelsArray = [channel.onDOMContentLoaded, channel.onNativeReady];
@@ -1303,6 +1330,13 @@ function replaceNavigator(origNavigator) {
if (typeof origNavigator[key] == 'function') {
newNavigator[key] = origNavigator[key].bind(origNavigator);
}
else {
(function(k) {
utils.defineGetterSetter(newNavigator,key,function() {
return origNavigator[k];
});
})(key);
}
}
}
return newNavigator;
@@ -1350,7 +1384,7 @@ platform.bootstrap && platform.bootstrap();
* Create all cordova objects once native side is ready.
*/
channel.join(function() {
platform.initialize && platform.initialize();
// Fire event to notify that all objects are created
@@ -1485,12 +1519,14 @@ module.exports = {
// TODO: Extract this as a proper plugin.
modulemapper.clobbers('cordova/plugin/android/app', 'navigator.app');
var APP_PLUGIN_NAME = Number(cordova.platformVersion.split('.')[0]) >= 4 ? 'CoreAndroid' : 'App';
// Inject a listener for the backbutton on the document.
var backButtonChannel = cordova.addDocumentEventHandler('backbutton');
backButtonChannel.onHasSubscribersChange = function() {
// If we just attached the first handler or detached the last handler,
// let native know we need to override the back button.
exec(null, null, "App", "overrideBackbutton", [this.numHandlers == 1]);
exec(null, null, APP_PLUGIN_NAME, "overrideBackbutton", [this.numHandlers == 1]);
};
// Add hardware MENU and SEARCH button handlers
@@ -1501,7 +1537,7 @@ module.exports = {
// generic button bind used for volumeup/volumedown buttons
var volumeButtonChannel = cordova.addDocumentEventHandler(buttonName + 'button');
volumeButtonChannel.onHasSubscribersChange = function() {
exec(null, null, "App", "overrideButton", [buttonName, this.numHandlers == 1]);
exec(null, null, APP_PLUGIN_NAME, "overrideButton", [buttonName, this.numHandlers == 1]);
};
}
// Inject a listener for the volume buttons on the document.
@@ -1511,11 +1547,38 @@ module.exports = {
// Let native code know we are all done on the JS side.
// Native code will then un-hide the WebView.
channel.onCordovaReady.subscribe(function() {
exec(null, null, "App", "show", []);
exec(onMessageFromNative, null, APP_PLUGIN_NAME, 'messageChannel', []);
exec(null, null, APP_PLUGIN_NAME, "show", []);
});
}
};
function onMessageFromNative(msg) {
var cordova = require('cordova');
var action = msg.action;
switch (action)
{
// Button events
case 'backbutton':
case 'menubutton':
case 'searchbutton':
// App life cycle events
case 'pause':
case 'resume':
// Keyboard events
case 'hidekeyboard':
case 'showkeyboard':
// Volume events
case 'volumedownbutton':
case 'volumeupbutton':
cordova.fireDocumentEvent(action);
break;
default:
throw new Error('Unknown event action ' + action);
}
}
});
// file: src/android/plugin/android/app.js

View File

@@ -1,20 +1,19 @@
/*
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
/* 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
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.
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.
*/
@@ -24,27 +23,35 @@ buildscript {
mavenCentral()
}
dependencies {
// This should be updated with each cordova-android release.
// It can affect things like where the .apk is generated.
// It also dictates what the minimum android build-tools version
// that you need (Set in bin/templates/project/cordova.gradle).
// Be sure to also update the value in:
// (1) bin/templates/project/build.gradle, and
// (2) the distribution URL in bin/templates/cordova/lib/build.js.
classpath 'com.android.tools.build:gradle:0.12.+'
// 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+'
}
}
}
apply plugin: 'android-library'
android {
compileSdkVersion cordova.cordovaSdkVersion
buildToolsVersion cordova.cordovaBuildToolsVersion
compileSdkVersion cdvCompileSdkVersion
buildToolsVersion cdvBuildToolsVersion
publishNonDefault true
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
sourceCompatibility JavaVersion.VERSION_1_6
targetCompatibility JavaVersion.VERSION_1_6
}
sourceSets {

View File

@@ -18,16 +18,21 @@
*/
import java.util.regex.Pattern
import groovy.swing.SwingBuilder
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 doEnsureValueExists(filePath, props, key) {
if (props.get(key) == null) {
throw new GradleException(filePath + ': Missing key required "' + key + '"')
}
return props.get(key)
}
String doGetProjectTarget() {
def props = new Properties()
file('project.properties').withReader { reader ->
props.load(reader)
}
return doEnsureValueExists('project.properties', props, 'target')
}
String[] getAvailableBuildTools() {
@@ -37,7 +42,7 @@ String[] getAvailableBuildTools() {
.sort { a, b -> compareVersions(b, a) }
}
String latestBuildToolsAvailable(String minBuildToolsVersion) {
String doFindLatestInstalledBuildTools(String minBuildToolsVersion) {
def availableBuildToolsVersions
try {
availableBuildToolsVersions = getAvailableBuildTools()
@@ -115,6 +120,46 @@ String getAndroidSdkDir() {
androidSdkDir
}
cordovaSdkVersion = System.env.MIN_SDK_VERSION ?: getProjectTarget("android-19")
cordovaBuildToolsVersion = latestBuildToolsAvailable("19.1.0")
def doExtractIntFromManifest(name) {
def manifestFile = file(android.sourceSets.main.manifest.srcFile)
def pattern = Pattern.compile(name + "=\"(\\d+)\"")
def matcher = pattern.matcher(manifestFile.getText())
matcher.find()
return Integer.parseInt(matcher.group(1))
}
def doPromptForPassword(msg) {
if (System.console() == null) {
def ret = null
new SwingBuilder().edt {
dialog(modal: true, title: 'Enter password', alwaysOnTop: true, resizable: false, locationRelativeTo: null, pack: true, show: true) {
vbox {
label(text: msg)
def input = passwordField()
button(defaultButton: true, text: 'OK', actionPerformed: {
ret = input.password;
dispose();
})
}
}
}
if (!ret) {
throw new GradleException('User canceled build')
}
return new String(ret)
} else {
return System.console().readPassword('\n' + msg);
}
}
// Properties exported here are visible to all plugins.
ext {
// These helpers are shared, but are not guaranteed to be stable / unchanged.
privateHelpers = {}
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) }
}

View File

@@ -10,7 +10,7 @@
# Indicates whether an apk should be generated for each density.
split.density=false
# Project target.
target=android-19
target=android-21
apk-configurations=
renderscript.opt.level=O0
android.library=true

View File

@@ -49,7 +49,6 @@
<content src="index.html" />
<preference name="loglevel" value="DEBUG" />
<!--
<preference name="splashscreen" value="resourceName" />
<preference name="backgroundColor" value="0xFFF" />

View File

@@ -1,50 +0,0 @@
/*
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.JavascriptInterface;
import org.json.JSONException;
/**
* Contains APIs that the JS can call. All functions in here should also have
* an equivalent entry in CordovaChromeClient.java, and be added to
* cordova-js/lib/android/plugin/android/promptbasednativeapi.js
*/
class AndroidExposedJsApi implements ExposedJsApi {
private final CordovaBridge bridge;
AndroidExposedJsApi(CordovaBridge bridge) {
this.bridge = bridge;
}
@JavascriptInterface
public String exec(int bridgeSecret, String service, String action, String callbackId, String arguments) throws JSONException, IllegalAccessException {
return bridge.jsExec(bridgeSecret, service, action, callbackId, arguments);
}
@JavascriptInterface
public void setNativeToJsBridgeMode(int bridgeSecret, int value) throws IllegalAccessException {
bridge.jsSetNativeToJsBridgeMode(bridgeSecret, value);
}
@JavascriptInterface
public String retrieveJsMessages(int bridgeSecret, boolean fromOnlineEvent) throws IllegalAccessException {
return bridge.jsRetrieveJsMessages(bridgeSecret, fromOnlineEvent);
}
}

View File

@@ -1,775 +0,0 @@
/*
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.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.webkit.WebBackForwardList;
import android.webkit.WebHistoryItem;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebSettings.LayoutAlgorithm;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
/*
* This class is our web view.
*
* @see <a href="http://developer.android.com/guide/webapps/webview.html">WebView guide</a>
* @see <a href="http://developer.android.com/reference/android/webkit/WebView.html">WebView</a>
*/
public class AndroidWebView extends WebView implements CordovaWebView {
public static final String TAG = "AndroidWebView";
private HashSet<Integer> boundKeyCodes = new HashSet<Integer>();
PluginManager pluginManager;
private BroadcastReceiver receiver;
/** Activities and other important classes **/
private CordovaInterface cordova;
AndroidWebViewClient viewClient;
private AndroidChromeClient chromeClient;
// Flag to track that a loadUrl timeout occurred
int loadUrlTimeout = 0;
private long lastMenuEventTime = 0;
CordovaBridge bridge;
/** custom view created by the browser (a video player for example) */
private View mCustomView;
private WebChromeClient.CustomViewCallback mCustomViewCallback;
private CordovaResourceApi resourceApi;
private Whitelist internalWhitelist;
private Whitelist externalWhitelist;
private CordovaPreferences preferences;
// The URL passed to loadUrl(), not necessarily the URL of the current page.
String loadedUrl;
static final FrameLayout.LayoutParams COVER_SCREEN_GRAVITY_CENTER =
new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
Gravity.CENTER);
/** Used when created via reflection. */
public AndroidWebView(Context context) {
this(context, null);
}
/** Required to allow view to be used within XML layouts. */
public AndroidWebView(Context context, AttributeSet attrs) {
super(context, attrs);
}
// Use two-phase init so that the control will work with XML layouts.
@Override
public void init(CordovaInterface cordova, List<PluginEntry> pluginEntries,
Whitelist internalWhitelist, Whitelist externalWhitelist,
CordovaPreferences preferences) {
if (this.cordova != null) {
throw new IllegalStateException();
}
this.cordova = cordova;
this.internalWhitelist = internalWhitelist;
this.externalWhitelist = externalWhitelist;
this.preferences = preferences;
pluginManager = new PluginManager(this, this.cordova, pluginEntries);
resourceApi = new CordovaResourceApi(this.getContext(), pluginManager);
bridge = new CordovaBridge(pluginManager, new NativeToJsMessageQueue(this, cordova));
pluginManager.addService("App", "org.apache.cordova.CoreAndroid");
initWebViewSettings();
if (this.viewClient == null) {
setWebViewClient(Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH ?
new AndroidWebViewClient(cordova, this) :
new IceCreamCordovaWebViewClient(cordova, this));
}
if (this.chromeClient == null) {
setWebChromeClient(new AndroidChromeClient(cordova, this));
}
exposeJsInterface();
}
@SuppressLint("SetJavaScriptEnabled")
@SuppressWarnings("deprecation")
private void initWebViewSettings() {
this.setInitialScale(0);
this.setVerticalScrollBarEnabled(false);
// TODO: The Activity is the one that should call requestFocus().
if (shouldRequestFocusOnInit()) {
this.requestFocusFromTouch();
}
this.setInitialScale(0);
this.setVerticalScrollBarEnabled(false);
if (shouldRequestFocusOnInit()) {
this.requestFocusFromTouch();
}
// Enable JavaScript
final WebSettings settings = this.getSettings();
settings.setJavaScriptEnabled(true);
settings.setJavaScriptCanOpenWindowsAutomatically(true);
settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL);
// Set the nav dump for HTC 2.x devices (disabling for ICS, deprecated entirely for Jellybean 4.2)
try {
Method gingerbread_getMethod = WebSettings.class.getMethod("setNavDump", new Class[] { boolean.class });
String manufacturer = android.os.Build.MANUFACTURER;
Log.d(TAG, "CordovaWebView is running on device made by: " + manufacturer);
if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB &&
android.os.Build.MANUFACTURER.contains("HTC"))
{
gingerbread_getMethod.invoke(settings, true);
}
} catch (NoSuchMethodException e) {
Log.d(TAG, "We are on a modern version of Android, we will deprecate HTC 2.3 devices in 2.8");
} catch (IllegalArgumentException e) {
Log.d(TAG, "Doing the NavDump failed with bad arguments");
} catch (IllegalAccessException e) {
Log.d(TAG, "This should never happen: IllegalAccessException means this isn't Android anymore");
} catch (InvocationTargetException e) {
Log.d(TAG, "This should never happen: InvocationTargetException means this isn't Android anymore.");
}
//We don't save any form data in the application
settings.setSaveFormData(false);
settings.setSavePassword(false);
// Jellybean rightfully tried to lock this down. Too bad they didn't give us a whitelist
// while we do this
if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
Level16Apis.enableUniversalAccess(settings);
// Enable database
// We keep this disabled because we use or shim to get around DOM_EXCEPTION_ERROR_16
String databasePath = getContext().getApplicationContext().getDir("database", Context.MODE_PRIVATE).getPath();
settings.setDatabaseEnabled(true);
settings.setDatabasePath(databasePath);
//Determine whether we're in debug or release mode, and turn on Debugging!
ApplicationInfo appInfo = getContext().getApplicationContext().getApplicationInfo();
if ((appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0 &&
android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
enableRemoteDebugging();
}
settings.setGeolocationDatabasePath(databasePath);
// Enable DOM storage
settings.setDomStorageEnabled(true);
// Enable built-in geolocation
settings.setGeolocationEnabled(true);
// Enable AppCache
// Fix for CB-2282
settings.setAppCacheMaxSize(5 * 1048576);
settings.setAppCachePath(databasePath);
settings.setAppCacheEnabled(true);
// Fix for CB-1405
// Google issue 4641
settings.getUserAgentString();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
if (this.receiver == null) {
this.receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
settings.getUserAgentString();
}
};
getContext().registerReceiver(this.receiver, intentFilter);
}
// end CB-1405
}
@TargetApi(Build.VERSION_CODES.KITKAT)
private void enableRemoteDebugging() {
try {
WebView.setWebContentsDebuggingEnabled(true);
} catch (IllegalArgumentException e) {
Log.d(TAG, "You have one job! To turn on Remote Web Debugging! YOU HAVE FAILED! ");
e.printStackTrace();
}
}
/**
* Override this method to decide whether or not you need to request the
* focus when your application start
*
* @return true unless this method is overriden to return a different value
*/
protected boolean shouldRequestFocusOnInit() {
return true;
}
private void exposeJsInterface() {
if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)) {
Log.i(TAG, "Disabled addJavascriptInterface() bridge since Android version is old.");
// Bug being that Java Strings do not get converted to JS strings automatically.
// This isn't hard to work-around on the JS side, but it's easier to just
// use the prompt bridge instead.
return;
}
AndroidExposedJsApi exposedJsApi = new AndroidExposedJsApi(bridge);
this.addJavascriptInterface(exposedJsApi, "_cordovaNative");
}
@Override
public void setWebViewClient(WebViewClient client) {
this.viewClient = (AndroidWebViewClient)client;
super.setWebViewClient(client);
}
@Override
public void setWebChromeClient(WebChromeClient client) {
this.chromeClient = (AndroidChromeClient)client;
super.setWebChromeClient(client);
}
/**
* Load the url into the webview.
*/
@Override
public void loadUrl(String url) {
this.loadUrlIntoView(url, true);
}
/**
* Load the url into the webview.
*/
@Override
public void loadUrlIntoView(final String url, boolean recreatePlugins) {
if (url.equals("about:blank") || url.startsWith("javascript:")) {
this.loadUrlNow(url);
return;
}
LOG.d(TAG, ">>> loadUrl(" + url + ")");
recreatePlugins = recreatePlugins || (loadedUrl == null);
if (recreatePlugins) {
this.loadedUrl = url;
this.pluginManager.init();
}
// Create a timeout timer for loadUrl
final AndroidWebView me = this;
final int currentLoadUrlTimeout = me.loadUrlTimeout;
final int loadUrlTimeoutValue = preferences.getInteger("LoadUrlTimeoutValue", 20000);
// Timeout error method
final Runnable loadError = new Runnable() {
public void run() {
me.stopLoading();
LOG.e(TAG, "CordovaWebView: TIMEOUT ERROR!");
if (viewClient != null) {
viewClient.onReceivedError(AndroidWebView.this, -6, "The connection to the server was unsuccessful.", url);
}
}
};
// Timeout timer method
final Runnable timeoutCheck = new Runnable() {
public void run() {
try {
synchronized (this) {
wait(loadUrlTimeoutValue);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// If timeout, then stop loading and handle error
if (me.loadUrlTimeout == currentLoadUrlTimeout) {
me.cordova.getActivity().runOnUiThread(loadError);
}
}
};
// Load url
this.cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
cordova.getThreadPool().execute(timeoutCheck);
me.loadUrlNow(url);
}
});
}
/**
* Load URL in webview.
*/
private 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)) {
super.loadUrl(url);
}
}
@Override
public void stopLoading() {
//viewClient.isCurrentlyLoading = false;
super.stopLoading();
}
public void onScrollChanged(int l, int t, int oldl, int oldt)
{
super.onScrollChanged(l, t, oldl, oldt);
//We should post a message that the scroll changed
ScrollEvent myEvent = new ScrollEvent(l, t, oldl, oldt, this);
pluginManager.postMessage("onScrollChanged", myEvent);
}
/**
* Send JavaScript statement back to JavaScript.
* (This is a convenience method)
*/
public void sendJavascript(String statement) {
bridge.getMessageQueue().addJavaScript(statement);
}
/**
* Send a plugin result back to JavaScript.
*/
public void sendPluginResult(PluginResult result, String callbackId) {
bridge.getMessageQueue().addPluginResult(result, callbackId);
}
/**
* Go to previous page in history. (We manage our own history)
*
* @return true if we went back, false if we are already at top
*/
public boolean backHistory() {
// Check webview first to see if there is a history
// This is needed to support curPage#diffLink, since they are added to appView's history, but not our history url array (JQMobile behavior)
if (super.canGoBack()) {
printBackForwardList();
super.goBack();
return true;
}
return false;
}
/**
* Load the specified URL in the Cordova webview or a new browser instance.
*
* NOTE: If openExternal is false, only URLs listed in whitelist can be loaded.
*
* @param url The url to load.
* @param openExternal Load url in browser instead of Cordova webview.
* @param clearHistory Clear the history stack, so new page becomes top of history
* @param params Parameters for new app
*/
public void showWebPage(String url, boolean openExternal, boolean clearHistory, HashMap<String, Object> params) {
LOG.d(TAG, "showWebPage(%s, %b, %b, HashMap", url, openExternal, clearHistory);
// If clearing history
if (clearHistory) {
this.clearHistory();
}
// If loading into our webview
if (!openExternal) {
// Make sure url is in whitelist
if (url.startsWith("file://") || internalWhitelist.isUrlWhiteListed(url)) {
// TODO: What about params?
// Load new URL
loadUrlIntoView(url, true);
return;
}
// Load in default viewer if not
LOG.w(TAG, "showWebPage: Cannot load URL into webview since it is not in white list. Loading into browser instead. (URL=" + url + ")");
}
try {
// Omitting the MIME type for file: URLs causes "No Activity found to handle Intent".
// Adding the MIME type to http: URLs causes them to not be handled by the downloader.
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uri = Uri.parse(url);
if ("file".equals(uri.getScheme())) {
intent.setDataAndType(uri, resourceApi.getMimeType(uri));
} else {
intent.setData(uri);
}
cordova.getActivity().startActivity(intent);
} catch (android.content.ActivityNotFoundException e) {
LOG.e(TAG, "Error loading url " + url, e);
}
}
/*
* onKeyDown
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event)
{
if(boundKeyCodes.contains(keyCode))
{
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
this.loadUrl("javascript:cordova.fireDocumentEvent('volumedownbutton');");
return true;
}
else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
this.loadUrl("javascript:cordova.fireDocumentEvent('volumeupbutton');");
return true;
}
else
{
return super.onKeyDown(keyCode, event);
}
}
else if(keyCode == KeyEvent.KEYCODE_BACK)
{
return !(this.startOfHistory()) || isButtonPlumbedToJs(KeyEvent.KEYCODE_BACK);
}
else if(keyCode == KeyEvent.KEYCODE_MENU)
{
//How did we get here? Is there a childView?
View childView = this.getFocusedChild();
if(childView != null)
{
//Make sure we close the keyboard if it's present
InputMethodManager imm = (InputMethodManager) cordova.getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(childView.getWindowToken(), 0);
cordova.getActivity().openOptionsMenu();
return true;
} else {
return super.onKeyDown(keyCode, event);
}
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event)
{
// If back key
if (keyCode == KeyEvent.KEYCODE_BACK) {
// A custom view is currently displayed (e.g. playing a video)
if(mCustomView != null) {
this.hideCustomView();
return true;
} else {
// The webview is currently displayed
// If back key is bound, then send event to JavaScript
if (isButtonPlumbedToJs(KeyEvent.KEYCODE_BACK)) {
this.loadUrl("javascript:cordova.fireDocumentEvent('backbutton');");
return true;
} else {
// If not bound
// Go to previous page in webview if it is possible to go back
if (this.backHistory()) {
return true;
}
// If not, then invoke default behavior
}
}
}
// Legacy
else if (keyCode == KeyEvent.KEYCODE_MENU) {
if (this.lastMenuEventTime < event.getEventTime()) {
this.loadUrl("javascript:cordova.fireDocumentEvent('menubutton');");
}
this.lastMenuEventTime = event.getEventTime();
return super.onKeyUp(keyCode, event);
}
// If search key
else if (keyCode == KeyEvent.KEYCODE_SEARCH) {
this.loadUrl("javascript:cordova.fireDocumentEvent('searchbutton');");
return true;
}
//Does webkit change this behavior?
return super.onKeyUp(keyCode, event);
}
@Override
public void setButtonPlumbedToJs(int keyCode, boolean override) {
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_DOWN:
case KeyEvent.KEYCODE_VOLUME_UP:
case KeyEvent.KEYCODE_BACK:
// TODO: Why are search and menu buttons handled separately?
if (override) {
boundKeyCodes.add(keyCode);
} else {
boundKeyCodes.remove(keyCode);
}
return;
default:
throw new IllegalArgumentException("Unsupported keycode: " + keyCode);
}
}
@Override
public boolean isButtonPlumbedToJs(int keyCode)
{
return boundKeyCodes.contains(keyCode);
}
public void handlePause(boolean keepRunning)
{
LOG.d(TAG, "Handle the pause");
// Send pause event to JavaScript
this.loadUrl("javascript:try{cordova.fireDocumentEvent('pause');}catch(e){console.log('exception firing pause event from native');};");
// Forward to plugins
if (this.pluginManager != null) {
this.pluginManager.onPause(keepRunning);
}
// If app doesn't want to run in background
if (!keepRunning) {
// Pause JavaScript timers (including setInterval)
this.pauseTimers();
}
}
public void handleResume(boolean keepRunning, boolean activityResultKeepRunning)
{
this.loadUrl("javascript:try{cordova.fireDocumentEvent('resume');}catch(e){console.log('exception firing resume event from native');};");
// Forward to plugins
if (this.pluginManager != null) {
this.pluginManager.onResume(keepRunning);
}
// Resume JavaScript timers (including setInterval)
this.resumeTimers();
}
public void handleDestroy()
{
// Send destroy event to JavaScript
this.loadUrl("javascript:try{cordova.require('cordova/channel').onDestroy.fire();}catch(e){console.log('exception firing destroy event from native');};");
// Load blank page so that JavaScript onunload is called
this.loadUrl("about:blank");
// Forward to plugins
if (this.pluginManager != null) {
this.pluginManager.onDestroy();
}
// unregister the receiver
if (this.receiver != null) {
try {
getContext().unregisterReceiver(this.receiver);
} catch (Exception e) {
Log.e(TAG, "Error unregistering configuration receiver: " + e.getMessage(), e);
}
}
}
public void onNewIntent(Intent intent)
{
//Forward to plugins
if (this.pluginManager != null) {
this.pluginManager.onNewIntent(intent);
}
}
// Wrapping these functions in their own class prevents warnings in adb like:
// VFY: unable to resolve virtual method 285: Landroid/webkit/WebSettings;.setAllowUniversalAccessFromFileURLs
@TargetApi(16)
private static class Level16Apis {
static void enableUniversalAccess(WebSettings settings) {
settings.setAllowUniversalAccessFromFileURLs(true);
}
}
public void printBackForwardList() {
WebBackForwardList currentList = this.copyBackForwardList();
int currentSize = currentList.getSize();
for(int i = 0; i < currentSize; ++i)
{
WebHistoryItem item = currentList.getItemAtIndex(i);
String url = item.getUrl();
LOG.d(TAG, "The URL at index: " + Integer.toString(i) + " is " + url );
}
}
//Can Go Back is BROKEN!
public boolean startOfHistory()
{
WebBackForwardList currentList = this.copyBackForwardList();
WebHistoryItem item = currentList.getItemAtIndex(0);
if( item!=null){ // Null-fence in case they haven't called loadUrl yet (CB-2458)
String url = item.getUrl();
String currentUrl = this.getUrl();
LOG.d(TAG, "The current URL is: " + currentUrl);
LOG.d(TAG, "The URL at item 0 is: " + url);
return currentUrl.equals(url);
}
return false;
}
public void showCustomView(View view, WebChromeClient.CustomViewCallback callback) {
// This code is adapted from the original Android Browser code, licensed under the Apache License, Version 2.0
Log.d(TAG, "showing Custom View");
// if a view already exists then immediately terminate the new one
if (mCustomView != null) {
callback.onCustomViewHidden();
return;
}
// Store the view and its callback for later (to kill it properly)
mCustomView = view;
mCustomViewCallback = callback;
// Add the custom view to its container.
ViewGroup parent = (ViewGroup) this.getParent();
parent.addView(view, COVER_SCREEN_GRAVITY_CENTER);
// Hide the content view.
this.setVisibility(View.GONE);
// Finally show the custom view container.
parent.setVisibility(View.VISIBLE);
parent.bringToFront();
}
public void hideCustomView() {
// This code is adapted from the original Android Browser code, licensed under the Apache License, Version 2.0
Log.d(TAG, "Hiding Custom View");
if (mCustomView == null) return;
// Hide the custom view.
mCustomView.setVisibility(View.GONE);
// Remove the custom view from its container.
ViewGroup parent = (ViewGroup) this.getParent();
parent.removeView(mCustomView);
mCustomView = null;
mCustomViewCallback.onCustomViewHidden();
// Show the content view.
this.setVisibility(View.VISIBLE);
}
/**
* if the video overlay is showing then we need to know
* as it effects back button handling
*
* @return true if custom view is showing
*/
public boolean isCustomViewShowing() {
return mCustomView != null;
}
public WebBackForwardList restoreState(Bundle savedInstanceState)
{
WebBackForwardList myList = super.restoreState(savedInstanceState);
Log.d(TAG, "WebView restoration crew now restoring!");
//Initialize the plugin manager once more
this.pluginManager.init();
return myList;
}
public CordovaResourceApi getResourceApi() {
return resourceApi;
}
void onPageReset() {
boundKeyCodes.clear();
pluginManager.onReset();
bridge.reset(loadedUrl);
}
@Override
public PluginManager getPluginManager() {
return this.pluginManager;
}
@Override
public View getView() {
return this;
}
@Override
public Whitelist getWhitelist() {
return this.internalWhitelist;
}
@Override
public Whitelist getExternalWhitelist() {
return this.externalWhitelist;
}
@Override
public CordovaPreferences getPreferences() {
return preferences;
}
@Override
public void onFilePickerResult(Uri uri) {
if (null == chromeClient.mUploadMessage)
return;
chromeClient.mUploadMessage.onReceiveValue(uri);
chromeClient.mUploadMessage = null;
}
@Override
public Object postMessage(String id, Object data) {
return pluginManager.postMessage(id, data);
}
}

View File

@@ -39,10 +39,21 @@ import java.util.HashMap;
/**
* This class exposes methods in Cordova that can be called from JavaScript.
*/
public class CoreAndroid extends CordovaPlugin {
public class App extends CordovaPlugin {
public static final String PLUGIN_NAME = "App";
protected static final String TAG = "CordovaApp";
private BroadcastReceiver telephonyReceiver;
private CallbackContext messageChannel;
/**
* Send an event to be fired on the Javascript side.
*
* @param action The name of the event to be fired
*/
public void fireJavascriptEvent(String action) {
sendEventMessage(action);
}
/**
* Sets the context of the Command. This can then be used to do things like
@@ -75,7 +86,7 @@ public class CoreAndroid extends CordovaPlugin {
// indicative of what this actually does (shows the webview).
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
webView.getPluginManager().postMessage("spinner", "stop");
webView.postMessage("spinner", "stop");
}
});
}
@@ -100,6 +111,11 @@ public class CoreAndroid extends CordovaPlugin {
else if (action.equals("exitApp")) {
this.exitApp();
}
else if (action.equals("messageChannel")) {
messageChannel = callbackContext;
return true;
}
callbackContext.sendPluginResult(new PluginResult(status, result));
return true;
} catch (JSONException e) {
@@ -247,9 +263,9 @@ public class CoreAndroid extends CordovaPlugin {
* Exit the Android application.
*/
public void exitApp() {
this.webView.getPluginManager().postMessage("exit", null);
this.webView.postMessage("exit", null);
}
/**
* Listen for telephony events: RINGING, OFFHOOK and IDLE
@@ -271,15 +287,15 @@ public class CoreAndroid extends CordovaPlugin {
String extraData = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
if (extraData.equals(TelephonyManager.EXTRA_STATE_RINGING)) {
LOG.i(TAG, "Telephone RINGING");
webView.getPluginManager().postMessage("telephone", "ringing");
webView.postMessage("telephone", "ringing");
}
else if (extraData.equals(TelephonyManager.EXTRA_STATE_OFFHOOK)) {
LOG.i(TAG, "Telephone OFFHOOK");
webView.getPluginManager().postMessage("telephone", "offhook");
webView.postMessage("telephone", "offhook");
}
else if (extraData.equals(TelephonyManager.EXTRA_STATE_IDLE)) {
LOG.i(TAG, "Telephone IDLE");
webView.getPluginManager().postMessage("telephone", "idle");
webView.postMessage("telephone", "idle");
}
}
}
@@ -287,7 +303,21 @@ public class CoreAndroid extends CordovaPlugin {
};
// Register the receiver
this.cordova.getActivity().registerReceiver(this.telephonyReceiver, intentFilter);
webView.getContext().registerReceiver(this.telephonyReceiver, intentFilter);
}
private void sendEventMessage(String action) {
JSONObject obj = new JSONObject();
try {
obj.put("action", action);
} catch (JSONException e) {
LOG.e(TAG, "Failed to create event message", e);
}
PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, obj);
pluginResult.setKeepCallback(true);
if (messageChannel != null) {
messageChannel.sendPluginResult(pluginResult);
}
}
/*
@@ -296,6 +326,6 @@ public class CoreAndroid extends CordovaPlugin {
*/
public void onDestroy()
{
this.cordova.getActivity().unregisterReceiver(this.telephonyReceiver);
webView.getContext().unregisterReceiver(this.telephonyReceiver);
}
}

View File

@@ -30,6 +30,7 @@ import org.xmlpull.v1.XmlPullParserException;
import android.app.Activity;
import android.content.res.XmlResourceParser;
import android.util.Log;
public class ConfigXmlParser {
private static String TAG = "ConfigXmlParser";
@@ -79,6 +80,7 @@ public class ConfigXmlParser {
String service = "", pluginClass = "", paramType = "";
boolean onload = false;
boolean insideFeature = false;
ArrayList<String> urlMap = null;
// Add implicitly allowed URLs
internalWhitelist.addWhiteListEntry("file:///*", false);
@@ -88,7 +90,13 @@ public class ConfigXmlParser {
while (eventType != XmlResourceParser.END_DOCUMENT) {
if (eventType == XmlResourceParser.START_TAG) {
String strNode = xml.getName();
if (strNode.equals("feature")) {
if (strNode.equals("url-filter")) {
Log.w(TAG, "Plugin " + service + " is using deprecated tag <url-filter>");
if (urlMap == null) {
urlMap = new ArrayList<String>(2);
}
urlMap.add(xml.getAttributeValue(null, "value"));
} else if (strNode.equals("feature")) {
//Check for supported feature sets aka. plugins (Accelerometer, Geolocation, etc)
//Set the bit for reading params
insideFeature = true;
@@ -139,12 +147,13 @@ public class ConfigXmlParser {
{
String strNode = xml.getName();
if (strNode.equals("feature")) {
pluginEntries.add(new PluginEntry(service, pluginClass, onload));
pluginEntries.add(new PluginEntry(service, pluginClass, onload, urlMap));
service = "";
pluginClass = "";
insideFeature = false;
onload = false;
urlMap = null;
}
}
try {

View File

@@ -18,36 +18,33 @@
*/
package org.apache.cordova;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.LOG;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Color;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.Display;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.Window;
import android.view.WindowManager;
import android.webkit.WebViewClient;
@@ -70,6 +67,7 @@ import android.widget.LinearLayout;
* &#64;Override
* public void onCreate(Bundle savedInstanceState) {
* super.onCreate(savedInstanceState);
* super.init();
* // Load your application
* loadUrl(launchUrl);
* }
@@ -89,7 +87,12 @@ public class CordovaActivity extends Activity implements CordovaInterface {
// The webview for our app
protected CordovaWebView appView;
protected ProgressDialog spinnerDialog = null;
@Deprecated // unused.
protected CordovaWebViewClient webViewClient;
@Deprecated // Will be removed. Use findViewById() to retrieve views.
protected LinearLayout root;
private final ExecutorService threadPool = Executors.newCachedThreadPool();
private static int ACTIVITY_STARTING = 0;
@@ -98,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;
/*
@@ -106,8 +110,10 @@ public class CordovaActivity extends Activity implements CordovaInterface {
*/
// Draw a splash screen using an image located in the drawable resource directory.
// This is not the same as calling super.loadSplashscreen(url)
@Deprecated // Use "SplashScreen" preference instead.
protected int splashscreen = 0;
@Deprecated // Use "SplashScreenDelay" preference instead.
protected int splashscreenTime = -1;
// LoadUrl timeout value in msec (default of 20 sec)
protected int loadUrlTimeoutValue = 20000;
@@ -126,6 +132,64 @@ public class CordovaActivity extends Activity implements CordovaInterface {
protected String launchUrl;
protected ArrayList<PluginEntry> pluginEntries;
/**
* Sets the authentication token.
*
* @param authenticationToken
* @param host
* @param realm
*/
public void setAuthenticationToken(AuthenticationToken authenticationToken, String host, String realm) {
if (this.appView != null && this.appView.viewClient != null) {
this.appView.viewClient.setAuthenticationToken(authenticationToken, host, realm);
}
}
/**
* Removes the authentication token.
*
* @param host
* @param realm
*
* @return the authentication token or null if did not exist
*/
public AuthenticationToken removeAuthenticationToken(String host, String realm) {
if (this.appView != null && this.appView.viewClient != null) {
return this.appView.viewClient.removeAuthenticationToken(host, realm);
}
return null;
}
/**
* Gets the authentication token.
*
* In order it tries:
* 1- host + realm
* 2- host
* 3- realm
* 4- no host, no realm
*
* @param host
* @param realm
*
* @return the authentication token
*/
public AuthenticationToken getAuthenticationToken(String host, String realm) {
if (this.appView != null && this.appView.viewClient != null) {
return this.appView.viewClient.getAuthenticationToken(host, realm);
}
return null;
}
/**
* Clear all authentication tokens.
*/
public void clearAuthenticationTokens() {
if (this.appView != null && this.appView.viewClient != null) {
this.appView.viewClient.clearAuthenticationTokens();
}
}
/**
* Called when the activity is first created.
*/
@@ -133,22 +197,14 @@ public class CordovaActivity extends Activity implements CordovaInterface {
public void onCreate(Bundle savedInstanceState) {
LOG.i(TAG, "Apache Cordova native platform version " + CordovaWebView.CORDOVA_VERSION + " is starting");
LOG.d(TAG, "CordovaActivity.onCreate()");
super.onCreate(savedInstanceState);
if(savedInstanceState != null)
{
initCallbackClass = savedInstanceState.getString("callbackClass");
}
// need to activate preferences before super.onCreate to avoid "requestFeature() must be called before adding content" exception
loadConfig();
}
protected void init() {
if(!preferences.getBoolean("ShowTitle", false))
{
getWindow().requestFeature(Window.FEATURE_NO_TITLE);
}
if(preferences.getBoolean("SetFullscreen", false))
{
Log.d(TAG, "The SetFullscreen configuration is deprecated in favor of Fullscreen, and will be removed in a future version.");
@@ -162,17 +218,12 @@ public class CordovaActivity extends Activity implements CordovaInterface {
WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
}
appView = makeWebView();
super.onCreate(savedInstanceState);
// TODO: Have the views set this themselves.
if (preferences.getBoolean("DisallowOverscroll", false)) {
appView.getView().setOverScrollMode(View.OVER_SCROLL_NEVER);
if(savedInstanceState != null)
{
initCallbackClass = savedInstanceState.getString("callbackClass");
}
createViews();
// TODO: Make this a preference (CB-6153)
// Setup the hardware volume controls to handle volume control
setVolumeControlStream(AudioManager.STREAM_MUSIC);
}
@SuppressWarnings("deprecation")
@@ -192,24 +243,32 @@ public class CordovaActivity extends Activity implements CordovaInterface {
@SuppressWarnings("deprecation")
protected void createViews() {
// This builds the view. We could probably get away with NOT having a LinearLayout, but I like having a bucket!
LOG.d(TAG, "CordovaActivity.createViews()");
Display display = getWindowManager().getDefaultDisplay();
int width = display.getWidth();
int height = display.getHeight();
LinearLayoutSoftKeyboardDetect root = new LinearLayoutSoftKeyboardDetect(this, width, height);
root = new LinearLayoutSoftKeyboardDetect(this, width, height);
root.setOrientation(LinearLayout.VERTICAL);
root.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT, 0.0F));
appView.getView().setId(100);
appView.getView().setLayoutParams(new LinearLayout.LayoutParams(
appView.setId(100);
appView.setLayoutParams(new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
1.0F));
// Add web view but make it invisible while loading URL
appView.getView().setVisibility(View.INVISIBLE);
root.addView(appView.getView());
// need to remove appView from any existing parent before invoking root.addView(appView)
ViewParent parent = appView.getParent();
if ((parent != null) && (parent != root)) {
LOG.d(TAG, "removing appView from existing parent");
ViewGroup parentGroup = (ViewGroup) parent;
parentGroup.removeView(appView);
}
root.addView((View) appView);
setContentView(root);
int backgroundColor = preferences.getInteger("BackgroundColor", Color.BLACK);
@@ -230,68 +289,82 @@ public class CordovaActivity extends Activity implements CordovaInterface {
* require a more specialized web view.
*/
protected CordovaWebView makeWebView() {
String r = preferences.getString("webView", null);
CordovaWebView ret = null;
if (r != null) {
try {
Class<?> webViewClass = Class.forName(r);
Constructor<?> constructor = webViewClass.getConstructor(Context.class);
ret = (CordovaWebView) constructor.newInstance((Context)this);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return new CordovaWebView(CordovaActivity.this);
}
/**
* Construct the client for the default web view object.
*
* This is intended to be overridable by subclasses of CordovaIntent which
* require a more specialized web view.
*
* @param webView the default constructed web view object
*/
protected CordovaWebViewClient makeWebViewClient(CordovaWebView webView) {
return webView.makeWebViewClient(this);
}
/**
* Construct the chrome client for the default web view object.
*
* This is intended to be overridable by subclasses of CordovaIntent which
* require a more specialized web view.
*
* @param webView the default constructed web view object
*/
protected CordovaChromeClient makeChromeClient(CordovaWebView webView) {
return webView.makeWebChromeClient(this);
}
public void init() {
this.init(appView, null, null);
}
@SuppressLint("NewApi")
@Deprecated // Call init() instead and override makeWebView() to customize.
public void init(CordovaWebView webView, CordovaWebViewClient webViewClient, CordovaChromeClient webChromeClient) {
LOG.d(TAG, "CordovaActivity.init()");
if (splashscreenTime >= 0) {
preferences.set("SplashScreenDelay", splashscreenTime);
}
if (ret == null) {
// If all else fails, return a default WebView
ret = new AndroidWebView(this);
if (splashscreen != 0) {
preferences.set("SplashDrawableId", splashscreen);
}
appView = webView != null ? webView : makeWebView();
// TODO: Have the views set this themselves.
if (preferences.getBoolean("DisallowOverscroll", false)) {
appView.setOverScrollMode(View.OVER_SCROLL_NEVER);
}
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))) {
setVolumeControlStream(AudioManager.STREAM_MUSIC);
}
ret.init(this, pluginEntries, internalWhitelist, externalWhitelist, preferences);
return ret;
}
/**
* Load the url into the webview.
*/
public void loadUrl(String url, int splashscreenTime) {
public void loadUrl(String url) {
if (appView == null) {
init();
}
String splash = preferences.getString("SplashScreen", null);
if(splashscreenTime > 0 && splash != null)
{
this.splashscreen = getResources().getIdentifier(splash, "drawable", getClass().getPackage().getName());;
if(this.splashscreen != 0)
{
this.showSplashScreen(splashscreenTime);
}
}
// If keepRunning
this.keepRunning = preferences.getBoolean("KeepRunning", true);
//Check if the view is attached to anything
if(appView.getView().getParent() != null)
{
// Then load the spinner
this.loadSpinner();
}
//Load the correct splashscreen
if(this.splashscreen != 0)
{
appView.getPluginManager().postMessage("splashscreen", "show");
}
this.appView.loadUrlIntoView(url, true);
appView.loadUrlIntoView(url, true);
}
/**
@@ -301,44 +374,139 @@ public class CordovaActivity extends Activity implements CordovaInterface {
* @param url
* @param time The number of ms to wait before loading webview
*/
public void loadUrl(final String url) {
@Deprecated // Use loadUrl(String url) instead.
public void loadUrl(final String url, int time) {
this.splashscreenTime = time;
this.loadUrl(url);
}
@Deprecated
public void cancelLoadUrl() {
}
/**
* Clear the resource cache.
*/
@Deprecated // Call method on appView directly.
public void clearCache() {
if (appView == null) {
init();
}
this.loadUrl(url, preferences.getInteger("SplashScreenDelay", 3000));
this.appView.clearCache(true);
}
/*
* Load the spinner
/**
* Clear web history in this web view.
*/
void loadSpinner() {
@Deprecated // Call method on appView directly.
public void clearHistory() {
this.appView.clearHistory();
}
// If loadingDialog property, then show the App loading dialog for first page of app
String loading = null;
if ((this.appView == null) || !this.appView.canGoBack()) {
loading = preferences.getString("LoadingDialog", null);
/**
* Go to previous page in history. (We manage our own history)
*
* @return true if we went back, false if we are already at top
*/
@Deprecated // Call method on appView directly.
public boolean backHistory() {
if (this.appView != null) {
return appView.backHistory();
}
else {
loading = preferences.getString("LoadingPageDialog", null);
}
if (loading != null) {
return false;
}
String title = "";
String message = "Loading Application...";
/**
* Get boolean property for activity.
*/
@Deprecated // Call method on preferences directly.
public boolean getBooleanProperty(String name, boolean defaultValue) {
return preferences.getBoolean(name, defaultValue);
}
if (loading.length() > 0) {
int comma = loading.indexOf(',');
if (comma > 0) {
title = loading.substring(0, comma);
message = loading.substring(comma + 1);
}
else {
title = "";
message = loading;
}
}
this.spinnerStart(title, message);
}
/**
* Get int property for activity.
*/
@Deprecated // Call method on preferences directly.
public int getIntegerProperty(String name, int defaultValue) {
return preferences.getInteger(name, defaultValue);
}
/**
* Get string property for activity.
*/
@Deprecated // Call method on preferences directly.
public String getStringProperty(String name, String defaultValue) {
return preferences.getString(name, defaultValue);
}
/**
* Get double property for activity.
*/
@Deprecated // Call method on preferences directly.
public double getDoubleProperty(String name, double defaultValue) {
return preferences.getDouble(name, defaultValue);
}
/**
* Set boolean property on activity.
* This method has been deprecated in 3.0 and will be removed at a future
* time. Please use config.xml instead.
*
* @param name
* @param value
* @deprecated
*/
@Deprecated
public void setBooleanProperty(String name, boolean value) {
Log.d(TAG, "Setting boolean properties in CordovaActivity will be deprecated in 3.0 on July 2013, please use config.xml");
this.getIntent().putExtra(name.toLowerCase(), value);
}
/**
* Set int property on activity.
* This method has been deprecated in 3.0 and will be removed at a future
* time. Please use config.xml instead.
*
* @param name
* @param value
* @deprecated
*/
@Deprecated
public void setIntegerProperty(String name, int value) {
Log.d(TAG, "Setting integer properties in CordovaActivity will be deprecated in 3.0 on July 2013, please use config.xml");
this.getIntent().putExtra(name.toLowerCase(), value);
}
/**
* Set string property on activity.
* This method has been deprecated in 3.0 and will be removed at a future
* time. Please use config.xml instead.
*
* @param name
* @param value
* @deprecated
*/
@Deprecated
public void setStringProperty(String name, String value) {
Log.d(TAG, "Setting string properties in CordovaActivity will be deprecated in 3.0 on July 2013, please use config.xml");
this.getIntent().putExtra(name.toLowerCase(), value);
}
/**
* Set double property on activity.
* This method has been deprecated in 3.0 and will be removed at a future
* time. Please use config.xml instead.
*
* @param name
* @param value
* @deprecated
*/
@Deprecated
public void setDoubleProperty(String name, double value) {
Log.d(TAG, "Setting double properties in CordovaActivity will be deprecated in 3.0 on July 2013, please use config.xml");
this.getIntent().putExtra(name.toLowerCase(), value);
}
/**
@@ -362,9 +530,6 @@ public class CordovaActivity extends Activity implements CordovaInterface {
{
this.appView.handlePause(this.keepRunning);
}
// hide the splash screen to avoid leaking a window
this.removeSplashScreen();
}
/**
@@ -419,9 +584,6 @@ public class CordovaActivity extends Activity implements CordovaInterface {
LOG.d(TAG, "CordovaActivity.onDestroy()");
super.onDestroy();
// hide the splash screen to avoid leaking a window
this.removeSplashScreen();
if (this.appView != null) {
appView.handleDestroy();
}
@@ -430,45 +592,67 @@ public class CordovaActivity extends Activity implements CordovaInterface {
}
}
/**
* Send a message to all plugins.
*/
public void postMessage(String id, Object data) {
if (this.appView != null) {
this.appView.postMessage(id, data);
}
}
/**
* @deprecated
* Add services to res/xml/plugins.xml instead.
*
* Add a class that implements a service.
*/
@Deprecated
public void addService(String serviceType, String className) {
if (this.appView != null && this.appView.pluginManager != null) {
this.appView.pluginManager.addService(serviceType, className);
}
}
/**
* Send JavaScript statement back to JavaScript.
* (This is a convenience method)
*
* @param statement
*/
@Deprecated // Call method on appView directly.
public void sendJavascript(String statement) {
if (this.appView != null) {
this.appView.bridge.getMessageQueue().addJavaScript(statement);
}
}
/**
* Show the spinner. Must be called from the UI thread.
*
* @param title Title of the dialog
* @param message The message of the dialog
*/
@Deprecated // Call this directly on SplashScreen plugin instead.
public void spinnerStart(final String title, final String message) {
if (this.spinnerDialog != null) {
this.spinnerDialog.dismiss();
this.spinnerDialog = null;
}
final CordovaActivity me = this;
this.spinnerDialog = ProgressDialog.show(CordovaActivity.this, title, message, true, true,
new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
me.spinnerDialog = null;
}
});
JSONArray args = new JSONArray();
args.put(title);
args.put(message);
doSplashScreenAction("spinnerStart", args);
}
/**
* Stop spinner - Must be called from UI thread
*/
@Deprecated // Call this directly on SplashScreen plugin instead.
public void spinnerStop() {
if (this.spinnerDialog != null && this.spinnerDialog.isShowing()) {
this.spinnerDialog.dismiss();
this.spinnerDialog = null;
}
doSplashScreenAction("spinnerStop", null);
}
/**
* End this activity by calling finish for activity
*/
public void endActivity() {
finish();
}
@Override
public void finish() {
this.activityState = ACTIVITY_EXITING;
super.finish();
}
@@ -483,7 +667,7 @@ public class CordovaActivity extends Activity implements CordovaInterface {
* @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
@@ -491,8 +675,19 @@ public class CordovaActivity extends Activity implements CordovaInterface {
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);
}
/**
@@ -502,32 +697,34 @@ public class CordovaActivity extends Activity implements CordovaInterface {
* @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 == AndroidChromeClient.FILECHOOSER_RESULTCODE) {
Uri result = intent == null || resultCode != Activity.RESULT_OK ? null : intent.getData();
appView.onFilePickerResult(result);
}
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);
this.activityResultCallback = appView.getPluginManager().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;
}
@@ -545,11 +742,10 @@ public class CordovaActivity extends Activity implements CordovaInterface {
// If errorUrl specified, then load it
final String errorUrl = preferences.getString("errorUrl", null);
if ((errorUrl != null) && (errorUrl.startsWith("file://") || internalWhitelist.isUrlWhiteListed(errorUrl)) && (!failingUrl.equals(errorUrl))) {
// Load URL on UI thread
me.runOnUiThread(new Runnable() {
public void run() {
// Stop "app loading" spinner if showing
me.spinnerStop();
me.appView.showWebPage(errorUrl, false, true, null);
}
});
@@ -560,7 +756,7 @@ public class CordovaActivity extends Activity implements CordovaInterface {
me.runOnUiThread(new Runnable() {
public void run() {
if (exit) {
me.appView.getView().setVisibility(View.GONE);
me.appView.setVisibility(View.GONE);
me.displayError("Application Error", description + " (" + failingUrl + ")", "OK", exit);
}
}
@@ -598,91 +794,121 @@ public class CordovaActivity extends Activity implements CordovaInterface {
});
}
/**
* Determine if URL is in approved list of URLs to load.
*/
@Deprecated // Use whitelist object directly.
public boolean isUrlWhiteListed(String url) {
return internalWhitelist.isUrlWhiteListed(url);
}
/*
* Hook in Cordova for menu plugins
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
if (appView != null) {
appView.getPluginManager().postMessage("onCreateOptionsMenu", menu);
}
this.postMessage("onCreateOptionsMenu", menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
if (appView != null) {
appView.getPluginManager().postMessage("onPrepareOptionsMenu", menu);
}
this.postMessage("onPrepareOptionsMenu", menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (appView != null) {
appView.getPluginManager().postMessage("onOptionsItemSelected", item);
}
this.postMessage("onOptionsItemSelected", item);
return true;
}
protected Dialog splashDialog;
/**
* Get Activity context.
*/
@Deprecated
public Context getContext() {
LOG.d(TAG, "This will be deprecated December 2012");
return this;
}
/**
* Load the specified URL in the Cordova webview or a new browser instance.
*
* NOTE: If openExternal is false, only URLs listed in whitelist can be loaded.
*
* @param url The url to load.
* @param openExternal Load url in browser instead of Cordova webview.
* @param clearHistory Clear the history stack, so new page becomes top of history
* @param params Parameters for new app
*/
@Deprecated // Call method on appView directly.
public void showWebPage(String url, boolean openExternal, boolean clearHistory, HashMap<String, Object> params) {
if (this.appView != null) {
appView.showWebPage(url, openExternal, clearHistory, params);
}
}
private void doSplashScreenAction(String action, JSONArray args) {
CordovaPlugin p = appView.pluginManager.getPlugin("org.apache.cordova.splashscreeninternal");
if (p != null) {
args = args == null ? new JSONArray() : args;
try {
p.execute(action, args, null);
} catch (JSONException e) {
e.printStackTrace();
}
}
}
/**
* Removes the Dialog that displays the splash screen
*/
@Deprecated
public void removeSplashScreen() {
if (splashDialog != null && splashDialog.isShowing()) {
splashDialog.dismiss();
splashDialog = null;
}
doSplashScreenAction("hide", null);
}
/**
* Shows the splash screen over the full Activity
*/
@SuppressWarnings("deprecation")
@Deprecated
protected void showSplashScreen(final int time) {
final CordovaActivity that = this;
Runnable runnable = new Runnable() {
public void run() {
// Get reference to display
Display display = getWindowManager().getDefaultDisplay();
// Create the layout for the dialog
LinearLayout root = new LinearLayout(that.getActivity());
root.setMinimumHeight(display.getHeight());
root.setMinimumWidth(display.getWidth());
root.setOrientation(LinearLayout.VERTICAL);
root.setBackgroundColor(preferences.getInteger("backgroundColor", Color.BLACK));
root.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT, 0.0F));
root.setBackgroundResource(that.splashscreen);
// Create and show the dialog
splashDialog = new Dialog(that, android.R.style.Theme_Translucent_NoTitleBar);
// check to see if the splash screen should be full screen
if ((getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN)
== WindowManager.LayoutParams.FLAG_FULLSCREEN) {
splashDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
splashDialog.setContentView(root);
splashDialog.setCancelable(false);
splashDialog.show();
// Set Runnable to remove splash screen just in case
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
public void run() {
removeSplashScreen();
}
}, time);
}
};
this.runOnUiThread(runnable);
preferences.set("SplashScreenDelay", time);
doSplashScreenAction("show", null);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event)
{
if (appView != null && (appView.isCustomViewShowing() || appView.getFocusedChild() != null ) &&
(keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU)) {
return appView.onKeyUp(keyCode, event);
} else {
return super.onKeyUp(keyCode, event);
}
}
/*
* Android 2.x needs to be able to check where the cursor is. Android 4.x does not
*
* (non-Javadoc)
* @see android.app.Activity#onKeyDown(int, android.view.KeyEvent)
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event)
{
//Determine if the focus is on the current view or not
if (appView != null && appView.getFocusedChild() != null && (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU)) {
return appView.onKeyDown(keyCode, event);
}
else
return super.onKeyDown(keyCode, event);
}
/**
* Called when a message is sent to plugin.
*
@@ -695,28 +921,7 @@ public class CordovaActivity extends Activity implements CordovaInterface {
LOG.d(TAG, "onMessage(" + id + "," + data + ")");
}
if ("splashscreen".equals(id)) {
if ("hide".equals(data.toString())) {
this.removeSplashScreen();
}
else {
// If the splash dialog is showing don't try to show it again
if (this.splashDialog == null || !this.splashDialog.isShowing()) {
String splashResource = preferences.getString("SplashScreen", null);
if (splashResource != null) {
splashscreen = getResources().getIdentifier(splashResource, "drawable", getClass().getPackage().getName());
}
this.showSplashScreen(preferences.getInteger("SplashScreenDelay", 3000));
}
}
}
else if ("spinner".equals(id)) {
if ("stop".equals(data.toString())) {
this.spinnerStop();
this.appView.getView().setVisibility(View.VISIBLE);
}
}
else if ("onReceivedError".equals(id)) {
if ("onReceivedError".equals(id)) {
JSONObject d = (JSONObject) data;
try {
this.onReceivedError(d.getInt("errorCode"), d.getString("description"), d.getString("url"));

View File

@@ -18,6 +18,8 @@
*/
package org.apache.cordova;
import java.security.SecureRandom;
import org.apache.cordova.PluginManager;
import org.json.JSONArray;
import org.json.JSONException;
@@ -35,12 +37,14 @@ public class CordovaBridge {
private NativeToJsMessageQueue jsMessageQueue;
private volatile int expectedBridgeSecret = -1; // written by UI thread, read by JS thread.
private String loadedUrl;
private String appContentUrlPrefix;
public CordovaBridge(PluginManager pluginManager, NativeToJsMessageQueue jsMessageQueue) {
public CordovaBridge(PluginManager pluginManager, NativeToJsMessageQueue jsMessageQueue, String packageName) {
this.pluginManager = pluginManager;
this.jsMessageQueue = jsMessageQueue;
this.appContentUrlPrefix = "content://" + packageName + ".";
}
public String jsExec(int bridgeSecret, String service, String action, String callbackId, String arguments) throws JSONException, IllegalAccessException {
if (!verifySecret("exec()", bridgeSecret)) {
return null;
@@ -95,6 +99,8 @@ public class CordovaBridge {
}
// Bridge secret wrong and bridge not due to it being from the previous page.
if (expectedBridgeSecret < 0 || bridgeSecret != expectedBridgeSecret) {
Log.e(LOG_TAG, "Bridge access attempt with wrong secret token, possibly from malicious code. Disabling exec() bridge!");
clearBridgeSecret();
throw new IllegalAccessException();
}
return true;
@@ -107,7 +113,8 @@ public class CordovaBridge {
/** Called by cordova.js to initialize the bridge. */
int generateBridgeSecret() {
expectedBridgeSecret = (int)(Math.random() * Integer.MAX_VALUE);
SecureRandom randGen = new SecureRandom();
expectedBridgeSecret = randGen.nextInt(Integer.MAX_VALUE);
return expectedBridgeSecret;
}
@@ -162,7 +169,9 @@ public class CordovaBridge {
// Protect against random iframes being able to talk through the bridge.
// Trust only file URLs and the start URL's domain.
// The extra origin.startsWith("http") is to protect against iframes with data: having "" as origin.
if (origin.startsWith("file:") || (origin.startsWith("http") && loadedUrl.startsWith(origin))) {
if (origin.startsWith("file:") ||
origin.startsWith(this.appContentUrlPrefix) ||
(origin.startsWith("http") && loadedUrl.startsWith(origin))) {
// Enable the bridge
int bridgeMode = Integer.parseInt(defaultValue.substring(9));
jsMessageQueue.setBridgeMode(bridgeMode);

View File

@@ -22,10 +22,14 @@ import org.apache.cordova.CordovaInterface;
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;
@@ -54,25 +58,35 @@ import android.widget.RelativeLayout;
* @see CordovaWebViewClient
* @see CordovaWebView
*/
public class AndroidChromeClient extends WebChromeClient {
public class CordovaChromeClient extends WebChromeClient {
public static final int FILECHOOSER_RESULTCODE = 5173;
private static final String LOG_TAG = "AndroidChromeClient";
private String TAG = "CordovaLog";
private long MAX_QUOTA = 100 * 1024 * 1024;
protected final CordovaInterface cordova;
protected final AndroidWebView appView;
protected CordovaInterface cordova;
protected CordovaWebView appView;
// the video progress view
private View mVideoProgressView;
// File Chooser
protected ValueCallback<Uri> mUploadMessage;
public AndroidChromeClient(CordovaInterface ctx, AndroidWebView app) {
//Keep track of last AlertDialog showed
private AlertDialog lastHandledDialog;
@Deprecated
public CordovaChromeClient(CordovaInterface cordova) {
this.cordova = cordova;
}
public CordovaChromeClient(CordovaInterface ctx, CordovaWebView app) {
this.cordova = ctx;
this.appView = app;
}
@Deprecated
public void setWebView(CordovaWebView view) {
this.appView = view;
}
/**
* Tell the client to display a javascript alert dialog.
*
@@ -113,7 +127,7 @@ public class AndroidChromeClient extends WebChromeClient {
return true;
}
});
dlg.show();
lastHandledDialog = dlg.show();
return true;
}
@@ -162,7 +176,7 @@ public class AndroidChromeClient extends WebChromeClient {
return true;
}
});
dlg.show();
lastHandledDialog = dlg.show();
return true;
}
@@ -206,7 +220,7 @@ public class AndroidChromeClient extends WebChromeClient {
res.cancel();
}
});
dlg.show();
lastHandledDialog = dlg.show();
}
return true;
}
@@ -218,7 +232,7 @@ public class AndroidChromeClient extends WebChromeClient {
public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize,
long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater)
{
LOG.d(LOG_TAG, "onExceededDatabaseQuota estimatedSize: %d currentQuota: %d totalUsedQuota: %d", estimatedSize, currentQuota, totalUsedQuota);
LOG.d(TAG, "onExceededDatabaseQuota estimatedSize: %d currentQuota: %d totalUsedQuota: %d", estimatedSize, currentQuota, totalUsedQuota);
quotaUpdater.updateQuota(MAX_QUOTA);
}
@@ -231,7 +245,7 @@ public class AndroidChromeClient extends WebChromeClient {
//This is only for Android 2.1
if(android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.ECLAIR_MR1)
{
LOG.d(LOG_TAG, "%s: Line %d : %s", sourceID, lineNumber, message);
LOG.d(TAG, "%s: Line %d : %s", sourceID, lineNumber, message);
super.onConsoleMessage(message, lineNumber, sourceID);
}
}
@@ -241,7 +255,7 @@ public class AndroidChromeClient extends WebChromeClient {
public boolean onConsoleMessage(ConsoleMessage consoleMessage)
{
if (consoleMessage.message() != null)
LOG.d(LOG_TAG, "%s: Line %d : %s" , consoleMessage.sourceId() , consoleMessage.lineNumber(), consoleMessage.message());
LOG.d(TAG, "%s: Line %d : %s" , consoleMessage.sourceId() , consoleMessage.lineNumber(), consoleMessage.message());
return super.onConsoleMessage(consoleMessage);
}
@@ -296,22 +310,57 @@ public class AndroidChromeClient extends WebChromeClient {
}
return mVideoProgressView;
}
// <input type=file> 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<Uri> uploadMsg) {
this.openFileChooser(uploadMsg, "*/*");
}
public void openFileChooser( ValueCallback<Uri> uploadMsg, String acceptType ) {
this.openFileChooser(uploadMsg, acceptType, null);
}
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture)
public void openFileChooser(final ValueCallback<Uri> 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);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean onShowFileChooser(WebView webView, final ValueCallback<Uri[]> 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();
}
}
}

View File

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

View File

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

View File

@@ -32,6 +32,8 @@ import android.net.Uri;
* Plugins must extend this class and override one of the execute methods.
*/
public class CordovaPlugin {
@Deprecated // This is never set.
public String id;
public CordovaWebView webView;
public CordovaInterface cordova;
protected CordovaPreferences preferences;
@@ -196,4 +198,34 @@ public class CordovaPlugin {
*/
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;
}
}

View File

@@ -133,6 +133,9 @@ public class CordovaPreferences {
} 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")) {

View File

@@ -84,7 +84,7 @@ public class CordovaResourceApi {
// Creating this is light-weight.
private static OkHttpClient httpClient = new OkHttpClient();
public static Thread jsThread;
static Thread jsThread;
private final AssetManager assetManager;
private final ContentResolver contentResolver;

View File

@@ -25,14 +25,14 @@ import android.net.Uri;
import android.os.Build;
import android.webkit.WebView;
public class CordovaUriHelper {
class CordovaUriHelper {
private static final String TAG = "CordovaUriHelper";
private CordovaWebView appView;
private CordovaInterface cordova;
public CordovaUriHelper(CordovaInterface cdv, CordovaWebView webView)
CordovaUriHelper(CordovaInterface cdv, CordovaWebView webView)
{
appView = webView;
cordova = cdv;
@@ -47,9 +47,9 @@ public class CordovaUriHelper {
* @return true to override, false for default behavior
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
public boolean shouldOverrideUrlLoading(String url) {
boolean shouldOverrideUrlLoading(WebView view, String url) {
// Give plugins the chance to handle the url
if (this.appView.getPluginManager().onOverrideUrlLoading(url)) {
if (this.appView.pluginManager.onOverrideUrlLoading(url)) {
// Do nothing other than what the plugins wanted.
// If any returned true, then the request was handled.
return true;

953
framework/src/org/apache/cordova/CordovaWebView.java Normal file → Executable file
View File

@@ -1,48 +1,504 @@
/*
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.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.webkit.WebChromeClient.CustomViewCallback;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.webkit.WebBackForwardList;
import android.webkit.WebHistoryItem;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebSettings.LayoutAlgorithm;
import android.webkit.WebViewClient;
import android.webkit.CookieManager;
import android.widget.FrameLayout;
public interface CordovaWebView {
public static final String CORDOVA_VERSION = "4.0.0-dev";
/*
* This class is our web view.
*
* @see <a href="http://developer.android.com/guide/webapps/webview.html">WebView guide</a>
* @see <a href="http://developer.android.com/reference/android/webkit/WebView.html">WebView</a>
*/
public class CordovaWebView extends WebView {
void init(CordovaInterface cordova, List<PluginEntry> pluginEntries,
Whitelist internalWhitelist, Whitelist externalWhitelist,
CordovaPreferences preferences);
public static final String TAG = "CordovaWebView";
public static final String CORDOVA_VERSION = "3.7.1";
View getView();
private HashSet<Integer> boundKeyCodes = new HashSet<Integer>();
void loadUrlIntoView(String url, boolean recreatePlugins);
public PluginManager pluginManager;
private boolean paused;
void stopLoading();
private BroadcastReceiver receiver;
boolean canGoBack();
void clearCache(boolean b);
/** Activities and other important classes **/
private CordovaInterface cordova;
CordovaWebViewClient viewClient;
private CordovaChromeClient chromeClient;
void clearHistory();
// Flag to track that a loadUrl timeout occurred
int loadUrlTimeout = 0;
boolean backHistory();
private long lastMenuEventTime = 0;
void handlePause(boolean keepRunning);
CordovaBridge bridge;
void onNewIntent(Intent intent);
/** custom view created by the browser (a video player for example) */
private View mCustomView;
private WebChromeClient.CustomViewCallback mCustomViewCallback;
void handleResume(boolean keepRunning, boolean activityResultKeepRunning);
private CordovaResourceApi resourceApi;
private Whitelist internalWhitelist;
private Whitelist externalWhitelist;
void handleDestroy();
// The URL passed to loadUrl(), not necessarily the URL of the current page.
String loadedUrl;
private CordovaPreferences preferences;
private App appPlugin;
class ActivityResult {
int request;
int result;
Intent incoming;
public ActivityResult(int req, int res, Intent intent) {
request = req;
result = res;
incoming = intent;
}
}
static final FrameLayout.LayoutParams COVER_SCREEN_GRAVITY_CENTER =
new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
Gravity.CENTER);
public CordovaWebView(Context context) {
this(context, null);
}
public CordovaWebView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Deprecated
public CordovaWebView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@TargetApi(11)
@Deprecated
public CordovaWebView(Context context, AttributeSet attrs, int defStyle, boolean privateBrowsing) {
super(context, attrs, defStyle, privateBrowsing);
}
// Use two-phase init so that the control will work with XML layouts.
public void init(CordovaInterface cordova, CordovaWebViewClient webViewClient, CordovaChromeClient webChromeClient,
List<PluginEntry> pluginEntries, Whitelist internalWhitelist, Whitelist externalWhitelist,
CordovaPreferences preferences) {
if (this.cordova != null) {
throw new IllegalStateException();
}
this.cordova = cordova;
this.viewClient = webViewClient;
this.chromeClient = webChromeClient;
this.internalWhitelist = internalWhitelist;
this.externalWhitelist = externalWhitelist;
this.preferences = preferences;
super.setWebChromeClient(webChromeClient);
super.setWebViewClient(webViewClient);
pluginManager = new PluginManager(this, this.cordova, pluginEntries);
bridge = new CordovaBridge(pluginManager, new NativeToJsMessageQueue(this, cordova), this.cordova.getActivity().getPackageName());
resourceApi = new CordovaResourceApi(this.getContext(), pluginManager);
pluginManager.addService(App.PLUGIN_NAME, "org.apache.cordova.App");
// This will be removed in 4.0.x in favour of the plugin not being bundled.
pluginManager.addService(new PluginEntry("SplashScreenInternal", "org.apache.cordova.SplashScreenInternal", true));
pluginManager.init();
initWebViewSettings();
exposeJsInterface();
}
@SuppressWarnings("deprecation")
private void initIfNecessary() {
if (pluginManager == null) {
Log.w(TAG, "CordovaWebView.init() was not called. This will soon be required.");
// Before the refactor to a two-phase init, the Context needed to implement CordovaInterface.
CordovaInterface cdv = (CordovaInterface)getContext();
if (!Config.isInitialized()) {
Config.init(cdv.getActivity());
}
init(cdv, makeWebViewClient(cdv), makeWebChromeClient(cdv), Config.getPluginEntries(), Config.getWhitelist(), Config.getExternalWhitelist(), Config.getPreferences());
}
}
@SuppressLint("SetJavaScriptEnabled")
@SuppressWarnings("deprecation")
private void initWebViewSettings() {
this.setInitialScale(0);
this.setVerticalScrollBarEnabled(false);
// TODO: The Activity is the one that should call requestFocus().
if (shouldRequestFocusOnInit()) {
this.requestFocusFromTouch();
}
// Enable JavaScript
WebSettings settings = this.getSettings();
settings.setJavaScriptEnabled(true);
settings.setJavaScriptCanOpenWindowsAutomatically(true);
settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL);
// Enable third-party cookies if on Lolipop. TODO: Make this configurable
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
{
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setAcceptThirdPartyCookies(this, true);
}
// Set the nav dump for HTC 2.x devices (disabling for ICS, deprecated entirely for Jellybean 4.2)
try {
Method gingerbread_getMethod = WebSettings.class.getMethod("setNavDump", new Class[] { boolean.class });
String manufacturer = android.os.Build.MANUFACTURER;
Log.d(TAG, "CordovaWebView is running on device made by: " + manufacturer);
if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB &&
android.os.Build.MANUFACTURER.contains("HTC"))
{
gingerbread_getMethod.invoke(settings, true);
}
} catch (NoSuchMethodException e) {
Log.d(TAG, "We are on a modern version of Android, we will deprecate HTC 2.3 devices in 2.8");
} catch (IllegalArgumentException e) {
Log.d(TAG, "Doing the NavDump failed with bad arguments");
} catch (IllegalAccessException e) {
Log.d(TAG, "This should never happen: IllegalAccessException means this isn't Android anymore");
} catch (InvocationTargetException e) {
Log.d(TAG, "This should never happen: InvocationTargetException means this isn't Android anymore.");
}
//We don't save any form data in the application
settings.setSaveFormData(false);
settings.setSavePassword(false);
// Jellybean rightfully tried to lock this down. Too bad they didn't give us a whitelist
// while we do this
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
Level16Apis.enableUniversalAccess(settings);
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
Level17Apis.setMediaPlaybackRequiresUserGesture(settings, false);
}
// Enable database
// We keep this disabled because we use or shim to get around DOM_EXCEPTION_ERROR_16
String databasePath = getContext().getApplicationContext().getDir("database", Context.MODE_PRIVATE).getPath();
settings.setDatabaseEnabled(true);
settings.setDatabasePath(databasePath);
//Determine whether we're in debug or release mode, and turn on Debugging!
ApplicationInfo appInfo = getContext().getApplicationContext().getApplicationInfo();
if ((appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0 &&
android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
enableRemoteDebugging();
}
settings.setGeolocationDatabasePath(databasePath);
// Enable DOM storage
settings.setDomStorageEnabled(true);
// Enable built-in geolocation
settings.setGeolocationEnabled(true);
// Enable AppCache
// Fix for CB-2282
settings.setAppCacheMaxSize(5 * 1048576);
settings.setAppCachePath(databasePath);
settings.setAppCacheEnabled(true);
// Fix for CB-1405
// Google issue 4641
settings.getUserAgentString();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
if (this.receiver == null) {
this.receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
getSettings().getUserAgentString();
}
};
getContext().registerReceiver(this.receiver, intentFilter);
}
// end CB-1405
}
@TargetApi(Build.VERSION_CODES.KITKAT)
private void enableRemoteDebugging() {
try {
WebView.setWebContentsDebuggingEnabled(true);
} catch (IllegalArgumentException e) {
Log.d(TAG, "You have one job! To turn on Remote Web Debugging! YOU HAVE FAILED! ");
e.printStackTrace();
}
}
public CordovaChromeClient makeWebChromeClient(CordovaInterface cordova) {
return new CordovaChromeClient(cordova, this);
}
public CordovaWebViewClient makeWebViewClient(CordovaInterface cordova) {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
return new CordovaWebViewClient(cordova, this);
}
return new IceCreamCordovaWebViewClient(cordova, this);
}
/**
* Override this method to decide whether or not you need to request the
* focus when your application start
*
* @return true unless this method is overriden to return a different value
*/
protected boolean shouldRequestFocusOnInit() {
return true;
}
private void exposeJsInterface() {
if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)) {
Log.i(TAG, "Disabled addJavascriptInterface() bridge since Android version is old.");
// Bug being that Java Strings do not get converted to JS strings automatically.
// This isn't hard to work-around on the JS side, but it's easier to just
// use the prompt bridge instead.
return;
}
this.addJavascriptInterface(new ExposedJsApi(bridge), "_cordovaNative");
}
@Override
public void setWebViewClient(WebViewClient client) {
this.viewClient = (CordovaWebViewClient)client;
super.setWebViewClient(client);
}
@Override
public void setWebChromeClient(WebChromeClient client) {
this.chromeClient = (CordovaChromeClient)client;
super.setWebChromeClient(client);
}
public CordovaChromeClient getWebChromeClient() {
return this.chromeClient;
}
public Whitelist getWhitelist() {
return this.internalWhitelist;
}
public Whitelist getExternalWhitelist() {
return this.externalWhitelist;
}
/**
* Send JavaScript statement back to JavaScript.
* (This is a convenience method)
* Load the url into the webview.
*
* @param statement
* @param url
*/
@Override
public void loadUrl(String url) {
if (url.equals("about:blank") || url.startsWith("javascript:")) {
this.loadUrlNow(url);
}
else {
this.loadUrlIntoView(url);
}
}
/**
* Load the url into the webview after waiting for period of time.
* This is used to display the splashscreen for certain amount of time.
*
* @param url
* @param time The number of ms to wait before loading webview
*/
@Deprecated
public void loadUrl(final String url, int time) {
if(url == null)
{
this.loadUrlIntoView(Config.getStartUrl());
}
else
{
this.loadUrlIntoView(url);
}
}
public void loadUrlIntoView(final String url) {
loadUrlIntoView(url, true);
}
/**
* Load the url into the webview.
*
* @param url
*/
public void loadUrlIntoView(final String url, boolean recreatePlugins) {
LOG.d(TAG, ">>> loadUrl(" + url + ")");
initIfNecessary();
if (recreatePlugins) {
// Don't re-initialize on first load.
if (loadedUrl != null) {
this.pluginManager.init();
}
this.loadedUrl = url;
}
// Create a timeout timer for loadUrl
final CordovaWebView me = this;
final int currentLoadUrlTimeout = me.loadUrlTimeout;
final int loadUrlTimeoutValue = Integer.parseInt(this.getProperty("LoadUrlTimeoutValue", "20000"));
// Timeout error method
final Runnable loadError = new Runnable() {
public void run() {
me.stopLoading();
LOG.e(TAG, "CordovaWebView: TIMEOUT ERROR!");
if (viewClient != null) {
viewClient.onReceivedError(me, -6, "The connection to the server was unsuccessful.", url);
}
}
};
// Timeout timer method
final Runnable timeoutCheck = new Runnable() {
public void run() {
try {
synchronized (this) {
wait(loadUrlTimeoutValue);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// If timeout, then stop loading and handle error
if (me.loadUrlTimeout == currentLoadUrlTimeout) {
me.cordova.getActivity().runOnUiThread(loadError);
}
}
};
// Load url
this.cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
cordova.getThreadPool().execute(timeoutCheck);
me.loadUrlNow(url);
}
});
}
/**
* Load URL in webview.
*
* @param url
*/
void loadUrlNow(String url) {
if (LOG.isLoggable(LOG.DEBUG) && !url.startsWith("javascript:")) {
LOG.d(TAG, ">>> loadUrlNow()");
}
if (url.startsWith("file://") || url.startsWith("javascript:") || url.startsWith("about:") || internalWhitelist.isUrlWhiteListed(url)) {
super.loadUrl(url);
}
}
/**
* Load the url into the webview after waiting for period of time.
* This is used to display the splashscreen for certain amount of time.
*
* @param url
* @param time The number of ms to wait before loading webview
*/
public void loadUrlIntoView(final String url, final int time) {
// If not first page of app, then load immediately
// Add support for browser history if we use it.
if ((url.startsWith("javascript:")) || this.canGoBack()) {
}
// If first page, then show splashscreen
else {
LOG.d(TAG, "loadUrlIntoView(%s, %d)", url, time);
}
// Load url
this.loadUrlIntoView(url);
}
@Override
public void stopLoading() {
viewClient.isCurrentlyLoading = false;
super.stopLoading();
}
public void onScrollChanged(int l, int t, int oldl, int oldt)
{
super.onScrollChanged(l, t, oldl, oldt);
//We should post a message that the scroll changed
ScrollEvent myEvent = new ScrollEvent(l, t, oldl, oldt, this);
this.postMessage("onScrollChanged", myEvent);
}
/**
* Send JavaScript statement back to JavaScript.
* Deprecated (https://issues.apache.org/jira/browse/CB-6851)
* Instead of executing snippets of JS, you should use the exec bridge
* to create a Java->JS communication channel.
@@ -62,37 +518,454 @@ public interface CordovaWebView {
* savedCallbackContext.sendPluginResult(dataResult);
*/
@Deprecated
void sendJavascript(String statememt);
public void sendJavascript(String statement) {
this.bridge.getMessageQueue().addJavaScript(statement);
}
void showWebPage(String errorUrl, boolean b, boolean c, HashMap<String, Object> params);
/**
* Send a plugin result back to JavaScript.
* (This is a convenience method)
*
* @param result
* @param callbackId
*/
public void sendPluginResult(PluginResult result, String callbackId) {
this.bridge.getMessageQueue().addPluginResult(result, callbackId);
}
boolean isCustomViewShowing();
/**
* Send a message to all plugins.
*
* @param id The message id
* @param data The message data
*/
public void postMessage(String id, Object data) {
if (this.pluginManager != null) {
this.pluginManager.postMessage(id, data);
}
}
void showCustomView(View view, CustomViewCallback callback);
void hideCustomView();
/**
* Go to previous page in history. (We manage our own history)
*
* @return true if we went back, false if we are already at top
*/
public boolean backHistory() {
// Check webview first to see if there is a history
// This is needed to support curPage#diffLink, since they are added to appView's history, but not our history url array (JQMobile behavior)
if (super.canGoBack()) {
super.goBack();
return true;
}
return false;
}
CordovaResourceApi getResourceApi();
void setButtonPlumbedToJs(int keyCode, boolean override);
boolean isButtonPlumbedToJs(int keyCode);
/**
* Load the specified URL in the Cordova webview or a new browser instance.
*
* NOTE: If openExternal is false, only URLs listed in whitelist can be loaded.
*
* @param url The url to load.
* @param openExternal Load url in browser instead of Cordova webview.
* @param clearHistory Clear the history stack, so new page becomes top of history
* @param params Parameters for new app
*/
public void showWebPage(String url, boolean openExternal, boolean clearHistory, HashMap<String, Object> params) {
LOG.d(TAG, "showWebPage(%s, %b, %b, HashMap", url, openExternal, clearHistory);
void sendPluginResult(PluginResult cr, String callbackId);
// If clearing history
if (clearHistory) {
this.clearHistory();
}
PluginManager getPluginManager();
// If loading into our webview
if (!openExternal) {
Whitelist getWhitelist();
Whitelist getExternalWhitelist();
CordovaPreferences getPreferences();
// Make sure url is in whitelist
if (url.startsWith("file://") || internalWhitelist.isUrlWhiteListed(url)) {
// TODO: What about params?
// Load new URL
this.loadUrl(url);
return;
}
// Load in default viewer if not
LOG.w(TAG, "showWebPage: Cannot load URL into webview since it is not in white list. Loading into browser instead. (URL=" + url + ")");
}
try {
// Omitting the MIME type for file: URLs causes "No Activity found to handle Intent".
// Adding the MIME type to http: URLs causes them to not be handled by the downloader.
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uri = Uri.parse(url);
if ("file".equals(uri.getScheme())) {
intent.setDataAndType(uri, resourceApi.getMimeType(uri));
} else {
intent.setData(uri);
}
cordova.getActivity().startActivity(intent);
} catch (android.content.ActivityNotFoundException e) {
LOG.e(TAG, "Error loading url " + url, e);
}
}
/**
* 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)
{
if(boundKeyCodes.contains(keyCode))
{
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
sendJavascriptEvent("volumedownbutton");
return true;
}
// If volumeup key
else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
sendJavascriptEvent("volumeupbutton");
return true;
}
else
{
return super.onKeyDown(keyCode, event);
}
}
else if(keyCode == KeyEvent.KEYCODE_BACK)
{
return !(this.startOfHistory()) || isButtonPlumbedToJs(KeyEvent.KEYCODE_BACK);
}
else if(keyCode == KeyEvent.KEYCODE_MENU)
{
//How did we get here? Is there a childView?
View childView = this.getFocusedChild();
if(childView != null)
{
//Make sure we close the keyboard if it's present
InputMethodManager imm = (InputMethodManager) cordova.getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(childView.getWindowToken(), 0);
cordova.getActivity().openOptionsMenu();
return true;
} else {
return super.onKeyDown(keyCode, event);
}
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event)
{
// If back key
if (keyCode == KeyEvent.KEYCODE_BACK) {
// A custom view is currently displayed (e.g. playing a video)
if(mCustomView != null) {
this.hideCustomView();
return true;
} else {
// The webview is currently displayed
// If back key is bound, then send event to JavaScript
if (isButtonPlumbedToJs(KeyEvent.KEYCODE_BACK)) {
sendJavascriptEvent("backbutton");
return true;
} else {
// If not bound
// Go to previous page in webview if it is possible to go back
if (this.backHistory()) {
return true;
}
// If not, then invoke default behavior
}
}
}
// Legacy
else if (keyCode == KeyEvent.KEYCODE_MENU) {
if (this.lastMenuEventTime < event.getEventTime()) {
sendJavascriptEvent("menubutton");
}
this.lastMenuEventTime = event.getEventTime();
return super.onKeyUp(keyCode, event);
}
// If search key
else if (keyCode == KeyEvent.KEYCODE_SEARCH) {
sendJavascriptEvent("searchbutton");
return true;
}
//Does webkit change this behavior?
return super.onKeyUp(keyCode, event);
}
private void sendJavascriptEvent(String event) {
if (appPlugin == null) {
appPlugin = (App)this.pluginManager.getPlugin(App.PLUGIN_NAME);
}
if (appPlugin == null) {
LOG.w(TAG, "Unable to fire event without existing plugin");
return;
}
appPlugin.fireJavascriptEvent(event);
}
public void setButtonPlumbedToJs(int keyCode, boolean override) {
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_DOWN:
case KeyEvent.KEYCODE_VOLUME_UP:
case KeyEvent.KEYCODE_BACK:
// TODO: Why are search and menu buttons handled separately?
if (override) {
boundKeyCodes.add(keyCode);
} else {
boundKeyCodes.remove(keyCode);
}
return;
default:
throw new IllegalArgumentException("Unsupported keycode: " + keyCode);
}
}
@Deprecated // Use setButtonPlumbedToJs() instead.
public void bindButton(boolean override)
{
setButtonPlumbedToJs(KeyEvent.KEYCODE_BACK, override);
}
@Deprecated // Use setButtonPlumbedToJs() instead.
public void bindButton(String button, boolean override) {
if (button.compareTo("volumeup")==0) {
setButtonPlumbedToJs(KeyEvent.KEYCODE_VOLUME_UP, override);
}
else if (button.compareTo("volumedown")==0) {
setButtonPlumbedToJs(KeyEvent.KEYCODE_VOLUME_DOWN, override);
}
}
@Deprecated // Use setButtonPlumbedToJs() instead.
public void bindButton(int keyCode, boolean keyDown, boolean override) {
setButtonPlumbedToJs(keyCode, override);
}
@Deprecated // Use isButtonPlumbedToJs
public boolean isBackButtonBound()
{
return isButtonPlumbedToJs(KeyEvent.KEYCODE_BACK);
}
public boolean isButtonPlumbedToJs(int keyCode)
{
return boundKeyCodes.contains(keyCode);
}
public void handlePause(boolean keepRunning)
{
LOG.d(TAG, "Handle the pause");
// Send pause event to JavaScript
sendJavascriptEvent("pause");
// Forward to plugins
if (this.pluginManager != null) {
this.pluginManager.onPause(keepRunning);
}
// If app doesn't want to run in background
if (!keepRunning) {
// Pause JavaScript timers (including setInterval)
this.pauseTimers();
}
paused = true;
}
void onFilePickerResult(Uri uri);
public void handleResume(boolean keepRunning, boolean activityResultKeepRunning)
{
sendJavascriptEvent("resume");
void setNetworkAvailable(boolean online);
// Forward to plugins
if (this.pluginManager != null) {
this.pluginManager.onResume(keepRunning);
}
// Resume JavaScript timers (including setInterval)
this.resumeTimers();
paused = false;
}
String getUrl();
public void handleDestroy()
{
// Cancel pending timeout timer.
loadUrlTimeout++;
// TODO: Work on deleting these by removing refs from plugins.
Context getContext();
void loadUrl(String url);
Object postMessage(String id, Object data);
// Load blank page so that JavaScript onunload is called
this.loadUrl("about:blank");
//Remove last AlertDialog
this.chromeClient.destroyLastDialog();
// Forward to plugins
if (this.pluginManager != null) {
this.pluginManager.onDestroy();
}
// unregister the receiver
if (this.receiver != null) {
try {
getContext().unregisterReceiver(this.receiver);
} catch (Exception e) {
Log.e(TAG, "Error unregistering configuration receiver: " + e.getMessage(), e);
}
}
}
public void onNewIntent(Intent intent)
{
//Forward to plugins
if (this.pluginManager != null) {
this.pluginManager.onNewIntent(intent);
}
}
public boolean isPaused()
{
return paused;
}
@Deprecated // This never did anything.
public boolean hadKeyEvent() {
return false;
}
// Wrapping these functions in their own class prevents warnings in adb like:
// VFY: unable to resolve virtual method 285: Landroid/webkit/WebSettings;.setAllowUniversalAccessFromFileURLs
@TargetApi(16)
private static final class Level16Apis {
static void enableUniversalAccess(WebSettings settings) {
settings.setAllowUniversalAccessFromFileURLs(true);
}
}
@TargetApi(17)
private static final class Level17Apis {
static void setMediaPlaybackRequiresUserGesture(WebSettings settings, boolean value) {
settings.setMediaPlaybackRequiresUserGesture(value);
}
}
public void printBackForwardList() {
WebBackForwardList currentList = this.copyBackForwardList();
int currentSize = currentList.getSize();
for(int i = 0; i < currentSize; ++i)
{
WebHistoryItem item = currentList.getItemAtIndex(i);
String url = item.getUrl();
LOG.d(TAG, "The URL at index: " + Integer.toString(i) + " is " + url );
}
}
//Can Go Back is BROKEN!
public boolean startOfHistory()
{
WebBackForwardList currentList = this.copyBackForwardList();
WebHistoryItem item = currentList.getItemAtIndex(0);
if( item!=null){ // Null-fence in case they haven't called loadUrl yet (CB-2458)
String url = item.getUrl();
String currentUrl = this.getUrl();
LOG.d(TAG, "The current URL is: " + currentUrl);
LOG.d(TAG, "The URL at item 0 is: " + url);
return currentUrl.equals(url);
}
return false;
}
public void showCustomView(View view, WebChromeClient.CustomViewCallback callback) {
// This code is adapted from the original Android Browser code, licensed under the Apache License, Version 2.0
Log.d(TAG, "showing Custom View");
// if a view already exists then immediately terminate the new one
if (mCustomView != null) {
callback.onCustomViewHidden();
return;
}
// Store the view and its callback for later (to kill it properly)
mCustomView = view;
mCustomViewCallback = callback;
// Add the custom view to its container.
ViewGroup parent = (ViewGroup) this.getParent();
parent.addView(view, COVER_SCREEN_GRAVITY_CENTER);
// Hide the content view.
this.setVisibility(View.GONE);
// Finally show the custom view container.
parent.setVisibility(View.VISIBLE);
parent.bringToFront();
}
public void hideCustomView() {
// This code is adapted from the original Android Browser code, licensed under the Apache License, Version 2.0
Log.d(TAG, "Hiding Custom View");
if (mCustomView == null) return;
// Hide the custom view.
mCustomView.setVisibility(View.GONE);
// Remove the custom view from its container.
ViewGroup parent = (ViewGroup) this.getParent();
parent.removeView(mCustomView);
mCustomView = null;
mCustomViewCallback.onCustomViewHidden();
// Show the content view.
this.setVisibility(View.VISIBLE);
}
/**
* if the video overlay is showing then we need to know
* as it effects back button handling
*
* @return true if custom view is showing
*/
public boolean isCustomViewShowing() {
return mCustomView != null;
}
public WebBackForwardList restoreState(Bundle savedInstanceState)
{
WebBackForwardList myList = super.restoreState(savedInstanceState);
Log.d(TAG, "WebView restoration crew now restoring!");
//Initialize the plugin manager once more
this.pluginManager.init();
return myList;
}
@Deprecated // This never did anything
public void storeResult(int requestCode, int resultCode, Intent intent) {
}
public CordovaResourceApi getResourceApi() {
return resourceApi;
}
public CordovaPreferences getPreferences() {
return preferences;
}
}

View File

@@ -20,6 +20,8 @@ package org.apache.cordova;
import java.util.Hashtable;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.LOG;
import org.json.JSONException;
import org.json.JSONObject;
@@ -30,12 +32,12 @@ import android.content.pm.PackageManager.NameNotFoundException;
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;
import android.webkit.WebViewClient;
/**
* This class is the WebViewClient that implements callbacks for our web view.
* The kind of callbacks that happen here are regarding the rendering of the
@@ -48,20 +50,42 @@ import android.webkit.WebViewClient;
* @see CordovaChromeClient
* @see CordovaWebView
*/
public class AndroidWebViewClient extends WebViewClient {
public class CordovaWebViewClient extends WebViewClient {
private static final String TAG = "AndroidWebViewClient";
protected final CordovaInterface cordova;
protected final AndroidWebView appView;
protected final CordovaUriHelper helper;
private static final String TAG = "CordovaWebViewClient";
CordovaInterface cordova;
CordovaWebView appView;
CordovaUriHelper helper;
private boolean doClearHistory = false;
boolean isCurrentlyLoading;
/** The authorization tokens. */
private Hashtable<String, AuthenticationToken> authenticationTokens = new Hashtable<String, AuthenticationToken>();
public AndroidWebViewClient(CordovaInterface cordova, AndroidWebView view) {
@Deprecated
public CordovaWebViewClient(CordovaInterface cordova) {
this.cordova = cordova;
}
/**
* Constructor.
*
* @param cordova
* @param view
*/
public CordovaWebViewClient(CordovaInterface cordova, CordovaWebView view) {
this.cordova = cordova;
this.appView = view;
helper = new CordovaUriHelper(cordova, view);
}
/**
* Constructor.
*
* @param view
*/
@Deprecated
public void setWebView(CordovaWebView view) {
this.appView = view;
helper = new CordovaUriHelper(cordova, view);
}
@@ -76,25 +100,60 @@ public class AndroidWebViewClient extends WebViewClient {
*/
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return helper.shouldOverrideUrlLoading(url);
return helper.shouldOverrideUrlLoading(view, url);
}
/**
* 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
* @param handler
* @param host
* @param realm
*/
@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;
}
else {
// Handle 401 like we'd normally do!
super.onReceivedHttpAuthRequest(view, handler, host, realm);
// 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;
}
// 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);
}
/**
@@ -111,11 +170,16 @@ public class AndroidWebViewClient extends WebViewClient {
super.onPageStarted(view, url, favicon);
isCurrentlyLoading = true;
LOG.d(TAG, "onPageStarted(" + url + ")");
// Flush stale messages & reset plugins.
this.appView.onPageReset();
// Flush stale messages.
this.appView.bridge.reset(url);
// Broadcast message that page has loaded
this.appView.getPluginManager().postMessage("onPageStarted", url);
this.appView.postMessage("onPageStarted", url);
// Notify all plugins of the navigation, so they can clean up if necessary.
if (this.appView.pluginManager != null) {
this.appView.pluginManager.onReset();
}
}
/**
@@ -129,8 +193,8 @@ public class AndroidWebViewClient extends WebViewClient {
@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;
@@ -148,10 +212,10 @@ public class AndroidWebViewClient extends WebViewClient {
}
// Clear timeout flag
appView.loadUrlTimeout++;
this.appView.loadUrlTimeout++;
// Broadcast message that page has loaded
this.appView.getPluginManager().postMessage("onPageFinished", url);
this.appView.postMessage("onPageFinished", url);
// Make app visible after 2 sec in case there was a JS error and Cordova JS never initialized correctly
if (this.appView.getVisibility() == View.INVISIBLE) {
@@ -161,7 +225,7 @@ public class AndroidWebViewClient extends WebViewClient {
Thread.sleep(2000);
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
appView.getPluginManager().postMessage("spinner", "stop");
appView.postMessage("spinner", "stop");
}
});
} catch (InterruptedException e) {
@@ -173,7 +237,7 @@ public class AndroidWebViewClient extends WebViewClient {
// Shutdown if blank loaded
if (url.equals("about:blank")) {
appView.getPluginManager().postMessage("exit", null);
appView.postMessage("exit", null);
}
}
@@ -195,7 +259,7 @@ public class AndroidWebViewClient extends WebViewClient {
LOG.d(TAG, "CordovaWebViewClient.onReceivedError: Error code=%s Description=%s URL=%s", errorCode, description, failingUrl);
// Clear timeout flag
appView.loadUrlTimeout++;
this.appView.loadUrlTimeout++;
// If this is a "Protocol Not Supported" error, then revert to the previous
// page. If there was no previous page, then punt. The application's config
@@ -218,7 +282,7 @@ public class AndroidWebViewClient extends WebViewClient {
} catch (JSONException e) {
e.printStackTrace();
}
this.appView.getPluginManager().postMessage("onReceivedError", data);
this.appView.postMessage("onReceivedError", data);
}
/**
@@ -327,4 +391,5 @@ public class AndroidWebViewClient extends WebViewClient {
public void clearAuthenticationTokens() {
this.authenticationTokens.clear();
}
}

View File

@@ -0,0 +1,162 @@
/*
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.io.File;
import android.content.Context;
import android.os.Environment;
import android.os.StatFs;
/**
* This class provides file directory utilities.
* All file operations are performed on the SD card.
*
* It is used by the FileUtils class.
*/
@Deprecated // Deprecated in 3.1. To be removed in 4.0.
public class DirectoryManager {
@SuppressWarnings("unused")
private static final String LOG_TAG = "DirectoryManager";
/**
* Determine if a file or directory exists.
* @param name The name of the file to check.
* @return T=exists, F=not found
*/
public static boolean testFileExists(String name) {
boolean status;
// If SD card exists
if ((testSaveLocationExists()) && (!name.equals(""))) {
File path = Environment.getExternalStorageDirectory();
File newPath = constructFilePaths(path.toString(), name);
status = newPath.exists();
}
// If no SD card
else {
status = false;
}
return status;
}
/**
* Get the free disk space
*
* @return Size in KB or -1 if not available
*/
public static long getFreeDiskSpace(boolean checkInternal) {
String status = Environment.getExternalStorageState();
long freeSpace = 0;
// If SD card exists
if (status.equals(Environment.MEDIA_MOUNTED)) {
freeSpace = freeSpaceCalculation(Environment.getExternalStorageDirectory().getPath());
}
else if (checkInternal) {
freeSpace = freeSpaceCalculation("/");
}
// If no SD card and we haven't been asked to check the internal directory then return -1
else {
return -1;
}
return freeSpace;
}
/**
* Given a path return the number of free KB
*
* @param path to the file system
* @return free space in KB
*/
private static long freeSpaceCalculation(String path) {
StatFs stat = new StatFs(path);
long blockSize = stat.getBlockSize();
long availableBlocks = stat.getAvailableBlocks();
return availableBlocks * blockSize / 1024;
}
/**
* Determine if SD card exists.
*
* @return T=exists, F=not found
*/
public static boolean testSaveLocationExists() {
String sDCardStatus = Environment.getExternalStorageState();
boolean status;
// If SD card is mounted
if (sDCardStatus.equals(Environment.MEDIA_MOUNTED)) {
status = true;
}
// If no SD card
else {
status = false;
}
return status;
}
/**
* Create a new file object from two file paths.
*
* @param file1 Base file path
* @param file2 Remaining file path
* @return File object
*/
private static File constructFilePaths (String file1, String file2) {
File newPath;
if (file2.startsWith(file1)) {
newPath = new File(file2);
}
else {
newPath = new File(file1 + "/" + file2);
}
return newPath;
}
/**
* Determine if we can use the SD Card to store the temporary file. If not then use
* the internal cache directory.
*
* @return the absolute path of where to store the file
*/
public static String getTempDirectoryPath(Context ctx) {
File cache = null;
// SD Card Mounted
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
cache = new File(Environment.getExternalStorageDirectory().getAbsolutePath() +
"/Android/data/" + ctx.getPackageName() + "/cache/");
}
// Use internal storage
else {
cache = ctx.getCacheDir();
}
// Create the cache directory if it doesn't exist
if (!cache.exists()) {
cache.mkdirs();
}
return cache.getAbsolutePath();
}
}

View File

@@ -0,0 +1,34 @@
/*
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;
/**
* This used to be the class that should be extended by application
* developers, but everything has been moved to CordovaActivity. So
* you should extend CordovaActivity instead of DroidGap. This class
* will be removed at a future time.
*
* @see CordovaActivity
* @deprecated
*/
@Deprecated
public class DroidGap extends CordovaActivity {
}

View File

@@ -0,0 +1,186 @@
/*
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.io.IOException;
import android.media.ExifInterface;
@Deprecated // Deprecated in 3.1. To be removed in 4.0.
public class ExifHelper {
private String aperture = null;
private String datetime = null;
private String exposureTime = null;
private String flash = null;
private String focalLength = null;
private String gpsAltitude = null;
private String gpsAltitudeRef = null;
private String gpsDateStamp = null;
private String gpsLatitude = null;
private String gpsLatitudeRef = null;
private String gpsLongitude = null;
private String gpsLongitudeRef = null;
private String gpsProcessingMethod = null;
private String gpsTimestamp = null;
private String iso = null;
private String make = null;
private String model = null;
private String orientation = null;
private String whiteBalance = null;
private ExifInterface inFile = null;
private ExifInterface outFile = null;
/**
* The file before it is compressed
*
* @param filePath
* @throws IOException
*/
public void createInFile(String filePath) throws IOException {
this.inFile = new ExifInterface(filePath);
}
/**
* The file after it has been compressed
*
* @param filePath
* @throws IOException
*/
public void createOutFile(String filePath) throws IOException {
this.outFile = new ExifInterface(filePath);
}
/**
* Reads all the EXIF data from the input file.
*/
public void readExifData() {
this.aperture = inFile.getAttribute(ExifInterface.TAG_APERTURE);
this.datetime = inFile.getAttribute(ExifInterface.TAG_DATETIME);
this.exposureTime = inFile.getAttribute(ExifInterface.TAG_EXPOSURE_TIME);
this.flash = inFile.getAttribute(ExifInterface.TAG_FLASH);
this.focalLength = inFile.getAttribute(ExifInterface.TAG_FOCAL_LENGTH);
this.gpsAltitude = inFile.getAttribute(ExifInterface.TAG_GPS_ALTITUDE);
this.gpsAltitudeRef = inFile.getAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF);
this.gpsDateStamp = inFile.getAttribute(ExifInterface.TAG_GPS_DATESTAMP);
this.gpsLatitude = inFile.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
this.gpsLatitudeRef = inFile.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
this.gpsLongitude = inFile.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
this.gpsLongitudeRef = inFile.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
this.gpsProcessingMethod = inFile.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD);
this.gpsTimestamp = inFile.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP);
this.iso = inFile.getAttribute(ExifInterface.TAG_ISO);
this.make = inFile.getAttribute(ExifInterface.TAG_MAKE);
this.model = inFile.getAttribute(ExifInterface.TAG_MODEL);
this.orientation = inFile.getAttribute(ExifInterface.TAG_ORIENTATION);
this.whiteBalance = inFile.getAttribute(ExifInterface.TAG_WHITE_BALANCE);
}
/**
* Writes the previously stored EXIF data to the output file.
*
* @throws IOException
*/
public void writeExifData() throws IOException {
// Don't try to write to a null file
if (this.outFile == null) {
return;
}
if (this.aperture != null) {
this.outFile.setAttribute(ExifInterface.TAG_APERTURE, this.aperture);
}
if (this.datetime != null) {
this.outFile.setAttribute(ExifInterface.TAG_DATETIME, this.datetime);
}
if (this.exposureTime != null) {
this.outFile.setAttribute(ExifInterface.TAG_EXPOSURE_TIME, this.exposureTime);
}
if (this.flash != null) {
this.outFile.setAttribute(ExifInterface.TAG_FLASH, this.flash);
}
if (this.focalLength != null) {
this.outFile.setAttribute(ExifInterface.TAG_FOCAL_LENGTH, this.focalLength);
}
if (this.gpsAltitude != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, this.gpsAltitude);
}
if (this.gpsAltitudeRef != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, this.gpsAltitudeRef);
}
if (this.gpsDateStamp != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, this.gpsDateStamp);
}
if (this.gpsLatitude != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_LATITUDE, this.gpsLatitude);
}
if (this.gpsLatitudeRef != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, this.gpsLatitudeRef);
}
if (this.gpsLongitude != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, this.gpsLongitude);
}
if (this.gpsLongitudeRef != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, this.gpsLongitudeRef);
}
if (this.gpsProcessingMethod != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, this.gpsProcessingMethod);
}
if (this.gpsTimestamp != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, this.gpsTimestamp);
}
if (this.iso != null) {
this.outFile.setAttribute(ExifInterface.TAG_ISO, this.iso);
}
if (this.make != null) {
this.outFile.setAttribute(ExifInterface.TAG_MAKE, this.make);
}
if (this.model != null) {
this.outFile.setAttribute(ExifInterface.TAG_MODEL, this.model);
}
if (this.orientation != null) {
this.outFile.setAttribute(ExifInterface.TAG_ORIENTATION, this.orientation);
}
if (this.whiteBalance != null) {
this.outFile.setAttribute(ExifInterface.TAG_WHITE_BALANCE, this.whiteBalance);
}
this.outFile.saveAttributes();
}
public int getOrientation() {
int o = Integer.parseInt(this.orientation);
if (o == ExifInterface.ORIENTATION_NORMAL) {
return 0;
} else if (o == ExifInterface.ORIENTATION_ROTATE_90) {
return 90;
} else if (o == ExifInterface.ORIENTATION_ROTATE_180) {
return 180;
} else if (o == ExifInterface.ORIENTATION_ROTATE_270) {
return 270;
} else {
return 0;
}
}
public void resetOrientation() {
this.orientation = "" + ExifInterface.ORIENTATION_NORMAL;
}
}

52
framework/src/org/apache/cordova/ExposedJsApi.java Normal file → Executable file
View File

@@ -1,12 +1,52 @@
/*
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.JavascriptInterface;
import org.json.JSONException;
/*
* Any exposed Javascript API MUST implement these three things!
/**
* Contains APIs that the JS can call. All functions in here should also have
* an equivalent entry in CordovaChromeClient.java, and be added to
* cordova-js/lib/android/plugin/android/promptbasednativeapi.js
*/
public interface ExposedJsApi {
public String exec(int bridgeSecret, String service, String action, String callbackId, String arguments) throws JSONException, IllegalAccessException;
public void setNativeToJsBridgeMode(int bridgeSecret, int value) throws IllegalAccessException;
public String retrieveJsMessages(int bridgeSecret, boolean fromOnlineEvent) throws IllegalAccessException;
/* package */ class ExposedJsApi {
private CordovaBridge bridge;
public ExposedJsApi(CordovaBridge bridge) {
this.bridge = bridge;
}
@JavascriptInterface
public String exec(int bridgeSecret, String service, String action, String callbackId, String arguments) throws JSONException, IllegalAccessException {
return bridge.jsExec(bridgeSecret, service, action, callbackId, arguments);
}
@JavascriptInterface
public void setNativeToJsBridgeMode(int bridgeSecret, int value) throws IllegalAccessException {
bridge.jsSetNativeToJsBridgeMode(bridgeSecret, value);
}
@JavascriptInterface
public String retrieveJsMessages(int bridgeSecret, boolean fromOnlineEvent) throws IllegalAccessException {
return bridge.jsRetrieveJsMessages(bridgeSecret, fromOnlineEvent);
}
}

View File

@@ -0,0 +1,163 @@
/*
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.database.Cursor;
import android.net.Uri;
import android.webkit.MimeTypeMap;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.LOG;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.Locale;
@Deprecated // Deprecated in 3.1. To be removed in 4.0.
public class FileHelper {
private static final String LOG_TAG = "FileUtils";
private static final String _DATA = "_data";
/**
* Returns the real path of the given URI string.
* If the given URI string represents a content:// URI, the real path is retrieved from the media store.
*
* @param uriString the URI string of the audio/image/video
* @param cordova the current application context
* @return the full path to the file
*/
@SuppressWarnings("deprecation")
public static String getRealPath(String uriString, CordovaInterface cordova) {
String realPath = null;
if (uriString.startsWith("content://")) {
String[] proj = { _DATA };
Cursor cursor = cordova.getActivity().managedQuery(Uri.parse(uriString), proj, null, null, null);
int column_index = cursor.getColumnIndexOrThrow(_DATA);
cursor.moveToFirst();
realPath = cursor.getString(column_index);
if (realPath == null) {
LOG.e(LOG_TAG, "Could get real path for URI string %s", uriString);
}
} else if (uriString.startsWith("file://")) {
realPath = uriString.substring(7);
if (realPath.startsWith("/android_asset/")) {
LOG.e(LOG_TAG, "Cannot get real path for URI string %s because it is a file:///android_asset/ URI.", uriString);
realPath = null;
}
} else {
realPath = uriString;
}
return realPath;
}
/**
* Returns the real path of the given URI.
* If the given URI is a content:// URI, the real path is retrieved from the media store.
*
* @param uri the URI of the audio/image/video
* @param cordova the current application context
* @return the full path to the file
*/
public static String getRealPath(Uri uri, CordovaInterface cordova) {
return FileHelper.getRealPath(uri.toString(), cordova);
}
/**
* Returns an input stream based on given URI string.
*
* @param uriString the URI string from which to obtain the input stream
* @param cordova the current application context
* @return an input stream into the data at the given URI or null if given an invalid URI string
* @throws IOException
*/
public static InputStream getInputStreamFromUriString(String uriString, CordovaInterface cordova) throws IOException {
if (uriString.startsWith("content")) {
Uri uri = Uri.parse(uriString);
return cordova.getActivity().getContentResolver().openInputStream(uri);
} else if (uriString.startsWith("file://")) {
int question = uriString.indexOf("?");
if (question > -1) {
uriString = uriString.substring(0,question);
}
if (uriString.startsWith("file:///android_asset/")) {
Uri uri = Uri.parse(uriString);
String relativePath = uri.getPath().substring(15);
return cordova.getActivity().getAssets().open(relativePath);
} else {
return new FileInputStream(getRealPath(uriString, cordova));
}
} else {
return new FileInputStream(getRealPath(uriString, cordova));
}
}
/**
* Removes the "file://" prefix from the given URI string, if applicable.
* If the given URI string doesn't have a "file://" prefix, it is returned unchanged.
*
* @param uriString the URI string to operate on
* @return a path without the "file://" prefix
*/
public static String stripFileProtocol(String uriString) {
if (uriString.startsWith("file://")) {
uriString = uriString.substring(7);
}
return uriString;
}
public static String getMimeTypeForExtension(String path) {
String extension = path;
int lastDot = extension.lastIndexOf('.');
if (lastDot != -1) {
extension = extension.substring(lastDot + 1);
}
// Convert the URI string to lower case to ensure compatibility with MimeTypeMap (see CB-2185).
extension = extension.toLowerCase(Locale.getDefault());
if (extension.equals("3ga")) {
return "audio/3gpp";
}
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
/**
* Returns the mime type of the data specified by the given URI string.
*
* @param uriString the URI string of the data
* @return the mime type of the specified data
*/
public static String getMimeType(String uriString, CordovaInterface cordova) {
String mimeType = null;
Uri uri = Uri.parse(uriString);
if (uriString.startsWith("content://")) {
mimeType = cordova.getActivity().getContentResolver().getType(uri);
} else {
mimeType = getMimeTypeForExtension(uri.getPath());
}
return mimeType;
}
}

View File

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

View File

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

View File

@@ -32,11 +32,16 @@ import android.webkit.WebResourceResponse;
import android.webkit.WebView;
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class IceCreamCordovaWebViewClient extends AndroidWebViewClient {
public class IceCreamCordovaWebViewClient extends CordovaWebViewClient {
private static final String TAG = "IceCreamCordovaWebViewClient";
private CordovaUriHelper helper;
public IceCreamCordovaWebViewClient(CordovaInterface cordova, AndroidWebView view) {
public IceCreamCordovaWebViewClient(CordovaInterface cordova) {
super(cordova);
}
public IceCreamCordovaWebViewClient(CordovaInterface cordova, CordovaWebView view) {
super(cordova, view);
}

View File

@@ -0,0 +1,43 @@
/*
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.util.ArrayList;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONException;
@Deprecated // Deprecated in 3.1. To be removed in 4.0.
public class JSONUtils {
public static List<String> toStringList(JSONArray array) throws JSONException {
if(array == null) {
return null;
}
else {
List<String> list = new ArrayList<String>();
for (int i = 0; i < array.length(); i++) {
list.add(array.get(i).toString());
}
return list;
}
}
}

View File

@@ -18,10 +18,7 @@
*/
package org.apache.cordova;
import org.apache.cordova.LOG;
import android.content.Context;
//import android.view.View.MeasureSpec;
import android.widget.LinearLayout;
/**
@@ -36,6 +33,7 @@ public class LinearLayoutSoftKeyboardDetect extends LinearLayout {
private int screenWidth = 0;
private int screenHeight = 0;
private CordovaActivity app = null;
private App appPlugin = null;
public LinearLayoutSoftKeyboardDetect(Context context, int width, int height) {
super(context);
@@ -50,7 +48,7 @@ public class LinearLayoutSoftKeyboardDetect extends LinearLayout {
* 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
* Note: We are using the core App plugin to send events over the bridge to Javascript
*
* @param widthMeasureSpec
* @param heightMeasureSpec
@@ -87,14 +85,12 @@ public class LinearLayoutSoftKeyboardDetect extends LinearLayout {
// If the height as gotten bigger then we will assume the soft keyboard has
// gone away.
else if (height > oldHeight) {
if (app != null)
app.appView.sendJavascript("cordova.fireDocumentEvent('hidekeyboard');");
sendEvent("hidekeyboard");
}
// If the height as gotten smaller then we will assume the soft keyboard has
// If the height as gotten smaller then we will assume the soft keyboard has
// been displayed.
else if (height < oldHeight) {
if (app != null)
app.appView.sendJavascript("cordova.fireDocumentEvent('showkeyboard');");
sendEvent("showkeyboard");
}
// Update the old height for the next event
@@ -102,4 +98,15 @@ public class LinearLayoutSoftKeyboardDetect extends LinearLayout {
oldWidth = width;
}
private void sendEvent(String event) {
if (appPlugin == null) {
appPlugin = (App)app.appView.pluginManager.getPlugin(App.PLUGIN_NAME);
}
if (appPlugin == null) {
LOG.w(TAG, "Unable to fire event without existing plugin");
return;
}
appPlugin.fireJavascriptEvent(event);
}
}

View File

@@ -37,6 +37,7 @@ public class NativeToJsMessageQueue {
// Set this to true to force plugin results to be encoding as
// JS instead of the custom format (useful for benchmarking).
// Doesn't work for multipart messages.
private static final boolean FORCE_ENCODE_USING_EVAL = false;
// Disable sending back native->JS messages during an exec() when the active
@@ -84,7 +85,7 @@ public class NativeToJsMessageQueue {
registeredListeners[3] = new PrivateApiBridgeMode();
reset();
}
public boolean isBridgeEnabled() {
return activeBridgeMode != null;
}
@@ -298,7 +299,7 @@ public class NativeToJsMessageQueue {
public void run() {
String js = popAndEncodeAsJs();
if (js != null) {
webView.loadUrlIntoView("javascript:" + js, false);
webView.loadUrlNow("javascript:" + js);
}
}
};
@@ -419,53 +420,43 @@ public class NativeToJsMessageQueue {
this.pluginResult = pluginResult;
}
static int calculateEncodedLengthHelper(PluginResult pluginResult) {
switch (pluginResult.getMessageType()) {
case PluginResult.MESSAGE_TYPE_BOOLEAN: // f or t
case PluginResult.MESSAGE_TYPE_NULL: // N
return 1;
case PluginResult.MESSAGE_TYPE_NUMBER: // n
return 1 + pluginResult.getMessage().length();
case PluginResult.MESSAGE_TYPE_STRING: // s
return 1 + pluginResult.getStrMessage().length();
case PluginResult.MESSAGE_TYPE_BINARYSTRING:
return 1 + pluginResult.getMessage().length();
case PluginResult.MESSAGE_TYPE_ARRAYBUFFER:
return 1 + pluginResult.getMessage().length();
case PluginResult.MESSAGE_TYPE_MULTIPART:
int ret = 1;
for (int i = 0; i < pluginResult.getMultipartMessagesSize(); i++) {
int length = calculateEncodedLengthHelper(pluginResult.getMultipartMessage(i));
int argLength = String.valueOf(length).length();
ret += argLength + 1 + length;
}
return ret;
case PluginResult.MESSAGE_TYPE_JSON:
default:
return pluginResult.getMessage().length();
}
}
int calculateEncodedLength() {
if (pluginResult == null) {
return jsPayloadOrCallbackId.length() + 1;
}
int statusLen = String.valueOf(pluginResult.getStatus()).length();
int ret = 2 + statusLen + 1 + jsPayloadOrCallbackId.length() + 1;
switch (pluginResult.getMessageType()) {
case PluginResult.MESSAGE_TYPE_BOOLEAN: // f or t
case PluginResult.MESSAGE_TYPE_NULL: // N
ret += 1;
break;
case PluginResult.MESSAGE_TYPE_NUMBER: // n
ret += 1 + pluginResult.getMessage().length();
break;
case PluginResult.MESSAGE_TYPE_STRING: // s
ret += 1 + pluginResult.getStrMessage().length();
break;
case PluginResult.MESSAGE_TYPE_BINARYSTRING:
ret += 1 + pluginResult.getMessage().length();
break;
case PluginResult.MESSAGE_TYPE_ARRAYBUFFER:
ret += 1 + pluginResult.getMessage().length();
break;
case PluginResult.MESSAGE_TYPE_JSON:
default:
ret += pluginResult.getMessage().length();
return ret + calculateEncodedLengthHelper(pluginResult);
}
return ret;
}
void encodeAsMessage(StringBuilder sb) {
if (pluginResult == null) {
sb.append('J')
.append(jsPayloadOrCallbackId);
return;
}
int status = pluginResult.getStatus();
boolean noResult = status == PluginResult.Status.NO_RESULT.ordinal();
boolean resultOk = status == PluginResult.Status.OK.ordinal();
boolean keepCallback = pluginResult.getKeepCallback();
sb.append((noResult || resultOk) ? 'S' : 'F')
.append(keepCallback ? '1' : '0')
.append(status)
.append(' ')
.append(jsPayloadOrCallbackId)
.append(' ');
static void encodeAsMessageHelper(StringBuilder sb, PluginResult pluginResult) {
switch (pluginResult.getMessageType()) {
case PluginResult.MESSAGE_TYPE_BOOLEAN:
sb.append(pluginResult.getMessage().charAt(0)); // t or f.
@@ -489,12 +480,42 @@ public class NativeToJsMessageQueue {
sb.append('A');
sb.append(pluginResult.getMessage());
break;
case PluginResult.MESSAGE_TYPE_MULTIPART:
sb.append('M');
for (int i = 0; i < pluginResult.getMultipartMessagesSize(); i++) {
PluginResult multipartMessage = pluginResult.getMultipartMessage(i);
sb.append(String.valueOf(calculateEncodedLengthHelper(multipartMessage)));
sb.append(' ');
encodeAsMessageHelper(sb, multipartMessage);
}
break;
case PluginResult.MESSAGE_TYPE_JSON:
default:
sb.append(pluginResult.getMessage()); // [ or {
}
}
void encodeAsMessage(StringBuilder sb) {
if (pluginResult == null) {
sb.append('J')
.append(jsPayloadOrCallbackId);
return;
}
int status = pluginResult.getStatus();
boolean noResult = status == PluginResult.Status.NO_RESULT.ordinal();
boolean resultOk = status == PluginResult.Status.OK.ordinal();
boolean keepCallback = pluginResult.getKeepCallback();
sb.append((noResult || resultOk) ? 'S' : 'F')
.append(keepCallback ? '1' : '0')
.append(status)
.append(' ')
.append(jsPayloadOrCallbackId)
.append(' ');
encodeAsMessageHelper(sb, pluginResult);
}
void encodeAsJsMessage(StringBuilder sb) {
if (pluginResult == null) {
sb.append(jsPayloadOrCallbackId);

View File

@@ -18,38 +18,43 @@
*/
package org.apache.cordova;
import java.util.List;
import org.apache.cordova.CordovaPlugin;
/**
* This class represents a service entry object.
*/
public final class PluginEntry {
public class PluginEntry {
/**
* The name of the service that this plugin implements
*/
public final String service;
public String service;
/**
* The plugin class name that implements the service.
*/
public final String pluginClass;
public String pluginClass;
/**
* The pre-instantiated plugin to use for this entry.
*/
public final CordovaPlugin plugin;
public CordovaPlugin plugin;
/**
* Flag that indicates the plugin object should be created when PluginManager is initialized.
*/
public final boolean onload;
public boolean onload;
private List<String> urlFilters;
/**
* Constructs with a CordovaPlugin already instantiated.
*/
public PluginEntry(String service, CordovaPlugin plugin) {
this(service, plugin.getClass().getName(), true, plugin);
this(service, plugin.getClass().getName(), true, plugin, null);
}
/**
@@ -58,13 +63,27 @@ public final class PluginEntry {
* @param onload Create plugin object when HTML page is loaded
*/
public PluginEntry(String service, String pluginClass, boolean onload) {
this(service, pluginClass, onload, null);
this(service, pluginClass, onload, null, null);
}
private PluginEntry(String service, String pluginClass, boolean onload, CordovaPlugin plugin) {
@Deprecated // urlFilters are going away
public PluginEntry(String service, String pluginClass, boolean onload, List<String> urlFilters) {
this.service = service;
this.pluginClass = pluginClass;
this.onload = onload;
this.urlFilters = urlFilters;
plugin = null;
}
private PluginEntry(String service, String pluginClass, boolean onload, CordovaPlugin plugin, List<String> urlFilters) {
this.service = service;
this.pluginClass = pluginClass;
this.onload = onload;
this.urlFilters = urlFilters;
this.plugin = plugin;
}
public List<String> getUrlFilters() {
return urlFilters;
}
}

View File

@@ -18,8 +18,8 @@
*/
package org.apache.cordova;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CallbackContext;
@@ -51,21 +51,31 @@ public class PluginManager {
private final CordovaInterface ctx;
private final CordovaWebView app;
public PluginManager(CordovaWebView cordovaWebView, CordovaInterface cordova, Collection<PluginEntry> pluginEntries) {
// Stores mapping of Plugin Name -> <url-filter> values.
// Using <url-filter> is deprecated.
protected HashMap<String, List<String>> urlMap = new HashMap<String, List<String>>();
@Deprecated
PluginManager(CordovaWebView cordovaWebView, CordovaInterface cordova) {
this(cordovaWebView, cordova, null);
}
PluginManager(CordovaWebView cordovaWebView, CordovaInterface cordova, List<PluginEntry> pluginEntries) {
this.ctx = cordova;
this.app = cordovaWebView;
if (pluginEntries == null) {
ConfigXmlParser parser = new ConfigXmlParser();
parser.parse(ctx.getActivity());
pluginEntries = parser.getPluginEntries();
}
setPluginEntries(pluginEntries);
}
public Collection<PluginEntry> getPluginEntries() {
return entryMap.values();
}
public void setPluginEntries(Collection<PluginEntry> pluginEntries) {
public void setPluginEntries(List<PluginEntry> pluginEntries) {
this.onPause(false);
this.onDestroy();
pluginMap.clear();
entryMap.clear();
urlMap.clear();
for (PluginEntry entry : pluginEntries) {
addService(entry);
}
@@ -82,13 +92,30 @@ public class PluginManager {
this.startupPlugins();
}
@Deprecated
public void loadPlugins() {
}
/**
* Delete all plugin objects.
*/
@Deprecated // Should not be exposed as public.
public void clearPluginObjects() {
pluginMap.clear();
}
/**
* Create plugins objects that have onload set.
*/
private void startupPlugins() {
@Deprecated // Should not be exposed as public.
public void startupPlugins() {
for (PluginEntry entry : entryMap.values()) {
// Add a null entry to for each non-startup plugin to avoid ConcurrentModificationException
// When iterating plugins.
if (entry.onload) {
getPlugin(entry.service);
} else {
pluginMap.put(entry.service, null);
}
}
}
@@ -140,6 +167,11 @@ public class PluginManager {
}
}
@Deprecated
public void exec(String service, String action, String callbackId, String jsonArgs, boolean async) {
exec(service, action, callbackId, jsonArgs);
}
/**
* Get the plugin object that implements the service.
* If the plugin object does not already exist, then create it.
@@ -186,10 +218,15 @@ public class PluginManager {
*/
public void addService(PluginEntry entry) {
this.entryMap.put(entry.service, entry);
List<String> urlFilters = entry.getUrlFilters();
if (urlFilters != null) {
urlMap.put(entry.service, urlFilters);
}
if (entry.plugin != null) {
entry.plugin.privateInitialize(ctx, app, app.getPreferences());
pluginMap.put(entry.service, entry.plugin);
}
}
/**
@@ -199,10 +236,52 @@ public class PluginManager {
*/
public void onPause(boolean multitasking) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
plugin.onPause(multitasking);
if (plugin != null) {
plugin.onPause(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.
*
@@ -210,7 +289,9 @@ public class PluginManager {
*/
public void onResume(boolean multitasking) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
plugin.onResume(multitasking);
if (plugin != null) {
plugin.onResume(multitasking);
}
}
}
@@ -219,7 +300,9 @@ public class PluginManager {
*/
public void onDestroy() {
for (CordovaPlugin plugin : this.pluginMap.values()) {
plugin.onDestroy();
if (plugin != null) {
plugin.onDestroy();
}
}
}
@@ -236,9 +319,11 @@ public class PluginManager {
return obj;
}
for (CordovaPlugin plugin : this.pluginMap.values()) {
obj = plugin.onMessage(id, data);
if (obj != null) {
return obj;
if (plugin != null) {
obj = plugin.onMessage(id, data);
if (obj != null) {
return obj;
}
}
}
return null;
@@ -249,7 +334,9 @@ public class PluginManager {
*/
public void onNewIntent(Intent intent) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
plugin.onNewIntent(intent);
if (plugin != null) {
plugin.onNewIntent(intent);
}
}
}
@@ -265,9 +352,18 @@ public class PluginManager {
// that they are loaded before this function is called (either by setting
// the onload <param> or by making an exec() call to them)
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null && plugin.onOverrideUrlLoading(url)) {
return true;
List<String> urlFilters = urlMap.get(entry.service);
if (urlFilters != null) {
for (String s : urlFilters) {
if (url.startsWith(s)) {
return getPlugin(entry.service).onOverrideUrlLoading(url);
}
}
} else {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null && plugin.onOverrideUrlLoading(url)) {
return true;
}
}
}
return false;
@@ -278,15 +374,19 @@ public class PluginManager {
*/
public void onReset() {
for (CordovaPlugin plugin : this.pluginMap.values()) {
plugin.onReset();
if (plugin != null) {
plugin.onReset();
}
}
}
Uri remapUri(Uri uri) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
Uri ret = plugin.remapUri(uri);
if (ret != null) {
return ret;
if (plugin != null) {
Uri ret = plugin.remapUri(uri);
if (ret != null) {
return ret;
}
}
}
return null;

19
framework/src/org/apache/cordova/PluginResult.java Executable file → Normal file
View File

@@ -18,6 +18,8 @@
*/
package org.apache.cordova;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -29,6 +31,7 @@ public class PluginResult {
private boolean keepCallback = false;
private String strMessage;
private String encodedMessage;
private List<PluginResult> multipartMessages;
public PluginResult(Status status) {
this(status, PluginResult.StatusMessages[status.ordinal()]);
@@ -80,6 +83,13 @@ public class PluginResult {
this.encodedMessage = Base64.encodeToString(data, Base64.NO_WRAP);
}
// The keepCallback and status of multipartMessages are ignored.
public PluginResult(Status status, List<PluginResult> multipartMessages) {
this.status = status.ordinal();
this.messageType = MESSAGE_TYPE_MULTIPART;
this.multipartMessages = multipartMessages;
}
public void setKeepCallback(boolean b) {
this.keepCallback = b;
}
@@ -99,6 +109,14 @@ public class PluginResult {
return encodedMessage;
}
public int getMultipartMessagesSize() {
return multipartMessages.size();
}
public PluginResult getMultipartMessage(int index) {
return multipartMessages.get(index);
}
/**
* If messageType == MESSAGE_TYPE_STRING, then returns the message string.
* Otherwise, returns null.
@@ -150,6 +168,7 @@ public class PluginResult {
// Use BINARYSTRING when your string may contain null characters.
// This is required to work around a bug in the platform :(.
public static final int MESSAGE_TYPE_BINARYSTRING = 7;
public static final int MESSAGE_TYPE_MULTIPART = 8;
public static String[] StatusMessages = new String[] {
"No result",

View File

@@ -43,7 +43,7 @@ public class ScrollEvent {
* @param view
*/
public ScrollEvent(int nx, int ny, int x, int y, View view)
ScrollEvent(int nx, int ny, int x, int y, View view)
{
l = x; y = t; nl = nx; nt = ny;
targetView = view;

View File

@@ -0,0 +1,257 @@
/*
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.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.os.Handler;
import android.view.Display;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.LinearLayout;
import org.json.JSONArray;
import org.json.JSONException;
// This file is a copy of SplashScreen.java from cordova-plugin-splashscreen, and is required only
// for pre-4.0 Cordova as a transition path to it being extracted into the plugin.
public class SplashScreenInternal extends CordovaPlugin {
private static final String LOG_TAG = "SplashScreenInternal";
private static Dialog splashDialog;
private static ProgressDialog spinnerDialog;
private static boolean firstShow = true;
@Override
protected void pluginInitialize() {
if (!firstShow) {
return;
}
// Make WebView invisible while loading URL
webView.setVisibility(View.INVISIBLE);
int drawableId = preferences.getInteger("SplashDrawableId", 0);
if (drawableId == 0) {
String splashResource = preferences.getString("SplashScreen", null);
if (splashResource != null) {
drawableId = cordova.getActivity().getResources().getIdentifier(splashResource, "drawable", cordova.getActivity().getClass().getPackage().getName());
if (drawableId == 0) {
drawableId = cordova.getActivity().getResources().getIdentifier(splashResource, "drawable", cordova.getActivity().getPackageName());
}
preferences.set("SplashDrawableId", drawableId);
}
}
firstShow = false;
loadSpinner();
showSplashScreen(true);
}
@Override
public void onPause(boolean multitasking) {
// hide the splash screen to avoid leaking a window
this.removeSplashScreen();
}
@Override
public void onDestroy() {
// hide the splash screen to avoid leaking a window
this.removeSplashScreen();
firstShow = true;
}
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
if (action.equals("hide")) {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
webView.postMessage("splashscreen", "hide");
}
});
} else if (action.equals("show")) {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
webView.postMessage("splashscreen", "show");
}
});
} else if (action.equals("spinnerStart")) {
final String title = args.getString(0);
final String message = args.getString(1);
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
spinnerStart(title, message);
}
});
} else {
return false;
}
callbackContext.success();
return true;
}
@Override
public Object onMessage(String id, Object data) {
if ("splashscreen".equals(id)) {
if ("hide".equals(data.toString())) {
this.removeSplashScreen();
} else {
this.showSplashScreen(false);
}
} else if ("spinner".equals(id)) {
if ("stop".equals(data.toString())) {
this.spinnerStop();
webView.setVisibility(View.VISIBLE);
}
} else if ("onReceivedError".equals(id)) {
spinnerStop();
}
return null;
}
private void removeSplashScreen() {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
if (splashDialog != null && splashDialog.isShowing()) {
splashDialog.dismiss();
splashDialog = null;
}
}
});
}
/**
* Shows the splash screen over the full Activity
*/
@SuppressWarnings("deprecation")
private void showSplashScreen(final boolean hideAfterDelay) {
final int splashscreenTime = preferences.getInteger("SplashScreenDelay", 3000);
final int drawableId = preferences.getInteger("SplashDrawableId", 0);
// If the splash dialog is showing don't try to show it again
if (this.splashDialog != null && splashDialog.isShowing()) {
return;
}
if (drawableId == 0 || (splashscreenTime <= 0 && hideAfterDelay)) {
return;
}
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
// Get reference to display
Display display = cordova.getActivity().getWindowManager().getDefaultDisplay();
Context context = webView.getContext();
// Create the layout for the dialog
LinearLayout root = new LinearLayout(context);
root.setMinimumHeight(display.getHeight());
root.setMinimumWidth(display.getWidth());
root.setOrientation(LinearLayout.VERTICAL);
// TODO: Use the background color of the webview's parent instead of using the
// preference.
root.setBackgroundColor(preferences.getInteger("backgroundColor", Color.BLACK));
root.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT, 0.0F));
root.setBackgroundResource(drawableId);
// Create and show the dialog
splashDialog = new Dialog(context, android.R.style.Theme_Translucent_NoTitleBar);
// check to see if the splash screen should be full screen
if ((cordova.getActivity().getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN)
== WindowManager.LayoutParams.FLAG_FULLSCREEN) {
splashDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
splashDialog.setContentView(root);
splashDialog.setCancelable(false);
splashDialog.show();
// Set Runnable to remove splash screen just in case
if (hideAfterDelay) {
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
public void run() {
removeSplashScreen();
}
}, splashscreenTime);
}
}
});
}
/*
* Load the spinner
*/
private void loadSpinner() {
// If loadingDialog property, then show the App loading dialog for first page of app
String loading = null;
if (webView.canGoBack()) {
loading = preferences.getString("LoadingDialog", null);
}
else {
loading = preferences.getString("LoadingPageDialog", null);
}
if (loading != null) {
String title = "";
String message = "Loading Application...";
if (loading.length() > 0) {
int comma = loading.indexOf(',');
if (comma > 0) {
title = loading.substring(0, comma);
message = loading.substring(comma + 1);
}
else {
title = "";
message = loading;
}
}
spinnerStart(title, message);
}
}
private void spinnerStart(final String title, final String message) {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
spinnerStop();
spinnerDialog = ProgressDialog.show(webView.getContext(), title, message, true, true,
new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
spinnerDialog = null;
}
});
}
});
}
private void spinnerStop() {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
if (spinnerDialog != null && spinnerDialog.isShowing()) {
spinnerDialog.dismiss();
spinnerDialog = null;
}
}
});
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "cordova-android",
"version": "4.0.0-dev",
"version": "3.7.1",
"description": "cordova-android release",
"main": "bin/create",
"repository": {
@@ -14,17 +14,16 @@
],
"scripts": {
"test": "jasmine-node --color spec",
"test-build": "rm -rf \"test create\"; ./bin/create \"test create\" com.test.app Test && \"./test create/cordova/build\" && rm -rf \"test create\""
"test-build": "rm -rf \"test create\"; ./bin/create \"test create\" com.test.app 応用 && \"./test create/cordova/build\" && rm -rf \"test create\""
},
"author": "Apache Software Foundation",
"license": "Apache version 2.0",
"dependencies": {
"q": "^0.9.0",
"shelljs": "^0.2.6",
"which": "^1.0.5"
"shelljs": "^0.2.6"
},
"devDependencies": {
"jasmine-node": "~1",
"promise-matchers": "~0"
}
}
}

View File

@@ -34,7 +34,7 @@ found at https://code.google.com/p/robotium/ and the jar should be put in the
To run manually from command line:
0. Build by entering `ant debug install`
0. Run tests by clicking on "CordovaTest" icon on device
0. Run tests by clicking on "CordovaNativeTests" app icon on the device
To run from Eclipse:

View File

@@ -1,8 +0,0 @@
<html>
<head>
<title>OH NOES!</title>
</head>
<body>
<h1>Things went terribly wrong!</h1>
</body>
</html>

View File

@@ -22,7 +22,7 @@
android:layout_height="fill_parent"
android:orientation="vertical" >
<org.apache.cordova.AndroidWebView
<org.apache.cordova.CordovaWebView
android:id="@+id/cordovaWebView"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />

View File

@@ -34,6 +34,7 @@
<preference name="loglevel" value="DEBUG" />
<preference name="useBrowserHistory" value="true" />
<preference name="exit-on-suspend" value="false" />
<preference name="showTitle" value="true" />
<feature name="Activity">
<param name="android-package" value="org.apache.cordova.test.ActivityPlugin" />
</feature>

View File

@@ -22,12 +22,12 @@ package org.apache.cordova.test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.cordova.AndroidChromeClient;
import org.apache.cordova.AndroidWebViewClient;
import org.apache.cordova.Config;
import org.apache.cordova.CordovaChromeClient;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebViewClient;
import org.apache.cordova.test.R;
import android.app.Activity;
@@ -51,8 +51,8 @@ public class CordovaWebViewTestActivity extends Activity implements CordovaInter
Config.init(this);
cordovaWebView = (CordovaWebView) findViewById(R.id.cordovaWebView);
cordovaWebView.init(this, Config.getPluginEntries(), Config.getWhitelist(),
Config.getExternalWhitelist(), Config.getPreferences());
cordovaWebView.init(this, new CordovaWebViewClient(this, cordovaWebView), new CordovaChromeClient(this, cordovaWebView),
Config.getPluginEntries(), Config.getWhitelist(), Config.getExternalWhitelist(), Config.getPreferences());
cordovaWebView.loadUrl("file:///android_asset/www/index.html");

View File

@@ -0,0 +1,46 @@
/*
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.test;
import android.os.Bundle;
import org.apache.cordova.*;
public class basicauth extends CordovaActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
super.init();
// LogCat: onReceivedHttpAuthRequest(browserspy.dk:80,BrowserSpy.dk - HTTP Password Test)
AuthenticationToken token = new AuthenticationToken();
token.setUserName("test");
token.setPassword("test");
// classic webview includes port in hostname, Chromium webview does not. Handle both here.
// BTW, the realm is optional.
setAuthenticationToken(token, "browserspy.dk:80", "BrowserSpy.dk - HTTP Password Test");
setAuthenticationToken(token, "browserspy.dk", "BrowserSpy.dk - HTTP Password Test");
// Add web site to whitelist
Config.getWhitelist().addWhiteListEntry("http://browserspy.dk/*", true);
// Load test
super.loadUrl("file:///android_asset/www/basicauth/index.html");
}
}

View File

@@ -27,7 +27,6 @@ import org.apache.cordova.test.backbuttonmultipage;
import android.test.ActivityInstrumentationTestCase2;
import android.test.UiThreadTest;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.BaseInputConnection;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
@@ -175,7 +174,7 @@ public class BackButtonMultiPageTest extends ActivityInstrumentationTestCase2<ba
{
String url = testView.getUrl();
assertTrue(url.endsWith("sample3.html"));
BaseInputConnection viewConnection = new BaseInputConnection((View) testView, true);
BaseInputConnection viewConnection = new BaseInputConnection(testView, true);
KeyEvent backDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
KeyEvent backUp = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK);
viewConnection.sendKeyEvent(backDown);
@@ -188,7 +187,7 @@ public class BackButtonMultiPageTest extends ActivityInstrumentationTestCase2<ba
{
String url = testView.getUrl();
assertTrue(url.endsWith("sample2.html"));
BaseInputConnection viewConnection = new BaseInputConnection((View) testView, true);
BaseInputConnection viewConnection = new BaseInputConnection(testView, true);
KeyEvent backDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
KeyEvent backUp = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK);
viewConnection.sendKeyEvent(backDown);

View File

@@ -59,9 +59,9 @@ public class CordovaActivityTest extends ActivityInstrumentationTestCase2<MainTe
}
public void testForAndroidWebView() {
public void testForCordovaView() {
String className = testView.getClass().getSimpleName();
assertTrue(className.equals("AndroidWebView"));
assertTrue(className.equals("CordovaWebView"));
}
public void testForLinearLayout() {

View File

@@ -65,7 +65,7 @@ public class CordovaResourceApiTest extends ActivityInstrumentationTestCase2<Cor
cordovaWebView = activity.cordovaWebView;
resourceApi = cordovaWebView.getResourceApi();
resourceApi.setThreadCheckingEnabled(false);
cordovaWebView.getPluginManager().addService(new PluginEntry("CordovaResourceApiTestPlugin1", new CordovaPlugin() {
cordovaWebView.pluginManager.addService(new PluginEntry("CordovaResourceApiTestPlugin1", new CordovaPlugin() {
@Override
public Uri remapUri(Uri uri) {
if (uri.getQuery() != null && uri.getQuery().contains("pluginRewrite")) {

View File

@@ -49,11 +49,11 @@ public class CordovaTest extends
assertNotNull(testView);
}
public void testForAndroidWebView() {
public void testForCordovaView() {
//Sleep for no reason!!!!
sleep();
sleep();
String className = testView.getClass().getSimpleName();
assertTrue(className.equals("AndroidWebView"));
assertTrue(className.equals("CordovaWebView"));
}
/*

View File

@@ -1,66 +0,0 @@
/*
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.test.junit;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginManager;
import org.apache.cordova.test.CordovaWebViewTestActivity;
import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.test.ActivityInstrumentationTestCase2;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
public class GapClientTest extends ActivityInstrumentationTestCase2<CordovaWebViewTestActivity> {
private CordovaWebViewTestActivity testActivity;
private FrameLayout containerView;
private LinearLayout innerContainer;
private View testView;
private String rString;
public GapClientTest() {
super("org.apache.cordova.test.activities",CordovaWebViewTestActivity.class);
}
protected void setUp() throws Exception{
super.setUp();
testActivity = this.getActivity();
containerView = (FrameLayout) testActivity.findViewById(android.R.id.content);
innerContainer = (LinearLayout) containerView.getChildAt(0);
testView = innerContainer.getChildAt(0);
}
public void testPreconditions(){
assertNotNull(innerContainer);
assertNotNull(testView);
}
public void testForAndroidWebView() {
String className = testView.getClass().getSimpleName();
assertTrue(className.equals("AndroidWebView"));
}
}

View File

@@ -75,6 +75,7 @@ public class IntentUriOverrideTest extends ActivityInstrumentationTestCase2<Sabo
runTestOnUiThread(new Runnable() {
public void run()
{
sleep();
boolean isBadUrl = testView.getUrl().equals(BAD_URL);
assertFalse(isBadUrl);
}

View File

@@ -0,0 +1,32 @@
/*
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.test.junit;
import org.apache.cordova.test.menus;
import android.test.ActivityInstrumentationTestCase2;
public class MenuTest extends ActivityInstrumentationTestCase2<menus> {
public MenuTest() {
super("org.apache.cordova.test", menus.class);
}
}

View File

@@ -0,0 +1,81 @@
/*
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.test;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import org.apache.cordova.*;
import org.apache.cordova.LOG;
public class menus extends CordovaActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// need the title to be shown (config.xml) for the options menu to be visible
super.init();
super.registerForContextMenu(super.appView);
super.loadUrl("file:///android_asset/www/menus/index.html");
}
// Demonstrate how to add your own menus to app
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
int base = Menu.FIRST;
// Group, item id, order, title
menu.add(base, base, base, "Item1");
menu.add(base, base + 1, base + 1, "Item2");
menu.add(base, base + 2, base + 2, "Item3");
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
LOG.d("menus", "Item " + item.getItemId() + " pressed.");
this.appView.loadUrl("javascript:alert('Menu " + item.getItemId() + " pressed.')");
return super.onOptionsItemSelected(item);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
LOG.d("menus", "onPrepareOptionsMenu()");
// this.appView.loadUrl("javascript:alert('onPrepareOptionsMenu()')");
return true;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo info) {
LOG.d("menus", "onCreateContextMenu()");
menu.setHeaderTitle("Test Context Menu");
menu.add(200, 200, 200, "Context Item1");
}
@Override
public boolean onContextItemSelected(MenuItem item) {
this.appView.loadUrl("javascript:alert('Context Menu " + item.getItemId() + " pressed.')");
return true;
}
}

View File

@@ -32,16 +32,16 @@ public class userwebview extends MainTestActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
testViewClient = new TestViewClient(this, ((AndroidWebView)appView));
testChromeClient = new TestChromeClient(this, ((AndroidWebView)appView));
testViewClient = new TestViewClient(this, appView);
testChromeClient = new TestChromeClient(this, appView);
super.init();
((AndroidWebView)appView).setWebViewClient(testViewClient);
((AndroidWebView)appView).setWebChromeClient(testChromeClient);
appView.setWebViewClient(testViewClient);
appView.setWebChromeClient(testChromeClient);
super.loadUrl("file:///android_asset/www/userwebview/index.html");
}
public class TestChromeClient extends AndroidChromeClient {
public TestChromeClient(CordovaInterface ctx, AndroidWebView app) {
public class TestChromeClient extends CordovaChromeClient {
public TestChromeClient(CordovaInterface ctx, CordovaWebView app) {
super(ctx, app);
LOG.d("userwebview", "TestChromeClient()");
}
@@ -57,8 +57,8 @@ public class userwebview extends MainTestActivity {
/**
* This class can be used to override the GapViewClient and receive notification of webview events.
*/
public class TestViewClient extends AndroidWebViewClient {
public TestViewClient(CordovaInterface ctx, AndroidWebView app) {
public class TestViewClient extends CordovaWebViewClient {
public TestViewClient(CordovaInterface ctx, CordovaWebView app) {
super(ctx, app);
LOG.d("userwebview", "TestViewClient()");
}

View File

@@ -22,22 +22,23 @@ import android.os.Bundle;
import android.webkit.WebView;
import org.apache.cordova.*;
import org.apache.cordova.LOG;
public class whitelist extends MainTestActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
super.init();
((AndroidWebView)appView).setWebViewClient(new TestViewClient(this, ((AndroidWebView)appView)));
appView.setWebViewClient(new TestViewClient(this, appView));
super.loadUrl("file:///android_asset/www/whitelist/index.html");
}
/**
* This class can be used to override the GapViewClient and receive notification of webview events.
*/
public class TestViewClient extends AndroidWebViewClient {
public class TestViewClient extends CordovaWebViewClient {
public TestViewClient(CordovaInterface ctx, AndroidWebView app) {
public TestViewClient(CordovaInterface ctx, CordovaWebView app) {
super(ctx, app);
}