add native android build sources

This commit is contained in:
Morgan McMillian 2018-07-15 19:24:48 -07:00
parent 79aee37688
commit 892cb0e141
600 changed files with 108263 additions and 11 deletions

4
.gitignore vendored
View file

@ -23,10 +23,6 @@ node_modules/
tmp/
temp/
hooks/
platforms/
plugins/
plugins/android.json
plugins/ios.json
www/
$RECYCLE.BIN/

View file

@ -37,3 +37,10 @@ release: $(APK)
clean:
rm $(OUT_DIR)/*.apk
distclean:
rm -r node_modules platforms plugins www
init:
npm install
ionic cordova platform add android

View file

@ -1,6 +1,6 @@
# Goober, a mobile app for pnut.io
Copyright 2017 Morgan McMillian
Copyright 2017 - 2018 Morgan McMillian
Goober is a cross platform mobile client for pnut.io built using the Ionic framework (http://ionicframework.com/).
@ -40,10 +40,6 @@ npm install -g ionic cordova
* Install Gradle (needed if you only installed the command line tools)
* https://gradle.org/
#### Windows
* Install VisualStudio
* https://www.visualstudio.com/
## Other build dependencies
@ -60,7 +56,7 @@ ionic cordova platform add android
```
## Build and run
## Build and run using ionic framework
#### Browser
```bash

View file

@ -101,5 +101,5 @@
<variable name="ANDROID_SUPPORT_VERSION" value="27.+" />
</plugin>
<plugin name="cordova-plugin-filepath" spec="^1.3.0" />
<engine name="android" spec="~7.1.0" />
<engine name="android" spec="7.1.0" />
</widget>

14
platforms/android/.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
# Non-project-specific build files:
build.xml
local.properties
/gradlew
/gradlew.bat
/gradle
# Ant builds
ant-build
ant-gen
# Eclipse builds
gen
out
# Gradle builds
/build

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.apache.cordova" android:versionName="1.0" android:versionCode="1">
<uses-sdk android:minSdkVersion="19" />
</manifest>

View file

@ -0,0 +1,137 @@
/* 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.
*/
ext {
apply from: 'cordova.gradle'
cdvCompileSdkVersion = privateHelpers.getProjectTarget()
cdvBuildToolsVersion = privateHelpers.findLatestInstalledBuildTools()
}
buildscript {
repositories {
jcenter()
maven {
url "https://maven.google.com"
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5'
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3'
}
}
apply plugin: 'com.android.library'
apply plugin: 'com.github.dcendents.android-maven'
apply plugin: 'com.jfrog.bintray'
group = 'org.apache.cordova'
version = '7.1.0'
android {
compileSdkVersion cdvCompileSdkVersion
buildToolsVersion cdvBuildToolsVersion
publishNonDefault true
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
resources.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
}
}
packagingOptions {
exclude 'META-INF/LICENSE'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/NOTICE'
}
}
install {
repositories.mavenInstaller {
pom {
project {
packaging 'aar'
name 'Cordova'
url 'https://cordova.apache.org'
licenses {
license {
name 'The Apache Software License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
developers {
developer {
id 'stevengill'
name 'Steve Gill'
}
}
scm {
connection 'https://git-wip-us.apache.org/repos/asf?p=cordova-android.git'
developerConnection 'https://git-wip-us.apache.org/repos/asf?p=cordova-android.git'
url 'https://git-wip-us.apache.org/repos/asf?p=cordova-android'
}
}
}
}
}
task sourcesJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier = 'sources'
}
artifacts {
archives sourcesJar
}
bintray {
user = System.getenv('BINTRAY_USER')
key = System.getenv('BINTRAY_KEY')
configurations = ['archives']
pkg {
repo = 'maven'
name = 'cordova-android'
userOrg = 'cordova'
licenses = ['Apache-2.0']
vcsUrl = 'https://git-wip-us.apache.org/repos/asf?p=cordova-android.git'
websiteUrl = 'https://cordova.apache.org'
issueTrackerUrl = 'https://issues.apache.org/jira/browse/CB'
publicDownloadNumbers = true
licenses = ['Apache-2.0']
labels = ['android', 'cordova', 'phonegap']
version {
name = '7.1.0'
released = new Date()
vcsTag = '7.1.0'
}
}
}

View file

@ -0,0 +1,205 @@
/*
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.
*/
import java.util.regex.Pattern
import groovy.swing.SwingBuilder
String doEnsureValueExists(filePath, props, key) {
if (props.get(key) == null) {
throw new GradleException(filePath + ': Missing key required "' + key + '"')
}
return props.get(key)
}
String doGetProjectTarget() {
def props = new Properties()
def propertiesFile = 'project.properties';
if(!(file(propertiesFile).exists())) {
propertiesFile = '../project.properties';
}
file(propertiesFile).withReader { reader ->
props.load(reader)
}
return doEnsureValueExists('project.properties', props, 'target')
}
String[] getAvailableBuildTools() {
def buildToolsDir = new File(getAndroidSdkDir(), "build-tools")
buildToolsDir.list()
.findAll { it ==~ /[0-9.]+/ }
.sort { a, b -> compareVersions(b, a) }
}
String doFindLatestInstalledBuildTools(String minBuildToolsVersion) {
def availableBuildToolsVersions
try {
availableBuildToolsVersions = getAvailableBuildTools()
} catch (e) {
println "An exception occurred while trying to find the Android build tools."
throw e
}
if (availableBuildToolsVersions.length > 0) {
def highestBuildToolsVersion = availableBuildToolsVersions[0]
if (compareVersions(highestBuildToolsVersion, minBuildToolsVersion) < 0) {
throw new RuntimeException(
"No usable Android build tools found. Highest installed version is " +
highestBuildToolsVersion + "; minimum version required is " +
minBuildToolsVersion + ".")
}
highestBuildToolsVersion
} else {
throw new RuntimeException(
"No installed build tools found. Install the Android build tools version " +
minBuildToolsVersion + " or higher.")
}
}
// Return the first non-zero result of subtracting version list elements
// pairwise. If they are all identical, return the difference in length of
// the two lists.
int compareVersionList(Collection aParts, Collection bParts) {
def pairs = ([aParts, bParts]).transpose()
pairs.findResult(aParts.size()-bParts.size()) {it[0] - it[1] != 0 ? it[0] - it[1] : null}
}
// Compare two version strings, such as "19.0.0" and "18.1.1.0". If all matched
// elements are identical, the longer version is the largest by this method.
// Examples:
// "19.0.0" > "19"
// "19.0.1" > "19.0.0"
// "19.1.0" > "19.0.1"
// "19" > "18.999.999"
int compareVersions(String a, String b) {
def aParts = a.tokenize('.').collect {it.toInteger()}
def bParts = b.tokenize('.').collect {it.toInteger()}
compareVersionList(aParts, bParts)
}
String getAndroidSdkDir() {
def rootDir = project.rootDir
def androidSdkDir = null
String envVar = System.getenv("ANDROID_HOME")
def localProperties = new File(rootDir, 'local.properties')
String systemProperty = System.getProperty("android.home")
if (envVar != null) {
androidSdkDir = envVar
} else if (localProperties.exists()) {
Properties properties = new Properties()
localProperties.withInputStream { instr ->
properties.load(instr)
}
def sdkDirProp = properties.getProperty('sdk.dir')
if (sdkDirProp != null) {
androidSdkDir = sdkDirProp
} else {
sdkDirProp = properties.getProperty('android.dir')
if (sdkDirProp != null) {
androidSdkDir = (new File(rootDir, sdkDirProp)).getAbsolutePath()
}
}
}
if (androidSdkDir == null && systemProperty != null) {
androidSdkDir = systemProperty
}
if (androidSdkDir == null) {
throw new RuntimeException(
"Unable to determine Android SDK directory.")
}
androidSdkDir
}
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 new BigInteger(matcher.group(1))
}
def doExtractStringFromManifest(name) {
def manifestFile = file(android.sourceSets.main.manifest.srcFile)
def pattern = Pattern.compile(name + "=\"(\\S+)\"")
def matcher = pattern.matcher(manifestFile.getText())
matcher.find()
return 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);
}
}
def doGetConfigXml() {
def xml = file("src/main/res/xml/config.xml").getText()
// Disable namespace awareness since Cordova doesn't use them properly
return new XmlParser(false, false).parseText(xml)
}
def doGetConfigPreference(name, defaultValue) {
name = name.toLowerCase()
def root = doGetConfigXml()
def ret = defaultValue
root.preference.each { it ->
def attrName = it.attribute("name")
if (attrName && attrName.toLowerCase() == name) {
ret = it.attribute("value")
}
}
return ret
}
// 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.extractStringFromManifest = { name -> doExtractStringFromManifest(name) }
privateHelpers.promptForPassword = { msg -> doPromptForPassword(msg) }
privateHelpers.ensureValueExists = { filePath, props, key -> doEnsureValueExists(filePath, props, key) }
// These helpers can be used by plugins / projects and will not change.
cdvHelpers = {}
// Returns a XmlParser for the config.xml. Added in 4.1.0.
cdvHelpers.getConfigXml = { doGetConfigXml() }
// Returns the value for the desired <preference>. Added in 4.1.0.
cdvHelpers.getConfigPreference = { name, defaultValue -> doGetConfigPreference(name, defaultValue) }
}

View file

@ -0,0 +1,16 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system use,
# "ant.properties", and override values to adapt the script to your
# project structure.
# Indicates whether an apk should be generated for each density.
split.density=false
# Project target.
target=android-27
apk-configurations=
renderscript.opt.level=O0
android.library=true

View file

@ -0,0 +1,69 @@
/*
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;
/**
* The Class AuthenticationToken defines the userName and password to be used for authenticating a web resource
*/
public class AuthenticationToken {
private String userName;
private String password;
/**
* Gets the user name.
*
* @return the user name
*/
public String getUserName() {
return userName;
}
/**
* Sets the user name.
*
* @param userName
* the new user name
*/
public void setUserName(String userName) {
this.userName = userName;
}
/**
* Gets the password.
*
* @return the password
*/
public String getPassword() {
return password;
}
/**
* Sets the password.
*
* @param password
* the new password
*/
public void setPassword(String password) {
this.password = password;
}
}

View file

@ -0,0 +1,70 @@
/*
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 is a utility class that allows us to get the BuildConfig variable, which is required
* for the use of different providers. This is not guaranteed to work, and it's better for this
* to be set in the build step in config.xml
*
*/
import android.app.Activity;
import android.content.Context;
import java.lang.reflect.Field;
public class BuildHelper {
private static String TAG="BuildHelper";
/*
* This needs to be implemented if you wish to use the Camera Plugin or other plugins
* that read the Build Configuration.
*
* Thanks to Phil@Medtronic and Graham Borland for finding the answer and posting it to
* StackOverflow. This is annoying as hell! However, this method does not work with
* ProGuard, and you should use the config.xml to define the application_id
*
*/
public static Object getBuildConfigValue(Context ctx, String key)
{
try
{
Class<?> clazz = Class.forName(ctx.getPackageName() + ".BuildConfig");
Field field = clazz.getField(key);
return field.get(null);
} catch (ClassNotFoundException e) {
LOG.d(TAG, "Unable to get the BuildConfig, is this built with ANT?");
e.printStackTrace();
} catch (NoSuchFieldException e) {
LOG.d(TAG, key + " is not a valid field. Check your build.gradle");
} catch (IllegalAccessException e) {
LOG.d(TAG, "Illegal Access Exception: Let's print a stack trace.");
e.printStackTrace();
}
return null;
}
}

View file

@ -0,0 +1,142 @@
/*
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 org.json.JSONArray;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginResult;
import org.json.JSONObject;
public class CallbackContext {
private static final String LOG_TAG = "CordovaPlugin";
private String callbackId;
private CordovaWebView webView;
protected boolean finished;
private int changingThreads;
public CallbackContext(String callbackId, CordovaWebView webView) {
this.callbackId = callbackId;
this.webView = webView;
}
public boolean isFinished() {
return finished;
}
public boolean isChangingThreads() {
return changingThreads > 0;
}
public String getCallbackId() {
return callbackId;
}
public void sendPluginResult(PluginResult pluginResult) {
synchronized (this) {
if (finished) {
LOG.w(LOG_TAG, "Attempted to send a second callback for ID: " + callbackId + "\nResult was: " + pluginResult.getMessage());
return;
} else {
finished = !pluginResult.getKeepCallback();
}
}
webView.sendPluginResult(pluginResult, callbackId);
}
/**
* Helper for success callbacks that just returns the Status.OK by default
*
* @param message The message to add to the success result.
*/
public void success(JSONObject message) {
sendPluginResult(new PluginResult(PluginResult.Status.OK, message));
}
/**
* Helper for success callbacks that just returns the Status.OK by default
*
* @param message The message to add to the success result.
*/
public void success(String message) {
sendPluginResult(new PluginResult(PluginResult.Status.OK, message));
}
/**
* Helper for success callbacks that just returns the Status.OK by default
*
* @param message The message to add to the success result.
*/
public void success(JSONArray message) {
sendPluginResult(new PluginResult(PluginResult.Status.OK, message));
}
/**
* Helper for success callbacks that just returns the Status.OK by default
*
* @param message The message to add to the success result.
*/
public void success(byte[] message) {
sendPluginResult(new PluginResult(PluginResult.Status.OK, message));
}
/**
* Helper for success callbacks that just returns the Status.OK by default
*
* @param message The message to add to the success result.
*/
public void success(int message) {
sendPluginResult(new PluginResult(PluginResult.Status.OK, message));
}
/**
* Helper for success callbacks that just returns the Status.OK by default
*/
public void success() {
sendPluginResult(new PluginResult(PluginResult.Status.OK));
}
/**
* Helper for error callbacks that just returns the Status.ERROR by default
*
* @param message The message to add to the error result.
*/
public void error(JSONObject message) {
sendPluginResult(new PluginResult(PluginResult.Status.ERROR, message));
}
/**
* Helper for error callbacks that just returns the Status.ERROR by default
*
* @param message The message to add to the error result.
*/
public void error(String message) {
sendPluginResult(new PluginResult(PluginResult.Status.ERROR, message));
}
/**
* Helper for error callbacks that just returns the Status.ERROR by default
*
* @param message The message to add to the error result.
*/
public void error(int message) {
sendPluginResult(new PluginResult(PluginResult.Status.ERROR, message));
}
}

View file

@ -0,0 +1,65 @@
/*
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.util.Pair;
import android.util.SparseArray;
/**
* Provides a collection that maps unique request codes to CordovaPlugins and Integers.
* Used to ensure that when plugins make requests for runtime permissions, those requests do not
* collide with requests from other plugins that use the same request code value.
*/
public class CallbackMap {
private int currentCallbackId = 0;
private SparseArray<Pair<CordovaPlugin, Integer>> callbacks;
public CallbackMap() {
this.callbacks = new SparseArray<Pair<CordovaPlugin, Integer>>();
}
/**
* Stores a CordovaPlugin and request code and returns a new unique request code to use
* in a permission request.
*
* @param receiver The plugin that is making the request
* @param requestCode The original request code used by the plugin
* @return A unique request code that can be used to retrieve this callback
* with getAndRemoveCallback()
*/
public synchronized int registerCallback(CordovaPlugin receiver, int requestCode) {
int mappedId = this.currentCallbackId++;
callbacks.put(mappedId, new Pair<CordovaPlugin, Integer>(receiver, requestCode));
return mappedId;
}
/**
* Retrieves and removes a callback stored in the map using the mapped request code
* obtained from registerCallback()
*
* @param mappedId The request code obtained from registerCallback()
* @return The CordovaPlugin and orignal request code that correspond to the
* given mappedCode
*/
public synchronized Pair<CordovaPlugin, Integer> getAndRemoveCallback(int mappedId) {
Pair<CordovaPlugin, Integer> callback = callbacks.get(mappedId);
callbacks.remove(mappedId);
return callback;
}
}

View file

@ -0,0 +1,71 @@
/*
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.List;
import android.app.Activity;
@Deprecated // Use Whitelist, CordovaPrefences, etc. directly.
public class Config {
private static final String TAG = "Config";
static ConfigXmlParser parser;
private Config() {
}
public static void init(Activity action) {
parser = new ConfigXmlParser();
parser.parse(action);
//TODO: Add feature to bring this back. Some preferences should be overridden by intents, but not all
parser.getPreferences().setPreferencesBundle(action.getIntent().getExtras());
}
// Intended to be used for testing only; creates an empty configuration.
public static void init() {
if (parser == null) {
parser = new ConfigXmlParser();
}
}
public static String getStartUrl() {
if (parser == null) {
return "file:///android_asset/www/index.html";
}
return parser.getLaunchUrl();
}
public static String getErrorUrl() {
return parser.getPreferences().getString("errorurl", null);
}
public static List<PluginEntry> getPluginEntries() {
return parser.getPluginEntries();
}
public static CordovaPreferences getPreferences() {
return parser.getPreferences();
}
public static boolean isInitialized() {
return parser != null;
}
}

View file

@ -0,0 +1,145 @@
/*
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 java.util.ArrayList;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import android.content.Context;
public class ConfigXmlParser {
private static String TAG = "ConfigXmlParser";
private String launchUrl = "file:///android_asset/www/index.html";
private CordovaPreferences prefs = new CordovaPreferences();
private ArrayList<PluginEntry> pluginEntries = new ArrayList<PluginEntry>(20);
public CordovaPreferences getPreferences() {
return prefs;
}
public ArrayList<PluginEntry> getPluginEntries() {
return pluginEntries;
}
public String getLaunchUrl() {
return launchUrl;
}
public void parse(Context action) {
// First checking the class namespace for config.xml
int id = action.getResources().getIdentifier("config", "xml", action.getClass().getPackage().getName());
if (id == 0) {
// If we couldn't find config.xml there, we'll look in the namespace from AndroidManifest.xml
id = action.getResources().getIdentifier("config", "xml", action.getPackageName());
if (id == 0) {
LOG.e(TAG, "res/xml/config.xml is missing!");
return;
}
}
parse(action.getResources().getXml(id));
}
boolean insideFeature = false;
String service = "", pluginClass = "", paramType = "";
boolean onload = false;
public void parse(XmlPullParser xml) {
int eventType = -1;
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG) {
handleStartTag(xml);
}
else if (eventType == XmlPullParser.END_TAG)
{
handleEndTag(xml);
}
try {
eventType = xml.next();
} catch (XmlPullParserException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void handleStartTag(XmlPullParser xml) {
String strNode = xml.getName();
if (strNode.equals("feature")) {
//Check for supported feature sets aka. plugins (Accelerometer, Geolocation, etc)
//Set the bit for reading params
insideFeature = true;
service = xml.getAttributeValue(null, "name");
}
else if (insideFeature && strNode.equals("param")) {
paramType = xml.getAttributeValue(null, "name");
if (paramType.equals("service")) // check if it is using the older service param
service = xml.getAttributeValue(null, "value");
else if (paramType.equals("package") || paramType.equals("android-package"))
pluginClass = xml.getAttributeValue(null,"value");
else if (paramType.equals("onload"))
onload = "true".equals(xml.getAttributeValue(null, "value"));
}
else if (strNode.equals("preference")) {
String name = xml.getAttributeValue(null, "name").toLowerCase(Locale.ENGLISH);
String value = xml.getAttributeValue(null, "value");
prefs.set(name, value);
}
else if (strNode.equals("content")) {
String src = xml.getAttributeValue(null, "src");
if (src != null) {
setStartUrl(src);
}
}
}
public void handleEndTag(XmlPullParser xml) {
String strNode = xml.getName();
if (strNode.equals("feature")) {
pluginEntries.add(new PluginEntry(service, pluginClass, onload));
service = "";
pluginClass = "";
insideFeature = false;
onload = false;
}
}
private void setStartUrl(String src) {
Pattern schemeRegex = Pattern.compile("^[a-z-]+://");
Matcher matcher = schemeRegex.matcher(src);
if (matcher.find()) {
launchUrl = src;
} else {
if (src.charAt(0) == '/') {
src = src.substring(1);
}
launchUrl = "file:///android_asset/www/" + src;
}
}
}

View file

@ -0,0 +1,519 @@
/*
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.Locale;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.app.AlertDialog;
import android.annotation.SuppressLint;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Color;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
/**
* This class is the main Android activity that represents the Cordova
* application. It should be extended by the user to load the specific
* html file that contains the application.
*
* As an example:
*
* <pre>
* package org.apache.cordova.examples;
*
* import android.os.Bundle;
* import org.apache.cordova.*;
*
* public class Example extends CordovaActivity {
* &#64;Override
* public void onCreate(Bundle savedInstanceState) {
* super.onCreate(savedInstanceState);
* super.init();
* // Load your application
* loadUrl(launchUrl);
* }
* }
* </pre>
*
* Cordova xml configuration: Cordova uses a configuration file at
* res/xml/config.xml to specify its settings. See "The config.xml File"
* guide in cordova-docs at http://cordova.apache.org/docs for the documentation
* for the configuration. The use of the set*Property() methods is
* deprecated in favor of the config.xml file.
*
*/
public class CordovaActivity extends Activity {
public static String TAG = "CordovaActivity";
// The webview for our app
protected CordovaWebView appView;
private static int ACTIVITY_STARTING = 0;
private static int ACTIVITY_RUNNING = 1;
private static int ACTIVITY_EXITING = 2;
// Keep app running when pause is received. (default = true)
// If true, then the JavaScript and native code continue to run in the background
// when another application (activity) is started.
protected boolean keepRunning = true;
// Flag to keep immersive mode if set to fullscreen
protected boolean immersiveMode;
// Read from config.xml:
protected CordovaPreferences preferences;
protected String launchUrl;
protected ArrayList<PluginEntry> pluginEntries;
protected CordovaInterfaceImpl cordovaInterface;
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
// need to activate preferences before super.onCreate to avoid "requestFeature() must be called before adding content" exception
loadConfig();
String logLevel = preferences.getString("loglevel", "ERROR");
LOG.setLogLevel(logLevel);
LOG.i(TAG, "Apache Cordova native platform version " + CordovaWebView.CORDOVA_VERSION + " is starting");
LOG.d(TAG, "CordovaActivity.onCreate()");
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.");
preferences.set("Fullscreen", true);
}
if (preferences.getBoolean("Fullscreen", false)) {
// NOTE: use the FullscreenNotImmersive configuration key to set the activity in a REAL full screen
// (as was the case in previous cordova versions)
if (!preferences.getBoolean("FullscreenNotImmersive", false)) {
immersiveMode = true;
} else {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
} else {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
}
super.onCreate(savedInstanceState);
cordovaInterface = makeCordovaInterface();
if (savedInstanceState != null) {
cordovaInterface.restoreInstanceState(savedInstanceState);
}
}
protected void init() {
appView = makeWebView();
createViews();
if (!appView.isInitialized()) {
appView.init(cordovaInterface, pluginEntries, preferences);
}
cordovaInterface.onCordovaInit(appView.getPluginManager());
// 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);
}
}
@SuppressWarnings("deprecation")
protected void loadConfig() {
ConfigXmlParser parser = new ConfigXmlParser();
parser.parse(this);
preferences = parser.getPreferences();
preferences.setPreferencesBundle(getIntent().getExtras());
launchUrl = parser.getLaunchUrl();
pluginEntries = parser.getPluginEntries();
Config.parser = parser;
}
//Suppressing warnings in AndroidStudio
@SuppressWarnings({"deprecation", "ResourceType"})
protected void createViews() {
//Why are we setting a constant as the ID? This should be investigated
appView.getView().setId(100);
appView.getView().setLayoutParams(new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
setContentView(appView.getView());
if (preferences.contains("BackgroundColor")) {
try {
int backgroundColor = preferences.getInteger("BackgroundColor", Color.BLACK);
// Background of activity:
appView.getView().setBackgroundColor(backgroundColor);
}
catch (NumberFormatException e){
e.printStackTrace();
}
}
appView.getView().requestFocusFromTouch();
}
/**
* Construct the default web view object.
* <p/>
* Override this to customize the webview that is used.
*/
protected CordovaWebView makeWebView() {
return new CordovaWebViewImpl(makeWebViewEngine());
}
protected CordovaWebViewEngine makeWebViewEngine() {
return CordovaWebViewImpl.createEngine(this, preferences);
}
protected CordovaInterfaceImpl makeCordovaInterface() {
return new CordovaInterfaceImpl(this) {
@Override
public Object onMessage(String id, Object data) {
// Plumb this to CordovaActivity.onMessage for backwards compatibility
return CordovaActivity.this.onMessage(id, data);
}
};
}
/**
* Load the url into the webview.
*/
public void loadUrl(String url) {
if (appView == null) {
init();
}
// If keepRunning
this.keepRunning = preferences.getBoolean("KeepRunning", true);
appView.loadUrlIntoView(url, true);
}
/**
* Called when the system is about to start resuming a previous activity.
*/
@Override
protected void onPause() {
super.onPause();
LOG.d(TAG, "Paused the activity.");
if (this.appView != null) {
// CB-9382 If there is an activity that started for result and main activity is waiting for callback
// result, we shoudn't stop WebView Javascript timers, as activity for result might be using them
boolean keepRunning = this.keepRunning || this.cordovaInterface.activityResultCallback != null;
this.appView.handlePause(keepRunning);
}
}
/**
* Called when the activity receives a new intent
*/
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
//Forward to plugins
if (this.appView != null)
this.appView.onNewIntent(intent);
}
/**
* Called when the activity will start interacting with the user.
*/
@Override
protected void onResume() {
super.onResume();
LOG.d(TAG, "Resumed the activity.");
if (this.appView == null) {
return;
}
// Force window to have focus, so application always
// receive user input. Workaround for some devices (Samsung Galaxy Note 3 at least)
this.getWindow().getDecorView().requestFocus();
this.appView.handleResume(this.keepRunning);
}
/**
* Called when the activity is no longer visible to the user.
*/
@Override
protected void onStop() {
super.onStop();
LOG.d(TAG, "Stopped the activity.");
if (this.appView == null) {
return;
}
this.appView.handleStop();
}
/**
* Called when the activity is becoming visible to the user.
*/
@Override
protected void onStart() {
super.onStart();
LOG.d(TAG, "Started the activity.");
if (this.appView == null) {
return;
}
this.appView.handleStart();
}
/**
* The final call you receive before your activity is destroyed.
*/
@Override
public void onDestroy() {
LOG.d(TAG, "CordovaActivity.onDestroy()");
super.onDestroy();
if (this.appView != null) {
appView.handleDestroy();
}
}
/**
* Called when view focus is changed
*/
@SuppressLint("InlinedApi")
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus && immersiveMode) {
final int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
getWindow().getDecorView().setSystemUiVisibility(uiOptions);
}
}
@SuppressLint("NewApi")
@Override
public void startActivityForResult(Intent intent, int requestCode, Bundle options) {
// Capture requestCode here so that it is captured in the setActivityResultCallback() case.
cordovaInterface.setActivityResultRequestCode(requestCode);
super.startActivityForResult(intent, requestCode, options);
}
/**
* Called when an activity you launched exits, giving you the requestCode you started it with,
* the resultCode it returned, and any additional data from it.
*
* @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 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. Request code = " + requestCode);
super.onActivityResult(requestCode, resultCode, intent);
cordovaInterface.onActivityResult(requestCode, resultCode, intent);
}
/**
* Report an error to the host application. These errors are unrecoverable (i.e. the main resource is unavailable).
* The errorCode parameter corresponds to one of the ERROR_* constants.
*
* @param errorCode The error code corresponding to an ERROR_* value.
* @param description A String describing the error.
* @param failingUrl The url that failed to load.
*/
public void onReceivedError(final int errorCode, final String description, final String failingUrl) {
final CordovaActivity me = this;
// If errorUrl specified, then load it
final String errorUrl = preferences.getString("errorUrl", null);
if ((errorUrl != null) && (!failingUrl.equals(errorUrl)) && (appView != null)) {
// Load URL on UI thread
me.runOnUiThread(new Runnable() {
public void run() {
me.appView.showWebPage(errorUrl, false, true, null);
}
});
}
// If not, then display error dialog
else {
final boolean exit = !(errorCode == WebViewClient.ERROR_HOST_LOOKUP);
me.runOnUiThread(new Runnable() {
public void run() {
if (exit) {
me.appView.getView().setVisibility(View.GONE);
me.displayError("Application Error", description + " (" + failingUrl + ")", "OK", exit);
}
}
});
}
}
/**
* Display an error dialog and optionally exit application.
*/
public void displayError(final String title, final String message, final String button, final boolean exit) {
final CordovaActivity me = this;
me.runOnUiThread(new Runnable() {
public void run() {
try {
AlertDialog.Builder dlg = new AlertDialog.Builder(me);
dlg.setMessage(message);
dlg.setTitle(title);
dlg.setCancelable(false);
dlg.setPositiveButton(button,
new AlertDialog.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
if (exit) {
finish();
}
}
});
dlg.create();
dlg.show();
} catch (Exception e) {
finish();
}
}
});
}
/*
* Hook in Cordova for menu plugins
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
if (appView != null) {
appView.getPluginManager().postMessage("onCreateOptionsMenu", menu);
}
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
if (appView != null) {
appView.getPluginManager().postMessage("onPrepareOptionsMenu", menu);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (appView != null) {
appView.getPluginManager().postMessage("onOptionsItemSelected", item);
}
return true;
}
/**
* Called when a message is sent to plugin.
*
* @param id The message id
* @param data The message data
* @return Object or null
*/
public Object onMessage(String id, Object data) {
if ("onReceivedError".equals(id)) {
JSONObject d = (JSONObject) data;
try {
this.onReceivedError(d.getInt("errorCode"), d.getString("description"), d.getString("url"));
} catch (JSONException e) {
e.printStackTrace();
}
} else if ("exit".equals(id)) {
finish();
}
return null;
}
protected void onSaveInstanceState(Bundle outState) {
cordovaInterface.onSaveInstanceState(outState);
super.onSaveInstanceState(outState);
}
/**
* Called by the system when the device configuration changes while your activity is running.
*
* @param newConfig The new device configuration
*/
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (this.appView == null) {
return;
}
PluginManager pm = this.appView.getPluginManager();
if (pm != null) {
pm.onConfigurationChanged(newConfig);
}
}
/**
* Called by the system when the user grants permissions
*
* @param requestCode
* @param permissions
* @param grantResults
*/
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[],
int[] grantResults) {
try
{
cordovaInterface.onRequestPermissionResult(requestCode, permissions, grantResults);
}
catch (JSONException e)
{
LOG.d(TAG, "JSONException: Parameters fed into the method are not valid");
e.printStackTrace();
}
}
}

View file

@ -0,0 +1,113 @@
/*
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 org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.util.Base64;
public class CordovaArgs {
private JSONArray baseArgs;
public CordovaArgs(JSONArray args) {
this.baseArgs = args;
}
// Pass through the basics to the base args.
public Object get(int index) throws JSONException {
return baseArgs.get(index);
}
public boolean getBoolean(int index) throws JSONException {
return baseArgs.getBoolean(index);
}
public double getDouble(int index) throws JSONException {
return baseArgs.getDouble(index);
}
public int getInt(int index) throws JSONException {
return baseArgs.getInt(index);
}
public JSONArray getJSONArray(int index) throws JSONException {
return baseArgs.getJSONArray(index);
}
public JSONObject getJSONObject(int index) throws JSONException {
return baseArgs.getJSONObject(index);
}
public long getLong(int index) throws JSONException {
return baseArgs.getLong(index);
}
public String getString(int index) throws JSONException {
return baseArgs.getString(index);
}
public Object opt(int index) {
return baseArgs.opt(index);
}
public boolean optBoolean(int index) {
return baseArgs.optBoolean(index);
}
public double optDouble(int index) {
return baseArgs.optDouble(index);
}
public int optInt(int index) {
return baseArgs.optInt(index);
}
public JSONArray optJSONArray(int index) {
return baseArgs.optJSONArray(index);
}
public JSONObject optJSONObject(int index) {
return baseArgs.optJSONObject(index);
}
public long optLong(int index) {
return baseArgs.optLong(index);
}
public String optString(int index) {
return baseArgs.optString(index);
}
public boolean isNull(int index) {
return baseArgs.isNull(index);
}
// The interesting custom helpers.
public byte[] getArrayBuffer(int index) throws JSONException {
String encoded = baseArgs.getString(index);
return Base64.decode(encoded, Base64.DEFAULT);
}
}

View file

@ -0,0 +1,187 @@
/*
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.annotation.SuppressLint;
import java.security.SecureRandom;
import org.json.JSONArray;
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
*/
public class CordovaBridge {
private static final String LOG_TAG = "CordovaBridge";
private PluginManager pluginManager;
private NativeToJsMessageQueue jsMessageQueue;
private volatile int expectedBridgeSecret = -1; // written by UI thread, read by JS thread.
public CordovaBridge(PluginManager pluginManager, NativeToJsMessageQueue jsMessageQueue) {
this.pluginManager = pluginManager;
this.jsMessageQueue = jsMessageQueue;
}
public String jsExec(int bridgeSecret, String service, String action, String callbackId, String arguments) throws JSONException, IllegalAccessException {
if (!verifySecret("exec()", bridgeSecret)) {
return null;
}
// If the arguments weren't received, send a message back to JS. It will switch bridge modes and try again. See CB-2666.
// We send a message meant specifically for this case. It starts with "@" so no other message can be encoded into the same string.
if (arguments == null) {
return "@Null arguments.";
}
jsMessageQueue.setPaused(true);
try {
// Tell the resourceApi what thread the JS is running on.
CordovaResourceApi.jsThread = Thread.currentThread();
pluginManager.exec(service, action, callbackId, arguments);
String ret = null;
if (!NativeToJsMessageQueue.DISABLE_EXEC_CHAINING) {
ret = jsMessageQueue.popAndEncode(false);
}
return ret;
} catch (Throwable e) {
e.printStackTrace();
return "";
} finally {
jsMessageQueue.setPaused(false);
}
}
public void jsSetNativeToJsBridgeMode(int bridgeSecret, int value) throws IllegalAccessException {
if (!verifySecret("setNativeToJsBridgeMode()", bridgeSecret)) {
return;
}
jsMessageQueue.setBridgeMode(value);
}
public String jsRetrieveJsMessages(int bridgeSecret, boolean fromOnlineEvent) throws IllegalAccessException {
if (!verifySecret("retrieveJsMessages()", bridgeSecret)) {
return null;
}
return jsMessageQueue.popAndEncode(fromOnlineEvent);
}
private boolean verifySecret(String action, int bridgeSecret) throws IllegalAccessException {
if (!jsMessageQueue.isBridgeEnabled()) {
if (bridgeSecret == -1) {
LOG.d(LOG_TAG, action + " call made before bridge was enabled.");
} else {
LOG.d(LOG_TAG, "Ignoring " + action + " from previous page load.");
}
return false;
}
// 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;
}
/** Called on page transitions */
void clearBridgeSecret() {
expectedBridgeSecret = -1;
}
public boolean isSecretEstablished() {
return expectedBridgeSecret != -1;
}
/** Called by cordova.js to initialize the bridge. */
//On old Androids SecureRandom isn't really secure, this is the least of your problems if
//you're running Android 4.3 and below in 2017
@SuppressLint("TrulyRandom")
int generateBridgeSecret() {
SecureRandom randGen = new SecureRandom();
expectedBridgeSecret = randGen.nextInt(Integer.MAX_VALUE);
return expectedBridgeSecret;
}
public void reset() {
jsMessageQueue.reset();
clearBridgeSecret();
}
public String promptOnJsPrompt(String origin, String message, String defaultValue) {
if (defaultValue != null && defaultValue.length() > 3 && defaultValue.startsWith("gap:")) {
JSONArray array;
try {
array = new JSONArray(defaultValue.substring(4));
int bridgeSecret = array.getInt(0);
String service = array.getString(1);
String action = array.getString(2);
String callbackId = array.getString(3);
String r = jsExec(bridgeSecret, service, action, callbackId, message);
return r == null ? "" : r;
} catch (JSONException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return "";
}
// Sets the native->JS bridge mode.
else if (defaultValue != null && defaultValue.startsWith("gap_bridge_mode:")) {
try {
int bridgeSecret = Integer.parseInt(defaultValue.substring(16));
jsSetNativeToJsBridgeMode(bridgeSecret, Integer.parseInt(message));
} catch (NumberFormatException e){
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return "";
}
// Polling for JavaScript messages
else if (defaultValue != null && defaultValue.startsWith("gap_poll:")) {
int bridgeSecret = Integer.parseInt(defaultValue.substring(9));
try {
String r = jsRetrieveJsMessages(bridgeSecret, "1".equals(message));
return r == null ? "" : r;
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return "";
}
else if (defaultValue != null && defaultValue.startsWith("gap_init:")) {
// Protect against random iframes being able to talk through the bridge.
// Trust only pages which the app would have been allowed to navigate to anyway.
if (pluginManager.shouldAllowBridgeAccess(origin)) {
// Enable the bridge
int bridgeMode = Integer.parseInt(defaultValue.substring(9));
jsMessageQueue.setBridgeMode(bridgeMode);
// Tell JS the bridge secret.
int secret = generateBridgeSecret();
return ""+secret;
} else {
LOG.e(LOG_TAG, "gap_init called from restricted origin: " + origin);
}
return "";
}
return null;
}
}

View file

@ -0,0 +1,105 @@
/*
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.annotation.SuppressLint;
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
*/
@SuppressLint("NewApi")
public void cancel()
{
request.cancel();
}
/*
* Returns the host name of the server requesting the certificate.
*/
@SuppressLint("NewApi")
public String getHost()
{
return request.getHost();
}
/*
* Returns the acceptable types of asymmetric keys (can be null).
*/
@SuppressLint("NewApi")
public String[] getKeyTypes()
{
return request.getKeyTypes();
}
/*
* Returns the port number of the server requesting the certificate.
*/
@SuppressLint("NewApi")
public int getPort()
{
return request.getPort();
}
/*
* Returns the acceptable certificate issuers for the certificate matching the private key (can be null).
*/
@SuppressLint("NewApi")
public Principal[] getPrincipals()
{
return request.getPrincipals();
}
/*
* Ignore the request for now. Do not remember user's choice.
*/
@SuppressLint("NewApi")
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
*/
@SuppressLint("NewApi")
public void proceed(PrivateKey privateKey, X509Certificate[] chain)
{
request.proceed(privateKey, chain);
}
}

View file

@ -0,0 +1,152 @@
/*
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.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.view.KeyEvent;
import android.widget.EditText;
/**
* Helper class for WebViews to implement prompt(), alert(), confirm() dialogs.
*/
public class CordovaDialogsHelper {
private final Context context;
private AlertDialog lastHandledDialog;
public CordovaDialogsHelper(Context context) {
this.context = context;
}
public void showAlert(String message, final Result result) {
AlertDialog.Builder dlg = new AlertDialog.Builder(context);
dlg.setMessage(message);
dlg.setTitle("Alert");
//Don't let alerts break the back button
dlg.setCancelable(true);
dlg.setPositiveButton(android.R.string.ok,
new AlertDialog.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
result.gotResult(true, null);
}
});
dlg.setOnCancelListener(
new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
result.gotResult(false, null);
}
});
dlg.setOnKeyListener(new DialogInterface.OnKeyListener() {
//DO NOTHING
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK)
{
result.gotResult(true, null);
return false;
}
else
return true;
}
});
lastHandledDialog = dlg.show();
}
public void showConfirm(String message, final Result result) {
AlertDialog.Builder dlg = new AlertDialog.Builder(context);
dlg.setMessage(message);
dlg.setTitle("Confirm");
dlg.setCancelable(true);
dlg.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
result.gotResult(true, null);
}
});
dlg.setNegativeButton(android.R.string.cancel,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
result.gotResult(false, null);
}
});
dlg.setOnCancelListener(
new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
result.gotResult(false, null);
}
});
dlg.setOnKeyListener(new DialogInterface.OnKeyListener() {
//DO NOTHING
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK)
{
result.gotResult(false, null);
return false;
}
else
return true;
}
});
lastHandledDialog = dlg.show();
}
/**
* Tell the client to display a prompt dialog to the user.
* If the client returns true, WebView will assume that the client will
* handle the prompt dialog and call the appropriate JsPromptResult method.
*
* Since we are hacking prompts for our own purposes, we should not be using them for
* this purpose, perhaps we should hack console.log to do this instead!
*/
public void showPrompt(String message, String defaultValue, final Result result) {
// Returning false would also show a dialog, but the default one shows the origin (ugly).
AlertDialog.Builder dlg = new AlertDialog.Builder(context);
dlg.setMessage(message);
final EditText input = new EditText(context);
if (defaultValue != null) {
input.setText(defaultValue);
}
dlg.setView(input);
dlg.setCancelable(false);
dlg.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
String userText = input.getText().toString();
result.gotResult(true, userText);
}
});
dlg.setNegativeButton(android.R.string.cancel,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
result.gotResult(false, null);
}
});
lastHandledDialog = dlg.show();
}
public void destroyLastDialog(){
if (lastHandledDialog != null){
lastHandledDialog.cancel();
}
}
public interface Result {
public void gotResult(boolean success, String value);
}
}

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

@ -0,0 +1,97 @@
/*
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.Activity;
import android.content.Context;
import android.content.Intent;
import org.apache.cordova.CordovaPlugin;
import java.util.concurrent.ExecutorService;
/**
* The Activity interface that is implemented by CordovaActivity.
* It is used to isolate plugin development, and remove dependency on entire Cordova library.
*/
public interface CordovaInterface {
/**
* Launch an activity for which you would like a result when it finished. When this activity exits,
* your onActivityResult() method will be called.
*
* @param command The command object
* @param intent The intent to start
* @param requestCode The request code that is passed to callback to identify the activity
*/
abstract public void startActivityForResult(CordovaPlugin command, Intent intent, int requestCode);
/**
* Set the plugin to be called when a sub-activity exits.
*
* @param plugin The plugin on which onActivityResult is to be called
*/
abstract public void setActivityResultCallback(CordovaPlugin plugin);
/**
* Get the Android activity.
*
* If a custom engine lives outside of the Activity's lifecycle the return value may be null.
*
* @return the Activity
*/
public abstract Activity getActivity();
/**
* Get the Android context.
*
* @return the Context
*/
public Context getContext();
/**
* Called when a message is sent to plugin.
*
* @param id The message id
* @param data The message data
* @return Object or null
*/
public Object onMessage(String id, Object data);
/**
* Returns a shared thread pool that can be used for background tasks.
*/
public ExecutorService getThreadPool();
/**
* Sends a permission request to the activity for one permission.
*/
public void requestPermission(CordovaPlugin plugin, int requestCode, String permission);
/**
* Sends a permission request to the activity for a group of permissions
*/
public void requestPermissions(CordovaPlugin plugin, int requestCode, String [] permissions);
/**
* Check for a permission. Returns true if the permission is granted, false otherwise.
*/
public boolean hasPermission(String permission);
}

View file

@ -0,0 +1,249 @@
/*
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.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Pair;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Default implementation of CordovaInterface.
*/
public class CordovaInterfaceImpl implements CordovaInterface {
private static final String TAG = "CordovaInterfaceImpl";
protected Activity activity;
protected ExecutorService threadPool;
protected PluginManager pluginManager;
protected ActivityResultHolder savedResult;
protected CallbackMap permissionResultCallbacks;
protected CordovaPlugin activityResultCallback;
protected String initCallbackService;
protected int activityResultRequestCode;
protected boolean activityWasDestroyed = false;
protected Bundle savedPluginState;
public CordovaInterfaceImpl(Activity activity) {
this(activity, Executors.newCachedThreadPool());
}
public CordovaInterfaceImpl(Activity activity, ExecutorService threadPool) {
this.activity = activity;
this.threadPool = threadPool;
this.permissionResultCallbacks = new CallbackMap();
}
@Override
public void startActivityForResult(CordovaPlugin command, Intent intent, int requestCode) {
setActivityResultCallback(command);
try {
activity.startActivityForResult(intent, requestCode);
} catch (RuntimeException e) { // E.g.: ActivityNotFoundException
activityResultCallback = null;
throw e;
}
}
@Override
public void setActivityResultCallback(CordovaPlugin plugin) {
// Cancel any previously pending activity.
if (activityResultCallback != null) {
activityResultCallback.onActivityResult(activityResultRequestCode, Activity.RESULT_CANCELED, null);
}
activityResultCallback = plugin;
}
@Override
public Activity getActivity() {
return activity;
}
@Override
public Context getContext() {
return activity;
}
@Override
public Object onMessage(String id, Object data) {
if ("exit".equals(id)) {
activity.finish();
}
return null;
}
@Override
public ExecutorService getThreadPool() {
return threadPool;
}
/**
* Dispatches any pending onActivityResult callbacks and sends the resume event if the
* Activity was destroyed by the OS.
*/
public void onCordovaInit(PluginManager pluginManager) {
this.pluginManager = pluginManager;
if (savedResult != null) {
onActivityResult(savedResult.requestCode, savedResult.resultCode, savedResult.intent);
} else if(activityWasDestroyed) {
// If there was no Activity result, we still need to send out the resume event if the
// Activity was destroyed by the OS
activityWasDestroyed = false;
if(pluginManager != null)
{
CoreAndroid appPlugin = (CoreAndroid) pluginManager.getPlugin(CoreAndroid.PLUGIN_NAME);
if(appPlugin != null) {
JSONObject obj = new JSONObject();
try {
obj.put("action", "resume");
} catch (JSONException e) {
LOG.e(TAG, "Failed to create event message", e);
}
appPlugin.sendResumeEvent(new PluginResult(PluginResult.Status.OK, obj));
}
}
}
}
/**
* Routes the result to the awaiting plugin. Returns false if no plugin was waiting.
*/
public boolean onActivityResult(int requestCode, int resultCode, Intent intent) {
CordovaPlugin callback = activityResultCallback;
if(callback == null && initCallbackService != null) {
// The application was restarted, but had defined an initial callback
// before being shut down.
savedResult = new ActivityResultHolder(requestCode, resultCode, intent);
if (pluginManager != null) {
callback = pluginManager.getPlugin(initCallbackService);
if(callback != null) {
callback.onRestoreStateForActivityResult(savedPluginState.getBundle(callback.getServiceName()),
new ResumeCallback(callback.getServiceName(), pluginManager));
}
}
}
activityResultCallback = null;
if (callback != null) {
LOG.d(TAG, "Sending activity result to plugin");
initCallbackService = null;
savedResult = null;
callback.onActivityResult(requestCode, resultCode, intent);
return true;
}
LOG.w(TAG, "Got an activity result, but no plugin was registered to receive it" + (savedResult != null ? " yet!" : "."));
return false;
}
/**
* Call this from your startActivityForResult() overload. This is required to catch the case
* where plugins use Activity.startActivityForResult() + CordovaInterface.setActivityResultCallback()
* rather than CordovaInterface.startActivityForResult().
*/
public void setActivityResultRequestCode(int requestCode) {
activityResultRequestCode = requestCode;
}
/**
* Saves parameters for startActivityForResult().
*/
public void onSaveInstanceState(Bundle outState) {
if (activityResultCallback != null) {
String serviceName = activityResultCallback.getServiceName();
outState.putString("callbackService", serviceName);
}
if(pluginManager != null){
outState.putBundle("plugin", pluginManager.onSaveInstanceState());
}
}
/**
* Call this from onCreate() so that any saved startActivityForResult parameters will be restored.
*/
public void restoreInstanceState(Bundle savedInstanceState) {
initCallbackService = savedInstanceState.getString("callbackService");
savedPluginState = savedInstanceState.getBundle("plugin");
activityWasDestroyed = true;
}
private static class ActivityResultHolder {
private int requestCode;
private int resultCode;
private Intent intent;
public ActivityResultHolder(int requestCode, int resultCode, Intent intent) {
this.requestCode = requestCode;
this.resultCode = resultCode;
this.intent = intent;
}
}
/**
* Called by the system when the user grants permissions
*
* @param requestCode
* @param permissions
* @param grantResults
*/
public void onRequestPermissionResult(int requestCode, String[] permissions,
int[] grantResults) throws JSONException {
Pair<CordovaPlugin, Integer> callback = permissionResultCallbacks.getAndRemoveCallback(requestCode);
if(callback != null) {
callback.first.onRequestPermissionResult(callback.second, permissions, grantResults);
}
}
public void requestPermission(CordovaPlugin plugin, int requestCode, String permission) {
String[] permissions = new String [1];
permissions[0] = permission;
requestPermissions(plugin, requestCode, permissions);
}
@SuppressLint("NewApi")
public void requestPermissions(CordovaPlugin plugin, int requestCode, String [] permissions) {
int mappedRequestCode = permissionResultCallbacks.registerCallback(plugin, requestCode);
getActivity().requestPermissions(permissions, mappedRequestCode);
}
public boolean hasPermission(String permission)
{
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
{
int result = activity.checkSelfPermission(permission);
return PackageManager.PERMISSION_GRANTED == result;
}
else
{
return true;
}
}
}

View file

@ -0,0 +1,422 @@
/*
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 org.apache.cordova.CordovaArgs;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CallbackContext;
import org.json.JSONArray;
import org.json.JSONException;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* Plugins must extend this class and override one of the execute methods.
*/
public class CordovaPlugin {
public CordovaWebView webView;
public CordovaInterface cordova;
protected CordovaPreferences preferences;
private String serviceName;
/**
* Call this after constructing to initialize the plugin.
* Final because we want to be able to change args without breaking plugins.
*/
public final void privateInitialize(String serviceName, CordovaInterface cordova, CordovaWebView webView, CordovaPreferences preferences) {
assert this.cordova == null;
this.serviceName = serviceName;
this.cordova = cordova;
this.webView = webView;
this.preferences = preferences;
initialize(cordova, webView);
pluginInitialize();
}
/**
* Called after plugin construction and fields have been initialized.
* Prefer to use pluginInitialize instead since there is no value in
* having parameters on the initialize() function.
*/
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
}
/**
* Called after plugin construction and fields have been initialized.
*/
protected void pluginInitialize() {
}
/**
* Returns the plugin's service name (what you'd use when calling pluginManger.getPlugin())
*/
public String getServiceName() {
return serviceName;
}
/**
* Executes the request.
*
* This method is called from the WebView thread. To do a non-trivial amount of work, use:
* cordova.getThreadPool().execute(runnable);
*
* To run on the UI thread, use:
* cordova.getActivity().runOnUiThread(runnable);
*
* @param action The action to execute.
* @param rawArgs The exec() arguments in JSON form.
* @param callbackContext The callback context used when calling back into JavaScript.
* @return Whether the action was valid.
*/
public boolean execute(String action, String rawArgs, CallbackContext callbackContext) throws JSONException {
JSONArray args = new JSONArray(rawArgs);
return execute(action, args, callbackContext);
}
/**
* Executes the request.
*
* This method is called from the WebView thread. To do a non-trivial amount of work, use:
* cordova.getThreadPool().execute(runnable);
*
* To run on the UI thread, use:
* cordova.getActivity().runOnUiThread(runnable);
*
* @param action The action to execute.
* @param args The exec() arguments.
* @param callbackContext The callback context used when calling back into JavaScript.
* @return Whether the action was valid.
*/
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
CordovaArgs cordovaArgs = new CordovaArgs(args);
return execute(action, cordovaArgs, callbackContext);
}
/**
* Executes the request.
*
* This method is called from the WebView thread. To do a non-trivial amount of work, use:
* cordova.getThreadPool().execute(runnable);
*
* To run on the UI thread, use:
* cordova.getActivity().runOnUiThread(runnable);
*
* @param action The action to execute.
* @param args The exec() arguments, wrapped with some Cordova helpers.
* @param callbackContext The callback context used when calling back into JavaScript.
* @return Whether the action was valid.
*/
public boolean execute(String action, CordovaArgs args, CallbackContext callbackContext) throws JSONException {
return false;
}
/**
* Called when the system is about to start resuming a previous activity.
*
* @param multitasking Flag indicating if multitasking is turned on for app
*/
public void onPause(boolean multitasking) {
}
/**
* Called when the activity will start interacting with the user.
*
* @param multitasking Flag indicating if multitasking is turned on for app
*/
public void onResume(boolean multitasking) {
}
/**
* Called when the activity is becoming visible to the user.
*/
public void onStart() {
}
/**
* Called when the activity is no longer visible to the user.
*/
public void onStop() {
}
/**
* Called when the activity receives a new intent.
*/
public void onNewIntent(Intent intent) {
}
/**
* The final call you receive before your activity is destroyed.
*/
public void onDestroy() {
}
/**
* Called when the Activity is being destroyed (e.g. if a plugin calls out to an external
* Activity and the OS kills the CordovaActivity in the background). The plugin should save its
* state in this method only if it is awaiting the result of an external Activity and needs
* to preserve some information so as to handle that result; onRestoreStateForActivityResult()
* will only be called if the plugin is the recipient of an Activity result
*
* @return Bundle containing the state of the plugin or null if state does not need to be saved
*/
public Bundle onSaveInstanceState() {
return null;
}
/**
* Called when a plugin is the recipient of an Activity result after the CordovaActivity has
* been destroyed. The Bundle will be the same as the one the plugin returned in
* onSaveInstanceState()
*
* @param state Bundle containing the state of the plugin
* @param callbackContext Replacement Context to return the plugin result to
*/
public void onRestoreStateForActivityResult(Bundle state, CallbackContext callbackContext) {}
/**
* Called when a message is sent to plugin.
*
* @param id The message id
* @param data The message data
* @return Object to stop propagation or null
*/
public Object onMessage(String id, Object data) {
return null;
}
/**
* Called when an activity you launched exits, giving you the requestCode you started it with,
* the resultCode it returned, and any additional data from it.
*
* @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 intent An Intent, which can return result data to the caller (various data can be
* attached to Intent "extras").
*/
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
}
/**
* Hook for blocking the loading of external resources.
*
* This will be called when the WebView's shouldInterceptRequest wants to
* know whether to open a connection to an external resource. Return false
* to block the request: if any plugin returns false, Cordova will block
* the request. If all plugins return null, the default policy will be
* enforced. If at least one plugin returns true, and no plugins return
* false, then the request will proceed.
*
* Note that this only affects resource requests which are routed through
* WebViewClient.shouldInterceptRequest, such as XMLHttpRequest requests and
* img tag loads. WebSockets and media requests (such as <video> and <audio>
* tags) are not affected by this method. Use CSP headers to control access
* to such resources.
*/
public Boolean shouldAllowRequest(String url) {
return null;
}
/**
* Hook for blocking navigation by the Cordova WebView. This applies both to top-level and
* iframe navigations.
*
* This will be called when the WebView's needs to know whether to navigate
* to a new page. Return false to block the navigation: if any plugin
* returns false, Cordova will block the navigation. If all plugins return
* null, the default policy will be enforced. It at least one plugin returns
* true, and no plugins return false, then the navigation will proceed.
*/
public Boolean shouldAllowNavigation(String url) {
return null;
}
/**
* Hook for allowing page to call exec(). By default, this returns the result of
* shouldAllowNavigation(). It's generally unsafe to allow untrusted content to be loaded
* into a CordovaWebView, even within an iframe, so it's best not to touch this.
*/
public Boolean shouldAllowBridgeAccess(String url) {
return shouldAllowNavigation(url);
}
/**
* Hook for blocking the launching of Intents by the Cordova application.
*
* This will be called when the WebView will not navigate to a page, but
* could launch an intent to handle the URL. Return false to block this: if
* any plugin returns false, Cordova will block the navigation. If all
* plugins return null, the default policy will be enforced. If at least one
* plugin returns true, and no plugins return false, then the URL will be
* opened.
*/
public Boolean shouldOpenExternalUrl(String url) {
return null;
}
/**
* Allows plugins to handle a link being clicked. Return true here to cancel the navigation.
*
* @param url The URL that is trying to be loaded in the Cordova webview.
* @return Return true to prevent the URL from loading. Default is false.
*/
public boolean onOverrideUrlLoading(String url) {
return false;
}
/**
* Hook for redirecting requests. Applies to WebView requests as well as requests made by plugins.
* To handle the request directly, return a URI in the form:
*
* cdvplugin://pluginId/...
*
* And implement handleOpenForRead().
* To make this easier, use the toPluginUri() and fromPluginUri() helpers:
*
* public Uri remapUri(Uri uri) { return toPluginUri(uri); }
*
* public CordovaResourceApi.OpenForReadResult handleOpenForRead(Uri uri) throws IOException {
* Uri origUri = fromPluginUri(uri);
* ...
* }
*/
public Uri remapUri(Uri uri) {
return null;
}
/**
* Called to handle CordovaResourceApi.openForRead() calls for a cdvplugin://pluginId/ URL.
* Should never return null.
* Added in cordova-android@4.0.0
*/
public CordovaResourceApi.OpenForReadResult handleOpenForRead(Uri uri) throws IOException {
throw new FileNotFoundException("Plugin can't handle uri: " + uri);
}
/**
* Refer to remapUri()
* Added in cordova-android@4.0.0
*/
protected Uri toPluginUri(Uri origUri) {
return new Uri.Builder()
.scheme(CordovaResourceApi.PLUGIN_URI_SCHEME)
.authority(serviceName)
.appendQueryParameter("origUri", origUri.toString())
.build();
}
/**
* Refer to remapUri()
* Added in cordova-android@4.0.0
*/
protected Uri fromPluginUri(Uri pluginUri) {
return Uri.parse(pluginUri.getQueryParameter("origUri"));
}
/**
* Called when the WebView does a top-level navigation or refreshes.
*
* Plugins should stop any long-running processes and clean up internal state.
*
* Does nothing by default.
*/
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;
}
/**
* Called by the system when the device configuration changes while your activity is running.
*
* @param newConfig The new device configuration
*/
public void onConfigurationChanged(Configuration newConfig) {
}
/**
* Called by the Plugin Manager when we need to actually request permissions
*
* @param requestCode Passed to the activity to track the request
*
* @return Returns the permission that was stored in the plugin
*/
public void requestPermissions(int requestCode) {
}
/*
* Called by the WebView implementation to check for geolocation permissions, can be used
* by other Java methods in the event that a plugin is using this as a dependency.
*
* @return Returns true if the plugin has all the permissions it needs to operate.
*/
public boolean hasPermisssion() {
return true;
}
/**
* Called by the system when the user grants permissions
*
* @param requestCode
* @param permissions
* @param grantResults
*/
public void onRequestPermissionResult(int requestCode, String[] permissions,
int[] grantResults) throws JSONException {
}
}

View file

@ -0,0 +1,101 @@
/*
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.HashMap;
import java.util.Locale;
import java.util.Map;
import org.apache.cordova.LOG;
import android.app.Activity;
import android.os.Bundle;
public class CordovaPreferences {
private HashMap<String, String> prefs = new HashMap<String, String>(20);
private Bundle preferencesBundleExtras;
public void setPreferencesBundle(Bundle extras) {
preferencesBundleExtras = extras;
}
public void set(String name, String value) {
prefs.put(name.toLowerCase(Locale.ENGLISH), value);
}
public void set(String name, boolean value) {
set(name, "" + value);
}
public void set(String name, int value) {
set(name, "" + value);
}
public void set(String name, double value) {
set(name, "" + value);
}
public Map<String, String> getAll() {
return prefs;
}
public boolean getBoolean(String name, boolean defaultValue) {
name = name.toLowerCase(Locale.ENGLISH);
String value = prefs.get(name);
if (value != null) {
return Boolean.parseBoolean(value);
}
return defaultValue;
}
// Added in 4.0.0
public boolean contains(String name) {
return getString(name, null) != null;
}
public int getInteger(String name, int defaultValue) {
name = name.toLowerCase(Locale.ENGLISH);
String value = prefs.get(name);
if (value != null) {
// Use Integer.decode() can't handle it if the highest bit is set.
return (int)(long)Long.decode(value);
}
return defaultValue;
}
public double getDouble(String name, double defaultValue) {
name = name.toLowerCase(Locale.ENGLISH);
String value = prefs.get(name);
if (value != null) {
return Double.valueOf(value);
}
return defaultValue;
}
public String getString(String name, String defaultValue) {
name = name.toLowerCase(Locale.ENGLISH);
String value = prefs.get(name);
if (value != null) {
return value;
}
return defaultValue;
}
}

View file

@ -0,0 +1,471 @@
/*
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.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Looper;
import android.util.Base64;
import android.webkit.MimeTypeMap;
import java.io.ByteArrayInputStream;
import java.io.File;
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.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.channels.FileChannel;
import java.util.Locale;
/**
* What this class provides:
* 1. Helpers for reading & writing to URLs.
* - E.g. handles assets, resources, content providers, files, data URIs, http[s]
* - E.g. Can be used to query for mime-type & content length.
*
* 2. To allow plugins to redirect URLs (via remapUrl).
* - All plugins should call remapUrl() on URLs they receive from JS *before*
* passing the URL onto other utility functions in this class.
* - For an example usage of this, refer to the org.apache.cordova.file plugin.
*
* Future Work:
* - Consider using a Cursor to query content URLs for their size (like the file plugin does).
* - Allow plugins to remapUri to "cdv-plugin://plugin-name/foo", which CordovaResourceApi
* would then delegate to pluginManager.getPlugin(plugin-name).openForRead(url)
* - Currently, plugins *can* do this by remapping to a data: URL, but it's inefficient
* for large payloads.
*/
public class CordovaResourceApi {
@SuppressWarnings("unused")
private static final String LOG_TAG = "CordovaResourceApi";
public static final int URI_TYPE_FILE = 0;
public static final int URI_TYPE_ASSET = 1;
public static final int URI_TYPE_CONTENT = 2;
public static final int URI_TYPE_RESOURCE = 3;
public static final int URI_TYPE_DATA = 4;
public static final int URI_TYPE_HTTP = 5;
public static final int URI_TYPE_HTTPS = 6;
public static final int URI_TYPE_PLUGIN = 7;
public static final int URI_TYPE_UNKNOWN = -1;
public static final String PLUGIN_URI_SCHEME = "cdvplugin";
private static final String[] LOCAL_FILE_PROJECTION = { "_data" };
public static Thread jsThread;
private final AssetManager assetManager;
private final ContentResolver contentResolver;
private final PluginManager pluginManager;
private boolean threadCheckingEnabled = true;
public CordovaResourceApi(Context context, PluginManager pluginManager) {
this.contentResolver = context.getContentResolver();
this.assetManager = context.getAssets();
this.pluginManager = pluginManager;
}
public void setThreadCheckingEnabled(boolean value) {
threadCheckingEnabled = value;
}
public boolean isThreadCheckingEnabled() {
return threadCheckingEnabled;
}
public static int getUriType(Uri uri) {
assertNonRelative(uri);
String scheme = uri.getScheme();
if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(scheme)) {
return URI_TYPE_CONTENT;
}
if (ContentResolver.SCHEME_ANDROID_RESOURCE.equalsIgnoreCase(scheme)) {
return URI_TYPE_RESOURCE;
}
if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(scheme)) {
if (uri.getPath().startsWith("/android_asset/")) {
return URI_TYPE_ASSET;
}
return URI_TYPE_FILE;
}
if ("data".equalsIgnoreCase(scheme)) {
return URI_TYPE_DATA;
}
if ("http".equalsIgnoreCase(scheme)) {
return URI_TYPE_HTTP;
}
if ("https".equalsIgnoreCase(scheme)) {
return URI_TYPE_HTTPS;
}
if (PLUGIN_URI_SCHEME.equalsIgnoreCase(scheme)) {
return URI_TYPE_PLUGIN;
}
return URI_TYPE_UNKNOWN;
}
public Uri remapUri(Uri uri) {
assertNonRelative(uri);
Uri pluginUri = pluginManager.remapUri(uri);
return pluginUri != null ? pluginUri : uri;
}
public String remapPath(String path) {
return remapUri(Uri.fromFile(new File(path))).getPath();
}
/**
* Returns a File that points to the resource, or null if the resource
* is not on the local filesystem.
*/
public File mapUriToFile(Uri uri) {
assertBackgroundThread();
switch (getUriType(uri)) {
case URI_TYPE_FILE:
return new File(uri.getPath());
case URI_TYPE_CONTENT: {
Cursor cursor = contentResolver.query(uri, LOCAL_FILE_PROJECTION, null, null, null);
if (cursor != null) {
try {
int columnIndex = cursor.getColumnIndex(LOCAL_FILE_PROJECTION[0]);
if (columnIndex != -1 && cursor.getCount() > 0) {
cursor.moveToFirst();
String realPath = cursor.getString(columnIndex);
if (realPath != null) {
return new File(realPath);
}
}
} finally {
cursor.close();
}
}
}
}
return null;
}
public String getMimeType(Uri uri) {
switch (getUriType(uri)) {
case URI_TYPE_FILE:
case URI_TYPE_ASSET:
return getMimeTypeFromPath(uri.getPath());
case URI_TYPE_CONTENT:
case URI_TYPE_RESOURCE:
return contentResolver.getType(uri);
case URI_TYPE_DATA: {
return getDataUriMimeType(uri);
}
case URI_TYPE_HTTP:
case URI_TYPE_HTTPS: {
try {
HttpURLConnection conn = (HttpURLConnection)new URL(uri.toString()).openConnection();
conn.setDoInput(false);
conn.setRequestMethod("HEAD");
String mimeType = conn.getHeaderField("Content-Type");
if (mimeType != null) {
mimeType = mimeType.split(";")[0];
}
return mimeType;
} catch (IOException e) {
}
}
}
return null;
}
//This already exists
private String getMimeTypeFromPath(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";
} else if (extension.equals("js")) {
// Missing from the map :(.
return "text/javascript";
}
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
/**
* Opens a stream to the given URI, also providing the MIME type & length.
* @return Never returns null.
* @throws Throws an InvalidArgumentException for relative URIs. Relative URIs should be
* resolved before being passed into this function.
* @throws Throws an IOException if the URI cannot be opened.
* @throws Throws an IllegalStateException if called on a foreground thread.
*/
public OpenForReadResult openForRead(Uri uri) throws IOException {
return openForRead(uri, false);
}
/**
* Opens a stream to the given URI, also providing the MIME type & length.
* @return Never returns null.
* @throws Throws an InvalidArgumentException for relative URIs. Relative URIs should be
* resolved before being passed into this function.
* @throws Throws an IOException if the URI cannot be opened.
* @throws Throws an IllegalStateException if called on a foreground thread and skipThreadCheck is false.
*/
public OpenForReadResult openForRead(Uri uri, boolean skipThreadCheck) throws IOException {
if (!skipThreadCheck) {
assertBackgroundThread();
}
switch (getUriType(uri)) {
case URI_TYPE_FILE: {
FileInputStream inputStream = new FileInputStream(uri.getPath());
String mimeType = getMimeTypeFromPath(uri.getPath());
long length = inputStream.getChannel().size();
return new OpenForReadResult(uri, inputStream, mimeType, length, null);
}
case URI_TYPE_ASSET: {
String assetPath = uri.getPath().substring(15);
AssetFileDescriptor assetFd = null;
InputStream inputStream;
long length = -1;
try {
assetFd = assetManager.openFd(assetPath);
inputStream = assetFd.createInputStream();
length = assetFd.getLength();
} catch (FileNotFoundException e) {
// Will occur if the file is compressed.
inputStream = assetManager.open(assetPath);
}
String mimeType = getMimeTypeFromPath(assetPath);
return new OpenForReadResult(uri, inputStream, mimeType, length, assetFd);
}
case URI_TYPE_CONTENT:
case URI_TYPE_RESOURCE: {
String mimeType = contentResolver.getType(uri);
AssetFileDescriptor assetFd = contentResolver.openAssetFileDescriptor(uri, "r");
InputStream inputStream = assetFd.createInputStream();
long length = assetFd.getLength();
return new OpenForReadResult(uri, inputStream, mimeType, length, assetFd);
}
case URI_TYPE_DATA: {
OpenForReadResult ret = readDataUri(uri);
if (ret == null) {
break;
}
return ret;
}
case URI_TYPE_HTTP:
case URI_TYPE_HTTPS: {
HttpURLConnection conn = (HttpURLConnection)new URL(uri.toString()).openConnection();
conn.setDoInput(true);
String mimeType = conn.getHeaderField("Content-Type");
if (mimeType != null) {
mimeType = mimeType.split(";")[0];
}
int length = conn.getContentLength();
InputStream inputStream = conn.getInputStream();
return new OpenForReadResult(uri, inputStream, mimeType, length, null);
}
case URI_TYPE_PLUGIN: {
String pluginId = uri.getHost();
CordovaPlugin plugin = pluginManager.getPlugin(pluginId);
if (plugin == null) {
throw new FileNotFoundException("Invalid plugin ID in URI: " + uri);
}
return plugin.handleOpenForRead(uri);
}
}
throw new FileNotFoundException("URI not supported by CordovaResourceApi: " + uri);
}
public OutputStream openOutputStream(Uri uri) throws IOException {
return openOutputStream(uri, false);
}
/**
* Opens a stream to the given URI.
* @return Never returns null.
* @throws Throws an InvalidArgumentException for relative URIs. Relative URIs should be
* resolved before being passed into this function.
* @throws Throws an IOException if the URI cannot be opened.
*/
public OutputStream openOutputStream(Uri uri, boolean append) throws IOException {
assertBackgroundThread();
switch (getUriType(uri)) {
case URI_TYPE_FILE: {
File localFile = new File(uri.getPath());
File parent = localFile.getParentFile();
if (parent != null) {
parent.mkdirs();
}
return new FileOutputStream(localFile, append);
}
case URI_TYPE_CONTENT:
case URI_TYPE_RESOURCE: {
AssetFileDescriptor assetFd = contentResolver.openAssetFileDescriptor(uri, append ? "wa" : "w");
return assetFd.createOutputStream();
}
}
throw new FileNotFoundException("URI not supported by CordovaResourceApi: " + uri);
}
public HttpURLConnection createHttpConnection(Uri uri) throws IOException {
assertBackgroundThread();
return (HttpURLConnection)new URL(uri.toString()).openConnection();
}
// Copies the input to the output in the most efficient manner possible.
// Closes both streams.
public void copyResource(OpenForReadResult input, OutputStream outputStream) throws IOException {
assertBackgroundThread();
try {
InputStream inputStream = input.inputStream;
if (inputStream instanceof FileInputStream && outputStream instanceof FileOutputStream) {
FileChannel inChannel = ((FileInputStream)input.inputStream).getChannel();
FileChannel outChannel = ((FileOutputStream)outputStream).getChannel();
long offset = 0;
long length = input.length;
if (input.assetFd != null) {
offset = input.assetFd.getStartOffset();
}
// transferFrom()'s 2nd arg is a relative position. Need to set the absolute
// position first.
inChannel.position(offset);
outChannel.transferFrom(inChannel, 0, length);
} else {
final int BUFFER_SIZE = 8192;
byte[] buffer = new byte[BUFFER_SIZE];
for (;;) {
int bytesRead = inputStream.read(buffer, 0, BUFFER_SIZE);
if (bytesRead <= 0) {
break;
}
outputStream.write(buffer, 0, bytesRead);
}
}
} finally {
input.inputStream.close();
if (outputStream != null) {
outputStream.close();
}
}
}
public void copyResource(Uri sourceUri, OutputStream outputStream) throws IOException {
copyResource(openForRead(sourceUri), outputStream);
}
// Added in 3.5.0.
public void copyResource(Uri sourceUri, Uri dstUri) throws IOException {
copyResource(openForRead(sourceUri), openOutputStream(dstUri));
}
private void assertBackgroundThread() {
if (threadCheckingEnabled) {
Thread curThread = Thread.currentThread();
if (curThread == Looper.getMainLooper().getThread()) {
throw new IllegalStateException("Do not perform IO operations on the UI thread. Use CordovaInterface.getThreadPool() instead.");
}
if (curThread == jsThread) {
throw new IllegalStateException("Tried to perform an IO operation on the WebCore thread. Use CordovaInterface.getThreadPool() instead.");
}
}
}
private String getDataUriMimeType(Uri uri) {
String uriAsString = uri.getSchemeSpecificPart();
int commaPos = uriAsString.indexOf(',');
if (commaPos == -1) {
return null;
}
String[] mimeParts = uriAsString.substring(0, commaPos).split(";");
if (mimeParts.length > 0) {
return mimeParts[0];
}
return null;
}
private OpenForReadResult readDataUri(Uri uri) {
String uriAsString = uri.getSchemeSpecificPart();
int commaPos = uriAsString.indexOf(',');
if (commaPos == -1) {
return null;
}
String[] mimeParts = uriAsString.substring(0, commaPos).split(";");
String contentType = null;
boolean base64 = false;
if (mimeParts.length > 0) {
contentType = mimeParts[0];
}
for (int i = 1; i < mimeParts.length; ++i) {
if ("base64".equalsIgnoreCase(mimeParts[i])) {
base64 = true;
}
}
String dataPartAsString = uriAsString.substring(commaPos + 1);
byte[] data;
if (base64) {
data = Base64.decode(dataPartAsString, Base64.DEFAULT);
} else {
try {
data = dataPartAsString.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
data = dataPartAsString.getBytes();
}
}
InputStream inputStream = new ByteArrayInputStream(data);
return new OpenForReadResult(uri, inputStream, contentType, data.length, null);
}
private static void assertNonRelative(Uri uri) {
if (!uri.isAbsolute()) {
throw new IllegalArgumentException("Relative URIs are not supported.");
}
}
public static final class OpenForReadResult {
public final Uri uri;
public final InputStream inputStream;
public final String mimeType;
public final long length;
public final AssetFileDescriptor assetFd;
public OpenForReadResult(Uri uri, InputStream inputStream, String mimeType, long length, AssetFileDescriptor assetFd) {
this.uri = uri;
this.inputStream = inputStream;
this.mimeType = mimeType;
this.length = length;
this.assetFd = assetFd;
}
}
}

View file

@ -0,0 +1,142 @@
/*
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.List;
import java.util.Map;
import android.content.Context;
import android.content.Intent;
import android.view.View;
import android.webkit.WebChromeClient.CustomViewCallback;
/**
* Main interface for interacting with a Cordova webview - implemented by CordovaWebViewImpl.
* This is an interface so that it can be easily mocked in tests.
* Methods may be added to this interface without a major version bump, as plugins & embedders
* are not expected to implement it.
*/
public interface CordovaWebView {
public static final String CORDOVA_VERSION = "7.1.0";
void init(CordovaInterface cordova, List<PluginEntry> pluginEntries, CordovaPreferences preferences);
boolean isInitialized();
View getView();
void loadUrlIntoView(String url, boolean recreatePlugins);
void stopLoading();
boolean canGoBack();
void clearCache();
/** Use parameter-less overload */
@Deprecated
void clearCache(boolean b);
void clearHistory();
boolean backHistory();
void handlePause(boolean keepRunning);
void onNewIntent(Intent intent);
void handleResume(boolean keepRunning);
void handleStart();
void handleStop();
void handleDestroy();
/**
* 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.
* To do this:
* 1. Within plugin.xml (to have your JS run before deviceready):
* <js-module><runs/></js-module>
* 2. Within your .js (call exec on start-up):
* require('cordova/channel').onCordovaReady.subscribe(function() {
* require('cordova/exec')(win, null, 'Plugin', 'method', []);
* function win(message) {
* ... process message from java here ...
* }
* });
* 3. Within your .java:
* PluginResult dataResult = new PluginResult(PluginResult.Status.OK, CODE);
* dataResult.setKeepCallback(true);
* savedCallbackContext.sendPluginResult(dataResult);
*/
@Deprecated
void sendJavascript(String statememt);
/**
* Load the specified URL in the Cordova webview or a new browser instance.
*
* NOTE: If openExternal is false, only whitelisted URLs 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
*/
void showWebPage(String url, boolean openExternal, boolean clearHistory, Map<String, Object> params);
/**
* Deprecated in 4.0.0. Use your own View-toggling logic.
*/
@Deprecated
boolean isCustomViewShowing();
/**
* Deprecated in 4.0.0. Use your own View-toggling logic.
*/
@Deprecated
void showCustomView(View view, CustomViewCallback callback);
/**
* Deprecated in 4.0.0. Use your own View-toggling logic.
*/
@Deprecated
void hideCustomView();
CordovaResourceApi getResourceApi();
void setButtonPlumbedToJs(int keyCode, boolean override);
boolean isButtonPlumbedToJs(int keyCode);
void sendPluginResult(PluginResult cr, String callbackId);
PluginManager getPluginManager();
CordovaWebViewEngine getEngine();
CordovaPreferences getPreferences();
ICordovaCookieManager getCookieManager();
String getUrl();
// TODO: Work on deleting these by removing refs from plugins.
Context getContext();
void loadUrl(String url);
Object postMessage(String id, Object data);
}

View file

@ -0,0 +1,85 @@
/*
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.view.KeyEvent;
import android.view.View;
import android.webkit.ValueCallback;
/**
* Interface for all Cordova engines.
* No methods will be added to this class (in order to be compatible with existing engines).
* Instead, we will create a new interface: e.g. CordovaWebViewEngineV2
*/
public interface CordovaWebViewEngine {
void init(CordovaWebView parentWebView, CordovaInterface cordova, Client client,
CordovaResourceApi resourceApi, PluginManager pluginManager,
NativeToJsMessageQueue nativeToJsMessageQueue);
CordovaWebView getCordovaWebView();
ICordovaCookieManager getCookieManager();
View getView();
void loadUrl(String url, boolean clearNavigationStack);
void stopLoading();
/** Return the currently loaded URL */
String getUrl();
void clearCache();
/** After calling clearHistory(), canGoBack() should be false. */
void clearHistory();
boolean canGoBack();
/** Returns whether a navigation occurred */
boolean goBack();
/** Pauses / resumes the WebView's event loop. */
void setPaused(boolean value);
/** Clean up all resources associated with the WebView. */
void destroy();
/** Add the evaulate Javascript method **/
void evaluateJavascript(String js, ValueCallback<String> callback);
/**
* Used to retrieve the associated CordovaWebView given a View without knowing the type of Engine.
* E.g. ((CordovaWebView.EngineView)activity.findViewById(android.R.id.webView)).getCordovaWebView();
*/
public interface EngineView {
CordovaWebView getCordovaWebView();
}
/**
* Contains methods that an engine uses to communicate with the parent CordovaWebView.
* Methods may be added in future cordova versions, but never removed.
*/
public interface Client {
Boolean onDispatchKeyEvent(KeyEvent event);
void clearLoadTimeoutTimer();
void onPageStarted(String newUrl);
void onReceivedError(int errorCode, String description, String failingUrl);
void onPageFinishedLoading(String url);
boolean onNavigationAttempt(String url);
}
}

View file

@ -0,0 +1,615 @@
/*
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.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebChromeClient;
import android.widget.FrameLayout;
import org.apache.cordova.engine.SystemWebViewEngine;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Main class for interacting with a Cordova webview. Manages plugins, events, and a CordovaWebViewEngine.
* Class uses two-phase initialization. You must call init() before calling any other methods.
*/
public class CordovaWebViewImpl implements CordovaWebView {
public static final String TAG = "CordovaWebViewImpl";
private PluginManager pluginManager;
protected final CordovaWebViewEngine engine;
private CordovaInterface cordova;
// Flag to track that a loadUrl timeout occurred
private int loadUrlTimeout = 0;
private CordovaResourceApi resourceApi;
private CordovaPreferences preferences;
private CoreAndroid appPlugin;
private NativeToJsMessageQueue nativeToJsMessageQueue;
private EngineClient engineClient = new EngineClient();
private boolean hasPausedEver;
// The URL passed to loadUrl(), not necessarily the URL of the current page.
String loadedUrl;
/** custom view created by the browser (a video player for example) */
private View mCustomView;
private WebChromeClient.CustomViewCallback mCustomViewCallback;
private Set<Integer> boundKeyCodes = new HashSet<Integer>();
public static CordovaWebViewEngine createEngine(Context context, CordovaPreferences preferences) {
String className = preferences.getString("webview", SystemWebViewEngine.class.getCanonicalName());
try {
Class<?> webViewClass = Class.forName(className);
Constructor<?> constructor = webViewClass.getConstructor(Context.class, CordovaPreferences.class);
return (CordovaWebViewEngine) constructor.newInstance(context, preferences);
} catch (Exception e) {
throw new RuntimeException("Failed to create webview. ", e);
}
}
public CordovaWebViewImpl(CordovaWebViewEngine cordovaWebViewEngine) {
this.engine = cordovaWebViewEngine;
}
// Convenience method for when creating programmatically (not from Config.xml).
public void init(CordovaInterface cordova) {
init(cordova, new ArrayList<PluginEntry>(), new CordovaPreferences());
}
@SuppressLint("Assert")
@Override
public void init(CordovaInterface cordova, List<PluginEntry> pluginEntries, CordovaPreferences preferences) {
if (this.cordova != null) {
throw new IllegalStateException();
}
this.cordova = cordova;
this.preferences = preferences;
pluginManager = new PluginManager(this, this.cordova, pluginEntries);
resourceApi = new CordovaResourceApi(engine.getView().getContext(), pluginManager);
nativeToJsMessageQueue = new NativeToJsMessageQueue();
nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.NoOpBridgeMode());
nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.LoadUrlBridgeMode(engine, cordova));
if (preferences.getBoolean("DisallowOverscroll", false)) {
engine.getView().setOverScrollMode(View.OVER_SCROLL_NEVER);
}
engine.init(this, cordova, engineClient, resourceApi, pluginManager, nativeToJsMessageQueue);
// This isn't enforced by the compiler, so assert here.
assert engine.getView() instanceof CordovaWebViewEngine.EngineView;
pluginManager.addService(CoreAndroid.PLUGIN_NAME, "org.apache.cordova.CoreAndroid");
pluginManager.init();
}
@Override
public boolean isInitialized() {
return cordova != null;
}
@Override
public void loadUrlIntoView(final String url, boolean recreatePlugins) {
LOG.d(TAG, ">>> loadUrl(" + url + ")");
if (url.equals("about:blank") || url.startsWith("javascript:")) {
engine.loadUrl(url, false);
return;
}
recreatePlugins = recreatePlugins || (loadedUrl == null);
if (recreatePlugins) {
// Don't re-initialize on first load.
if (loadedUrl != null) {
appPlugin = null;
pluginManager.init();
}
loadedUrl = url;
}
// Create a timeout timer for loadUrl
final int currentLoadUrlTimeout = loadUrlTimeout;
final int loadUrlTimeoutValue = preferences.getInteger("LoadUrlTimeoutValue", 20000);
// Timeout error method
final Runnable loadError = new Runnable() {
public void run() {
stopLoading();
LOG.e(TAG, "CordovaWebView: TIMEOUT ERROR!");
// Handle other errors by passing them to the webview in JS
JSONObject data = new JSONObject();
try {
data.put("errorCode", -6);
data.put("description", "The connection to the server was unsuccessful.");
data.put("url", url);
} catch (JSONException e) {
// Will never happen.
}
pluginManager.postMessage("onReceivedError", data);
}
};
// 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 (loadUrlTimeout == currentLoadUrlTimeout) {
cordova.getActivity().runOnUiThread(loadError);
}
}
};
final boolean _recreatePlugins = recreatePlugins;
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
if (loadUrlTimeoutValue > 0) {
cordova.getThreadPool().execute(timeoutCheck);
}
engine.loadUrl(url, _recreatePlugins);
}
});
}
@Override
public void loadUrl(String url) {
loadUrlIntoView(url, true);
}
@Override
public void showWebPage(String url, boolean openExternal, boolean clearHistory, Map<String, Object> params) {
LOG.d(TAG, "showWebPage(%s, %b, %b, HashMap)", url, openExternal, clearHistory);
// If clearing history
if (clearHistory) {
engine.clearHistory();
}
// If loading into our webview
if (!openExternal) {
// Make sure url is in whitelist
if (pluginManager.shouldAllowNavigation(url)) {
// TODO: What about params?
// Load new URL
loadUrlIntoView(url, true);
} else {
LOG.w(TAG, "showWebPage: Refusing to load URL into webview since it is not in the <allow-navigation> whitelist. URL=" + url);
}
}
if (!pluginManager.shouldOpenExternalUrl(url)) {
LOG.w(TAG, "showWebPage: Refusing to send intent for URL since it is not in the <allow-intent> whitelist. URL=" + url);
return;
}
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
// To send an intent without CATEGORY_BROWSER, a custom plugin should be used.
intent.addCategory(Intent.CATEGORY_BROWSABLE);
Uri uri = Uri.parse(url);
// 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.
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);
}
}
@Override
@Deprecated
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) engine.getView().getParent();
parent.addView(view, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
Gravity.CENTER));
// Hide the content view.
engine.getView().setVisibility(View.GONE);
// Finally show the custom view container.
parent.setVisibility(View.VISIBLE);
parent.bringToFront();
}
@Override
@Deprecated
public void hideCustomView() {
// This code is adapted from the original Android Browser code, licensed under the Apache License, Version 2.0
if (mCustomView == null) return;
LOG.d(TAG, "Hiding Custom View");
// Hide the custom view.
mCustomView.setVisibility(View.GONE);
// Remove the custom view from its container.
ViewGroup parent = (ViewGroup) engine.getView().getParent();
parent.removeView(mCustomView);
mCustomView = null;
mCustomViewCallback.onCustomViewHidden();
// Show the content view.
engine.getView().setVisibility(View.VISIBLE);
}
@Override
@Deprecated
public boolean isCustomViewShowing() {
return mCustomView != null;
}
@Override
@Deprecated
public void sendJavascript(String statement) {
nativeToJsMessageQueue.addJavaScript(statement);
}
@Override
public void sendPluginResult(PluginResult cr, String callbackId) {
nativeToJsMessageQueue.addPluginResult(cr, callbackId);
}
@Override
public PluginManager getPluginManager() {
return pluginManager;
}
@Override
public CordovaPreferences getPreferences() {
return preferences;
}
@Override
public ICordovaCookieManager getCookieManager() {
return engine.getCookieManager();
}
@Override
public CordovaResourceApi getResourceApi() {
return resourceApi;
}
@Override
public CordovaWebViewEngine getEngine() {
return engine;
}
@Override
public View getView() {
return engine.getView();
}
@Override
public Context getContext() {
return engine.getView().getContext();
}
private void sendJavascriptEvent(String event) {
if (appPlugin == null) {
appPlugin = (CoreAndroid)pluginManager.getPlugin(CoreAndroid.PLUGIN_NAME);
}
if (appPlugin == null) {
LOG.w(TAG, "Unable to fire event without existing plugin");
return;
}
appPlugin.fireJavascriptEvent(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:
case KeyEvent.KEYCODE_MENU:
// 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);
}
@Override
public Object postMessage(String id, Object data) {
return pluginManager.postMessage(id, data);
}
// Engine method proxies:
@Override
public String getUrl() {
return engine.getUrl();
}
@Override
public void stopLoading() {
// Clear timeout flag
loadUrlTimeout++;
}
@Override
public boolean canGoBack() {
return engine.canGoBack();
}
@Override
public void clearCache() {
engine.clearCache();
}
@Override
@Deprecated
public void clearCache(boolean b) {
engine.clearCache();
}
@Override
public void clearHistory() {
engine.clearHistory();
}
@Override
public boolean backHistory() {
return engine.goBack();
}
/////// LifeCycle methods ///////
@Override
public void onNewIntent(Intent intent) {
if (this.pluginManager != null) {
this.pluginManager.onNewIntent(intent);
}
}
@Override
public void handlePause(boolean keepRunning) {
if (!isInitialized()) {
return;
}
hasPausedEver = true;
pluginManager.onPause(keepRunning);
sendJavascriptEvent("pause");
// If app doesn't want to run in background
if (!keepRunning) {
// Pause JavaScript timers. This affects all webviews within the app!
engine.setPaused(true);
}
}
@Override
public void handleResume(boolean keepRunning) {
if (!isInitialized()) {
return;
}
// Resume JavaScript timers. This affects all webviews within the app!
engine.setPaused(false);
this.pluginManager.onResume(keepRunning);
// In order to match the behavior of the other platforms, we only send onResume after an
// onPause has occurred. The resume event might still be sent if the Activity was killed
// while waiting for the result of an external Activity once the result is obtained
if (hasPausedEver) {
sendJavascriptEvent("resume");
}
}
@Override
public void handleStart() {
if (!isInitialized()) {
return;
}
pluginManager.onStart();
}
@Override
public void handleStop() {
if (!isInitialized()) {
return;
}
pluginManager.onStop();
}
@Override
public void handleDestroy() {
if (!isInitialized()) {
return;
}
// Cancel pending timeout timer.
loadUrlTimeout++;
// Forward to plugins
this.pluginManager.onDestroy();
// TODO: about:blank is a bit special (and the default URL for new frames)
// We should use a blank data: url instead so it's more obvious
this.loadUrl("about:blank");
// TODO: Should not destroy webview until after about:blank is done loading.
engine.destroy();
hideCustomView();
}
protected class EngineClient implements CordovaWebViewEngine.Client {
@Override
public void clearLoadTimeoutTimer() {
loadUrlTimeout++;
}
@Override
public void onPageStarted(String newUrl) {
LOG.d(TAG, "onPageDidNavigate(" + newUrl + ")");
boundKeyCodes.clear();
pluginManager.onReset();
pluginManager.postMessage("onPageStarted", newUrl);
}
@Override
public void onReceivedError(int errorCode, String description, String failingUrl) {
clearLoadTimeoutTimer();
JSONObject data = new JSONObject();
try {
data.put("errorCode", errorCode);
data.put("description", description);
data.put("url", failingUrl);
} catch (JSONException e) {
e.printStackTrace();
}
pluginManager.postMessage("onReceivedError", data);
}
@Override
public void onPageFinishedLoading(String url) {
LOG.d(TAG, "onPageFinished(" + url + ")");
clearLoadTimeoutTimer();
// Broadcast message that page has loaded
pluginManager.postMessage("onPageFinished", url);
// Make app visible after 2 sec in case there was a JS error and Cordova JS never initialized correctly
if (engine.getView().getVisibility() != View.VISIBLE) {
Thread t = new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(2000);
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
pluginManager.postMessage("spinner", "stop");
}
});
} catch (InterruptedException e) {
}
}
});
t.start();
}
// Shutdown if blank loaded
if (url.equals("about:blank")) {
pluginManager.postMessage("exit", null);
}
}
@Override
public Boolean onDispatchKeyEvent(KeyEvent event) {
int keyCode = event.getKeyCode();
boolean isBackButton = keyCode == KeyEvent.KEYCODE_BACK;
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (isBackButton && mCustomView != null) {
return true;
} else if (boundKeyCodes.contains(keyCode)) {
return true;
} else if (isBackButton) {
return engine.canGoBack();
}
} else if (event.getAction() == KeyEvent.ACTION_UP) {
if (isBackButton && mCustomView != null) {
hideCustomView();
return true;
} else if (boundKeyCodes.contains(keyCode)) {
String eventName = null;
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_DOWN:
eventName = "volumedownbutton";
break;
case KeyEvent.KEYCODE_VOLUME_UP:
eventName = "volumeupbutton";
break;
case KeyEvent.KEYCODE_SEARCH:
eventName = "searchbutton";
break;
case KeyEvent.KEYCODE_MENU:
eventName = "menubutton";
break;
case KeyEvent.KEYCODE_BACK:
eventName = "backbutton";
break;
}
if (eventName != null) {
sendJavascriptEvent(eventName);
return true;
}
} else if (isBackButton) {
return engine.goBack();
}
}
return null;
}
@Override
public boolean onNavigationAttempt(String url) {
// Give plugins the chance to handle the url
if (pluginManager.onOverrideUrlLoading(url)) {
return true;
} else if (pluginManager.shouldAllowNavigation(url)) {
return false;
} else if (pluginManager.shouldOpenExternalUrl(url)) {
showWebPage(url, true, false, null);
return true;
}
LOG.w(TAG, "Blocked (possibly sub-frame) navigation to non-allowed URL: " + url);
return true;
}
}
}

View file

@ -0,0 +1,390 @@
/*
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 org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.telephony.TelephonyManager;
import android.view.KeyEvent;
import java.lang.reflect.Field;
import java.util.HashMap;
/**
* This class exposes methods in Cordova that can be called from JavaScript.
*/
public class CoreAndroid extends CordovaPlugin {
public static final String PLUGIN_NAME = "CoreAndroid";
protected static final String TAG = "CordovaApp";
private BroadcastReceiver telephonyReceiver;
private CallbackContext messageChannel;
private PluginResult pendingResume;
private final Object messageChannelLock = new Object();
/**
* 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
* get file paths associated with the Activity.
*/
@Override
public void pluginInitialize() {
this.initTelephonyReceiver();
}
/**
* Executes the request and returns PluginResult.
*
* @param action The action to execute.
* @param args JSONArry of arguments for the plugin.
* @param callbackContext The callback context from which we were invoked.
* @return A PluginResult object with a status and message.
*/
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
PluginResult.Status status = PluginResult.Status.OK;
String result = "";
try {
if (action.equals("clearCache")) {
this.clearCache();
}
else if (action.equals("show")) {
// This gets called from JavaScript onCordovaReady to show the webview.
// I recommend we change the name of the Message as spinner/stop is not
// indicative of what this actually does (shows the webview).
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
webView.getPluginManager().postMessage("spinner", "stop");
}
});
}
else if (action.equals("loadUrl")) {
this.loadUrl(args.getString(0), args.optJSONObject(1));
}
else if (action.equals("cancelLoadUrl")) {
//this.cancelLoadUrl();
}
else if (action.equals("clearHistory")) {
this.clearHistory();
}
else if (action.equals("backHistory")) {
this.backHistory();
}
else if (action.equals("overrideButton")) {
this.overrideButton(args.getString(0), args.getBoolean(1));
}
else if (action.equals("overrideBackbutton")) {
this.overrideBackbutton(args.getBoolean(0));
}
else if (action.equals("exitApp")) {
this.exitApp();
}
else if (action.equals("messageChannel")) {
synchronized(messageChannelLock) {
messageChannel = callbackContext;
if (pendingResume != null) {
sendEventMessage(pendingResume);
pendingResume = null;
}
}
return true;
}
callbackContext.sendPluginResult(new PluginResult(status, result));
return true;
} catch (JSONException e) {
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION));
return false;
}
}
//--------------------------------------------------------------------------
// LOCAL METHODS
//--------------------------------------------------------------------------
/**
* Clear the resource cache.
*/
public void clearCache() {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
webView.clearCache(true);
}
});
}
/**
* Load the url into the webview.
*
* @param url
* @param props Properties that can be passed in to the Cordova activity (i.e. loadingDialog, wait, ...)
* @throws JSONException
*/
public void loadUrl(String url, JSONObject props) throws JSONException {
LOG.d("App", "App.loadUrl("+url+","+props+")");
int wait = 0;
boolean openExternal = false;
boolean clearHistory = false;
// If there are properties, then set them on the Activity
HashMap<String, Object> params = new HashMap<String, Object>();
if (props != null) {
JSONArray keys = props.names();
for (int i = 0; i < keys.length(); i++) {
String key = keys.getString(i);
if (key.equals("wait")) {
wait = props.getInt(key);
}
else if (key.equalsIgnoreCase("openexternal")) {
openExternal = props.getBoolean(key);
}
else if (key.equalsIgnoreCase("clearhistory")) {
clearHistory = props.getBoolean(key);
}
else {
Object value = props.get(key);
if (value == null) {
}
else if (value.getClass().equals(String.class)) {
params.put(key, (String)value);
}
else if (value.getClass().equals(Boolean.class)) {
params.put(key, (Boolean)value);
}
else if (value.getClass().equals(Integer.class)) {
params.put(key, (Integer)value);
}
}
}
}
// If wait property, then delay loading
if (wait > 0) {
try {
synchronized(this) {
this.wait(wait);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.webView.showWebPage(url, openExternal, clearHistory, params);
}
/**
* Clear page history for the app.
*/
public void clearHistory() {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
webView.clearHistory();
}
});
}
/**
* Go to previous page displayed.
* This is the same as pressing the backbutton on Android device.
*/
public void backHistory() {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
webView.backHistory();
}
});
}
/**
* Override the default behavior of the Android back button.
* If overridden, when the back button is pressed, the "backKeyDown" JavaScript event will be fired.
*
* @param override T=override, F=cancel override
*/
public void overrideBackbutton(boolean override) {
LOG.i("App", "WARNING: Back Button Default Behavior will be overridden. The backbutton event will be fired!");
webView.setButtonPlumbedToJs(KeyEvent.KEYCODE_BACK, override);
}
/**
* Override the default behavior of the Android volume buttons.
* If overridden, when the volume button is pressed, the "volume[up|down]button" JavaScript event will be fired.
*
* @param button volumeup, volumedown
* @param override T=override, F=cancel override
*/
public void overrideButton(String button, boolean override) {
LOG.i("App", "WARNING: Volume Button Default Behavior will be overridden. The volume event will be fired!");
if (button.equals("volumeup")) {
webView.setButtonPlumbedToJs(KeyEvent.KEYCODE_VOLUME_UP, override);
}
else if (button.equals("volumedown")) {
webView.setButtonPlumbedToJs(KeyEvent.KEYCODE_VOLUME_DOWN, override);
}
else if (button.equals("menubutton")) {
webView.setButtonPlumbedToJs(KeyEvent.KEYCODE_MENU, override);
}
}
/**
* Return whether the Android back button is overridden by the user.
*
* @return boolean
*/
public boolean isBackbuttonOverridden() {
return webView.isButtonPlumbedToJs(KeyEvent.KEYCODE_BACK);
}
/**
* Exit the Android application.
*/
public void exitApp() {
this.webView.getPluginManager().postMessage("exit", null);
}
/**
* Listen for telephony events: RINGING, OFFHOOK and IDLE
* Send these events to all plugins using
* CordovaActivity.onMessage("telephone", "ringing" | "offhook" | "idle")
*/
private void initTelephonyReceiver() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
//final CordovaInterface mycordova = this.cordova;
this.telephonyReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// If state has changed
if ((intent != null) && intent.getAction().equals(TelephonyManager.ACTION_PHONE_STATE_CHANGED)) {
if (intent.hasExtra(TelephonyManager.EXTRA_STATE)) {
String extraData = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
if (extraData.equals(TelephonyManager.EXTRA_STATE_RINGING)) {
LOG.i(TAG, "Telephone RINGING");
webView.getPluginManager().postMessage("telephone", "ringing");
}
else if (extraData.equals(TelephonyManager.EXTRA_STATE_OFFHOOK)) {
LOG.i(TAG, "Telephone OFFHOOK");
webView.getPluginManager().postMessage("telephone", "offhook");
}
else if (extraData.equals(TelephonyManager.EXTRA_STATE_IDLE)) {
LOG.i(TAG, "Telephone IDLE");
webView.getPluginManager().postMessage("telephone", "idle");
}
}
}
}
};
// Register the receiver
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);
}
sendEventMessage(new PluginResult(PluginResult.Status.OK, obj));
}
private void sendEventMessage(PluginResult payload) {
payload.setKeepCallback(true);
if (messageChannel != null) {
messageChannel.sendPluginResult(payload);
}
}
/*
* Unregister the receiver
*
*/
public void onDestroy()
{
webView.getContext().unregisterReceiver(this.telephonyReceiver);
}
/**
* Used to send the resume event in the case that the Activity is destroyed by the OS
*
* @param resumeEvent PluginResult containing the payload for the resume event to be fired
*/
public void sendResumeEvent(PluginResult resumeEvent) {
// This operation must be synchronized because plugin results that trigger resume
// events can be processed asynchronously
synchronized(messageChannelLock) {
if (messageChannel != null) {
sendEventMessage(resumeEvent);
} else {
// Might get called before the page loads, so we need to store it until the
// messageChannel gets created
this.pendingResume = resumeEvent;
}
}
}
/*
* This needs to be implemented if you wish to use the Camera Plugin or other plugins
* that read the Build Configuration.
*
* Thanks to Phil@Medtronic and Graham Borland for finding the answer and posting it to
* StackOverflow. This is annoying as hell!
*
*/
public static Object getBuildConfigValue(Context ctx, String key)
{
try
{
Class<?> clazz = Class.forName(ctx.getPackageName() + ".BuildConfig");
Field field = clazz.getField(key);
return field.get(null);
} catch (ClassNotFoundException e) {
LOG.d(TAG, "Unable to get the BuildConfig, is this built with ANT?");
e.printStackTrace();
} catch (NoSuchFieldException e) {
LOG.d(TAG, key + " is not a valid field. Check your build.gradle");
} catch (IllegalAccessException e) {
LOG.d(TAG, "Illegal Access Exception: Let's print a stack trace.");
e.printStackTrace();
}
return null;
}
}

View file

@ -0,0 +1,31 @@
/*
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 org.json.JSONException;
/*
* Any exposed Javascript API MUST implement these three things!
*/
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;
}

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,33 @@
/*
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;
public interface ICordovaCookieManager {
public void setCookiesEnabled(boolean accept);
public void setCookie(final String url, final String value);
public String getCookie(final String url);
public void clearCookies();
public void flush();
};

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

@ -0,0 +1,244 @@
/*
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.util.Log;
/**
* Log to Android logging system.
*
* Log message can be a string or a printf formatted string with arguments.
* See http://developer.android.com/reference/java/util/Formatter.html
*/
public class LOG {
public static final int VERBOSE = Log.VERBOSE;
public static final int DEBUG = Log.DEBUG;
public static final int INFO = Log.INFO;
public static final int WARN = Log.WARN;
public static final int ERROR = Log.ERROR;
// Current log level
public static int LOGLEVEL = Log.ERROR;
/**
* Set the current log level.
*
* @param logLevel
*/
public static void setLogLevel(int logLevel) {
LOGLEVEL = logLevel;
Log.i("CordovaLog", "Changing log level to " + logLevel);
}
/**
* Set the current log level.
*
* @param logLevel
*/
public static void setLogLevel(String logLevel) {
if ("VERBOSE".equals(logLevel)) LOGLEVEL = VERBOSE;
else if ("DEBUG".equals(logLevel)) LOGLEVEL = DEBUG;
else if ("INFO".equals(logLevel)) LOGLEVEL = INFO;
else if ("WARN".equals(logLevel)) LOGLEVEL = WARN;
else if ("ERROR".equals(logLevel)) LOGLEVEL = ERROR;
Log.i("CordovaLog", "Changing log level to " + logLevel + "(" + LOGLEVEL + ")");
}
/**
* Determine if log level will be logged
*
* @param logLevel
* @return true if the parameter passed in is greater than or equal to the current log level
*/
public static boolean isLoggable(int logLevel) {
return (logLevel >= LOGLEVEL);
}
/**
* Verbose log message.
*
* @param tag
* @param s
*/
public static void v(String tag, String s) {
if (LOG.VERBOSE >= LOGLEVEL) Log.v(tag, s);
}
/**
* Debug log message.
*
* @param tag
* @param s
*/
public static void d(String tag, String s) {
if (LOG.DEBUG >= LOGLEVEL) Log.d(tag, s);
}
/**
* Info log message.
*
* @param tag
* @param s
*/
public static void i(String tag, String s) {
if (LOG.INFO >= LOGLEVEL) Log.i(tag, s);
}
/**
* Warning log message.
*
* @param tag
* @param s
*/
public static void w(String tag, String s) {
if (LOG.WARN >= LOGLEVEL) Log.w(tag, s);
}
/**
* Error log message.
*
* @param tag
* @param s
*/
public static void e(String tag, String s) {
if (LOG.ERROR >= LOGLEVEL) Log.e(tag, s);
}
/**
* Verbose log message.
*
* @param tag
* @param s
* @param e
*/
public static void v(String tag, String s, Throwable e) {
if (LOG.VERBOSE >= LOGLEVEL) Log.v(tag, s, e);
}
/**
* Debug log message.
*
* @param tag
* @param s
* @param e
*/
public static void d(String tag, String s, Throwable e) {
if (LOG.DEBUG >= LOGLEVEL) Log.d(tag, s, e);
}
/**
* Info log message.
*
* @param tag
* @param s
* @param e
*/
public static void i(String tag, String s, Throwable e) {
if (LOG.INFO >= LOGLEVEL) Log.i(tag, s, e);
}
/**
* Warning log message.
*
* @param tag
* @param e
*/
public static void w(String tag, Throwable e) {
if (LOG.WARN >= LOGLEVEL) Log.w(tag, e);
}
/**
* Warning log message.
*
* @param tag
* @param s
* @param e
*/
public static void w(String tag, String s, Throwable e) {
if (LOG.WARN >= LOGLEVEL) Log.w(tag, s, e);
}
/**
* Error log message.
*
* @param tag
* @param s
* @param e
*/
public static void e(String tag, String s, Throwable e) {
if (LOG.ERROR >= LOGLEVEL) Log.e(tag, s, e);
}
/**
* Verbose log message with printf formatting.
*
* @param tag
* @param s
* @param args
*/
public static void v(String tag, String s, Object... args) {
if (LOG.VERBOSE >= LOGLEVEL) Log.v(tag, String.format(s, args));
}
/**
* Debug log message with printf formatting.
*
* @param tag
* @param s
* @param args
*/
public static void d(String tag, String s, Object... args) {
if (LOG.DEBUG >= LOGLEVEL) Log.d(tag, String.format(s, args));
}
/**
* Info log message with printf formatting.
*
* @param tag
* @param s
* @param args
*/
public static void i(String tag, String s, Object... args) {
if (LOG.INFO >= LOGLEVEL) Log.i(tag, String.format(s, args));
}
/**
* Warning log message with printf formatting.
*
* @param tag
* @param s
* @param args
*/
public static void w(String tag, String s, Object... args) {
if (LOG.WARN >= LOGLEVEL) Log.w(tag, String.format(s, args));
}
/**
* Error log message with printf formatting.
*
* @param tag
* @param s
* @param args
*/
public static void e(String tag, String s, Object... args) {
if (LOG.ERROR >= LOGLEVEL) Log.e(tag, String.format(s, args));
}
}

View file

@ -0,0 +1,542 @@
/*
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.LinkedList;
/**
* Holds the list of messages to be sent to the WebView.
*/
public class NativeToJsMessageQueue {
private static final String LOG_TAG = "JsMessageQueue";
// 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
// exec() is asynchronous. Set this to true when running bridge benchmarks.
static final boolean DISABLE_EXEC_CHAINING = false;
// Arbitrarily chosen upper limit for how much data to send to JS in one shot.
// This currently only chops up on message boundaries. It may be useful
// to allow it to break up messages.
private static int MAX_PAYLOAD_SIZE = 50 * 1024 * 10240;
/**
* When true, the active listener is not fired upon enqueue. When set to false,
* the active listener will be fired if the queue is non-empty.
*/
private boolean paused;
/**
* The list of JavaScript statements to be sent to JavaScript.
*/
private final LinkedList<JsMessage> queue = new LinkedList<JsMessage>();
/**
* The array of listeners that can be used to send messages to JS.
*/
private ArrayList<BridgeMode> bridgeModes = new ArrayList<BridgeMode>();
/**
* When null, the bridge is disabled. This occurs during page transitions.
* When disabled, all callbacks are dropped since they are assumed to be
* relevant to the previous page.
*/
private BridgeMode activeBridgeMode;
public void addBridgeMode(BridgeMode bridgeMode) {
bridgeModes.add(bridgeMode);
}
public boolean isBridgeEnabled() {
return activeBridgeMode != null;
}
public boolean isEmpty() {
return queue.isEmpty();
}
/**
* Changes the bridge mode.
*/
public void setBridgeMode(int value) {
if (value < -1 || value >= bridgeModes.size()) {
LOG.d(LOG_TAG, "Invalid NativeToJsBridgeMode: " + value);
} else {
BridgeMode newMode = value < 0 ? null : bridgeModes.get(value);
if (newMode != activeBridgeMode) {
LOG.d(LOG_TAG, "Set native->JS mode to " + (newMode == null ? "null" : newMode.getClass().getSimpleName()));
synchronized (this) {
activeBridgeMode = newMode;
if (newMode != null) {
newMode.reset();
if (!paused && !queue.isEmpty()) {
newMode.onNativeToJsMessageAvailable(this);
}
}
}
}
}
}
/**
* Clears all messages and resets to the default bridge mode.
*/
public void reset() {
synchronized (this) {
queue.clear();
setBridgeMode(-1);
}
}
private int calculatePackedMessageLength(JsMessage message) {
int messageLen = message.calculateEncodedLength();
String messageLenStr = String.valueOf(messageLen);
return messageLenStr.length() + messageLen + 1;
}
private void packMessage(JsMessage message, StringBuilder sb) {
int len = message.calculateEncodedLength();
sb.append(len)
.append(' ');
message.encodeAsMessage(sb);
}
/**
* Combines and returns queued messages combined into a single string.
* Combines as many messages as possible, while staying under MAX_PAYLOAD_SIZE.
* Returns null if the queue is empty.
*/
public String popAndEncode(boolean fromOnlineEvent) {
synchronized (this) {
if (activeBridgeMode == null) {
return null;
}
activeBridgeMode.notifyOfFlush(this, fromOnlineEvent);
if (queue.isEmpty()) {
return null;
}
int totalPayloadLen = 0;
int numMessagesToSend = 0;
for (JsMessage message : queue) {
int messageSize = calculatePackedMessageLength(message);
if (numMessagesToSend > 0 && totalPayloadLen + messageSize > MAX_PAYLOAD_SIZE && MAX_PAYLOAD_SIZE > 0) {
break;
}
totalPayloadLen += messageSize;
numMessagesToSend += 1;
}
StringBuilder sb = new StringBuilder(totalPayloadLen);
for (int i = 0; i < numMessagesToSend; ++i) {
JsMessage message = queue.removeFirst();
packMessage(message, sb);
}
if (!queue.isEmpty()) {
// Attach a char to indicate that there are more messages pending.
sb.append('*');
}
String ret = sb.toString();
return ret;
}
}
/**
* Same as popAndEncode(), except encodes in a form that can be executed as JS.
*/
public String popAndEncodeAsJs() {
synchronized (this) {
int length = queue.size();
if (length == 0) {
return null;
}
int totalPayloadLen = 0;
int numMessagesToSend = 0;
for (JsMessage message : queue) {
int messageSize = message.calculateEncodedLength() + 50; // overestimate.
if (numMessagesToSend > 0 && totalPayloadLen + messageSize > MAX_PAYLOAD_SIZE && MAX_PAYLOAD_SIZE > 0) {
break;
}
totalPayloadLen += messageSize;
numMessagesToSend += 1;
}
boolean willSendAllMessages = numMessagesToSend == queue.size();
StringBuilder sb = new StringBuilder(totalPayloadLen + (willSendAllMessages ? 0 : 100));
// Wrap each statement in a try/finally so that if one throws it does
// not affect the next.
for (int i = 0; i < numMessagesToSend; ++i) {
JsMessage message = queue.removeFirst();
if (willSendAllMessages && (i + 1 == numMessagesToSend)) {
message.encodeAsJsMessage(sb);
} else {
sb.append("try{");
message.encodeAsJsMessage(sb);
sb.append("}finally{");
}
}
if (!willSendAllMessages) {
sb.append("window.setTimeout(function(){cordova.require('cordova/plugin/android/polling').pollOnce();},0);");
}
for (int i = willSendAllMessages ? 1 : 0; i < numMessagesToSend; ++i) {
sb.append('}');
}
String ret = sb.toString();
return ret;
}
}
/**
* Add a JavaScript statement to the list.
*/
public void addJavaScript(String statement) {
enqueueMessage(new JsMessage(statement));
}
/**
* Add a JavaScript statement to the list.
*/
public void addPluginResult(PluginResult result, String callbackId) {
if (callbackId == null) {
LOG.e(LOG_TAG, "Got plugin result with no callbackId", new Throwable());
return;
}
// Don't send anything if there is no result and there is no need to
// clear the callbacks.
boolean noResult = result.getStatus() == PluginResult.Status.NO_RESULT.ordinal();
boolean keepCallback = result.getKeepCallback();
if (noResult && keepCallback) {
return;
}
JsMessage message = new JsMessage(result, callbackId);
if (FORCE_ENCODE_USING_EVAL) {
StringBuilder sb = new StringBuilder(message.calculateEncodedLength() + 50);
message.encodeAsJsMessage(sb);
message = new JsMessage(sb.toString());
}
enqueueMessage(message);
}
private void enqueueMessage(JsMessage message) {
synchronized (this) {
if (activeBridgeMode == null) {
LOG.d(LOG_TAG, "Dropping Native->JS message due to disabled bridge");
return;
}
queue.add(message);
if (!paused) {
activeBridgeMode.onNativeToJsMessageAvailable(this);
}
}
}
public void setPaused(boolean value) {
if (paused && value) {
// This should never happen. If a use-case for it comes up, we should
// change pause to be a counter.
LOG.e(LOG_TAG, "nested call to setPaused detected.", new Throwable());
}
paused = value;
if (!value) {
synchronized (this) {
if (!queue.isEmpty() && activeBridgeMode != null) {
activeBridgeMode.onNativeToJsMessageAvailable(this);
}
}
}
}
public static abstract class BridgeMode {
public abstract void onNativeToJsMessageAvailable(NativeToJsMessageQueue queue);
public void notifyOfFlush(NativeToJsMessageQueue queue, boolean fromOnlineEvent) {}
public void reset() {}
}
/** Uses JS polls for messages on a timer.. */
public static class NoOpBridgeMode extends BridgeMode {
@Override public void onNativeToJsMessageAvailable(NativeToJsMessageQueue queue) {
}
}
/** Uses webView.loadUrl("javascript:") to execute messages. */
public static class LoadUrlBridgeMode extends BridgeMode {
private final CordovaWebViewEngine engine;
private final CordovaInterface cordova;
public LoadUrlBridgeMode(CordovaWebViewEngine engine, CordovaInterface cordova) {
this.engine = engine;
this.cordova = cordova;
}
@Override
public void onNativeToJsMessageAvailable(final NativeToJsMessageQueue queue) {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
String js = queue.popAndEncodeAsJs();
if (js != null) {
engine.loadUrl("javascript:" + js, false);
}
}
});
}
}
/** Uses online/offline events to tell the JS when to poll for messages. */
public static class OnlineEventsBridgeMode extends BridgeMode {
private final OnlineEventsBridgeModeDelegate delegate;
private boolean online;
private boolean ignoreNextFlush;
public interface OnlineEventsBridgeModeDelegate {
void setNetworkAvailable(boolean value);
void runOnUiThread(Runnable r);
}
public OnlineEventsBridgeMode(OnlineEventsBridgeModeDelegate delegate) {
this.delegate = delegate;
}
@Override
public void reset() {
delegate.runOnUiThread(new Runnable() {
public void run() {
online = false;
// If the following call triggers a notifyOfFlush, then ignore it.
ignoreNextFlush = true;
delegate.setNetworkAvailable(true);
}
});
}
@Override
public void onNativeToJsMessageAvailable(final NativeToJsMessageQueue queue) {
delegate.runOnUiThread(new Runnable() {
public void run() {
if (!queue.isEmpty()) {
ignoreNextFlush = false;
delegate.setNetworkAvailable(online);
}
}
});
}
// Track when online/offline events are fired so that we don't fire excess events.
@Override
public void notifyOfFlush(final NativeToJsMessageQueue queue, boolean fromOnlineEvent) {
if (fromOnlineEvent && !ignoreNextFlush) {
online = !online;
}
}
}
/** Uses webView.evaluateJavascript to execute messages. */
public static class EvalBridgeMode extends BridgeMode {
private final CordovaWebViewEngine engine;
private final CordovaInterface cordova;
public EvalBridgeMode(CordovaWebViewEngine engine, CordovaInterface cordova) {
this.engine = engine;
this.cordova = cordova;
}
@Override
public void onNativeToJsMessageAvailable(final NativeToJsMessageQueue queue) {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
String js = queue.popAndEncodeAsJs();
if (js != null) {
engine.evaluateJavascript(js, null);
}
}
});
}
}
private static class JsMessage {
final String jsPayloadOrCallbackId;
final PluginResult pluginResult;
JsMessage(String js) {
if (js == null) {
throw new NullPointerException();
}
jsPayloadOrCallbackId = js;
pluginResult = null;
}
JsMessage(PluginResult pluginResult, String callbackId) {
if (callbackId == null || pluginResult == null) {
throw new NullPointerException();
}
jsPayloadOrCallbackId = callbackId;
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;
return ret + calculateEncodedLengthHelper(pluginResult);
}
static void encodeAsMessageHelper(StringBuilder sb, PluginResult pluginResult) {
switch (pluginResult.getMessageType()) {
case PluginResult.MESSAGE_TYPE_BOOLEAN:
sb.append(pluginResult.getMessage().charAt(0)); // t or f.
break;
case PluginResult.MESSAGE_TYPE_NULL: // N
sb.append('N');
break;
case PluginResult.MESSAGE_TYPE_NUMBER: // n
sb.append('n')
.append(pluginResult.getMessage());
break;
case PluginResult.MESSAGE_TYPE_STRING: // s
sb.append('s');
sb.append(pluginResult.getStrMessage());
break;
case PluginResult.MESSAGE_TYPE_BINARYSTRING: // S
sb.append('S');
sb.append(pluginResult.getMessage());
break;
case PluginResult.MESSAGE_TYPE_ARRAYBUFFER: // A
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 buildJsMessage(StringBuilder sb) {
switch (pluginResult.getMessageType()) {
case PluginResult.MESSAGE_TYPE_MULTIPART:
int size = pluginResult.getMultipartMessagesSize();
for (int i=0; i<size; i++) {
PluginResult subresult = pluginResult.getMultipartMessage(i);
JsMessage submessage = new JsMessage(subresult, jsPayloadOrCallbackId);
submessage.buildJsMessage(sb);
if (i < (size-1)) {
sb.append(",");
}
}
break;
case PluginResult.MESSAGE_TYPE_BINARYSTRING:
sb.append("atob('")
.append(pluginResult.getMessage())
.append("')");
break;
case PluginResult.MESSAGE_TYPE_ARRAYBUFFER:
sb.append("cordova.require('cordova/base64').toArrayBuffer('")
.append(pluginResult.getMessage())
.append("')");
break;
case PluginResult.MESSAGE_TYPE_NULL:
sb.append("null");
break;
default:
sb.append(pluginResult.getMessage());
}
}
void encodeAsJsMessage(StringBuilder sb) {
if (pluginResult == null) {
sb.append(jsPayloadOrCallbackId);
} else {
int status = pluginResult.getStatus();
boolean success = (status == PluginResult.Status.OK.ordinal()) || (status == PluginResult.Status.NO_RESULT.ordinal());
sb.append("cordova.callbackFromNative('")
.append(jsPayloadOrCallbackId)
.append("',")
.append(success)
.append(",")
.append(status)
.append(",[");
buildJsMessage(sb);
sb.append("],")
.append(pluginResult.getKeepCallback())
.append(");");
}
}
}
}

View file

@ -0,0 +1,87 @@
/*
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.Arrays;
import org.json.JSONException;
import android.content.pm.PackageManager;
/**
* This class provides reflective methods for permission requesting and checking so that plugins
* written for cordova-android 5.0.0+ can still compile with earlier cordova-android versions.
*/
public class PermissionHelper {
private static final String LOG_TAG = "CordovaPermissionHelper";
/**
* Requests a "dangerous" permission for the application at runtime. This is a helper method
* alternative to cordovaInterface.requestPermission() that does not require the project to be
* built with cordova-android 5.0.0+
*
* @param plugin The plugin the permission is being requested for
* @param requestCode A requestCode to be passed to the plugin's onRequestPermissionResult()
* along with the result of the permission request
* @param permission The permission to be requested
*/
public static void requestPermission(CordovaPlugin plugin, int requestCode, String permission) {
PermissionHelper.requestPermissions(plugin, requestCode, new String[] {permission});
}
/**
* Requests "dangerous" permissions for the application at runtime. This is a helper method
* alternative to cordovaInterface.requestPermissions() that does not require the project to be
* built with cordova-android 5.0.0+
*
* @param plugin The plugin the permissions are being requested for
* @param requestCode A requestCode to be passed to the plugin's onRequestPermissionResult()
* along with the result of the permissions request
* @param permissions The permissions to be requested
*/
public static void requestPermissions(CordovaPlugin plugin, int requestCode, String[] permissions) {
plugin.cordova.requestPermissions(plugin, requestCode, permissions);
}
/**
* Checks at runtime to see if the application has been granted a permission. This is a helper
* method alternative to cordovaInterface.hasPermission() that does not require the project to
* be built with cordova-android 5.0.0+
*
* @param plugin The plugin the permission is being checked against
* @param permission The permission to be checked
*
* @return True if the permission has already been granted and false otherwise
*/
public static boolean hasPermission(CordovaPlugin plugin, String permission) {
return plugin.cordova.hasPermission(permission);
}
private static void deliverPermissionResult(CordovaPlugin plugin, int requestCode, String[] permissions) {
// Generate the request results
int[] requestResults = new int[permissions.length];
Arrays.fill(requestResults, PackageManager.PERMISSION_GRANTED);
try {
plugin.onRequestPermissionResult(requestCode, permissions, requestResults);
} catch (JSONException e) {
LOG.e(LOG_TAG, "JSONException when delivering permissions results", e);
}
}
}

View file

@ -0,0 +1,70 @@
/*
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 org.apache.cordova.CordovaPlugin;
/**
* This class represents a service entry object.
*/
public final class PluginEntry {
/**
* The name of the service that this plugin implements
*/
public final String service;
/**
* The plugin class name that implements the service.
*/
public final String pluginClass;
/**
* The pre-instantiated plugin to use for this entry.
*/
public final CordovaPlugin plugin;
/**
* Flag that indicates the plugin object should be created when PluginManager is initialized.
*/
public final boolean onload;
/**
* Constructs with a CordovaPlugin already instantiated.
*/
public PluginEntry(String service, CordovaPlugin plugin) {
this(service, plugin.getClass().getName(), true, plugin);
}
/**
* @param service The name of the service
* @param pluginClass The plugin class name
* @param onload Create plugin object when HTML page is loaded
*/
public PluginEntry(String service, String pluginClass, boolean onload) {
this(service, pluginClass, onload, null);
}
private PluginEntry(String service, String pluginClass, boolean onload, CordovaPlugin plugin) {
this.service = service;
this.pluginClass = pluginClass;
this.onload = onload;
this.plugin = plugin;
}
}

View file

@ -0,0 +1,526 @@
/*
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.Collection;
import java.util.LinkedHashMap;
import org.json.JSONException;
import android.content.Intent;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.os.Debug;
/**
* PluginManager is exposed to JavaScript in the Cordova WebView.
*
* Calling native plugin code can be done by calling PluginManager.exec(...)
* from JavaScript.
*/
public class PluginManager {
private static String TAG = "PluginManager";
private static final int SLOW_EXEC_WARNING_THRESHOLD = Debug.isDebuggerConnected() ? 60 : 16;
// List of service entries
private final LinkedHashMap<String, CordovaPlugin> pluginMap = new LinkedHashMap<String, CordovaPlugin>();
private final LinkedHashMap<String, PluginEntry> entryMap = new LinkedHashMap<String, PluginEntry>();
private final CordovaInterface ctx;
private final CordovaWebView app;
private boolean isInitialized;
private CordovaPlugin permissionRequester;
public PluginManager(CordovaWebView cordovaWebView, CordovaInterface cordova, Collection<PluginEntry> pluginEntries) {
this.ctx = cordova;
this.app = cordovaWebView;
setPluginEntries(pluginEntries);
}
public Collection<PluginEntry> getPluginEntries() {
return entryMap.values();
}
public void setPluginEntries(Collection<PluginEntry> pluginEntries) {
if (isInitialized) {
this.onPause(false);
this.onDestroy();
pluginMap.clear();
entryMap.clear();
}
for (PluginEntry entry : pluginEntries) {
addService(entry);
}
if (isInitialized) {
startupPlugins();
}
}
/**
* Init when loading a new HTML page into webview.
*/
public void init() {
LOG.d(TAG, "init()");
isInitialized = true;
this.onPause(false);
this.onDestroy();
pluginMap.clear();
this.startupPlugins();
}
/**
* Create plugins objects that have onload set.
*/
private 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);
}
}
}
/**
* Receives a request for execution and fulfills it by finding the appropriate
* Java class and calling it's execute method.
*
* PluginManager.exec can be used either synchronously or async. In either case, a JSON encoded
* string is returned that will indicate if any errors have occurred when trying to find
* or execute the class denoted by the clazz argument.
*
* @param service String containing the service to run
* @param action String containing the action that the class is supposed to perform. This is
* passed to the plugin execute method and it is up to the plugin developer
* how to deal with it.
* @param callbackId String containing the id of the callback that is execute in JavaScript if
* this is an async plugin call.
* @param rawArgs An Array literal string containing any arguments needed in the
* plugin execute method.
*/
public void exec(final String service, final String action, final String callbackId, final String rawArgs) {
CordovaPlugin plugin = getPlugin(service);
if (plugin == null) {
LOG.d(TAG, "exec() call to unknown plugin: " + service);
PluginResult cr = new PluginResult(PluginResult.Status.CLASS_NOT_FOUND_EXCEPTION);
app.sendPluginResult(cr, callbackId);
return;
}
CallbackContext callbackContext = new CallbackContext(callbackId, app);
try {
long pluginStartTime = System.currentTimeMillis();
boolean wasValidAction = plugin.execute(action, rawArgs, callbackContext);
long duration = System.currentTimeMillis() - pluginStartTime;
if (duration > SLOW_EXEC_WARNING_THRESHOLD) {
LOG.w(TAG, "THREAD WARNING: exec() call to " + service + "." + action + " blocked the main thread for " + duration + "ms. Plugin should use CordovaInterface.getThreadPool().");
}
if (!wasValidAction) {
PluginResult cr = new PluginResult(PluginResult.Status.INVALID_ACTION);
callbackContext.sendPluginResult(cr);
}
} catch (JSONException e) {
PluginResult cr = new PluginResult(PluginResult.Status.JSON_EXCEPTION);
callbackContext.sendPluginResult(cr);
} catch (Exception e) {
LOG.e(TAG, "Uncaught exception from plugin", e);
callbackContext.error(e.getMessage());
}
}
/**
* Get the plugin object that implements the service.
* If the plugin object does not already exist, then create it.
* If the service doesn't exist, then return null.
*
* @param service The name of the service.
* @return CordovaPlugin or null
*/
public CordovaPlugin getPlugin(String service) {
CordovaPlugin ret = pluginMap.get(service);
if (ret == null) {
PluginEntry pe = entryMap.get(service);
if (pe == null) {
return null;
}
if (pe.plugin != null) {
ret = pe.plugin;
} else {
ret = instantiatePlugin(pe.pluginClass);
}
ret.privateInitialize(service, ctx, app, app.getPreferences());
pluginMap.put(service, ret);
}
return ret;
}
/**
* Add a plugin class that implements a service to the service entry table.
* This does not create the plugin object instance.
*
* @param service The service name
* @param className The plugin class name
*/
public void addService(String service, String className) {
PluginEntry entry = new PluginEntry(service, className, false);
this.addService(entry);
}
/**
* Add a plugin class that implements a service to the service entry table.
* This does not create the plugin object instance.
*
* @param entry The plugin entry
*/
public void addService(PluginEntry entry) {
this.entryMap.put(entry.service, entry);
if (entry.plugin != null) {
entry.plugin.privateInitialize(entry.service, ctx, app, app.getPreferences());
pluginMap.put(entry.service, entry.plugin);
}
}
/**
* Called when the system is about to start resuming a previous activity.
*
* @param multitasking Flag indicating if multitasking is turned on for app
*/
public void onPause(boolean multitasking) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
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(app, 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(app, request)) {
return true;
}
}
return false;
}
/**
* Called when the activity will start interacting with the user.
*
* @param multitasking Flag indicating if multitasking is turned on for app
*/
public void onResume(boolean multitasking) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onResume(multitasking);
}
}
}
/**
* Called when the activity is becoming visible to the user.
*/
public void onStart() {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onStart();
}
}
}
/**
* Called when the activity is no longer visible to the user.
*/
public void onStop() {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onStop();
}
}
}
/**
* The final call you receive before your activity is destroyed.
*/
public void onDestroy() {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onDestroy();
}
}
}
/**
* Send a message to all plugins.
*
* @param id The message id
* @param data The message data
* @return Object to stop propagation or null
*/
public Object postMessage(String id, Object data) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
Object obj = plugin.onMessage(id, data);
if (obj != null) {
return obj;
}
}
}
return ctx.onMessage(id, data);
}
/**
* Called when the activity receives a new intent.
*/
public void onNewIntent(Intent intent) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onNewIntent(intent);
}
}
}
/**
* Called when the webview is going to request an external resource.
*
* This delegates to the installed plugins, and returns true/false for the
* first plugin to provide a non-null result. If no plugins respond, then
* the default policy is applied.
*
* @param url The URL that is being requested.
* @return Returns true to allow the resource to load,
* false to block the resource.
*/
public boolean shouldAllowRequest(String url) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldAllowRequest(url);
if (result != null) {
return result;
}
}
}
// Default policy:
if (url.startsWith("blob:") || url.startsWith("data:") || url.startsWith("about:blank")) {
return true;
}
// TalkBack requires this, so allow it by default.
if (url.startsWith("https://ssl.gstatic.com/accessibility/javascript/android/")) {
return true;
}
if (url.startsWith("file://")) {
//This directory on WebKit/Blink based webviews contains SQLite databases!
//DON'T CHANGE THIS UNLESS YOU KNOW WHAT YOU'RE DOING!
return !url.contains("/app_webview/");
}
return false;
}
/**
* Called when the webview is going to change the URL of the loaded content.
*
* This delegates to the installed plugins, and returns true/false for the
* first plugin to provide a non-null result. If no plugins respond, then
* the default policy is applied.
*
* @param url The URL that is being requested.
* @return Returns true to allow the navigation,
* false to block the navigation.
*/
public boolean shouldAllowNavigation(String url) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldAllowNavigation(url);
if (result != null) {
return result;
}
}
}
// Default policy:
return url.startsWith("file://") || url.startsWith("about:blank");
}
/**
* Called when the webview is requesting the exec() bridge be enabled.
*/
public boolean shouldAllowBridgeAccess(String url) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldAllowBridgeAccess(url);
if (result != null) {
return result;
}
}
}
// Default policy:
return url.startsWith("file://");
}
/**
* Called when the webview is going not going to navigate, but may launch
* an Intent for an URL.
*
* This delegates to the installed plugins, and returns true/false for the
* first plugin to provide a non-null result. If no plugins respond, then
* the default policy is applied.
*
* @param url The URL that is being requested.
* @return Returns true to allow the URL to launch an intent,
* false to block the intent.
*/
public Boolean shouldOpenExternalUrl(String url) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldOpenExternalUrl(url);
if (result != null) {
return result;
}
}
}
// Default policy:
// External URLs are not allowed
return false;
}
/**
* Called when the URL of the webview changes.
*
* @param url The URL that is being changed to.
* @return Return false to allow the URL to load, return true to prevent the URL from loading.
*/
public boolean onOverrideUrlLoading(String url) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null && plugin.onOverrideUrlLoading(url)) {
return true;
}
}
return false;
}
/**
* Called when the app navigates or refreshes.
*/
public void onReset() {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onReset();
}
}
}
Uri remapUri(Uri uri) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
Uri ret = plugin.remapUri(uri);
if (ret != null) {
return ret;
}
}
}
return null;
}
/**
* Create a plugin based on class name.
*/
private CordovaPlugin instantiatePlugin(String className) {
CordovaPlugin ret = null;
try {
Class<?> c = null;
if ((className != null) && !("".equals(className))) {
c = Class.forName(className);
}
if (c != null & CordovaPlugin.class.isAssignableFrom(c)) {
ret = (CordovaPlugin) c.newInstance();
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("Error adding plugin " + className + ".");
}
return ret;
}
/**
* Called by the system when the device configuration changes while your activity is running.
*
* @param newConfig The new device configuration
*/
public void onConfigurationChanged(Configuration newConfig) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onConfigurationChanged(newConfig);
}
}
}
public Bundle onSaveInstanceState() {
Bundle state = new Bundle();
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
Bundle pluginState = plugin.onSaveInstanceState();
if(pluginState != null) {
state.putBundle(plugin.getServiceName(), pluginState);
}
}
}
return state;
}
}

View file

@ -0,0 +1,198 @@
/*
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.List;
import org.json.JSONArray;
import org.json.JSONObject;
import android.util.Base64;
public class PluginResult {
private final int status;
private final int messageType;
private boolean keepCallback = false;
private String strMessage;
private String encodedMessage;
private List<PluginResult> multipartMessages;
public PluginResult(Status status) {
this(status, PluginResult.StatusMessages[status.ordinal()]);
}
public PluginResult(Status status, String message) {
this.status = status.ordinal();
this.messageType = message == null ? MESSAGE_TYPE_NULL : MESSAGE_TYPE_STRING;
this.strMessage = message;
}
public PluginResult(Status status, JSONArray message) {
this.status = status.ordinal();
this.messageType = MESSAGE_TYPE_JSON;
encodedMessage = message.toString();
}
public PluginResult(Status status, JSONObject message) {
this.status = status.ordinal();
this.messageType = MESSAGE_TYPE_JSON;
encodedMessage = message.toString();
}
public PluginResult(Status status, int i) {
this.status = status.ordinal();
this.messageType = MESSAGE_TYPE_NUMBER;
this.encodedMessage = ""+i;
}
public PluginResult(Status status, float f) {
this.status = status.ordinal();
this.messageType = MESSAGE_TYPE_NUMBER;
this.encodedMessage = ""+f;
}
public PluginResult(Status status, boolean b) {
this.status = status.ordinal();
this.messageType = MESSAGE_TYPE_BOOLEAN;
this.encodedMessage = Boolean.toString(b);
}
public PluginResult(Status status, byte[] data) {
this(status, data, false);
}
public PluginResult(Status status, byte[] data, boolean binaryString) {
this.status = status.ordinal();
this.messageType = binaryString ? MESSAGE_TYPE_BINARYSTRING : MESSAGE_TYPE_ARRAYBUFFER;
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;
}
public int getStatus() {
return status;
}
public int getMessageType() {
return messageType;
}
public String getMessage() {
if (encodedMessage == null) {
encodedMessage = JSONObject.quote(strMessage);
}
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.
*/
public String getStrMessage() {
return strMessage;
}
public boolean getKeepCallback() {
return this.keepCallback;
}
@Deprecated // Use sendPluginResult instead of sendJavascript.
public String getJSONString() {
return "{\"status\":" + this.status + ",\"message\":" + this.getMessage() + ",\"keepCallback\":" + this.keepCallback + "}";
}
@Deprecated // Use sendPluginResult instead of sendJavascript.
public String toCallbackString(String callbackId) {
// If no result to be sent and keeping callback, then no need to sent back to JavaScript
if ((status == PluginResult.Status.NO_RESULT.ordinal()) && keepCallback) {
return null;
}
// Check the success (OK, NO_RESULT & !KEEP_CALLBACK)
if ((status == PluginResult.Status.OK.ordinal()) || (status == PluginResult.Status.NO_RESULT.ordinal())) {
return toSuccessCallbackString(callbackId);
}
return toErrorCallbackString(callbackId);
}
@Deprecated // Use sendPluginResult instead of sendJavascript.
public String toSuccessCallbackString(String callbackId) {
return "cordova.callbackSuccess('"+callbackId+"',"+this.getJSONString()+");";
}
@Deprecated // Use sendPluginResult instead of sendJavascript.
public String toErrorCallbackString(String callbackId) {
return "cordova.callbackError('"+callbackId+"', " + this.getJSONString()+ ");";
}
public static final int MESSAGE_TYPE_STRING = 1;
public static final int MESSAGE_TYPE_JSON = 2;
public static final int MESSAGE_TYPE_NUMBER = 3;
public static final int MESSAGE_TYPE_BOOLEAN = 4;
public static final int MESSAGE_TYPE_NULL = 5;
public static final int MESSAGE_TYPE_ARRAYBUFFER = 6;
// 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",
"OK",
"Class not found",
"Illegal access",
"Instantiation error",
"Malformed url",
"IO error",
"Invalid action",
"JSON error",
"Error"
};
public enum Status {
NO_RESULT,
OK,
CLASS_NOT_FOUND_EXCEPTION,
ILLEGAL_ACCESS_EXCEPTION,
INSTANTIATION_EXCEPTION,
MALFORMED_URL_EXCEPTION,
IO_EXCEPTION,
INVALID_ACTION,
JSON_EXCEPTION,
ERROR
}
}

View file

@ -0,0 +1,76 @@
/*
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 org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
public class ResumeCallback extends CallbackContext {
private final String TAG = "CordovaResumeCallback";
private String serviceName;
private PluginManager pluginManager;
public ResumeCallback(String serviceName, PluginManager pluginManager) {
super("resumecallback", null);
this.serviceName = serviceName;
this.pluginManager = pluginManager;
}
@Override
public void sendPluginResult(PluginResult pluginResult) {
synchronized (this) {
if (finished) {
LOG.w(TAG, serviceName + " attempted to send a second callback to ResumeCallback\nResult was: " + pluginResult.getMessage());
return;
} else {
finished = true;
}
}
JSONObject event = new JSONObject();
JSONObject pluginResultObject = new JSONObject();
try {
pluginResultObject.put("pluginServiceName", this.serviceName);
pluginResultObject.put("pluginStatus", PluginResult.StatusMessages[pluginResult.getStatus()]);
event.put("action", "resume");
event.put("pendingResult", pluginResultObject);
} catch (JSONException e) {
LOG.e(TAG, "Unable to create resume object for Activity Result");
}
PluginResult eventResult = new PluginResult(PluginResult.Status.OK, event);
// We send a list of results to the js so that we don't have to decode
// the PluginResult passed to this CallbackContext into JSON twice.
// The results are combined into an event payload before the event is
// fired on the js side of things (see platform.js)
List<PluginResult> result = new ArrayList<PluginResult>();
result.add(eventResult);
result.add(pluginResult);
CoreAndroid appPlugin = (CoreAndroid) pluginManager.getPlugin(CoreAndroid.PLUGIN_NAME);
appPlugin.sendResumeEvent(new PluginResult(PluginResult.Status.OK, result));
}
}

View file

@ -0,0 +1,170 @@
/*
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.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.cordova.LOG;
import android.net.Uri;
public class Whitelist {
private static class URLPattern {
public Pattern scheme;
public Pattern host;
public Integer port;
public Pattern path;
private String regexFromPattern(String pattern, boolean allowWildcards) {
final String toReplace = "\\.[]{}()^$?+|";
StringBuilder regex = new StringBuilder();
for (int i=0; i < pattern.length(); i++) {
char c = pattern.charAt(i);
if (c == '*' && allowWildcards) {
regex.append(".");
} else if (toReplace.indexOf(c) > -1) {
regex.append('\\');
}
regex.append(c);
}
return regex.toString();
}
public URLPattern(String scheme, String host, String port, String path) throws MalformedURLException {
try {
if (scheme == null || "*".equals(scheme)) {
this.scheme = null;
} else {
this.scheme = Pattern.compile(regexFromPattern(scheme, false), Pattern.CASE_INSENSITIVE);
}
if ("*".equals(host)) {
this.host = null;
} else if (host.startsWith("*.")) {
this.host = Pattern.compile("([a-z0-9.-]*\\.)?" + regexFromPattern(host.substring(2), false), Pattern.CASE_INSENSITIVE);
} else {
this.host = Pattern.compile(regexFromPattern(host, false), Pattern.CASE_INSENSITIVE);
}
if (port == null || "*".equals(port)) {
this.port = null;
} else {
this.port = Integer.parseInt(port,10);
}
if (path == null || "/*".equals(path)) {
this.path = null;
} else {
this.path = Pattern.compile(regexFromPattern(path, true));
}
} catch (NumberFormatException e) {
throw new MalformedURLException("Port must be a number");
}
}
public boolean matches(Uri uri) {
try {
return ((scheme == null || scheme.matcher(uri.getScheme()).matches()) &&
(host == null || host.matcher(uri.getHost()).matches()) &&
(port == null || port.equals(uri.getPort())) &&
(path == null || path.matcher(uri.getPath()).matches()));
} catch (Exception e) {
LOG.d(TAG, e.toString());
return false;
}
}
}
private ArrayList<URLPattern> whiteList;
public static final String TAG = "Whitelist";
public Whitelist() {
this.whiteList = new ArrayList<URLPattern>();
}
/* Match patterns (from http://developer.chrome.com/extensions/match_patterns.html)
*
* <url-pattern> := <scheme>://<host><path>
* <scheme> := '*' | 'http' | 'https' | 'file' | 'ftp' | 'chrome-extension'
* <host> := '*' | '*.' <any char except '/' and '*'>+
* <path> := '/' <any chars>
*
* We extend this to explicitly allow a port attached to the host, and we allow
* the scheme to be omitted for backwards compatibility. (Also host is not required
* to begin with a "*" or "*.".)
*/
public void addWhiteListEntry(String origin, boolean subdomains) {
if (whiteList != null) {
try {
// Unlimited access to network resources
if (origin.compareTo("*") == 0) {
LOG.d(TAG, "Unlimited access to network resources");
whiteList = null;
}
else { // specific access
Pattern parts = Pattern.compile("^((\\*|[A-Za-z-]+):(//)?)?(\\*|((\\*\\.)?[^*/:]+))?(:(\\d+))?(/.*)?");
Matcher m = parts.matcher(origin);
if (m.matches()) {
String scheme = m.group(2);
String host = m.group(4);
// Special case for two urls which are allowed to have empty hosts
if (("file".equals(scheme) || "content".equals(scheme)) && host == null) host = "*";
String port = m.group(8);
String path = m.group(9);
if (scheme == null) {
// XXX making it stupid friendly for people who forget to include protocol/SSL
whiteList.add(new URLPattern("http", host, port, path));
whiteList.add(new URLPattern("https", host, port, path));
} else {
whiteList.add(new URLPattern(scheme, host, port, path));
}
}
}
} catch (Exception e) {
LOG.d(TAG, "Failed to add origin %s", origin);
}
}
}
/**
* Determine if URL is in approved list of URLs to load.
*
* @param uri
* @return true if wide open or whitelisted
*/
public boolean isUrlWhiteListed(String uri) {
// If there is no whitelist, then it's wide open
if (whiteList == null) return true;
Uri parsedUri = Uri.parse(uri);
// Look for match in white list
Iterator<URLPattern> pit = whiteList.iterator();
while (pit.hasNext()) {
URLPattern p = pit.next();
if (p.matches(parsedUri)) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,69 @@
/*
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.engine;
import android.annotation.TargetApi;
import android.os.Build;
import android.webkit.CookieManager;
import android.webkit.WebView;
import org.apache.cordova.ICordovaCookieManager;
class SystemCookieManager implements ICordovaCookieManager {
protected final WebView webView;
private final CookieManager cookieManager;
//Added because lint can't see the conditional RIGHT ABOVE this
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public SystemCookieManager(WebView webview) {
webView = webview;
cookieManager = CookieManager.getInstance();
//REALLY? Nobody has seen this UNTIL NOW?
cookieManager.setAcceptFileSchemeCookies(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.setAcceptThirdPartyCookies(webView, true);
}
}
public void setCookiesEnabled(boolean accept) {
cookieManager.setAcceptCookie(accept);
}
public void setCookie(final String url, final String value) {
cookieManager.setCookie(url, value);
}
public String getCookie(final String url) {
return cookieManager.getCookie(url);
}
public void clearCookies() {
cookieManager.removeAllCookie();
}
public void flush() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.flush();
}
}
};

View file

@ -0,0 +1,53 @@
/*
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.engine;
import android.webkit.JavascriptInterface;
import org.apache.cordova.CordovaBridge;
import org.apache.cordova.ExposedJsApi;
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 SystemExposedJsApi implements ExposedJsApi {
private final CordovaBridge bridge;
SystemExposedJsApi(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,277 @@
/*
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.engine;
import java.util.Arrays;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.webkit.ConsoleMessage;
import android.webkit.GeolocationPermissions.Callback;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebStorage;
import android.webkit.WebView;
import android.webkit.PermissionRequest;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import org.apache.cordova.CordovaDialogsHelper;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.LOG;
/**
* This class is the WebChromeClient that implements callbacks for our web view.
* The kind of callbacks that happen here are on the chrome outside the document,
* such as onCreateWindow(), onConsoleMessage(), onProgressChanged(), etc. Related
* to but different than CordovaWebViewClient.
*/
public class SystemWebChromeClient extends WebChromeClient {
private static final int FILECHOOSER_RESULTCODE = 5173;
private static final String LOG_TAG = "SystemWebChromeClient";
private long MAX_QUOTA = 100 * 1024 * 1024;
protected final SystemWebViewEngine parentEngine;
// the video progress view
private View mVideoProgressView;
private CordovaDialogsHelper dialogsHelper;
private Context appContext;
private WebChromeClient.CustomViewCallback mCustomViewCallback;
private View mCustomView;
public SystemWebChromeClient(SystemWebViewEngine parentEngine) {
this.parentEngine = parentEngine;
appContext = parentEngine.webView.getContext();
dialogsHelper = new CordovaDialogsHelper(appContext);
}
/**
* Tell the client to display a javascript alert dialog.
*/
@Override
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
dialogsHelper.showAlert(message, new CordovaDialogsHelper.Result() {
@Override public void gotResult(boolean success, String value) {
if (success) {
result.confirm();
} else {
result.cancel();
}
}
});
return true;
}
/**
* Tell the client to display a confirm dialog to the user.
*/
@Override
public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
dialogsHelper.showConfirm(message, new CordovaDialogsHelper.Result() {
@Override
public void gotResult(boolean success, String value) {
if (success) {
result.confirm();
} else {
result.cancel();
}
}
});
return true;
}
/**
* Tell the client to display a prompt dialog to the user.
* If the client returns true, WebView will assume that the client will
* handle the prompt dialog and call the appropriate JsPromptResult method.
*
* Since we are hacking prompts for our own purposes, we should not be using them for
* this purpose, perhaps we should hack console.log to do this instead!
*/
@Override
public boolean onJsPrompt(WebView view, String origin, String message, String defaultValue, final JsPromptResult result) {
// Unlike the @JavascriptInterface bridge, this method is always called on the UI thread.
String handledRet = parentEngine.bridge.promptOnJsPrompt(origin, message, defaultValue);
if (handledRet != null) {
result.confirm(handledRet);
} else {
dialogsHelper.showPrompt(message, defaultValue, new CordovaDialogsHelper.Result() {
@Override
public void gotResult(boolean success, String value) {
if (success) {
result.confirm(value);
} else {
result.cancel();
}
}
});
}
return true;
}
/**
* Handle database quota exceeded notification.
*/
@Override
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);
quotaUpdater.updateQuota(MAX_QUOTA);
}
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage)
{
if (consoleMessage.message() != null)
LOG.d(LOG_TAG, "%s: Line %d : %s" , consoleMessage.sourceId() , consoleMessage.lineNumber(), consoleMessage.message());
return super.onConsoleMessage(consoleMessage);
}
@Override
/**
* Instructs the client to show a prompt to ask the user to set the Geolocation permission state for the specified origin.
*
* This also checks for the Geolocation Plugin and requests permission from the application to use Geolocation.
*
* @param origin
* @param callback
*/
public void onGeolocationPermissionsShowPrompt(String origin, Callback callback) {
super.onGeolocationPermissionsShowPrompt(origin, callback);
callback.invoke(origin, true, false);
//Get the plugin, it should be loaded
CordovaPlugin geolocation = parentEngine.pluginManager.getPlugin("Geolocation");
if(geolocation != null && !geolocation.hasPermisssion())
{
geolocation.requestPermissions(0);
}
}
// API level 7 is required for this, see if we could lower this using something else
@Override
public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) {
parentEngine.getCordovaWebView().showCustomView(view, callback);
}
@Override
public void onHideCustomView() {
parentEngine.getCordovaWebView().hideCustomView();
}
@Override
/**
* Ask the host application for a custom progress view to show while
* a <video> is loading.
* @return View The progress view.
*/
public View getVideoLoadingProgressView() {
if (mVideoProgressView == null) {
// Create a new Loading view programmatically.
// create the linear layout
LinearLayout layout = new LinearLayout(parentEngine.getView().getContext());
layout.setOrientation(LinearLayout.VERTICAL);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
layout.setLayoutParams(layoutParams);
// the proress bar
ProgressBar bar = new ProgressBar(parentEngine.getView().getContext());
LinearLayout.LayoutParams barLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
barLayoutParams.gravity = Gravity.CENTER;
bar.setLayoutParams(barLayoutParams);
layout.addView(bar);
mVideoProgressView = layout;
}
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(final ValueCallback<Uri> uploadMsg, String acceptType, String capture)
{
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
parentEngine.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(LOG_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 {
parentEngine.cordova.startActivityForResult(new CordovaPlugin() {
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
Uri[] result = WebChromeClient.FileChooserParams.parseResult(resultCode, intent);
LOG.d(LOG_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;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void onPermissionRequest(final PermissionRequest request) {
LOG.d(LOG_TAG, "onPermissionRequest: " + Arrays.toString(request.getResources()));
request.grant(request.getResources());
}
public void destroyLastDialog(){
dialogsHelper.destroyLastDialog();
}
}

View file

@ -0,0 +1,88 @@
/*
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.engine;
import android.content.Context;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CordovaWebViewEngine;
/**
* Custom WebView subclass that enables us to capture events needed for Cordova.
*/
public class SystemWebView extends WebView implements CordovaWebViewEngine.EngineView {
private SystemWebViewClient viewClient;
SystemWebChromeClient chromeClient;
private SystemWebViewEngine parentEngine;
private CordovaInterface cordova;
public SystemWebView(Context context) {
this(context, null);
}
public SystemWebView(Context context, AttributeSet attrs) {
super(context, attrs);
}
// Package visibility to enforce that only SystemWebViewEngine should call this method.
void init(SystemWebViewEngine parentEngine, CordovaInterface cordova) {
this.cordova = cordova;
this.parentEngine = parentEngine;
if (this.viewClient == null) {
setWebViewClient(new SystemWebViewClient(parentEngine));
}
if (this.chromeClient == null) {
setWebChromeClient(new SystemWebChromeClient(parentEngine));
}
}
@Override
public CordovaWebView getCordovaWebView() {
return parentEngine != null ? parentEngine.getCordovaWebView() : null;
}
@Override
public void setWebViewClient(WebViewClient client) {
viewClient = (SystemWebViewClient)client;
super.setWebViewClient(client);
}
@Override
public void setWebChromeClient(WebChromeClient client) {
chromeClient = (SystemWebChromeClient)client;
super.setWebChromeClient(client);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
Boolean ret = parentEngine.client.onDispatchKeyEvent(event);
if (ret != null) {
return ret.booleanValue();
}
return super.dispatchKeyEvent(event);
}
}

View file

@ -0,0 +1,367 @@
/*
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.engine;
import android.annotation.TargetApi;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Bitmap;
import android.net.Uri;
import android.net.http.SslError;
import android.os.Build;
import android.webkit.ClientCertRequest;
import android.webkit.HttpAuthHandler;
import android.webkit.SslErrorHandler;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import org.apache.cordova.AuthenticationToken;
import org.apache.cordova.CordovaClientCertRequest;
import org.apache.cordova.CordovaHttpAuthHandler;
import org.apache.cordova.CordovaResourceApi;
import org.apache.cordova.LOG;
import org.apache.cordova.PluginManager;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Hashtable;
/**
* 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
* document instead of the chrome surrounding it, such as onPageStarted(),
* shouldOverrideUrlLoading(), etc. Related to but different than
* CordovaChromeClient.
*/
public class SystemWebViewClient extends WebViewClient {
private static final String TAG = "SystemWebViewClient";
protected final SystemWebViewEngine parentEngine;
private boolean doClearHistory = false;
boolean isCurrentlyLoading;
/** The authorization tokens. */
private Hashtable<String, AuthenticationToken> authenticationTokens = new Hashtable<String, AuthenticationToken>();
public SystemWebViewClient(SystemWebViewEngine parentEngine) {
this.parentEngine = parentEngine;
}
/**
* Give the host application a chance to take over the control when a new url
* is about to be loaded in the current WebView.
*
* @param view The WebView that is initiating the callback.
* @param url The url to be loaded.
* @return true to override, false for default behavior
*/
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return parentEngine.client.onNavigationAttempt(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
*/
@Override
public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
// Get the authentication token (if specified)
AuthenticationToken token = this.getAuthenticationToken(host, realm);
if (token != null) {
handler.proceed(token.getUserName(), token.getPassword());
return;
}
// Check if there is some plugin which can resolve this auth challenge
PluginManager pluginManager = this.parentEngine.pluginManager;
if (pluginManager != null && pluginManager.onReceivedHttpAuthRequest(null, new CordovaHttpAuthHandler(handler), host, realm)) {
parentEngine.client.clearLoadTimeoutTimer();
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(Build.VERSION_CODES.LOLLIPOP)
public void onReceivedClientCertRequest (WebView view, ClientCertRequest request)
{
// Check if there is some plugin which can resolve this certificate request
PluginManager pluginManager = this.parentEngine.pluginManager;
if (pluginManager != null && pluginManager.onReceivedClientCertRequest(null, new CordovaClientCertRequest(request))) {
parentEngine.client.clearLoadTimeoutTimer();
return;
}
// By default pass to WebViewClient
super.onReceivedClientCertRequest(view, request);
}
/**
* Notify the host application that a page has started loading.
* This method is called once for each main frame load so a page with iframes or framesets will call onPageStarted
* one time for the main frame. This also means that onPageStarted will not be called when the contents of an
* embedded frame changes, i.e. clicking a link whose target is an iframe.
*
* @param view The webview initiating the callback.
* @param url The url of the page.
*/
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
isCurrentlyLoading = true;
// Flush stale messages & reset plugins.
parentEngine.bridge.reset();
parentEngine.client.onPageStarted(url);
}
/**
* Notify the host application that a page has finished loading.
* This method is called only for main frame. When onPageFinished() is called, the rendering picture may not be updated yet.
*
*
* @param view The webview initiating the callback.
* @param url The url of the page.
*/
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
// Ignore excessive calls, if url is not about:blank (CB-8317).
if (!isCurrentlyLoading && !url.startsWith("about:")) {
return;
}
isCurrentlyLoading = false;
/**
* Because of a timing issue we need to clear this history in onPageFinished as well as
* onPageStarted. However we only want to do this if the doClearHistory boolean is set to
* true. You see when you load a url with a # in it which is common in jQuery applications
* onPageStared is not called. Clearing the history at that point would break jQuery apps.
*/
if (this.doClearHistory) {
view.clearHistory();
this.doClearHistory = false;
}
parentEngine.client.onPageFinishedLoading(url);
}
/**
* Report an error to the host application. These errors are unrecoverable (i.e. the main resource is unavailable).
* The errorCode parameter corresponds to one of the ERROR_* constants.
*
* @param view The WebView that is initiating the callback.
* @param errorCode The error code corresponding to an ERROR_* value.
* @param description A String describing the error.
* @param failingUrl The url that failed to load.
*/
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
// Ignore error due to stopLoading().
if (!isCurrentlyLoading) {
return;
}
LOG.d(TAG, "CordovaWebViewClient.onReceivedError: Error code=%s Description=%s URL=%s", errorCode, description, failingUrl);
// 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
// is likely incorrect (start page set to sms: or something like that)
if (errorCode == WebViewClient.ERROR_UNSUPPORTED_SCHEME) {
parentEngine.client.clearLoadTimeoutTimer();
if (view.canGoBack()) {
view.goBack();
return;
} else {
super.onReceivedError(view, errorCode, description, failingUrl);
}
}
parentEngine.client.onReceivedError(errorCode, description, failingUrl);
}
/**
* Notify the host application that an SSL error occurred while loading a resource.
* The host application must call either handler.cancel() or handler.proceed().
* Note that the decision may be retained for use in response to future SSL errors.
* The default behavior is to cancel the load.
*
* @param view The WebView that is initiating the callback.
* @param handler An SslErrorHandler object that will handle the user's response.
* @param error The SSL error object.
*/
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
final String packageName = parentEngine.cordova.getActivity().getPackageName();
final PackageManager pm = parentEngine.cordova.getActivity().getPackageManager();
ApplicationInfo appInfo;
try {
appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
if ((appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
// debug = true
handler.proceed();
return;
} else {
// debug = false
super.onReceivedSslError(view, handler, error);
}
} catch (NameNotFoundException e) {
// When it doubt, lock it out!
super.onReceivedSslError(view, handler, error);
}
}
/**
* Sets the authentication token.
*
* @param authenticationToken
* @param host
* @param realm
*/
public void setAuthenticationToken(AuthenticationToken authenticationToken, String host, String realm) {
if (host == null) {
host = "";
}
if (realm == null) {
realm = "";
}
this.authenticationTokens.put(host.concat(realm), authenticationToken);
}
/**
* 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) {
return this.authenticationTokens.remove(host.concat(realm));
}
/**
* 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) {
AuthenticationToken token = null;
token = this.authenticationTokens.get(host.concat(realm));
if (token == null) {
// try with just the host
token = this.authenticationTokens.get(host);
// Try the realm
if (token == null) {
token = this.authenticationTokens.get(realm);
}
// if no host found, just query for default
if (token == null) {
token = this.authenticationTokens.get("");
}
}
return token;
}
/**
* Clear all authentication tokens.
*/
public void clearAuthenticationTokens() {
this.authenticationTokens.clear();
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
try {
// Check the against the whitelist and lock out access to the WebView directory
// Changing this will cause problems for your application
if (!parentEngine.pluginManager.shouldAllowRequest(url)) {
LOG.w(TAG, "URL blocked by whitelist: " + url);
// Results in a 404.
return new WebResourceResponse("text/plain", "UTF-8", null);
}
CordovaResourceApi resourceApi = parentEngine.resourceApi;
Uri origUri = Uri.parse(url);
// Allow plugins to intercept WebView requests.
Uri remappedUri = resourceApi.remapUri(origUri);
if (!origUri.equals(remappedUri) || needsSpecialsInAssetUrlFix(origUri) || needsKitKatContentUrlFix(origUri)) {
CordovaResourceApi.OpenForReadResult result = resourceApi.openForRead(remappedUri, true);
return new WebResourceResponse(result.mimeType, "UTF-8", result.inputStream);
}
// If we don't need to special-case the request, let the browser load it.
return null;
} catch (IOException e) {
if (!(e instanceof FileNotFoundException)) {
LOG.e(TAG, "Error occurred while loading a file (returning a 404).", e);
}
// Results in a 404.
return new WebResourceResponse("text/plain", "UTF-8", null);
}
}
private static boolean needsKitKatContentUrlFix(Uri uri) {
return "content".equals(uri.getScheme());
}
private static boolean needsSpecialsInAssetUrlFix(Uri uri) {
if (CordovaResourceApi.getUriType(uri) != CordovaResourceApi.URI_TYPE_ASSET) {
return false;
}
if (uri.getQuery() != null || uri.getFragment() != null) {
return true;
}
if (!uri.toString().contains("%")) {
return false;
}
return false;
}
}

View file

@ -0,0 +1,319 @@
/*
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.engine;
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.os.Build;
import android.view.View;
import android.webkit.ValueCallback;
import android.webkit.WebSettings;
import android.webkit.WebSettings.LayoutAlgorithm;
import android.webkit.WebView;
import org.apache.cordova.CordovaBridge;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPreferences;
import org.apache.cordova.CordovaResourceApi;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CordovaWebViewEngine;
import org.apache.cordova.ICordovaCookieManager;
import org.apache.cordova.LOG;
import org.apache.cordova.NativeToJsMessageQueue;
import org.apache.cordova.PluginManager;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Glue class between CordovaWebView (main Cordova logic) and SystemWebView (the actual View).
* We make the Engine separate from the actual View so that:
* A) We don't need to worry about WebView methods clashing with CordovaWebViewEngine methods
* (e.g.: goBack() is void for WebView, and boolean for CordovaWebViewEngine)
* B) Separating the actual View from the Engine makes API surfaces smaller.
* Class uses two-phase initialization. However, CordovaWebView is responsible for calling .init().
*/
public class SystemWebViewEngine implements CordovaWebViewEngine {
public static final String TAG = "SystemWebViewEngine";
protected final SystemWebView webView;
protected final SystemCookieManager cookieManager;
protected CordovaPreferences preferences;
protected CordovaBridge bridge;
protected CordovaWebViewEngine.Client client;
protected CordovaWebView parentWebView;
protected CordovaInterface cordova;
protected PluginManager pluginManager;
protected CordovaResourceApi resourceApi;
protected NativeToJsMessageQueue nativeToJsMessageQueue;
private BroadcastReceiver receiver;
/** Used when created via reflection. */
public SystemWebViewEngine(Context context, CordovaPreferences preferences) {
this(new SystemWebView(context), preferences);
}
public SystemWebViewEngine(SystemWebView webView) {
this(webView, null);
}
public SystemWebViewEngine(SystemWebView webView, CordovaPreferences preferences) {
this.preferences = preferences;
this.webView = webView;
cookieManager = new SystemCookieManager(webView);
}
@Override
public void init(CordovaWebView parentWebView, CordovaInterface cordova, CordovaWebViewEngine.Client client,
CordovaResourceApi resourceApi, PluginManager pluginManager,
NativeToJsMessageQueue nativeToJsMessageQueue) {
if (this.cordova != null) {
throw new IllegalStateException();
}
// Needed when prefs are not passed by the constructor
if (preferences == null) {
preferences = parentWebView.getPreferences();
}
this.parentWebView = parentWebView;
this.cordova = cordova;
this.client = client;
this.resourceApi = resourceApi;
this.pluginManager = pluginManager;
this.nativeToJsMessageQueue = nativeToJsMessageQueue;
webView.init(this, cordova);
initWebViewSettings();
nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.OnlineEventsBridgeMode(new NativeToJsMessageQueue.OnlineEventsBridgeMode.OnlineEventsBridgeModeDelegate() {
@Override
public void setNetworkAvailable(boolean value) {
//sometimes this can be called after calling webview.destroy() on destroy()
//thus resulting in a NullPointerException
if(webView!=null) {
webView.setNetworkAvailable(value);
}
}
@Override
public void runOnUiThread(Runnable r) {
SystemWebViewEngine.this.cordova.getActivity().runOnUiThread(r);
}
}));
nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.EvalBridgeMode(this, cordova));
bridge = new CordovaBridge(pluginManager, nativeToJsMessageQueue);
exposeJsInterface(webView, bridge);
}
@Override
public CordovaWebView getCordovaWebView() {
return parentWebView;
}
@Override
public ICordovaCookieManager getCookieManager() {
return cookieManager;
}
@Override
public View getView() {
return webView;
}
@SuppressLint({"NewApi", "SetJavaScriptEnabled"})
@SuppressWarnings("deprecation")
private void initWebViewSettings() {
webView.setInitialScale(0);
webView.setVerticalScrollBarEnabled(false);
// Enable JavaScript
final WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setJavaScriptCanOpenWindowsAutomatically(true);
settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL);
String manufacturer = android.os.Build.MANUFACTURER;
LOG.d(TAG, "CordovaWebView is running on device made by: " + manufacturer);
//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
settings.setAllowUniversalAccessFromFileURLs(true);
settings.setMediaPlaybackRequiresUserGesture(false);
// Enable database
// We keep this disabled because we use or shim to get around DOM_EXCEPTION_ERROR_16
String databasePath = webView.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 = webView.getContext().getApplicationContext().getApplicationInfo();
if ((appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
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
String defaultUserAgent = settings.getUserAgentString();
// Fix for CB-3360
String overrideUserAgent = preferences.getString("OverrideUserAgent", null);
if (overrideUserAgent != null) {
settings.setUserAgentString(overrideUserAgent);
} else {
String appendUserAgent = preferences.getString("AppendUserAgent", null);
if (appendUserAgent != null) {
settings.setUserAgentString(defaultUserAgent + " " + appendUserAgent);
}
}
// End CB-3360
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();
}
};
webView.getContext().registerReceiver(this.receiver, intentFilter);
}
// end CB-1405
}
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();
}
}
// Yeah, we know. It'd be great if lint was just a little smarter.
@SuppressLint("AddJavascriptInterface")
private static void exposeJsInterface(WebView webView, CordovaBridge bridge) {
SystemExposedJsApi exposedJsApi = new SystemExposedJsApi(bridge);
webView.addJavascriptInterface(exposedJsApi, "_cordovaNative");
}
/**
* Load the url into the webview.
*/
@Override
public void loadUrl(final String url, boolean clearNavigationStack) {
webView.loadUrl(url);
}
@Override
public String getUrl() {
return webView.getUrl();
}
@Override
public void stopLoading() {
webView.stopLoading();
}
@Override
public void clearCache() {
webView.clearCache(true);
}
@Override
public void clearHistory() {
webView.clearHistory();
}
@Override
public boolean canGoBack() {
return webView.canGoBack();
}
/**
* Go to previous page in history. (We manage our own history)
*
* @return true if we went back, false if we are already at top
*/
@Override
public boolean goBack() {
// Check webview first to see if there is a history
// This is needed to support curPage#diffLink, since they are added to parentEngine's history, but not our history url array (JQMobile behavior)
if (webView.canGoBack()) {
webView.goBack();
return true;
}
return false;
}
@Override
public void setPaused(boolean value) {
if (value) {
webView.onPause();
webView.pauseTimers();
} else {
webView.onResume();
webView.resumeTimers();
}
}
@Override
public void destroy() {
webView.chromeClient.destroyLastDialog();
webView.destroy();
// unregister the receiver
if (receiver != null) {
try {
webView.getContext().unregisterReceiver(receiver);
} catch (Exception e) {
LOG.e(TAG, "Error unregistering configuration receiver: " + e.getMessage(), e);
}
}
}
@Override
public void evaluateJavascript(String js, ValueCallback<String> callback) {
webView.evaluateJavascript(js, callback);
}
}

View file

@ -0,0 +1,439 @@
{
"prepare_queue": {
"installed": [],
"uninstalled": []
},
"config_munge": {
"files": {
"res/xml/config.xml": {
"parents": {
"/widget": [
{
"xml": "<feature name=\"FileChooser\"><param name=\"android-package\" value=\"com.megster.cordova.FileChooser\" /></feature>",
"count": 1
}
],
"/*": [
{
"xml": "<feature name=\"Keyboard\"><param name=\"android-package\" value=\"io.ionic.keyboard.IonicKeyboard\" /><param name=\"onload\" value=\"true\" /></feature>",
"count": 1
},
{
"xml": "<feature name=\"File\"><param name=\"android-package\" value=\"org.apache.cordova.file.FileUtils\" /><param name=\"onload\" value=\"true\" /></feature>",
"count": 1
},
{
"xml": "<allow-navigation href=\"cdvfile:*\" />",
"count": 1
},
{
"xml": "<feature name=\"FileTransfer\"><param name=\"android-package\" value=\"org.apache.cordova.filetransfer.FileTransfer\" /></feature>",
"count": 1
},
{
"xml": "<feature name=\"IntentShim\"><param name=\"android-package\" value=\"com.darryncampbell.cordova.plugin.intent.IntentShim\" /><param name=\"onload\" value=\"true\" /></feature>",
"count": 1
},
{
"xml": "<feature name=\"InAppBrowser\"><param name=\"android-package\" value=\"org.apache.cordova.inappbrowser.InAppBrowser\" /></feature>",
"count": 1
},
{
"xml": "<feature name=\"Whitelist\"><param name=\"android-package\" value=\"org.apache.cordova.whitelist.WhitelistPlugin\" /><param name=\"onload\" value=\"true\" /></feature>",
"count": 1
},
{
"xml": "<feature name=\"StatusBar\"><param name=\"android-package\" value=\"org.apache.cordova.statusbar.StatusBar\" /><param name=\"onload\" value=\"true\" /></feature>",
"count": 1
},
{
"xml": "<feature name=\"SplashScreen\"><param name=\"android-package\" value=\"org.apache.cordova.splashscreen.SplashScreen\" /><param name=\"onload\" value=\"true\" /></feature>",
"count": 1
},
{
"xml": "<feature name=\"Device\"><param name=\"android-package\" value=\"org.apache.cordova.device.Device\" /></feature>",
"count": 1
},
{
"xml": "<feature name=\"FilePath\"><param name=\"android-package\" value=\"com.hiddentao.cordova.filepath.FilePath\" /><param name=\"onload\" value=\"true\" /></feature>",
"count": 1
}
]
}
},
"config.xml": {
"parents": {
"/*": [
{
"xml": "<feature name=\"ShareContentPlugin\"><param name=\"android-package\" value=\"com.ferdinandsilva.android.ShareContentPlugin\" /></feature>",
"count": 1
}
]
}
},
"AndroidManifest.xml": {
"parents": {
"/*": [
{
"xml": "<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />",
"count": 3
},
{
"xml": "<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />",
"count": 1
}
],
"/manifest/application/activity": [
{
"xml": "<intent-filter><action android:name=\"com.darryncampbell.cordova.plugin.intent.ACTION\" /><category android:name=\"android.intent.category.DEFAULT\" /></intent-filter>",
"count": 1
}
],
"/manifest": [
{
"xml": "<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />",
"count": 1
},
{
"xml": "<uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\" />",
"count": 1
}
],
"/manifest/application": [
{
"xml": "<provider android:authorities=\"${applicationId}.provider\" android:exported=\"false\" android:grantUriPermissions=\"true\" android:name=\"android.support.v4.content.FileProvider\"><meta-data android:name=\"android.support.FILE_PROVIDER_PATHS\" android:resource=\"@xml/provider_paths\" /></provider>",
"count": 1
}
]
}
}
}
},
"installed_plugins": {
"cordova-plugin-filechooser": {
"PACKAGE_NAME": "com.monkeystew.goober_m"
},
"cordova-plugin-share-content": {
"PACKAGE_NAME": "com.monkeystew.goober_m"
},
"ionic-plugin-keyboard": {
"PACKAGE_NAME": "com.monkeystew.goober_m"
},
"cordova-plugin-file": {
"PACKAGE_NAME": "com.monkeystew.goober_m"
},
"cordova-plugin-file-transfer": {
"PACKAGE_NAME": "com.monkeystew.goober_m"
},
"com-darryncampbell-cordova-plugin-intent": {
"PACKAGE_NAME": "com.monkeystew.goober_m"
},
"cordova-plugin-inappbrowser": {
"PACKAGE_NAME": "com.monkeystew.goober_m"
},
"cordova-plugin-whitelist": {
"PACKAGE_NAME": "com.monkeystew.goober_m"
},
"cordova-plugin-statusbar": {
"PACKAGE_NAME": "com.monkeystew.goober_m"
},
"cordova-plugin-splashscreen": {
"PACKAGE_NAME": "com.monkeystew.goober_m"
},
"cordova-plugin-device": {
"PACKAGE_NAME": "com.monkeystew.goober_m"
},
"cordova-plugin-console": {
"PACKAGE_NAME": "com.monkeystew.goober_m"
},
"cordova-android-support-gradle-release": {
"ANDROID_SUPPORT_VERSION": "27.+",
"PACKAGE_NAME": "com.monkeystew.goober_m"
},
"cordova-plugin-filepath": {
"PACKAGE_NAME": "com.monkeystew.goober_m"
}
},
"dependent_plugins": {},
"modules": [
{
"id": "cordova-plugin-filechooser.FileChooser",
"file": "plugins/cordova-plugin-filechooser/www/fileChooser.js",
"pluginId": "cordova-plugin-filechooser",
"clobbers": [
"fileChooser"
]
},
{
"id": "cordova-plugin-share-content.ShareContentPlugin",
"file": "plugins/cordova-plugin-share-content/www/sharecontentplugin.js",
"pluginId": "cordova-plugin-share-content",
"merges": [
"shareContentPlugin"
]
},
{
"id": "ionic-plugin-keyboard.keyboard",
"file": "plugins/ionic-plugin-keyboard/www/android/keyboard.js",
"pluginId": "ionic-plugin-keyboard",
"clobbers": [
"cordova.plugins.Keyboard"
],
"runs": true
},
{
"id": "cordova-plugin-file.DirectoryEntry",
"file": "plugins/cordova-plugin-file/www/DirectoryEntry.js",
"pluginId": "cordova-plugin-file",
"clobbers": [
"window.DirectoryEntry"
]
},
{
"id": "cordova-plugin-file.DirectoryReader",
"file": "plugins/cordova-plugin-file/www/DirectoryReader.js",
"pluginId": "cordova-plugin-file",
"clobbers": [
"window.DirectoryReader"
]
},
{
"id": "cordova-plugin-file.Entry",
"file": "plugins/cordova-plugin-file/www/Entry.js",
"pluginId": "cordova-plugin-file",
"clobbers": [
"window.Entry"
]
},
{
"id": "cordova-plugin-file.File",
"file": "plugins/cordova-plugin-file/www/File.js",
"pluginId": "cordova-plugin-file",
"clobbers": [
"window.File"
]
},
{
"id": "cordova-plugin-file.FileEntry",
"file": "plugins/cordova-plugin-file/www/FileEntry.js",
"pluginId": "cordova-plugin-file",
"clobbers": [
"window.FileEntry"
]
},
{
"id": "cordova-plugin-file.FileError",
"file": "plugins/cordova-plugin-file/www/FileError.js",
"pluginId": "cordova-plugin-file",
"clobbers": [
"window.FileError"
]
},
{
"id": "cordova-plugin-file.FileReader",
"file": "plugins/cordova-plugin-file/www/FileReader.js",
"pluginId": "cordova-plugin-file",
"clobbers": [
"window.FileReader"
]
},
{
"id": "cordova-plugin-file.FileSystem",
"file": "plugins/cordova-plugin-file/www/FileSystem.js",
"pluginId": "cordova-plugin-file",
"clobbers": [
"window.FileSystem"
]
},
{
"id": "cordova-plugin-file.FileUploadOptions",
"file": "plugins/cordova-plugin-file/www/FileUploadOptions.js",
"pluginId": "cordova-plugin-file",
"clobbers": [
"window.FileUploadOptions"
]
},
{
"id": "cordova-plugin-file.FileUploadResult",
"file": "plugins/cordova-plugin-file/www/FileUploadResult.js",
"pluginId": "cordova-plugin-file",
"clobbers": [
"window.FileUploadResult"
]
},
{
"id": "cordova-plugin-file.FileWriter",
"file": "plugins/cordova-plugin-file/www/FileWriter.js",
"pluginId": "cordova-plugin-file",
"clobbers": [
"window.FileWriter"
]
},
{
"id": "cordova-plugin-file.Flags",
"file": "plugins/cordova-plugin-file/www/Flags.js",
"pluginId": "cordova-plugin-file",
"clobbers": [
"window.Flags"
]
},
{
"id": "cordova-plugin-file.LocalFileSystem",
"file": "plugins/cordova-plugin-file/www/LocalFileSystem.js",
"pluginId": "cordova-plugin-file",
"clobbers": [
"window.LocalFileSystem"
],
"merges": [
"window"
]
},
{
"id": "cordova-plugin-file.Metadata",
"file": "plugins/cordova-plugin-file/www/Metadata.js",
"pluginId": "cordova-plugin-file",
"clobbers": [
"window.Metadata"
]
},
{
"id": "cordova-plugin-file.ProgressEvent",
"file": "plugins/cordova-plugin-file/www/ProgressEvent.js",
"pluginId": "cordova-plugin-file",
"clobbers": [
"window.ProgressEvent"
]
},
{
"id": "cordova-plugin-file.fileSystems",
"file": "plugins/cordova-plugin-file/www/fileSystems.js",
"pluginId": "cordova-plugin-file"
},
{
"id": "cordova-plugin-file.requestFileSystem",
"file": "plugins/cordova-plugin-file/www/requestFileSystem.js",
"pluginId": "cordova-plugin-file",
"clobbers": [
"window.requestFileSystem"
]
},
{
"id": "cordova-plugin-file.resolveLocalFileSystemURI",
"file": "plugins/cordova-plugin-file/www/resolveLocalFileSystemURI.js",
"pluginId": "cordova-plugin-file",
"merges": [
"window"
]
},
{
"id": "cordova-plugin-file.isChrome",
"file": "plugins/cordova-plugin-file/www/browser/isChrome.js",
"pluginId": "cordova-plugin-file",
"runs": true
},
{
"id": "cordova-plugin-file.androidFileSystem",
"file": "plugins/cordova-plugin-file/www/android/FileSystem.js",
"pluginId": "cordova-plugin-file",
"merges": [
"FileSystem"
]
},
{
"id": "cordova-plugin-file.fileSystems-roots",
"file": "plugins/cordova-plugin-file/www/fileSystems-roots.js",
"pluginId": "cordova-plugin-file",
"runs": true
},
{
"id": "cordova-plugin-file.fileSystemPaths",
"file": "plugins/cordova-plugin-file/www/fileSystemPaths.js",
"pluginId": "cordova-plugin-file",
"merges": [
"cordova"
],
"runs": true
},
{
"id": "cordova-plugin-file-transfer.FileTransferError",
"file": "plugins/cordova-plugin-file-transfer/www/FileTransferError.js",
"pluginId": "cordova-plugin-file-transfer",
"clobbers": [
"window.FileTransferError"
]
},
{
"id": "cordova-plugin-file-transfer.FileTransfer",
"file": "plugins/cordova-plugin-file-transfer/www/FileTransfer.js",
"pluginId": "cordova-plugin-file-transfer",
"clobbers": [
"window.FileTransfer"
]
},
{
"id": "com-darryncampbell-cordova-plugin-intent.IntentShim",
"file": "plugins/com-darryncampbell-cordova-plugin-intent/www/IntentShim.js",
"pluginId": "com-darryncampbell-cordova-plugin-intent",
"clobbers": [
"intentShim"
]
},
{
"id": "cordova-plugin-inappbrowser.inappbrowser",
"file": "plugins/cordova-plugin-inappbrowser/www/inappbrowser.js",
"pluginId": "cordova-plugin-inappbrowser",
"clobbers": [
"cordova.InAppBrowser.open",
"window.open"
]
},
{
"id": "cordova-plugin-statusbar.statusbar",
"file": "plugins/cordova-plugin-statusbar/www/statusbar.js",
"pluginId": "cordova-plugin-statusbar",
"clobbers": [
"window.StatusBar"
]
},
{
"id": "cordova-plugin-splashscreen.SplashScreen",
"file": "plugins/cordova-plugin-splashscreen/www/splashscreen.js",
"pluginId": "cordova-plugin-splashscreen",
"clobbers": [
"navigator.splashscreen"
]
},
{
"id": "cordova-plugin-device.device",
"file": "plugins/cordova-plugin-device/www/device.js",
"pluginId": "cordova-plugin-device",
"clobbers": [
"device"
]
},
{
"id": "cordova-plugin-filepath.FilePath",
"file": "plugins/cordova-plugin-filepath/www/FilePath.js",
"pluginId": "cordova-plugin-filepath",
"clobbers": [
"window.FilePath"
]
}
],
"plugin_metadata": {
"cordova-plugin-filechooser": "1.0.1",
"cordova-plugin-share-content": "1.0.0",
"ionic-plugin-keyboard": "2.2.1",
"cordova-plugin-file": "6.0.1",
"cordova-plugin-file-transfer": "1.7.1",
"com-darryncampbell-cordova-plugin-intent": "1.1.0",
"cordova-plugin-inappbrowser": "3.0.0",
"cordova-plugin-whitelist": "1.3.3",
"cordova-plugin-statusbar": "2.4.2",
"cordova-plugin-splashscreen": "5.0.2",
"cordova-plugin-device": "2.0.2",
"cordova-plugin-console": "1.1.0",
"cordova-android-support-gradle-release": "1.4.4",
"cordova-plugin-filepath": "1.0.2"
}
}

View file

@ -0,0 +1,327 @@
/*
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.
*/
apply plugin: 'com.android.application'
buildscript {
repositories {
mavenCentral()
jcenter()
maven {
url "https://maven.google.com"
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
}
}
// Allow plugins to declare Maven dependencies via build-extras.gradle.
allprojects {
repositories {
mavenCentral();
jcenter()
}
}
task wrapper(type: Wrapper) {
gradleVersion = '4.1.0'
}
// 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 = null;
}
// The value for android.buildToolsVersion.
if (!project.hasProperty('cdvBuildToolsVersion')) {
cdvBuildToolsVersion = null;
}
// 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 = null
}
// Whether to append a 0 "abi digit" to versionCode when only a single APK is build
if (!project.hasProperty('cdvVersionCodeForceAbiDigit')) {
cdvVersionCodeForceAbiDigit = null
}
// .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
}
// Plugin gradle extensions can append to this to have code run at the end.
cdvPluginPostBuildExtras = []
}
// PLUGIN GRADLE EXTENSIONS START
apply from: "../cordova-android-support-gradle-release/goober_m-cordova-android-support-gradle-release.gradle"
// PLUGIN GRADLE EXTENSIONS END
def hasBuildExtras = file('build-extras.gradle').exists()
if (hasBuildExtras) {
apply from: 'build-extras.gradle'
}
// Set property defaults after extension .gradle files.
if (ext.cdvCompileSdkVersion == null) {
ext.cdvCompileSdkVersion = privateHelpers.getProjectTarget()
//ext.cdvCompileSdkVersion = project.ext.defaultCompileSdkVersion
}
if (ext.cdvBuildToolsVersion == null) {
ext.cdvBuildToolsVersion = privateHelpers.findLatestInstalledBuildTools()
//ext.cdvBuildToolsVersion = project.ext.defaultBuildToolsVersion
}
if (ext.cdvDebugSigningPropertiesFile == null && file('../debug-signing.properties').exists()) {
ext.cdvDebugSigningPropertiesFile = '../debug-signing.properties'
}
if (ext.cdvReleaseSigningPropertiesFile == null && file('../release-signing.properties').exists()) {
ext.cdvReleaseSigningPropertiesFile = '../release-signing.properties'
}
// Cast to appropriate types.
ext.cdvBuildMultipleApks = cdvBuildMultipleApks == null ? false : cdvBuildMultipleApks.toBoolean();
ext.cdvVersionCodeForceAbiDigit = cdvVersionCodeForceAbiDigit == null ? false : cdvVersionCodeForceAbiDigit.toBoolean();
ext.cdvMinSdkVersion = cdvMinSdkVersion == null ? null : defaultMinSdkVersion
ext.cdvVersionCode = cdvVersionCode == null ? null : Integer.parseInt('' + cdvVersionCode)
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('cdvVersionCodeForceAbiDigit=' + cdvVersionCodeForceAbiDigit)
println('cdvMinSdkVersion=' + cdvMinSdkVersion)
println('cdvBuildMultipleApks=' + cdvBuildMultipleApks)
println('cdvReleaseSigningPropertiesFile=' + cdvReleaseSigningPropertiesFile)
println('cdvDebugSigningPropertiesFile=' + cdvDebugSigningPropertiesFile)
println('cdvBuildArch=' + cdvBuildArch)
println('computedVersionCode=' + android.defaultConfig.versionCode)
android.productFlavors.each { flavor ->
println('computed' + flavor.name.capitalize() + 'VersionCode=' + flavor.versionCode)
}
}
android {
defaultConfig {
versionCode cdvVersionCode ?: new BigInteger("" + privateHelpers.extractIntFromManifest("versionCode"))
applicationId privateHelpers.extractStringFromManifest("package")
if (cdvMinSdkVersion != null) {
minSdkVersion cdvMinSdkVersion
}
}
lintOptions {
abortOnError false;
}
compileSdkVersion cdvCompileSdkVersion
buildToolsVersion cdvBuildToolsVersion
//This code exists for Crosswalk and other Native APIs.
//By default, we multiply the existing version code in the Android Manifest by 10 and
//add a number for each architecture. If you are not using Crosswalk or SQLite, you can
//ignore this chunk of code, and your version codes will be respected.
if (Boolean.valueOf(cdvBuildMultipleApks)) {
flavorDimensions "default"
productFlavors {
armeabi {
versionCode defaultConfig.versionCode*10 + 1
ndk {
abiFilters = ["armeabi"]
}
}
armv7 {
versionCode defaultConfig.versionCode*10 + 2
ndk {
abiFilters = ["armeabi-v7a"]
}
}
arm64 {
versionCode defaultConfig.versionCode*10 + 3
ndk {
abiFilters = ["arm64-v8a"]
}
}
x86 {
versionCode defaultConfig.versionCode*10 + 4
ndk {
abiFilters = ["x86"]
}
}
x86_64 {
versionCode defaultConfig.versionCode*10 + 5
ndk {
abiFilters = ["x86_64"]
}
}
}
} else if (Boolean.valueOf(cdvVersionCodeForceAbiDigit)) {
// This provides compatibility to the default logic for versionCode before cordova-android 5.2.0
defaultConfig {
versionCode defaultConfig.versionCode*10
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
if (cdvReleaseSigningPropertiesFile) {
signingConfigs {
release {
// These must be set or Gradle will complain (even if they are overridden).
keyAlias = ""
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 = "__unset"
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
addSigningProps(cdvReleaseSigningPropertiesFile, signingConfigs.release)
}
if (cdvDebugSigningPropertiesFile) {
addSigningProps(cdvDebugSigningPropertiesFile, signingConfigs.debug)
}
}
/*
* WARNING: Cordova Lib and platform scripts do management inside of this code here,
* if you are adding the dependencies manually, do so outside the comments, otherwise
* the Cordova tools will overwrite them
*/
dependencies {
implementation fileTree(dir: 'libs', include: '*.jar')
// SUB-PROJECT DEPENDENCIES START
implementation(project(path: ":CordovaLib"))
compile "com.android.support:support-v4:27.+"
compile "com.android.support:support-v4:27.+"
compile "com.android.support:appcompat-v7:27.+"
// SUB-PROJECT DEPENDENCIES END
}
def promptForReleaseKeyPassword() {
if (!cdvReleaseSigningPropertiesFile) {
return;
}
if ('__unset'.equals(android.signingConfigs.release.storePassword)) {
android.signingConfigs.release.storePassword = privateHelpers.promptForPassword('Enter key store password: ')
}
if ('__unset'.equals(android.signingConfigs.release.keyPassword)) {
android.signingConfigs.release.keyPassword = privateHelpers.promptForPassword('Enter key password: ');
}
}
gradle.taskGraph.whenReady { taskGraph ->
taskGraph.getAllTasks().each() { task ->
if(['validateReleaseSigning', 'validateSigningRelease', 'validateSigningArmv7Release', 'validateSigningX76Release'].contains(task.name)) {
promptForReleaseKeyPassword()
}
}
}
def addSigningProps(propsFilePath, signingConfig) {
def propsFile = file(propsFilePath)
def props = new Properties()
propsFile.withReader { reader ->
props.load(reader)
}
def storeFile = new File(props.get('key.store') ?: 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 = props.get('key.alias') ?: privateHelpers.ensureValueExists(propsFilePath, props, 'keyAlias')
signingConfig.keyPassword = props.get('keyPassword', props.get('key.alias.password', signingConfig.keyPassword))
signingConfig.storeFile = storeFile
signingConfig.storePassword = props.get('storePassword', props.get('key.store.password', signingConfig.storePassword))
def storeType = props.get('storeType', props.get('key.store.type', ''))
if (!storeType) {
def filename = storeFile.getName().toLowerCase();
if (filename.endsWith('.p12') || filename.endsWith('.pfx')) {
storeType = 'pkcs12'
} else {
storeType = signingConfig.storeType // "jks"
}
}
signingConfig.storeType = storeType
}
for (def func : cdvPluginPostBuildExtras) {
func()
}
// This can be defined within build-extras.gradle as:
// ext.postBuildExtras = { ... code here ... }
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}

View file

@ -0,0 +1 @@
ext {ANDROID_SUPPORT_VERSION = "27.+"}

View file

@ -0,0 +1,24 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest android:hardwareAccelerated="true" android:versionCode="602" android:versionName="0.6.2" package="com.monkeystew.goober_m" xmlns:android="http://schemas.android.com/apk/res/android">
<supports-screens android:anyDensity="true" android:largeScreens="true" android:normalScreens="true" android:resizeable="true" android:smallScreens="true" android:xlargeScreens="true" />
<uses-permission android:name="android.permission.INTERNET" />
<application android:hardwareAccelerated="true" android:icon="@mipmap/icon" android:label="@string/app_name" android:supportsRtl="true">
<activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale" android:label="@string/activity_name" android:launchMode="singleTop" android:name="MainActivity" android:theme="@android:style/Theme.DeviceDefault.NoActionBar" android:windowSoftInputMode="adjustResize">
<intent-filter android:label="@string/launcher_name">
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="com.darryncampbell.cordova.plugin.intent.ACTION" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<provider android:authorities="${applicationId}.provider" android:exported="false" android:grantUriPermissions="true" android:name="android.support.v4.content.FileProvider">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths" />
</provider>
</application>
<uses-sdk android:minSdkVersion="19" android:targetSdkVersion="27" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
</manifest>

View file

@ -0,0 +1,794 @@
package com.darryncampbell.cordova.plugin.intent;
import android.Manifest;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.FileProvider;
import android.text.Html;
import android.util.Log;
import android.webkit.MimeTypeMap;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaActivity;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaResourceApi;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import static android.os.Environment.getExternalStorageDirectory;
import static android.os.Environment.getExternalStorageState;
public class IntentShim extends CordovaPlugin {
private static final String LOG_TAG = "Cordova Intents Shim";
private CallbackContext onNewIntentCallbackContext = null;
private CallbackContext onBroadcastCallbackContext = null;
private CallbackContext onActivityResultCallbackContext = null;
private Intent deferredIntent = null;
public IntentShim() {
}
public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException
{
Log.d(LOG_TAG, "Action: " + action);
if (action.equals("startActivity") || action.equals("startActivityForResult"))
{
// Credit: https://github.com/chrisekelley/cordova-webintent
if (args.length() != 1) {
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.INVALID_ACTION));
return false;
}
JSONObject obj = args.getJSONObject(0);
Intent intent = populateIntent(obj, callbackContext);
int requestCode = obj.has("requestCode") ? obj.getInt("requestCode") : 1;
boolean bExpectResult = false;
if (action.equals("startActivityForResult"))
{
bExpectResult = true;
this.onActivityResultCallbackContext = callbackContext;
}
startActivity(intent, bExpectResult, requestCode, callbackContext);
return true;
}
else if (action.equals("sendBroadcast"))
{
// Credit: https://github.com/chrisekelley/cordova-webintent
if (args.length() != 1) {
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.INVALID_ACTION));
return false;
}
// Parse the arguments
JSONObject obj = args.getJSONObject(0);
Intent intent = populateIntent(obj, callbackContext);
sendBroadcast(intent);
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK));
return true;
}
else if (action.equals("startService"))
{
if (args.length() != 1) {
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.INVALID_ACTION));
return false;
}
JSONObject obj = args.getJSONObject(0);
Intent intent = populateIntent(obj, callbackContext);
startService(intent);
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK));
return true;
}
else if (action.equals("registerBroadcastReceiver")) {
try
{
// Ensure we only have a single registered broadcast receiver
((CordovaActivity)this.cordova.getActivity()).unregisterReceiver(myBroadcastReceiver);
}
catch (IllegalArgumentException e) {}
// No error callback
if(args.length() != 1) {
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.INVALID_ACTION));
return false;
}
// Expect an array of filterActions
JSONObject obj = args.getJSONObject(0);
JSONArray filterActions = obj.has("filterActions") ? obj.getJSONArray("filterActions") : null;
if (filterActions == null || filterActions.length() == 0)
{
// The arguments are not correct
Log.w(LOG_TAG, "filterActions argument is not in the expected format");
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.INVALID_ACTION));
return false;
}
this.onBroadcastCallbackContext = callbackContext;
PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
result.setKeepCallback(true);
IntentFilter filter = new IntentFilter();
for (int i = 0; i < filterActions.length(); i++) {
Log.d(LOG_TAG, "Registering broadcast receiver for filter: " + filterActions.getString(i));
filter.addAction(filterActions.getString(i));
}
// Allow an array of filterCategories
JSONArray filterCategories = obj.has("filterCategories") ? obj.getJSONArray("filterCategories") : null;
if (filterCategories != null) {
for (int i = 0; i < filterCategories.length(); i++) {
Log.d(LOG_TAG, "Registering broadcast receiver for category filter: " + filterCategories.getString(i));
filter.addCategory(filterCategories.getString(i));
}
}
// Add any specified Data Schemes
// https://github.com/darryncampbell/darryncampbell-cordova-plugin-intent/issues/24
JSONArray filterDataSchemes = obj.has("filterDataSchemes") ? obj.getJSONArray("filterDataSchemes") : null;
if (filterDataSchemes != null && filterDataSchemes.length() > 0)
{
for (int i = 0; i < filterDataSchemes.length(); i++)
{
Log.d(LOG_TAG, "Associating data scheme to filter: " + filterDataSchemes.getString(i));
filter.addDataScheme(filterDataSchemes.getString(i));
}
}
((CordovaActivity)this.cordova.getActivity()).registerReceiver(myBroadcastReceiver, filter);
callbackContext.sendPluginResult(result);
}
else if (action.equals("unregisterBroadcastReceiver"))
{
try
{
((CordovaActivity)this.cordova.getActivity()).unregisterReceiver(myBroadcastReceiver);
}
catch (IllegalArgumentException e) {}
}
else if (action.equals("onIntent"))
{
// Credit: https://github.com/napolitano/cordova-plugin-intent
if(args.length() != 1) {
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.INVALID_ACTION));
return false;
}
this.onNewIntentCallbackContext = callbackContext;
if (this.deferredIntent != null) {
fireOnNewIntent(this.deferredIntent);
this.deferredIntent = null;
}
PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
result.setKeepCallback(true);
callbackContext.sendPluginResult(result);
return true;
}
else if (action.equals("onActivityResult"))
{
if(args.length() != 1) {
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.INVALID_ACTION));
return false;
}
this.onActivityResultCallbackContext = callbackContext;
PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
result.setKeepCallback(true);
callbackContext.sendPluginResult(result);
return true;
}
else if (action.equals("getIntent"))
{
// Credit: https://github.com/napolitano/cordova-plugin-intent
if(args.length() != 0) {
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.INVALID_ACTION));
return false;
}
Intent intent;
if (this.deferredIntent != null) {
intent = this.deferredIntent;
this.deferredIntent = null;
}
else {
intent = cordova.getActivity().getIntent();
}
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, getIntentJson(intent)));
return true;
}
else if (action.equals("sendResult"))
{
// Assuming this application was started with startActivityForResult, send the result back
// https://github.com/darryncampbell/darryncampbell-cordova-plugin-intent/issues/3
Intent result = new Intent();
if (args.length() > 0)
{
JSONObject json = args.getJSONObject(0);
JSONObject extras = (json.has("extras"))?json.getJSONObject("extras"):null;
// Populate the extras if any exist
if (extras != null) {
JSONArray extraNames = extras.names();
for (int i = 0; i < extraNames.length(); i++) {
String key = extraNames.getString(i);
String value = extras.getString(key);
result.putExtra(key, value);
}
}
}
//set result
cordova.getActivity().setResult(Activity.RESULT_OK, result);
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK));
//finish the activity
cordova.getActivity().finish();
}
else if (action.equals("realPathFromUri"))
{
if (args.length() != 1) {
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.INVALID_ACTION));
return false;
}
JSONObject obj = args.getJSONObject(0);
String realPath = getRealPathFromURI_API19(obj, callbackContext);
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, realPath));
return true;
}
return true;
}
private Uri remapUriWithFileProvider(String uriAsString, final CallbackContext callbackContext)
{
// Create the URI via FileProvider Special case for N and above when installing apks
int permissionCheck = ContextCompat.checkSelfPermission(this.cordova.getActivity(),
Manifest.permission.READ_EXTERNAL_STORAGE);
if (permissionCheck != PackageManager.PERMISSION_GRANTED)
{
// Could do better here - if the app does not already have permission should
// only continue when we get the success callback from this.
ActivityCompat.requestPermissions(this.cordova.getActivity(),
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
callbackContext.error("Please grant read external storage permission");
return null;
}
try
{
String externalStorageState = getExternalStorageState();
if (externalStorageState.equals(Environment.MEDIA_MOUNTED) || externalStorageState.equals(Environment.MEDIA_MOUNTED_READ_ONLY)) {
String fileName = uriAsString.substring(uriAsString.indexOf('/') + 2, uriAsString.length());
File uriAsFile = new File(fileName);
boolean fileExists = uriAsFile.exists();
if (!fileExists)
{
Log.e(LOG_TAG, "File at path " + uriAsFile.getPath() + " with name " + uriAsFile.getName() + "does not exist");
callbackContext.error("File not found: " + uriAsFile.toString());
return null;
}
String PACKAGE_NAME = this.cordova.getActivity().getPackageName() + ".provider";
Uri uri = FileProvider.getUriForFile(this.cordova.getActivity().getApplicationContext(), PACKAGE_NAME, uriAsFile);
return uri;
}
else
{
Log.e(LOG_TAG, "Storage directory is not mounted. Please ensure the device is not connected via USB for file transfer");
callbackContext.error("Storage directory is returning not mounted");
return null;
}
}
catch(StringIndexOutOfBoundsException e)
{
Log.e(LOG_TAG, "URL is not well formed");
callbackContext.error("URL is not well formed");
return null;
}
}
private String getRealPathFromURI_API19(JSONObject obj, CallbackContext callbackContext) throws JSONException
{
// Credit: https://stackoverflow.com/questions/2789276/android-get-real-path-by-uri-getpath/2790688
Uri uri = obj.has("uri") ? Uri.parse(obj.getString("uri")) : null;
if (uri == null)
{
Log.w(LOG_TAG, "URI is not a specified parameter");
throw new JSONException("URI is not a specified parameter");
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
String filePath = "";
if (uri.getHost().contains("com.android.providers.media")) {
int permissionCheck = ContextCompat.checkSelfPermission(this.cordova.getActivity(),
Manifest.permission.READ_EXTERNAL_STORAGE);
if (permissionCheck != PackageManager.PERMISSION_GRANTED)
{
// Could do better here - if the app does not already have permission should
// only continue when we get the success callback from this.
ActivityCompat.requestPermissions(this.cordova.getActivity(),
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
callbackContext.error("Please grant read external storage permission");
return null;
}
// Image pick from recent
String wholeID = DocumentsContract.getDocumentId(uri);
// Split at colon, use second item in the array
String id = wholeID.split(":")[1];
String[] column = {MediaStore.Images.Media.DATA};
// where id is equal to
String sel = MediaStore.Images.Media._ID + "=?";
// This line requires read storage permission
Cursor cursor = this.cordova.getActivity().getApplicationContext().getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
column, sel, new String[]{id}, null);
int columnIndex = cursor.getColumnIndex(column[0]);
if (cursor.moveToFirst()) {
filePath = cursor.getString(columnIndex);
}
cursor.close();
return filePath;
} else {
// image pick from gallery
String[] proj = {MediaStore.Images.Media.DATA};
Cursor cursor = this.cordova.getActivity().getApplicationContext().getContentResolver().query(uri, proj, null, null, null);
int column_index
= cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
cursor.moveToFirst();
return cursor.getString(column_index);
}
}
return "Requires KK or higher";
}
private void startActivity(Intent i, boolean bExpectResult, int requestCode, CallbackContext callbackContext) {
if (i.resolveActivityInfo(this.cordova.getActivity().getPackageManager(), 0) != null)
{
if (bExpectResult)
{
cordova.setActivityResultCallback(this);
((CordovaActivity) this.cordova.getActivity()).startActivityForResult(i, requestCode);
}
else
{
((CordovaActivity)this.cordova.getActivity()).startActivity(i);
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK));
}
}
else
{
// Return an error as there is no app to handle this intent
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR));
}
}
private void sendBroadcast(Intent intent) {
((CordovaActivity)this.cordova.getActivity()).sendBroadcast(intent);
}
private void startService(Intent intent)
{
((CordovaActivity)this.cordova.getActivity()).startService(intent);
}
private Intent populateIntent(JSONObject obj, CallbackContext callbackContext) throws JSONException
{
// Credit: https://github.com/chrisekelley/cordova-webintent
// Credit: https://github.com/chrisekelley/cordova-webintent
String type = obj.has("type") ? obj.getString("type") : null;
String packageAssociated = obj.has("package") ? obj.getString("package") : null;
//Uri uri = obj.has("url") ? resourceApi.remapUri(Uri.parse(obj.getString("url"))) : null;
Uri uri = null;
final CordovaResourceApi resourceApi = webView.getResourceApi();
if (obj.has("url"))
{
String uriAsString = obj.getString("url");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && uriAsString.startsWith("file://"))
{
uri = remapUriWithFileProvider(uriAsString, callbackContext);
}
else
{
uri = resourceApi.remapUri(Uri.parse(obj.getString("url")));
}
}
JSONObject extras = obj.has("extras") ? obj.getJSONObject("extras") : null;
Map<String, Object> extrasMap = new HashMap<String, Object>();
Bundle bundle = null;
String bundleKey = "";
if (extras != null) {
JSONArray extraNames = extras.names();
for (int i = 0; i < extraNames.length(); i++) {
String key = extraNames.getString(i);
Object extrasObj = extras.get(key);
if (extrasObj instanceof JSONObject) {
// The extra is a bundle
bundleKey = key;
bundle = toBundle((JSONObject) extras.get(key));
} else {
extrasMap.put(key, extras.get(key));
}
}
}
String action = obj.has("action") ? obj.getString("action") : null;
Intent i = new Intent();
if (action != null)
i.setAction(action);
if (type != null && uri != null) {
i.setDataAndType(uri, type); //Fix the crash problem with android 2.3.6
} else {
if (type != null) {
i.setType(type);
}
if (uri != null)
{
i.setData(uri);
}
}
JSONObject component = obj.has("component") ? obj.getJSONObject("component") : null;
if (component != null)
{
// User has specified an explicit intent
String componentPackage = component.has("package") ? component.getString("package") : null;
String componentClass = component.has("class") ? component.getString("class") : null;
if (componentPackage == null || componentClass == null)
{
Log.w(LOG_TAG, "Component specified but missing corresponding package or class");
throw new JSONException("Component specified but missing corresponding package or class");
}
else
{
ComponentName componentName = new ComponentName(componentPackage, componentClass);
i.setComponent(componentName);
}
}
if (packageAssociated != null)
i.setPackage(packageAssociated);
JSONArray flags = obj.has("flags") ? obj.getJSONArray("flags") : null;
if (flags != null)
{
int length = flags.length();
for (int k = 0; k < length; k++)
{
i.addFlags(flags.getInt(k));
}
}
if (bundle != null)
i.putExtra(bundleKey, bundle);
for (String key : extrasMap.keySet()) {
Object value = extrasMap.get(key);
String valueStr = String.valueOf(value);
// If type is text html, the extra text must sent as HTML
if (key.equals(Intent.EXTRA_TEXT) && type.equals("text/html")) {
i.putExtra(key, Html.fromHtml(valueStr));
} else if (key.equals(Intent.EXTRA_STREAM)) {
// allows sharing of images as attachments.
// value in this case should be a URI of a file
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && valueStr.startsWith("file://"))
{
Uri uriOfStream = remapUriWithFileProvider(valueStr, callbackContext);
if (uriOfStream != null)
i.putExtra(key, uriOfStream);
}
else
{
//final CordovaResourceApi resourceApi = webView.getResourceApi();
i.putExtra(key, resourceApi.remapUri(Uri.parse(valueStr)));
}
} else if (key.equals(Intent.EXTRA_EMAIL)) {
// allows to add the email address of the receiver
i.putExtra(Intent.EXTRA_EMAIL, new String[] { valueStr });
} else {
if (value instanceof Boolean) {
i.putExtra(key, Boolean.valueOf(valueStr));
} else if(value instanceof Integer) {
i.putExtra(key, Integer.valueOf(valueStr));
} else if(value instanceof Long) {
i.putExtra(key, Long.valueOf(valueStr));
} else if(value instanceof Double) {
i.putExtra(key, Double.valueOf(valueStr));
} else {
i.putExtra(key, valueStr);
}
}
}
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
return i;
}
@Override
public void onNewIntent(Intent intent) {
if (this.onNewIntentCallbackContext != null) {
fireOnNewIntent(intent);
}
else {
// save the intent for use when onIntent action is called in the execute method
this.deferredIntent = intent;
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent)
{
super.onActivityResult(requestCode, resultCode, intent);
if (onActivityResultCallbackContext != null && intent != null)
{
intent.putExtra("requestCode", requestCode);
intent.putExtra("resultCode", resultCode);
PluginResult result = new PluginResult(PluginResult.Status.OK, getIntentJson(intent));
result.setKeepCallback(true);
onActivityResultCallbackContext.sendPluginResult(result);
}
else if (onActivityResultCallbackContext != null)
{
Intent canceledIntent = new Intent();
canceledIntent.putExtra("requestCode", requestCode);
canceledIntent.putExtra("resultCode", resultCode);
PluginResult canceledResult = new PluginResult(PluginResult.Status.OK, getIntentJson(canceledIntent));
canceledResult.setKeepCallback(true);
onActivityResultCallbackContext.sendPluginResult(canceledResult);
}
}
private BroadcastReceiver myBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (onBroadcastCallbackContext != null)
{
PluginResult result = new PluginResult(PluginResult.Status.OK, getIntentJson(intent));
result.setKeepCallback(true);
onBroadcastCallbackContext.sendPluginResult(result);
}
}
};
/**
* Sends the provided Intent to the onNewIntentCallbackContext.
*
* @param intent This is the intent to send to the JS layer.
*/
private void fireOnNewIntent(Intent intent) {
PluginResult result = new PluginResult(PluginResult.Status.OK, getIntentJson(intent));
result.setKeepCallback(true);
this.onNewIntentCallbackContext.sendPluginResult(result);
}
/**
* Return JSON representation of intent attributes
*
* @param intent
* Credit: https://github.com/napolitano/cordova-plugin-intent
*/
private JSONObject getIntentJson(Intent intent) {
JSONObject intentJSON = null;
ClipData clipData = null;
JSONObject[] items = null;
ContentResolver cR = this.cordova.getActivity().getApplicationContext().getContentResolver();
MimeTypeMap mime = MimeTypeMap.getSingleton();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
clipData = intent.getClipData();
if(clipData != null) {
int clipItemCount = clipData.getItemCount();
items = new JSONObject[clipItemCount];
for (int i = 0; i < clipItemCount; i++) {
ClipData.Item item = clipData.getItemAt(i);
try {
items[i] = new JSONObject();
items[i].put("htmlText", item.getHtmlText());
items[i].put("intent", item.getIntent());
items[i].put("text", item.getText());
items[i].put("uri", item.getUri());
if (item.getUri() != null) {
String type = cR.getType(item.getUri());
String extension = mime.getExtensionFromMimeType(cR.getType(item.getUri()));
items[i].put("type", type);
items[i].put("extension", extension);
}
} catch (JSONException e) {
Log.d(LOG_TAG, " Error thrown during intent > JSON conversion");
Log.d(LOG_TAG, e.getMessage());
Log.d(LOG_TAG, Arrays.toString(e.getStackTrace()));
}
}
}
}
try {
intentJSON = new JSONObject();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if(items != null) {
intentJSON.put("clipItems", new JSONArray(items));
}
}
intentJSON.put("type", intent.getType());
intentJSON.put("extras", toJsonObject(intent.getExtras()));
intentJSON.put("action", intent.getAction());
intentJSON.put("categories", intent.getCategories());
intentJSON.put("flags", intent.getFlags());
intentJSON.put("component", intent.getComponent());
intentJSON.put("data", intent.getData());
intentJSON.put("package", intent.getPackage());
return intentJSON;
} catch (JSONException e) {
Log.d(LOG_TAG, " Error thrown during intent > JSON conversion");
Log.d(LOG_TAG, e.getMessage());
Log.d(LOG_TAG, Arrays.toString(e.getStackTrace()));
return null;
}
}
private static JSONObject toJsonObject(Bundle bundle) {
// Credit: https://github.com/napolitano/cordova-plugin-intent
try {
return (JSONObject) toJsonValue(bundle);
} catch (JSONException e) {
throw new IllegalArgumentException("Cannot convert bundle to JSON: " + e.getMessage(), e);
}
}
private static Object toJsonValue(final Object value) throws JSONException {
// Credit: https://github.com/napolitano/cordova-plugin-intent
if (value == null) {
return null;
} else if (value instanceof Bundle) {
final Bundle bundle = (Bundle) value;
final JSONObject result = new JSONObject();
for (final String key : bundle.keySet()) {
result.put(key, toJsonValue(bundle.get(key)));
}
return result;
} else if ((value.getClass().isArray())) {
final JSONArray result = new JSONArray();
int length = Array.getLength(value);
for (int i = 0; i < length; ++i) {
result.put(i, toJsonValue(Array.get(value, i)));
}
return result;
}
else if (value instanceof ArrayList<?>) {
final ArrayList arrayList = (ArrayList<?>)value;
final JSONArray result = new JSONArray();
for (int i = 0; i < arrayList.size(); i++)
result.put(toJsonValue(arrayList.get(i)));
return result;
}
else if (
value instanceof String
|| value instanceof Boolean
|| value instanceof Integer
|| value instanceof Long
|| value instanceof Double) {
return value;
} else {
return String.valueOf(value);
}
}
private Bundle toBundle(final JSONObject obj) {
Bundle returnBundle = new Bundle();
if (obj == null) {
return null;
}
try {
Iterator<?> keys = obj.keys();
while(keys.hasNext())
{
String key = (String)keys.next();
Object compare = obj.get(key);
if (obj.get(key) instanceof String)
returnBundle.putString(key, obj.getString(key));
else if (obj.get(key) instanceof Boolean)
returnBundle.putBoolean(key, obj.getBoolean(key));
else if (obj.get(key) instanceof Integer)
returnBundle.putInt(key, obj.getInt(key));
else if (obj.get(key) instanceof Long)
returnBundle.putLong(key, obj.getLong(key));
else if (obj.get(key) instanceof Double)
returnBundle.putDouble(key, obj.getDouble(key));
else if (obj.get(key).getClass().isArray() || obj.get(key) instanceof JSONArray)
{
JSONArray jsonArray = obj.getJSONArray(key);
int length = jsonArray.length();
if (jsonArray.get(0) instanceof String)
{
String[] stringArray = new String[length];
for (int j = 0; j < length; j++)
stringArray[j] = jsonArray.getString(j);
returnBundle.putStringArray(key, stringArray);
//returnBundle.putParcelableArray(key, obj.get);
}
else
{
Bundle[] bundleArray = new Bundle[length];
for (int k = 0; k < length ; k++)
bundleArray[k] = toBundle(jsonArray.getJSONObject(k));
returnBundle.putParcelableArray(key, bundleArray);
}
}
else if (obj.get(key) instanceof JSONObject)
returnBundle.putBundle(key, toBundle((JSONObject)obj.get(key)));
}
}
catch (JSONException e) {
e.printStackTrace();
}
return returnBundle;
}
}

View file

@ -0,0 +1,62 @@
package com.ferdinandsilva.android;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ContentValues;
import android.net.Uri;
import android.app.Activity;
import android.telephony.SmsManager;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaInterface;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class ShareContentPlugin extends CordovaPlugin {
public static final String TAG = "ShareContentPlugin";
public static Context thisContext;
public ShareContentPlugin() {
}
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
super.initialize(cordova, webView);
ShareContentPlugin.thisContext = cordova.getActivity().getApplicationContext();
}
public void share(String text_to_share, CallbackContext callbackContext) {
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_TEXT, text_to_share);
shareIntent.setType("text/plain");
shareIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
ShareContentPlugin.thisContext.startActivity(shareIntent);
callbackContext.success(text_to_share);
}
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
if ("share".equals(action)) {
share(args.get(0).toString(), callbackContext);
} else {
return false;
}
return true;
}
}

View file

@ -0,0 +1,431 @@
package com.hiddentao.cordova.filepath;
import android.Manifest;
import android.content.ContentUris;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.util.Log;
import android.database.Cursor;
import android.os.Build;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.support.v4.app.ActivityCompat;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PermissionHelper;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.List;
import java.io.File;
public class FilePath extends CordovaPlugin {
private static final String TAG = "[FilePath plugin]: ";
private static final int INVALID_ACTION_ERROR_CODE = -1;
private static final int GET_PATH_ERROR_CODE = 0;
private static final String GET_PATH_ERROR_ID = null;
private static final int GET_CLOUD_PATH_ERROR_CODE = 1;
private static final String GET_CLOUD_PATH_ERROR_ID = "cloud";
private static final int RC_READ_EXTERNAL_STORAGE = 5;
private static CallbackContext callback;
private static String uriStr;
public static final int READ_REQ_CODE = 0;
public static final String READ = Manifest.permission.READ_EXTERNAL_STORAGE;
protected void getReadPermission(int requestCode) {
PermissionHelper.requestPermission(this, requestCode, READ);
}
public void initialize(CordovaInterface cordova, final CordovaWebView webView) {
super.initialize(cordova, webView);
}
/**
* Executes the request and returns PluginResult.
*
* @param action The action to execute.
* @param args JSONArry of arguments for the plugin.
* @param callbackContext The callback context through which to return stuff to caller.
* @return A PluginResult object with a status and message.
*/
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
this.callback = callbackContext;
this.uriStr = args.getString(0);
if (action.equals("resolveNativePath")) {
if (PermissionHelper.hasPermission(this, READ)) {
resolveNativePath();
}
else {
getReadPermission(READ_REQ_CODE);
}
return true;
}
else {
JSONObject resultObj = new JSONObject();
resultObj.put("code", INVALID_ACTION_ERROR_CODE);
resultObj.put("message", "Invalid action.");
callbackContext.error(resultObj);
}
return false;
}
public void resolveNativePath() throws JSONException {
JSONObject resultObj = new JSONObject();
/* content:///... */
Uri pvUrl = Uri.parse(this.uriStr);
Log.d(TAG, "URI: " + this.uriStr);
Context appContext = this.cordova.getActivity().getApplicationContext();
String filePath = getPath(appContext, pvUrl);
//check result; send error/success callback
if (filePath == GET_PATH_ERROR_ID) {
resultObj.put("code", GET_PATH_ERROR_CODE);
resultObj.put("message", "Unable to resolve filesystem path.");
this.callback.error(resultObj);
}
else if (filePath.equals(GET_CLOUD_PATH_ERROR_ID)) {
resultObj.put("code", GET_CLOUD_PATH_ERROR_CODE);
resultObj.put("message", "Files from cloud cannot be resolved to filesystem, download is required.");
this.callback.error(resultObj);
}
else {
Log.d(TAG, "Filepath: " + filePath);
this.callback.success("file://" + filePath);
}
}
public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException {
for (int r:grantResults) {
if (r == PackageManager.PERMISSION_DENIED) {
JSONObject resultObj = new JSONObject();
resultObj.put("code", 3);
resultObj.put("message", "Filesystem permission was denied.");
this.callback.error(resultObj);
return;
}
}
if (requestCode == READ_REQ_CODE) {
resolveNativePath();
}
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
private static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
private static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
private static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
private static boolean isGooglePhotosUri(Uri uri) {
return ("com.google.android.apps.photos.content".equals(uri.getAuthority())
|| "com.google.android.apps.photos.contentprovider".equals(uri.getAuthority()));
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Drive.
*/
private static boolean isGoogleDriveUri(Uri uri) {
return "com.google.android.apps.docs.storage".equals(uri.getAuthority()) || "com.google.android.apps.docs.storage.legacy".equals(uri.getAuthority());
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
*/
private static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String[] projection = {
column
};
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
null);
if (cursor != null && cursor.moveToFirst()) {
final int column_index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(column_index);
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
/**
* Get content:// from segment list
* In the new Uri Authority of Google Photos, the last segment is not the content:// anymore
* So let's iterate through all segments and find the content uri!
*
* @param segments The list of segment
*/
private static String getContentFromSegments(List<String> segments) {
String contentPath = "";
for(String item : segments) {
if (item.startsWith("content://")) {
contentPath = item;
break;
}
}
return contentPath;
}
/**
* Check if a file exists on device
*
* @param filePath The absolute file path
*/
private static boolean fileExists(String filePath) {
File file = new File(filePath);
return file.exists();
}
/**
* Get full file path from external storage
*
* @param pathData The storage type and the relative path
*/
private static String getPathFromExtSD(String[] pathData) {
final String type = pathData[0];
final String relativePath = "/" + pathData[1];
String fullPath = "";
// on my Sony devices (4.4.4 & 5.1.1), `type` is a dynamic string
// something like "71F8-2C0A", some kind of unique id per storage
// don't know any API that can get the root path of that storage based on its id.
//
// so no "primary" type, but let the check here for other devices
if ("primary".equalsIgnoreCase(type)) {
fullPath = Environment.getExternalStorageDirectory() + relativePath;
if (fileExists(fullPath)) {
return fullPath;
}
}
// Environment.isExternalStorageRemovable() is `true` for external and internal storage
// so we cannot relay on it.
//
// instead, for each possible path, check if file exists
// we'll start with secondary storage as this could be our (physically) removable sd card
fullPath = System.getenv("SECONDARY_STORAGE") + relativePath;
if (fileExists(fullPath)) {
return fullPath;
}
fullPath = System.getenv("EXTERNAL_STORAGE") + relativePath;
if (fileExists(fullPath)) {
return fullPath;
}
return fullPath;
}
/**
* Get a file path from a Uri. This will get the the path for Storage Access
* Framework Documents, as well as the _data field for the MediaStore and
* other file-based ContentProviders.<br>
* <br>
* Callers should check whether the path is local before assuming it
* represents a local file.
*
* @param context The context.
* @param uri The Uri to query.
*/
private static String getPath(final Context context, final Uri uri) {
Log.d(TAG, "File - " +
"Authority: " + uri.getAuthority() +
", Fragment: " + uri.getFragment() +
", Port: " + uri.getPort() +
", Query: " + uri.getQuery() +
", Scheme: " + uri.getScheme() +
", Host: " + uri.getHost() +
", Segments: " + uri.getPathSegments().toString()
);
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
String fullPath = getPathFromExtSD(split);
if (fullPath != "") {
return fullPath;
}
else {
return null;
}
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[] {
split[1]
};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
else if(isGoogleDriveUri(uri)){
return getDriveFilePath(uri,context);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
// Return the remote address
if (isGooglePhotosUri(uri)) {
String contentPath = getContentFromSegments(uri.getPathSegments());
if (contentPath != "") {
return getPath(context, Uri.parse(contentPath));
}
else {
return null;
}
}
if(isGoogleDriveUri(uri)){
return getDriveFilePath(uri,context);
}
return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
private static String getDriveFilePath(Uri uri,Context context){
Uri returnUri =uri;
Cursor returnCursor = context.getContentResolver().query(returnUri, null, null, null, null);
/*
* Get the column indexes of the data in the Cursor,
* * move to the first row in the Cursor, get the data,
* * and display it.
* */
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE);
returnCursor.moveToFirst();
String name = (returnCursor.getString(nameIndex));
String size = (Long.toString(returnCursor.getLong(sizeIndex)));
File file = new File(context.getCacheDir(),name);
try {
InputStream inputStream = context.getContentResolver().openInputStream(uri);
FileOutputStream outputStream = new FileOutputStream(file);
int read = 0;
int maxBufferSize = 1 * 1024 * 1024;
int bytesAvailable = inputStream.available();
//int bufferSize = 1024;
int bufferSize = Math.min(bytesAvailable, maxBufferSize);
final byte[] buffers = new byte[bufferSize];
while ((read = inputStream.read(buffers)) != -1) {
outputStream.write(buffers, 0, read);
}
Log.e("File Size","Size " + file.length());
inputStream.close();
outputStream.close();
Log.e("File Path","Path " + file.getPath());
Log.e("File Size","Size " + file.length());
}catch (Exception e){
Log.e("Exception",e.getMessage());
}
return file.getPath();
}
}

View file

@ -0,0 +1,82 @@
package com.megster.cordova;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import org.apache.cordova.CordovaArgs;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.PluginResult;
import org.json.JSONException;
public class FileChooser extends CordovaPlugin {
private static final String TAG = "FileChooser";
private static final String ACTION_OPEN = "open";
private static final int PICK_FILE_REQUEST = 1;
CallbackContext callback;
@Override
public boolean execute(String action, CordovaArgs args, CallbackContext callbackContext) throws JSONException {
if (action.equals(ACTION_OPEN)) {
chooseFile(callbackContext);
return true;
}
return false;
}
public void chooseFile(CallbackContext callbackContext) {
// type and title should be configurable
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
Intent chooser = Intent.createChooser(intent, "Select File");
cordova.startActivityForResult(this, chooser, PICK_FILE_REQUEST);
PluginResult pluginResult = new PluginResult(PluginResult.Status.NO_RESULT);
pluginResult.setKeepCallback(true);
callback = callbackContext;
callbackContext.sendPluginResult(pluginResult);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == PICK_FILE_REQUEST && callback != null) {
if (resultCode == Activity.RESULT_OK) {
Uri uri = data.getData();
if (uri != null) {
Log.w(TAG, uri.toString());
callback.success(uri.toString());
} else {
callback.error("File uri was null");
}
} else if (resultCode == Activity.RESULT_CANCELED) {
// TODO NO_RESULT or error callback?
PluginResult pluginResult = new PluginResult(PluginResult.Status.NO_RESULT);
callback.sendPluginResult(pluginResult);
} else {
callback.error(resultCode);
}
}
}
}

View file

@ -0,0 +1,41 @@
/*
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 com.monkeystew.goober_m;
import android.os.Bundle;
import org.apache.cordova.*;
public class MainActivity extends CordovaActivity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// enable Cordova apps to be started in the background
Bundle extras = getIntent().getExtras();
if (extras != null && extras.getBoolean("cdvStartInBackground", false)) {
moveTaskToBack(true);
}
// Set by <content src="index.html" /> in config.xml
loadUrl(launchUrl);
}
}

View file

@ -0,0 +1,130 @@
package io.ionic.keyboard;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginResult;
import org.apache.cordova.PluginResult.Status;
import org.json.JSONArray;
import org.json.JSONException;
import android.content.Context;
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.inputmethod.InputMethodManager;
// import additionally required classes for calculating screen height
import android.view.Display;
import android.graphics.Point;
import android.os.Build;
public class IonicKeyboard extends CordovaPlugin {
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
super.initialize(cordova, webView);
}
public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
if ("close".equals(action)) {
cordova.getThreadPool().execute(new Runnable() {
public void run() {
//http://stackoverflow.com/a/7696791/1091751
InputMethodManager inputManager = (InputMethodManager) cordova.getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
View v = cordova.getActivity().getCurrentFocus();
if (v == null) {
callbackContext.error("No current focus");
} else {
inputManager.hideSoftInputFromWindow(v.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
callbackContext.success(); // Thread-safe.
}
}
});
return true;
}
if ("show".equals(action)) {
cordova.getThreadPool().execute(new Runnable() {
public void run() {
((InputMethodManager) cordova.getActivity().getSystemService(Context.INPUT_METHOD_SERVICE)).toggleSoftInput(0, InputMethodManager.HIDE_IMPLICIT_ONLY);
callbackContext.success(); // Thread-safe.
}
});
return true;
}
if ("init".equals(action)) {
cordova.getThreadPool().execute(new Runnable() {
public void run() {
//calculate density-independent pixels (dp)
//http://developer.android.com/guide/practices/screens_support.html
DisplayMetrics dm = new DisplayMetrics();
cordova.getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
final float density = dm.density;
//http://stackoverflow.com/a/4737265/1091751 detect if keyboard is showing
final View rootView = cordova.getActivity().getWindow().getDecorView().findViewById(android.R.id.content).getRootView();
OnGlobalLayoutListener list = new OnGlobalLayoutListener() {
int previousHeightDiff = 0;
@Override
public void onGlobalLayout() {
Rect r = new Rect();
//r will be populated with the coordinates of your view that area still visible.
rootView.getWindowVisibleDisplayFrame(r);
PluginResult result;
// cache properties for later use
int rootViewHeight = rootView.getRootView().getHeight();
int resultBottom = r.bottom;
// calculate screen height differently for android versions >= 21: Lollipop 5.x, Marshmallow 6.x
//http://stackoverflow.com/a/29257533/3642890 beware of nexus 5
int screenHeight;
if (Build.VERSION.SDK_INT >= 21) {
Display display = cordova.getActivity().getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
screenHeight = size.y;
} else {
screenHeight = rootViewHeight;
}
int heightDiff = screenHeight - resultBottom;
int pixelHeightDiff = (int)(heightDiff / density);
if (pixelHeightDiff > 100 && pixelHeightDiff != previousHeightDiff) { // if more than 100 pixels, its probably a keyboard...
String msg = "S" + Integer.toString(pixelHeightDiff);
result = new PluginResult(PluginResult.Status.OK, msg);
result.setKeepCallback(true);
callbackContext.sendPluginResult(result);
}
else if ( pixelHeightDiff != previousHeightDiff && ( previousHeightDiff - pixelHeightDiff ) > 100 ){
String msg = "H";
result = new PluginResult(PluginResult.Status.OK, msg);
result.setKeepCallback(true);
callbackContext.sendPluginResult(result);
}
previousHeightDiff = pixelHeightDiff;
}
};
rootView.getViewTreeObserver().addOnGlobalLayoutListener(list);
PluginResult dataResult = new PluginResult(PluginResult.Status.OK);
dataResult.setKeepCallback(true);
callbackContext.sendPluginResult(dataResult);
}
});
return true;
}
return false; // Returning false results in a "MethodNotFound" error.
}
}

View file

@ -0,0 +1,174 @@
/*
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.device;
import java.util.TimeZone;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaInterface;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.provider.Settings;
public class Device extends CordovaPlugin {
public static final String TAG = "Device";
public static String platform; // Device OS
public static String uuid; // Device UUID
private static final String ANDROID_PLATFORM = "Android";
private static final String AMAZON_PLATFORM = "amazon-fireos";
private static final String AMAZON_DEVICE = "Amazon";
/**
* Constructor.
*/
public Device() {
}
/**
* Sets the context of the Command. This can then be used to do things like
* get file paths associated with the Activity.
*
* @param cordova The context of the main Activity.
* @param webView The CordovaWebView Cordova is running in.
*/
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
super.initialize(cordova, webView);
Device.uuid = getUuid();
}
/**
* Executes the request and returns PluginResult.
*
* @param action The action to execute.
* @param args JSONArry of arguments for the plugin.
* @param callbackContext The callback id used when calling back into JavaScript.
* @return True if the action was valid, false if not.
*/
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
if ("getDeviceInfo".equals(action)) {
JSONObject r = new JSONObject();
r.put("uuid", Device.uuid);
r.put("version", this.getOSVersion());
r.put("platform", this.getPlatform());
r.put("model", this.getModel());
r.put("manufacturer", this.getManufacturer());
r.put("isVirtual", this.isVirtual());
r.put("serial", this.getSerialNumber());
callbackContext.success(r);
}
else {
return false;
}
return true;
}
//--------------------------------------------------------------------------
// LOCAL METHODS
//--------------------------------------------------------------------------
/**
* Get the OS name.
*
* @return
*/
public String getPlatform() {
String platform;
if (isAmazonDevice()) {
platform = AMAZON_PLATFORM;
} else {
platform = ANDROID_PLATFORM;
}
return platform;
}
/**
* Get the device's Universally Unique Identifier (UUID).
*
* @return
*/
public String getUuid() {
String uuid = Settings.Secure.getString(this.cordova.getActivity().getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);
return uuid;
}
public String getModel() {
String model = android.os.Build.MODEL;
return model;
}
public String getProductName() {
String productname = android.os.Build.PRODUCT;
return productname;
}
public String getManufacturer() {
String manufacturer = android.os.Build.MANUFACTURER;
return manufacturer;
}
public String getSerialNumber() {
String serial = android.os.Build.SERIAL;
return serial;
}
/**
* Get the OS version.
*
* @return
*/
public String getOSVersion() {
String osversion = android.os.Build.VERSION.RELEASE;
return osversion;
}
public String getSDKVersion() {
@SuppressWarnings("deprecation")
String sdkversion = android.os.Build.VERSION.SDK;
return sdkversion;
}
public String getTimeZoneID() {
TimeZone tz = TimeZone.getDefault();
return (tz.getID());
}
/**
* Function to check if the device is manufactured by Amazon
*
* @return
*/
public boolean isAmazonDevice() {
if (android.os.Build.MANUFACTURER.equals(AMAZON_DEVICE)) {
return true;
}
return false;
}
public boolean isVirtual() {
return android.os.Build.FINGERPRINT.contains("generic") ||
android.os.Build.PRODUCT.contains("sdk");
}
}

View file

@ -0,0 +1,294 @@
/*
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.file;
import android.content.res.AssetManager;
import android.net.Uri;
import org.apache.cordova.CordovaResourceApi;
import org.apache.cordova.LOG;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.HashMap;
import java.util.Map;
public class AssetFilesystem extends Filesystem {
private final AssetManager assetManager;
// A custom gradle hook creates the cdvasset.manifest file, which speeds up asset listing a tonne.
// See: http://stackoverflow.com/questions/16911558/android-assetmanager-list-incredibly-slow
private static Object listCacheLock = new Object();
private static boolean listCacheFromFile;
private static Map<String, String[]> listCache;
private static Map<String, Long> lengthCache;
private static final String LOG_TAG = "AssetFilesystem";
private void lazyInitCaches() {
synchronized (listCacheLock) {
if (listCache == null) {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(assetManager.open("cdvasset.manifest"));
listCache = (Map<String, String[]>) ois.readObject();
lengthCache = (Map<String, Long>) ois.readObject();
listCacheFromFile = true;
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
// Asset manifest won't exist if the gradle hook isn't set up correctly.
} finally {
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
LOG.d(LOG_TAG, e.getLocalizedMessage());
}
}
}
if (listCache == null) {
LOG.w("AssetFilesystem", "Asset manifest not found. Recursive copies and directory listing will be slow.");
listCache = new HashMap<String, String[]>();
}
}
}
}
private String[] listAssets(String assetPath) throws IOException {
if (assetPath.startsWith("/")) {
assetPath = assetPath.substring(1);
}
if (assetPath.endsWith("/")) {
assetPath = assetPath.substring(0, assetPath.length() - 1);
}
lazyInitCaches();
String[] ret = listCache.get(assetPath);
if (ret == null) {
if (listCacheFromFile) {
ret = new String[0];
} else {
ret = assetManager.list(assetPath);
listCache.put(assetPath, ret);
}
}
return ret;
}
private long getAssetSize(String assetPath) throws FileNotFoundException {
if (assetPath.startsWith("/")) {
assetPath = assetPath.substring(1);
}
lazyInitCaches();
if (lengthCache != null) {
Long ret = lengthCache.get(assetPath);
if (ret == null) {
throw new FileNotFoundException("Asset not found: " + assetPath);
}
return ret;
}
CordovaResourceApi.OpenForReadResult offr = null;
try {
offr = resourceApi.openForRead(nativeUriForFullPath(assetPath));
long length = offr.length;
if (length < 0) {
// available() doesn't always yield the file size, but for assets it does.
length = offr.inputStream.available();
}
return length;
} catch (IOException e) {
FileNotFoundException fnfe = new FileNotFoundException("File not found: " + assetPath);
fnfe.initCause(e);
throw fnfe;
} finally {
if (offr != null) {
try {
offr.inputStream.close();
} catch (IOException e) {
LOG.d(LOG_TAG, e.getLocalizedMessage());
}
}
}
}
public AssetFilesystem(AssetManager assetManager, CordovaResourceApi resourceApi) {
super(Uri.parse("file:///android_asset/"), "assets", resourceApi);
this.assetManager = assetManager;
}
@Override
public Uri toNativeUri(LocalFilesystemURL inputURL) {
return nativeUriForFullPath(inputURL.path);
}
@Override
public LocalFilesystemURL toLocalUri(Uri inputURL) {
if (!"file".equals(inputURL.getScheme())) {
return null;
}
File f = new File(inputURL.getPath());
// Removes and duplicate /s (e.g. file:///a//b/c)
Uri resolvedUri = Uri.fromFile(f);
String rootUriNoTrailingSlash = rootUri.getEncodedPath();
rootUriNoTrailingSlash = rootUriNoTrailingSlash.substring(0, rootUriNoTrailingSlash.length() - 1);
if (!resolvedUri.getEncodedPath().startsWith(rootUriNoTrailingSlash)) {
return null;
}
String subPath = resolvedUri.getEncodedPath().substring(rootUriNoTrailingSlash.length());
// Strip leading slash
if (!subPath.isEmpty()) {
subPath = subPath.substring(1);
}
Uri.Builder b = new Uri.Builder()
.scheme(LocalFilesystemURL.FILESYSTEM_PROTOCOL)
.authority("localhost")
.path(name);
if (!subPath.isEmpty()) {
b.appendEncodedPath(subPath);
}
if (isDirectory(subPath) || inputURL.getPath().endsWith("/")) {
// Add trailing / for directories.
b.appendEncodedPath("");
}
return LocalFilesystemURL.parse(b.build());
}
private boolean isDirectory(String assetPath) {
try {
return listAssets(assetPath).length != 0;
} catch (IOException e) {
return false;
}
}
@Override
public LocalFilesystemURL[] listChildren(LocalFilesystemURL inputURL) throws FileNotFoundException {
String pathNoSlashes = inputURL.path.substring(1);
if (pathNoSlashes.endsWith("/")) {
pathNoSlashes = pathNoSlashes.substring(0, pathNoSlashes.length() - 1);
}
String[] files;
try {
files = listAssets(pathNoSlashes);
} catch (IOException e) {
FileNotFoundException fnfe = new FileNotFoundException();
fnfe.initCause(e);
throw fnfe;
}
LocalFilesystemURL[] entries = new LocalFilesystemURL[files.length];
for (int i = 0; i < files.length; ++i) {
entries[i] = localUrlforFullPath(new File(inputURL.path, files[i]).getPath());
}
return entries;
}
@Override
public JSONObject getFileForLocalURL(LocalFilesystemURL inputURL,
String path, JSONObject options, boolean directory)
throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
if (options != null && options.optBoolean("create")) {
throw new UnsupportedOperationException("Assets are read-only");
}
// Check whether the supplied path is absolute or relative
if (directory && !path.endsWith("/")) {
path += "/";
}
LocalFilesystemURL requestedURL;
if (path.startsWith("/")) {
requestedURL = localUrlforFullPath(normalizePath(path));
} else {
requestedURL = localUrlforFullPath(normalizePath(inputURL.path + "/" + path));
}
// Throws a FileNotFoundException if it doesn't exist.
getFileMetadataForLocalURL(requestedURL);
boolean isDir = isDirectory(requestedURL.path);
if (directory && !isDir) {
throw new TypeMismatchException("path doesn't exist or is file");
} else if (!directory && isDir) {
throw new TypeMismatchException("path doesn't exist or is directory");
}
// Return the directory
return makeEntryForURL(requestedURL);
}
@Override
public JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException {
JSONObject metadata = new JSONObject();
long size = inputURL.isDirectory ? 0 : getAssetSize(inputURL.path);
try {
metadata.put("size", size);
metadata.put("type", inputURL.isDirectory ? "text/directory" : resourceApi.getMimeType(toNativeUri(inputURL)));
metadata.put("name", new File(inputURL.path).getName());
metadata.put("fullPath", inputURL.path);
metadata.put("lastModifiedDate", 0);
} catch (JSONException e) {
return null;
}
return metadata;
}
@Override
public boolean canRemoveFileAtLocalURL(LocalFilesystemURL inputURL) {
return false;
}
@Override
long writeToFileAtURL(LocalFilesystemURL inputURL, String data, int offset, boolean isBinary) throws NoModificationAllowedException, IOException {
throw new NoModificationAllowedException("Assets are read-only");
}
@Override
long truncateFileAtURL(LocalFilesystemURL inputURL, long size) throws IOException, NoModificationAllowedException {
throw new NoModificationAllowedException("Assets are read-only");
}
@Override
String filesystemPathForURL(LocalFilesystemURL url) {
return new File(rootUri.getPath(), url.path).toString();
}
@Override
LocalFilesystemURL URLforFilesystemPath(String path) {
return null;
}
@Override
boolean removeFileAtLocalURL(LocalFilesystemURL inputURL) throws InvalidModificationException, NoModificationAllowedException {
throw new NoModificationAllowedException("Assets are read-only");
}
@Override
boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL) throws NoModificationAllowedException {
throw new NoModificationAllowedException("Assets are read-only");
}
}

View file

@ -0,0 +1,223 @@
/*
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.file;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import org.apache.cordova.CordovaResourceApi;
import org.json.JSONException;
import org.json.JSONObject;
public class ContentFilesystem extends Filesystem {
private final Context context;
public ContentFilesystem(Context context, CordovaResourceApi resourceApi) {
super(Uri.parse("content://"), "content", resourceApi);
this.context = context;
}
@Override
public Uri toNativeUri(LocalFilesystemURL inputURL) {
String authorityAndPath = inputURL.uri.getEncodedPath().substring(this.name.length() + 2);
if (authorityAndPath.length() < 2) {
return null;
}
String ret = "content://" + authorityAndPath;
String query = inputURL.uri.getEncodedQuery();
if (query != null) {
ret += '?' + query;
}
String frag = inputURL.uri.getEncodedFragment();
if (frag != null) {
ret += '#' + frag;
}
return Uri.parse(ret);
}
@Override
public LocalFilesystemURL toLocalUri(Uri inputURL) {
if (!"content".equals(inputURL.getScheme())) {
return null;
}
String subPath = inputURL.getEncodedPath();
if (subPath.length() > 0) {
subPath = subPath.substring(1);
}
Uri.Builder b = new Uri.Builder()
.scheme(LocalFilesystemURL.FILESYSTEM_PROTOCOL)
.authority("localhost")
.path(name)
.appendPath(inputURL.getAuthority());
if (subPath.length() > 0) {
b.appendEncodedPath(subPath);
}
Uri localUri = b.encodedQuery(inputURL.getEncodedQuery())
.encodedFragment(inputURL.getEncodedFragment())
.build();
return LocalFilesystemURL.parse(localUri);
}
@Override
public JSONObject getFileForLocalURL(LocalFilesystemURL inputURL,
String fileName, JSONObject options, boolean directory) throws IOException, TypeMismatchException, JSONException {
throw new UnsupportedOperationException("getFile() not supported for content:. Use resolveLocalFileSystemURL instead.");
}
@Override
public boolean removeFileAtLocalURL(LocalFilesystemURL inputURL)
throws NoModificationAllowedException {
Uri contentUri = toNativeUri(inputURL);
try {
context.getContentResolver().delete(contentUri, null, null);
} catch (UnsupportedOperationException t) {
// Was seeing this on the File mobile-spec tests on 4.0.3 x86 emulator.
// The ContentResolver applies only when the file was registered in the
// first case, which is generally only the case with images.
NoModificationAllowedException nmae = new NoModificationAllowedException("Deleting not supported for content uri: " + contentUri);
nmae.initCause(t);
throw nmae;
}
return true;
}
@Override
public boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL)
throws NoModificationAllowedException {
throw new NoModificationAllowedException("Cannot remove content url");
}
@Override
public LocalFilesystemURL[] listChildren(LocalFilesystemURL inputURL) throws FileNotFoundException {
throw new UnsupportedOperationException("readEntriesAtLocalURL() not supported for content:. Use resolveLocalFileSystemURL instead.");
}
@Override
public JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException {
long size = -1;
long lastModified = 0;
Uri nativeUri = toNativeUri(inputURL);
String mimeType = resourceApi.getMimeType(nativeUri);
Cursor cursor = openCursorForURL(nativeUri);
try {
if (cursor != null && cursor.moveToFirst()) {
Long sizeForCursor = resourceSizeForCursor(cursor);
if (sizeForCursor != null) {
size = sizeForCursor.longValue();
}
Long modified = lastModifiedDateForCursor(cursor);
if (modified != null)
lastModified = modified.longValue();
} else {
// Some content providers don't support cursors at all!
CordovaResourceApi.OpenForReadResult offr = resourceApi.openForRead(nativeUri);
size = offr.length;
}
} catch (IOException e) {
FileNotFoundException fnfe = new FileNotFoundException();
fnfe.initCause(e);
throw fnfe;
} finally {
if (cursor != null)
cursor.close();
}
JSONObject metadata = new JSONObject();
try {
metadata.put("size", size);
metadata.put("type", mimeType);
metadata.put("name", name);
metadata.put("fullPath", inputURL.path);
metadata.put("lastModifiedDate", lastModified);
} catch (JSONException e) {
return null;
}
return metadata;
}
@Override
public long writeToFileAtURL(LocalFilesystemURL inputURL, String data,
int offset, boolean isBinary) throws NoModificationAllowedException {
throw new NoModificationAllowedException("Couldn't write to file given its content URI");
}
@Override
public long truncateFileAtURL(LocalFilesystemURL inputURL, long size)
throws NoModificationAllowedException {
throw new NoModificationAllowedException("Couldn't truncate file given its content URI");
}
protected Cursor openCursorForURL(Uri nativeUri) {
ContentResolver contentResolver = context.getContentResolver();
try {
return contentResolver.query(nativeUri, null, null, null, null);
} catch (UnsupportedOperationException e) {
return null;
}
}
private Long resourceSizeForCursor(Cursor cursor) {
int columnIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
if (columnIndex != -1) {
String sizeStr = cursor.getString(columnIndex);
if (sizeStr != null) {
return Long.parseLong(sizeStr);
}
}
return null;
}
protected Long lastModifiedDateForCursor(Cursor cursor) {
int columnIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED);
if (columnIndex == -1) {
columnIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED);
}
if (columnIndex != -1) {
String dateStr = cursor.getString(columnIndex);
if (dateStr != null) {
return Long.parseLong(dateStr);
}
}
return null;
}
@Override
public String filesystemPathForURL(LocalFilesystemURL url) {
File f = resourceApi.mapUriToFile(toNativeUri(url));
return f == null ? null : f.getAbsolutePath();
}
@Override
public LocalFilesystemURL URLforFilesystemPath(String path) {
// Returns null as we don't support reverse mapping back to content:// URLs
return null;
}
@Override
public boolean canRemoveFileAtLocalURL(LocalFilesystemURL inputURL) {
return true;
}
}

View file

@ -0,0 +1,134 @@
/*
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.file;
import android.os.Environment;
import android.os.StatFs;
import java.io.File;
/**
* This class provides file directory utilities.
* All file operations are performed on the SD card.
*
* It is used by the FileUtils class.
*/
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 space in external storage
*
* @return Size in KB or -1 if not available
*/
public static long getFreeExternalStorageSpace() {
String status = Environment.getExternalStorageState();
long freeSpaceInBytes = 0;
// Check if external storage exists
if (status.equals(Environment.MEDIA_MOUNTED)) {
freeSpaceInBytes = getFreeSpaceInBytes(Environment.getExternalStorageDirectory().getPath());
} else {
// If no external storage then return -1
return -1;
}
return freeSpaceInBytes / 1024;
}
/**
* Given a path return the number of free bytes in the filesystem containing the path.
*
* @param path to the file system
* @return free space in bytes
*/
public static long getFreeSpaceInBytes(String path) {
try {
StatFs stat = new StatFs(path);
long blockSize = stat.getBlockSize();
long availableBlocks = stat.getAvailableBlocks();
return availableBlocks * blockSize;
} catch (IllegalArgumentException e) {
// The path was invalid. Just return 0 free bytes.
return 0;
}
}
/**
* 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;
}
}

View file

@ -0,0 +1,29 @@
/*
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.file;
@SuppressWarnings("serial")
public class EncodingException extends Exception {
public EncodingException(String message) {
super(message);
}
}

View file

@ -0,0 +1,29 @@
/*
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.file;
@SuppressWarnings("serial")
public class FileExistsException extends Exception {
public FileExistsException(String msg) {
super(msg);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,331 @@
/*
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.file;
import android.net.Uri;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import org.apache.cordova.CordovaResourceApi;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public abstract class Filesystem {
protected final Uri rootUri;
protected final CordovaResourceApi resourceApi;
public final String name;
private JSONObject rootEntry;
public Filesystem(Uri rootUri, String name, CordovaResourceApi resourceApi) {
this.rootUri = rootUri;
this.name = name;
this.resourceApi = resourceApi;
}
public interface ReadFileCallback {
public void handleData(InputStream inputStream, String contentType) throws IOException;
}
public static JSONObject makeEntryForURL(LocalFilesystemURL inputURL, Uri nativeURL) {
try {
String path = inputURL.path;
int end = path.endsWith("/") ? 1 : 0;
String[] parts = path.substring(0, path.length() - end).split("/+");
String fileName = parts[parts.length - 1];
JSONObject entry = new JSONObject();
entry.put("isFile", !inputURL.isDirectory);
entry.put("isDirectory", inputURL.isDirectory);
entry.put("name", fileName);
entry.put("fullPath", path);
// The file system can't be specified, as it would lead to an infinite loop,
// but the filesystem name can be.
entry.put("filesystemName", inputURL.fsName);
// Backwards compatibility
entry.put("filesystem", "temporary".equals(inputURL.fsName) ? 0 : 1);
String nativeUrlStr = nativeURL.toString();
if (inputURL.isDirectory && !nativeUrlStr.endsWith("/")) {
nativeUrlStr += "/";
}
entry.put("nativeURL", nativeUrlStr);
return entry;
} catch (JSONException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public JSONObject makeEntryForURL(LocalFilesystemURL inputURL) {
Uri nativeUri = toNativeUri(inputURL);
return nativeUri == null ? null : makeEntryForURL(inputURL, nativeUri);
}
public JSONObject makeEntryForNativeUri(Uri nativeUri) {
LocalFilesystemURL inputUrl = toLocalUri(nativeUri);
return inputUrl == null ? null : makeEntryForURL(inputUrl, nativeUri);
}
public JSONObject getEntryForLocalURL(LocalFilesystemURL inputURL) throws IOException {
return makeEntryForURL(inputURL);
}
public JSONObject makeEntryForFile(File file) {
return makeEntryForNativeUri(Uri.fromFile(file));
}
abstract JSONObject getFileForLocalURL(LocalFilesystemURL inputURL, String path,
JSONObject options, boolean directory) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException;
abstract boolean removeFileAtLocalURL(LocalFilesystemURL inputURL) throws InvalidModificationException, NoModificationAllowedException;
abstract boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL) throws FileExistsException, NoModificationAllowedException;
abstract LocalFilesystemURL[] listChildren(LocalFilesystemURL inputURL) throws FileNotFoundException;
public final JSONArray readEntriesAtLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException {
LocalFilesystemURL[] children = listChildren(inputURL);
JSONArray entries = new JSONArray();
if (children != null) {
for (LocalFilesystemURL url : children) {
entries.put(makeEntryForURL(url));
}
}
return entries;
}
abstract JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException;
public Uri getRootUri() {
return rootUri;
}
public boolean exists(LocalFilesystemURL inputURL) {
try {
getFileMetadataForLocalURL(inputURL);
} catch (FileNotFoundException e) {
return false;
}
return true;
}
public Uri nativeUriForFullPath(String fullPath) {
Uri ret = null;
if (fullPath != null) {
String encodedPath = Uri.fromFile(new File(fullPath)).getEncodedPath();
if (encodedPath.startsWith("/")) {
encodedPath = encodedPath.substring(1);
}
ret = rootUri.buildUpon().appendEncodedPath(encodedPath).build();
}
return ret;
}
public LocalFilesystemURL localUrlforFullPath(String fullPath) {
Uri nativeUri = nativeUriForFullPath(fullPath);
if (nativeUri != null) {
return toLocalUri(nativeUri);
}
return null;
}
/**
* Removes multiple repeated //s, and collapses processes ../s.
*/
protected static String normalizePath(String rawPath) {
// If this is an absolute path, trim the leading "/" and replace it later
boolean isAbsolutePath = rawPath.startsWith("/");
if (isAbsolutePath) {
rawPath = rawPath.replaceFirst("/+", "");
}
ArrayList<String> components = new ArrayList<String>(Arrays.asList(rawPath.split("/+")));
for (int index = 0; index < components.size(); ++index) {
if (components.get(index).equals("..")) {
components.remove(index);
if (index > 0) {
components.remove(index-1);
--index;
}
}
}
StringBuilder normalizedPath = new StringBuilder();
for(String component: components) {
normalizedPath.append("/");
normalizedPath.append(component);
}
if (isAbsolutePath) {
return normalizedPath.toString();
} else {
return normalizedPath.toString().substring(1);
}
}
/**
* Gets the free space in bytes available on this filesystem.
* Subclasses may override this method to return nonzero free space.
*/
public long getFreeSpaceInBytes() {
return 0;
}
public abstract Uri toNativeUri(LocalFilesystemURL inputURL);
public abstract LocalFilesystemURL toLocalUri(Uri inputURL);
public JSONObject getRootEntry() {
if (rootEntry == null) {
rootEntry = makeEntryForNativeUri(rootUri);
}
return rootEntry;
}
public JSONObject getParentForLocalURL(LocalFilesystemURL inputURL) throws IOException {
Uri parentUri = inputURL.uri;
String parentPath = new File(inputURL.uri.getPath()).getParent();
if (!"/".equals(parentPath)) {
parentUri = inputURL.uri.buildUpon().path(parentPath + '/').build();
}
return getEntryForLocalURL(LocalFilesystemURL.parse(parentUri));
}
protected LocalFilesystemURL makeDestinationURL(String newName, LocalFilesystemURL srcURL, LocalFilesystemURL destURL, boolean isDirectory) {
// I know this looks weird but it is to work around a JSON bug.
if ("null".equals(newName) || "".equals(newName)) {
newName = srcURL.uri.getLastPathSegment();;
}
String newDest = destURL.uri.toString();
if (newDest.endsWith("/")) {
newDest = newDest + newName;
} else {
newDest = newDest + "/" + newName;
}
if (isDirectory) {
newDest += '/';
}
return LocalFilesystemURL.parse(newDest);
}
/* Read a source URL (possibly from a different filesystem, srcFs,) and copy it to
* the destination URL on this filesystem, optionally with a new filename.
* If move is true, then this method should either perform an atomic move operation
* or remove the source file when finished.
*/
public JSONObject copyFileToURL(LocalFilesystemURL destURL, String newName,
Filesystem srcFs, LocalFilesystemURL srcURL, boolean move) throws IOException, InvalidModificationException, JSONException, NoModificationAllowedException, FileExistsException {
// First, check to see that we can do it
if (move && !srcFs.canRemoveFileAtLocalURL(srcURL)) {
throw new NoModificationAllowedException("Cannot move file at source URL");
}
final LocalFilesystemURL destination = makeDestinationURL(newName, srcURL, destURL, srcURL.isDirectory);
Uri srcNativeUri = srcFs.toNativeUri(srcURL);
CordovaResourceApi.OpenForReadResult ofrr = resourceApi.openForRead(srcNativeUri);
OutputStream os = null;
try {
os = getOutputStreamForURL(destination);
} catch (IOException e) {
ofrr.inputStream.close();
throw e;
}
// Closes streams.
resourceApi.copyResource(ofrr, os);
if (move) {
srcFs.removeFileAtLocalURL(srcURL);
}
return getEntryForLocalURL(destination);
}
public OutputStream getOutputStreamForURL(LocalFilesystemURL inputURL) throws IOException {
return resourceApi.openOutputStream(toNativeUri(inputURL));
}
public void readFileAtURL(LocalFilesystemURL inputURL, long start, long end,
ReadFileCallback readFileCallback) throws IOException {
CordovaResourceApi.OpenForReadResult ofrr = resourceApi.openForRead(toNativeUri(inputURL));
if (end < 0) {
end = ofrr.length;
}
long numBytesToRead = end - start;
try {
if (start > 0) {
ofrr.inputStream.skip(start);
}
InputStream inputStream = ofrr.inputStream;
if (end < ofrr.length) {
inputStream = new LimitedInputStream(inputStream, numBytesToRead);
}
readFileCallback.handleData(inputStream, ofrr.mimeType);
} finally {
ofrr.inputStream.close();
}
}
abstract long writeToFileAtURL(LocalFilesystemURL inputURL, String data, int offset,
boolean isBinary) throws NoModificationAllowedException, IOException;
abstract long truncateFileAtURL(LocalFilesystemURL inputURL, long size)
throws IOException, NoModificationAllowedException;
// This method should return null if filesystem urls cannot be mapped to paths
abstract String filesystemPathForURL(LocalFilesystemURL url);
abstract LocalFilesystemURL URLforFilesystemPath(String path);
abstract boolean canRemoveFileAtLocalURL(LocalFilesystemURL inputURL);
protected class LimitedInputStream extends FilterInputStream {
long numBytesToRead;
public LimitedInputStream(InputStream in, long numBytesToRead) {
super(in);
this.numBytesToRead = numBytesToRead;
}
@Override
public int read() throws IOException {
if (numBytesToRead <= 0) {
return -1;
}
numBytesToRead--;
return in.read();
}
@Override
public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
if (numBytesToRead <= 0) {
return -1;
}
int bytesToRead = byteCount;
if (byteCount > numBytesToRead) {
bytesToRead = (int)numBytesToRead; // Cast okay; long is less than int here.
}
int numBytesRead = in.read(buffer, byteOffset, bytesToRead);
numBytesToRead -= numBytesRead;
return numBytesRead;
}
}
}

View file

@ -0,0 +1,30 @@
/*
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.file;
@SuppressWarnings("serial")
public class InvalidModificationException extends Exception {
public InvalidModificationException(String message) {
super(message);
}
}

View file

@ -0,0 +1,513 @@
/*
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.file;
import java.io.ByteArrayInputStream;
import java.io.File;
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.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import org.apache.cordova.CordovaResourceApi;
import org.json.JSONException;
import org.json.JSONObject;
import android.os.Build;
import android.os.Environment;
import android.util.Base64;
import android.net.Uri;
import android.content.Context;
import android.content.Intent;
import java.nio.charset.Charset;
public class LocalFilesystem extends Filesystem {
private final Context context;
public LocalFilesystem(String name, Context context, CordovaResourceApi resourceApi, File fsRoot) {
super(Uri.fromFile(fsRoot).buildUpon().appendEncodedPath("").build(), name, resourceApi);
this.context = context;
}
public String filesystemPathForFullPath(String fullPath) {
return new File(rootUri.getPath(), fullPath).toString();
}
@Override
public String filesystemPathForURL(LocalFilesystemURL url) {
return filesystemPathForFullPath(url.path);
}
private String fullPathForFilesystemPath(String absolutePath) {
if (absolutePath != null && absolutePath.startsWith(rootUri.getPath())) {
return absolutePath.substring(rootUri.getPath().length() - 1);
}
return null;
}
@Override
public Uri toNativeUri(LocalFilesystemURL inputURL) {
return nativeUriForFullPath(inputURL.path);
}
@Override
public LocalFilesystemURL toLocalUri(Uri inputURL) {
if (!"file".equals(inputURL.getScheme())) {
return null;
}
File f = new File(inputURL.getPath());
// Removes and duplicate /s (e.g. file:///a//b/c)
Uri resolvedUri = Uri.fromFile(f);
String rootUriNoTrailingSlash = rootUri.getEncodedPath();
rootUriNoTrailingSlash = rootUriNoTrailingSlash.substring(0, rootUriNoTrailingSlash.length() - 1);
if (!resolvedUri.getEncodedPath().startsWith(rootUriNoTrailingSlash)) {
return null;
}
String subPath = resolvedUri.getEncodedPath().substring(rootUriNoTrailingSlash.length());
// Strip leading slash
if (!subPath.isEmpty()) {
subPath = subPath.substring(1);
}
Uri.Builder b = new Uri.Builder()
.scheme(LocalFilesystemURL.FILESYSTEM_PROTOCOL)
.authority("localhost")
.path(name);
if (!subPath.isEmpty()) {
b.appendEncodedPath(subPath);
}
if (f.isDirectory()) {
// Add trailing / for directories.
b.appendEncodedPath("");
}
return LocalFilesystemURL.parse(b.build());
}
@Override
public LocalFilesystemURL URLforFilesystemPath(String path) {
return localUrlforFullPath(fullPathForFilesystemPath(path));
}
@Override
public JSONObject getFileForLocalURL(LocalFilesystemURL inputURL,
String path, JSONObject options, boolean directory) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
boolean create = false;
boolean exclusive = false;
if (options != null) {
create = options.optBoolean("create");
if (create) {
exclusive = options.optBoolean("exclusive");
}
}
// Check for a ":" character in the file to line up with BB and iOS
if (path.contains(":")) {
throw new EncodingException("This path has an invalid \":\" in it.");
}
LocalFilesystemURL requestedURL;
// Check whether the supplied path is absolute or relative
if (directory && !path.endsWith("/")) {
path += "/";
}
if (path.startsWith("/")) {
requestedURL = localUrlforFullPath(normalizePath(path));
} else {
requestedURL = localUrlforFullPath(normalizePath(inputURL.path + "/" + path));
}
File fp = new File(this.filesystemPathForURL(requestedURL));
if (create) {
if (exclusive && fp.exists()) {
throw new FileExistsException("create/exclusive fails");
}
if (directory) {
fp.mkdir();
} else {
fp.createNewFile();
}
if (!fp.exists()) {
throw new FileExistsException("create fails");
}
}
else {
if (!fp.exists()) {
throw new FileNotFoundException("path does not exist");
}
if (directory) {
if (fp.isFile()) {
throw new TypeMismatchException("path doesn't exist or is file");
}
} else {
if (fp.isDirectory()) {
throw new TypeMismatchException("path doesn't exist or is directory");
}
}
}
// Return the directory
return makeEntryForURL(requestedURL);
}
@Override
public boolean removeFileAtLocalURL(LocalFilesystemURL inputURL) throws InvalidModificationException {
File fp = new File(filesystemPathForURL(inputURL));
// You can't delete a directory that is not empty
if (fp.isDirectory() && fp.list().length > 0) {
throw new InvalidModificationException("You can't delete a directory that is not empty.");
}
return fp.delete();
}
@Override
public boolean exists(LocalFilesystemURL inputURL) {
File fp = new File(filesystemPathForURL(inputURL));
return fp.exists();
}
@Override
public long getFreeSpaceInBytes() {
return DirectoryManager.getFreeSpaceInBytes(rootUri.getPath());
}
@Override
public boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL) throws FileExistsException {
File directory = new File(filesystemPathForURL(inputURL));
return removeDirRecursively(directory);
}
protected boolean removeDirRecursively(File directory) throws FileExistsException {
if (directory.isDirectory()) {
for (File file : directory.listFiles()) {
removeDirRecursively(file);
}
}
if (!directory.delete()) {
throw new FileExistsException("could not delete: " + directory.getName());
} else {
return true;
}
}
@Override
public LocalFilesystemURL[] listChildren(LocalFilesystemURL inputURL) throws FileNotFoundException {
File fp = new File(filesystemPathForURL(inputURL));
if (!fp.exists()) {
// The directory we are listing doesn't exist so we should fail.
throw new FileNotFoundException();
}
File[] files = fp.listFiles();
if (files == null) {
// inputURL is a directory
return null;
}
LocalFilesystemURL[] entries = new LocalFilesystemURL[files.length];
for (int i = 0; i < files.length; i++) {
entries[i] = URLforFilesystemPath(files[i].getPath());
}
return entries;
}
@Override
public JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException {
File file = new File(filesystemPathForURL(inputURL));
if (!file.exists()) {
throw new FileNotFoundException("File at " + inputURL.uri + " does not exist.");
}
JSONObject metadata = new JSONObject();
try {
// Ensure that directories report a size of 0
metadata.put("size", file.isDirectory() ? 0 : file.length());
metadata.put("type", resourceApi.getMimeType(Uri.fromFile(file)));
metadata.put("name", file.getName());
metadata.put("fullPath", inputURL.path);
metadata.put("lastModifiedDate", file.lastModified());
} catch (JSONException e) {
return null;
}
return metadata;
}
private void copyFile(Filesystem srcFs, LocalFilesystemURL srcURL, File destFile, boolean move) throws IOException, InvalidModificationException, NoModificationAllowedException {
if (move) {
String realSrcPath = srcFs.filesystemPathForURL(srcURL);
if (realSrcPath != null) {
File srcFile = new File(realSrcPath);
if (srcFile.renameTo(destFile)) {
return;
}
// Trying to rename the file failed. Possibly because we moved across file system on the device.
}
}
CordovaResourceApi.OpenForReadResult offr = resourceApi.openForRead(srcFs.toNativeUri(srcURL));
copyResource(offr, new FileOutputStream(destFile));
if (move) {
srcFs.removeFileAtLocalURL(srcURL);
}
}
private void copyDirectory(Filesystem srcFs, LocalFilesystemURL srcURL, File dstDir, boolean move) throws IOException, NoModificationAllowedException, InvalidModificationException, FileExistsException {
if (move) {
String realSrcPath = srcFs.filesystemPathForURL(srcURL);
if (realSrcPath != null) {
File srcDir = new File(realSrcPath);
// If the destination directory already exists and is empty then delete it. This is according to spec.
if (dstDir.exists()) {
if (dstDir.list().length > 0) {
throw new InvalidModificationException("directory is not empty");
}
dstDir.delete();
}
// Try to rename the directory
if (srcDir.renameTo(dstDir)) {
return;
}
// Trying to rename the file failed. Possibly because we moved across file system on the device.
}
}
if (dstDir.exists()) {
if (dstDir.list().length > 0) {
throw new InvalidModificationException("directory is not empty");
}
} else {
if (!dstDir.mkdir()) {
// If we can't create the directory then fail
throw new NoModificationAllowedException("Couldn't create the destination directory");
}
}
LocalFilesystemURL[] children = srcFs.listChildren(srcURL);
for (LocalFilesystemURL childLocalUrl : children) {
File target = new File(dstDir, new File(childLocalUrl.path).getName());
if (childLocalUrl.isDirectory) {
copyDirectory(srcFs, childLocalUrl, target, false);
} else {
copyFile(srcFs, childLocalUrl, target, false);
}
}
if (move) {
srcFs.recursiveRemoveFileAtLocalURL(srcURL);
}
}
@Override
public JSONObject copyFileToURL(LocalFilesystemURL destURL, String newName,
Filesystem srcFs, LocalFilesystemURL srcURL, boolean move) throws IOException, InvalidModificationException, JSONException, NoModificationAllowedException, FileExistsException {
// Check to see if the destination directory exists
String newParent = this.filesystemPathForURL(destURL);
File destinationDir = new File(newParent);
if (!destinationDir.exists()) {
// The destination does not exist so we should fail.
throw new FileNotFoundException("The source does not exist");
}
// Figure out where we should be copying to
final LocalFilesystemURL destinationURL = makeDestinationURL(newName, srcURL, destURL, srcURL.isDirectory);
Uri dstNativeUri = toNativeUri(destinationURL);
Uri srcNativeUri = srcFs.toNativeUri(srcURL);
// Check to see if source and destination are the same file
if (dstNativeUri.equals(srcNativeUri)) {
throw new InvalidModificationException("Can't copy onto itself");
}
if (move && !srcFs.canRemoveFileAtLocalURL(srcURL)) {
throw new InvalidModificationException("Source URL is read-only (cannot move)");
}
File destFile = new File(dstNativeUri.getPath());
if (destFile.exists()) {
if (!srcURL.isDirectory && destFile.isDirectory()) {
throw new InvalidModificationException("Can't copy/move a file to an existing directory");
} else if (srcURL.isDirectory && destFile.isFile()) {
throw new InvalidModificationException("Can't copy/move a directory to an existing file");
}
}
if (srcURL.isDirectory) {
// E.g. Copy /sdcard/myDir to /sdcard/myDir/backup
if (dstNativeUri.toString().startsWith(srcNativeUri.toString() + '/')) {
throw new InvalidModificationException("Can't copy directory into itself");
}
copyDirectory(srcFs, srcURL, destFile, move);
} else {
copyFile(srcFs, srcURL, destFile, move);
}
return makeEntryForURL(destinationURL);
}
@Override
public long writeToFileAtURL(LocalFilesystemURL inputURL, String data,
int offset, boolean isBinary) throws IOException, NoModificationAllowedException {
boolean append = false;
if (offset > 0) {
this.truncateFileAtURL(inputURL, offset);
append = true;
}
byte[] rawData;
if (isBinary) {
rawData = Base64.decode(data, Base64.DEFAULT);
} else {
rawData = data.getBytes(Charset.defaultCharset());
}
ByteArrayInputStream in = new ByteArrayInputStream(rawData);
try
{
byte buff[] = new byte[rawData.length];
String absolutePath = filesystemPathForURL(inputURL);
FileOutputStream out = new FileOutputStream(absolutePath, append);
try {
in.read(buff, 0, buff.length);
out.write(buff, 0, rawData.length);
out.flush();
} finally {
// Always close the output
out.close();
}
if (isPublicDirectory(absolutePath)) {
broadcastNewFile(Uri.fromFile(new File(absolutePath)));
}
}
catch (NullPointerException e)
{
// This is a bug in the Android implementation of the Java Stack
NoModificationAllowedException realException = new NoModificationAllowedException(inputURL.toString());
realException.initCause(e);
throw realException;
}
return rawData.length;
}
private boolean isPublicDirectory(String absolutePath) {
// TODO: should expose a way to scan app's private files (maybe via a flag).
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Lollipop has a bug where SD cards are null.
for (File f : context.getExternalMediaDirs()) {
if(f != null && absolutePath.startsWith(f.getAbsolutePath())) {
return true;
}
}
}
String extPath = Environment.getExternalStorageDirectory().getAbsolutePath();
return absolutePath.startsWith(extPath);
}
/**
* Send broadcast of new file so files appear over MTP
*/
private void broadcastNewFile(Uri nativeUri) {
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, nativeUri);
context.sendBroadcast(intent);
}
@Override
public long truncateFileAtURL(LocalFilesystemURL inputURL, long size) throws IOException {
File file = new File(filesystemPathForURL(inputURL));
if (!file.exists()) {
throw new FileNotFoundException("File at " + inputURL.uri + " does not exist.");
}
RandomAccessFile raf = new RandomAccessFile(filesystemPathForURL(inputURL), "rw");
try {
if (raf.length() >= size) {
FileChannel channel = raf.getChannel();
channel.truncate(size);
return size;
}
return raf.length();
} finally {
raf.close();
}
}
@Override
public boolean canRemoveFileAtLocalURL(LocalFilesystemURL inputURL) {
String path = filesystemPathForURL(inputURL);
File file = new File(path);
return file.exists();
}
// This is a copy & paste from CordovaResource API that is required since CordovaResourceApi
// has a bug pre-4.0.0.
// TODO: Once cordova-android@4.0.0 is released, delete this copy and make the plugin depend on
// 4.0.0 with an engine tag.
private static void copyResource(CordovaResourceApi.OpenForReadResult input, OutputStream outputStream) throws IOException {
try {
InputStream inputStream = input.inputStream;
if (inputStream instanceof FileInputStream && outputStream instanceof FileOutputStream) {
FileChannel inChannel = ((FileInputStream)input.inputStream).getChannel();
FileChannel outChannel = ((FileOutputStream)outputStream).getChannel();
long offset = 0;
long length = input.length;
if (input.assetFd != null) {
offset = input.assetFd.getStartOffset();
}
// transferFrom()'s 2nd arg is a relative position. Need to set the absolute
// position first.
inChannel.position(offset);
outChannel.transferFrom(inChannel, 0, length);
} else {
final int BUFFER_SIZE = 8192;
byte[] buffer = new byte[BUFFER_SIZE];
for (;;) {
int bytesRead = inputStream.read(buffer, 0, BUFFER_SIZE);
if (bytesRead <= 0) {
break;
}
outputStream.write(buffer, 0, bytesRead);
}
}
} finally {
input.inputStream.close();
if (outputStream != null) {
outputStream.close();
}
}
}
}

View file

@ -0,0 +1,64 @@
/*
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.file;
import android.net.Uri;
public class LocalFilesystemURL {
public static final String FILESYSTEM_PROTOCOL = "cdvfile";
public final Uri uri;
public final String fsName;
public final String path;
public final boolean isDirectory;
private LocalFilesystemURL(Uri uri, String fsName, String fsPath, boolean isDirectory) {
this.uri = uri;
this.fsName = fsName;
this.path = fsPath;
this.isDirectory = isDirectory;
}
public static LocalFilesystemURL parse(Uri uri) {
if (!FILESYSTEM_PROTOCOL.equals(uri.getScheme())) {
return null;
}
String path = uri.getPath();
if (path.length() < 1) {
return null;
}
int firstSlashIdx = path.indexOf('/', 1);
if (firstSlashIdx < 0) {
return null;
}
String fsName = path.substring(1, firstSlashIdx);
path = path.substring(firstSlashIdx);
boolean isDirectory = path.charAt(path.length() - 1) == '/';
return new LocalFilesystemURL(uri, fsName, path, isDirectory);
}
public static LocalFilesystemURL parse(String uri) {
return parse(Uri.parse(uri));
}
public String toString() {
return uri.toString();
}
}

View file

@ -0,0 +1,29 @@
/*
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.file;
@SuppressWarnings("serial")
public class NoModificationAllowedException extends Exception {
public NoModificationAllowedException(String message) {
super(message);
}
}

View file

@ -0,0 +1,94 @@
/*
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.file;
import android.util.SparseArray;
import org.apache.cordova.CallbackContext;
/**
* Holds pending runtime permission requests
*/
class PendingRequests {
private int currentReqId = 0;
private SparseArray<Request> requests = new SparseArray<Request>();
/**
* Creates a request and adds it to the array of pending requests. Each created request gets a
* unique result code for use with requestPermission()
* @param rawArgs The raw arguments passed to the plugin
* @param action The action this request corresponds to (get file, etc.)
* @param callbackContext The CallbackContext for this plugin call
* @return The request code that can be used to retrieve the Request object
*/
public synchronized int createRequest(String rawArgs, int action, CallbackContext callbackContext) {
Request req = new Request(rawArgs, action, callbackContext);
requests.put(req.requestCode, req);
return req.requestCode;
}
/**
* Gets the request corresponding to this request code and removes it from the pending requests
* @param requestCode The request code for the desired request
* @return The request corresponding to the given request code or null if such a
* request is not found
*/
public synchronized Request getAndRemove(int requestCode) {
Request result = requests.get(requestCode);
requests.remove(requestCode);
return result;
}
/**
* Holds the options and CallbackContext for a call made to the plugin.
*/
public class Request {
// Unique int used to identify this request in any Android permission callback
private int requestCode;
// Action to be performed after permission request result
private int action;
// Raw arguments passed to plugin
private String rawArgs;
// The callback context for this plugin request
private CallbackContext callbackContext;
private Request(String rawArgs, int action, CallbackContext callbackContext) {
this.rawArgs = rawArgs;
this.action = action;
this.callbackContext = callbackContext;
this.requestCode = currentReqId ++;
}
public int getAction() {
return this.action;
}
public String getRawArgs() {
return rawArgs;
}
public CallbackContext getCallbackContext() {
return callbackContext;
}
}
}

View file

@ -0,0 +1,30 @@
/*
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.file;
@SuppressWarnings("serial")
public class TypeMismatchException extends Exception {
public TypeMismatchException(String message) {
super(message);
}
}

View file

@ -0,0 +1,63 @@
/*
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.filetransfer;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Encapsulates in-progress status of uploading or downloading a file to a remote server.
*/
public class FileProgressResult {
private boolean lengthComputable = false; // declares whether total is known
private long loaded = 0; // bytes sent so far
private long total = 0; // bytes total, if known
public boolean getLengthComputable() {
return lengthComputable;
}
public void setLengthComputable(boolean computable) {
this.lengthComputable = computable;
}
public long getLoaded() {
return loaded;
}
public void setLoaded(long bytes) {
this.loaded = bytes;
}
public long getTotal() {
return total;
}
public void setTotal(long bytes) {
this.total = bytes;
}
public JSONObject toJSONObject() throws JSONException {
return new JSONObject(
"{loaded:" + loaded +
",total:" + total +
",lengthComputable:" + (lengthComputable ? "true" : "false") + "}");
}
}

View file

@ -0,0 +1,932 @@
/*
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.filetransfer;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.zip.GZIPInputStream;
import java.util.zip.Inflater;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaResourceApi;
import org.apache.cordova.CordovaResourceApi.OpenForReadResult;
import org.apache.cordova.LOG;
import org.apache.cordova.PluginManager;
import org.apache.cordova.PluginResult;
import org.apache.cordova.Whitelist;
import org.apache.cordova.file.FileUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.net.Uri;
import android.os.Build;
import android.webkit.CookieManager;
public class FileTransfer extends CordovaPlugin {
private static final String LOG_TAG = "FileTransfer";
private static final String LINE_START = "--";
private static final String LINE_END = "\r\n";
private static final String BOUNDARY = "+++++";
public static int FILE_NOT_FOUND_ERR = 1;
public static int INVALID_URL_ERR = 2;
public static int CONNECTION_ERR = 3;
public static int ABORTED_ERR = 4;
public static int NOT_MODIFIED_ERR = 5;
private static HashMap<String, RequestContext> activeRequests = new HashMap<String, RequestContext>();
private static final int MAX_BUFFER_SIZE = 16 * 1024;
private static final class RequestContext {
String source;
String target;
File targetFile;
CallbackContext callbackContext;
HttpURLConnection connection;
boolean aborted;
RequestContext(String source, String target, CallbackContext callbackContext) {
this.source = source;
this.target = target;
this.callbackContext = callbackContext;
}
void sendPluginResult(PluginResult pluginResult) {
synchronized (this) {
if (!aborted) {
callbackContext.sendPluginResult(pluginResult);
}
}
}
}
/**
* Adds an interface method to an InputStream to return the number of bytes
* read from the raw stream. This is used to track total progress against
* the HTTP Content-Length header value from the server.
*/
private static abstract class TrackingInputStream extends FilterInputStream {
public TrackingInputStream(final InputStream in) {
super(in);
}
public abstract long getTotalRawBytesRead();
}
private static class ExposedGZIPInputStream extends GZIPInputStream {
public ExposedGZIPInputStream(final InputStream in) throws IOException {
super(in);
}
public Inflater getInflater() {
return inf;
}
}
/**
* Provides raw bytes-read tracking for a GZIP input stream. Reports the
* total number of compressed bytes read from the input, rather than the
* number of uncompressed bytes.
*/
private static class TrackingGZIPInputStream extends TrackingInputStream {
private ExposedGZIPInputStream gzin;
public TrackingGZIPInputStream(final ExposedGZIPInputStream gzin) throws IOException {
super(gzin);
this.gzin = gzin;
}
public long getTotalRawBytesRead() {
return gzin.getInflater().getBytesRead();
}
}
/**
* Provides simple total-bytes-read tracking for an existing InputStream
*/
private static class SimpleTrackingInputStream extends TrackingInputStream {
private long bytesRead = 0;
public SimpleTrackingInputStream(InputStream stream) {
super(stream);
}
private int updateBytesRead(int newBytesRead) {
if (newBytesRead != -1) {
bytesRead += newBytesRead;
}
return newBytesRead;
}
@Override
public int read() throws IOException {
return updateBytesRead(super.read());
}
// Note: FilterInputStream delegates read(byte[] bytes) to the below method,
// so we don't override it or else double count (CB-5631).
@Override
public int read(byte[] bytes, int offset, int count) throws IOException {
return updateBytesRead(super.read(bytes, offset, count));
}
public long getTotalRawBytesRead() {
return bytesRead;
}
}
@Override
public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
if (action.equals("upload") || action.equals("download")) {
String source = args.getString(0);
String target = args.getString(1);
if (action.equals("upload")) {
upload(source, target, args, callbackContext);
} else {
download(source, target, args, callbackContext);
}
return true;
} else if (action.equals("abort")) {
String objectId = args.getString(0);
abort(objectId);
callbackContext.success();
return true;
}
return false;
}
private static void addHeadersToRequest(URLConnection connection, JSONObject headers) {
try {
for (Iterator<?> iter = headers.keys(); iter.hasNext(); ) {
/* RFC 2616 says that non-ASCII characters and control
* characters are not allowed in header names or values.
* Additionally, spaces are not allowed in header names.
* RFC 2046 Quoted-printable encoding may be used to encode
* arbitrary characters, but we donon- not do that encoding here.
*/
String headerKey = iter.next().toString();
String cleanHeaderKey = headerKey.replaceAll("\\n","")
.replaceAll("\\s+","")
.replaceAll(":", "")
.replaceAll("[^\\x20-\\x7E]+", "");
JSONArray headerValues = headers.optJSONArray(headerKey);
if (headerValues == null) {
headerValues = new JSONArray();
/* RFC 2616 also says that any amount of consecutive linear
* whitespace within a header value can be replaced with a
* single space character, without affecting the meaning of
* that value.
*/
String headerValue = headers.getString(headerKey);
String finalValue = headerValue.replaceAll("\\s+", " ").replaceAll("\\n"," ").replaceAll("[^\\x20-\\x7E]+", " ");
headerValues.put(finalValue);
}
//Use the clean header key, not the one that we passed in
connection.setRequestProperty(cleanHeaderKey, headerValues.getString(0));
for (int i = 1; i < headerValues.length(); ++i) {
connection.addRequestProperty(headerKey, headerValues.getString(i));
}
}
} catch (JSONException e1) {
// No headers to be manipulated!
}
}
private String getCookies(final String target) {
boolean gotCookie = false;
String cookie = null;
Class webViewClass = webView.getClass();
try {
Method gcmMethod = webViewClass.getMethod("getCookieManager");
Class iccmClass = gcmMethod.getReturnType();
Method gcMethod = iccmClass.getMethod("getCookie", String.class);
cookie = (String)gcMethod.invoke(
iccmClass.cast(
gcmMethod.invoke(webView)
), target);
gotCookie = true;
} catch (NoSuchMethodException e) {
} catch (IllegalAccessException e) {
} catch (InvocationTargetException e) {
} catch (ClassCastException e) {
}
if (!gotCookie && CookieManager.getInstance() != null) {
cookie = CookieManager.getInstance().getCookie(target);
}
return cookie;
}
/**
* Uploads the specified file to the server URL provided using an HTTP multipart request.
* @param source Full path of the file on the file system
* @param target URL of the server to receive the file
* @param args JSON Array of args
* @param callbackContext callback id for optional progress reports
*
* args[2] fileKey Name of file request parameter
* args[3] fileName File name to be used on server
* args[4] mimeType Describes file content type
* args[5] params key:value pairs of user-defined parameters
* @return FileUploadResult containing result of upload request
*/
private void upload(final String source, final String target, JSONArray args, CallbackContext callbackContext) throws JSONException {
LOG.d(LOG_TAG, "upload " + source + " to " + target);
// Setup the options
final String fileKey = getArgument(args, 2, "file");
final String fileName = getArgument(args, 3, "image.jpg");
final String mimeType = getArgument(args, 4, "image/jpeg");
final JSONObject params = args.optJSONObject(5) == null ? new JSONObject() : args.optJSONObject(5);
// Always use chunked mode unless set to false as per API
final boolean chunkedMode = args.optBoolean(7) || args.isNull(7);
// Look for headers on the params map for backwards compatibility with older Cordova versions.
final JSONObject headers = args.optJSONObject(8) == null ? params.optJSONObject("headers") : args.optJSONObject(8);
final String objectId = args.getString(9);
final String httpMethod = getArgument(args, 10, "POST");
final CordovaResourceApi resourceApi = webView.getResourceApi();
LOG.d(LOG_TAG, "fileKey: " + fileKey);
LOG.d(LOG_TAG, "fileName: " + fileName);
LOG.d(LOG_TAG, "mimeType: " + mimeType);
LOG.d(LOG_TAG, "params: " + params);
LOG.d(LOG_TAG, "chunkedMode: " + chunkedMode);
LOG.d(LOG_TAG, "headers: " + headers);
LOG.d(LOG_TAG, "objectId: " + objectId);
LOG.d(LOG_TAG, "httpMethod: " + httpMethod);
final Uri targetUri = resourceApi.remapUri(Uri.parse(target));
int uriType = CordovaResourceApi.getUriType(targetUri);
final boolean useHttps = uriType == CordovaResourceApi.URI_TYPE_HTTPS;
if (uriType != CordovaResourceApi.URI_TYPE_HTTP && !useHttps) {
JSONObject error = createFileTransferError(INVALID_URL_ERR, source, target, null, 0, null);
LOG.e(LOG_TAG, "Unsupported URI: " + targetUri);
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
return;
}
final RequestContext context = new RequestContext(source, target, callbackContext);
synchronized (activeRequests) {
activeRequests.put(objectId, context);
}
cordova.getThreadPool().execute(new Runnable() {
public void run() {
if (context.aborted) {
return;
}
// We should call remapUri on background thread otherwise it throws
// IllegalStateException when trying to remap 'cdvfile://localhost/content/...' URIs
// via ContentFilesystem (see https://issues.apache.org/jira/browse/CB-9022)
Uri tmpSrc = Uri.parse(source);
final Uri sourceUri = resourceApi.remapUri(
tmpSrc.getScheme() != null ? tmpSrc : Uri.fromFile(new File(source)));
HttpURLConnection conn = null;
int totalBytes = 0;
int fixedLength = -1;
try {
// Create return object
FileUploadResult result = new FileUploadResult();
FileProgressResult progress = new FileProgressResult();
//------------------ CLIENT REQUEST
// Open a HTTP connection to the URL based on protocol
conn = resourceApi.createHttpConnection(targetUri);
// Allow Inputs
conn.setDoInput(true);
// Allow Outputs
conn.setDoOutput(true);
// Don't use a cached copy.
conn.setUseCaches(false);
// Use a post method.
conn.setRequestMethod(httpMethod);
// if we specified a Content-Type header, don't do multipart form upload
boolean multipartFormUpload = (headers == null) || !headers.has("Content-Type");
if (multipartFormUpload) {
conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
}
// Set the cookies on the response
String cookie = getCookies(target);
if (cookie != null) {
conn.setRequestProperty("Cookie", cookie);
}
// Handle the other headers
if (headers != null) {
addHeadersToRequest(conn, headers);
}
/*
* Store the non-file portions of the multipart data as a string, so that we can add it
* to the contentSize, since it is part of the body of the HTTP request.
*/
StringBuilder beforeData = new StringBuilder();
try {
for (Iterator<?> iter = params.keys(); iter.hasNext();) {
Object key = iter.next();
if(!String.valueOf(key).equals("headers"))
{
beforeData.append(LINE_START).append(BOUNDARY).append(LINE_END);
beforeData.append("Content-Disposition: form-data; name=\"").append(key.toString()).append('"');
beforeData.append(LINE_END).append(LINE_END);
beforeData.append(params.getString(key.toString()));
beforeData.append(LINE_END);
}
}
} catch (JSONException e) {
LOG.e(LOG_TAG, e.getMessage(), e);
}
beforeData.append(LINE_START).append(BOUNDARY).append(LINE_END);
beforeData.append("Content-Disposition: form-data; name=\"").append(fileKey).append("\";");
beforeData.append(" filename=\"").append(fileName).append('"').append(LINE_END);
beforeData.append("Content-Type: ").append(mimeType).append(LINE_END).append(LINE_END);
byte[] beforeDataBytes = beforeData.toString().getBytes("UTF-8");
byte[] tailParamsBytes = (LINE_END + LINE_START + BOUNDARY + LINE_START + LINE_END).getBytes("UTF-8");
// Get a input stream of the file on the phone
OpenForReadResult readResult = resourceApi.openForRead(sourceUri);
int stringLength = beforeDataBytes.length + tailParamsBytes.length;
if (readResult.length >= 0) {
fixedLength = (int)readResult.length;
if (multipartFormUpload)
fixedLength += stringLength;
progress.setLengthComputable(true);
progress.setTotal(fixedLength);
}
LOG.d(LOG_TAG, "Content Length: " + fixedLength);
// setFixedLengthStreamingMode causes and OutOfMemoryException on pre-Froyo devices.
// http://code.google.com/p/android/issues/detail?id=3164
// It also causes OOM if HTTPS is used, even on newer devices.
boolean useChunkedMode = chunkedMode || (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO);
useChunkedMode = useChunkedMode || (fixedLength == -1);
if (useChunkedMode) {
conn.setChunkedStreamingMode(MAX_BUFFER_SIZE);
// Although setChunkedStreamingMode sets this header, setting it explicitly here works
// around an OutOfMemoryException when using https.
conn.setRequestProperty("Transfer-Encoding", "chunked");
} else {
conn.setFixedLengthStreamingMode(fixedLength);
if (useHttps) {
LOG.w(LOG_TAG, "setFixedLengthStreamingMode could cause OutOfMemoryException - switch to chunkedMode=true to avoid it if this is an issue.");
}
}
conn.connect();
OutputStream sendStream = null;
try {
sendStream = conn.getOutputStream();
synchronized (context) {
if (context.aborted) {
return;
}
context.connection = conn;
}
if (multipartFormUpload) {
//We don't want to change encoding, we just want this to write for all Unicode.
sendStream.write(beforeDataBytes);
totalBytes += beforeDataBytes.length;
}
// create a buffer of maximum size
int bytesAvailable = readResult.inputStream.available();
int bufferSize = Math.min(bytesAvailable, MAX_BUFFER_SIZE);
byte[] buffer = new byte[bufferSize];
// read file and write it into form...
int bytesRead = readResult.inputStream.read(buffer, 0, bufferSize);
long prevBytesRead = 0;
while (bytesRead > 0) {
totalBytes += bytesRead;
result.setBytesSent(totalBytes);
sendStream.write(buffer, 0, bytesRead);
if (totalBytes > prevBytesRead + 102400) {
prevBytesRead = totalBytes;
LOG.d(LOG_TAG, "Uploaded " + totalBytes + " of " + fixedLength + " bytes");
}
bytesAvailable = readResult.inputStream.available();
bufferSize = Math.min(bytesAvailable, MAX_BUFFER_SIZE);
bytesRead = readResult.inputStream.read(buffer, 0, bufferSize);
// Send a progress event.
progress.setLoaded(totalBytes);
PluginResult progressResult = new PluginResult(PluginResult.Status.OK, progress.toJSONObject());
progressResult.setKeepCallback(true);
context.sendPluginResult(progressResult);
}
if (multipartFormUpload) {
// send multipart form data necessary after file data...
sendStream.write(tailParamsBytes);
totalBytes += tailParamsBytes.length;
}
sendStream.flush();
} finally {
safeClose(readResult.inputStream);
safeClose(sendStream);
}
synchronized (context) {
context.connection = null;
}
LOG.d(LOG_TAG, "Sent " + totalBytes + " of " + fixedLength);
//------------------ read the SERVER RESPONSE
String responseString;
int responseCode = conn.getResponseCode();
LOG.d(LOG_TAG, "response code: " + responseCode);
LOG.d(LOG_TAG, "response headers: " + conn.getHeaderFields());
TrackingInputStream inStream = null;
try {
inStream = getInputStream(conn);
synchronized (context) {
if (context.aborted) {
return;
}
context.connection = conn;
}
ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(1024, conn.getContentLength()));
byte[] buffer = new byte[1024];
int bytesRead = 0;
// write bytes to file
while ((bytesRead = inStream.read(buffer)) > 0) {
out.write(buffer, 0, bytesRead);
}
responseString = out.toString("UTF-8");
} finally {
synchronized (context) {
context.connection = null;
}
safeClose(inStream);
}
LOG.d(LOG_TAG, "got response from server");
LOG.d(LOG_TAG, responseString.substring(0, Math.min(256, responseString.length())));
// send request and retrieve response
result.setResponseCode(responseCode);
result.setResponse(responseString);
context.sendPluginResult(new PluginResult(PluginResult.Status.OK, result.toJSONObject()));
} catch (FileNotFoundException e) {
JSONObject error = createFileTransferError(FILE_NOT_FOUND_ERR, source, target, conn, e);
LOG.e(LOG_TAG, error.toString(), e);
context.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
} catch (IOException e) {
JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, conn, e);
LOG.e(LOG_TAG, error.toString(), e);
LOG.e(LOG_TAG, "Failed after uploading " + totalBytes + " of " + fixedLength + " bytes.");
context.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
} catch (JSONException e) {
LOG.e(LOG_TAG, e.getMessage(), e);
context.sendPluginResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION));
} catch (Throwable t) {
// Shouldn't happen, but will
JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, conn, t);
LOG.e(LOG_TAG, error.toString(), t);
context.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
} finally {
synchronized (activeRequests) {
activeRequests.remove(objectId);
}
}
}
});
}
private static void safeClose(Closeable stream) {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
}
}
}
private static TrackingInputStream getInputStream(URLConnection conn) throws IOException {
String encoding = conn.getContentEncoding();
if (encoding != null && encoding.equalsIgnoreCase("gzip")) {
return new TrackingGZIPInputStream(new ExposedGZIPInputStream(conn.getInputStream()));
}
return new SimpleTrackingInputStream(conn.getInputStream());
}
private static JSONObject createFileTransferError(int errorCode, String source, String target, URLConnection connection, Throwable throwable) {
int httpStatus = 0;
StringBuilder bodyBuilder = new StringBuilder();
String body = null;
if (connection != null) {
try {
if (connection instanceof HttpURLConnection) {
httpStatus = ((HttpURLConnection)connection).getResponseCode();
InputStream err = ((HttpURLConnection) connection).getErrorStream();
if(err != null)
{
BufferedReader reader = new BufferedReader(new InputStreamReader(err, "UTF-8"));
try {
String line = reader.readLine();
while(line != null) {
bodyBuilder.append(line);
line = reader.readLine();
if(line != null) {
bodyBuilder.append('\n');
}
}
body = bodyBuilder.toString();
} finally {
reader.close();
}
}
}
// IOException can leave connection object in a bad state, so catch all exceptions.
} catch (Throwable e) {
LOG.w(LOG_TAG, "Error getting HTTP status code from connection.", e);
}
}
return createFileTransferError(errorCode, source, target, body, httpStatus, throwable);
}
/**
* Create an error object based on the passed in errorCode
* @param errorCode the error
* @return JSONObject containing the error
*/
private static JSONObject createFileTransferError(int errorCode, String source, String target, String body, Integer httpStatus, Throwable throwable) {
JSONObject error = null;
try {
error = new JSONObject();
error.put("code", errorCode);
error.put("source", source);
error.put("target", target);
if(body != null)
{
error.put("body", body);
}
if (httpStatus != null) {
error.put("http_status", httpStatus);
}
if (throwable != null) {
String msg = throwable.getMessage();
if (msg == null || "".equals(msg)) {
msg = throwable.toString();
}
error.put("exception", msg);
}
} catch (JSONException e) {
LOG.e(LOG_TAG, e.getMessage(), e);
}
return error;
}
/**
* Convenience method to read a parameter from the list of JSON args.
* @param args the args passed to the Plugin
* @param position the position to retrieve the arg from
* @param defaultString the default to be used if the arg does not exist
* @return String with the retrieved value
*/
private static String getArgument(JSONArray args, int position, String defaultString) {
String arg = defaultString;
if (args.length() > position) {
arg = args.optString(position);
if (arg == null || "null".equals(arg)) {
arg = defaultString;
}
}
return arg;
}
/**
* Downloads a file form a given URL and saves it to the specified directory.
*
* @param source URL of the server to receive the file
* @param target Full path of the file on the file system
*/
private void download(final String source, final String target, JSONArray args, CallbackContext callbackContext) throws JSONException {
LOG.d(LOG_TAG, "download " + source + " to " + target);
final CordovaResourceApi resourceApi = webView.getResourceApi();
final String objectId = args.getString(3);
final JSONObject headers = args.optJSONObject(4);
final Uri sourceUri = resourceApi.remapUri(Uri.parse(source));
int uriType = CordovaResourceApi.getUriType(sourceUri);
final boolean useHttps = uriType == CordovaResourceApi.URI_TYPE_HTTPS;
final boolean isLocalTransfer = !useHttps && uriType != CordovaResourceApi.URI_TYPE_HTTP;
if (uriType == CordovaResourceApi.URI_TYPE_UNKNOWN) {
JSONObject error = createFileTransferError(INVALID_URL_ERR, source, target, null, 0, null);
LOG.e(LOG_TAG, "Unsupported URI: " + sourceUri);
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
return;
}
/* This code exists for compatibility between 3.x and 4.x versions of Cordova.
* Previously the CordovaWebView class had a method, getWhitelist, which would
* return a Whitelist object. Since the fixed whitelist is removed in Cordova 4.x,
* the correct call now is to shouldAllowRequest from the plugin manager.
*/
Boolean shouldAllowRequest = null;
if (isLocalTransfer) {
shouldAllowRequest = true;
}
if (shouldAllowRequest == null) {
try {
Method gwl = webView.getClass().getMethod("getWhitelist");
Whitelist whitelist = (Whitelist)gwl.invoke(webView);
shouldAllowRequest = whitelist.isUrlWhiteListed(source);
} catch (NoSuchMethodException e) {
} catch (IllegalAccessException e) {
} catch (InvocationTargetException e) {
}
}
if (shouldAllowRequest == null) {
try {
Method gpm = webView.getClass().getMethod("getPluginManager");
PluginManager pm = (PluginManager)gpm.invoke(webView);
Method san = pm.getClass().getMethod("shouldAllowRequest", String.class);
shouldAllowRequest = (Boolean)san.invoke(pm, source);
} catch (NoSuchMethodException e) {
} catch (IllegalAccessException e) {
} catch (InvocationTargetException e) {
}
}
if (!Boolean.TRUE.equals(shouldAllowRequest)) {
LOG.w(LOG_TAG, "Source URL is not in white list: '" + source + "'");
JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, null, 401, null);
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
return;
}
final RequestContext context = new RequestContext(source, target, callbackContext);
synchronized (activeRequests) {
activeRequests.put(objectId, context);
}
cordova.getThreadPool().execute(new Runnable() {
public void run() {
if (context.aborted) {
return;
}
// Accept a path or a URI for the source.
Uri tmpTarget = Uri.parse(target);
Uri targetUri = resourceApi.remapUri(
tmpTarget.getScheme() != null ? tmpTarget : Uri.fromFile(new File(target)));
HttpURLConnection connection = null;
File file = null;
PluginResult result = null;
TrackingInputStream inputStream = null;
boolean cached = false;
OutputStream outputStream = null;
try {
OpenForReadResult readResult = null;
file = resourceApi.mapUriToFile(targetUri);
context.targetFile = file;
LOG.d(LOG_TAG, "Download file:" + sourceUri);
FileProgressResult progress = new FileProgressResult();
if (isLocalTransfer) {
readResult = resourceApi.openForRead(sourceUri);
if (readResult.length != -1) {
progress.setLengthComputable(true);
progress.setTotal(readResult.length);
}
inputStream = new SimpleTrackingInputStream(readResult.inputStream);
} else {
// connect to server
// Open a HTTP connection to the URL based on protocol
connection = resourceApi.createHttpConnection(sourceUri);
connection.setRequestMethod("GET");
// TODO: Make OkHttp use this CookieManager by default.
String cookie = getCookies(sourceUri.toString());
if(cookie != null)
{
connection.setRequestProperty("cookie", cookie);
}
// This must be explicitly set for gzip progress tracking to work.
connection.setRequestProperty("Accept-Encoding", "gzip");
// Handle the other headers
if (headers != null) {
addHeadersToRequest(connection, headers);
}
connection.connect();
if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
cached = true;
connection.disconnect();
LOG.d(LOG_TAG, "Resource not modified: " + source);
JSONObject error = createFileTransferError(NOT_MODIFIED_ERR, source, target, connection, null);
result = new PluginResult(PluginResult.Status.ERROR, error);
} else {
if (connection.getContentEncoding() == null || connection.getContentEncoding().equalsIgnoreCase("gzip")) {
// Only trust content-length header if we understand
// the encoding -- identity or gzip
if (connection.getContentLength() != -1) {
progress.setLengthComputable(true);
progress.setTotal(connection.getContentLength());
}
}
inputStream = getInputStream(connection);
}
}
if (!cached) {
try {
synchronized (context) {
if (context.aborted) {
return;
}
context.connection = connection;
}
// write bytes to file
byte[] buffer = new byte[MAX_BUFFER_SIZE];
int bytesRead = 0;
outputStream = resourceApi.openOutputStream(targetUri);
while ((bytesRead = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, bytesRead);
// Send a progress event.
progress.setLoaded(inputStream.getTotalRawBytesRead());
PluginResult progressResult = new PluginResult(PluginResult.Status.OK, progress.toJSONObject());
progressResult.setKeepCallback(true);
context.sendPluginResult(progressResult);
}
} finally {
synchronized (context) {
context.connection = null;
}
safeClose(inputStream);
safeClose(outputStream);
}
LOG.d(LOG_TAG, "Saved file: " + target);
// create FileEntry object
Class webViewClass = webView.getClass();
PluginManager pm = null;
try {
Method gpm = webViewClass.getMethod("getPluginManager");
pm = (PluginManager) gpm.invoke(webView);
} catch (NoSuchMethodException e) {
} catch (IllegalAccessException e) {
} catch (InvocationTargetException e) {
}
if (pm == null) {
try {
Field pmf = webViewClass.getField("pluginManager");
pm = (PluginManager)pmf.get(webView);
} catch (NoSuchFieldException e) {
} catch (IllegalAccessException e) {
}
}
file = resourceApi.mapUriToFile(targetUri);
context.targetFile = file;
FileUtils filePlugin = (FileUtils) pm.getPlugin("File");
if (filePlugin != null) {
JSONObject fileEntry = filePlugin.getEntryForFile(file);
if (fileEntry != null) {
result = new PluginResult(PluginResult.Status.OK, fileEntry);
} else {
JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, connection, null);
LOG.e(LOG_TAG, "File plugin cannot represent download path");
result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
}
} else {
LOG.e(LOG_TAG, "File plugin not found; cannot save downloaded file");
result = new PluginResult(PluginResult.Status.ERROR, "File plugin not found; cannot save downloaded file");
}
}
} catch (FileNotFoundException e) {
JSONObject error = createFileTransferError(FILE_NOT_FOUND_ERR, source, target, connection, e);
LOG.e(LOG_TAG, error.toString(), e);
result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
} catch (IOException e) {
JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, connection, e);
LOG.e(LOG_TAG, error.toString(), e);
result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
} catch (JSONException e) {
LOG.e(LOG_TAG, e.getMessage(), e);
result = new PluginResult(PluginResult.Status.JSON_EXCEPTION);
} catch (Throwable e) {
JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, connection, e);
LOG.e(LOG_TAG, error.toString(), e);
result = new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
} finally {
synchronized (activeRequests) {
activeRequests.remove(objectId);
}
if (result == null) {
result = new PluginResult(PluginResult.Status.ERROR, createFileTransferError(CONNECTION_ERR, source, target, connection, null));
}
// Remove incomplete download.
if (!cached && result.getStatus() != PluginResult.Status.OK.ordinal() && file != null) {
file.delete();
}
context.sendPluginResult(result);
}
}
});
}
/**
* Abort an ongoing upload or download.
*/
private void abort(String objectId) {
final RequestContext context;
synchronized (activeRequests) {
context = activeRequests.remove(objectId);
}
if (context != null) {
// Closing the streams can block, so execute on a background thread.
cordova.getThreadPool().execute(new Runnable() {
public void run() {
synchronized (context) {
File file = context.targetFile;
if (file != null) {
file.delete();
}
// Trigger the abort callback immediately to minimize latency between it and abort() being called.
JSONObject error = createFileTransferError(ABORTED_ERR, context.source, context.target, null, -1, null);
context.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, error));
context.aborted = true;
if (context.connection != null) {
try {
context.connection.disconnect();
} catch (Exception e) {
LOG.e(LOG_TAG, "CB-8431 Catch workaround for fatal exception", e);
}
}
}
}
});
}
}
}

View file

@ -0,0 +1,73 @@
/*
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.filetransfer;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Encapsulates the result and/or status of uploading a file to a remote server.
*/
public class FileUploadResult {
private long bytesSent = 0; // bytes sent
private int responseCode = -1; // HTTP response code
private String response = null; // HTTP response
private String objectId = null; // FileTransfer object id
public long getBytesSent() {
return bytesSent;
}
public void setBytesSent(long bytes) {
this.bytesSent = bytes;
}
public int getResponseCode() {
return responseCode;
}
public void setResponseCode(int responseCode) {
this.responseCode = responseCode;
}
public String getResponse() {
return response;
}
public void setResponse(String response) {
this.response = response;
}
public String getObjectId() {
return objectId;
}
public void setObjectId(String objectId) {
this.objectId = objectId;
}
public JSONObject toJSONObject() throws JSONException {
return new JSONObject(
"{bytesSent:" + bytesSent +
",responseCode:" + responseCode +
",response:" + JSONObject.quote(response) +
",objectId:" + JSONObject.quote(objectId) + "}");
}
}

View file

@ -0,0 +1,57 @@
/*
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.inappbrowser;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Created by Oliver on 22/11/2013.
*/
public class InAppBrowserDialog extends Dialog {
Context context;
InAppBrowser inAppBrowser = null;
public InAppBrowserDialog(Context context, int theme) {
super(context, theme);
this.context = context;
}
public void setInAppBroswer(InAppBrowser browser) {
this.inAppBrowser = browser;
}
public void onBackPressed () {
if (this.inAppBrowser == null) {
this.dismiss();
} else {
// better to go through the in inAppBrowser
// because it does a clean up
if (this.inAppBrowser.hardwareBack() && this.inAppBrowser.canGoBack()) {
this.inAppBrowser.goBack();
} else {
this.inAppBrowser.closeDialog();
}
}
}
}

View file

@ -0,0 +1,133 @@
/*
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.inappbrowser;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.LOG;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import android.webkit.JsPromptResult;
import android.webkit.WebChromeClient;
import android.webkit.WebStorage;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.webkit.GeolocationPermissions.Callback;
public class InAppChromeClient extends WebChromeClient {
private CordovaWebView webView;
private String LOG_TAG = "InAppChromeClient";
private long MAX_QUOTA = 100 * 1024 * 1024;
public InAppChromeClient(CordovaWebView webView) {
super();
this.webView = webView;
}
/**
* Handle database quota exceeded notification.
*
* @param url
* @param databaseIdentifier
* @param currentQuota
* @param estimatedSize
* @param totalUsedQuota
* @param quotaUpdater
*/
@Override
public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize,
long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater)
{
LOG.d(LOG_TAG, "onExceededDatabaseQuota estimatedSize: %d currentQuota: %d totalUsedQuota: %d", estimatedSize, currentQuota, totalUsedQuota);
quotaUpdater.updateQuota(MAX_QUOTA);
}
/**
* Instructs the client to show a prompt to ask the user to set the Geolocation permission state for the specified origin.
*
* @param origin
* @param callback
*/
@Override
public void onGeolocationPermissionsShowPrompt(String origin, Callback callback) {
super.onGeolocationPermissionsShowPrompt(origin, callback);
callback.invoke(origin, true, false);
}
/**
* Tell the client to display a prompt dialog to the user.
* If the client returns true, WebView will assume that the client will
* handle the prompt dialog and call the appropriate JsPromptResult method.
*
* The prompt bridge provided for the InAppBrowser is capable of executing any
* oustanding callback belonging to the InAppBrowser plugin. Care has been
* taken that other callbacks cannot be triggered, and that no other code
* execution is possible.
*
* To trigger the bridge, the prompt default value should be of the form:
*
* gap-iab://<callbackId>
*
* where <callbackId> is the string id of the callback to trigger (something
* like "InAppBrowser0123456789")
*
* If present, the prompt message is expected to be a JSON-encoded value to
* pass to the callback. A JSON_EXCEPTION is returned if the JSON is invalid.
*
* @param view
* @param url
* @param message
* @param defaultValue
* @param result
*/
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
// See if the prompt string uses the 'gap-iab' protocol. If so, the remainder should be the id of a callback to execute.
if (defaultValue != null && defaultValue.startsWith("gap")) {
if(defaultValue.startsWith("gap-iab://")) {
PluginResult scriptResult;
String scriptCallbackId = defaultValue.substring(10);
if (scriptCallbackId.startsWith("InAppBrowser")) {
if(message == null || message.length() == 0) {
scriptResult = new PluginResult(PluginResult.Status.OK, new JSONArray());
} else {
try {
scriptResult = new PluginResult(PluginResult.Status.OK, new JSONArray(message));
} catch(JSONException e) {
scriptResult = new PluginResult(PluginResult.Status.JSON_EXCEPTION, e.getMessage());
}
}
this.webView.sendPluginResult(scriptResult, scriptCallbackId);
result.confirm("");
return true;
}
}
else
{
// Anything else with a gap: prefix should get this message
LOG.w(LOG_TAG, "InAppBrowser does not support Cordova API calls: " + url + " " + defaultValue);
result.cancel();
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,413 @@
/*
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.splashscreen;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Configuration;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Handler;
import android.view.Display;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.AlphaAnimation;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.json.JSONArray;
import org.json.JSONException;
public class SplashScreen extends CordovaPlugin {
private static final String LOG_TAG = "SplashScreen";
// Cordova 3.x.x has a copy of this plugin bundled with it (SplashScreenInternal.java).
// Enable functionality only if running on 4.x.x.
private static final boolean HAS_BUILT_IN_SPLASH_SCREEN = Integer.valueOf(CordovaWebView.CORDOVA_VERSION.split("\\.")[0]) < 4;
private static final int DEFAULT_SPLASHSCREEN_DURATION = 3000;
private static final int DEFAULT_FADE_DURATION = 500;
private static Dialog splashDialog;
private static ProgressDialog spinnerDialog;
private static boolean firstShow = true;
private static boolean lastHideAfterDelay; // https://issues.apache.org/jira/browse/CB-9094
/**
* Displays the splash drawable.
*/
private ImageView splashImageView;
/**
* Remember last device orientation to detect orientation changes.
*/
private int orientation;
// Helper to be compile-time compatible with both Cordova 3.x and 4.x.
private View getView() {
try {
return (View)webView.getClass().getMethod("getView").invoke(webView);
} catch (Exception e) {
return (View)webView;
}
}
private int getSplashId() {
int drawableId = 0;
String splashResource = preferences.getString("SplashScreen", "screen");
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());
}
}
return drawableId;
}
@Override
protected void pluginInitialize() {
if (HAS_BUILT_IN_SPLASH_SCREEN) {
return;
}
// Make WebView invisible while loading URL
// CB-11326 Ensure we're calling this on UI thread
cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
getView().setVisibility(View.INVISIBLE);
}
});
int drawableId = getSplashId();
// Save initial orientation.
orientation = cordova.getActivity().getResources().getConfiguration().orientation;
if (firstShow) {
boolean autoHide = preferences.getBoolean("AutoHideSplashScreen", true);
showSplashScreen(autoHide);
}
if (preferences.getBoolean("SplashShowOnlyFirstTime", true)) {
firstShow = false;
}
}
/**
* Shorter way to check value of "SplashMaintainAspectRatio" preference.
*/
private boolean isMaintainAspectRatio () {
return preferences.getBoolean("SplashMaintainAspectRatio", false);
}
private int getFadeDuration () {
int fadeSplashScreenDuration = preferences.getBoolean("FadeSplashScreen", true) ?
preferences.getInteger("FadeSplashScreenDuration", DEFAULT_FADE_DURATION) : 0;
if (fadeSplashScreenDuration < 30) {
// [CB-9750] This value used to be in decimal seconds, so we will assume that if someone specifies 10
// they mean 10 seconds, and not the meaningless 10ms
fadeSplashScreenDuration *= 1000;
}
return fadeSplashScreenDuration;
}
@Override
public void onPause(boolean multitasking) {
if (HAS_BUILT_IN_SPLASH_SCREEN) {
return;
}
// hide the splash screen to avoid leaking a window
this.removeSplashScreen(true);
}
@Override
public void onDestroy() {
if (HAS_BUILT_IN_SPLASH_SCREEN) {
return;
}
// hide the splash screen to avoid leaking a window
this.removeSplashScreen(true);
// If we set this to true onDestroy, we lose track when we go from page to page!
//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 {
return false;
}
callbackContext.success();
return true;
}
@Override
public Object onMessage(String id, Object data) {
if (HAS_BUILT_IN_SPLASH_SCREEN) {
return null;
}
if ("splashscreen".equals(id)) {
if ("hide".equals(data.toString())) {
this.removeSplashScreen(false);
} else {
this.showSplashScreen(false);
}
} else if ("spinner".equals(id)) {
if ("stop".equals(data.toString())) {
getView().setVisibility(View.VISIBLE);
}
} else if ("onReceivedError".equals(id)) {
this.spinnerStop();
}
return null;
}
// Don't add @Override so that plugin still compiles on 3.x.x for a while
public void onConfigurationChanged(Configuration newConfig) {
if (newConfig.orientation != orientation) {
orientation = newConfig.orientation;
// Splash drawable may change with orientation, so reload it.
if (splashImageView != null) {
int drawableId = getSplashId();
if (drawableId != 0) {
splashImageView.setImageDrawable(cordova.getActivity().getResources().getDrawable(drawableId));
}
}
}
}
private void removeSplashScreen(final boolean forceHideImmediately) {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
if (splashDialog != null && splashDialog.isShowing()) {
final int fadeSplashScreenDuration = getFadeDuration();
// CB-10692 If the plugin is being paused/destroyed, skip the fading and hide it immediately
if (fadeSplashScreenDuration > 0 && forceHideImmediately == false) {
AlphaAnimation fadeOut = new AlphaAnimation(1, 0);
fadeOut.setInterpolator(new DecelerateInterpolator());
fadeOut.setDuration(fadeSplashScreenDuration);
splashImageView.setAnimation(fadeOut);
splashImageView.startAnimation(fadeOut);
fadeOut.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
spinnerStop();
}
@Override
public void onAnimationEnd(Animation animation) {
if (splashDialog != null && splashDialog.isShowing()) {
splashDialog.dismiss();
splashDialog = null;
splashImageView = null;
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
} else {
spinnerStop();
splashDialog.dismiss();
splashDialog = null;
splashImageView = null;
}
}
}
});
}
/**
* Shows the splash screen over the full Activity
*/
@SuppressWarnings("deprecation")
private void showSplashScreen(final boolean hideAfterDelay) {
final int splashscreenTime = preferences.getInteger("SplashScreenDelay", DEFAULT_SPLASHSCREEN_DURATION);
final int drawableId = getSplashId();
final int fadeSplashScreenDuration = getFadeDuration();
final int effectiveSplashDuration = Math.max(0, splashscreenTime - fadeSplashScreenDuration);
lastHideAfterDelay = hideAfterDelay;
// Prevent to show the splash dialog if the activity is in the process of finishing
if (cordova.getActivity().isFinishing()) {
return;
}
// If the splash dialog is showing don't try to show it again
if (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();
// Use an ImageView to render the image because of its flexible scaling options.
splashImageView = new ImageView(context);
splashImageView.setImageResource(drawableId);
LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
splashImageView.setLayoutParams(layoutParams);
splashImageView.setMinimumHeight(display.getHeight());
splashImageView.setMinimumWidth(display.getWidth());
// TODO: Use the background color of the webView's parent instead of using the preference.
splashImageView.setBackgroundColor(preferences.getInteger("backgroundColor", Color.BLACK));
if (isMaintainAspectRatio()) {
// CENTER_CROP scale mode is equivalent to CSS "background-size:cover"
splashImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
}
else {
// FIT_XY scales image non-uniformly to fit into image view.
splashImageView.setScaleType(ImageView.ScaleType.FIT_XY);
}
// 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(splashImageView);
splashDialog.setCancelable(false);
splashDialog.show();
if (preferences.getBoolean("ShowSplashScreenSpinner", true)) {
spinnerStart();
}
// Set Runnable to remove splash screen just in case
if (hideAfterDelay) {
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
public void run() {
if (lastHideAfterDelay) {
removeSplashScreen(false);
}
}
}, effectiveSplashDuration);
}
}
});
}
// Show only spinner in the center of the screen
private void spinnerStart() {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
spinnerStop();
spinnerDialog = new ProgressDialog(webView.getContext());
spinnerDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
spinnerDialog = null;
}
});
spinnerDialog.setCancelable(false);
spinnerDialog.setIndeterminate(true);
RelativeLayout centeredLayout = new RelativeLayout(cordova.getActivity());
centeredLayout.setGravity(Gravity.CENTER);
centeredLayout.setLayoutParams(new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
ProgressBar progressBar = new ProgressBar(webView.getContext());
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
progressBar.setLayoutParams(layoutParams);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
String colorName = preferences.getString("SplashScreenSpinnerColor", null);
if(colorName != null){
int[][] states = new int[][] {
new int[] { android.R.attr.state_enabled}, // enabled
new int[] {-android.R.attr.state_enabled}, // disabled
new int[] {-android.R.attr.state_checked}, // unchecked
new int[] { android.R.attr.state_pressed} // pressed
};
int progressBarColor = Color.parseColor(colorName);
int[] colors = new int[] {
progressBarColor,
progressBarColor,
progressBarColor,
progressBarColor
};
ColorStateList colorStateList = new ColorStateList(states, colors);
progressBar.setIndeterminateTintList(colorStateList);
}
}
centeredLayout.addView(progressBar);
spinnerDialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
spinnerDialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
spinnerDialog.show();
spinnerDialog.setContentView(centeredLayout);
}
});
}
private void spinnerStop() {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
if (spinnerDialog != null && spinnerDialog.isShowing()) {
spinnerDialog.dismiss();
spinnerDialog = null;
}
}
});
}
}

View file

@ -0,0 +1,276 @@
/*
* 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.statusbar;
import android.app.Activity;
import android.graphics.Color;
import android.os.Build;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaArgs;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.LOG;
import org.apache.cordova.PluginResult;
import org.json.JSONException;
import java.util.Arrays;
public class StatusBar extends CordovaPlugin {
private static final String TAG = "StatusBar";
/**
* Sets the context of the Command. This can then be used to do things like
* get file paths associated with the Activity.
*
* @param cordova The context of the main Activity.
* @param webView The CordovaWebView Cordova is running in.
*/
@Override
public void initialize(final CordovaInterface cordova, CordovaWebView webView) {
LOG.v(TAG, "StatusBar: initialization");
super.initialize(cordova, webView);
this.cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
// Clear flag FLAG_FORCE_NOT_FULLSCREEN which is set initially
// by the Cordova.
Window window = cordova.getActivity().getWindow();
window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
// Read 'StatusBarBackgroundColor' from config.xml, default is #000000.
setStatusBarBackgroundColor(preferences.getString("StatusBarBackgroundColor", "#000000"));
// Read 'StatusBarStyle' from config.xml, default is 'lightcontent'.
setStatusBarStyle(preferences.getString("StatusBarStyle", "lightcontent"));
}
});
}
/**
* Executes the request and returns PluginResult.
*
* @param action The action to execute.
* @param args JSONArry of arguments for the plugin.
* @param callbackContext The callback id used when calling back into JavaScript.
* @return True if the action was valid, false otherwise.
*/
@Override
public boolean execute(final String action, final CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
LOG.v(TAG, "Executing action: " + action);
final Activity activity = this.cordova.getActivity();
final Window window = activity.getWindow();
if ("_ready".equals(action)) {
boolean statusBarVisible = (window.getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) == 0;
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, statusBarVisible));
return true;
}
if ("show".equals(action)) {
this.cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
// SYSTEM_UI_FLAG_FULLSCREEN is available since JellyBean, but we
// use KitKat here to be aligned with "Fullscreen" preference
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
int uiOptions = window.getDecorView().getSystemUiVisibility();
uiOptions &= ~View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
uiOptions &= ~View.SYSTEM_UI_FLAG_FULLSCREEN;
window.getDecorView().setSystemUiVisibility(uiOptions);
}
// CB-11197 We still need to update LayoutParams to force status bar
// to be hidden when entering e.g. text fields
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
});
return true;
}
if ("hide".equals(action)) {
this.cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
// SYSTEM_UI_FLAG_FULLSCREEN is available since JellyBean, but we
// use KitKat here to be aligned with "Fullscreen" preference
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
int uiOptions = window.getDecorView().getSystemUiVisibility()
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_FULLSCREEN;
window.getDecorView().setSystemUiVisibility(uiOptions);
}
// CB-11197 We still need to update LayoutParams to force status bar
// to be hidden when entering e.g. text fields
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
});
return true;
}
if ("backgroundColorByHexString".equals(action)) {
this.cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
try {
setStatusBarBackgroundColor(args.getString(0));
} catch (JSONException ignore) {
LOG.e(TAG, "Invalid hexString argument, use f.i. '#777777'");
}
}
});
return true;
}
if ("overlaysWebView".equals(action)) {
if (Build.VERSION.SDK_INT >= 21) {
this.cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
try {
setStatusBarTransparent(args.getBoolean(0));
} catch (JSONException ignore) {
LOG.e(TAG, "Invalid boolean argument");
}
}
});
return true;
}
else return args.getBoolean(0) == false;
}
if ("styleDefault".equals(action)) {
this.cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
setStatusBarStyle("default");
}
});
return true;
}
if ("styleLightContent".equals(action)) {
this.cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
setStatusBarStyle("lightcontent");
}
});
return true;
}
if ("styleBlackTranslucent".equals(action)) {
this.cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
setStatusBarStyle("blacktranslucent");
}
});
return true;
}
if ("styleBlackOpaque".equals(action)) {
this.cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
setStatusBarStyle("blackopaque");
}
});
return true;
}
return false;
}
private void setStatusBarBackgroundColor(final String colorPref) {
if (Build.VERSION.SDK_INT >= 21) {
if (colorPref != null && !colorPref.isEmpty()) {
final Window window = cordova.getActivity().getWindow();
// Method and constants not available on all SDKs but we want to be able to compile this code with any SDK
window.clearFlags(0x04000000); // SDK 19: WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.addFlags(0x80000000); // SDK 21: WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
try {
// Using reflection makes sure any 5.0+ device will work without having to compile with SDK level 21
window.getClass().getMethod("setStatusBarColor", int.class).invoke(window, Color.parseColor(colorPref));
} catch (IllegalArgumentException ignore) {
LOG.e(TAG, "Invalid hexString argument, use f.i. '#999999'");
} catch (Exception ignore) {
// this should not happen, only in case Android removes this method in a version > 21
LOG.w(TAG, "Method window.setStatusBarColor not found for SDK level " + Build.VERSION.SDK_INT);
}
}
}
}
private void setStatusBarTransparent(final boolean transparent) {
if (Build.VERSION.SDK_INT >= 21) {
final Window window = cordova.getActivity().getWindow();
if (transparent) {
window.getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
window.setStatusBarColor(Color.TRANSPARENT);
}
else {
window.getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_VISIBLE);
}
}
}
private void setStatusBarStyle(final String style) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (style != null && !style.isEmpty()) {
View decorView = cordova.getActivity().getWindow().getDecorView();
int uiOptions = decorView.getSystemUiVisibility();
String[] darkContentStyles = {
"default",
};
String[] lightContentStyles = {
"lightcontent",
"blacktranslucent",
"blackopaque",
};
if (Arrays.asList(darkContentStyles).contains(style.toLowerCase())) {
decorView.setSystemUiVisibility(uiOptions | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
return;
}
if (Arrays.asList(lightContentStyles).contains(style.toLowerCase())) {
decorView.setSystemUiVisibility(uiOptions & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
return;
}
LOG.e(TAG, "Invalid style, must be either 'default', 'lightcontent' or the deprecated 'blacktranslucent' and 'blackopaque'");
}
}
}
}

View file

@ -0,0 +1,161 @@
/*
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.whitelist;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.ConfigXmlParser;
import org.apache.cordova.LOG;
import org.apache.cordova.Whitelist;
import org.xmlpull.v1.XmlPullParser;
import android.content.Context;
public class WhitelistPlugin extends CordovaPlugin {
private static final String LOG_TAG = "WhitelistPlugin";
private Whitelist allowedNavigations;
private Whitelist allowedIntents;
private Whitelist allowedRequests;
// Used when instantiated via reflection by PluginManager
public WhitelistPlugin() {
}
// These can be used by embedders to allow Java-configuration of whitelists.
public WhitelistPlugin(Context context) {
this(new Whitelist(), new Whitelist(), null);
new CustomConfigXmlParser().parse(context);
}
public WhitelistPlugin(XmlPullParser xmlParser) {
this(new Whitelist(), new Whitelist(), null);
new CustomConfigXmlParser().parse(xmlParser);
}
public WhitelistPlugin(Whitelist allowedNavigations, Whitelist allowedIntents, Whitelist allowedRequests) {
if (allowedRequests == null) {
allowedRequests = new Whitelist();
allowedRequests.addWhiteListEntry("file:///*", false);
allowedRequests.addWhiteListEntry("data:*", false);
}
this.allowedNavigations = allowedNavigations;
this.allowedIntents = allowedIntents;
this.allowedRequests = allowedRequests;
}
@Override
public void pluginInitialize() {
if (allowedNavigations == null) {
allowedNavigations = new Whitelist();
allowedIntents = new Whitelist();
allowedRequests = new Whitelist();
new CustomConfigXmlParser().parse(webView.getContext());
}
}
private class CustomConfigXmlParser extends ConfigXmlParser {
@Override
public void handleStartTag(XmlPullParser xml) {
String strNode = xml.getName();
if (strNode.equals("content")) {
String startPage = xml.getAttributeValue(null, "src");
allowedNavigations.addWhiteListEntry(startPage, false);
} else if (strNode.equals("allow-navigation")) {
String origin = xml.getAttributeValue(null, "href");
if ("*".equals(origin)) {
allowedNavigations.addWhiteListEntry("http://*/*", false);
allowedNavigations.addWhiteListEntry("https://*/*", false);
allowedNavigations.addWhiteListEntry("data:*", false);
} else {
allowedNavigations.addWhiteListEntry(origin, false);
}
} else if (strNode.equals("allow-intent")) {
String origin = xml.getAttributeValue(null, "href");
allowedIntents.addWhiteListEntry(origin, false);
} else if (strNode.equals("access")) {
String origin = xml.getAttributeValue(null, "origin");
String subdomains = xml.getAttributeValue(null, "subdomains");
boolean external = (xml.getAttributeValue(null, "launch-external") != null);
if (origin != null) {
if (external) {
LOG.w(LOG_TAG, "Found <access launch-external> within config.xml. Please use <allow-intent> instead.");
allowedIntents.addWhiteListEntry(origin, (subdomains != null) && (subdomains.compareToIgnoreCase("true") == 0));
} else {
if ("*".equals(origin)) {
allowedRequests.addWhiteListEntry("http://*/*", false);
allowedRequests.addWhiteListEntry("https://*/*", false);
} else {
allowedRequests.addWhiteListEntry(origin, (subdomains != null) && (subdomains.compareToIgnoreCase("true") == 0));
}
}
}
}
}
@Override
public void handleEndTag(XmlPullParser xml) {
}
}
@Override
public Boolean shouldAllowNavigation(String url) {
if (allowedNavigations.isUrlWhiteListed(url)) {
return true;
}
return null; // Default policy
}
@Override
public Boolean shouldAllowRequest(String url) {
if (Boolean.TRUE == shouldAllowNavigation(url)) {
return true;
}
if (allowedRequests.isUrlWhiteListed(url)) {
return true;
}
return null; // Default policy
}
@Override
public Boolean shouldOpenExternalUrl(String url) {
if (allowedIntents.isUrlWhiteListed(url)) {
return true;
}
return null; // Default policy
}
public Whitelist getAllowedNavigations() {
return allowedNavigations;
}
public void setAllowedNavigations(Whitelist allowedNavigations) {
this.allowedNavigations = allowedNavigations;
}
public Whitelist getAllowedIntents() {
return allowedIntents;
}
public void setAllowedIntents(Whitelist allowedIntents) {
this.allowedIntents = allowedIntents;
}
public Whitelist getAllowedRequests() {
return allowedRequests;
}
public void setAllowedRequests(Whitelist allowedRequests) {
this.allowedRequests = allowedRequests;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Some files were not shown because too many files have changed in this diff Show more