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;
+ }
+ }
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/CordovaActivity.java b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaActivity.java
new file mode 100755
index 0000000..dbbb48f
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaActivity.java
@@ -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:
+ *
+ *
+ * package org.apache.cordova.examples;
+ *
+ * import android.os.Bundle;
+ * import org.apache.cordova.*;
+ *
+ * public class Example extends CordovaActivity {
+ * @Override
+ * public void onCreate(Bundle savedInstanceState) {
+ * super.onCreate(savedInstanceState);
+ * super.init();
+ * // Load your application
+ * loadUrl(launchUrl);
+ * }
+ * }
+ *
+ *
+ * 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 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.
+ *
+ * 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();
+ }
+
+ }
+
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/CordovaArgs.java b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaArgs.java
new file mode 100644
index 0000000..d40d26e
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaArgs.java
@@ -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);
+ }
+}
+
+
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/CordovaBridge.java b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaBridge.java
new file mode 100644
index 0000000..28c407f
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaBridge.java
@@ -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;
+ }
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/CordovaClientCertRequest.java b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaClientCertRequest.java
new file mode 100644
index 0000000..ccda027
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaClientCertRequest.java
@@ -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);
+ }
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/CordovaDialogsHelper.java b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaDialogsHelper.java
new file mode 100644
index 0000000..a219c99
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaDialogsHelper.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/CordovaHttpAuthHandler.java b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaHttpAuthHandler.java
new file mode 100644
index 0000000..724381e
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaHttpAuthHandler.java
@@ -0,0 +1,51 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+package org.apache.cordova;
+
+import android.webkit.HttpAuthHandler;
+
+/**
+ * Specifies interface for HTTP auth handler object which is used to handle auth requests and
+ * specifying user credentials.
+ */
+public class CordovaHttpAuthHandler implements ICordovaHttpAuthHandler {
+
+ private final HttpAuthHandler handler;
+
+ public CordovaHttpAuthHandler(HttpAuthHandler handler) {
+ this.handler = handler;
+ }
+
+ /**
+ * Instructs the WebView to cancel the authentication request.
+ */
+ public void cancel () {
+ this.handler.cancel();
+ }
+
+ /**
+ * Instructs the WebView to proceed with the authentication with the given credentials.
+ *
+ * @param username
+ * @param password
+ */
+ public void proceed (String username, String password) {
+ this.handler.proceed(username, password);
+ }
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/CordovaInterface.java b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaInterface.java
new file mode 100755
index 0000000..ff90683
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaInterface.java
@@ -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);
+
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/CordovaInterfaceImpl.java b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaInterfaceImpl.java
new file mode 100644
index 0000000..9a6e924
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaInterfaceImpl.java
@@ -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 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;
+ }
+ }
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/CordovaPlugin.java b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaPlugin.java
new file mode 100644
index 0000000..41af1db
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaPlugin.java
@@ -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 and
+ * 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 {
+
+ }
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/CordovaPreferences.java b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaPreferences.java
new file mode 100644
index 0000000..4dbc93e
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaPreferences.java
@@ -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 prefs = new HashMap(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 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;
+ }
+
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/CordovaResourceApi.java b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaResourceApi.java
new file mode 100644
index 0000000..e725e25
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaResourceApi.java
@@ -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;
+ }
+ }
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebView.java b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebView.java
new file mode 100644
index 0000000..e60db42
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebView.java
@@ -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 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):
+ *
+ * 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 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);
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebViewEngine.java b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebViewEngine.java
new file mode 100644
index 0000000..c8e5a55
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebViewEngine.java
@@ -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 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);
+ }
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebViewImpl.java b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebViewImpl.java
new file mode 100644
index 0000000..fb99c34
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebViewImpl.java
@@ -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 boundKeyCodes = new HashSet();
+
+ 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(), new CordovaPreferences());
+ }
+
+ @SuppressLint("Assert")
+ @Override
+ public void init(CordovaInterface cordova, List 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 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 whitelist. URL=" + url);
+ }
+ }
+ if (!pluginManager.shouldOpenExternalUrl(url)) {
+ LOG.w(TAG, "showWebPage: Refusing to send intent for URL since it is not in the 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;
+ }
+ }
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/CoreAndroid.java b/platforms/android/CordovaLib/src/org/apache/cordova/CoreAndroid.java
new file mode 100755
index 0000000..e384f8d
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/CoreAndroid.java
@@ -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 params = new HashMap();
+ 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;
+ }
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/ExposedJsApi.java b/platforms/android/CordovaLib/src/org/apache/cordova/ExposedJsApi.java
new file mode 100644
index 0000000..acc65c6
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/ExposedJsApi.java
@@ -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;
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaClientCertRequest.java b/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaClientCertRequest.java
new file mode 100644
index 0000000..455d2f9
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaClientCertRequest.java
@@ -0,0 +1,66 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+package org.apache.cordova;
+
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+
+/**
+ * Specifies interface for handling certificate requests.
+ */
+public interface ICordovaClientCertRequest {
+ /**
+ * Cancel this request
+ */
+ public void cancel();
+
+ /*
+ * Returns the host name of the server requesting the certificate.
+ */
+ public String getHost();
+
+ /*
+ * Returns the acceptable types of asymmetric keys (can be null).
+ */
+ public String[] getKeyTypes();
+
+ /*
+ * Returns the port number of the server requesting the certificate.
+ */
+ public int getPort();
+
+ /*
+ * Returns the acceptable certificate issuers for the certificate matching the private key (can be null).
+ */
+ public Principal[] getPrincipals();
+
+ /*
+ * Ignore the request for now. Do not remember user's choice.
+ */
+ public void ignore();
+
+ /*
+ * Proceed with the specified private key and client certificate chain. Remember the user's positive choice and use it for future requests.
+ *
+ * @param privateKey The privateKey
+ * @param chain The certificate chain
+ */
+ public void proceed(PrivateKey privateKey, X509Certificate[] chain);
+}
\ No newline at end of file
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaCookieManager.java b/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaCookieManager.java
new file mode 100644
index 0000000..e776194
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaCookieManager.java
@@ -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();
+};
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaHttpAuthHandler.java b/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaHttpAuthHandler.java
new file mode 100644
index 0000000..c55818a
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaHttpAuthHandler.java
@@ -0,0 +1,38 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+package org.apache.cordova;
+
+/**
+ * Specifies interface for HTTP auth handler object which is used to handle auth requests and
+ * specifying user credentials.
+ */
+ public interface ICordovaHttpAuthHandler {
+ /**
+ * Instructs the WebView to cancel the authentication request.
+ */
+ public void cancel ();
+
+ /**
+ * Instructs the WebView to proceed with the authentication with the given credentials.
+ *
+ * @param username The user name
+ * @param password The password
+ */
+ public void proceed (String username, String password);
+}
\ No newline at end of file
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/LOG.java b/platforms/android/CordovaLib/src/org/apache/cordova/LOG.java
new file mode 100755
index 0000000..9fe7a7d
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/LOG.java
@@ -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));
+ }
+
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/NativeToJsMessageQueue.java b/platforms/android/CordovaLib/src/org/apache/cordova/NativeToJsMessageQueue.java
new file mode 100755
index 0000000..d17b1c4
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/NativeToJsMessageQueue.java
@@ -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 queue = new LinkedList();
+
+ /**
+ * The array of listeners that can be used to send messages to JS.
+ */
+ private ArrayList bridgeModes = new ArrayList();
+
+ /**
+ * 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 pluginMap = new LinkedHashMap();
+ private final LinkedHashMap entryMap = new LinkedHashMap();
+
+ private final CordovaInterface ctx;
+ private final CordovaWebView app;
+ private boolean isInitialized;
+
+ private CordovaPlugin permissionRequester;
+
+ public PluginManager(CordovaWebView cordovaWebView, CordovaInterface cordova, Collection pluginEntries) {
+ this.ctx = cordova;
+ this.app = cordovaWebView;
+ setPluginEntries(pluginEntries);
+ }
+
+ public Collection getPluginEntries() {
+ return entryMap.values();
+ }
+
+ public void setPluginEntries(Collection 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;
+ }
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/PluginResult.java b/platforms/android/CordovaLib/src/org/apache/cordova/PluginResult.java
new file mode 100644
index 0000000..2b3ac72
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/PluginResult.java
@@ -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 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 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
+ }
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/ResumeCallback.java b/platforms/android/CordovaLib/src/org/apache/cordova/ResumeCallback.java
new file mode 100644
index 0000000..49a43b5
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/ResumeCallback.java
@@ -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 result = new ArrayList();
+ result.add(eventResult);
+ result.add(pluginResult);
+
+ CoreAndroid appPlugin = (CoreAndroid) pluginManager.getPlugin(CoreAndroid.PLUGIN_NAME);
+ appPlugin.sendResumeEvent(new PluginResult(PluginResult.Status.OK, result));
+ }
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/Whitelist.java b/platforms/android/CordovaLib/src/org/apache/cordova/Whitelist.java
new file mode 100644
index 0000000..d0f823c
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/Whitelist.java
@@ -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 whiteList;
+
+ public static final String TAG = "Whitelist";
+
+ public Whitelist() {
+ this.whiteList = new ArrayList();
+ }
+
+ /* Match patterns (from http://developer.chrome.com/extensions/match_patterns.html)
+ *
+ * := ://
+ * := '*' | 'http' | 'https' | 'file' | 'ftp' | 'chrome-extension'
+ * := '*' | '*.' +
+ * := '/'
+ *
+ * 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 pit = whiteList.iterator();
+ while (pit.hasNext()) {
+ URLPattern p = pit.next();
+ if (p.matches(parsedUri)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemCookieManager.java b/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemCookieManager.java
new file mode 100644
index 0000000..acf795f
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemCookieManager.java
@@ -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();
+ }
+ }
+};
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemExposedJsApi.java b/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemExposedJsApi.java
new file mode 100755
index 0000000..94c3d93
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemExposedJsApi.java
@@ -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);
+ }
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebChromeClient.java b/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebChromeClient.java
new file mode 100755
index 0000000..a27fc6a
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebChromeClient.java
@@ -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 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;
+ }
+
+ // support:
+ // openFileChooser() is for pre KitKat and in KitKat mr1 (it's known broken in KitKat).
+ // For Lollipop, we use onShowFileChooser().
+ public void openFileChooser(ValueCallback uploadMsg) {
+ this.openFileChooser(uploadMsg, "*/*");
+ }
+
+ public void openFileChooser( ValueCallback uploadMsg, String acceptType ) {
+ this.openFileChooser(uploadMsg, acceptType, null);
+ }
+
+ public void openFileChooser(final ValueCallback 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 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();
+ }
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebView.java b/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebView.java
new file mode 100644
index 0000000..01c2f00
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebView.java
@@ -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);
+ }
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebViewClient.java b/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebViewClient.java
new file mode 100755
index 0000000..0b6a577
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebViewClient.java
@@ -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 authenticationTokens = new Hashtable();
+
+ 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;
+ }
+}
diff --git a/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebViewEngine.java b/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebViewEngine.java
new file mode 100755
index 0000000..1cbd7f8
--- /dev/null
+++ b/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebViewEngine.java
@@ -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 callback) {
+ webView.evaluateJavascript(js, callback);
+ }
+}
diff --git a/platforms/android/android.json b/platforms/android/android.json
new file mode 100644
index 0000000..cf38d64
--- /dev/null
+++ b/platforms/android/android.json
@@ -0,0 +1,439 @@
+{
+ "prepare_queue": {
+ "installed": [],
+ "uninstalled": []
+ },
+ "config_munge": {
+ "files": {
+ "res/xml/config.xml": {
+ "parents": {
+ "/widget": [
+ {
+ "xml": " ",
+ "count": 1
+ }
+ ],
+ "/*": [
+ {
+ "xml": " ",
+ "count": 1
+ },
+ {
+ "xml": " ",
+ "count": 1
+ },
+ {
+ "xml": " ",
+ "count": 1
+ },
+ {
+ "xml": " ",
+ "count": 1
+ },
+ {
+ "xml": " ",
+ "count": 1
+ },
+ {
+ "xml": " ",
+ "count": 1
+ },
+ {
+ "xml": " ",
+ "count": 1
+ },
+ {
+ "xml": " ",
+ "count": 1
+ },
+ {
+ "xml": " ",
+ "count": 1
+ },
+ {
+ "xml": " ",
+ "count": 1
+ },
+ {
+ "xml": " ",
+ "count": 1
+ }
+ ]
+ }
+ },
+ "config.xml": {
+ "parents": {
+ "/*": [
+ {
+ "xml": " ",
+ "count": 1
+ }
+ ]
+ }
+ },
+ "AndroidManifest.xml": {
+ "parents": {
+ "/*": [
+ {
+ "xml": " ",
+ "count": 3
+ },
+ {
+ "xml": " ",
+ "count": 1
+ }
+ ],
+ "/manifest/application/activity": [
+ {
+ "xml": " ",
+ "count": 1
+ }
+ ],
+ "/manifest": [
+ {
+ "xml": " ",
+ "count": 1
+ },
+ {
+ "xml": " ",
+ "count": 1
+ }
+ ],
+ "/manifest/application": [
+ {
+ "xml": " ",
+ "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"
+ }
+}
\ No newline at end of file
diff --git a/platforms/android/app/build.gradle b/platforms/android/app/build.gradle
new file mode 100644
index 0000000..0751d81
--- /dev/null
+++ b/platforms/android/app/build.gradle
@@ -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()
+}
diff --git a/platforms/android/app/cordova-android-support-gradle-release/properties.gradle b/platforms/android/app/cordova-android-support-gradle-release/properties.gradle
new file mode 100644
index 0000000..5d42a85
--- /dev/null
+++ b/platforms/android/app/cordova-android-support-gradle-release/properties.gradle
@@ -0,0 +1 @@
+ext {ANDROID_SUPPORT_VERSION = "27.+"}
\ No newline at end of file
diff --git a/platforms/android/app/src/main/AndroidManifest.xml b/platforms/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..b698ebf
--- /dev/null
+++ b/platforms/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platforms/android/app/src/main/java/com/darryncampbell/plugin/intent/IntentShim.java b/platforms/android/app/src/main/java/com/darryncampbell/plugin/intent/IntentShim.java
new file mode 100644
index 0000000..b67978e
--- /dev/null
+++ b/platforms/android/app/src/main/java/com/darryncampbell/plugin/intent/IntentShim.java
@@ -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 extrasMap = new HashMap();
+ 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;
+ }
+}
diff --git a/platforms/android/app/src/main/java/com/ferdinandsilva/android/ShareContentPlugin.java b/platforms/android/app/src/main/java/com/ferdinandsilva/android/ShareContentPlugin.java
new file mode 100644
index 0000000..b8320cc
--- /dev/null
+++ b/platforms/android/app/src/main/java/com/ferdinandsilva/android/ShareContentPlugin.java
@@ -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;
+ }
+
+}
\ No newline at end of file
diff --git a/platforms/android/app/src/main/java/com/hiddentao/cordova/filepath/FilePath.java b/platforms/android/app/src/main/java/com/hiddentao/cordova/filepath/FilePath.java
new file mode 100644
index 0000000..52bf946
--- /dev/null
+++ b/platforms/android/app/src/main/java/com/hiddentao/cordova/filepath/FilePath.java
@@ -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 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.
+ *
+ * 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();
+ }
+}
diff --git a/platforms/android/app/src/main/java/com/megster/cordova/FileChooser.java b/platforms/android/app/src/main/java/com/megster/cordova/FileChooser.java
new file mode 100644
index 0000000..087b540
--- /dev/null
+++ b/platforms/android/app/src/main/java/com/megster/cordova/FileChooser.java
@@ -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);
+ }
+ }
+ }
+}
diff --git a/platforms/android/app/src/main/java/com/monkeystew/goober_m/MainActivity.java b/platforms/android/app/src/main/java/com/monkeystew/goober_m/MainActivity.java
new file mode 100644
index 0000000..d1d6b7c
--- /dev/null
+++ b/platforms/android/app/src/main/java/com/monkeystew/goober_m/MainActivity.java
@@ -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 in config.xml
+ loadUrl(launchUrl);
+ }
+}
diff --git a/platforms/android/app/src/main/java/io/ionic/keyboard/IonicKeyboard.java b/platforms/android/app/src/main/java/io/ionic/keyboard/IonicKeyboard.java
new file mode 100644
index 0000000..128063b
--- /dev/null
+++ b/platforms/android/app/src/main/java/io/ionic/keyboard/IonicKeyboard.java
@@ -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.
+ }
+
+
+}
+
+
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/device/Device.java b/platforms/android/app/src/main/java/org/apache/cordova/device/Device.java
new file mode 100644
index 0000000..e9efcb4
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/device/Device.java
@@ -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");
+ }
+
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/file/AssetFilesystem.java b/platforms/android/app/src/main/java/org/apache/cordova/file/AssetFilesystem.java
new file mode 100644
index 0000000..b035c40
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/file/AssetFilesystem.java
@@ -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 listCache;
+ private static Map 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) ois.readObject();
+ lengthCache = (Map) 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();
+ }
+ }
+ }
+ }
+
+ 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");
+ }
+
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/file/ContentFilesystem.java b/platforms/android/app/src/main/java/org/apache/cordova/file/ContentFilesystem.java
new file mode 100644
index 0000000..6b983c0
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/file/ContentFilesystem.java
@@ -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;
+ }
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/file/DirectoryManager.java b/platforms/android/app/src/main/java/org/apache/cordova/file/DirectoryManager.java
new file mode 100644
index 0000000..07af5ea
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/file/DirectoryManager.java
@@ -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;
+ }
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/file/EncodingException.java b/platforms/android/app/src/main/java/org/apache/cordova/file/EncodingException.java
new file mode 100644
index 0000000..e9e1653
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/file/EncodingException.java
@@ -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);
+ }
+
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/file/FileExistsException.java b/platforms/android/app/src/main/java/org/apache/cordova/file/FileExistsException.java
new file mode 100644
index 0000000..5c4d83d
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/file/FileExistsException.java
@@ -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);
+ }
+
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/file/FileUtils.java b/platforms/android/app/src/main/java/org/apache/cordova/file/FileUtils.java
new file mode 100644
index 0000000..1d6e61f
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/file/FileUtils.java
@@ -0,0 +1,1225 @@
+/*
+ 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.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.util.Base64;
+
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.LOG;
+import org.apache.cordova.PermissionHelper;
+import org.apache.cordova.PluginResult;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.security.Permission;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+
+/**
+ * This class provides file and directory services to JavaScript.
+ */
+public class FileUtils extends CordovaPlugin {
+ private static final String LOG_TAG = "FileUtils";
+
+ public static int NOT_FOUND_ERR = 1;
+ public static int SECURITY_ERR = 2;
+ public static int ABORT_ERR = 3;
+
+ public static int NOT_READABLE_ERR = 4;
+ public static int ENCODING_ERR = 5;
+ public static int NO_MODIFICATION_ALLOWED_ERR = 6;
+ public static int INVALID_STATE_ERR = 7;
+ public static int SYNTAX_ERR = 8;
+ public static int INVALID_MODIFICATION_ERR = 9;
+ public static int QUOTA_EXCEEDED_ERR = 10;
+ public static int TYPE_MISMATCH_ERR = 11;
+ public static int PATH_EXISTS_ERR = 12;
+
+ /*
+ * Permission callback codes
+ */
+
+ public static final int ACTION_GET_FILE = 0;
+ public static final int ACTION_WRITE = 1;
+ public static final int ACTION_GET_DIRECTORY = 2;
+
+ public static final int WRITE = 3;
+ public static final int READ = 4;
+
+ public static int UNKNOWN_ERR = 1000;
+
+ private boolean configured = false;
+
+ private PendingRequests pendingRequests;
+
+
+
+ /*
+ * We need both read and write when accessing the storage, I think.
+ */
+
+ private String [] permissions = {
+ Manifest.permission.READ_EXTERNAL_STORAGE,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE };
+
+ // This field exists only to support getEntry, below, which has been deprecated
+ private static FileUtils filePlugin;
+
+ private interface FileOp {
+ void run(JSONArray args) throws Exception;
+ }
+
+ private ArrayList filesystems;
+
+ public void registerFilesystem(Filesystem fs) {
+ if (fs != null && filesystemForName(fs.name)== null) {
+ this.filesystems.add(fs);
+ }
+ }
+
+ private Filesystem filesystemForName(String name) {
+ for (Filesystem fs:filesystems) {
+ if (fs != null && fs.name != null && fs.name.equals(name)) {
+ return fs;
+ }
+ }
+ return null;
+ }
+
+ protected String[] getExtraFileSystemsPreference(Activity activity) {
+ String fileSystemsStr = preferences.getString("androidextrafilesystems", "files,files-external,documents,sdcard,cache,cache-external,assets,root");
+ return fileSystemsStr.split(",");
+ }
+
+ protected void registerExtraFileSystems(String[] filesystems, HashMap availableFileSystems) {
+ HashSet installedFileSystems = new HashSet();
+
+ /* Register filesystems in order */
+ for (String fsName : filesystems) {
+ if (!installedFileSystems.contains(fsName)) {
+ String fsRoot = availableFileSystems.get(fsName);
+ if (fsRoot != null) {
+ File newRoot = new File(fsRoot);
+ if (newRoot.mkdirs() || newRoot.isDirectory()) {
+ registerFilesystem(new LocalFilesystem(fsName, webView.getContext(), webView.getResourceApi(), newRoot));
+ installedFileSystems.add(fsName);
+ } else {
+ LOG.d(LOG_TAG, "Unable to create root dir for filesystem \"" + fsName + "\", skipping");
+ }
+ } else {
+ LOG.d(LOG_TAG, "Unrecognized extra filesystem identifier: " + fsName);
+ }
+ }
+ }
+ }
+
+ protected HashMap getAvailableFileSystems(Activity activity) {
+ Context context = activity.getApplicationContext();
+ HashMap availableFileSystems = new HashMap();
+
+ availableFileSystems.put("files", context.getFilesDir().getAbsolutePath());
+ availableFileSystems.put("documents", new File(context.getFilesDir(), "Documents").getAbsolutePath());
+ availableFileSystems.put("cache", context.getCacheDir().getAbsolutePath());
+ availableFileSystems.put("root", "/");
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ try {
+ availableFileSystems.put("files-external", context.getExternalFilesDir(null).getAbsolutePath());
+ availableFileSystems.put("sdcard", Environment.getExternalStorageDirectory().getAbsolutePath());
+ availableFileSystems.put("cache-external", context.getExternalCacheDir().getAbsolutePath());
+ }
+ catch(NullPointerException e) {
+ LOG.d(LOG_TAG, "External storage unavailable, check to see if USB Mass Storage Mode is on");
+ }
+ }
+
+ return availableFileSystems;
+ }
+
+ @Override
+ public void initialize(CordovaInterface cordova, CordovaWebView webView) {
+ super.initialize(cordova, webView);
+ this.filesystems = new ArrayList();
+ this.pendingRequests = new PendingRequests();
+
+ String tempRoot = null;
+ String persistentRoot = null;
+
+ Activity activity = cordova.getActivity();
+ String packageName = activity.getPackageName();
+
+ String location = preferences.getString("androidpersistentfilelocation", "internal");
+
+ tempRoot = activity.getCacheDir().getAbsolutePath();
+ if ("internal".equalsIgnoreCase(location)) {
+ persistentRoot = activity.getFilesDir().getAbsolutePath() + "/files/";
+ this.configured = true;
+ } else if ("compatibility".equalsIgnoreCase(location)) {
+ /*
+ * Fall-back to compatibility mode -- this is the logic implemented in
+ * earlier versions of this plugin, and should be maintained here so
+ * that apps which were originally deployed with older versions of the
+ * plugin can continue to provide access to files stored under those
+ * versions.
+ */
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ persistentRoot = Environment.getExternalStorageDirectory().getAbsolutePath();
+ tempRoot = Environment.getExternalStorageDirectory().getAbsolutePath() +
+ "/Android/data/" + packageName + "/cache/";
+ } else {
+ persistentRoot = "/data/data/" + packageName;
+ }
+ this.configured = true;
+ }
+
+ if (this.configured) {
+ // Create the directories if they don't exist.
+ File tmpRootFile = new File(tempRoot);
+ File persistentRootFile = new File(persistentRoot);
+ tmpRootFile.mkdirs();
+ persistentRootFile.mkdirs();
+
+ // Register initial filesystems
+ // Note: The temporary and persistent filesystems need to be the first two
+ // registered, so that they will match window.TEMPORARY and window.PERSISTENT,
+ // per spec.
+ this.registerFilesystem(new LocalFilesystem("temporary", webView.getContext(), webView.getResourceApi(), tmpRootFile));
+ this.registerFilesystem(new LocalFilesystem("persistent", webView.getContext(), webView.getResourceApi(), persistentRootFile));
+ this.registerFilesystem(new ContentFilesystem(webView.getContext(), webView.getResourceApi()));
+ this.registerFilesystem(new AssetFilesystem(webView.getContext().getAssets(), webView.getResourceApi()));
+
+ registerExtraFileSystems(getExtraFileSystemsPreference(activity), getAvailableFileSystems(activity));
+
+ // Initialize static plugin reference for deprecated getEntry method
+ if (filePlugin == null) {
+ FileUtils.filePlugin = this;
+ }
+ } else {
+ LOG.e(LOG_TAG, "File plugin configuration error: Please set AndroidPersistentFileLocation in config.xml to one of \"internal\" (for new applications) or \"compatibility\" (for compatibility with previous versions)");
+ activity.finish();
+ }
+ }
+
+ public static FileUtils getFilePlugin() {
+ return filePlugin;
+ }
+
+ private Filesystem filesystemForURL(LocalFilesystemURL localURL) {
+ if (localURL == null) return null;
+ return filesystemForName(localURL.fsName);
+ }
+
+ @Override
+ public Uri remapUri(Uri uri) {
+ // Remap only cdvfile: URLs (not content:).
+ if (!LocalFilesystemURL.FILESYSTEM_PROTOCOL.equals(uri.getScheme())) {
+ return null;
+ }
+ try {
+ LocalFilesystemURL inputURL = LocalFilesystemURL.parse(uri);
+ Filesystem fs = this.filesystemForURL(inputURL);
+ if (fs == null) {
+ return null;
+ }
+ String path = fs.filesystemPathForURL(inputURL);
+ if (path != null) {
+ return Uri.parse("file://" + fs.filesystemPathForURL(inputURL));
+ }
+ return null;
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ public boolean execute(String action, final String rawArgs, final CallbackContext callbackContext) {
+ if (!configured) {
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, "File plugin is not configured. Please see the README.md file for details on how to update config.xml"));
+ return true;
+ }
+ if (action.equals("testSaveLocationExists")) {
+ threadhelper(new FileOp() {
+ public void run(JSONArray args) {
+ boolean b = DirectoryManager.testSaveLocationExists();
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b));
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("getFreeDiskSpace")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) {
+ // The getFreeDiskSpace plugin API is not documented, but some apps call it anyway via exec().
+ // For compatibility it always returns free space in the primary external storage, and
+ // does NOT fallback to internal store if external storage is unavailable.
+ long l = DirectoryManager.getFreeExternalStorageSpace();
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, l));
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("testFileExists")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws JSONException {
+ String fname=args.getString(0);
+ boolean b = DirectoryManager.testFileExists(fname);
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b));
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("testDirectoryExists")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws JSONException {
+ String fname=args.getString(0);
+ boolean b = DirectoryManager.testFileExists(fname);
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b));
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("readAsText")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws JSONException, MalformedURLException {
+ String encoding = args.getString(1);
+ int start = args.getInt(2);
+ int end = args.getInt(3);
+ String fname=args.getString(0);
+ readFileAs(fname, start, end, callbackContext, encoding, PluginResult.MESSAGE_TYPE_STRING);
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("readAsDataURL")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws JSONException, MalformedURLException {
+ int start = args.getInt(1);
+ int end = args.getInt(2);
+ String fname=args.getString(0);
+ readFileAs(fname, start, end, callbackContext, null, -1);
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("readAsArrayBuffer")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws JSONException, MalformedURLException {
+ int start = args.getInt(1);
+ int end = args.getInt(2);
+ String fname=args.getString(0);
+ readFileAs(fname, start, end, callbackContext, null, PluginResult.MESSAGE_TYPE_ARRAYBUFFER);
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("readAsBinaryString")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws JSONException, MalformedURLException {
+ int start = args.getInt(1);
+ int end = args.getInt(2);
+ String fname=args.getString(0);
+ readFileAs(fname, start, end, callbackContext, null, PluginResult.MESSAGE_TYPE_BINARYSTRING);
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("write")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws JSONException, FileNotFoundException, IOException, NoModificationAllowedException {
+ String fname=args.getString(0);
+ String nativeURL = resolveLocalFileSystemURI(fname).getString("nativeURL");
+ String data=args.getString(1);
+ int offset=args.getInt(2);
+ Boolean isBinary=args.getBoolean(3);
+
+ if(needPermission(nativeURL, WRITE)) {
+ getWritePermission(rawArgs, ACTION_WRITE, callbackContext);
+ }
+ else {
+ long fileSize = write(fname, data, offset, isBinary);
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, fileSize));
+ }
+
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("truncate")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws JSONException, FileNotFoundException, IOException, NoModificationAllowedException {
+ String fname=args.getString(0);
+ int offset=args.getInt(1);
+ long fileSize = truncateFile(fname, offset);
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, fileSize));
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("requestAllFileSystems")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws IOException, JSONException {
+ callbackContext.success(requestAllFileSystems());
+ }
+ }, rawArgs, callbackContext);
+ } else if (action.equals("requestAllPaths")) {
+ cordova.getThreadPool().execute(
+ new Runnable() {
+ public void run() {
+ try {
+ callbackContext.success(requestAllPaths());
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ }
+ );
+ } else if (action.equals("requestFileSystem")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws JSONException {
+ int fstype = args.getInt(0);
+ long requiredSize = args.optLong(1);
+ requestFileSystem(fstype, requiredSize, callbackContext);
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("resolveLocalFileSystemURI")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws IOException, JSONException {
+ String fname=args.getString(0);
+ JSONObject obj = resolveLocalFileSystemURI(fname);
+ callbackContext.success(obj);
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("getFileMetadata")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws FileNotFoundException, JSONException, MalformedURLException {
+ String fname=args.getString(0);
+ JSONObject obj = getFileMetadata(fname);
+ callbackContext.success(obj);
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("getParent")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws JSONException, IOException {
+ String fname=args.getString(0);
+ JSONObject obj = getParent(fname);
+ callbackContext.success(obj);
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("getDirectory")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
+ String dirname = args.getString(0);
+ String path = args.getString(1);
+ String nativeURL = resolveLocalFileSystemURI(dirname).getString("nativeURL");
+ boolean containsCreate = (args.isNull(2)) ? false : args.getJSONObject(2).optBoolean("create", false);
+
+ if(containsCreate && needPermission(nativeURL, WRITE)) {
+ getWritePermission(rawArgs, ACTION_GET_DIRECTORY, callbackContext);
+ }
+ else if(!containsCreate && needPermission(nativeURL, READ)) {
+ getReadPermission(rawArgs, ACTION_GET_DIRECTORY, callbackContext);
+ }
+ else {
+ JSONObject obj = getFile(dirname, path, args.optJSONObject(2), true);
+ callbackContext.success(obj);
+ }
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("getFile")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
+ String dirname = args.getString(0);
+ String path = args.getString(1);
+ String nativeURL = resolveLocalFileSystemURI(dirname).getString("nativeURL");
+ boolean containsCreate = (args.isNull(2)) ? false : args.getJSONObject(2).optBoolean("create", false);
+
+ if(containsCreate && needPermission(nativeURL, WRITE)) {
+ getWritePermission(rawArgs, ACTION_GET_FILE, callbackContext);
+ }
+ else if(!containsCreate && needPermission(nativeURL, READ)) {
+ getReadPermission(rawArgs, ACTION_GET_FILE, callbackContext);
+ }
+ else {
+ JSONObject obj = getFile(dirname, path, args.optJSONObject(2), false);
+ callbackContext.success(obj);
+ }
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("remove")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws JSONException, NoModificationAllowedException, InvalidModificationException, MalformedURLException {
+ String fname=args.getString(0);
+ boolean success = remove(fname);
+ if (success) {
+ callbackContext.success();
+ } else {
+ callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR);
+ }
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("removeRecursively")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws JSONException, FileExistsException, MalformedURLException, NoModificationAllowedException {
+ String fname=args.getString(0);
+ boolean success = removeRecursively(fname);
+ if (success) {
+ callbackContext.success();
+ } else {
+ callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR);
+ }
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("moveTo")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws JSONException, NoModificationAllowedException, IOException, InvalidModificationException, EncodingException, FileExistsException {
+ String fname=args.getString(0);
+ String newParent=args.getString(1);
+ String newName=args.getString(2);
+ JSONObject entry = transferTo(fname, newParent, newName, true);
+ callbackContext.success(entry);
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("copyTo")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws JSONException, NoModificationAllowedException, IOException, InvalidModificationException, EncodingException, FileExistsException {
+ String fname=args.getString(0);
+ String newParent=args.getString(1);
+ String newName=args.getString(2);
+ JSONObject entry = transferTo(fname, newParent, newName, false);
+ callbackContext.success(entry);
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("readEntries")) {
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws FileNotFoundException, JSONException, MalformedURLException {
+ String fname=args.getString(0);
+ JSONArray entries = readEntries(fname);
+ callbackContext.success(entries);
+ }
+ }, rawArgs, callbackContext);
+ }
+ else if (action.equals("_getLocalFilesystemPath")) {
+ // Internal method for testing: Get the on-disk location of a local filesystem url.
+ // [Currently used for testing file-transfer]
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws FileNotFoundException, JSONException, MalformedURLException {
+ String localURLstr = args.getString(0);
+ String fname = filesystemPathForURL(localURLstr);
+ callbackContext.success(fname);
+ }
+ }, rawArgs, callbackContext);
+ }
+ else {
+ return false;
+ }
+ return true;
+ }
+
+ private void getReadPermission(String rawArgs, int action, CallbackContext callbackContext) {
+ int requestCode = pendingRequests.createRequest(rawArgs, action, callbackContext);
+ PermissionHelper.requestPermission(this, requestCode, Manifest.permission.READ_EXTERNAL_STORAGE);
+ }
+
+ private void getWritePermission(String rawArgs, int action, CallbackContext callbackContext) {
+ int requestCode = pendingRequests.createRequest(rawArgs, action, callbackContext);
+ PermissionHelper.requestPermission(this, requestCode, Manifest.permission.WRITE_EXTERNAL_STORAGE);
+ }
+
+ private boolean hasReadPermission() {
+ return PermissionHelper.hasPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
+ }
+
+ private boolean hasWritePermission() {
+ return PermissionHelper.hasPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
+ }
+
+ private boolean needPermission(String nativeURL, int permissionType) throws JSONException {
+ JSONObject j = requestAllPaths();
+ ArrayList allowedStorageDirectories = new ArrayList();
+ allowedStorageDirectories.add(j.getString("applicationDirectory"));
+ allowedStorageDirectories.add(j.getString("applicationStorageDirectory"));
+ if(j.has("externalApplicationStorageDirectory")) {
+ allowedStorageDirectories.add(j.getString("externalApplicationStorageDirectory"));
+ }
+
+ if(permissionType == READ && hasReadPermission()) {
+ return false;
+ }
+ else if(permissionType == WRITE && hasWritePermission()) {
+ return false;
+ }
+
+ // Permission required if the native url lies outside the allowed storage directories
+ for(String directory : allowedStorageDirectories) {
+ if(nativeURL.startsWith(directory)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+
+ public LocalFilesystemURL resolveNativeUri(Uri nativeUri) {
+ LocalFilesystemURL localURL = null;
+
+ // Try all installed filesystems. Return the best matching URL
+ // (determined by the shortest resulting URL)
+ for (Filesystem fs : filesystems) {
+ LocalFilesystemURL url = fs.toLocalUri(nativeUri);
+ if (url != null) {
+ // A shorter fullPath implies that the filesystem is a better
+ // match for the local path than the previous best.
+ if (localURL == null || (url.uri.toString().length() < localURL.toString().length())) {
+ localURL = url;
+ }
+ }
+ }
+ return localURL;
+ }
+
+ /*
+ * These two native-only methods can be used by other plugins to translate between
+ * device file system paths and URLs. By design, there is no direct JavaScript
+ * interface to these methods.
+ */
+
+ public String filesystemPathForURL(String localURLstr) throws MalformedURLException {
+ try {
+ LocalFilesystemURL inputURL = LocalFilesystemURL.parse(localURLstr);
+ Filesystem fs = this.filesystemForURL(inputURL);
+ if (fs == null) {
+ throw new MalformedURLException("No installed handlers for this URL");
+ }
+ return fs.filesystemPathForURL(inputURL);
+ } catch (IllegalArgumentException e) {
+ MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+ mue.initCause(e);
+ throw mue;
+ }
+ }
+
+ public LocalFilesystemURL filesystemURLforLocalPath(String localPath) {
+ LocalFilesystemURL localURL = null;
+ int shortestFullPath = 0;
+
+ // Try all installed filesystems. Return the best matching URL
+ // (determined by the shortest resulting URL)
+ for (Filesystem fs: filesystems) {
+ LocalFilesystemURL url = fs.URLforFilesystemPath(localPath);
+ if (url != null) {
+ // A shorter fullPath implies that the filesystem is a better
+ // match for the local path than the previous best.
+ if (localURL == null || (url.path.length() < shortestFullPath)) {
+ localURL = url;
+ shortestFullPath = url.path.length();
+ }
+ }
+ }
+ return localURL;
+ }
+
+
+ /* helper to execute functions async and handle the result codes
+ *
+ */
+ private void threadhelper(final FileOp f, final String rawArgs, final CallbackContext callbackContext){
+ cordova.getThreadPool().execute(new Runnable() {
+ public void run() {
+ try {
+ JSONArray args = new JSONArray(rawArgs);
+ f.run(args);
+ } catch ( Exception e) {
+ if( e instanceof EncodingException){
+ callbackContext.error(FileUtils.ENCODING_ERR);
+ } else if(e instanceof FileNotFoundException) {
+ callbackContext.error(FileUtils.NOT_FOUND_ERR);
+ } else if(e instanceof FileExistsException) {
+ callbackContext.error(FileUtils.PATH_EXISTS_ERR);
+ } else if(e instanceof NoModificationAllowedException ) {
+ callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR);
+ } else if(e instanceof InvalidModificationException ) {
+ callbackContext.error(FileUtils.INVALID_MODIFICATION_ERR);
+ } else if(e instanceof MalformedURLException ) {
+ callbackContext.error(FileUtils.ENCODING_ERR);
+ } else if(e instanceof IOException ) {
+ callbackContext.error(FileUtils.INVALID_MODIFICATION_ERR);
+ } else if(e instanceof EncodingException ) {
+ callbackContext.error(FileUtils.ENCODING_ERR);
+ } else if(e instanceof TypeMismatchException ) {
+ callbackContext.error(FileUtils.TYPE_MISMATCH_ERR);
+ } else if(e instanceof JSONException ) {
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION));
+ } else if (e instanceof SecurityException) {
+ callbackContext.error(FileUtils.SECURITY_ERR);
+ } else {
+ e.printStackTrace();
+ callbackContext.error(FileUtils.UNKNOWN_ERR);
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Allows the user to look up the Entry for a file or directory referred to by a local URI.
+ *
+ * @param uriString of the file/directory to look up
+ * @return a JSONObject representing a Entry from the filesystem
+ * @throws MalformedURLException if the url is not valid
+ * @throws FileNotFoundException if the file does not exist
+ * @throws IOException if the user can't read the file
+ * @throws JSONException
+ */
+ private JSONObject resolveLocalFileSystemURI(String uriString) throws IOException, JSONException {
+ if (uriString == null) {
+ throw new MalformedURLException("Unrecognized filesystem URL");
+ }
+ Uri uri = Uri.parse(uriString);
+ boolean isNativeUri = false;
+
+ LocalFilesystemURL inputURL = LocalFilesystemURL.parse(uri);
+ if (inputURL == null) {
+ /* Check for file://, content:// urls */
+ inputURL = resolveNativeUri(uri);
+ isNativeUri = true;
+ }
+
+ try {
+ Filesystem fs = this.filesystemForURL(inputURL);
+ if (fs == null) {
+ throw new MalformedURLException("No installed handlers for this URL");
+ }
+ if (fs.exists(inputURL)) {
+ if (!isNativeUri) {
+ // If not already resolved as native URI, resolve to a native URI and back to
+ // fix the terminating slash based on whether the entry is a directory or file.
+ inputURL = fs.toLocalUri(fs.toNativeUri(inputURL));
+ }
+
+ return fs.getEntryForLocalURL(inputURL);
+ }
+ } catch (IllegalArgumentException e) {
+ MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+ mue.initCause(e);
+ throw mue;
+ }
+ throw new FileNotFoundException();
+ }
+
+ /**
+ * Read the list of files from this directory.
+ *
+ * @return a JSONArray containing JSONObjects that represent Entry objects.
+ * @throws FileNotFoundException if the directory is not found.
+ * @throws JSONException
+ * @throws MalformedURLException
+ */
+ private JSONArray readEntries(String baseURLstr) throws FileNotFoundException, JSONException, MalformedURLException {
+ try {
+ LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);
+ Filesystem fs = this.filesystemForURL(inputURL);
+ if (fs == null) {
+ throw new MalformedURLException("No installed handlers for this URL");
+ }
+ return fs.readEntriesAtLocalURL(inputURL);
+
+ } catch (IllegalArgumentException e) {
+ MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+ mue.initCause(e);
+ throw mue;
+ }
+ }
+
+ /**
+ * A setup method that handles the move/copy of files/directories
+ *
+ * @param newName for the file directory to be called, if null use existing file name
+ * @param move if false do a copy, if true do a move
+ * @return a Entry object
+ * @throws NoModificationAllowedException
+ * @throws IOException
+ * @throws InvalidModificationException
+ * @throws EncodingException
+ * @throws JSONException
+ * @throws FileExistsException
+ */
+ private JSONObject transferTo(String srcURLstr, String destURLstr, String newName, boolean move) throws JSONException, NoModificationAllowedException, IOException, InvalidModificationException, EncodingException, FileExistsException {
+ if (srcURLstr == null || destURLstr == null) {
+ // either no source or no destination provided
+ throw new FileNotFoundException();
+ }
+
+ LocalFilesystemURL srcURL = LocalFilesystemURL.parse(srcURLstr);
+ LocalFilesystemURL destURL = LocalFilesystemURL.parse(destURLstr);
+
+ Filesystem srcFs = this.filesystemForURL(srcURL);
+ Filesystem destFs = this.filesystemForURL(destURL);
+
+ // Check for invalid file name
+ if (newName != null && newName.contains(":")) {
+ throw new EncodingException("Bad file name");
+ }
+
+ return destFs.copyFileToURL(destURL, newName, srcFs, srcURL, move);
+ }
+
+ /**
+ * Deletes a directory and all of its contents, if any. In the event of an error
+ * [e.g. trying to delete a directory that contains a file that cannot be removed],
+ * some of the contents of the directory may be deleted.
+ * It is an error to attempt to delete the root directory of a filesystem.
+ *
+ * @return a boolean representing success of failure
+ * @throws FileExistsException
+ * @throws NoModificationAllowedException
+ * @throws MalformedURLException
+ */
+ private boolean removeRecursively(String baseURLstr) throws FileExistsException, NoModificationAllowedException, MalformedURLException {
+ try {
+ LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);
+ // You can't delete the root directory.
+ if ("".equals(inputURL.path) || "/".equals(inputURL.path)) {
+ throw new NoModificationAllowedException("You can't delete the root directory");
+ }
+
+ Filesystem fs = this.filesystemForURL(inputURL);
+ if (fs == null) {
+ throw new MalformedURLException("No installed handlers for this URL");
+ }
+ return fs.recursiveRemoveFileAtLocalURL(inputURL);
+
+ } catch (IllegalArgumentException e) {
+ MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+ mue.initCause(e);
+ throw mue;
+ }
+ }
+
+
+ /**
+ * Deletes a file or directory. It is an error to attempt to delete a directory that is not empty.
+ * It is an error to attempt to delete the root directory of a filesystem.
+ *
+ * @return a boolean representing success of failure
+ * @throws NoModificationAllowedException
+ * @throws InvalidModificationException
+ * @throws MalformedURLException
+ */
+ private boolean remove(String baseURLstr) throws NoModificationAllowedException, InvalidModificationException, MalformedURLException {
+ try {
+ LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);
+ // You can't delete the root directory.
+ if ("".equals(inputURL.path) || "/".equals(inputURL.path)) {
+
+ throw new NoModificationAllowedException("You can't delete the root directory");
+ }
+
+ Filesystem fs = this.filesystemForURL(inputURL);
+ if (fs == null) {
+ throw new MalformedURLException("No installed handlers for this URL");
+ }
+ return fs.removeFileAtLocalURL(inputURL);
+
+ } catch (IllegalArgumentException e) {
+ MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+ mue.initCause(e);
+ throw mue;
+ }
+ }
+
+ /**
+ * Creates or looks up a file.
+ *
+ * @param baseURLstr base directory
+ * @param path file/directory to lookup or create
+ * @param options specify whether to create or not
+ * @param directory if true look up directory, if false look up file
+ * @return a Entry object
+ * @throws FileExistsException
+ * @throws IOException
+ * @throws TypeMismatchException
+ * @throws EncodingException
+ * @throws JSONException
+ */
+ private JSONObject getFile(String baseURLstr, String path, JSONObject options, boolean directory) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
+ try {
+ LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);
+ Filesystem fs = this.filesystemForURL(inputURL);
+ if (fs == null) {
+ throw new MalformedURLException("No installed handlers for this URL");
+ }
+ return fs.getFileForLocalURL(inputURL, path, options, directory);
+
+ } catch (IllegalArgumentException e) {
+ MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+ mue.initCause(e);
+ throw mue;
+ }
+
+ }
+
+ /**
+ * Look up the parent DirectoryEntry containing this Entry.
+ * If this Entry is the root of its filesystem, its parent is itself.
+ */
+ private JSONObject getParent(String baseURLstr) throws JSONException, IOException {
+ try {
+ LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);
+ Filesystem fs = this.filesystemForURL(inputURL);
+ if (fs == null) {
+ throw new MalformedURLException("No installed handlers for this URL");
+ }
+ return fs.getParentForLocalURL(inputURL);
+
+ } catch (IllegalArgumentException e) {
+ MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+ mue.initCause(e);
+ throw mue;
+ }
+ }
+
+ /**
+ * Returns a File that represents the current state of the file that this FileEntry represents.
+ *
+ * @return returns a JSONObject represent a W3C File object
+ */
+ private JSONObject getFileMetadata(String baseURLstr) throws FileNotFoundException, JSONException, MalformedURLException {
+ try {
+ LocalFilesystemURL inputURL = LocalFilesystemURL.parse(baseURLstr);
+ Filesystem fs = this.filesystemForURL(inputURL);
+ if (fs == null) {
+ throw new MalformedURLException("No installed handlers for this URL");
+ }
+ return fs.getFileMetadataForLocalURL(inputURL);
+
+ } catch (IllegalArgumentException e) {
+ MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+ mue.initCause(e);
+ throw mue;
+ }
+ }
+
+ /**
+ * Requests a filesystem in which to store application data.
+ *
+ * @param type of file system requested
+ * @param requiredSize required free space in the file system in bytes
+ * @param callbackContext context for returning the result or error
+ * @throws JSONException
+ */
+ private void requestFileSystem(int type, long requiredSize, final CallbackContext callbackContext) throws JSONException {
+ Filesystem rootFs = null;
+ try {
+ rootFs = this.filesystems.get(type);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // Pass null through
+ }
+ if (rootFs == null) {
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_FOUND_ERR));
+ } else {
+ // If a nonzero required size was specified, check that the retrieved filesystem has enough free space.
+ long availableSize = 0;
+ if (requiredSize > 0) {
+ availableSize = rootFs.getFreeSpaceInBytes();
+ }
+
+ if (availableSize < requiredSize) {
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, FileUtils.QUOTA_EXCEEDED_ERR));
+ } else {
+ JSONObject fs = new JSONObject();
+ fs.put("name", rootFs.name);
+ fs.put("root", rootFs.getRootEntry());
+ callbackContext.success(fs);
+ }
+ }
+ }
+
+ /**
+ * Requests a filesystem in which to store application data.
+ *
+ * @return a JSONObject representing the file system
+ */
+ private JSONArray requestAllFileSystems() throws IOException, JSONException {
+ JSONArray ret = new JSONArray();
+ for (Filesystem fs : filesystems) {
+ ret.put(fs.getRootEntry());
+ }
+ return ret;
+ }
+
+ private static String toDirUrl(File f) {
+ return Uri.fromFile(f).toString() + '/';
+ }
+
+ private JSONObject requestAllPaths() throws JSONException {
+ Context context = cordova.getActivity();
+ JSONObject ret = new JSONObject();
+ ret.put("applicationDirectory", "file:///android_asset/");
+ ret.put("applicationStorageDirectory", toDirUrl(context.getFilesDir().getParentFile()));
+ ret.put("dataDirectory", toDirUrl(context.getFilesDir()));
+ ret.put("cacheDirectory", toDirUrl(context.getCacheDir()));
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ try {
+ ret.put("externalApplicationStorageDirectory", toDirUrl(context.getExternalFilesDir(null).getParentFile()));
+ ret.put("externalDataDirectory", toDirUrl(context.getExternalFilesDir(null)));
+ ret.put("externalCacheDirectory", toDirUrl(context.getExternalCacheDir()));
+ ret.put("externalRootDirectory", toDirUrl(Environment.getExternalStorageDirectory()));
+ }
+ catch(NullPointerException e) {
+ /* If external storage is unavailable, context.getExternal* returns null */
+ LOG.d(LOG_TAG, "Unable to access these paths, most liklely due to USB storage");
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Returns a JSON object representing the given File. Internal APIs should be modified
+ * to use URLs instead of raw FS paths wherever possible, when interfacing with this plugin.
+ *
+ * @param file the File to convert
+ * @return a JSON representation of the given File
+ * @throws JSONException
+ */
+ public JSONObject getEntryForFile(File file) throws JSONException {
+ JSONObject entry;
+
+ for (Filesystem fs : filesystems) {
+ entry = fs.makeEntryForFile(file);
+ if (entry != null) {
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a JSON object representing the given File. Deprecated, as this is only used by
+ * FileTransfer, and because it is a static method that should really be an instance method,
+ * since it depends on the actual filesystem roots in use. Internal APIs should be modified
+ * to use URLs instead of raw FS paths wherever possible, when interfacing with this plugin.
+ *
+ * @param file the File to convert
+ * @return a JSON representation of the given File
+ * @throws JSONException
+ */
+ @Deprecated
+ public static JSONObject getEntry(File file) throws JSONException {
+ if (getFilePlugin() != null) {
+ return getFilePlugin().getEntryForFile(file);
+ }
+ return null;
+ }
+
+ /**
+ * Read the contents of a file.
+ * This is done in a background thread; the result is sent to the callback.
+ *
+ * @param start Start position in the file.
+ * @param end End position to stop at (exclusive).
+ * @param callbackContext The context through which to send the result.
+ * @param encoding The encoding to return contents as. Typical value is UTF-8. (see http://www.iana.org/assignments/character-sets)
+ * @param resultType The desired type of data to send to the callback.
+ * @return Contents of file.
+ */
+ public void readFileAs(final String srcURLstr, final int start, final int end, final CallbackContext callbackContext, final String encoding, final int resultType) throws MalformedURLException {
+ try {
+ LocalFilesystemURL inputURL = LocalFilesystemURL.parse(srcURLstr);
+ Filesystem fs = this.filesystemForURL(inputURL);
+ if (fs == null) {
+ throw new MalformedURLException("No installed handlers for this URL");
+ }
+
+ fs.readFileAtURL(inputURL, start, end, new Filesystem.ReadFileCallback() {
+ public void handleData(InputStream inputStream, String contentType) {
+ try {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ final int BUFFER_SIZE = 8192;
+ byte[] buffer = new byte[BUFFER_SIZE];
+
+ for (;;) {
+ int bytesRead = inputStream.read(buffer, 0, BUFFER_SIZE);
+
+ if (bytesRead <= 0) {
+ break;
+ }
+ os.write(buffer, 0, bytesRead);
+ }
+
+ PluginResult result;
+ switch (resultType) {
+ case PluginResult.MESSAGE_TYPE_STRING:
+ result = new PluginResult(PluginResult.Status.OK, os.toString(encoding));
+ break;
+ case PluginResult.MESSAGE_TYPE_ARRAYBUFFER:
+ result = new PluginResult(PluginResult.Status.OK, os.toByteArray());
+ break;
+ case PluginResult.MESSAGE_TYPE_BINARYSTRING:
+ result = new PluginResult(PluginResult.Status.OK, os.toByteArray(), true);
+ break;
+ default: // Base64.
+ byte[] base64 = Base64.encode(os.toByteArray(), Base64.NO_WRAP);
+ String s = "data:" + contentType + ";base64," + new String(base64, "US-ASCII");
+ result = new PluginResult(PluginResult.Status.OK, s);
+ }
+
+ callbackContext.sendPluginResult(result);
+ } catch (IOException e) {
+ LOG.d(LOG_TAG, e.getLocalizedMessage());
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, NOT_READABLE_ERR));
+ }
+ }
+ });
+
+
+ } catch (IllegalArgumentException e) {
+ MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+ mue.initCause(e);
+ throw mue;
+ } catch (FileNotFoundException e) {
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, NOT_FOUND_ERR));
+ } catch (IOException e) {
+ LOG.d(LOG_TAG, e.getLocalizedMessage());
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, NOT_READABLE_ERR));
+ }
+ }
+
+
+ /**
+ * Write contents of file.
+ *
+ * @param data The contents of the file.
+ * @param offset The position to begin writing the file.
+ * @param isBinary True if the file contents are base64-encoded binary data
+ */
+ /**/
+ public long write(String srcURLstr, String data, int offset, boolean isBinary) throws FileNotFoundException, IOException, NoModificationAllowedException {
+ try {
+ LocalFilesystemURL inputURL = LocalFilesystemURL.parse(srcURLstr);
+ Filesystem fs = this.filesystemForURL(inputURL);
+ if (fs == null) {
+ throw new MalformedURLException("No installed handlers for this URL");
+ }
+
+ long x = fs.writeToFileAtURL(inputURL, data, offset, isBinary); LOG.d("TEST",srcURLstr + ": "+x); return x;
+ } catch (IllegalArgumentException e) {
+ MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+ mue.initCause(e);
+ throw mue;
+ }
+
+ }
+
+ /**
+ * Truncate the file to size
+ */
+ private long truncateFile(String srcURLstr, long size) throws FileNotFoundException, IOException, NoModificationAllowedException {
+ try {
+ LocalFilesystemURL inputURL = LocalFilesystemURL.parse(srcURLstr);
+ Filesystem fs = this.filesystemForURL(inputURL);
+ if (fs == null) {
+ throw new MalformedURLException("No installed handlers for this URL");
+ }
+
+ return fs.truncateFileAtURL(inputURL, size);
+ } catch (IllegalArgumentException e) {
+ MalformedURLException mue = new MalformedURLException("Unrecognized filesystem URL");
+ mue.initCause(e);
+ throw mue;
+ }
+ }
+
+
+ /*
+ * Handle the response
+ */
+
+ public void onRequestPermissionResult(int requestCode, String[] permissions,
+ int[] grantResults) throws JSONException {
+
+ final PendingRequests.Request req = pendingRequests.getAndRemove(requestCode);
+ if (req != null) {
+ for(int r:grantResults)
+ {
+ if(r == PackageManager.PERMISSION_DENIED)
+ {
+ req.getCallbackContext().sendPluginResult(new PluginResult(PluginResult.Status.ERROR, SECURITY_ERR));
+ return;
+ }
+ }
+ switch(req.getAction())
+ {
+ case ACTION_GET_FILE:
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
+ String dirname = args.getString(0);
+
+ String path = args.getString(1);
+ JSONObject obj = getFile(dirname, path, args.optJSONObject(2), false);
+ req.getCallbackContext().success(obj);
+ }
+ }, req.getRawArgs(), req.getCallbackContext());
+ break;
+ case ACTION_GET_DIRECTORY:
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
+ String dirname = args.getString(0);
+
+ String path = args.getString(1);
+ JSONObject obj = getFile(dirname, path, args.optJSONObject(2), true);
+ req.getCallbackContext().success(obj);
+ }
+ }, req.getRawArgs(), req.getCallbackContext());
+ break;
+ case ACTION_WRITE:
+ threadhelper( new FileOp( ){
+ public void run(JSONArray args) throws JSONException, FileNotFoundException, IOException, NoModificationAllowedException {
+ String fname=args.getString(0);
+ String data=args.getString(1);
+ int offset=args.getInt(2);
+ Boolean isBinary=args.getBoolean(3);
+ long fileSize = write(fname, data, offset, isBinary);
+ req.getCallbackContext().sendPluginResult(new PluginResult(PluginResult.Status.OK, fileSize));
+ }
+ }, req.getRawArgs(), req.getCallbackContext());
+ break;
+ }
+ } else {
+ LOG.d(LOG_TAG, "Received permission callback for unknown request code");
+ }
+ }
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/file/Filesystem.java b/platforms/android/app/src/main/java/org/apache/cordova/file/Filesystem.java
new file mode 100644
index 0000000..c69d3bd
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/file/Filesystem.java
@@ -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 components = new ArrayList(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;
+ }
+ }
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/file/InvalidModificationException.java b/platforms/android/app/src/main/java/org/apache/cordova/file/InvalidModificationException.java
new file mode 100644
index 0000000..8f6bec5
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/file/InvalidModificationException.java
@@ -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);
+ }
+
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/file/LocalFilesystem.java b/platforms/android/app/src/main/java/org/apache/cordova/file/LocalFilesystem.java
new file mode 100644
index 0000000..051f994
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/file/LocalFilesystem.java
@@ -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();
+ }
+ }
+ }
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/file/LocalFilesystemURL.java b/platforms/android/app/src/main/java/org/apache/cordova/file/LocalFilesystemURL.java
new file mode 100644
index 0000000..b96b6ee
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/file/LocalFilesystemURL.java
@@ -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();
+ }
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/file/NoModificationAllowedException.java b/platforms/android/app/src/main/java/org/apache/cordova/file/NoModificationAllowedException.java
new file mode 100644
index 0000000..627eafb
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/file/NoModificationAllowedException.java
@@ -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);
+ }
+
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/file/PendingRequests.java b/platforms/android/app/src/main/java/org/apache/cordova/file/PendingRequests.java
new file mode 100644
index 0000000..23d6d73
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/file/PendingRequests.java
@@ -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 requests = new SparseArray();
+
+ /**
+ * 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;
+ }
+ }
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/file/TypeMismatchException.java b/platforms/android/app/src/main/java/org/apache/cordova/file/TypeMismatchException.java
new file mode 100644
index 0000000..1315f9a
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/file/TypeMismatchException.java
@@ -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);
+ }
+
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/filetransfer/FileProgressResult.java b/platforms/android/app/src/main/java/org/apache/cordova/filetransfer/FileProgressResult.java
new file mode 100644
index 0000000..76a7b13
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/filetransfer/FileProgressResult.java
@@ -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") + "}");
+ }
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/filetransfer/FileTransfer.java b/platforms/android/app/src/main/java/org/apache/cordova/filetransfer/FileTransfer.java
new file mode 100644
index 0000000..5a3c5d6
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/filetransfer/FileTransfer.java
@@ -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 activeRequests = new HashMap();
+ 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);
+ }
+ }
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/filetransfer/FileUploadResult.java b/platforms/android/app/src/main/java/org/apache/cordova/filetransfer/FileUploadResult.java
new file mode 100644
index 0000000..c24ea78
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/filetransfer/FileUploadResult.java
@@ -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) + "}");
+ }
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/inappbrowser/InAppBrowser.java b/platforms/android/app/src/main/java/org/apache/cordova/inappbrowser/InAppBrowser.java
new file mode 100644
index 0000000..9b3388c
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/inappbrowser/InAppBrowser.java
@@ -0,0 +1,1259 @@
+/*
+ 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.annotation.SuppressLint;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Browser;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.text.InputType;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.webkit.CookieManager;
+import android.webkit.CookieSyncManager;
+import android.webkit.HttpAuthHandler;
+import android.webkit.ValueCallback;
+import android.webkit.WebChromeClient;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.Config;
+import org.apache.cordova.CordovaArgs;
+import org.apache.cordova.CordovaHttpAuthHandler;
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.LOG;
+import org.apache.cordova.PluginManager;
+import org.apache.cordova.PluginResult;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.List;
+import java.util.HashMap;
+import java.util.StringTokenizer;
+
+@SuppressLint("SetJavaScriptEnabled")
+public class InAppBrowser extends CordovaPlugin {
+
+ private static final String NULL = "null";
+ protected static final String LOG_TAG = "InAppBrowser";
+ private static final String SELF = "_self";
+ private static final String SYSTEM = "_system";
+ private static final String EXIT_EVENT = "exit";
+ private static final String LOCATION = "location";
+ private static final String ZOOM = "zoom";
+ private static final String HIDDEN = "hidden";
+ private static final String LOAD_START_EVENT = "loadstart";
+ private static final String LOAD_STOP_EVENT = "loadstop";
+ private static final String LOAD_ERROR_EVENT = "loaderror";
+ private static final String CLEAR_ALL_CACHE = "clearcache";
+ private static final String CLEAR_SESSION_CACHE = "clearsessioncache";
+ private static final String HARDWARE_BACK_BUTTON = "hardwareback";
+ private static final String MEDIA_PLAYBACK_REQUIRES_USER_ACTION = "mediaPlaybackRequiresUserAction";
+ private static final String SHOULD_PAUSE = "shouldPauseOnSuspend";
+ private static final Boolean DEFAULT_HARDWARE_BACK = true;
+ private static final String USER_WIDE_VIEW_PORT = "useWideViewPort";
+ private static final String TOOLBAR_COLOR = "toolbarcolor";
+ private static final String CLOSE_BUTTON_CAPTION = "closebuttoncaption";
+ private static final String CLOSE_BUTTON_COLOR = "closebuttoncolor";
+ private static final String HIDE_NAVIGATION = "hidenavigationbuttons";
+ private static final String NAVIGATION_COLOR = "navigationbuttoncolor";
+ private static final String HIDE_URL = "hideurlbar";
+ private static final String FOOTER = "footer";
+ private static final String FOOTER_COLOR = "footercolor";
+
+ private static final List customizableOptions = Arrays.asList(CLOSE_BUTTON_CAPTION, TOOLBAR_COLOR, NAVIGATION_COLOR, CLOSE_BUTTON_COLOR, FOOTER_COLOR);
+
+ private InAppBrowserDialog dialog;
+ private WebView inAppWebView;
+ private EditText edittext;
+ private CallbackContext callbackContext;
+ private boolean showLocationBar = true;
+ private boolean showZoomControls = true;
+ private boolean openWindowHidden = false;
+ private boolean clearAllCache = false;
+ private boolean clearSessionCache = false;
+ private boolean hadwareBackButton = true;
+ private boolean mediaPlaybackRequiresUserGesture = false;
+ private boolean shouldPauseInAppBrowser = false;
+ private boolean useWideViewPort = true;
+ private ValueCallback mUploadCallback;
+ private ValueCallback mUploadCallbackLollipop;
+ private final static int FILECHOOSER_REQUESTCODE = 1;
+ private final static int FILECHOOSER_REQUESTCODE_LOLLIPOP = 2;
+ private String closeButtonCaption = "";
+ private String closeButtonColor = "";
+ private int toolbarColor = android.graphics.Color.LTGRAY;
+ private boolean hideNavigationButtons = false;
+ private String navigationButtonColor = "";
+ private boolean hideUrlBar = false;
+ private boolean showFooter = false;
+ private String footerColor = "";
+ private String[] allowedSchemes;
+
+ /**
+ * Executes the request and returns PluginResult.
+ *
+ * @param action the action to execute.
+ * @param args JSONArry of arguments for the plugin.
+ * @param callbackContext the callbackContext used when calling back into JavaScript.
+ * @return A PluginResult object with a status and message.
+ */
+ public boolean execute(String action, CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
+ if (action.equals("open")) {
+ this.callbackContext = callbackContext;
+ final String url = args.getString(0);
+ String t = args.optString(1);
+ if (t == null || t.equals("") || t.equals(NULL)) {
+ t = SELF;
+ }
+ final String target = t;
+ final HashMap features = parseFeature(args.optString(2));
+
+ LOG.d(LOG_TAG, "target = " + target);
+
+ this.cordova.getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ String result = "";
+ // SELF
+ if (SELF.equals(target)) {
+ LOG.d(LOG_TAG, "in self");
+ /* This code exists for compatibility between 3.x and 4.x versions of Cordova.
+ * Previously the Config class had a static method, isUrlWhitelisted(). That
+ * responsibility has been moved to the plugins, with an aggregating method in
+ * PluginManager.
+ */
+ Boolean shouldAllowNavigation = null;
+ if (url.startsWith("javascript:")) {
+ shouldAllowNavigation = true;
+ }
+ if (shouldAllowNavigation == null) {
+ try {
+ Method iuw = Config.class.getMethod("isUrlWhiteListed", String.class);
+ shouldAllowNavigation = (Boolean)iuw.invoke(null, url);
+ } catch (NoSuchMethodException e) {
+ LOG.d(LOG_TAG, e.getLocalizedMessage());
+ } catch (IllegalAccessException e) {
+ LOG.d(LOG_TAG, e.getLocalizedMessage());
+ } catch (InvocationTargetException e) {
+ LOG.d(LOG_TAG, e.getLocalizedMessage());
+ }
+ }
+ if (shouldAllowNavigation == null) {
+ try {
+ Method gpm = webView.getClass().getMethod("getPluginManager");
+ PluginManager pm = (PluginManager)gpm.invoke(webView);
+ Method san = pm.getClass().getMethod("shouldAllowNavigation", String.class);
+ shouldAllowNavigation = (Boolean)san.invoke(pm, url);
+ } catch (NoSuchMethodException e) {
+ LOG.d(LOG_TAG, e.getLocalizedMessage());
+ } catch (IllegalAccessException e) {
+ LOG.d(LOG_TAG, e.getLocalizedMessage());
+ } catch (InvocationTargetException e) {
+ LOG.d(LOG_TAG, e.getLocalizedMessage());
+ }
+ }
+ // load in webview
+ if (Boolean.TRUE.equals(shouldAllowNavigation)) {
+ LOG.d(LOG_TAG, "loading in webview");
+ webView.loadUrl(url);
+ }
+ //Load the dialer
+ else if (url.startsWith(WebView.SCHEME_TEL))
+ {
+ try {
+ LOG.d(LOG_TAG, "loading in dialer");
+ Intent intent = new Intent(Intent.ACTION_DIAL);
+ intent.setData(Uri.parse(url));
+ cordova.getActivity().startActivity(intent);
+ } catch (android.content.ActivityNotFoundException e) {
+ LOG.e(LOG_TAG, "Error dialing " + url + ": " + e.toString());
+ }
+ }
+ // load in InAppBrowser
+ else {
+ LOG.d(LOG_TAG, "loading in InAppBrowser");
+ result = showWebPage(url, features);
+ }
+ }
+ // SYSTEM
+ else if (SYSTEM.equals(target)) {
+ LOG.d(LOG_TAG, "in system");
+ result = openExternal(url);
+ }
+ // BLANK - or anything else
+ else {
+ LOG.d(LOG_TAG, "in blank");
+ result = showWebPage(url, features);
+ }
+
+ PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, result);
+ pluginResult.setKeepCallback(true);
+ callbackContext.sendPluginResult(pluginResult);
+ }
+ });
+ }
+ else if (action.equals("close")) {
+ closeDialog();
+ }
+ else if (action.equals("injectScriptCode")) {
+ String jsWrapper = null;
+ if (args.getBoolean(1)) {
+ jsWrapper = String.format("(function(){prompt(JSON.stringify([eval(%%s)]), 'gap-iab://%s')})()", callbackContext.getCallbackId());
+ }
+ injectDeferredObject(args.getString(0), jsWrapper);
+ }
+ else if (action.equals("injectScriptFile")) {
+ String jsWrapper;
+ if (args.getBoolean(1)) {
+ jsWrapper = String.format("(function(d) { var c = d.createElement('script'); c.src = %%s; c.onload = function() { prompt('', 'gap-iab://%s'); }; d.body.appendChild(c); })(document)", callbackContext.getCallbackId());
+ } else {
+ jsWrapper = "(function(d) { var c = d.createElement('script'); c.src = %s; d.body.appendChild(c); })(document)";
+ }
+ injectDeferredObject(args.getString(0), jsWrapper);
+ }
+ else if (action.equals("injectStyleCode")) {
+ String jsWrapper;
+ if (args.getBoolean(1)) {
+ jsWrapper = String.format("(function(d) { var c = d.createElement('style'); c.innerHTML = %%s; d.body.appendChild(c); prompt('', 'gap-iab://%s');})(document)", callbackContext.getCallbackId());
+ } else {
+ jsWrapper = "(function(d) { var c = d.createElement('style'); c.innerHTML = %s; d.body.appendChild(c); })(document)";
+ }
+ injectDeferredObject(args.getString(0), jsWrapper);
+ }
+ else if (action.equals("injectStyleFile")) {
+ String jsWrapper;
+ if (args.getBoolean(1)) {
+ jsWrapper = String.format("(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %%s; d.head.appendChild(c); prompt('', 'gap-iab://%s');})(document)", callbackContext.getCallbackId());
+ } else {
+ jsWrapper = "(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %s; d.head.appendChild(c); })(document)";
+ }
+ injectDeferredObject(args.getString(0), jsWrapper);
+ }
+ else if (action.equals("show")) {
+ this.cordova.getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ dialog.show();
+ }
+ });
+ PluginResult pluginResult = new PluginResult(PluginResult.Status.OK);
+ pluginResult.setKeepCallback(true);
+ this.callbackContext.sendPluginResult(pluginResult);
+ }
+ else if (action.equals("hide")) {
+ this.cordova.getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ dialog.hide();
+ }
+ });
+ PluginResult pluginResult = new PluginResult(PluginResult.Status.OK);
+ pluginResult.setKeepCallback(true);
+ this.callbackContext.sendPluginResult(pluginResult);
+ }
+ else {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Called when the view navigates.
+ */
+ @Override
+ public void onReset() {
+ closeDialog();
+ }
+
+ /**
+ * Called when the system is about to start resuming a previous activity.
+ */
+ @Override
+ public void onPause(boolean multitasking) {
+ if (shouldPauseInAppBrowser) {
+ inAppWebView.onPause();
+ }
+ }
+
+ /**
+ * Called when the activity will start interacting with the user.
+ */
+ @Override
+ public void onResume(boolean multitasking) {
+ if (shouldPauseInAppBrowser) {
+ inAppWebView.onResume();
+ }
+ }
+
+ /**
+ * Called by AccelBroker when listener is to be shut down.
+ * Stop listener.
+ */
+ public void onDestroy() {
+ closeDialog();
+ }
+
+ /**
+ * Inject an object (script or style) into the InAppBrowser WebView.
+ *
+ * This is a helper method for the inject{Script|Style}{Code|File} API calls, which
+ * provides a consistent method for injecting JavaScript code into the document.
+ *
+ * If a wrapper string is supplied, then the source string will be JSON-encoded (adding
+ * quotes) and wrapped using string formatting. (The wrapper string should have a single
+ * '%s' marker)
+ *
+ * @param source The source object (filename or script/style text) to inject into
+ * the document.
+ * @param jsWrapper A JavaScript string to wrap the source string in, so that the object
+ * is properly injected, or null if the source string is JavaScript text
+ * which should be executed directly.
+ */
+ private void injectDeferredObject(String source, String jsWrapper) {
+ if (inAppWebView!=null) {
+ String scriptToInject;
+ if (jsWrapper != null) {
+ org.json.JSONArray jsonEsc = new org.json.JSONArray();
+ jsonEsc.put(source);
+ String jsonRepr = jsonEsc.toString();
+ String jsonSourceString = jsonRepr.substring(1, jsonRepr.length()-1);
+ scriptToInject = String.format(jsWrapper, jsonSourceString);
+ } else {
+ scriptToInject = source;
+ }
+ final String finalScriptToInject = scriptToInject;
+ this.cordova.getActivity().runOnUiThread(new Runnable() {
+ @SuppressLint("NewApi")
+ @Override
+ public void run() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ // This action will have the side-effect of blurring the currently focused element
+ inAppWebView.loadUrl("javascript:" + finalScriptToInject);
+ } else {
+ inAppWebView.evaluateJavascript(finalScriptToInject, null);
+ }
+ }
+ });
+ } else {
+ LOG.d(LOG_TAG, "Can't inject code into the system browser");
+ }
+ }
+
+ /**
+ * Put the list of features into a hash map
+ *
+ * @param optString
+ * @return
+ */
+ private HashMap parseFeature(String optString) {
+ if (optString.equals(NULL)) {
+ return null;
+ } else {
+ HashMap map = new HashMap();
+ StringTokenizer features = new StringTokenizer(optString, ",");
+ StringTokenizer option;
+ while(features.hasMoreElements()) {
+ option = new StringTokenizer(features.nextToken(), "=");
+ if (option.hasMoreElements()) {
+ String key = option.nextToken();
+ String value = option.nextToken();
+ if (!customizableOptions.contains(key)){
+ value = value.equals("yes") || value.equals("no") ? value : "yes";
+ }
+ map.put(key, value);
+ }
+ }
+ return map;
+ }
+ }
+
+ /**
+ * Display a new browser with the specified URL.
+ *
+ * @param url the url to load.
+ * @return "" if ok, or error message.
+ */
+ public String openExternal(String url) {
+ try {
+ Intent intent = null;
+ intent = new Intent(Intent.ACTION_VIEW);
+ // 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.
+ Uri uri = Uri.parse(url);
+ if ("file".equals(uri.getScheme())) {
+ intent.setDataAndType(uri, webView.getResourceApi().getMimeType(uri));
+ } else {
+ intent.setData(uri);
+ }
+ intent.putExtra(Browser.EXTRA_APPLICATION_ID, cordova.getActivity().getPackageName());
+ this.cordova.getActivity().startActivity(intent);
+ return "";
+ // not catching FileUriExposedException explicitly because buildtools<24 doesn't know about it
+ } catch (java.lang.RuntimeException e) {
+ LOG.d(LOG_TAG, "InAppBrowser: Error loading url "+url+":"+ e.toString());
+ return e.toString();
+ }
+ }
+
+ /**
+ * Closes the dialog
+ */
+ public void closeDialog() {
+ this.cordova.getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final WebView childView = inAppWebView;
+ // The JS protects against multiple calls, so this should happen only when
+ // closeDialog() is called by other native code.
+ if (childView == null) {
+ return;
+ }
+
+ childView.setWebViewClient(new WebViewClient() {
+ // NB: wait for about:blank before dismissing
+ public void onPageFinished(WebView view, String url) {
+ if (dialog != null) {
+ dialog.dismiss();
+ dialog = null;
+ }
+ }
+ });
+ // NB: From SDK 19: "If you call methods on WebView from any thread
+ // other than your app's UI thread, it can cause unexpected results."
+ // http://developer.android.com/guide/webapps/migrating.html#Threads
+ childView.loadUrl("about:blank");
+
+ try {
+ JSONObject obj = new JSONObject();
+ obj.put("type", EXIT_EVENT);
+ sendUpdate(obj, false);
+ } catch (JSONException ex) {
+ LOG.d(LOG_TAG, "Should never happen");
+ }
+ }
+ });
+ }
+
+ /**
+ * Checks to see if it is possible to go back one page in history, then does so.
+ */
+ public void goBack() {
+ if (this.inAppWebView.canGoBack()) {
+ this.inAppWebView.goBack();
+ }
+ }
+
+ /**
+ * Can the web browser go back?
+ * @return boolean
+ */
+ public boolean canGoBack() {
+ return this.inAppWebView.canGoBack();
+ }
+
+ /**
+ * Has the user set the hardware back button to go back
+ * @return boolean
+ */
+ public boolean hardwareBack() {
+ return hadwareBackButton;
+ }
+
+ /**
+ * Checks to see if it is possible to go forward one page in history, then does so.
+ */
+ private void goForward() {
+ if (this.inAppWebView.canGoForward()) {
+ this.inAppWebView.goForward();
+ }
+ }
+
+ /**
+ * Navigate to the new page
+ *
+ * @param url to load
+ */
+ private void navigate(String url) {
+ InputMethodManager imm = (InputMethodManager)this.cordova.getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(edittext.getWindowToken(), 0);
+
+ if (!url.startsWith("http") && !url.startsWith("file:")) {
+ this.inAppWebView.loadUrl("http://" + url);
+ } else {
+ this.inAppWebView.loadUrl(url);
+ }
+ this.inAppWebView.requestFocus();
+ }
+
+
+ /**
+ * Should we show the location bar?
+ *
+ * @return boolean
+ */
+ private boolean getShowLocationBar() {
+ return this.showLocationBar;
+ }
+
+ private InAppBrowser getInAppBrowser(){
+ return this;
+ }
+
+ /**
+ * Display a new browser with the specified URL.
+ *
+ * @param url the url to load.
+ * @param features jsonObject
+ */
+ public String showWebPage(final String url, HashMap features) {
+ // Determine if we should hide the location bar.
+ showLocationBar = true;
+ showZoomControls = true;
+ openWindowHidden = false;
+ mediaPlaybackRequiresUserGesture = false;
+
+ if (features != null) {
+ String show = features.get(LOCATION);
+ if (show != null) {
+ showLocationBar = show.equals("yes") ? true : false;
+ }
+ if(showLocationBar) {
+ String hideNavigation = features.get(HIDE_NAVIGATION);
+ String hideUrl = features.get(HIDE_URL);
+ if(hideNavigation != null) hideNavigationButtons = hideNavigation.equals("yes") ? true : false;
+ if(hideUrl != null) hideUrlBar = hideUrl.equals("yes") ? true : false;
+ }
+ String zoom = features.get(ZOOM);
+ if (zoom != null) {
+ showZoomControls = zoom.equals("yes") ? true : false;
+ }
+ String hidden = features.get(HIDDEN);
+ if (hidden != null) {
+ openWindowHidden = hidden.equals("yes") ? true : false;
+ }
+ String hardwareBack = features.get(HARDWARE_BACK_BUTTON);
+ if (hardwareBack != null) {
+ hadwareBackButton = hardwareBack.equals("yes") ? true : false;
+ } else {
+ hadwareBackButton = DEFAULT_HARDWARE_BACK;
+ }
+ String mediaPlayback = features.get(MEDIA_PLAYBACK_REQUIRES_USER_ACTION);
+ if (mediaPlayback != null) {
+ mediaPlaybackRequiresUserGesture = mediaPlayback.equals("yes") ? true : false;
+ }
+ String cache = features.get(CLEAR_ALL_CACHE);
+ if (cache != null) {
+ clearAllCache = cache.equals("yes") ? true : false;
+ } else {
+ cache = features.get(CLEAR_SESSION_CACHE);
+ if (cache != null) {
+ clearSessionCache = cache.equals("yes") ? true : false;
+ }
+ }
+ String shouldPause = features.get(SHOULD_PAUSE);
+ if (shouldPause != null) {
+ shouldPauseInAppBrowser = shouldPause.equals("yes") ? true : false;
+ }
+ String wideViewPort = features.get(USER_WIDE_VIEW_PORT);
+ if (wideViewPort != null ) {
+ useWideViewPort = wideViewPort.equals("yes") ? true : false;
+ }
+ String closeButtonCaptionSet = features.get(CLOSE_BUTTON_CAPTION);
+ if (closeButtonCaptionSet != null) {
+ closeButtonCaption = closeButtonCaptionSet;
+ }
+ String closeButtonColorSet = features.get(CLOSE_BUTTON_COLOR);
+ if (closeButtonColorSet != null) {
+ closeButtonColor = closeButtonColorSet;
+ }
+ String toolbarColorSet = features.get(TOOLBAR_COLOR);
+ if (toolbarColorSet != null) {
+ toolbarColor = android.graphics.Color.parseColor(toolbarColorSet);
+ }
+ String navigationButtonColorSet = features.get(NAVIGATION_COLOR);
+ if (navigationButtonColorSet != null) {
+ navigationButtonColor = navigationButtonColorSet;
+ }
+ String showFooterSet = features.get(FOOTER);
+ if (showFooterSet != null) {
+ showFooter = showFooterSet.equals("yes") ? true : false;
+ }
+ String footerColorSet = features.get(FOOTER_COLOR);
+ if (footerColorSet != null) {
+ footerColor = footerColorSet;
+ }
+ }
+
+ final CordovaWebView thatWebView = this.webView;
+
+ // Create dialog in new thread
+ Runnable runnable = new Runnable() {
+ /**
+ * Convert our DIP units to Pixels
+ *
+ * @return int
+ */
+ private int dpToPixels(int dipValue) {
+ int value = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP,
+ (float) dipValue,
+ cordova.getActivity().getResources().getDisplayMetrics()
+ );
+
+ return value;
+ }
+
+ private View createCloseButton(int id){
+ View _close;
+ Resources activityRes = cordova.getActivity().getResources();
+
+ if (closeButtonCaption != "") {
+ // Use TextView for text
+ TextView close = new TextView(cordova.getActivity());
+ close.setText(closeButtonCaption);
+ close.setTextSize(20);
+ if (closeButtonColor != "") close.setTextColor(android.graphics.Color.parseColor(closeButtonColor));
+ close.setGravity(android.view.Gravity.CENTER_VERTICAL);
+ close.setPadding(this.dpToPixels(10), 0, this.dpToPixels(10), 0);
+ _close = close;
+ }
+ else {
+ ImageButton close = new ImageButton(cordova.getActivity());
+ int closeResId = activityRes.getIdentifier("ic_action_remove", "drawable", cordova.getActivity().getPackageName());
+ Drawable closeIcon = activityRes.getDrawable(closeResId);
+ if (closeButtonColor != "") close.setColorFilter(android.graphics.Color.parseColor(closeButtonColor));
+ close.setImageDrawable(closeIcon);
+ close.setScaleType(ImageView.ScaleType.FIT_CENTER);
+ if (Build.VERSION.SDK_INT >= 16)
+ close.getAdjustViewBounds();
+
+ _close = close;
+ }
+
+ RelativeLayout.LayoutParams closeLayoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
+ closeLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
+ _close.setLayoutParams(closeLayoutParams);
+
+ if (Build.VERSION.SDK_INT >= 16)
+ _close.setBackground(null);
+ else
+ _close.setBackgroundDrawable(null);
+
+ _close.setContentDescription("Close Button");
+ _close.setId(Integer.valueOf(id));
+ _close.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ closeDialog();
+ }
+ });
+
+ return _close;
+ }
+
+ @SuppressLint("NewApi")
+ public void run() {
+
+ // CB-6702 InAppBrowser hangs when opening more than one instance
+ if (dialog != null) {
+ dialog.dismiss();
+ };
+
+ // Let's create the main dialog
+ dialog = new InAppBrowserDialog(cordova.getActivity(), android.R.style.Theme_NoTitleBar);
+ dialog.getWindow().getAttributes().windowAnimations = android.R.style.Animation_Dialog;
+ dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
+ dialog.setCancelable(true);
+ dialog.setInAppBroswer(getInAppBrowser());
+
+ // Main container layout
+ LinearLayout main = new LinearLayout(cordova.getActivity());
+ main.setOrientation(LinearLayout.VERTICAL);
+
+ // Toolbar layout
+ RelativeLayout toolbar = new RelativeLayout(cordova.getActivity());
+ //Please, no more black!
+ toolbar.setBackgroundColor(toolbarColor);
+ toolbar.setLayoutParams(new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, this.dpToPixels(44)));
+ toolbar.setPadding(this.dpToPixels(2), this.dpToPixels(2), this.dpToPixels(2), this.dpToPixels(2));
+ toolbar.setHorizontalGravity(Gravity.LEFT);
+ toolbar.setVerticalGravity(Gravity.TOP);
+
+ // Action Button Container layout
+ RelativeLayout actionButtonContainer = new RelativeLayout(cordova.getActivity());
+ actionButtonContainer.setLayoutParams(new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+ actionButtonContainer.setHorizontalGravity(Gravity.LEFT);
+ actionButtonContainer.setVerticalGravity(Gravity.CENTER_VERTICAL);
+ actionButtonContainer.setId(Integer.valueOf(1));
+
+ // Back button
+ ImageButton back = new ImageButton(cordova.getActivity());
+ RelativeLayout.LayoutParams backLayoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
+ backLayoutParams.addRule(RelativeLayout.ALIGN_LEFT);
+ back.setLayoutParams(backLayoutParams);
+ back.setContentDescription("Back Button");
+ back.setId(Integer.valueOf(2));
+ Resources activityRes = cordova.getActivity().getResources();
+ int backResId = activityRes.getIdentifier("ic_action_previous_item", "drawable", cordova.getActivity().getPackageName());
+ Drawable backIcon = activityRes.getDrawable(backResId);
+ if (navigationButtonColor != "") back.setColorFilter(android.graphics.Color.parseColor(navigationButtonColor));
+ if (Build.VERSION.SDK_INT >= 16)
+ back.setBackground(null);
+ else
+ back.setBackgroundDrawable(null);
+ back.setImageDrawable(backIcon);
+ back.setScaleType(ImageView.ScaleType.FIT_CENTER);
+ back.setPadding(0, this.dpToPixels(10), 0, this.dpToPixels(10));
+ if (Build.VERSION.SDK_INT >= 16)
+ back.getAdjustViewBounds();
+
+ back.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ goBack();
+ }
+ });
+
+ // Forward button
+ ImageButton forward = new ImageButton(cordova.getActivity());
+ RelativeLayout.LayoutParams forwardLayoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
+ forwardLayoutParams.addRule(RelativeLayout.RIGHT_OF, 2);
+ forward.setLayoutParams(forwardLayoutParams);
+ forward.setContentDescription("Forward Button");
+ forward.setId(Integer.valueOf(3));
+ int fwdResId = activityRes.getIdentifier("ic_action_next_item", "drawable", cordova.getActivity().getPackageName());
+ Drawable fwdIcon = activityRes.getDrawable(fwdResId);
+ if (navigationButtonColor != "") forward.setColorFilter(android.graphics.Color.parseColor(navigationButtonColor));
+ if (Build.VERSION.SDK_INT >= 16)
+ forward.setBackground(null);
+ else
+ forward.setBackgroundDrawable(null);
+ forward.setImageDrawable(fwdIcon);
+ forward.setScaleType(ImageView.ScaleType.FIT_CENTER);
+ forward.setPadding(0, this.dpToPixels(10), 0, this.dpToPixels(10));
+ if (Build.VERSION.SDK_INT >= 16)
+ forward.getAdjustViewBounds();
+
+ forward.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ goForward();
+ }
+ });
+
+ // Edit Text Box
+ edittext = new EditText(cordova.getActivity());
+ RelativeLayout.LayoutParams textLayoutParams = new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ textLayoutParams.addRule(RelativeLayout.RIGHT_OF, 1);
+ textLayoutParams.addRule(RelativeLayout.LEFT_OF, 5);
+ edittext.setLayoutParams(textLayoutParams);
+ edittext.setId(Integer.valueOf(4));
+ edittext.setSingleLine(true);
+ edittext.setText(url);
+ edittext.setInputType(InputType.TYPE_TEXT_VARIATION_URI);
+ edittext.setImeOptions(EditorInfo.IME_ACTION_GO);
+ edittext.setInputType(InputType.TYPE_NULL); // Will not except input... Makes the text NON-EDITABLE
+ edittext.setOnKeyListener(new View.OnKeyListener() {
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ // If the event is a key-down event on the "enter" button
+ if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
+ navigate(edittext.getText().toString());
+ return true;
+ }
+ return false;
+ }
+ });
+
+
+ // Header Close/Done button
+ View close = createCloseButton(5);
+ toolbar.addView(close);
+
+ // Footer
+ RelativeLayout footer = new RelativeLayout(cordova.getActivity());
+ int _footerColor;
+ if(footerColor != ""){
+ _footerColor = Color.parseColor(footerColor);
+ }else{
+ _footerColor = android.graphics.Color.LTGRAY;
+ }
+ footer.setBackgroundColor(_footerColor);
+ RelativeLayout.LayoutParams footerLayout = new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, this.dpToPixels(44));
+ footerLayout.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, RelativeLayout.TRUE);
+ footer.setLayoutParams(footerLayout);
+ if (closeButtonCaption != "") footer.setPadding(this.dpToPixels(8), this.dpToPixels(8), this.dpToPixels(8), this.dpToPixels(8));
+ footer.setHorizontalGravity(Gravity.LEFT);
+ footer.setVerticalGravity(Gravity.BOTTOM);
+
+ View footerClose = createCloseButton(7);
+ footer.addView(footerClose);
+
+
+ // WebView
+ inAppWebView = new WebView(cordova.getActivity());
+ inAppWebView.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ inAppWebView.setId(Integer.valueOf(6));
+ // File Chooser Implemented ChromeClient
+ inAppWebView.setWebChromeClient(new InAppChromeClient(thatWebView) {
+ // For Android 5.0+
+ public boolean onShowFileChooser (WebView webView, ValueCallback filePathCallback, WebChromeClient.FileChooserParams fileChooserParams)
+ {
+ LOG.d(LOG_TAG, "File Chooser 5.0+");
+ // If callback exists, finish it.
+ if(mUploadCallbackLollipop != null) {
+ mUploadCallbackLollipop.onReceiveValue(null);
+ }
+ mUploadCallbackLollipop = filePathCallback;
+
+ // Create File Chooser Intent
+ Intent content = new Intent(Intent.ACTION_GET_CONTENT);
+ content.addCategory(Intent.CATEGORY_OPENABLE);
+ content.setType("*/*");
+
+ // Run cordova startActivityForResult
+ cordova.startActivityForResult(InAppBrowser.this, Intent.createChooser(content, "Select File"), FILECHOOSER_REQUESTCODE_LOLLIPOP);
+ return true;
+ }
+
+ // For Android 4.1+
+ public void openFileChooser(ValueCallback uploadMsg, String acceptType, String capture)
+ {
+ LOG.d(LOG_TAG, "File Chooser 4.1+");
+ // Call file chooser for Android 3.0+
+ openFileChooser(uploadMsg, acceptType);
+ }
+
+ // For Android 3.0+
+ public void openFileChooser(ValueCallback uploadMsg, String acceptType)
+ {
+ LOG.d(LOG_TAG, "File Chooser 3.0+");
+ mUploadCallback = uploadMsg;
+ Intent content = new Intent(Intent.ACTION_GET_CONTENT);
+ content.addCategory(Intent.CATEGORY_OPENABLE);
+
+ // run startActivityForResult
+ cordova.startActivityForResult(InAppBrowser.this, Intent.createChooser(content, "Select File"), FILECHOOSER_REQUESTCODE);
+ }
+
+ });
+ WebViewClient client = new InAppBrowserClient(thatWebView, edittext);
+ inAppWebView.setWebViewClient(client);
+ WebSettings settings = inAppWebView.getSettings();
+ settings.setJavaScriptEnabled(true);
+ settings.setJavaScriptCanOpenWindowsAutomatically(true);
+ settings.setBuiltInZoomControls(showZoomControls);
+ settings.setPluginState(android.webkit.WebSettings.PluginState.ON);
+
+ if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ settings.setMediaPlaybackRequiresUserGesture(mediaPlaybackRequiresUserGesture);
+ }
+
+ String overrideUserAgent = preferences.getString("OverrideUserAgent", null);
+ String appendUserAgent = preferences.getString("AppendUserAgent", null);
+
+ if (overrideUserAgent != null) {
+ settings.setUserAgentString(overrideUserAgent);
+ }
+ if (appendUserAgent != null) {
+ settings.setUserAgentString(settings.getUserAgentString() + appendUserAgent);
+ }
+
+ //Toggle whether this is enabled or not!
+ Bundle appSettings = cordova.getActivity().getIntent().getExtras();
+ boolean enableDatabase = appSettings == null ? true : appSettings.getBoolean("InAppBrowserStorageEnabled", true);
+ if (enableDatabase) {
+ String databasePath = cordova.getActivity().getApplicationContext().getDir("inAppBrowserDB", Context.MODE_PRIVATE).getPath();
+ settings.setDatabasePath(databasePath);
+ settings.setDatabaseEnabled(true);
+ }
+ settings.setDomStorageEnabled(true);
+
+ if (clearAllCache) {
+ CookieManager.getInstance().removeAllCookie();
+ } else if (clearSessionCache) {
+ CookieManager.getInstance().removeSessionCookie();
+ }
+
+ // Enable Thirdparty Cookies on >=Android 5.0 device
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
+ CookieManager.getInstance().setAcceptThirdPartyCookies(inAppWebView,true);
+ }
+
+ inAppWebView.loadUrl(url);
+ inAppWebView.setId(Integer.valueOf(6));
+ inAppWebView.getSettings().setLoadWithOverviewMode(true);
+ inAppWebView.getSettings().setUseWideViewPort(useWideViewPort);
+ inAppWebView.requestFocus();
+ inAppWebView.requestFocusFromTouch();
+
+ // Add the back and forward buttons to our action button container layout
+ actionButtonContainer.addView(back);
+ actionButtonContainer.addView(forward);
+
+ // Add the views to our toolbar if they haven't been disabled
+ if (!hideNavigationButtons) toolbar.addView(actionButtonContainer);
+ if (!hideUrlBar) toolbar.addView(edittext);
+
+ // Don't add the toolbar if its been disabled
+ if (getShowLocationBar()) {
+ // Add our toolbar to our main view/layout
+ main.addView(toolbar);
+ }
+
+ // Add our webview to our main view/layout
+ RelativeLayout webViewLayout = new RelativeLayout(cordova.getActivity());
+ webViewLayout.addView(inAppWebView);
+ main.addView(webViewLayout);
+
+ // Don't add the footer unless it's been enabled
+ if (showFooter) {
+ webViewLayout.addView(footer);
+ }
+
+ WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
+ lp.copyFrom(dialog.getWindow().getAttributes());
+ lp.width = WindowManager.LayoutParams.MATCH_PARENT;
+ lp.height = WindowManager.LayoutParams.MATCH_PARENT;
+
+ dialog.setContentView(main);
+ dialog.show();
+ dialog.getWindow().setAttributes(lp);
+ // the goal of openhidden is to load the url and not display it
+ // Show() needs to be called to cause the URL to be loaded
+ if(openWindowHidden) {
+ dialog.hide();
+ }
+ }
+ };
+ this.cordova.getActivity().runOnUiThread(runnable);
+ return "";
+ }
+
+ /**
+ * Create a new plugin success result and send it back to JavaScript
+ *
+ * @param obj a JSONObject contain event payload information
+ */
+ private void sendUpdate(JSONObject obj, boolean keepCallback) {
+ sendUpdate(obj, keepCallback, PluginResult.Status.OK);
+ }
+
+ /**
+ * Create a new plugin result and send it back to JavaScript
+ *
+ * @param obj a JSONObject contain event payload information
+ * @param status the status code to return to the JavaScript environment
+ */
+ private void sendUpdate(JSONObject obj, boolean keepCallback, PluginResult.Status status) {
+ if (callbackContext != null) {
+ PluginResult result = new PluginResult(status, obj);
+ result.setKeepCallback(keepCallback);
+ callbackContext.sendPluginResult(result);
+ if (!keepCallback) {
+ callbackContext = null;
+ }
+ }
+ }
+
+ /**
+ * Receive File Data from File Chooser
+ *
+ * @param requestCode the requested code from chromeclient
+ * @param resultCode the result code returned from android system
+ * @param intent the data from android file chooser
+ */
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ // For Android >= 5.0
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ LOG.d(LOG_TAG, "onActivityResult (For Android >= 5.0)");
+ // If RequestCode or Callback is Invalid
+ if(requestCode != FILECHOOSER_REQUESTCODE_LOLLIPOP || mUploadCallbackLollipop == null) {
+ super.onActivityResult(requestCode, resultCode, intent);
+ return;
+ }
+ mUploadCallbackLollipop.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, intent));
+ mUploadCallbackLollipop = null;
+ }
+ // For Android < 5.0
+ else {
+ LOG.d(LOG_TAG, "onActivityResult (For Android < 5.0)");
+ // If RequestCode or Callback is Invalid
+ if(requestCode != FILECHOOSER_REQUESTCODE || mUploadCallback == null) {
+ super.onActivityResult(requestCode, resultCode, intent);
+ return;
+ }
+
+ if (null == mUploadCallback) return;
+ Uri result = intent == null || resultCode != cordova.getActivity().RESULT_OK ? null : intent.getData();
+
+ mUploadCallback.onReceiveValue(result);
+ mUploadCallback = null;
+ }
+ }
+
+ /**
+ * The webview client receives notifications about appView
+ */
+ public class InAppBrowserClient extends WebViewClient {
+ EditText edittext;
+ CordovaWebView webView;
+
+ /**
+ * Constructor.
+ *
+ * @param webView
+ * @param mEditText
+ */
+ public InAppBrowserClient(CordovaWebView webView, EditText mEditText) {
+ this.webView = webView;
+ this.edittext = mEditText;
+ }
+
+ /**
+ * Override the URL that should be loaded
+ *
+ * This handles a small subset of all the URIs that would be encountered.
+ *
+ * @param webView
+ * @param url
+ */
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView webView, String url) {
+ if (url.startsWith(WebView.SCHEME_TEL)) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_DIAL);
+ intent.setData(Uri.parse(url));
+ cordova.getActivity().startActivity(intent);
+ return true;
+ } catch (android.content.ActivityNotFoundException e) {
+ LOG.e(LOG_TAG, "Error dialing " + url + ": " + e.toString());
+ }
+ } else if (url.startsWith("geo:") || url.startsWith(WebView.SCHEME_MAILTO) || url.startsWith("market:") || url.startsWith("intent:")) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(url));
+ cordova.getActivity().startActivity(intent);
+ return true;
+ } catch (android.content.ActivityNotFoundException e) {
+ LOG.e(LOG_TAG, "Error with " + url + ": " + e.toString());
+ }
+ }
+ // If sms:5551212?body=This is the message
+ else if (url.startsWith("sms:")) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+
+ // Get address
+ String address = null;
+ int parmIndex = url.indexOf('?');
+ if (parmIndex == -1) {
+ address = url.substring(4);
+ } else {
+ address = url.substring(4, parmIndex);
+
+ // If body, then set sms body
+ Uri uri = Uri.parse(url);
+ String query = uri.getQuery();
+ if (query != null) {
+ if (query.startsWith("body=")) {
+ intent.putExtra("sms_body", query.substring(5));
+ }
+ }
+ }
+ intent.setData(Uri.parse("sms:" + address));
+ intent.putExtra("address", address);
+ intent.setType("vnd.android-dir/mms-sms");
+ cordova.getActivity().startActivity(intent);
+ return true;
+ } catch (android.content.ActivityNotFoundException e) {
+ LOG.e(LOG_TAG, "Error sending sms " + url + ":" + e.toString());
+ }
+ }
+ // Test for whitelisted custom scheme names like mycoolapp:// or twitteroauthresponse:// (Twitter Oauth Response)
+ else if (!url.startsWith("http:") && !url.startsWith("https:") && url.matches("^[a-z]*://.*?$")) {
+ if (allowedSchemes == null) {
+ String allowed = preferences.getString("AllowedSchemes", "");
+ allowedSchemes = allowed.split(",");
+ }
+ if (allowedSchemes != null) {
+ for (String scheme : allowedSchemes) {
+ if (url.startsWith(scheme)) {
+ try {
+ JSONObject obj = new JSONObject();
+ obj.put("type", "customscheme");
+ obj.put("url", url);
+ sendUpdate(obj, true);
+ return true;
+ } catch (JSONException ex) {
+ LOG.e(LOG_TAG, "Custom Scheme URI passed in has caused a JSON error.");
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+
+ /*
+ * onPageStarted fires the LOAD_START_EVENT
+ *
+ * @param view
+ * @param url
+ * @param favicon
+ */
+ @Override
+ public void onPageStarted(WebView view, String url, Bitmap favicon) {
+ super.onPageStarted(view, url, favicon);
+ String newloc = "";
+ if (url.startsWith("http:") || url.startsWith("https:") || url.startsWith("file:")) {
+ newloc = url;
+ }
+ else
+ {
+ // Assume that everything is HTTP at this point, because if we don't specify,
+ // it really should be. Complain loudly about this!!!
+ LOG.e(LOG_TAG, "Possible Uncaught/Unknown URI");
+ newloc = "http://" + url;
+ }
+
+ // Update the UI if we haven't already
+ if (!newloc.equals(edittext.getText().toString())) {
+ edittext.setText(newloc);
+ }
+
+ try {
+ JSONObject obj = new JSONObject();
+ obj.put("type", LOAD_START_EVENT);
+ obj.put("url", newloc);
+ sendUpdate(obj, true);
+ } catch (JSONException ex) {
+ LOG.e(LOG_TAG, "URI passed in has caused a JSON error.");
+ }
+ }
+
+
+
+ public void onPageFinished(WebView view, String url) {
+ super.onPageFinished(view, url);
+
+ // CB-10395 InAppBrowser's WebView not storing cookies reliable to local device storage
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
+ CookieManager.getInstance().flush();
+ } else {
+ CookieSyncManager.getInstance().sync();
+ }
+
+ // https://issues.apache.org/jira/browse/CB-11248
+ view.clearFocus();
+ view.requestFocus();
+
+ try {
+ JSONObject obj = new JSONObject();
+ obj.put("type", LOAD_STOP_EVENT);
+ obj.put("url", url);
+
+ sendUpdate(obj, true);
+ } catch (JSONException ex) {
+ LOG.d(LOG_TAG, "Should never happen");
+ }
+ }
+
+ public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+ super.onReceivedError(view, errorCode, description, failingUrl);
+
+ try {
+ JSONObject obj = new JSONObject();
+ obj.put("type", LOAD_ERROR_EVENT);
+ obj.put("url", failingUrl);
+ obj.put("code", errorCode);
+ obj.put("message", description);
+
+ sendUpdate(obj, true, PluginResult.Status.ERROR);
+ } catch (JSONException ex) {
+ LOG.d(LOG_TAG, "Should never happen");
+ }
+ }
+
+ /**
+ * On received http auth request.
+ */
+ @Override
+ public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
+
+ // Check if there is some plugin which can resolve this auth challenge
+ PluginManager pluginManager = null;
+ try {
+ Method gpm = webView.getClass().getMethod("getPluginManager");
+ pluginManager = (PluginManager)gpm.invoke(webView);
+ } catch (NoSuchMethodException e) {
+ LOG.d(LOG_TAG, e.getLocalizedMessage());
+ } catch (IllegalAccessException e) {
+ LOG.d(LOG_TAG, e.getLocalizedMessage());
+ } catch (InvocationTargetException e) {
+ LOG.d(LOG_TAG, e.getLocalizedMessage());
+ }
+
+ if (pluginManager == null) {
+ try {
+ Field pmf = webView.getClass().getField("pluginManager");
+ pluginManager = (PluginManager)pmf.get(webView);
+ } catch (NoSuchFieldException e) {
+ LOG.d(LOG_TAG, e.getLocalizedMessage());
+ } catch (IllegalAccessException e) {
+ LOG.d(LOG_TAG, e.getLocalizedMessage());
+ }
+ }
+
+ if (pluginManager != null && pluginManager.onReceivedHttpAuthRequest(webView, new CordovaHttpAuthHandler(handler), host, realm)) {
+ return;
+ }
+
+ // By default handle 401 like we'd normally do!
+ super.onReceivedHttpAuthRequest(view, handler, host, realm);
+ }
+ }
+}
\ No newline at end of file
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/inappbrowser/InAppBrowserDialog.java b/platforms/android/app/src/main/java/org/apache/cordova/inappbrowser/InAppBrowserDialog.java
new file mode 100644
index 0000000..e7b212f
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/inappbrowser/InAppBrowserDialog.java
@@ -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();
+ }
+ }
+ }
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/inappbrowser/InAppChromeClient.java b/platforms/android/app/src/main/java/org/apache/cordova/inappbrowser/InAppChromeClient.java
new file mode 100644
index 0000000..a2145e6
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/inappbrowser/InAppChromeClient.java
@@ -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://
+ *
+ * where 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;
+ }
+
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/splashscreen/SplashScreen.java b/platforms/android/app/src/main/java/org/apache/cordova/splashscreen/SplashScreen.java
new file mode 100644
index 0000000..6f56c6c
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/splashscreen/SplashScreen.java
@@ -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;
+ }
+ }
+ });
+ }
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/statusbar/StatusBar.java b/platforms/android/app/src/main/java/org/apache/cordova/statusbar/StatusBar.java
new file mode 100644
index 0000000..714c30e
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/statusbar/StatusBar.java
@@ -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'");
+ }
+ }
+ }
+}
diff --git a/platforms/android/app/src/main/java/org/apache/cordova/whitelist/WhitelistPlugin.java b/platforms/android/app/src/main/java/org/apache/cordova/whitelist/WhitelistPlugin.java
new file mode 100644
index 0000000..3656788
--- /dev/null
+++ b/platforms/android/app/src/main/java/org/apache/cordova/whitelist/WhitelistPlugin.java
@@ -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 within config.xml. Please use 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;
+ }
+}
diff --git a/platforms/android/app/src/main/res/drawable-hdpi/ic_action_next_item.png b/platforms/android/app/src/main/res/drawable-hdpi/ic_action_next_item.png
new file mode 100644
index 0000000..fa469d8
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-hdpi/ic_action_next_item.png differ
diff --git a/platforms/android/app/src/main/res/drawable-hdpi/ic_action_previous_item.png b/platforms/android/app/src/main/res/drawable-hdpi/ic_action_previous_item.png
new file mode 100644
index 0000000..e861ecc
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-hdpi/ic_action_previous_item.png differ
diff --git a/platforms/android/app/src/main/res/drawable-hdpi/ic_action_remove.png b/platforms/android/app/src/main/res/drawable-hdpi/ic_action_remove.png
new file mode 100644
index 0000000..f889617
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-hdpi/ic_action_remove.png differ
diff --git a/platforms/android/app/src/main/res/drawable-land-hdpi/screen.png b/platforms/android/app/src/main/res/drawable-land-hdpi/screen.png
new file mode 100644
index 0000000..150564a
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-land-hdpi/screen.png differ
diff --git a/platforms/android/app/src/main/res/drawable-land-ldpi/screen.png b/platforms/android/app/src/main/res/drawable-land-ldpi/screen.png
new file mode 100644
index 0000000..3052cec
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-land-ldpi/screen.png differ
diff --git a/platforms/android/app/src/main/res/drawable-land-mdpi/screen.png b/platforms/android/app/src/main/res/drawable-land-mdpi/screen.png
new file mode 100644
index 0000000..536a819
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-land-mdpi/screen.png differ
diff --git a/platforms/android/app/src/main/res/drawable-land-xhdpi/screen.png b/platforms/android/app/src/main/res/drawable-land-xhdpi/screen.png
new file mode 100644
index 0000000..be50593
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-land-xhdpi/screen.png differ
diff --git a/platforms/android/app/src/main/res/drawable-land-xxhdpi/screen.png b/platforms/android/app/src/main/res/drawable-land-xxhdpi/screen.png
new file mode 100644
index 0000000..2903a88
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-land-xxhdpi/screen.png differ
diff --git a/platforms/android/app/src/main/res/drawable-land-xxxhdpi/screen.png b/platforms/android/app/src/main/res/drawable-land-xxxhdpi/screen.png
new file mode 100644
index 0000000..34a2aad
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-land-xxxhdpi/screen.png differ
diff --git a/platforms/android/app/src/main/res/drawable-mdpi/ic_action_next_item.png b/platforms/android/app/src/main/res/drawable-mdpi/ic_action_next_item.png
new file mode 100644
index 0000000..47365a3
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-mdpi/ic_action_next_item.png differ
diff --git a/platforms/android/app/src/main/res/drawable-mdpi/ic_action_previous_item.png b/platforms/android/app/src/main/res/drawable-mdpi/ic_action_previous_item.png
new file mode 100644
index 0000000..4ad2df4
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-mdpi/ic_action_previous_item.png differ
diff --git a/platforms/android/app/src/main/res/drawable-mdpi/ic_action_remove.png b/platforms/android/app/src/main/res/drawable-mdpi/ic_action_remove.png
new file mode 100644
index 0000000..e84853e
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-mdpi/ic_action_remove.png differ
diff --git a/platforms/android/app/src/main/res/drawable-port-hdpi/screen.png b/platforms/android/app/src/main/res/drawable-port-hdpi/screen.png
new file mode 100644
index 0000000..fa9efc4
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-port-hdpi/screen.png differ
diff --git a/platforms/android/app/src/main/res/drawable-port-ldpi/screen.png b/platforms/android/app/src/main/res/drawable-port-ldpi/screen.png
new file mode 100644
index 0000000..80b314a
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-port-ldpi/screen.png differ
diff --git a/platforms/android/app/src/main/res/drawable-port-mdpi/screen.png b/platforms/android/app/src/main/res/drawable-port-mdpi/screen.png
new file mode 100644
index 0000000..f5181dc
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-port-mdpi/screen.png differ
diff --git a/platforms/android/app/src/main/res/drawable-port-xhdpi/screen.png b/platforms/android/app/src/main/res/drawable-port-xhdpi/screen.png
new file mode 100644
index 0000000..43df4e8
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-port-xhdpi/screen.png differ
diff --git a/platforms/android/app/src/main/res/drawable-port-xxhdpi/screen.png b/platforms/android/app/src/main/res/drawable-port-xxhdpi/screen.png
new file mode 100644
index 0000000..bba7ecc
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-port-xxhdpi/screen.png differ
diff --git a/platforms/android/app/src/main/res/drawable-port-xxxhdpi/screen.png b/platforms/android/app/src/main/res/drawable-port-xxxhdpi/screen.png
new file mode 100644
index 0000000..27e017a
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-port-xxxhdpi/screen.png differ
diff --git a/platforms/android/app/src/main/res/drawable-xhdpi/ic_action_next_item.png b/platforms/android/app/src/main/res/drawable-xhdpi/ic_action_next_item.png
new file mode 100644
index 0000000..5f30474
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-xhdpi/ic_action_next_item.png differ
diff --git a/platforms/android/app/src/main/res/drawable-xhdpi/ic_action_previous_item.png b/platforms/android/app/src/main/res/drawable-xhdpi/ic_action_previous_item.png
new file mode 100644
index 0000000..ed8ac91
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-xhdpi/ic_action_previous_item.png differ
diff --git a/platforms/android/app/src/main/res/drawable-xhdpi/ic_action_remove.png b/platforms/android/app/src/main/res/drawable-xhdpi/ic_action_remove.png
new file mode 100644
index 0000000..4cd0458
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-xhdpi/ic_action_remove.png differ
diff --git a/platforms/android/app/src/main/res/drawable-xxhdpi/ic_action_next_item.png b/platforms/android/app/src/main/res/drawable-xxhdpi/ic_action_next_item.png
new file mode 100644
index 0000000..51479d8
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-xxhdpi/ic_action_next_item.png differ
diff --git a/platforms/android/app/src/main/res/drawable-xxhdpi/ic_action_previous_item.png b/platforms/android/app/src/main/res/drawable-xxhdpi/ic_action_previous_item.png
new file mode 100644
index 0000000..bc8ff12
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-xxhdpi/ic_action_previous_item.png differ
diff --git a/platforms/android/app/src/main/res/drawable-xxhdpi/ic_action_remove.png b/platforms/android/app/src/main/res/drawable-xxhdpi/ic_action_remove.png
new file mode 100644
index 0000000..331c545
Binary files /dev/null and b/platforms/android/app/src/main/res/drawable-xxhdpi/ic_action_remove.png differ
diff --git a/platforms/android/app/src/main/res/mipmap-hdpi/icon.png b/platforms/android/app/src/main/res/mipmap-hdpi/icon.png
new file mode 100644
index 0000000..f1232b6
Binary files /dev/null and b/platforms/android/app/src/main/res/mipmap-hdpi/icon.png differ
diff --git a/platforms/android/app/src/main/res/mipmap-ldpi/icon.png b/platforms/android/app/src/main/res/mipmap-ldpi/icon.png
new file mode 100644
index 0000000..25c9f51
Binary files /dev/null and b/platforms/android/app/src/main/res/mipmap-ldpi/icon.png differ
diff --git a/platforms/android/app/src/main/res/mipmap-mdpi/icon.png b/platforms/android/app/src/main/res/mipmap-mdpi/icon.png
new file mode 100644
index 0000000..dae5103
Binary files /dev/null and b/platforms/android/app/src/main/res/mipmap-mdpi/icon.png differ
diff --git a/platforms/android/app/src/main/res/mipmap-xhdpi/icon.png b/platforms/android/app/src/main/res/mipmap-xhdpi/icon.png
new file mode 100644
index 0000000..f8a4e3a
Binary files /dev/null and b/platforms/android/app/src/main/res/mipmap-xhdpi/icon.png differ
diff --git a/platforms/android/app/src/main/res/mipmap-xxhdpi/icon.png b/platforms/android/app/src/main/res/mipmap-xxhdpi/icon.png
new file mode 100644
index 0000000..6edb68f
Binary files /dev/null and b/platforms/android/app/src/main/res/mipmap-xxhdpi/icon.png differ
diff --git a/platforms/android/app/src/main/res/mipmap-xxxhdpi/icon.png b/platforms/android/app/src/main/res/mipmap-xxxhdpi/icon.png
new file mode 100644
index 0000000..6a18693
Binary files /dev/null and b/platforms/android/app/src/main/res/mipmap-xxxhdpi/icon.png differ
diff --git a/platforms/android/app/src/main/res/values/strings.xml b/platforms/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..d177188
--- /dev/null
+++ b/platforms/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Goober
+ @string/app_name
+ @string/launcher_name
+
diff --git a/platforms/android/app/src/main/res/xml/config.xml b/platforms/android/app/src/main/res/xml/config.xml
new file mode 100644
index 0000000..c6801ce
--- /dev/null
+++ b/platforms/android/app/src/main/res/xml/config.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Goober
+ Goober, a mobile app for pnut.io
+ Morgan McMillian
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platforms/android/app/src/main/res/xml/provider_paths.xml b/platforms/android/app/src/main/res/xml/provider_paths.xml
new file mode 100644
index 0000000..3785fcc
--- /dev/null
+++ b/platforms/android/app/src/main/res/xml/provider_paths.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/platforms/android/build.gradle b/platforms/android/build.gradle
new file mode 100644
index 0000000..d982091
--- /dev/null
+++ b/platforms/android/build.gradle
@@ -0,0 +1,54 @@
+/* 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.
+*/
+
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ jcenter()
+ maven {
+ url "https://maven.google.com"
+ }
+ }
+ dependencies {
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ classpath 'com.android.tools.build:gradle:3.0.1'
+ }
+}
+
+allprojects {
+ repositories {
+ jcenter()
+ maven {
+ url "https://maven.google.com"
+ }
+ }
+ //This replaces project.properties w.r.t. build settings
+ project.ext {
+ defaultBuildToolsVersion="27.0.1" //String
+ defaultMinSdkVersion=19 //Integer - Minimum requirement is Android 4.4
+ defaultTargetSdkVersion=27 //Integer - We ALWAYS target the latest by default
+ defaultCompileSdkVersion=27 //Integer - We ALWAYS compile with the latest by default
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/platforms/android/cordova-android-support-gradle-release/goober_m-cordova-android-support-gradle-release.gradle b/platforms/android/cordova-android-support-gradle-release/goober_m-cordova-android-support-gradle-release.gradle
new file mode 100644
index 0000000..1b0bd72
--- /dev/null
+++ b/platforms/android/cordova-android-support-gradle-release/goober_m-cordova-android-support-gradle-release.gradle
@@ -0,0 +1,31 @@
+repositories{
+ // Google APIs are now hosted at Maven
+ maven {
+ url 'https://maven.google.com'
+ }
+}
+
+def PLUGIN_NAME = "cordova-android-support-gradle-release"
+
+// Fetch ANDROID_SUPPORT_VERSION var from properties.gradle
+apply from: PLUGIN_NAME + '/properties.gradle'
+
+// List of libs to search for.
+def LIBS = [
+ 'com.android.support'
+]
+
+def IGNORED = [
+ 'multidex',
+ 'multidex-instrumentation'
+]
+
+println("+-----------------------------------------------------------------");
+println("| " + PLUGIN_NAME + ": " + ANDROID_SUPPORT_VERSION);
+println("+-----------------------------------------------------------------");
+
+configurations.all {
+ resolutionStrategy.eachDependency { DependencyResolveDetails details ->
+ if (details.requested.group in LIBS && !(details.requested.name in IGNORED)) { details.useVersion ANDROID_SUPPORT_VERSION }
+ }
+}
diff --git a/platforms/android/cordova-android-support-gradle-release/properties.gradle b/platforms/android/cordova-android-support-gradle-release/properties.gradle
new file mode 100644
index 0000000..5d42a85
--- /dev/null
+++ b/platforms/android/cordova-android-support-gradle-release/properties.gradle
@@ -0,0 +1 @@
+ext {ANDROID_SUPPORT_VERSION = "27.+"}
\ No newline at end of file
diff --git a/platforms/android/cordova/Api.js b/platforms/android/cordova/Api.js
new file mode 100644
index 0000000..e97f538
--- /dev/null
+++ b/platforms/android/cordova/Api.js
@@ -0,0 +1,411 @@
+/**
+ 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.
+*/
+
+var path = require('path');
+var Q = require('q');
+
+var AndroidProject = require('./lib/AndroidProject');
+var AndroidStudio = require('./lib/AndroidStudio');
+var PluginManager = require('cordova-common').PluginManager;
+
+var CordovaLogger = require('cordova-common').CordovaLogger;
+var selfEvents = require('cordova-common').events;
+
+var PLATFORM = 'android';
+
+function setupEvents (externalEventEmitter) {
+ if (externalEventEmitter) {
+ // This will make the platform internal events visible outside
+ selfEvents.forwardEventsTo(externalEventEmitter);
+ return externalEventEmitter;
+ }
+
+ // There is no logger if external emitter is not present,
+ // so attach a console logger
+ CordovaLogger.get().subscribe(selfEvents);
+ return selfEvents;
+}
+
+/**
+ * Class, that acts as abstraction over particular platform. Encapsulates the
+ * platform's properties and methods.
+ *
+ * Platform that implements own PlatformApi instance _should implement all
+ * prototype methods_ of this class to be fully compatible with cordova-lib.
+ *
+ * The PlatformApi instance also should define the following field:
+ *
+ * * platform: String that defines a platform name.
+ */
+function Api (platform, platformRootDir, events) {
+ this.platform = PLATFORM;
+ this.root = path.resolve(__dirname, '..');
+ this.builder = 'gradle';
+
+ setupEvents(events);
+
+ var self = this;
+
+ this.locations = {
+ root: self.root,
+ www: path.join(self.root, 'assets/www'),
+ res: path.join(self.root, 'res'),
+ platformWww: path.join(self.root, 'platform_www'),
+ configXml: path.join(self.root, 'res/xml/config.xml'),
+ defaultConfigXml: path.join(self.root, 'cordova/defaults.xml'),
+ strings: path.join(self.root, 'res/values/strings.xml'),
+ manifest: path.join(self.root, 'AndroidManifest.xml'),
+ build: path.join(self.root, 'build'),
+ javaSrc: path.join(self.root, 'src'),
+ // NOTE: Due to platformApi spec we need to return relative paths here
+ cordovaJs: 'bin/templates/project/assets/www/cordova.js',
+ cordovaJsSrc: 'cordova-js-src'
+ };
+
+ // XXX Override some locations for Android Studio projects
+ if (AndroidStudio.isAndroidStudioProject(self.root) === true) {
+ selfEvents.emit('log', 'Android Studio project detected');
+ this.builder = 'studio';
+ this.android_studio = true;
+ this.locations.configXml = path.join(self.root, 'app/src/main/res/xml/config.xml');
+ this.locations.strings = path.join(self.root, 'app/src/main/res/values/strings.xml');
+ this.locations.manifest = path.join(self.root, 'app/src/main/AndroidManifest.xml');
+ // We could have Java Source, we could have other languages
+ this.locations.javaSrc = path.join(self.root, 'app/src/main/java/');
+ this.locations.www = path.join(self.root, 'app/src/main/assets/www');
+ this.locations.res = path.join(self.root, 'app/src/main/res');
+ }
+}
+
+/**
+ * Installs platform to specified directory and creates a platform project.
+ *
+ * @param {String} destination Destination directory, where insatll platform to
+ * @param {ConfigParser} [config] ConfgiParser instance, used to retrieve
+ * project creation options, such as package id and project name.
+ * @param {Object} [options] An options object. The most common options are:
+ * @param {String} [options.customTemplate] A path to custom template, that
+ * should override the default one from platform.
+ * @param {Boolean} [options.link] Flag that indicates that platform's
+ * sources will be linked to installed platform instead of copying.
+ * @param {EventEmitter} [events] An EventEmitter instance that will be used for
+ * logging purposes. If no EventEmitter provided, all events will be logged to
+ * console
+ *
+ * @return {Promise} Promise either fulfilled with PlatformApi
+ * instance or rejected with CordovaError.
+ */
+Api.createPlatform = function (destination, config, options, events) {
+ events = setupEvents(events);
+ var result;
+ try {
+ result = require('../../lib/create').create(destination, config, options, events).then(function (destination) {
+ var PlatformApi = require(path.resolve(destination, 'cordova/Api'));
+ return new PlatformApi(PLATFORM, destination, events);
+ });
+ } catch (e) {
+ events.emit('error', 'createPlatform is not callable from the android project API.');
+ throw (e);
+ }
+ return result;
+};
+
+/**
+ * Updates already installed platform.
+ *
+ * @param {String} destination Destination directory, where platform installed
+ * @param {Object} [options] An options object. The most common options are:
+ * @param {String} [options.customTemplate] A path to custom template, that
+ * should override the default one from platform.
+ * @param {Boolean} [options.link] Flag that indicates that platform's
+ * sources will be linked to installed platform instead of copying.
+ * @param {EventEmitter} [events] An EventEmitter instance that will be used for
+ * logging purposes. If no EventEmitter provided, all events will be logged to
+ * console
+ *
+ * @return {Promise} Promise either fulfilled with PlatformApi
+ * instance or rejected with CordovaError.
+ */
+Api.updatePlatform = function (destination, options, events) {
+ events = setupEvents(events);
+ var result;
+ try {
+ result = require('../../lib/create').update(destination, options, events).then(function (destination) {
+ var PlatformApi = require(path.resolve(destination, 'cordova/Api'));
+ return new PlatformApi('android', destination, events);
+ });
+ } catch (e) {
+ events.emit('error', 'updatePlatform is not callable from the android project API, you will need to do this manually.');
+ throw (e);
+ }
+ return result;
+};
+
+/**
+ * Gets a CordovaPlatform object, that represents the platform structure.
+ *
+ * @return {CordovaPlatform} A structure that contains the description of
+ * platform's file structure and other properties of platform.
+ */
+Api.prototype.getPlatformInfo = function () {
+ var result = {};
+ result.locations = this.locations;
+ result.root = this.root;
+ result.name = this.platform;
+ result.version = require('./version');
+ result.projectConfig = this._config;
+
+ return result;
+};
+
+/**
+ * Updates installed platform with provided www assets and new app
+ * configuration. This method is required for CLI workflow and will be called
+ * each time before build, so the changes, made to app configuration and www
+ * code, will be applied to platform.
+ *
+ * @param {CordovaProject} cordovaProject A CordovaProject instance, that defines a
+ * project structure and configuration, that should be applied to platform
+ * (contains project's www location and ConfigParser instance for project's
+ * config).
+ *
+ * @return {Promise} Return a promise either fulfilled, or rejected with
+ * CordovaError instance.
+ */
+Api.prototype.prepare = function (cordovaProject, prepareOptions) {
+ return require('./lib/prepare').prepare.call(this, cordovaProject, prepareOptions);
+};
+
+/**
+ * Installs a new plugin into platform. This method only copies non-www files
+ * (sources, libs, etc.) to platform. It also doesn't resolves the
+ * dependencies of plugin. Both of handling of www files, such as assets and
+ * js-files and resolving dependencies are the responsibility of caller.
+ *
+ * @param {PluginInfo} plugin A PluginInfo instance that represents plugin
+ * that will be installed.
+ * @param {Object} installOptions An options object. Possible options below:
+ * @param {Boolean} installOptions.link: Flag that specifies that plugin
+ * sources will be symlinked to app's directory instead of copying (if
+ * possible).
+ * @param {Object} installOptions.variables An object that represents
+ * variables that will be used to install plugin. See more details on plugin
+ * variables in documentation:
+ * https://cordova.apache.org/docs/en/4.0.0/plugin_ref_spec.md.html
+ *
+ * @return {Promise} Return a promise either fulfilled, or rejected with
+ * CordovaError instance.
+ */
+Api.prototype.addPlugin = function (plugin, installOptions) {
+ var project = AndroidProject.getProjectFile(this.root);
+ var self = this;
+
+ installOptions = installOptions || {};
+ installOptions.variables = installOptions.variables || {};
+ // Add PACKAGE_NAME variable into vars
+ if (!installOptions.variables.PACKAGE_NAME) {
+ installOptions.variables.PACKAGE_NAME = project.getPackageName();
+ }
+
+ if (this.android_studio === true) {
+ installOptions.android_studio = true;
+ }
+
+ return Q().then(function () {
+ // CB-11964: Do a clean when installing the plugin code to get around
+ // the Gradle bug introduced by the Android Gradle Plugin Version 2.2
+ // TODO: Delete when the next version of Android Gradle plugin comes out
+ // Since clean doesn't just clean the build, it also wipes out www, we need
+ // to pass additional options.
+
+ // Do some basic argument parsing
+ var opts = {};
+
+ // Skip cleaning prepared files when not invoking via cordova CLI.
+ opts.noPrepare = true;
+
+ if (!AndroidStudio.isAndroidStudioProject(self.root) && !project.isClean()) {
+ return self.clean(opts);
+ }
+ }).then(function () {
+ return PluginManager.get(self.platform, self.locations, project).addPlugin(plugin, installOptions);
+ }).then(function () {
+ if (plugin.getFrameworks(this.platform).length === 0) return;
+ selfEvents.emit('verbose', 'Updating build files since android plugin contained ');
+ // This should pick the correct builder, not just get gradle
+ require('./lib/builders/builders').getBuilder(this.builder).prepBuildFiles();
+ }.bind(this))
+ // CB-11022 Return truthy value to prevent running prepare after
+ .thenResolve(true);
+};
+
+/**
+ * Removes an installed plugin from platform.
+ *
+ * Since method accepts PluginInfo instance as input parameter instead of plugin
+ * id, caller shoud take care of managing/storing PluginInfo instances for
+ * future uninstalls.
+ *
+ * @param {PluginInfo} plugin A PluginInfo instance that represents plugin
+ * that will be installed.
+ *
+ * @return {Promise} Return a promise either fulfilled, or rejected with
+ * CordovaError instance.
+ */
+Api.prototype.removePlugin = function (plugin, uninstallOptions) {
+ var project = AndroidProject.getProjectFile(this.root);
+
+ if (uninstallOptions && uninstallOptions.usePlatformWww === true && this.android_studio === true) {
+ uninstallOptions.usePlatformWww = false;
+ uninstallOptions.android_studio = true;
+ }
+
+ return PluginManager.get(this.platform, this.locations, project)
+ .removePlugin(plugin, uninstallOptions)
+ .then(function () {
+ if (plugin.getFrameworks(this.platform).length === 0) return;
+
+ selfEvents.emit('verbose', 'Updating build files since android plugin contained ');
+ require('./lib/builders/builders').getBuilder(this.builder).prepBuildFiles();
+ }.bind(this))
+ // CB-11022 Return truthy value to prevent running prepare after
+ .thenResolve(true);
+};
+
+/**
+ * Builds an application package for current platform.
+ *
+ * @param {Object} buildOptions A build options. This object's structure is
+ * highly depends on platform's specific. The most common options are:
+ * @param {Boolean} buildOptions.debug Indicates that packages should be
+ * built with debug configuration. This is set to true by default unless the
+ * 'release' option is not specified.
+ * @param {Boolean} buildOptions.release Indicates that packages should be
+ * built with release configuration. If not set to true, debug configuration
+ * will be used.
+ * @param {Boolean} buildOptions.device Specifies that built app is intended
+ * to run on device
+ * @param {Boolean} buildOptions.emulator: Specifies that built app is
+ * intended to run on emulator
+ * @param {String} buildOptions.target Specifies the device id that will be
+ * used to run built application.
+ * @param {Boolean} buildOptions.nobuild Indicates that this should be a
+ * dry-run call, so no build artifacts will be produced.
+ * @param {String[]} buildOptions.archs Specifies chip architectures which
+ * app packages should be built for. List of valid architectures is depends on
+ * platform.
+ * @param {String} buildOptions.buildConfig The path to build configuration
+ * file. The format of this file is depends on platform.
+ * @param {String[]} buildOptions.argv Raw array of command-line arguments,
+ * passed to `build` command. The purpose of this property is to pass a
+ * platform-specific arguments, and eventually let platform define own
+ * arguments processing logic.
+ *
+ * @return {Promise} A promise either fulfilled with an array of build
+ * artifacts (application packages) if package was built successfully,
+ * or rejected with CordovaError. The resultant build artifact objects is not
+ * strictly typed and may conatin arbitrary set of fields as in sample below.
+ *
+ * {
+ * architecture: 'x86',
+ * buildType: 'debug',
+ * path: '/path/to/build',
+ * type: 'app'
+ * }
+ *
+ * The return value in most cases will contain only one item but in some cases
+ * there could be multiple items in output array, e.g. when multiple
+ * arhcitectures is specified.
+ */
+Api.prototype.build = function (buildOptions) {
+ var self = this;
+ if (this.android_studio) {
+ buildOptions.studio = true;
+ }
+ return require('./lib/check_reqs').run().then(function () {
+ return require('./lib/build').run.call(self, buildOptions);
+ }).then(function (buildResults) {
+ // Cast build result to array of build artifacts
+ return buildResults.apkPaths.map(function (apkPath) {
+ return {
+ buildType: buildResults.buildType,
+ buildMethod: buildResults.buildMethod,
+ path: apkPath,
+ type: 'apk'
+ };
+ });
+ });
+};
+
+/**
+ * Builds an application package for current platform and runs it on
+ * specified/default device. If no 'device'/'emulator'/'target' options are
+ * specified, then tries to run app on default device if connected, otherwise
+ * runs the app on emulator.
+ *
+ * @param {Object} runOptions An options object. The structure is the same
+ * as for build options.
+ *
+ * @return {Promise} A promise either fulfilled if package was built and ran
+ * successfully, or rejected with CordovaError.
+ */
+Api.prototype.run = function (runOptions) {
+ var self = this;
+ return require('./lib/check_reqs').run().then(function () {
+ return require('./lib/run').run.call(self, runOptions);
+ });
+};
+
+/**
+ * Cleans out the build artifacts from platform's directory, and also
+ * cleans out the platform www directory if called without options specified.
+ *
+ * @return {Promise} Return a promise either fulfilled, or rejected with
+ * CordovaError.
+ */
+Api.prototype.clean = function (cleanOptions) {
+ var self = this;
+ if (this.android_studio) {
+ // This will lint, checking for null won't
+ if (typeof cleanOptions === 'undefined') {
+ cleanOptions = {};
+ }
+ cleanOptions.studio = true;
+ }
+
+ return require('./lib/check_reqs').run().then(function () {
+ return require('./lib/build').runClean.call(self, cleanOptions);
+ }).then(function () {
+ return require('./lib/prepare').clean.call(self, cleanOptions);
+ });
+};
+
+/**
+ * Performs a requirements check for current platform. Each platform defines its
+ * own set of requirements, which should be resolved before platform can be
+ * built successfully.
+ *
+ * @return {Promise} Promise, resolved with set of Requirement
+ * objects for current platform.
+ */
+Api.prototype.requirements = function () {
+ return require('./lib/check_reqs').check_all();
+};
+
+module.exports = Api;
diff --git a/platforms/android/cordova/android_sdk_version b/platforms/android/cordova/android_sdk_version
new file mode 100755
index 0000000..34ed28f
--- /dev/null
+++ b/platforms/android/cordova/android_sdk_version
@@ -0,0 +1,29 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+var android_sdk = require('./lib/android_sdk');
+
+android_sdk.print_newest_available_sdk_target().done(null, function(err) {
+ console.error(err);
+ process.exit(2);
+});
+
+
diff --git a/platforms/android/cordova/android_sdk_version.bat b/platforms/android/cordova/android_sdk_version.bat
new file mode 100644
index 0000000..a6bc104
--- /dev/null
+++ b/platforms/android/cordova/android_sdk_version.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0android_sdk_version"
+IF EXIST %script_path% (
+ node %script_path% %*
+) ELSE (
+ ECHO.
+ ECHO ERROR: Could not find 'android_sdk_version' script in 'bin' folder, aborting...>&2
+ EXIT /B 1
+)
diff --git a/platforms/android/cordova/build b/platforms/android/cordova/build
new file mode 100755
index 0000000..222e84a
--- /dev/null
+++ b/platforms/android/cordova/build
@@ -0,0 +1,50 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+var args = process.argv;
+var Api = require('./Api');
+var nopt = require('nopt');
+var path = require('path');
+
+// Support basic help commands
+if(['--help', '/?', '-h', 'help', '-help', '/help'].indexOf(process.argv[2]) >= 0)
+ require('./lib/build').help();
+
+// Do some basic argument parsing
+var buildOpts = nopt({
+ 'verbose' : Boolean,
+ 'silent' : Boolean,
+ 'debug' : Boolean,
+ 'release' : Boolean,
+ 'nobuild': Boolean,
+ 'buildConfig' : path
+}, { 'd' : '--verbose' });
+
+// Make buildOptions compatible with PlatformApi build method spec
+buildOpts.argv = buildOpts.argv.original;
+
+require('./loggingHelper').adjustLoggerLevel(buildOpts);
+
+new Api().build(buildOpts)
+.catch(function(err) {
+ console.error(err.stack);
+ process.exit(2);
+});
diff --git a/platforms/android/cordova/build.bat b/platforms/android/cordova/build.bat
new file mode 100644
index 0000000..46e966a
--- /dev/null
+++ b/platforms/android/cordova/build.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0build"
+IF EXIST %script_path% (
+ node %script_path% %*
+) ELSE (
+ ECHO.
+ ECHO ERROR: Could not find 'build' script in 'cordova' folder, aborting...>&2
+ EXIT /B 1
+)
\ No newline at end of file
diff --git a/platforms/android/cordova/check_reqs b/platforms/android/cordova/check_reqs
new file mode 100755
index 0000000..372a383
--- /dev/null
+++ b/platforms/android/cordova/check_reqs
@@ -0,0 +1,31 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+var check_reqs = require('./lib/check_reqs');
+
+check_reqs.run().done(
+ function success() {
+ console.log('Looks like your environment fully supports cordova-android development!');
+ }, function fail(err) {
+ console.log(err);
+ process.exit(2);
+ }
+);
diff --git a/platforms/android/cordova/check_reqs.bat b/platforms/android/cordova/check_reqs.bat
new file mode 100644
index 0000000..846dfa1
--- /dev/null
+++ b/platforms/android/cordova/check_reqs.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0check_reqs"
+IF EXIST %script_path% (
+ node %script_path% %*
+) ELSE (
+ ECHO.
+ ECHO ERROR: Could not find 'check_reqs' script in 'bin' folder, aborting...>&2
+ EXIT /B 1
+)
diff --git a/platforms/android/cordova/clean b/platforms/android/cordova/clean
new file mode 100755
index 0000000..22065cc
--- /dev/null
+++ b/platforms/android/cordova/clean
@@ -0,0 +1,51 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+var Api = require('./Api');
+var path = require('path');
+var nopt = require('nopt');
+
+// Support basic help commands
+if(['--help', '/?', '-h', 'help', '-help', '/help'].indexOf(process.argv[2]) >= 0) {
+ console.log('Usage: ' + path.relative(process.cwd(), process.argv[1]));
+ console.log('Cleans the project directory.');
+ process.exit(0);
+}
+
+// Do some basic argument parsing
+var opts = nopt({
+ 'verbose' : Boolean,
+ 'silent' : Boolean
+}, { 'd' : '--verbose' });
+
+// Make buildOptions compatible with PlatformApi clean method spec
+opts.argv = opts.argv.original;
+
+// Skip cleaning prepared files when not invoking via cordova CLI.
+opts.noPrepare = true;
+
+require('./loggingHelper').adjustLoggerLevel(opts);
+
+new Api().clean(opts)
+.catch(function(err) {
+ console.error(err.stack);
+ process.exit(2);
+});
diff --git a/platforms/android/cordova/clean.bat b/platforms/android/cordova/clean.bat
new file mode 100644
index 0000000..445ef6e
--- /dev/null
+++ b/platforms/android/cordova/clean.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0clean"
+IF EXIST %script_path% (
+ node %script_path% %*
+) ELSE (
+ ECHO.
+ ECHO ERROR: Could not find 'clean' script in 'cordova' folder, aborting...>&2
+ EXIT /B 1
+)
\ No newline at end of file
diff --git a/platforms/android/cordova/defaults.xml b/platforms/android/cordova/defaults.xml
new file mode 100644
index 0000000..5286ab9
--- /dev/null
+++ b/platforms/android/cordova/defaults.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
diff --git a/platforms/android/cordova/lib/Adb.js b/platforms/android/cordova/lib/Adb.js
new file mode 100644
index 0000000..038c67c
--- /dev/null
+++ b/platforms/android/cordova/lib/Adb.js
@@ -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.
+*/
+
+var Q = require('q');
+var os = require('os');
+var events = require('cordova-common').events;
+var spawn = require('cordova-common').superspawn.spawn;
+var CordovaError = require('cordova-common').CordovaError;
+
+var Adb = {};
+
+function isDevice (line) {
+ return line.match(/\w+\tdevice/) && !line.match(/emulator/);
+}
+
+function isEmulator (line) {
+ return line.match(/device/) && line.match(/emulator/);
+}
+
+/**
+ * Lists available/connected devices and emulators
+ *
+ * @param {Object} opts Various options
+ * @param {Boolean} opts.emulators Specifies whether this method returns
+ * emulators only
+ *
+ * @return {Promise} list of available/connected
+ * devices/emulators
+ */
+Adb.devices = function (opts) {
+ return spawn('adb', ['devices'], {cwd: os.tmpdir()}).then(function (output) {
+ return output.split('\n').filter(function (line) {
+ // Filter out either real devices or emulators, depending on options
+ return (line && opts && opts.emulators) ? isEmulator(line) : isDevice(line);
+ }).map(function (line) {
+ return line.replace(/\tdevice/, '').replace('\r', '');
+ });
+ });
+};
+
+Adb.install = function (target, packagePath, opts) {
+ events.emit('verbose', 'Installing apk ' + packagePath + ' on target ' + target + '...');
+ var args = ['-s', target, 'install'];
+ if (opts && opts.replace) args.push('-r');
+ return spawn('adb', args.concat(packagePath), {cwd: os.tmpdir()}).then(function (output) {
+ // 'adb install' seems to always returns no error, even if installation fails
+ // so we catching output to detect installation failure
+ if (output.match(/Failure/)) {
+ if (output.match(/INSTALL_PARSE_FAILED_NO_CERTIFICATES/)) {
+ output += '\n\n' + 'Sign the build using \'-- --keystore\' or \'--buildConfig\'' +
+ ' or sign and deploy the unsigned apk manually using Android tools.';
+ } else if (output.match(/INSTALL_FAILED_VERSION_DOWNGRADE/)) {
+ output += '\n\n' + 'You\'re trying to install apk with a lower versionCode that is already installed.' +
+ '\nEither uninstall an app or increment the versionCode.';
+ }
+
+ return Q.reject(new CordovaError('Failed to install apk to device: ' + output));
+ }
+ });
+};
+
+Adb.uninstall = function (target, packageId) {
+ events.emit('verbose', 'Uninstalling package ' + packageId + ' from target ' + target + '...');
+ return spawn('adb', ['-s', target, 'uninstall', packageId], {cwd: os.tmpdir()});
+};
+
+Adb.shell = function (target, shellCommand) {
+ events.emit('verbose', 'Running adb shell command "' + shellCommand + '" on target ' + target + '...');
+ var args = ['-s', target, 'shell'];
+ shellCommand = shellCommand.split(/\s+/);
+ return spawn('adb', args.concat(shellCommand), {cwd: os.tmpdir()}).catch(function (output) {
+ return Q.reject(new CordovaError('Failed to execute shell command "' +
+ shellCommand + '"" on device: ' + output));
+ });
+};
+
+Adb.start = function (target, activityName) {
+ events.emit('verbose', 'Starting application "' + activityName + '" on target ' + target + '...');
+ return Adb.shell(target, 'am start -W -a android.intent.action.MAIN -n' + activityName).catch(function (output) {
+ return Q.reject(new CordovaError('Failed to start application "' +
+ activityName + '"" on device: ' + output));
+ });
+};
+
+module.exports = Adb;
diff --git a/platforms/android/cordova/lib/AndroidManifest.js b/platforms/android/cordova/lib/AndroidManifest.js
new file mode 100644
index 0000000..5b7077a
--- /dev/null
+++ b/platforms/android/cordova/lib/AndroidManifest.js
@@ -0,0 +1,160 @@
+/**
+ 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.
+*/
+
+var fs = require('fs');
+var et = require('elementtree');
+var xml = require('cordova-common').xmlHelpers;
+
+var DEFAULT_ORIENTATION = 'default';
+
+/** Wraps an AndroidManifest file */
+function AndroidManifest (path) {
+ this.path = path;
+ this.doc = xml.parseElementtreeSync(path);
+ if (this.doc.getroot().tag !== 'manifest') {
+ throw new Error('AndroidManifest at ' + path + ' has incorrect root node name (expected "manifest")');
+ }
+}
+
+AndroidManifest.prototype.getVersionName = function () {
+ return this.doc.getroot().attrib['android:versionName'];
+};
+
+AndroidManifest.prototype.setVersionName = function (versionName) {
+ this.doc.getroot().attrib['android:versionName'] = versionName;
+ return this;
+};
+
+AndroidManifest.prototype.getVersionCode = function () {
+ return this.doc.getroot().attrib['android:versionCode'];
+};
+
+AndroidManifest.prototype.setVersionCode = function (versionCode) {
+ this.doc.getroot().attrib['android:versionCode'] = versionCode;
+ return this;
+};
+
+AndroidManifest.prototype.getPackageId = function () {
+ /* jshint -W069 */
+ return this.doc.getroot().attrib['package'];
+ /* jshint +W069 */
+};
+
+AndroidManifest.prototype.setPackageId = function (pkgId) {
+ /* jshint -W069 */
+ this.doc.getroot().attrib['package'] = pkgId;
+ /* jshint +W069 */
+ return this;
+};
+
+AndroidManifest.prototype.getActivity = function () {
+ var activity = this.doc.getroot().find('./application/activity');
+ return {
+ getName: function () {
+ return activity.attrib['android:name'];
+ },
+ setName: function (name) {
+ if (!name) {
+ delete activity.attrib['android:name'];
+ } else {
+ activity.attrib['android:name'] = name;
+ }
+ return this;
+ },
+ getOrientation: function () {
+ return activity.attrib['android:screenOrientation'];
+ },
+ setOrientation: function (orientation) {
+ if (!orientation || orientation.toLowerCase() === DEFAULT_ORIENTATION) {
+ delete activity.attrib['android:screenOrientation'];
+ } else {
+ activity.attrib['android:screenOrientation'] = orientation;
+ }
+ return this;
+ },
+ getLaunchMode: function () {
+ return activity.attrib['android:launchMode'];
+ },
+ setLaunchMode: function (launchMode) {
+ if (!launchMode) {
+ delete activity.attrib['android:launchMode'];
+ } else {
+ activity.attrib['android:launchMode'] = launchMode;
+ }
+ return this;
+ }
+ };
+};
+
+['minSdkVersion', 'maxSdkVersion', 'targetSdkVersion'].forEach(function (sdkPrefName) {
+ // Copy variable reference to avoid closure issues
+ var prefName = sdkPrefName;
+
+ AndroidManifest.prototype['get' + capitalize(prefName)] = function () {
+ var usesSdk = this.doc.getroot().find('./uses-sdk');
+ return usesSdk && usesSdk.attrib['android:' + prefName];
+ };
+
+ AndroidManifest.prototype['set' + capitalize(prefName)] = function (prefValue) {
+ var usesSdk = this.doc.getroot().find('./uses-sdk');
+
+ if (!usesSdk && prefValue) { // if there is no required uses-sdk element, we should create it first
+ usesSdk = new et.Element('uses-sdk');
+ this.doc.getroot().append(usesSdk);
+ }
+
+ if (prefValue) {
+ usesSdk.attrib['android:' + prefName] = prefValue;
+ }
+
+ return this;
+ };
+});
+
+AndroidManifest.prototype.getDebuggable = function () {
+ return this.doc.getroot().find('./application').attrib['android:debuggable'] === 'true';
+};
+
+AndroidManifest.prototype.setDebuggable = function (value) {
+ var application = this.doc.getroot().find('./application');
+ if (value) {
+ application.attrib['android:debuggable'] = 'true';
+ } else {
+ // The default value is "false", so we can remove attribute at all.
+ delete application.attrib['android:debuggable'];
+ }
+ return this;
+};
+
+/**
+ * Writes manifest to disk syncronously. If filename is specified, then manifest
+ * will be written to that file
+ *
+ * @param {String} [destPath] File to write manifest to. If omitted,
+ * manifest will be written to file it has been read from.
+ */
+AndroidManifest.prototype.write = function (destPath) {
+ fs.writeFileSync(destPath || this.path, this.doc.write({indent: 4}), 'utf-8');
+};
+
+module.exports = AndroidManifest;
+
+function capitalize (str) {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+}
diff --git a/platforms/android/cordova/lib/AndroidProject.js b/platforms/android/cordova/lib/AndroidProject.js
new file mode 100644
index 0000000..bf55cad
--- /dev/null
+++ b/platforms/android/cordova/lib/AndroidProject.js
@@ -0,0 +1,209 @@
+/**
+ 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.
+*/
+
+var fs = require('fs');
+var path = require('path');
+var properties_parser = require('properties-parser');
+var AndroidManifest = require('./AndroidManifest');
+var AndroidStudio = require('./AndroidStudio');
+var pluginHandlers = require('./pluginHandlers');
+
+var projectFileCache = {};
+
+function addToPropertyList (projectProperties, key, value) {
+ var i = 1;
+ while (projectProperties.get(key + '.' + i)) { i++; }
+
+ projectProperties.set(key + '.' + i, value);
+ projectProperties.dirty = true;
+}
+
+function removeFromPropertyList (projectProperties, key, value) {
+ var i = 1;
+ var currentValue;
+ while ((currentValue = projectProperties.get(key + '.' + i))) {
+ if (currentValue === value) {
+ while ((currentValue = projectProperties.get(key + '.' + (i + 1)))) {
+ projectProperties.set(key + '.' + i, currentValue);
+ i++;
+ }
+ projectProperties.set(key + '.' + i);
+ break;
+ }
+ i++;
+ }
+ projectProperties.dirty = true;
+}
+
+function getRelativeLibraryPath (parentDir, subDir) {
+ var libraryPath = path.relative(parentDir, subDir);
+ return (path.sep === '\\') ? libraryPath.replace(/\\/g, '/') : libraryPath;
+}
+
+function AndroidProject (projectDir) {
+ this._propertiesEditors = {};
+ this._subProjectDirs = {};
+ this._dirty = false;
+ this.projectDir = projectDir;
+ this.platformWww = path.join(this.projectDir, 'platform_www');
+ this.www = path.join(this.projectDir, 'assets/www');
+ if (AndroidStudio.isAndroidStudioProject(projectDir) === true) {
+ this.www = path.join(this.projectDir, 'app/src/main/assets/www');
+ }
+}
+
+AndroidProject.getProjectFile = function (projectDir) {
+ if (!projectFileCache[projectDir]) {
+ projectFileCache[projectDir] = new AndroidProject(projectDir);
+ }
+
+ return projectFileCache[projectDir];
+};
+
+AndroidProject.purgeCache = function (projectDir) {
+ if (projectDir) {
+ delete projectFileCache[projectDir];
+ } else {
+ projectFileCache = {};
+ }
+};
+
+/**
+ * Reads the package name out of the Android Manifest file
+ *
+ * @param {String} projectDir The absolute path to the directory containing the project
+ *
+ * @return {String} The name of the package
+ */
+AndroidProject.prototype.getPackageName = function () {
+ var manifestPath = path.join(this.projectDir, 'AndroidManifest.xml');
+ if (AndroidStudio.isAndroidStudioProject(this.projectDir) === true) {
+ manifestPath = path.join(this.projectDir, 'app/src/main/AndroidManifest.xml');
+ }
+ return new AndroidManifest(manifestPath).getPackageId();
+};
+
+AndroidProject.prototype.getCustomSubprojectRelativeDir = function (plugin_id, src) {
+ // All custom subprojects are prefixed with the last portion of the package id.
+ // This is to avoid collisions when opening multiple projects in Eclipse that have subprojects with the same name.
+ var packageName = this.getPackageName();
+ var lastDotIndex = packageName.lastIndexOf('.');
+ var prefix = packageName.substring(lastDotIndex + 1);
+ var subRelativeDir = path.join(plugin_id, prefix + '-' + path.basename(src));
+ return subRelativeDir;
+};
+
+AndroidProject.prototype.addSubProject = function (parentDir, subDir) {
+ var parentProjectFile = path.resolve(parentDir, 'project.properties');
+ var subProjectFile = path.resolve(subDir, 'project.properties');
+ var parentProperties = this._getPropertiesFile(parentProjectFile);
+ // TODO: Setting the target needs to happen only for pre-3.7.0 projects
+ if (fs.existsSync(subProjectFile)) {
+ var subProperties = this._getPropertiesFile(subProjectFile);
+ subProperties.set('target', parentProperties.get('target'));
+ subProperties.dirty = true;
+ this._subProjectDirs[subDir] = true;
+ }
+ addToPropertyList(parentProperties, 'android.library.reference', getRelativeLibraryPath(parentDir, subDir));
+
+ this._dirty = true;
+};
+
+AndroidProject.prototype.removeSubProject = function (parentDir, subDir) {
+ var parentProjectFile = path.resolve(parentDir, 'project.properties');
+ var parentProperties = this._getPropertiesFile(parentProjectFile);
+ removeFromPropertyList(parentProperties, 'android.library.reference', getRelativeLibraryPath(parentDir, subDir));
+ delete this._subProjectDirs[subDir];
+ this._dirty = true;
+};
+
+AndroidProject.prototype.addGradleReference = function (parentDir, subDir) {
+ var parentProjectFile = path.resolve(parentDir, 'project.properties');
+ var parentProperties = this._getPropertiesFile(parentProjectFile);
+ addToPropertyList(parentProperties, 'cordova.gradle.include', getRelativeLibraryPath(parentDir, subDir));
+ this._dirty = true;
+};
+
+AndroidProject.prototype.removeGradleReference = function (parentDir, subDir) {
+ var parentProjectFile = path.resolve(parentDir, 'project.properties');
+ var parentProperties = this._getPropertiesFile(parentProjectFile);
+ removeFromPropertyList(parentProperties, 'cordova.gradle.include', getRelativeLibraryPath(parentDir, subDir));
+ this._dirty = true;
+};
+
+AndroidProject.prototype.addSystemLibrary = function (parentDir, value) {
+ var parentProjectFile = path.resolve(parentDir, 'project.properties');
+ var parentProperties = this._getPropertiesFile(parentProjectFile);
+ addToPropertyList(parentProperties, 'cordova.system.library', value);
+ this._dirty = true;
+};
+
+AndroidProject.prototype.removeSystemLibrary = function (parentDir, value) {
+ var parentProjectFile = path.resolve(parentDir, 'project.properties');
+ var parentProperties = this._getPropertiesFile(parentProjectFile);
+ removeFromPropertyList(parentProperties, 'cordova.system.library', value);
+ this._dirty = true;
+};
+
+AndroidProject.prototype.write = function () {
+ if (!this._dirty) {
+ return;
+ }
+ this._dirty = false;
+
+ for (var filename in this._propertiesEditors) {
+ var editor = this._propertiesEditors[filename];
+ if (editor.dirty) {
+ fs.writeFileSync(filename, editor.toString());
+ editor.dirty = false;
+ }
+ }
+};
+
+AndroidProject.prototype._getPropertiesFile = function (filename) {
+ if (!this._propertiesEditors[filename]) {
+ if (fs.existsSync(filename)) {
+ this._propertiesEditors[filename] = properties_parser.createEditor(filename);
+ } else {
+ this._propertiesEditors[filename] = properties_parser.createEditor();
+ }
+ }
+
+ return this._propertiesEditors[filename];
+};
+
+AndroidProject.prototype.getInstaller = function (type) {
+ return pluginHandlers.getInstaller(type);
+};
+
+AndroidProject.prototype.getUninstaller = function (type) {
+ return pluginHandlers.getUninstaller(type);
+};
+
+/*
+ * This checks if an Android project is clean or has old build artifacts
+ */
+
+AndroidProject.prototype.isClean = function () {
+ var build_path = path.join(this.projectDir, 'build');
+ // If the build directory doesn't exist, it's clean
+ return !(fs.existsSync(build_path));
+};
+
+module.exports = AndroidProject;
diff --git a/platforms/android/cordova/lib/AndroidStudio.js b/platforms/android/cordova/lib/AndroidStudio.js
new file mode 100644
index 0000000..fbcb926
--- /dev/null
+++ b/platforms/android/cordova/lib/AndroidStudio.js
@@ -0,0 +1,42 @@
+/*
+ * This is a simple routine that checks if project is an Android Studio Project
+ *
+ * @param {String} root Root folder of the project
+ */
+
+/* jshint esnext: false */
+
+var path = require('path');
+var fs = require('fs');
+var CordovaError = require('cordova-common').CordovaError;
+
+module.exports.isAndroidStudioProject = function isAndroidStudioProject (root) {
+ var eclipseFiles = ['AndroidManifest.xml', 'libs', 'res'];
+ var androidStudioFiles = ['app', 'app/src/main'];
+
+ // assume it is an AS project and not an Eclipse project
+ var isEclipse = false;
+ var isAS = true;
+
+ if (!fs.existsSync(root)) {
+ throw new CordovaError('AndroidStudio.js:inAndroidStudioProject root does not exist: ' + root);
+ }
+
+ // if any of the following exists, then we are not an ASProj
+ eclipseFiles.forEach(function (file) {
+ if (fs.existsSync(path.join(root, file))) {
+ isEclipse = true;
+ }
+ });
+
+ // if it is NOT an eclipse project, check that all required files exist
+ if (!isEclipse) {
+ androidStudioFiles.forEach(function (file) {
+ if (!fs.existsSync(path.join(root, file))) {
+ console.log('missing file :: ' + file);
+ isAS = false;
+ }
+ });
+ }
+ return (!isEclipse && isAS);
+};
diff --git a/platforms/android/cordova/lib/android_sdk.js b/platforms/android/cordova/lib/android_sdk.js
new file mode 100755
index 0000000..148f9f3
--- /dev/null
+++ b/platforms/android/cordova/lib/android_sdk.js
@@ -0,0 +1,102 @@
+/*
+ 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.
+*/
+
+var Q = require('q');
+var superspawn = require('cordova-common').superspawn;
+
+var suffix_number_regex = /(\d+)$/;
+// Used for sorting Android targets, example strings to sort:
+// android-19
+// android-L
+// Google Inc.:Google APIs:20
+// Google Inc.:Glass Development Kit Preview:20
+// The idea is to sort based on largest "suffix" number - meaning the bigger
+// the number at the end, the more recent the target, the closer to the
+// start of the array.
+function sort_by_largest_numerical_suffix (a, b) {
+ var suffix_a = a.match(suffix_number_regex);
+ var suffix_b = b.match(suffix_number_regex);
+ if (suffix_a && suffix_b) {
+ // If the two targets being compared have suffixes, return less than
+ // zero, or greater than zero, based on which suffix is larger.
+ return (parseInt(suffix_a[1]) > parseInt(suffix_b[1]) ? -1 : 1);
+ } else {
+ // If no suffix numbers were detected, leave the order as-is between
+ // elements a and b.
+ return 0;
+ }
+}
+
+module.exports.print_newest_available_sdk_target = function () {
+ return module.exports.list_targets().then(function (targets) {
+ targets.sort(sort_by_largest_numerical_suffix);
+ console.log(targets[0]);
+ });
+};
+
+module.exports.version_string_to_api_level = {
+ '4.0': 14,
+ '4.0.3': 15,
+ '4.1': 16,
+ '4.2': 17,
+ '4.3': 18,
+ '4.4': 19,
+ '4.4W': 20,
+ '5.0': 21,
+ '5.1': 22,
+ '6.0': 23,
+ '7.0': 24,
+ '7.1.1': 25,
+ '8.0': 26
+};
+
+function parse_targets (output) {
+ var target_out = output.split('\n');
+ var targets = [];
+ for (var i = target_out.length - 1; i >= 0; i--) {
+ if (target_out[i].match(/id:/)) { // if "id:" is in the line...
+ targets.push(target_out[i].match(/"(.+)"/)[1]); // .. match whatever is in quotes.
+ }
+ }
+ return targets;
+}
+
+module.exports.list_targets_with_android = function () {
+ return superspawn.spawn('android', ['list', 'target']).then(parse_targets);
+};
+
+module.exports.list_targets_with_avdmanager = function () {
+ return superspawn.spawn('avdmanager', ['list', 'target']).then(parse_targets);
+};
+
+module.exports.list_targets = function () {
+ return module.exports.list_targets_with_avdmanager().catch(function (err) {
+ // If there's an error, like avdmanager could not be found, we can try
+ // as a last resort, to run `android`, in case this is a super old
+ // SDK installation.
+ if (err && (err.code === 'ENOENT' || (err.stderr && err.stderr.match(/not recognized/)))) {
+ return module.exports.list_targets_with_android();
+ } else throw err;
+ }).then(function (targets) {
+ if (targets.length === 0) {
+ return Q.reject(new Error('No android targets (SDKs) installed!'));
+ }
+ return targets;
+ });
+};
diff --git a/platforms/android/cordova/lib/build.js b/platforms/android/cordova/lib/build.js
new file mode 100644
index 0000000..e33cfae
--- /dev/null
+++ b/platforms/android/cordova/lib/build.js
@@ -0,0 +1,294 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+var Q = require('q');
+var path = require('path');
+var fs = require('fs');
+var nopt = require('nopt');
+
+var Adb = require('./Adb');
+
+var builders = require('./builders/builders');
+var events = require('cordova-common').events;
+var spawn = require('cordova-common').superspawn.spawn;
+var CordovaError = require('cordova-common').CordovaError;
+
+function parseOpts (options, resolvedTarget, projectRoot) {
+ options = options || {};
+ options.argv = nopt({
+ gradle: Boolean,
+ studio: Boolean,
+ prepenv: Boolean,
+ versionCode: String,
+ minSdkVersion: String,
+ gradleArg: [String, Array],
+ keystore: path,
+ alias: String,
+ storePassword: String,
+ password: String,
+ keystoreType: String
+ }, {}, options.argv, 0);
+
+ // Android Studio Build method is the default
+ var ret = {
+ buildType: options.release ? 'release' : 'debug',
+ buildMethod: process.env.ANDROID_BUILD || 'studio',
+ prepEnv: options.argv.prepenv,
+ arch: resolvedTarget && resolvedTarget.arch,
+ extraArgs: []
+ };
+
+ if (options.argv.gradle || options.argv.studio) {
+ ret.buildMethod = options.argv.studio ? 'studio' : 'gradle';
+ }
+
+ // This comes from cordova/run
+ if (options.studio) ret.buildMethod = 'studio';
+ if (options.gradle) ret.buildMethod = 'gradle';
+
+ if (options.nobuild) ret.buildMethod = 'none';
+
+ if (options.argv.versionCode) { ret.extraArgs.push('-PcdvVersionCode=' + options.argv.versionCode); }
+
+ if (options.argv.minSdkVersion) { ret.extraArgs.push('-PcdvMinSdkVersion=' + options.argv.minSdkVersion); }
+
+ if (options.argv.gradleArg) {
+ ret.extraArgs = ret.extraArgs.concat(options.argv.gradleArg);
+ }
+
+ var packageArgs = {};
+
+ if (options.argv.keystore) { packageArgs.keystore = path.relative(projectRoot, path.resolve(options.argv.keystore)); }
+
+ ['alias', 'storePassword', 'password', 'keystoreType'].forEach(function (flagName) {
+ if (options.argv[flagName]) { packageArgs[flagName] = options.argv[flagName]; }
+ });
+
+ var buildConfig = options.buildConfig;
+
+ // If some values are not specified as command line arguments - use build config to supplement them.
+ // Command line arguemnts have precedence over build config.
+ if (buildConfig) {
+ if (!fs.existsSync(buildConfig)) {
+ throw new Error('Specified build config file does not exist: ' + buildConfig);
+ }
+ events.emit('log', 'Reading build config file: ' + path.resolve(buildConfig));
+ var buildjson = fs.readFileSync(buildConfig, 'utf8');
+ var config = JSON.parse(buildjson.replace(/^\ufeff/, '')); // Remove BOM
+ if (config.android && config.android[ret.buildType]) {
+ var androidInfo = config.android[ret.buildType];
+ if (androidInfo.keystore && !packageArgs.keystore) {
+ if (androidInfo.keystore.substr(0, 1) === '~') {
+ androidInfo.keystore = process.env.HOME + androidInfo.keystore.substr(1);
+ }
+ packageArgs.keystore = path.resolve(path.dirname(buildConfig), androidInfo.keystore);
+ events.emit('log', 'Reading the keystore from: ' + packageArgs.keystore);
+ }
+
+ ['alias', 'storePassword', 'password', 'keystoreType'].forEach(function (key) {
+ packageArgs[key] = packageArgs[key] || androidInfo[key];
+ });
+ }
+ }
+
+ if (packageArgs.keystore && packageArgs.alias) {
+ ret.packageInfo = new PackageInfo(packageArgs.keystore, packageArgs.alias, packageArgs.storePassword,
+ packageArgs.password, packageArgs.keystoreType);
+ }
+
+ if (!ret.packageInfo) {
+ if (Object.keys(packageArgs).length > 0) {
+ events.emit('warn', '\'keystore\' and \'alias\' need to be specified to generate a signed archive.');
+ }
+ }
+
+ return ret;
+}
+
+/*
+ * Builds the project with the specifed options
+ * Returns a promise.
+ */
+module.exports.runClean = function (options) {
+ var opts = parseOpts(options, null, this.root);
+ var builder = builders.getBuilder(opts.buildMethod);
+ return builder.prepEnv(opts).then(function () {
+ return builder.clean(opts);
+ });
+};
+
+/**
+ * Builds the project with the specifed options.
+ *
+ * @param {BuildOptions} options A set of options. See PlatformApi.build
+ * method documentation for reference.
+ * @param {Object} optResolvedTarget A deployment target. Used to pass
+ * target architecture from upstream 'run' call. TODO: remove this option in
+ * favor of setting buildOptions.archs field.
+ *
+ * @return {Promise} Promise, resolved with built packages
+ * information.
+ */
+module.exports.run = function (options, optResolvedTarget) {
+ var opts = parseOpts(options, optResolvedTarget, this.root);
+ console.log(opts.buildMethod);
+ var builder = builders.getBuilder(opts.buildMethod);
+ return builder.prepEnv(opts).then(function () {
+ if (opts.prepEnv) {
+ events.emit('verbose', 'Build file successfully prepared.');
+ return;
+ }
+ return builder.build(opts).then(function () {
+ var apkPaths = builder.findOutputApks(opts.buildType, opts.arch);
+ events.emit('log', 'Built the following apk(s): \n\t' + apkPaths.join('\n\t'));
+ return {
+ apkPaths: apkPaths,
+ buildType: opts.buildType,
+ buildMethod: opts.buildMethod
+ };
+ });
+ });
+};
+
+/*
+ * Detects the architecture of a device/emulator
+ * Returns "arm" or "x86".
+ */
+module.exports.detectArchitecture = function (target) {
+ function helper () {
+ return Adb.shell(target, 'cat /proc/cpuinfo').then(function (output) {
+ return /intel/i.exec(output) ? 'x86' : 'arm';
+ });
+ }
+ // It sometimes happens (at least on OS X), that this command will hang forever.
+ // To fix it, either unplug & replug device, or restart adb server.
+ return helper().timeout(1000, new CordovaError('Device communication timed out. Try unplugging & replugging the device.')).then(null, function (err) {
+ if (/timed out/.exec('' + err)) {
+ // adb kill-server doesn't seem to do the trick.
+ // Could probably find a x-platform version of killall, but I'm not actually
+ // sure that this scenario even happens on non-OSX machines.
+ events.emit('verbose', 'adb timed out while detecting device/emulator architecture. Killing adb and trying again.');
+ return spawn('killall', ['adb']).then(function () {
+ return helper().then(null, function () {
+ // The double kill is sadly often necessary, at least on mac.
+ events.emit('warn', 'adb timed out a second time while detecting device/emulator architecture. Killing adb and trying again.');
+ return spawn('killall', ['adb']).then(function () {
+ return helper().then(null, function () {
+ return Q.reject(new CordovaError('adb timed out a third time while detecting device/emulator architecture. Try unplugging & replugging the device.'));
+ });
+ });
+ });
+ }, function () {
+ // For non-killall OS's.
+ return Q.reject(err);
+ });
+ }
+ throw err;
+ });
+};
+
+module.exports.findBestApkForArchitecture = function (buildResults, arch) {
+ var paths = buildResults.apkPaths.filter(function (p) {
+ var apkName = path.basename(p);
+ if (buildResults.buildType === 'debug') {
+ return /-debug/.exec(apkName);
+ }
+ return !/-debug/.exec(apkName);
+ });
+ var archPattern = new RegExp('-' + arch);
+ var hasArchPattern = /-x86|-arm/;
+ for (var i = 0; i < paths.length; ++i) {
+ var apkName = path.basename(paths[i]);
+ if (hasArchPattern.exec(apkName)) {
+ if (archPattern.exec(apkName)) {
+ return paths[i];
+ }
+ } else {
+ return paths[i];
+ }
+ }
+ throw new Error('Could not find apk architecture: ' + arch + ' build-type: ' + buildResults.buildType);
+};
+
+function PackageInfo (keystore, alias, storePassword, password, keystoreType) {
+ this.keystore = {
+ 'name': 'key.store',
+ 'value': keystore
+ };
+ this.alias = {
+ 'name': 'key.alias',
+ 'value': alias
+ };
+ if (storePassword) {
+ this.storePassword = {
+ 'name': 'key.store.password',
+ 'value': storePassword
+ };
+ }
+ if (password) {
+ this.password = {
+ 'name': 'key.alias.password',
+ 'value': password
+ };
+ }
+ if (keystoreType) {
+ this.keystoreType = {
+ 'name': 'key.store.type',
+ 'value': keystoreType
+ };
+ }
+}
+
+PackageInfo.prototype = {
+ toProperties: function () {
+ var self = this;
+ var result = '';
+ Object.keys(self).forEach(function (key) {
+ result += self[key].name;
+ result += '=';
+ result += self[key].value.replace(/\\/g, '\\\\');
+ result += '\n';
+ });
+ return result;
+ }
+};
+
+module.exports.help = function () {
+ console.log('Usage: ' + path.relative(process.cwd(), path.join('../build')) + ' [flags] [Signed APK flags]');
+ console.log('Flags:');
+ console.log(' \'--debug\': will build project in debug mode (default)');
+ console.log(' \'--release\': will build project for release');
+ console.log(' \'--ant\': will build project with ant');
+ console.log(' \'--gradle\': will build project with gradle (default)');
+ console.log(' \'--nobuild\': will skip build process (useful when using run command)');
+ console.log(' \'--prepenv\': don\'t build, but copy in build scripts where necessary');
+ console.log(' \'--versionCode=#\': Override versionCode for this build. Useful for uploading multiple APKs. Requires --gradle.');
+ console.log(' \'--minSdkVersion=#\': Override minSdkVersion for this build. Useful for uploading multiple APKs. Requires --gradle.');
+ console.log(' \'--gradleArg=\': Extra args to pass to the gradle command. Use one flag per arg. Ex. --gradleArg=-PcdvBuildMultipleApks=true');
+ console.log('');
+ console.log('Signed APK flags (overwrites debug/release-signing.proprties) :');
+ console.log(' \'--keystore=\': Key store used to build a signed archive. (Required)');
+ console.log(' \'--alias=\': Alias for the key store. (Required)');
+ console.log(' \'--storePassword=\': Password for the key store. (Optional - prompted)');
+ console.log(' \'--password=\': Password for the key. (Optional - prompted)');
+ console.log(' \'--keystoreType\': Type of the keystore. (Optional)');
+ process.exit(0);
+};
diff --git a/platforms/android/cordova/lib/builders/GenericBuilder.js b/platforms/android/cordova/lib/builders/GenericBuilder.js
new file mode 100644
index 0000000..892aa38
--- /dev/null
+++ b/platforms/android/cordova/lib/builders/GenericBuilder.js
@@ -0,0 +1,124 @@
+/*
+ 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.
+*/
+/* eslint no-self-assign: 0 */
+/* eslint no-unused-vars: 0 */
+
+var Q = require('q');
+var fs = require('fs');
+var path = require('path');
+var shell = require('shelljs');
+var events = require('cordova-common').events;
+
+function GenericBuilder (projectDir) {
+ this.root = projectDir || path.resolve(__dirname, '../../..');
+ this.binDirs = {
+ studio: path.join(this.root, 'app', 'build', 'outputs', 'apk'),
+ gradle: path.join(this.root, 'build', 'outputs', 'apk')
+ };
+}
+
+GenericBuilder.prototype.prepEnv = function () {
+ return Q();
+};
+
+GenericBuilder.prototype.build = function () {
+ events.emit('log', 'Skipping build...');
+ return Q(null);
+};
+
+GenericBuilder.prototype.clean = function () {
+ return Q();
+};
+
+GenericBuilder.prototype.findOutputApks = function (build_type, arch) {
+ var self = this;
+ return Object.keys(this.binDirs).reduce(function (result, builderName) {
+ var binDir = self.binDirs[builderName];
+ return result.concat(findOutputApksHelper(binDir, build_type, builderName === 'ant' ? null : arch));
+ }, []).sort(apkSorter);
+};
+
+module.exports = GenericBuilder;
+
+function apkSorter (fileA, fileB) {
+ // De-prioritize arch specific builds
+ var archSpecificRE = /-x86|-arm/;
+ if (archSpecificRE.exec(fileA)) {
+ return 1;
+ } else if (archSpecificRE.exec(fileB)) {
+ return -1;
+ }
+
+ // De-prioritize unsigned builds
+ var unsignedRE = /-unsigned/;
+ if (unsignedRE.exec(fileA)) {
+ return 1;
+ } else if (unsignedRE.exec(fileB)) {
+ return -1;
+ }
+
+ var timeDiff = fs.statSync(fileB).mtime - fs.statSync(fileA).mtime;
+ return timeDiff === 0 ? fileA.length - fileB.length : timeDiff;
+}
+
+function findOutputApksHelper (dir, build_type, arch) {
+ var shellSilent = shell.config.silent;
+ shell.config.silent = true;
+
+ // list directory recursively
+ var ret = shell.ls('-R', dir).map(function (file) {
+ // ls does not include base directory
+ return path.join(dir, file);
+ }).filter(function (file) {
+ // find all APKs
+ return file.match(/\.apk?$/i);
+ }).filter(function (candidate) {
+ var apkName = path.basename(candidate);
+ // Need to choose between release and debug .apk.
+ if (build_type === 'debug') {
+ return /-debug/.exec(apkName) && !/-unaligned|-unsigned/.exec(apkName);
+ }
+ if (build_type === 'release') {
+ return /-release/.exec(apkName) && !/-unaligned/.exec(apkName);
+ }
+ return true;
+ }).sort(apkSorter);
+
+ shellSilent = shellSilent;
+
+ if (ret.length === 0) {
+ return ret;
+ }
+ // Assume arch-specific build if newest apk has -x86 or -arm.
+ var archSpecific = !!/-x86|-arm/.exec(path.basename(ret[0]));
+ // And show only arch-specific ones (or non-arch-specific)
+ ret = ret.filter(function (p) {
+ /* jshint -W018 */
+ return !!/-x86|-arm/.exec(path.basename(p)) === archSpecific;
+ /* jshint +W018 */
+ });
+
+ if (archSpecific && ret.length > 1 && arch) {
+ ret = ret.filter(function (p) {
+ return path.basename(p).indexOf('-' + arch) !== -1;
+ });
+ }
+
+ return ret;
+}
diff --git a/platforms/android/cordova/lib/builders/GradleBuilder.js b/platforms/android/cordova/lib/builders/GradleBuilder.js
new file mode 100644
index 0000000..8237c73
--- /dev/null
+++ b/platforms/android/cordova/lib/builders/GradleBuilder.js
@@ -0,0 +1,330 @@
+/*
+ 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.
+*/
+
+var Q = require('q');
+var fs = require('fs');
+var util = require('util');
+var path = require('path');
+var shell = require('shelljs');
+var superspawn = require('cordova-common').superspawn;
+var CordovaError = require('cordova-common').CordovaError;
+var check_reqs = require('../check_reqs');
+
+var GenericBuilder = require('./GenericBuilder');
+
+var MARKER = 'YOUR CHANGES WILL BE ERASED!';
+var SIGNING_PROPERTIES = '-signing.properties';
+var TEMPLATE =
+ '# This file is automatically generated.\n' +
+ '# Do not modify this file -- ' + MARKER + '\n';
+
+function GradleBuilder (projectRoot) {
+ GenericBuilder.call(this, projectRoot);
+
+ this.binDirs = { gradle: this.binDirs.gradle };
+}
+
+util.inherits(GradleBuilder, GenericBuilder);
+
+GradleBuilder.prototype.getArgs = function (cmd, opts) {
+ if (cmd === 'release') {
+ cmd = 'cdvBuildRelease';
+ } else if (cmd === 'debug') {
+ cmd = 'cdvBuildDebug';
+ }
+ var args = [cmd, '-b', path.join(this.root, 'build.gradle')];
+ if (opts.arch) {
+ args.push('-PcdvBuildArch=' + opts.arch);
+ }
+
+ // 10 seconds -> 6 seconds
+ args.push('-Dorg.gradle.daemon=true');
+ // to allow dex in process
+ args.push('-Dorg.gradle.jvmargs=-Xmx2048m');
+ // allow NDK to be used - required by Gradle 1.5 plugin
+ args.push('-Pandroid.useDeprecatedNdk=true');
+ args.push.apply(args, opts.extraArgs);
+ // Shaves another 100ms, but produces a "try at own risk" warning. Not worth it (yet):
+ // args.push('-Dorg.gradle.parallel=true');
+ return args;
+};
+
+/*
+ * This returns a promise
+ */
+
+GradleBuilder.prototype.runGradleWrapper = function (gradle_cmd, gradle_file) {
+ var gradlePath = path.join(this.root, 'gradlew');
+ gradle_file = path.join(this.root, (gradle_file || 'wrapper.gradle'));
+ if (fs.existsSync(gradlePath)) {
+ // Literally do nothing, for some reason this works, while !fs.existsSync didn't on Windows
+ } else {
+ return superspawn.spawn(gradle_cmd, ['-p', this.root, 'wrapper', '-b', gradle_file], { stdio: 'pipe' })
+ .progress(function (stdio) {
+ suppressJavaOptionsInfo(stdio);
+ });
+ }
+};
+
+/*
+ * We need to kill this in a fire.
+ */
+
+GradleBuilder.prototype.readProjectProperties = function () {
+ function findAllUniq (data, r) {
+ var s = {};
+ var m;
+ while ((m = r.exec(data))) {
+ s[m[1]] = 1;
+ }
+ return Object.keys(s);
+ }
+
+ var data = fs.readFileSync(path.join(this.root, 'project.properties'), 'utf8');
+ return {
+ libs: findAllUniq(data, /^\s*android\.library\.reference\.\d+=(.*)(?:\s|$)/mg),
+ gradleIncludes: findAllUniq(data, /^\s*cordova\.gradle\.include\.\d+=(.*)(?:\s|$)/mg),
+ systemLibs: findAllUniq(data, /^\s*cordova\.system\.library\.\d+=(.*)(?:\s|$)/mg)
+ };
+};
+
+GradleBuilder.prototype.extractRealProjectNameFromManifest = function () {
+ var manifestPath = path.join(this.root, 'AndroidManifest.xml');
+ var manifestData = fs.readFileSync(manifestPath, 'utf8');
+ var m = /= 0) {
+ return check_reqs.check_android_target(error).then(function () {
+ // If due to some odd reason - check_android_target succeeds
+ // we should still fail here.
+ return Q.reject(error);
+ });
+ }
+ return Q.reject(error);
+ });
+};
+
+GradleBuilder.prototype.clean = function (opts) {
+ var builder = this;
+ var wrapper = path.join(this.root, 'gradlew');
+ var args = builder.getArgs('clean', opts);
+ return Q().then(function () {
+ return superspawn.spawn(wrapper, args, { stdio: 'inherit' });
+ }).then(function () {
+ shell.rm('-rf', path.join(builder.root, 'out'));
+
+ ['debug', 'release'].forEach(function (config) {
+ var propertiesFilePath = path.join(builder.root, config + SIGNING_PROPERTIES);
+ if (isAutoGenerated(propertiesFilePath)) {
+ shell.rm('-f', propertiesFilePath);
+ }
+ });
+ });
+};
+
+module.exports = GradleBuilder;
+
+function suppressJavaOptionsInfo (stdio) {
+ if (stdio.stderr) {
+ /*
+ * Workaround for the issue with Java printing some unwanted information to
+ * stderr instead of stdout.
+ * This function suppresses 'Picked up _JAVA_OPTIONS' message from being
+ * printed to stderr. See https://issues.apache.org/jira/browse/CB-9971 for
+ * explanation.
+ */
+ var suppressThisLine = /^Picked up _JAVA_OPTIONS: /i.test(stdio.stderr.toString());
+ if (suppressThisLine) {
+ return;
+ }
+ process.stderr.write(stdio.stderr);
+ } else {
+ process.stdout.write(stdio.stdout);
+ }
+}
+
+function isAutoGenerated (file) {
+ return fs.existsSync(file) && fs.readFileSync(file, 'utf8').indexOf(MARKER) > 0;
+}
diff --git a/platforms/android/cordova/lib/builders/StudioBuilder.js b/platforms/android/cordova/lib/builders/StudioBuilder.js
new file mode 100644
index 0000000..8322e15
--- /dev/null
+++ b/platforms/android/cordova/lib/builders/StudioBuilder.js
@@ -0,0 +1,302 @@
+/*
+ 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.
+*/
+
+var Q = require('q');
+var fs = require('fs');
+var util = require('util');
+var path = require('path');
+var shell = require('shelljs');
+var spawn = require('cordova-common').superspawn.spawn;
+var CordovaError = require('cordova-common').CordovaError;
+var check_reqs = require('../check_reqs');
+
+var GenericBuilder = require('./GenericBuilder');
+
+var MARKER = 'YOUR CHANGES WILL BE ERASED!';
+var SIGNING_PROPERTIES = '-signing.properties';
+var TEMPLATE =
+ '# This file is automatically generated.\n' +
+ '# Do not modify this file -- ' + MARKER + '\n';
+
+function StudioBuilder (projectRoot) {
+ GenericBuilder.call(this, projectRoot);
+
+ this.binDirs = {gradle: this.binDirs.studio};
+}
+
+util.inherits(StudioBuilder, GenericBuilder);
+
+StudioBuilder.prototype.getArgs = function (cmd, opts) {
+ if (cmd === 'release') {
+ cmd = 'cdvBuildRelease';
+ } else if (cmd === 'debug') {
+ cmd = 'cdvBuildDebug';
+ }
+ var args = [cmd, '-b', path.join(this.root, 'build.gradle')];
+ if (opts.arch) {
+ args.push('-PcdvBuildArch=' + opts.arch);
+ }
+
+ // 10 seconds -> 6 seconds
+ args.push('-Dorg.gradle.daemon=true');
+ // to allow dex in process
+ args.push('-Dorg.gradle.jvmargs=-Xmx2048m');
+ // allow NDK to be used - required by Gradle 1.5 plugin
+ // args.push('-Pandroid.useDeprecatedNdk=true');
+ args.push.apply(args, opts.extraArgs);
+ // Shaves another 100ms, but produces a "try at own risk" warning. Not worth it (yet):
+ // args.push('-Dorg.gradle.parallel=true');
+ return args;
+};
+
+/*
+ * This returns a promise
+ */
+
+StudioBuilder.prototype.runGradleWrapper = function (gradle_cmd) {
+ var gradlePath = path.join(this.root, 'gradlew');
+ var wrapperGradle = path.join(this.root, 'wrapper.gradle');
+ if (fs.existsSync(gradlePath)) {
+ // Literally do nothing, for some reason this works, while !fs.existsSync didn't on Windows
+ } else {
+ return spawn(gradle_cmd, ['-p', this.root, 'wrapper', '-b', wrapperGradle], {stdio: 'inherit'});
+ }
+};
+
+StudioBuilder.prototype.readProjectProperties = function () {
+
+ function findAllUniq (data, r) {
+ var s = {};
+ var m;
+ while ((m = r.exec(data))) {
+ s[m[1]] = 1;
+ }
+ return Object.keys(s);
+ }
+
+ var data = fs.readFileSync(path.join(this.root, 'project.properties'), 'utf8');
+ return {
+ libs: findAllUniq(data, /^\s*android\.library\.reference\.\d+=(.*)(?:\s|$)/mg),
+ gradleIncludes: findAllUniq(data, /^\s*cordova\.gradle\.include\.\d+=(.*)(?:\s|$)/mg),
+ systemLibs: findAllUniq(data, /^\s*cordova\.system\.library\.\d+=(.*)(?:\s|$)/mg)
+ };
+};
+
+StudioBuilder.prototype.extractRealProjectNameFromManifest = function () {
+ var manifestPath = path.join(this.root, 'app', 'src', 'main', 'AndroidManifest.xml');
+ var manifestData = fs.readFileSync(manifestPath, 'utf8');
+ var m = /= 0) {
+ return check_reqs.check_android_target(error).then(function () {
+ // If due to some odd reason - check_android_target succeeds
+ // we should still fail here.
+ return Q.reject(error);
+ });
+ }
+ return Q.reject(error);
+ });
+};
+
+StudioBuilder.prototype.clean = function (opts) {
+ var builder = this;
+ var wrapper = path.join(this.root, 'gradlew');
+ var args = builder.getArgs('clean', opts);
+ return Q().then(function () {
+ return spawn(wrapper, args, {stdio: 'inherit'});
+ })
+ .then(function () {
+ shell.rm('-rf', path.join(builder.root, 'out'));
+
+ ['debug', 'release'].forEach(function (config) {
+ var propertiesFilePath = path.join(builder.root, config + SIGNING_PROPERTIES);
+ if (isAutoGenerated(propertiesFilePath)) {
+ shell.rm('-f', propertiesFilePath);
+ }
+ });
+ });
+};
+
+module.exports = StudioBuilder;
+
+function isAutoGenerated (file) {
+ return fs.existsSync(file) && fs.readFileSync(file, 'utf8').indexOf(MARKER) > 0;
+}
diff --git a/platforms/android/cordova/lib/builders/builders.js b/platforms/android/cordova/lib/builders/builders.js
new file mode 100644
index 0000000..aedf9be
--- /dev/null
+++ b/platforms/android/cordova/lib/builders/builders.js
@@ -0,0 +1,46 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+var CordovaError = require('cordova-common').CordovaError;
+
+var knownBuilders = {
+ gradle: 'GradleBuilder',
+ studio: 'StudioBuilder',
+ none: 'GenericBuilder'
+};
+
+/**
+ * Helper method that instantiates and returns a builder for specified build
+ * type.
+ *
+ * @param {String} builderType Builder name to construct and return. Must
+ * be one of 'ant', 'gradle' or 'none'
+ *
+ * @return {Builder} A builder instance for specified build type.
+ */
+module.exports.getBuilder = function (builderType, projectRoot) {
+ if (!knownBuilders[builderType]) { throw new CordovaError('Builder ' + builderType + ' is not supported.'); }
+
+ try {
+ var Builder = require('./' + knownBuilders[builderType]);
+ return new Builder(projectRoot);
+ } catch (err) {
+ throw new CordovaError('Failed to instantiate ' + knownBuilders[builderType] + ' builder: ' + err);
+ }
+};
diff --git a/platforms/android/cordova/lib/check_reqs.js b/platforms/android/cordova/lib/check_reqs.js
new file mode 100644
index 0000000..4a22360
--- /dev/null
+++ b/platforms/android/cordova/lib/check_reqs.js
@@ -0,0 +1,432 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+/* jshint sub:true */
+
+var shelljs = require('shelljs');
+var child_process = require('child_process');
+var Q = require('q');
+var path = require('path');
+var fs = require('fs');
+var os = require('os');
+var REPO_ROOT = path.join(__dirname, '..', '..', '..', '..');
+var PROJECT_ROOT = path.join(__dirname, '..', '..');
+var CordovaError = require('cordova-common').CordovaError;
+var superspawn = require('cordova-common').superspawn;
+var android_sdk = require('./android_sdk');
+
+function forgivingWhichSync (cmd) {
+ try {
+ return fs.realpathSync(shelljs.which(cmd));
+ } catch (e) {
+ return '';
+ }
+}
+
+function tryCommand (cmd, errMsg, catchStderr) {
+ var d = Q.defer();
+ child_process.exec(cmd, function (err, stdout, stderr) {
+ if (err) d.reject(new CordovaError(errMsg));
+ // Sometimes it is necessary to return an stderr instead of stdout in case of success, since
+ // some commands prints theirs output to stderr instead of stdout. 'javac' is the example
+ else d.resolve((catchStderr ? stderr : stdout).trim());
+ });
+ return d.promise;
+}
+
+module.exports.isWindows = function () {
+ return (os.platform() === 'win32');
+};
+
+module.exports.isDarwin = function () {
+ return (os.platform() === 'darwin');
+};
+
+// Get valid target from framework/project.properties if run from this repo
+// Otherwise get target from project.properties file within a generated cordova-android project
+module.exports.get_target = function () {
+ function extractFromFile (filePath) {
+ var target = shelljs.grep(/\btarget=/, filePath);
+ if (!target) {
+ throw new Error('Could not find android target within: ' + filePath);
+ }
+ return target.split('=')[1].trim();
+ }
+ var repo_file = path.join(REPO_ROOT, 'framework', 'project.properties');
+ if (fs.existsSync(repo_file)) {
+ return extractFromFile(repo_file);
+ }
+ var project_file = path.join(PROJECT_ROOT, 'project.properties');
+ if (fs.existsSync(project_file)) {
+ // if no target found, we're probably in a project and project.properties is in PROJECT_ROOT.
+ return extractFromFile(project_file);
+ }
+ throw new Error('Could not find android target in either ' + repo_file + ' nor ' + project_file);
+};
+
+// Returns a promise. Called only by build and clean commands.
+module.exports.check_ant = function () {
+ return superspawn.spawn('ant', ['-version']).then(function (output) {
+ // Parse Ant version from command output
+ return /version ((?:\d+\.)+(?:\d+))/i.exec(output)[1];
+ }).catch(function (err) {
+ if (err) {
+ throw new CordovaError('Failed to run `ant -version`. Make sure you have `ant` on your $PATH.');
+ }
+ });
+};
+
+module.exports.get_gradle_wrapper = function () {
+ var androidStudioPath;
+ var i = 0;
+ var foundStudio = false;
+ var program_dir;
+ // OK, This hack only works on Windows, not on Mac OS or Linux. We will be deleting this eventually!
+ if (module.exports.isWindows()) {
+
+ var result = child_process.spawnSync(path.join(__dirname, 'getASPath.bat'));
+ // console.log('result.stdout =' + result.stdout.toString());
+ // console.log('result.stderr =' + result.stderr.toString());
+
+ if (result.stderr.toString().length > 0) {
+ var androidPath = path.join(process.env['ProgramFiles'], 'Android') + '/';
+ if (fs.existsSync(androidPath)) {
+ program_dir = fs.readdirSync(androidPath);
+ while (i < program_dir.length && !foundStudio) {
+ if (program_dir[i].startsWith('Android Studio')) {
+ foundStudio = true;
+ androidStudioPath = path.join(process.env['ProgramFiles'], 'Android', program_dir[i], 'gradle');
+ } else { ++i; }
+ }
+ }
+ } else {
+ // console.log('got android studio path from registry');
+ // remove the (os independent) new line char at the end of stdout
+ // add gradle to match the above.
+ androidStudioPath = path.join(result.stdout.toString().split('\r\n')[0], 'gradle');
+ }
+ }
+
+ if (androidStudioPath !== null && fs.existsSync(androidStudioPath)) {
+ var dirs = fs.readdirSync(androidStudioPath);
+ if (dirs[0].split('-')[0] === 'gradle') {
+ return path.join(androidStudioPath, dirs[0], 'bin', 'gradle');
+ }
+ } else {
+ // OK, let's try to check for Gradle!
+ return forgivingWhichSync('gradle');
+ }
+};
+
+// Returns a promise. Called only by build and clean commands.
+module.exports.check_gradle = function () {
+ var sdkDir = process.env['ANDROID_HOME'];
+ var d = Q.defer();
+ if (!sdkDir) {
+ return Q.reject(new CordovaError('Could not find gradle wrapper within Android SDK. Could not find Android SDK directory.\n' +
+ 'Might need to install Android SDK or set up \'ANDROID_HOME\' env variable.'));
+ }
+
+ var gradlePath = module.exports.get_gradle_wrapper();
+ if (gradlePath.length !== 0) { d.resolve(gradlePath); } else {
+ d.reject(new CordovaError('Could not find an installed version of Gradle either in Android Studio,\n' +
+ 'or on your system to install the gradle wrapper. Please include gradle \n' +
+ 'in your path, or install Android Studio'));
+ }
+ return d.promise;
+};
+
+// Returns a promise.
+module.exports.check_java = function () {
+ var javacPath = forgivingWhichSync('javac');
+ var hasJavaHome = !!process.env['JAVA_HOME'];
+ return Q().then(function () {
+ if (hasJavaHome) {
+ // Windows java installer doesn't add javac to PATH, nor set JAVA_HOME (ugh).
+ if (!javacPath) {
+ process.env['PATH'] += path.delimiter + path.join(process.env['JAVA_HOME'], 'bin');
+ }
+ } else {
+ if (javacPath) {
+ // OS X has a command for finding JAVA_HOME.
+ var find_java = '/usr/libexec/java_home';
+ var default_java_error_msg = 'Failed to find \'JAVA_HOME\' environment variable. Try setting it manually.';
+ if (fs.existsSync(find_java)) {
+ return superspawn.spawn(find_java).then(function (stdout) {
+ process.env['JAVA_HOME'] = stdout.trim();
+ }).catch(function (err) {
+ if (err) {
+ throw new CordovaError(default_java_error_msg);
+ }
+ });
+ } else {
+ // See if we can derive it from javac's location.
+ // fs.realpathSync is require on Ubuntu, which symplinks from /usr/bin -> JDK
+ var maybeJavaHome = path.dirname(path.dirname(javacPath));
+ if (fs.existsSync(path.join(maybeJavaHome, 'lib', 'tools.jar'))) {
+ process.env['JAVA_HOME'] = maybeJavaHome;
+ } else {
+ throw new CordovaError(default_java_error_msg);
+ }
+ }
+ } else if (module.exports.isWindows()) {
+ // Try to auto-detect java in the default install paths.
+ var oldSilent = shelljs.config.silent;
+ shelljs.config.silent = true;
+ var firstJdkDir =
+ shelljs.ls(process.env['ProgramFiles'] + '\\java\\jdk*')[0] ||
+ shelljs.ls('C:\\Program Files\\java\\jdk*')[0] ||
+ shelljs.ls('C:\\Program Files (x86)\\java\\jdk*')[0];
+ shelljs.config.silent = oldSilent;
+ if (firstJdkDir) {
+ // shelljs always uses / in paths.
+ firstJdkDir = firstJdkDir.replace(/\//g, path.sep);
+ if (!javacPath) {
+ process.env['PATH'] += path.delimiter + path.join(firstJdkDir, 'bin');
+ }
+ process.env['JAVA_HOME'] = firstJdkDir;
+ }
+ }
+ }
+ }).then(function () {
+ var msg =
+ 'Failed to run "javac -version", make sure that you have a JDK installed.\n' +
+ 'You can get it from: http://www.oracle.com/technetwork/java/javase/downloads.\n';
+ if (process.env['JAVA_HOME']) {
+ msg += 'Your JAVA_HOME is invalid: ' + process.env['JAVA_HOME'] + '\n';
+ }
+ // We use tryCommand with catchStderr = true, because
+ // javac writes version info to stderr instead of stdout
+ return tryCommand('javac -version', msg, true).then(function (output) {
+ // Let's check for at least Java 8, and keep it future proof so we can support Java 10
+ var match = /javac ((?:1\.)(?:[8-9]\.)(?:\d+))|((?:1\.)(?:[1-9]\d+\.)(?:\d+))/i.exec(output);
+ return match && match[1];
+ });
+ });
+};
+
+// Returns a promise.
+module.exports.check_android = function () {
+ return Q().then(function () {
+ var androidCmdPath = forgivingWhichSync('android');
+ var adbInPath = forgivingWhichSync('adb');
+ var avdmanagerInPath = forgivingWhichSync('avdmanager');
+ var hasAndroidHome = !!process.env['ANDROID_HOME'] && fs.existsSync(process.env['ANDROID_HOME']);
+ function maybeSetAndroidHome (value) {
+ if (!hasAndroidHome && fs.existsSync(value)) {
+ hasAndroidHome = true;
+ process.env['ANDROID_HOME'] = value;
+ }
+ }
+ // First ensure ANDROID_HOME is set
+ // If we have no hints (nothing in PATH), try a few default locations
+ if (!hasAndroidHome && !androidCmdPath && !adbInPath && !avdmanagerInPath) {
+ if (module.exports.isWindows()) {
+ // Android Studio 1.0 installer
+ maybeSetAndroidHome(path.join(process.env['LOCALAPPDATA'], 'Android', 'sdk'));
+ maybeSetAndroidHome(path.join(process.env['ProgramFiles'], 'Android', 'sdk'));
+ // Android Studio pre-1.0 installer
+ maybeSetAndroidHome(path.join(process.env['LOCALAPPDATA'], 'Android', 'android-studio', 'sdk'));
+ maybeSetAndroidHome(path.join(process.env['ProgramFiles'], 'Android', 'android-studio', 'sdk'));
+ // Stand-alone installer
+ maybeSetAndroidHome(path.join(process.env['LOCALAPPDATA'], 'Android', 'android-sdk'));
+ maybeSetAndroidHome(path.join(process.env['ProgramFiles'], 'Android', 'android-sdk'));
+ } else if (module.exports.isDarwin()) {
+ // Android Studio 1.0 installer
+ maybeSetAndroidHome(path.join(process.env['HOME'], 'Library', 'Android', 'sdk'));
+ // Android Studio pre-1.0 installer
+ maybeSetAndroidHome('/Applications/Android Studio.app/sdk');
+ // Stand-alone zip file that user might think to put under /Applications
+ maybeSetAndroidHome('/Applications/android-sdk-macosx');
+ maybeSetAndroidHome('/Applications/android-sdk');
+ }
+ if (process.env['HOME']) {
+ // Stand-alone zip file that user might think to put under their home directory
+ maybeSetAndroidHome(path.join(process.env['HOME'], 'android-sdk-macosx'));
+ maybeSetAndroidHome(path.join(process.env['HOME'], 'android-sdk'));
+ }
+ }
+ if (!hasAndroidHome) {
+ // If we dont have ANDROID_HOME, but we do have some tools on the PATH, try to infer from the tooling PATH.
+ var parentDir, grandParentDir;
+ if (androidCmdPath) {
+ parentDir = path.dirname(androidCmdPath);
+ grandParentDir = path.dirname(parentDir);
+ if (path.basename(parentDir) === 'tools' || fs.existsSync(path.join(grandParentDir, 'tools', 'android'))) {
+ maybeSetAndroidHome(grandParentDir);
+ } else {
+ throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting it manually.\n' +
+ 'Detected \'android\' command at ' + parentDir + ' but no \'tools\' directory found near.\n' +
+ 'Try reinstall Android SDK or update your PATH to include valid path to SDK' + path.sep + 'tools directory.');
+ }
+ }
+ if (adbInPath) {
+ parentDir = path.dirname(adbInPath);
+ grandParentDir = path.dirname(parentDir);
+ if (path.basename(parentDir) === 'platform-tools') {
+ maybeSetAndroidHome(grandParentDir);
+ } else {
+ throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting it manually.\n' +
+ 'Detected \'adb\' command at ' + parentDir + ' but no \'platform-tools\' directory found near.\n' +
+ 'Try reinstall Android SDK or update your PATH to include valid path to SDK' + path.sep + 'platform-tools directory.');
+ }
+ }
+ if (avdmanagerInPath) {
+ parentDir = path.dirname(avdmanagerInPath);
+ grandParentDir = path.dirname(parentDir);
+ if (path.basename(parentDir) === 'bin' && path.basename(grandParentDir) === 'tools') {
+ maybeSetAndroidHome(path.dirname(grandParentDir));
+ } else {
+ throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting it manually.\n' +
+ 'Detected \'avdmanager\' command at ' + parentDir + ' but no \'tools' + path.sep + 'bin\' directory found near.\n' +
+ 'Try reinstall Android SDK or update your PATH to include valid path to SDK' + path.sep + 'tools' + path.sep + 'bin directory.');
+ }
+ }
+ }
+ if (!process.env['ANDROID_HOME']) {
+ throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting it manually.\n' +
+ 'Failed to find \'android\' command in your \'PATH\'. Try update your \'PATH\' to include path to valid SDK directory.');
+ }
+ if (!fs.existsSync(process.env['ANDROID_HOME'])) {
+ throw new CordovaError('\'ANDROID_HOME\' environment variable is set to non-existent path: ' + process.env['ANDROID_HOME'] +
+ '\nTry update it manually to point to valid SDK directory.');
+ }
+ // Next let's make sure relevant parts of the SDK tooling is in our PATH
+ if (hasAndroidHome && !androidCmdPath) {
+ process.env['PATH'] += path.delimiter + path.join(process.env['ANDROID_HOME'], 'tools');
+ }
+ if (hasAndroidHome && !adbInPath) {
+ process.env['PATH'] += path.delimiter + path.join(process.env['ANDROID_HOME'], 'platform-tools');
+ }
+ if (hasAndroidHome && !avdmanagerInPath) {
+ process.env['PATH'] += path.delimiter + path.join(process.env['ANDROID_HOME'], 'tools', 'bin');
+ }
+ return hasAndroidHome;
+ });
+};
+
+// TODO: is this actually needed?
+module.exports.getAbsoluteAndroidCmd = function () {
+ var cmd = forgivingWhichSync('android');
+ if (cmd.length === 0) {
+ cmd = forgivingWhichSync('sdkmanager');
+ }
+ if (module.exports.isWindows()) {
+ return '"' + cmd + '"';
+ }
+ return cmd.replace(/(\s)/g, '\\$1');
+};
+
+module.exports.check_android_target = function (originalError) {
+ // valid_target can look like:
+ // android-19
+ // android-L
+ // Google Inc.:Google APIs:20
+ // Google Inc.:Glass Development Kit Preview:20
+ var desired_api_level = module.exports.get_target();
+ return android_sdk.list_targets().then(function (targets) {
+ if (targets.indexOf(desired_api_level) >= 0) {
+ return targets;
+ }
+ var androidCmd = module.exports.getAbsoluteAndroidCmd();
+ var msg = 'Please install Android target / API level: "' + desired_api_level + '".\n\n' +
+ 'Hint: Open the SDK manager by running: ' + androidCmd + '\n' +
+ 'You will require:\n' +
+ '1. "SDK Platform" for API level ' + desired_api_level + '\n' +
+ '2. "Android SDK Platform-tools (latest)\n' +
+ '3. "Android SDK Build-tools" (latest)';
+ if (originalError) {
+ msg = originalError + '\n' + msg;
+ }
+ throw new CordovaError(msg);
+ });
+};
+
+// Returns a promise.
+module.exports.run = function () {
+ return Q.all([this.check_java(), this.check_android()]).then(function (values) {
+ console.log('ANDROID_HOME=' + process.env['ANDROID_HOME']);
+ console.log('JAVA_HOME=' + process.env['JAVA_HOME']);
+
+ if (!values[0]) {
+ throw new CordovaError('Requirements check failed for JDK 1.8 or greater');
+ }
+
+ if (!values[1]) {
+ throw new CordovaError('Requirements check failed for Android SDK');
+ }
+ });
+};
+
+/**
+ * Object thar represents one of requirements for current platform.
+ * @param {String} id The unique identifier for this requirements.
+ * @param {String} name The name of requirements. Human-readable field.
+ * @param {String} version The version of requirement installed. In some cases could be an array of strings
+ * (for example, check_android_target returns an array of android targets installed)
+ * @param {Boolean} installed Indicates whether the requirement is installed or not
+ */
+var Requirement = function (id, name, version, installed) {
+ this.id = id;
+ this.name = name;
+ this.installed = installed || false;
+ this.metadata = {
+ version: version
+ };
+};
+
+/**
+ * Methods that runs all checks one by one and returns a result of checks
+ * as an array of Requirement objects. This method intended to be used by cordova-lib check_reqs method
+ *
+ * @return Promise Array of requirements. Due to implementation, promise is always fulfilled.
+ */
+module.exports.check_all = function () {
+
+ var requirements = [
+ new Requirement('java', 'Java JDK'),
+ new Requirement('androidSdk', 'Android SDK'),
+ new Requirement('androidTarget', 'Android target'),
+ new Requirement('gradle', 'Gradle')
+ ];
+
+ var checkFns = [
+ this.check_java,
+ this.check_android,
+ this.check_android_target,
+ this.check_gradle
+ ];
+
+ // Then execute requirement checks one-by-one
+ return checkFns.reduce(function (promise, checkFn, idx) {
+ // Update each requirement with results
+ var requirement = requirements[idx];
+ return promise.then(checkFn).then(function (version) {
+ requirement.installed = true;
+ requirement.metadata.version = version;
+ }, function (err) {
+ requirement.metadata.reason = err instanceof Error ? err.message : err;
+ });
+ }, Q()).then(function () {
+ // When chain is completed, return requirements array to upstream API
+ return requirements;
+ });
+};
diff --git a/platforms/android/cordova/lib/device.js b/platforms/android/cordova/lib/device.js
new file mode 100644
index 0000000..84b5094
--- /dev/null
+++ b/platforms/android/cordova/lib/device.js
@@ -0,0 +1,112 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+var Q = require('q');
+var build = require('./build');
+var path = require('path');
+var Adb = require('./Adb');
+var AndroidManifest = require('./AndroidManifest');
+var spawn = require('cordova-common').superspawn.spawn;
+var CordovaError = require('cordova-common').CordovaError;
+var events = require('cordova-common').events;
+
+/**
+ * Returns a promise for the list of the device ID's found
+ * @param lookHarder When true, try restarting adb if no devices are found.
+ */
+module.exports.list = function (lookHarder) {
+ return Adb.devices().then(function (list) {
+ if (list.length === 0 && lookHarder) {
+ // adb kill-server doesn't seem to do the trick.
+ // Could probably find a x-platform version of killall, but I'm not actually
+ // sure that this scenario even happens on non-OSX machines.
+ return spawn('killall', ['adb']).then(function () {
+ events.emit('verbose', 'Restarting adb to see if more devices are detected.');
+ return Adb.devices();
+ }, function () {
+ // For non-killall OS's.
+ return list;
+ });
+ }
+ return list;
+ });
+};
+
+module.exports.resolveTarget = function (target) {
+ return this.list(true).then(function (device_list) {
+ if (!device_list || !device_list.length) {
+ return Q.reject(new CordovaError('Failed to deploy to device, no devices found.'));
+ }
+ // default device
+ target = target || device_list[0];
+
+ if (device_list.indexOf(target) < 0) {
+ return Q.reject('ERROR: Unable to find target \'' + target + '\'.');
+ }
+
+ return build.detectArchitecture(target).then(function (arch) {
+ return { target: target, arch: arch, isEmulator: false };
+ });
+ });
+};
+
+/*
+ * Installs a previously built application on the device
+ * and launches it.
+ * Returns a promise.
+ */
+module.exports.install = function (target, buildResults) {
+ return Q().then(function () {
+ if (target && typeof target === 'object') {
+ return target;
+ }
+ return module.exports.resolveTarget(target);
+ }).then(function (resolvedTarget) {
+ var apk_path = build.findBestApkForArchitecture(buildResults, resolvedTarget.arch);
+ var manifest = new AndroidManifest(path.join(__dirname, '../../app/src/main/AndroidManifest.xml'));
+ var pkgName = manifest.getPackageId();
+ var launchName = pkgName + '/.' + manifest.getActivity().getName();
+ events.emit('log', 'Using apk: ' + apk_path);
+ events.emit('log', 'Package name: ' + pkgName);
+
+ return Adb.install(resolvedTarget.target, apk_path, {replace: true}).catch(function (error) {
+ // CB-9557 CB-10157 only uninstall and reinstall app if the one that
+ // is already installed on device was signed w/different certificate
+ if (!/INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES/.test(error.toString())) { throw error; }
+
+ events.emit('warn', 'Uninstalling app from device and reinstalling it again because the ' +
+ 'installed app already signed with different key');
+
+ // This promise is always resolved, even if 'adb uninstall' fails to uninstall app
+ // or the app doesn't installed at all, so no error catching needed.
+ return Adb.uninstall(resolvedTarget.target, pkgName).then(function () {
+ return Adb.install(resolvedTarget.target, apk_path, {replace: true});
+ });
+ }).then(function () {
+ // unlock screen
+ return Adb.shell(resolvedTarget.target, 'input keyevent 82');
+ }).then(function () {
+ return Adb.start(resolvedTarget.target, launchName);
+ }).then(function () {
+ events.emit('log', 'LAUNCH SUCCESS');
+ });
+ });
+};
diff --git a/platforms/android/cordova/lib/emulator.js b/platforms/android/cordova/lib/emulator.js
new file mode 100644
index 0000000..305e2e3
--- /dev/null
+++ b/platforms/android/cordova/lib/emulator.js
@@ -0,0 +1,533 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+/* jshint sub:true */
+
+var android_versions = require('android-versions');
+var retry = require('./retry');
+var build = require('./build');
+var path = require('path');
+var Adb = require('./Adb');
+var AndroidManifest = require('./AndroidManifest');
+var events = require('cordova-common').events;
+var superspawn = require('cordova-common').superspawn;
+var CordovaError = require('cordova-common').CordovaError;
+var shelljs = require('shelljs');
+var android_sdk = require('./android_sdk');
+var check_reqs = require('./check_reqs');
+
+var Q = require('q');
+var os = require('os');
+var fs = require('fs');
+var child_process = require('child_process');
+
+// constants
+var ONE_SECOND = 1000; // in milliseconds
+var ONE_MINUTE = 60 * ONE_SECOND; // in milliseconds
+var INSTALL_COMMAND_TIMEOUT = 5 * ONE_MINUTE; // in milliseconds
+var NUM_INSTALL_RETRIES = 3;
+var CHECK_BOOTED_INTERVAL = 3 * ONE_SECOND; // in milliseconds
+var EXEC_KILL_SIGNAL = 'SIGKILL';
+
+function forgivingWhichSync (cmd) {
+ try {
+ return fs.realpathSync(shelljs.which(cmd));
+ } catch (e) {
+ return '';
+ }
+}
+
+module.exports.list_images_using_avdmanager = function () {
+ return superspawn.spawn('avdmanager', ['list', 'avd']).then(function (output) {
+ var response = output.split('\n');
+ var emulator_list = [];
+ for (var i = 1; i < response.length; i++) {
+ // To return more detailed information use img_obj
+ var img_obj = {};
+ if (response[i].match(/Name:\s/)) {
+ img_obj['name'] = response[i].split('Name: ')[1].replace('\r', '');
+ if (response[i + 1].match(/Device:\s/)) {
+ i++;
+ img_obj['device'] = response[i].split('Device: ')[1].replace('\r', '');
+ }
+ if (response[i + 1].match(/Path:\s/)) {
+ i++;
+ img_obj['path'] = response[i].split('Path: ')[1].replace('\r', '');
+ }
+ if (response[i + 1].match(/Target:\s/)) {
+ i++;
+ if (response[i + 1].match(/ABI:\s/)) {
+ img_obj['abi'] = response[i + 1].split('ABI: ')[1].replace('\r', '');
+ }
+ // This next conditional just aims to match the old output of `android list avd`
+ // We do so so that we don't have to change the logic when parsing for the
+ // best emulator target to spawn (see below in `best_image`)
+ // This allows us to transitionally support both `android` and `avdmanager` binaries,
+ // depending on what SDK version the user has
+ if (response[i + 1].match(/Based\son:\s/)) {
+ img_obj['target'] = response[i + 1].split('Based on:')[1];
+ if (img_obj['target'].match(/Tag\/ABI:\s/)) {
+ img_obj['target'] = img_obj['target'].split('Tag/ABI:')[0].replace('\r', '').trim();
+ if (img_obj['target'].indexOf('(') > -1) {
+ img_obj['target'] = img_obj['target'].substr(0, img_obj['target'].indexOf('(') - 1).trim();
+ }
+ }
+ var version_string = img_obj['target'].replace(/Android\s+/, '');
+
+ var api_level = android_sdk.version_string_to_api_level[version_string];
+ if (api_level) {
+ img_obj['target'] += ' (API level ' + api_level + ')';
+ }
+ }
+ }
+ if (response[i + 1].match(/Skin:\s/)) {
+ i++;
+ img_obj['skin'] = response[i].split('Skin: ')[1].replace('\r', '');
+ }
+
+ emulator_list.push(img_obj);
+ }
+ /* To just return a list of names use this
+ if (response[i].match(/Name:\s/)) {
+ emulator_list.push(response[i].split('Name: ')[1].replace('\r', '');
+ } */
+
+ }
+ return emulator_list;
+ });
+};
+
+module.exports.list_images_using_android = function () {
+ return superspawn.spawn('android', ['list', 'avd']).then(function (output) {
+ var response = output.split('\n');
+ var emulator_list = [];
+ for (var i = 1; i < response.length; i++) {
+ // To return more detailed information use img_obj
+ var img_obj = {};
+ if (response[i].match(/Name:\s/)) {
+ img_obj['name'] = response[i].split('Name: ')[1].replace('\r', '');
+ if (response[i + 1].match(/Device:\s/)) {
+ i++;
+ img_obj['device'] = response[i].split('Device: ')[1].replace('\r', '');
+ }
+ if (response[i + 1].match(/Path:\s/)) {
+ i++;
+ img_obj['path'] = response[i].split('Path: ')[1].replace('\r', '');
+ }
+ if (response[i + 1].match(/\(API\slevel\s/) || (response[i + 2] && response[i + 2].match(/\(API\slevel\s/))) {
+ i++;
+ var secondLine = response[i + 1].match(/\(API\slevel\s/) ? response[i + 1] : '';
+ img_obj['target'] = (response[i] + secondLine).split('Target: ')[1].replace('\r', '');
+ }
+ if (response[i + 1].match(/ABI:\s/)) {
+ i++;
+ img_obj['abi'] = response[i].split('ABI: ')[1].replace('\r', '');
+ }
+ if (response[i + 1].match(/Skin:\s/)) {
+ i++;
+ img_obj['skin'] = response[i].split('Skin: ')[1].replace('\r', '');
+ }
+
+ emulator_list.push(img_obj);
+ }
+ /* To just return a list of names use this
+ if (response[i].match(/Name:\s/)) {
+ emulator_list.push(response[i].split('Name: ')[1].replace('\r', '');
+ } */
+
+ }
+ return emulator_list;
+ });
+};
+
+/**
+ * Returns a Promise for a list of emulator images in the form of objects
+ * {
+ name : ,
+ device : ,
+ path : ,
+ target : ,
+ abi : ,
+ skin :
+ }
+ */
+module.exports.list_images = function () {
+ return Q.fcall(function () {
+ if (forgivingWhichSync('avdmanager')) {
+ return module.exports.list_images_using_avdmanager();
+ } else if (forgivingWhichSync('android')) {
+ return module.exports.list_images_using_android();
+ } else {
+ return Q().then(function () {
+ throw new CordovaError('Could not find either `android` or `avdmanager` on your $PATH! Are you sure the Android SDK is installed and available?');
+ });
+ }
+ }).then(function (avds) {
+ // In case we're missing the Android OS version string from the target description, add it.
+ return avds.map(function (avd) {
+ if (avd.target && avd.target.indexOf('Android API') > -1 && avd.target.indexOf('API level') < 0) {
+ var api_level = avd.target.match(/\d+/);
+ if (api_level) {
+ var level = android_versions.get(api_level);
+ if (level) {
+ avd.target = 'Android ' + level.semver + ' (API level ' + api_level + ')';
+ }
+ }
+ }
+ return avd;
+ });
+ });
+};
+
+/**
+ * Will return the closest avd to the projects target
+ * or undefined if no avds exist.
+ * Returns a promise.
+ */
+module.exports.best_image = function () {
+ return this.list_images().then(function (images) {
+ // Just return undefined if there is no images
+ if (images.length === 0) return;
+
+ var closest = 9999;
+ var best = images[0];
+ var project_target = parseInt(check_reqs.get_target().replace('android-', ''));
+ for (var i in images) {
+ var target = images[i].target;
+ if (target && target.indexOf('API level') > -1) {
+ var num = parseInt(target.split('(API level ')[1].replace(')', ''));
+ if (num === project_target) {
+ return images[i];
+ } else if (project_target - num < closest && project_target > num) {
+ closest = project_target - num;
+ best = images[i];
+ }
+ }
+ }
+ return best;
+ });
+};
+
+// Returns a promise.
+module.exports.list_started = function () {
+ return Adb.devices({emulators: true});
+};
+
+// Returns a promise.
+// TODO: we should remove this, there's a more robust method under android_sdk.js
+module.exports.list_targets = function () {
+ return superspawn.spawn('android', ['list', 'targets'], {cwd: os.tmpdir()}).then(function (output) {
+ var target_out = output.split('\n');
+ var targets = [];
+ for (var i = target_out.length; i >= 0; i--) {
+ if (target_out[i].match(/id:/)) {
+ targets.push(targets[i].split(' ')[1]);
+ }
+ }
+ return targets;
+ });
+};
+
+/*
+ * Gets unused port for android emulator, between 5554 and 5584
+ * Returns a promise.
+ */
+module.exports.get_available_port = function () {
+ var self = this;
+
+ return self.list_started().then(function (emulators) {
+ for (var p = 5584; p >= 5554; p -= 2) {
+ if (emulators.indexOf('emulator-' + p) === -1) {
+ events.emit('verbose', 'Found available port: ' + p);
+ return p;
+ }
+ }
+ throw new CordovaError('Could not find an available avd port');
+ });
+};
+
+/*
+ * Starts an emulator with the given ID,
+ * and returns the started ID of that emulator.
+ * If no ID is given it will use the first image available,
+ * if no image is available it will error out (maybe create one?).
+ * If no boot timeout is given or the value is negative it will wait forever for
+ * the emulator to boot
+ *
+ * Returns a promise.
+ */
+module.exports.start = function (emulator_ID, boot_timeout) {
+ var self = this;
+
+ return Q().then(function () {
+ if (emulator_ID) return Q(emulator_ID);
+
+ return self.best_image().then(function (best) {
+ if (best && best.name) {
+ events.emit('warn', 'No emulator specified, defaulting to ' + best.name);
+ return best.name;
+ }
+
+ var androidCmd = check_reqs.getAbsoluteAndroidCmd();
+ return Q.reject(new CordovaError('No emulator images (avds) found.\n' +
+ '1. Download desired System Image by running: ' + androidCmd + ' sdk\n' +
+ '2. Create an AVD by running: ' + androidCmd + ' avd\n' +
+ 'HINT: For a faster emulator, use an Intel System Image and install the HAXM device driver\n'));
+ });
+ }).then(function (emulatorId) {
+ return self.get_available_port().then(function (port) {
+ // Figure out the directory the emulator binary runs in, and set the cwd to that directory.
+ // Workaround for https://code.google.com/p/android/issues/detail?id=235461
+ var emulator_dir = path.dirname(shelljs.which('emulator'));
+ var args = ['-avd', emulatorId, '-port', port];
+ // Don't wait for it to finish, since the emulator will probably keep running for a long time.
+ child_process
+ .spawn('emulator', args, { stdio: 'inherit', detached: true, cwd: emulator_dir })
+ .unref();
+
+ // wait for emulator to start
+ events.emit('log', 'Waiting for emulator to start...');
+ return self.wait_for_emulator(port);
+ });
+ }).then(function (emulatorId) {
+ if (!emulatorId) { return Q.reject(new CordovaError('Failed to start emulator')); }
+
+ // wait for emulator to boot up
+ process.stdout.write('Waiting for emulator to boot (this may take a while)...');
+ return self.wait_for_boot(emulatorId, boot_timeout).then(function (success) {
+ if (success) {
+ events.emit('log', 'BOOT COMPLETE');
+ // unlock screen
+ return Adb.shell(emulatorId, 'input keyevent 82').then(function () {
+ // return the new emulator id for the started emulators
+ return emulatorId;
+ });
+ } else {
+ // We timed out waiting for the boot to happen
+ return null;
+ }
+ });
+ });
+};
+
+/*
+ * Waits for an emulator to boot on a given port.
+ * Returns this emulator's ID in a promise.
+ */
+module.exports.wait_for_emulator = function (port) {
+ var self = this;
+ return Q().then(function () {
+ var emulator_id = 'emulator-' + port;
+ return Adb.shell(emulator_id, 'getprop dev.bootcomplete').then(function (output) {
+ if (output.indexOf('1') >= 0) {
+ return emulator_id;
+ }
+ return self.wait_for_emulator(port);
+ }, function (error) {
+ if ((error && error.message &&
+ (error.message.indexOf('not found') > -1)) ||
+ (error.message.indexOf('device offline') > -1)) {
+ // emulator not yet started, continue waiting
+ return self.wait_for_emulator(port);
+ } else {
+ // something unexpected has happened
+ throw error;
+ }
+ });
+ });
+};
+
+/*
+ * Waits for the core android process of the emulator to start. Returns a
+ * promise that resolves to a boolean indicating success. Not specifying a
+ * time_remaining or passing a negative value will cause it to wait forever
+ */
+module.exports.wait_for_boot = function (emulator_id, time_remaining) {
+ var self = this;
+ return Adb.shell(emulator_id, 'ps').then(function (output) {
+ if (output.match(/android\.process\.acore/)) {
+ return true;
+ } else if (time_remaining === 0) {
+ return false;
+ } else {
+ process.stdout.write('.');
+
+ // Check at regular intervals
+ return Q.delay(time_remaining < CHECK_BOOTED_INTERVAL ? time_remaining : CHECK_BOOTED_INTERVAL).then(function () {
+ var updated_time = time_remaining >= 0 ? Math.max(time_remaining - CHECK_BOOTED_INTERVAL, 0) : time_remaining;
+ return self.wait_for_boot(emulator_id, updated_time);
+ });
+ }
+ });
+};
+
+/*
+ * Create avd
+ * TODO : Enter the stdin input required to complete the creation of an avd.
+ * Returns a promise.
+ */
+module.exports.create_image = function (name, target) {
+ console.log('Creating new avd named ' + name);
+ if (target) {
+ return superspawn.spawn('android', ['create', 'avd', '--name', name, '--target', target]).then(null, function (error) {
+ console.error('ERROR : Failed to create emulator image : ');
+ console.error(' Do you have the latest android targets including ' + target + '?');
+ console.error(error);
+ });
+ } else {
+ console.log('WARNING : Project target not found, creating avd with a different target but the project may fail to install.');
+ // TODO: there's a more robust method for finding targets in android_sdk.js
+ return superspawn.spawn('android', ['create', 'avd', '--name', name, '--target', this.list_targets()[0]]).then(function () {
+ // TODO: This seems like another error case, even though it always happens.
+ console.error('ERROR : Unable to create an avd emulator, no targets found.');
+ console.error('Ensure you have targets available by running the "android" command');
+ return Q.reject();
+ }, function (error) {
+ console.error('ERROR : Failed to create emulator image : ');
+ console.error(error);
+ });
+ }
+};
+
+module.exports.resolveTarget = function (target) {
+ return this.list_started().then(function (emulator_list) {
+ if (emulator_list.length < 1) {
+ return Q.reject('No running Android emulators found, please start an emulator before deploying your project.');
+ }
+
+ // default emulator
+ target = target || emulator_list[0];
+ if (emulator_list.indexOf(target) < 0) {
+ return Q.reject('Unable to find target \'' + target + '\'. Failed to deploy to emulator.');
+ }
+
+ return build.detectArchitecture(target).then(function (arch) {
+ return {target: target, arch: arch, isEmulator: true};
+ });
+ });
+};
+
+/*
+ * Installs a previously built application on the emulator and launches it.
+ * If no target is specified, then it picks one.
+ * If no started emulators are found, error out.
+ * Returns a promise.
+ */
+module.exports.install = function (givenTarget, buildResults) {
+
+ var target;
+ // We need to find the proper path to the Android Manifest
+ var manifestPath = path.join(__dirname, '..', '..', 'app', 'src', 'main', 'AndroidManifest.xml');
+ if (buildResults.buildMethod === 'gradle') {
+ manifestPath = path.join(__dirname, '../../AndroidManifest.xml');
+ }
+ var manifest = new AndroidManifest(manifestPath);
+ var pkgName = manifest.getPackageId();
+
+ // resolve the target emulator
+ return Q().then(function () {
+ if (givenTarget && typeof givenTarget === 'object') {
+ return givenTarget;
+ } else {
+ return module.exports.resolveTarget(givenTarget);
+ }
+
+ // set the resolved target
+ }).then(function (resolvedTarget) {
+ target = resolvedTarget;
+
+ // install the app
+ }).then(function () {
+ // This promise is always resolved, even if 'adb uninstall' fails to uninstall app
+ // or the app doesn't installed at all, so no error catching needed.
+ return Q.when().then(function () {
+
+ var apk_path = build.findBestApkForArchitecture(buildResults, target.arch);
+ var execOptions = {
+ cwd: os.tmpdir(),
+ timeout: INSTALL_COMMAND_TIMEOUT, // in milliseconds
+ killSignal: EXEC_KILL_SIGNAL
+ };
+
+ events.emit('log', 'Using apk: ' + apk_path);
+ events.emit('log', 'Package name: ' + pkgName);
+ events.emit('verbose', 'Installing app on emulator...');
+
+ // A special function to call adb install in specific environment w/ specific options.
+ // Introduced as a part of fix for http://issues.apache.org/jira/browse/CB-9119
+ // to workaround sporadic emulator hangs
+ function adbInstallWithOptions (target, apk, opts) {
+ events.emit('verbose', 'Installing apk ' + apk + ' on ' + target + '...');
+
+ var command = 'adb -s ' + target + ' install -r "' + apk + '"';
+ return Q.promise(function (resolve, reject) {
+ child_process.exec(command, opts, function (err, stdout, stderr) {
+ if (err) reject(new CordovaError('Error executing "' + command + '": ' + stderr));
+ // adb does not return an error code even if installation fails. Instead it puts a specific
+ // message to stdout, so we have to use RegExp matching to detect installation failure.
+ else if (/Failure/.test(stdout)) {
+ if (stdout.match(/INSTALL_PARSE_FAILED_NO_CERTIFICATES/)) {
+ stdout += 'Sign the build using \'-- --keystore\' or \'--buildConfig\'' +
+ ' or sign and deploy the unsigned apk manually using Android tools.';
+ } else if (stdout.match(/INSTALL_FAILED_VERSION_DOWNGRADE/)) {
+ stdout += 'You\'re trying to install apk with a lower versionCode that is already installed.' +
+ '\nEither uninstall an app or increment the versionCode.';
+ }
+
+ reject(new CordovaError('Failed to install apk to emulator: ' + stdout));
+ } else resolve(stdout);
+ });
+ });
+ }
+
+ function installPromise () {
+ return adbInstallWithOptions(target.target, apk_path, execOptions).catch(function (error) {
+ // CB-9557 CB-10157 only uninstall and reinstall app if the one that
+ // is already installed on device was signed w/different certificate
+ if (!/INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES/.test(error.toString())) { throw error; }
+
+ events.emit('warn', 'Uninstalling app from device and reinstalling it because the ' +
+ 'currently installed app was signed with different key');
+
+ // This promise is always resolved, even if 'adb uninstall' fails to uninstall app
+ // or the app doesn't installed at all, so no error catching needed.
+ return Adb.uninstall(target.target, pkgName).then(function () {
+ return adbInstallWithOptions(target.target, apk_path, execOptions);
+ });
+ });
+ }
+
+ return retry.retryPromise(NUM_INSTALL_RETRIES, installPromise).then(function (output) {
+ events.emit('log', 'INSTALL SUCCESS');
+ });
+ });
+ // unlock screen
+ }).then(function () {
+
+ events.emit('verbose', 'Unlocking screen...');
+ return Adb.shell(target.target, 'input keyevent 82');
+ }).then(function () {
+ Adb.start(target.target, pkgName + '/.' + manifest.getActivity().getName());
+ // report success or failure
+ }).then(function (output) {
+ events.emit('log', 'LAUNCH SUCCESS');
+ });
+};
diff --git a/platforms/android/cordova/lib/getASPath.bat b/platforms/android/cordova/lib/getASPath.bat
new file mode 100644
index 0000000..14dad43
--- /dev/null
+++ b/platforms/android/cordova/lib/getASPath.bat
@@ -0,0 +1,3 @@
+@ECHO OFF
+for /f "tokens=2*" %%a in ('REG QUERY "HKEY_LOCAL_MACHINE\SOFTWARE\Android Studio" /v Path') do set "ASPath=%%~b"
+ECHO %ASPath%
\ No newline at end of file
diff --git a/platforms/android/cordova/lib/install-device b/platforms/android/cordova/lib/install-device
new file mode 100755
index 0000000..fc4b784
--- /dev/null
+++ b/platforms/android/cordova/lib/install-device
@@ -0,0 +1,42 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+var device = require('./device'),
+ args = process.argv;
+
+if(args.length > 2) {
+ var install_target;
+ if (args[2].substring(0, 9) == '--target=') {
+ install_target = args[2].substring(9, args[2].length);
+ device.install(install_target).done(null, function(err) {
+ console.error('ERROR: ' + err);
+ process.exit(2);
+ });
+ } else {
+ console.error('ERROR : argument \'' + args[2] + '\' not recognized.');
+ process.exit(2);
+ }
+} else {
+ device.install().done(null, function(err) {
+ console.error('ERROR: ' + err);
+ process.exit(2);
+ });
+}
diff --git a/platforms/android/cordova/lib/install-device.bat b/platforms/android/cordova/lib/install-device.bat
new file mode 100644
index 0000000..109b470
--- /dev/null
+++ b/platforms/android/cordova/lib/install-device.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0install-device"
+IF EXIST %script_path% (
+ node %script_path% %*
+) ELSE (
+ ECHO.
+ ECHO ERROR: Could not find 'install-device' script in 'cordova\lib' folder, aborting...>&2
+ EXIT /B 1
+)
\ No newline at end of file
diff --git a/platforms/android/cordova/lib/install-emulator b/platforms/android/cordova/lib/install-emulator
new file mode 100755
index 0000000..aa2a34f
--- /dev/null
+++ b/platforms/android/cordova/lib/install-emulator
@@ -0,0 +1,38 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+var emulator = require('./emulator'),
+ args = process.argv;
+
+var install_target;
+if(args.length > 2) {
+ if (args[2].substring(0, 9) == '--target=') {
+ install_target = args[2].substring(9, args[2].length);
+ } else {
+ console.error('ERROR : argument \'' + args[2] + '\' not recognized.');
+ process.exit(2);
+ }
+}
+
+emulator.install(install_target).done(null, function(err) {
+ console.error('ERROR: ' + err);
+ process.exit(2);
+});
diff --git a/platforms/android/cordova/lib/install-emulator.bat b/platforms/android/cordova/lib/install-emulator.bat
new file mode 100644
index 0000000..a28c23a
--- /dev/null
+++ b/platforms/android/cordova/lib/install-emulator.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0install-emulator"
+IF EXIST %script_path% (
+ node %script_path% %*
+) ELSE (
+ ECHO.
+ ECHO ERROR: Could not find 'install-emulator' script in 'cordova\lib' folder, aborting...>&2
+ EXIT /B 1
+)
\ No newline at end of file
diff --git a/platforms/android/cordova/lib/list-devices b/platforms/android/cordova/lib/list-devices
new file mode 100755
index 0000000..8e22c7f
--- /dev/null
+++ b/platforms/android/cordova/lib/list-devices
@@ -0,0 +1,34 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+var devices = require('./device');
+
+// Usage support for when args are given
+require('./check_reqs').check_android().then(function() {
+ devices.list().done(function(device_list) {
+ device_list && device_list.forEach(function(dev) {
+ console.log(dev);
+ });
+ }, function(err) {
+ console.error('ERROR: ' + err);
+ process.exit(2);
+ });
+});
diff --git a/platforms/android/cordova/lib/list-devices.bat b/platforms/android/cordova/lib/list-devices.bat
new file mode 100644
index 0000000..ad5f03e
--- /dev/null
+++ b/platforms/android/cordova/lib/list-devices.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0list-devices"
+IF EXIST %script_path% (
+ node %script_path% %*
+) ELSE (
+ ECHO.
+ ECHO ERROR: Could not find 'list-devices' script in 'cordova\lib' folder, aborting...>&2
+ EXIT /B 1
+)
\ No newline at end of file
diff --git a/platforms/android/cordova/lib/list-emulator-images b/platforms/android/cordova/lib/list-emulator-images
new file mode 100755
index 0000000..25e5c81
--- /dev/null
+++ b/platforms/android/cordova/lib/list-emulator-images
@@ -0,0 +1,34 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+var emulators = require('./emulator');
+
+// Usage support for when args are given
+require('./check_reqs').check_android().then(function() {
+ emulators.list_images().done(function(emulator_list) {
+ emulator_list && emulator_list.forEach(function(emu) {
+ console.log(emu.name);
+ });
+ }, function(err) {
+ console.error('ERROR: ' + err);
+ process.exit(2);
+ });
+});
diff --git a/platforms/android/cordova/lib/list-emulator-images.bat b/platforms/android/cordova/lib/list-emulator-images.bat
new file mode 100644
index 0000000..616ffb7
--- /dev/null
+++ b/platforms/android/cordova/lib/list-emulator-images.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0list-emulator-images"
+IF EXIST %script_path% (
+ node %script_path% %*
+) ELSE (
+ ECHO.
+ ECHO ERROR: Could not find 'list-emulator-images' script in 'cordova\lib' folder, aborting...>&2
+ EXIT /B 1
+)
diff --git a/platforms/android/cordova/lib/list-started-emulators b/platforms/android/cordova/lib/list-started-emulators
new file mode 100755
index 0000000..43ebda2
--- /dev/null
+++ b/platforms/android/cordova/lib/list-started-emulators
@@ -0,0 +1,34 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+var emulators = require('./emulator');
+
+// Usage support for when args are given
+require('./check_reqs').check_android().then(function() {
+ emulators.list_started().done(function(emulator_list) {
+ emulator_list && emulator_list.forEach(function(emu) {
+ console.log(emu);
+ });
+ }, function(err) {
+ console.error('ERROR: ' + err);
+ process.exit(2);
+ });
+});
diff --git a/platforms/android/cordova/lib/list-started-emulators.bat b/platforms/android/cordova/lib/list-started-emulators.bat
new file mode 100644
index 0000000..eed02a5
--- /dev/null
+++ b/platforms/android/cordova/lib/list-started-emulators.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0list-started-emulators"
+IF EXIST %script_path% (
+ node %script_path% %*
+) ELSE (
+ ECHO.
+ ECHO ERROR: Could not find 'list-started-emulators' script in 'cordova\lib' folder, aborting...>&2
+ EXIT /B 1
+)
\ No newline at end of file
diff --git a/platforms/android/cordova/lib/log.js b/platforms/android/cordova/lib/log.js
new file mode 100644
index 0000000..ef2dd5c
--- /dev/null
+++ b/platforms/android/cordova/lib/log.js
@@ -0,0 +1,56 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+var path = require('path');
+var os = require('os');
+var Q = require('q');
+var child_process = require('child_process');
+var ROOT = path.join(__dirname, '..', '..');
+
+/*
+ * Starts running logcat in the shell.
+ * Returns a promise.
+ */
+module.exports.run = function () {
+ var d = Q.defer();
+ var adb = child_process.spawn('adb', ['logcat'], {cwd: os.tmpdir()});
+
+ adb.stdout.on('data', function (data) {
+ var lines = data ? data.toString().split('\n') : [];
+ var out = lines.filter(function (x) { return x.indexOf('nativeGetEnabledTags') < 0; });
+ console.log(out.join('\n'));
+ });
+
+ adb.stderr.on('data', console.error);
+ adb.on('close', function (code) {
+ if (code > 0) {
+ d.reject('Failed to run logcat command.');
+ } else d.resolve();
+ });
+
+ return d.promise;
+};
+
+module.exports.help = function () {
+ console.log('Usage: ' + path.relative(process.cwd(), path.join(ROOT, 'cordova', 'log')));
+ console.log('Gives the logcat output on the command line.');
+ process.exit(0);
+};
diff --git a/platforms/android/cordova/lib/plugin-build.gradle b/platforms/android/cordova/lib/plugin-build.gradle
new file mode 100644
index 0000000..bf8c59a
--- /dev/null
+++ b/platforms/android/cordova/lib/plugin-build.gradle
@@ -0,0 +1,72 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+// GENERATED FILE! DO NOT EDIT!
+
+buildscript {
+ repositories {
+ jcenter()
+ maven {
+ url "https://maven.google.com"
+ }
+ }
+
+ // Switch the Android Gradle plugin version requirement depending on the
+ // installed version of Gradle. This dependency is documented at
+ // http://tools.android.com/tech-docs/new-build-system/version-compatibility
+ // and https://issues.apache.org/jira/browse/CB-8143
+ dependencies {
+ classpath 'com.android.tools.build:gradle:1.0.0+'
+ }
+}
+
+apply plugin: 'com.android.library'
+
+dependencies {
+ compile fileTree(dir: 'libs', include: '*.jar')
+ debugCompile project(path: ":CordovaLib", configuration: "debug")
+ releaseCompile project(path: ":CordovaLib", configuration: "release")
+}
+
+android {
+ compileSdkVersion cdvCompileSdkVersion
+ buildToolsVersion cdvBuildToolsVersion
+ publishNonDefault true
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_6
+ targetCompatibility JavaVersion.VERSION_1_6
+ }
+
+ sourceSets {
+ main {
+ manifest.srcFile 'AndroidManifest.xml'
+ java.srcDirs = ['src']
+ resources.srcDirs = ['src']
+ aidl.srcDirs = ['src']
+ renderscript.srcDirs = ['src']
+ res.srcDirs = ['res']
+ assets.srcDirs = ['assets']
+ jniLibs.srcDirs = ['libs']
+ }
+ }
+}
+
+if (file('build-extras.gradle').exists()) {
+ apply from: 'build-extras.gradle'
+}
diff --git a/platforms/android/cordova/lib/pluginHandlers.js b/platforms/android/cordova/lib/pluginHandlers.js
new file mode 100644
index 0000000..842489a
--- /dev/null
+++ b/platforms/android/cordova/lib/pluginHandlers.js
@@ -0,0 +1,320 @@
+/*
+ * Licensed 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.
+ *
+*/
+
+/* jshint unused: vars */
+
+var fs = require('fs');
+var path = require('path');
+var shell = require('shelljs');
+var events = require('cordova-common').events;
+var CordovaError = require('cordova-common').CordovaError;
+
+var handlers = {
+ 'source-file': {
+ install: function (obj, plugin, project, options) {
+ if (!obj.src) throw new CordovaError(generateAttributeError('src', 'source-file', plugin.id));
+ if (!obj.targetDir) throw new CordovaError(generateAttributeError('target-dir', 'source-file', plugin.id));
+
+ var dest = path.join(obj.targetDir, path.basename(obj.src));
+
+ // TODO: This code needs to be replaced, since the core plugins need to be re-mapped to a different location in
+ // a later plugins release. This is for legacy plugins to work with Cordova.
+
+ if (options && options.android_studio === true) {
+ // If a Java file is using the new directory structure, don't penalize it
+ if (!obj.targetDir.includes('app/src/main')) {
+ if (obj.src.endsWith('.java')) {
+ dest = path.join('app/src/main/java', obj.targetDir.substring(4), path.basename(obj.src));
+ } else if (obj.src.endsWith('.xml')) {
+ // We are making a huge assumption here that XML files will be going to res/xml or values/xml
+ dest = path.join('app/src/main', obj.targetDir, path.basename(obj.src));
+ }
+ }
+ }
+
+ if (options && options.force) {
+ copyFile(plugin.dir, obj.src, project.projectDir, dest, !!(options && options.link));
+ } else {
+ copyNewFile(plugin.dir, obj.src, project.projectDir, dest, !!(options && options.link));
+ }
+ },
+ uninstall: function (obj, plugin, project, options) {
+ var dest = path.join(obj.targetDir, path.basename(obj.src));
+
+ if (options && options.android_studio === true) {
+ dest = path.join('app/src/main/java', obj.targetDir.substring(4), path.basename(obj.src));
+ }
+
+ deleteJava(project.projectDir, dest);
+ }
+ },
+ 'lib-file': {
+ install: function (obj, plugin, project, options) {
+ var dest = path.join('libs', path.basename(obj.src));
+ if (options && options.android_studio === true) {
+ dest = path.join('app/libs', path.basename(obj.src));
+ }
+ copyFile(plugin.dir, obj.src, project.projectDir, dest, !!(options && options.link));
+ },
+ uninstall: function (obj, plugin, project, options) {
+ var dest = path.join('libs', path.basename(obj.src));
+ if (options && options.android_studio === true) {
+ dest = path.join('app/libs', path.basename(obj.src));
+ }
+ removeFile(project.projectDir, dest);
+ }
+ },
+ 'resource-file': {
+ install: function (obj, plugin, project, options) {
+ var dest = path.normalize(obj.target);
+ if (options && options.android_studio === true) {
+ dest = path.join('app/src/main', dest);
+ }
+ copyFile(plugin.dir, obj.src, project.projectDir, dest, !!(options && options.link));
+ },
+ uninstall: function (obj, plugin, project, options) {
+ var dest = path.normalize(obj.target);
+ if (options && options.android_studio === true) {
+ dest = path.join('app/src/main', dest);
+ }
+ removeFile(project.projectDir, dest);
+ }
+ },
+ 'framework': {
+ install: function (obj, plugin, project, options) {
+ var src = obj.src;
+ if (!src) throw new CordovaError(generateAttributeError('src', 'framework', plugin.id));
+
+ events.emit('verbose', 'Installing Android library: ' + src);
+ var parentDir = obj.parent ? path.resolve(project.projectDir, obj.parent) : project.projectDir;
+ var subDir;
+
+ if (obj.custom) {
+ var subRelativeDir = project.getCustomSubprojectRelativeDir(plugin.id, src);
+ copyNewFile(plugin.dir, src, project.projectDir, subRelativeDir, !!(options && options.link));
+ subDir = path.resolve(project.projectDir, subRelativeDir);
+ } else {
+ obj.type = 'sys';
+ subDir = src;
+ }
+
+ if (obj.type === 'gradleReference') {
+ project.addGradleReference(parentDir, subDir);
+ } else if (obj.type === 'sys') {
+ project.addSystemLibrary(parentDir, subDir);
+ } else {
+ project.addSubProject(parentDir, subDir);
+ }
+ },
+ uninstall: function (obj, plugin, project, options) {
+ var src = obj.src;
+ if (!src) throw new CordovaError(generateAttributeError('src', 'framework', plugin.id));
+
+ events.emit('verbose', 'Uninstalling Android library: ' + src);
+ var parentDir = obj.parent ? path.resolve(project.projectDir, obj.parent) : project.projectDir;
+ var subDir;
+
+ if (obj.custom) {
+ var subRelativeDir = project.getCustomSubprojectRelativeDir(plugin.id, src);
+ removeFile(project.projectDir, subRelativeDir);
+ subDir = path.resolve(project.projectDir, subRelativeDir);
+ // If it's the last framework in the plugin, remove the parent directory.
+ var parDir = path.dirname(subDir);
+ if (fs.existsSync(parDir) && fs.readdirSync(parDir).length === 0) {
+ fs.rmdirSync(parDir);
+ }
+ } else {
+ obj.type = 'sys';
+ subDir = src;
+ }
+
+ if (obj.type === 'gradleReference') {
+ project.removeGradleReference(parentDir, subDir);
+ } else if (obj.type === 'sys') {
+ project.removeSystemLibrary(parentDir, subDir);
+ } else {
+ project.removeSubProject(parentDir, subDir);
+ }
+ }
+ },
+ asset: {
+ install: function (obj, plugin, project, options) {
+ if (!obj.src) {
+ throw new CordovaError(generateAttributeError('src', 'asset', plugin.id));
+ }
+ if (!obj.target) {
+ throw new CordovaError(generateAttributeError('target', 'asset', plugin.id));
+ }
+
+ copyFile(plugin.dir, obj.src, project.www, obj.target);
+ if (options && options.usePlatformWww) {
+ // CB-11022 copy file to both directories if usePlatformWww is specified
+ copyFile(plugin.dir, obj.src, project.platformWww, obj.target);
+ }
+ },
+ uninstall: function (obj, plugin, project, options) {
+ var target = obj.target || obj.src;
+
+ if (!target) throw new CordovaError(generateAttributeError('target', 'asset', plugin.id));
+
+ removeFileF(path.resolve(project.www, target));
+ removeFileF(path.resolve(project.www, 'plugins', plugin.id));
+ if (options && options.usePlatformWww) {
+ // CB-11022 remove file from both directories if usePlatformWww is specified
+ removeFileF(path.resolve(project.platformWww, target));
+ removeFileF(path.resolve(project.platformWww, 'plugins', plugin.id));
+ }
+ }
+ },
+ 'js-module': {
+ install: function (obj, plugin, project, options) {
+ // Copy the plugin's files into the www directory.
+ var moduleSource = path.resolve(plugin.dir, obj.src);
+ var moduleName = plugin.id + '.' + (obj.name || path.basename(obj.src, path.extname(obj.src)));
+
+ // Read in the file, prepend the cordova.define, and write it back out.
+ var scriptContent = fs.readFileSync(moduleSource, 'utf-8').replace(/^\ufeff/, ''); // Window BOM
+ if (moduleSource.match(/.*\.json$/)) {
+ scriptContent = 'module.exports = ' + scriptContent;
+ }
+ scriptContent = 'cordova.define("' + moduleName + '", function(require, exports, module) {\n' + scriptContent + '\n});\n';
+
+ var wwwDest = path.resolve(project.www, 'plugins', plugin.id, obj.src);
+ shell.mkdir('-p', path.dirname(wwwDest));
+ fs.writeFileSync(wwwDest, scriptContent, 'utf-8');
+
+ if (options && options.usePlatformWww) {
+ // CB-11022 copy file to both directories if usePlatformWww is specified
+ var platformWwwDest = path.resolve(project.platformWww, 'plugins', plugin.id, obj.src);
+ shell.mkdir('-p', path.dirname(platformWwwDest));
+ fs.writeFileSync(platformWwwDest, scriptContent, 'utf-8');
+ }
+ },
+ uninstall: function (obj, plugin, project, options) {
+ var pluginRelativePath = path.join('plugins', plugin.id, obj.src);
+ removeFileAndParents(project.www, pluginRelativePath);
+ if (options && options.usePlatformWww) {
+ // CB-11022 remove file from both directories if usePlatformWww is specified
+ removeFileAndParents(project.platformWww, pluginRelativePath);
+ }
+ }
+ }
+};
+
+module.exports.getInstaller = function (type) {
+ if (handlers[type] && handlers[type].install) {
+ return handlers[type].install;
+ }
+
+ events.emit('verbose', '<' + type + '> is not supported for android plugins');
+};
+
+module.exports.getUninstaller = function (type) {
+ if (handlers[type] && handlers[type].uninstall) {
+ return handlers[type].uninstall;
+ }
+
+ events.emit('verbose', '<' + type + '> is not supported for android plugins');
+};
+
+function copyFile (plugin_dir, src, project_dir, dest, link) {
+ src = path.resolve(plugin_dir, src);
+ if (!fs.existsSync(src)) throw new CordovaError('"' + src + '" not found!');
+
+ // check that src path is inside plugin directory
+ var real_path = fs.realpathSync(src);
+ var real_plugin_path = fs.realpathSync(plugin_dir);
+ if (real_path.indexOf(real_plugin_path) !== 0) { throw new CordovaError('File "' + src + '" is located outside the plugin directory "' + plugin_dir + '"'); }
+
+ dest = path.resolve(project_dir, dest);
+
+ // check that dest path is located in project directory
+ if (dest.indexOf(project_dir) !== 0) { throw new CordovaError('Destination "' + dest + '" for source file "' + src + '" is located outside the project'); }
+
+ shell.mkdir('-p', path.dirname(dest));
+ if (link) {
+ symlinkFileOrDirTree(src, dest);
+ } else if (fs.statSync(src).isDirectory()) {
+ // XXX shelljs decides to create a directory when -R|-r is used which sucks. http://goo.gl/nbsjq
+ shell.cp('-Rf', src + '/*', dest);
+ } else {
+ shell.cp('-f', src, dest);
+ }
+}
+
+// Same as copy file but throws error if target exists
+function copyNewFile (plugin_dir, src, project_dir, dest, link) {
+ var target_path = path.resolve(project_dir, dest);
+ if (fs.existsSync(target_path)) { throw new CordovaError('"' + target_path + '" already exists!'); }
+
+ copyFile(plugin_dir, src, project_dir, dest, !!link);
+}
+
+function symlinkFileOrDirTree (src, dest) {
+ if (fs.existsSync(dest)) {
+ shell.rm('-Rf', dest);
+ }
+
+ if (fs.statSync(src).isDirectory()) {
+ shell.mkdir('-p', dest);
+ fs.readdirSync(src).forEach(function (entry) {
+ symlinkFileOrDirTree(path.join(src, entry), path.join(dest, entry));
+ });
+ } else {
+ fs.symlinkSync(path.relative(fs.realpathSync(path.dirname(dest)), src), dest);
+ }
+}
+
+// checks if file exists and then deletes. Error if doesn't exist
+function removeFile (project_dir, src) {
+ var file = path.resolve(project_dir, src);
+ shell.rm('-Rf', file);
+}
+
+// deletes file/directory without checking
+function removeFileF (file) {
+ shell.rm('-Rf', file);
+}
+
+// Sometimes we want to remove some java, and prune any unnecessary empty directories
+function deleteJava (project_dir, destFile) {
+ removeFileAndParents(project_dir, destFile, 'src');
+}
+
+function removeFileAndParents (baseDir, destFile, stopper) {
+ stopper = stopper || '.';
+ var file = path.resolve(baseDir, destFile);
+ if (!fs.existsSync(file)) return;
+
+ removeFileF(file);
+
+ // check if directory is empty
+ var curDir = path.dirname(file);
+
+ while (curDir !== path.resolve(baseDir, stopper)) {
+ if (fs.existsSync(curDir) && fs.readdirSync(curDir).length === 0) {
+ fs.rmdirSync(curDir);
+ curDir = path.resolve(curDir, '..');
+ } else {
+ // directory not empty...do nothing
+ break;
+ }
+ }
+}
+
+function generateAttributeError (attribute, element, id) {
+ return 'Required attribute "' + attribute + '" not specified in <' + element + '> element from plugin: ' + id;
+}
diff --git a/platforms/android/cordova/lib/prepare.js b/platforms/android/cordova/lib/prepare.js
new file mode 100644
index 0000000..ac63f8a
--- /dev/null
+++ b/platforms/android/cordova/lib/prepare.js
@@ -0,0 +1,480 @@
+/**
+ 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.
+*/
+/* eslint no-useless-escape: 0 */
+
+var Q = require('q');
+var fs = require('fs');
+var path = require('path');
+var shell = require('shelljs');
+var events = require('cordova-common').events;
+var AndroidManifest = require('./AndroidManifest');
+var checkReqs = require('./check_reqs');
+var xmlHelpers = require('cordova-common').xmlHelpers;
+var CordovaError = require('cordova-common').CordovaError;
+var ConfigParser = require('cordova-common').ConfigParser;
+var FileUpdater = require('cordova-common').FileUpdater;
+var PlatformJson = require('cordova-common').PlatformJson;
+var PlatformMunger = require('cordova-common').ConfigChanges.PlatformMunger;
+var PluginInfoProvider = require('cordova-common').PluginInfoProvider;
+
+module.exports.prepare = function (cordovaProject, options) {
+ var self = this;
+
+ var platformJson = PlatformJson.load(this.locations.root, this.platform);
+ var munger = new PlatformMunger(this.platform, this.locations.root, platformJson, new PluginInfoProvider());
+
+ this._config = updateConfigFilesFrom(cordovaProject.projectConfig, munger, this.locations);
+
+ // Update own www dir with project's www assets and plugins' assets and js-files
+ return Q.when(updateWww(cordovaProject, this.locations)).then(function () {
+ // update project according to config.xml changes.
+ return updateProjectAccordingTo(self._config, self.locations);
+ }).then(function () {
+ updateIcons(cordovaProject, path.relative(cordovaProject.root, self.locations.res));
+ updateSplashes(cordovaProject, path.relative(cordovaProject.root, self.locations.res));
+ updateFileResources(cordovaProject, path.relative(cordovaProject.root, self.locations.root));
+ }).then(function () {
+ events.emit('verbose', 'Prepared android project successfully');
+ });
+};
+
+module.exports.clean = function (options) {
+ // A cordovaProject isn't passed into the clean() function, because it might have
+ // been called from the platform shell script rather than the CLI. Check for the
+ // noPrepare option passed in by the non-CLI clean script. If that's present, or if
+ // there's no config.xml found at the project root, then don't clean prepared files.
+ var projectRoot = path.resolve(this.root, '../..');
+ if ((options && options.noPrepare) || !fs.existsSync(this.locations.configXml) ||
+ !fs.existsSync(this.locations.configXml)) {
+ return Q();
+ }
+
+ var projectConfig = new ConfigParser(this.locations.configXml);
+
+ var self = this;
+ return Q().then(function () {
+ cleanWww(projectRoot, self.locations);
+ cleanIcons(projectRoot, projectConfig, path.relative(projectRoot, self.locations.res));
+ cleanSplashes(projectRoot, projectConfig, path.relative(projectRoot, self.locations.res));
+ cleanFileResources(projectRoot, projectConfig, path.relative(projectRoot, self.locations.root));
+ });
+};
+
+/**
+ * Updates config files in project based on app's config.xml and config munge,
+ * generated by plugins.
+ *
+ * @param {ConfigParser} sourceConfig A project's configuration that will
+ * be merged into platform's config.xml
+ * @param {ConfigChanges} configMunger An initialized ConfigChanges instance
+ * for this platform.
+ * @param {Object} locations A map of locations for this platform
+ *
+ * @return {ConfigParser} An instance of ConfigParser, that
+ * represents current project's configuration. When returned, the
+ * configuration is already dumped to appropriate config.xml file.
+ */
+function updateConfigFilesFrom (sourceConfig, configMunger, locations) {
+ events.emit('verbose', 'Generating platform-specific config.xml from defaults for android at ' + locations.configXml);
+
+ // First cleanup current config and merge project's one into own
+ // Overwrite platform config.xml with defaults.xml.
+ shell.cp('-f', locations.defaultConfigXml, locations.configXml);
+
+ // Then apply config changes from global munge to all config files
+ // in project (including project's config)
+ configMunger.reapply_global_munge().save_all();
+
+ events.emit('verbose', 'Merging project\'s config.xml into platform-specific android config.xml');
+ // Merge changes from app's config.xml into platform's one
+ var config = new ConfigParser(locations.configXml);
+ xmlHelpers.mergeXml(sourceConfig.doc.getroot(),
+ config.doc.getroot(), 'android', /* clobber= */true);
+
+ config.write();
+ return config;
+}
+
+/**
+ * Logs all file operations via the verbose event stream, indented.
+ */
+function logFileOp (message) {
+ events.emit('verbose', ' ' + message);
+}
+
+/**
+ * Updates platform 'www' directory by replacing it with contents of
+ * 'platform_www' and app www. Also copies project's overrides' folder into
+ * the platform 'www' folder
+ *
+ * @param {Object} cordovaProject An object which describes cordova project.
+ * @param {Object} destinations An object that contains destination
+ * paths for www files.
+ */
+function updateWww (cordovaProject, destinations) {
+ var sourceDirs = [
+ path.relative(cordovaProject.root, cordovaProject.locations.www),
+ path.relative(cordovaProject.root, destinations.platformWww)
+ ];
+
+ // If project contains 'merges' for our platform, use them as another overrides
+ var merges_path = path.join(cordovaProject.root, 'merges', 'android');
+ if (fs.existsSync(merges_path)) {
+ events.emit('verbose', 'Found "merges/android" folder. Copying its contents into the android project.');
+ sourceDirs.push(path.join('merges', 'android'));
+ }
+
+ var targetDir = path.relative(cordovaProject.root, destinations.www);
+ events.emit(
+ 'verbose', 'Merging and updating files from [' + sourceDirs.join(', ') + '] to ' + targetDir);
+ FileUpdater.mergeAndUpdateDir(
+ sourceDirs, targetDir, { rootDir: cordovaProject.root }, logFileOp);
+}
+
+/**
+ * Cleans all files from the platform 'www' directory.
+ */
+function cleanWww (projectRoot, locations) {
+ var targetDir = path.relative(projectRoot, locations.www);
+ events.emit('verbose', 'Cleaning ' + targetDir);
+
+ // No source paths are specified, so mergeAndUpdateDir() will clear the target directory.
+ FileUpdater.mergeAndUpdateDir(
+ [], targetDir, { rootDir: projectRoot, all: true }, logFileOp);
+}
+
+/**
+ * Updates project structure and AndroidManifest according to project's configuration.
+ *
+ * @param {ConfigParser} platformConfig A project's configuration that will
+ * be used to update project
+ * @param {Object} locations A map of locations for this platform
+ */
+function updateProjectAccordingTo (platformConfig, locations) {
+ // Update app name by editing res/values/strings.xml
+ var strings = xmlHelpers.parseElementtreeSync(locations.strings);
+
+ var name = platformConfig.name();
+ strings.find('string[@name="app_name"]').text = name.replace(/\'/g, '\\\'');
+
+ var shortName = platformConfig.shortName && platformConfig.shortName();
+ if (shortName && shortName !== name) {
+ strings.find('string[@name="launcher_name"]').text = shortName.replace(/\'/g, '\\\'');
+ }
+
+ fs.writeFileSync(locations.strings, strings.write({indent: 4}), 'utf-8');
+ events.emit('verbose', 'Wrote out android application name "' + name + '" to ' + locations.strings);
+
+ // Java packages cannot support dashes
+ var androidPkgName = (platformConfig.android_packageName() || platformConfig.packageName()).replace(/-/g, '_');
+
+ var manifest = new AndroidManifest(locations.manifest);
+ var manifestId = manifest.getPackageId();
+
+ manifest.getActivity()
+ .setOrientation(platformConfig.getPreference('orientation'))
+ .setLaunchMode(findAndroidLaunchModePreference(platformConfig));
+
+ manifest.setVersionName(platformConfig.version())
+ .setVersionCode(platformConfig.android_versionCode() || default_versionCode(platformConfig.version()))
+ .setPackageId(androidPkgName)
+ .setMinSdkVersion(platformConfig.getPreference('android-minSdkVersion', 'android'))
+ .setMaxSdkVersion(platformConfig.getPreference('android-maxSdkVersion', 'android'))
+ .setTargetSdkVersion(platformConfig.getPreference('android-targetSdkVersion', 'android'))
+ .write();
+
+ // Java file paths shouldn't be hard coded
+ var javaPattern = path.join(locations.javaSrc, manifestId.replace(/\./g, '/'), '*.java');
+ var java_files = shell.ls(javaPattern).filter(function (f) {
+ return shell.grep(/extends\s+CordovaActivity/g, f);
+ });
+
+ if (java_files.length === 0) {
+ throw new CordovaError('No Java files found that extend CordovaActivity.');
+ } else if (java_files.length > 1) {
+ events.emit('log', 'Multiple candidate Java files that extend CordovaActivity found. Guessing at the first one, ' + java_files[0]);
+ }
+
+ var destFile = path.join(locations.root, 'app', 'src', 'main', 'java', androidPkgName.replace(/\./g, '/'), path.basename(java_files[0]));
+ shell.mkdir('-p', path.dirname(destFile));
+ shell.sed(/package [\w\.]*;/, 'package ' + androidPkgName + ';', java_files[0]).to(destFile);
+ events.emit('verbose', 'Wrote out Android package name "' + androidPkgName + '" to ' + destFile);
+
+ var removeOrigPkg = checkReqs.isWindows() || checkReqs.isDarwin() ?
+ manifestId.toUpperCase() !== androidPkgName.toUpperCase() :
+ manifestId !== androidPkgName;
+
+ if (removeOrigPkg) {
+ // If package was name changed we need to remove old java with main activity
+ shell.rm('-Rf', java_files[0]);
+ // remove any empty directories
+ var currentDir = path.dirname(java_files[0]);
+ var sourcesRoot = path.resolve(locations.root, 'src');
+ while (currentDir !== sourcesRoot) {
+ if (fs.existsSync(currentDir) && fs.readdirSync(currentDir).length === 0) {
+ fs.rmdirSync(currentDir);
+ currentDir = path.resolve(currentDir, '..');
+ } else {
+ break;
+ }
+ }
+ }
+}
+
+// Consturct the default value for versionCode as
+// PATCH + MINOR * 100 + MAJOR * 10000
+// see http://developer.android.com/tools/publishing/versioning.html
+function default_versionCode (version) {
+ var nums = version.split('-')[0].split('.');
+ var versionCode = 0;
+ if (+nums[0]) {
+ versionCode += +nums[0] * 10000;
+ }
+ if (+nums[1]) {
+ versionCode += +nums[1] * 100;
+ }
+ if (+nums[2]) {
+ versionCode += +nums[2];
+ }
+
+ events.emit('verbose', 'android-versionCode not found in config.xml. Generating a code based on version in config.xml (' + version + '): ' + versionCode);
+ return versionCode;
+}
+
+function getImageResourcePath (resourcesDir, type, density, name, sourceName) {
+ if (/\.9\.png$/.test(sourceName)) {
+ name = name.replace(/\.png$/, '.9.png');
+ }
+ var resourcePath = path.join(resourcesDir, (density ? type + '-' + density : type), name);
+ return resourcePath;
+}
+
+function updateSplashes (cordovaProject, platformResourcesDir) {
+ var resources = cordovaProject.projectConfig.getSplashScreens('android');
+
+ // if there are "splash" elements in config.xml
+ if (resources.length === 0) {
+ events.emit('verbose', 'This app does not have splash screens defined');
+ return;
+ }
+
+ var resourceMap = mapImageResources(cordovaProject.root, platformResourcesDir, 'drawable', 'screen.png');
+
+ var hadMdpi = false;
+ resources.forEach(function (resource) {
+ if (!resource.density) {
+ return;
+ }
+ if (resource.density === 'mdpi') {
+ hadMdpi = true;
+ }
+ var targetPath = getImageResourcePath(
+ platformResourcesDir, 'drawable', resource.density, 'screen.png', path.basename(resource.src));
+ resourceMap[targetPath] = resource.src;
+ });
+
+ // There's no "default" drawable, so assume default == mdpi.
+ if (!hadMdpi && resources.defaultResource) {
+ var targetPath = getImageResourcePath(
+ platformResourcesDir, 'drawable', 'mdpi', 'screen.png', path.basename(resources.defaultResource.src));
+ resourceMap[targetPath] = resources.defaultResource.src;
+ }
+
+ events.emit('verbose', 'Updating splash screens at ' + platformResourcesDir);
+ FileUpdater.updatePaths(
+ resourceMap, { rootDir: cordovaProject.root }, logFileOp);
+}
+
+function cleanSplashes (projectRoot, projectConfig, platformResourcesDir) {
+ var resources = projectConfig.getSplashScreens('android');
+ if (resources.length > 0) {
+ var resourceMap = mapImageResources(projectRoot, platformResourcesDir, 'drawable', 'screen.png');
+ events.emit('verbose', 'Cleaning splash screens at ' + platformResourcesDir);
+
+ // No source paths are specified in the map, so updatePaths() will delete the target files.
+ FileUpdater.updatePaths(
+ resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
+ }
+}
+
+function updateIcons (cordovaProject, platformResourcesDir) {
+ var icons = cordovaProject.projectConfig.getIcons('android');
+
+ // if there are icon elements in config.xml
+ if (icons.length === 0) {
+ events.emit('verbose', 'This app does not have launcher icons defined');
+ return;
+ }
+
+ var resourceMap = mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'icon.png');
+
+ var android_icons = {};
+ var default_icon;
+ // http://developer.android.com/design/style/iconography.html
+ var sizeToDensityMap = {
+ 36: 'ldpi',
+ 48: 'mdpi',
+ 72: 'hdpi',
+ 96: 'xhdpi',
+ 144: 'xxhdpi',
+ 192: 'xxxhdpi'
+ };
+ // find the best matching icon for a given density or size
+ // @output android_icons
+ var parseIcon = function (icon, icon_size) {
+ // do I have a platform icon for that density already
+ var density = icon.density || sizeToDensityMap[icon_size];
+ if (!density) {
+ // invalid icon defition ( or unsupported size)
+ return;
+ }
+ var previous = android_icons[density];
+ if (previous && previous.platform) {
+ return;
+ }
+ android_icons[density] = icon;
+ };
+
+ // iterate over all icon elements to find the default icon and call parseIcon
+ for (var i = 0; i < icons.length; i++) {
+ var icon = icons[i];
+ var size = icon.width;
+ if (!size) {
+ size = icon.height;
+ }
+ if (!size && !icon.density) {
+ if (default_icon) {
+ events.emit('verbose', 'Found extra default icon: ' + icon.src + ' (ignoring in favor of ' + default_icon.src + ')');
+ } else {
+ default_icon = icon;
+ }
+ } else {
+ parseIcon(icon, size);
+ }
+ }
+
+ // The source paths for icons and splashes are relative to
+ // project's config.xml location, so we use it as base path.
+ for (var density in android_icons) {
+ var targetPath = getImageResourcePath(
+ platformResourcesDir, 'mipmap', density, 'icon.png', path.basename(android_icons[density].src));
+ resourceMap[targetPath] = android_icons[density].src;
+ }
+
+ // There's no "default" drawable, so assume default == mdpi.
+ if (default_icon && !android_icons.mdpi) {
+ var defaultTargetPath = getImageResourcePath(
+ platformResourcesDir, 'mipmap', 'mdpi', 'icon.png', path.basename(default_icon.src));
+ resourceMap[defaultTargetPath] = default_icon.src;
+ }
+
+ events.emit('verbose', 'Updating icons at ' + platformResourcesDir);
+ FileUpdater.updatePaths(
+ resourceMap, { rootDir: cordovaProject.root }, logFileOp);
+}
+
+function cleanIcons (projectRoot, projectConfig, platformResourcesDir) {
+ var icons = projectConfig.getIcons('android');
+ if (icons.length > 0) {
+ var resourceMap = mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'icon.png');
+ events.emit('verbose', 'Cleaning icons at ' + platformResourcesDir);
+
+ // No source paths are specified in the map, so updatePaths() will delete the target files.
+ FileUpdater.updatePaths(
+ resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
+ }
+}
+
+/**
+ * Gets a map containing resources of a specified name from all drawable folders in a directory.
+ */
+function mapImageResources (rootDir, subDir, type, resourceName) {
+ var pathMap = {};
+ shell.ls(path.join(rootDir, subDir, type + '-*')).forEach(function (drawableFolder) {
+ var imagePath = path.join(subDir, path.basename(drawableFolder), resourceName);
+ pathMap[imagePath] = null;
+ });
+ return pathMap;
+}
+
+function updateFileResources (cordovaProject, platformDir) {
+ var files = cordovaProject.projectConfig.getFileResources('android');
+
+ // if there are resource-file elements in config.xml
+ if (files.length === 0) {
+ events.emit('verbose', 'This app does not have additional resource files defined');
+ return;
+ }
+
+ var resourceMap = {};
+ files.forEach(function (res) {
+ var targetPath = path.join(platformDir, res.target);
+ resourceMap[targetPath] = res.src;
+ });
+
+ events.emit('verbose', 'Updating resource files at ' + platformDir);
+ FileUpdater.updatePaths(
+ resourceMap, { rootDir: cordovaProject.root }, logFileOp);
+}
+
+function cleanFileResources (projectRoot, projectConfig, platformDir) {
+ var files = projectConfig.getFileResources('android', true);
+ if (files.length > 0) {
+ events.emit('verbose', 'Cleaning resource files at ' + platformDir);
+
+ var resourceMap = {};
+ files.forEach(function (res) {
+ var filePath = path.join(platformDir, res.target);
+ resourceMap[filePath] = null;
+ });
+
+ FileUpdater.updatePaths(
+ resourceMap, {
+ rootDir: projectRoot, all: true}, logFileOp);
+ }
+}
+
+/**
+ * Gets and validates 'AndroidLaunchMode' prepference from config.xml. Returns
+ * preference value and warns if it doesn't seems to be valid
+ *
+ * @param {ConfigParser} platformConfig A configParser instance for
+ * platform.
+ *
+ * @return {String} Preference's value from config.xml or
+ * default value, if there is no such preference. The default value is
+ * 'singleTop'
+ */
+function findAndroidLaunchModePreference (platformConfig) {
+ var launchMode = platformConfig.getPreference('AndroidLaunchMode');
+ if (!launchMode) {
+ // Return a default value
+ return 'singleTop';
+ }
+
+ var expectedValues = ['standard', 'singleTop', 'singleTask', 'singleInstance'];
+ var valid = expectedValues.indexOf(launchMode) >= 0;
+ if (!valid) {
+ // Note: warn, but leave the launch mode as developer wanted, in case the list of options changes in the future
+ events.emit('warn', 'Unrecognized value for AndroidLaunchMode preference: ' +
+ launchMode + '. Expected values are: ' + expectedValues.join(', '));
+ }
+
+ return launchMode;
+}
diff --git a/platforms/android/cordova/lib/retry.js b/platforms/android/cordova/lib/retry.js
new file mode 100644
index 0000000..c464b9d
--- /dev/null
+++ b/platforms/android/cordova/lib/retry.js
@@ -0,0 +1,68 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+/* jshint node: true */
+
+'use strict';
+
+var events = require('cordova-common').events;
+
+/*
+ * Retry a promise-returning function a number of times, propagating its
+ * results on success or throwing its error on a failed final attempt.
+ *
+ * @arg {Number} attemts_left - The number of times to retry the passed call.
+ * @arg {Function} promiseFunction - A function that returns a promise.
+ * @arg {...} - Arguments to pass to promiseFunction.
+ *
+ * @returns {Promise}
+ */
+module.exports.retryPromise = function (attemts_left, promiseFunction) {
+
+ // NOTE:
+ // get all trailing arguments, by skipping the first two (attemts_left and
+ // promiseFunction) because they shouldn't get passed to promiseFunction
+ var promiseFunctionArguments = Array.prototype.slice.call(arguments, 2);
+
+ return promiseFunction.apply(undefined, promiseFunctionArguments).then(
+
+ // on success pass results through
+ function onFulfilled (value) {
+ return value;
+ },
+
+ // on rejection either retry, or throw the error
+ function onRejected (error) {
+
+ attemts_left -= 1;
+
+ if (attemts_left < 1) {
+ throw error;
+ }
+
+ events.emit('verbose', 'A retried call failed. Retrying ' + attemts_left + ' more time(s).');
+
+ // retry call self again with the same arguments, except attemts_left is now lower
+ var fullArguments = [attemts_left, promiseFunction].concat(promiseFunctionArguments);
+ return module.exports.retryPromise.apply(undefined, fullArguments);
+ }
+ );
+};
diff --git a/platforms/android/cordova/lib/run.js b/platforms/android/cordova/lib/run.js
new file mode 100644
index 0000000..b97fce2
--- /dev/null
+++ b/platforms/android/cordova/lib/run.js
@@ -0,0 +1,132 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+/* jshint loopfunc:true */
+
+var path = require('path');
+var build = require('./build');
+var emulator = require('./emulator');
+var device = require('./device');
+var Q = require('q');
+var events = require('cordova-common').events;
+
+function getInstallTarget (runOptions) {
+ var install_target;
+ if (runOptions.target) {
+ install_target = runOptions.target;
+ } else if (runOptions.device) {
+ install_target = '--device';
+ } else if (runOptions.emulator) {
+ install_target = '--emulator';
+ }
+
+ return install_target;
+}
+
+/**
+ * Runs the application on a device if available. If no device is found, it will
+ * use a started emulator. If no started emulators are found it will attempt
+ * to start an avd. If no avds are found it will error out.
+ *
+ * @param {Object} runOptions various run/build options. See Api.js build/run
+ * methods for reference.
+ *
+ * @return {Promise}
+ */
+module.exports.run = function (runOptions) {
+
+ var self = this;
+ var install_target = getInstallTarget(runOptions);
+
+ return Q().then(function () {
+ if (!install_target) {
+ // no target given, deploy to device if available, otherwise use the emulator.
+ return device.list().then(function (device_list) {
+ if (device_list.length > 0) {
+ events.emit('warn', 'No target specified, deploying to device \'' + device_list[0] + '\'.');
+ install_target = device_list[0];
+ } else {
+ events.emit('warn', 'No target specified and no devices found, deploying to emulator');
+ install_target = '--emulator';
+ }
+ });
+ }
+ }).then(function () {
+ if (install_target === '--device') {
+ return device.resolveTarget(null);
+ } else if (install_target === '--emulator') {
+ // Give preference to any already started emulators. Else, start one.
+ return emulator.list_started().then(function (started) {
+ return started && started.length > 0 ? started[0] : emulator.start();
+ }).then(function (emulatorId) {
+ return emulator.resolveTarget(emulatorId);
+ });
+ }
+ // They specified a specific device/emulator ID.
+ return device.list().then(function (devices) {
+ if (devices.indexOf(install_target) > -1) {
+ return device.resolveTarget(install_target);
+ }
+ return emulator.list_started().then(function (started_emulators) {
+ if (started_emulators.indexOf(install_target) > -1) {
+ return emulator.resolveTarget(install_target);
+ }
+ return emulator.list_images().then(function (avds) {
+ // if target emulator isn't started, then start it.
+ for (var avd in avds) {
+ if (avds[avd].name === install_target) {
+ return emulator.start(install_target).then(function (emulatorId) {
+ return emulator.resolveTarget(emulatorId);
+ });
+ }
+ }
+ return Q.reject('Target \'' + install_target + '\' not found, unable to run project');
+ });
+ });
+ });
+ }).then(function (resolvedTarget) {
+ // Better just call self.build, but we're doing some processing of
+ // build results (according to platformApi spec) so they are in different
+ // format than emulator.install expects.
+ // TODO: Update emulator/device.install to handle this change
+ return build.run.call(self, runOptions, resolvedTarget).then(function (buildResults) {
+ if (resolvedTarget.isEmulator) {
+ return emulator.wait_for_boot(resolvedTarget.target).then(function () {
+ return emulator.install(resolvedTarget, buildResults);
+ });
+ }
+ return device.install(resolvedTarget, buildResults);
+ });
+ });
+};
+
+module.exports.help = function () {
+ console.log('Usage: ' + path.relative(process.cwd(), process.argv[1]) + ' [options]');
+ console.log('Build options :');
+ console.log(' --debug : Builds project in debug mode');
+ console.log(' --release : Builds project in release mode');
+ console.log(' --nobuild : Runs the currently built project without recompiling');
+ console.log('Deploy options :');
+ console.log(' --device : Will deploy the built project to a device');
+ console.log(' --emulator : Will deploy the built project to an emulator if one exists');
+ console.log(' --target= : Installs to the target with the specified id.');
+ process.exit(0);
+};
diff --git a/platforms/android/cordova/lib/start-emulator b/platforms/android/cordova/lib/start-emulator
new file mode 100755
index 0000000..f96bdc3
--- /dev/null
+++ b/platforms/android/cordova/lib/start-emulator
@@ -0,0 +1,39 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+var emulator = require('./emulator'),
+ args = process.argv;
+
+var install_target;
+if(args.length > 2) {
+ if (args[2].substring(0, 9) == '--target=') {
+ install_target = args[2].substring(9, args[2].length);
+ } else {
+ console.error('ERROR : argument \'' + args[2] + '\' not recognized.');
+ process.exit(2);
+ }
+}
+
+emulator.start(install_target).done(null, function(err) {
+ console.error('ERROR: ' + err);
+ process.exit(2);
+});
+
diff --git a/platforms/android/cordova/lib/start-emulator.bat b/platforms/android/cordova/lib/start-emulator.bat
new file mode 100644
index 0000000..6c237ea
--- /dev/null
+++ b/platforms/android/cordova/lib/start-emulator.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0start-emulator"
+IF EXIST %script_path% (
+ node %script_path% %*
+) ELSE (
+ ECHO.
+ ECHO ERROR: Could not find 'start-emulator' script in 'cordova\lib' folder, aborting...>&2
+ EXIT /B 1
+)
\ No newline at end of file
diff --git a/platforms/android/cordova/log b/platforms/android/cordova/log
new file mode 100755
index 0000000..47f0605
--- /dev/null
+++ b/platforms/android/cordova/log
@@ -0,0 +1,36 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+var log = require('./lib/log'),
+ reqs = require('./lib/check_reqs'),
+ args = process.argv;
+
+// Usage support for when args are given
+if(args.length > 2) {
+ log.help();
+} else {
+ reqs.run().done(function() {
+ return log.run();
+ }, function(err) {
+ console.error('ERROR: ' + err);
+ process.exit(2);
+ });
+}
diff --git a/platforms/android/cordova/log.bat b/platforms/android/cordova/log.bat
new file mode 100644
index 0000000..4b2b434
--- /dev/null
+++ b/platforms/android/cordova/log.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0log"
+IF EXIST %script_path% (
+ node %script_path% %*
+) ELSE (
+ ECHO.
+ ECHO ERROR: Could not find 'log' script in 'cordova' folder, aborting...>&2
+ EXIT /B 1
+)
\ No newline at end of file
diff --git a/platforms/android/cordova/loggingHelper.js b/platforms/android/cordova/loggingHelper.js
new file mode 100644
index 0000000..32b2ee0
--- /dev/null
+++ b/platforms/android/cordova/loggingHelper.js
@@ -0,0 +1,18 @@
+var CordovaLogger = require('cordova-common').CordovaLogger;
+
+module.exports = {
+ adjustLoggerLevel: function (opts) {
+ if (opts instanceof Array) {
+ opts.silent = opts.indexOf('--silent') !== -1;
+ opts.verbose = opts.indexOf('--verbose') !== -1;
+ }
+
+ if (opts.silent) {
+ CordovaLogger.get().setLevel('error');
+ }
+
+ if (opts.verbose) {
+ CordovaLogger.get().setLevel('verbose');
+ }
+ }
+};
diff --git a/platforms/android/cordova/run b/platforms/android/cordova/run
new file mode 100755
index 0000000..9544c1d
--- /dev/null
+++ b/platforms/android/cordova/run
@@ -0,0 +1,53 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+var Api = require('./Api');
+var nopt = require('nopt');
+var path = require('path');
+
+// Support basic help commands
+if(['--help', '/?', '-h', 'help', '-help', '/help'].indexOf(process.argv[2]) >= 0)
+ require('./lib/run').help();
+
+// Do some basic argument parsing
+var runOpts = nopt({
+ 'verbose' : Boolean,
+ 'silent' : Boolean,
+ 'debug' : Boolean,
+ 'release' : Boolean,
+ 'nobuild': Boolean,
+ 'buildConfig' : path,
+ 'archs' : String,
+ 'device' : Boolean,
+ 'emulator': Boolean,
+ 'target' : String
+}, { 'd' : '--verbose' });
+
+// Make runOptions compatible with PlatformApi run method spec
+runOpts.argv = runOpts.argv.remain;
+
+require('./loggingHelper').adjustLoggerLevel(runOpts);
+
+new Api().run(runOpts)
+.catch(function(err) {
+ console.error(err, err.stack);
+ process.exit(2);
+});
diff --git a/platforms/android/cordova/run.bat b/platforms/android/cordova/run.bat
new file mode 100644
index 0000000..b0bc28b
--- /dev/null
+++ b/platforms/android/cordova/run.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0run"
+IF EXIST %script_path% (
+ node %script_path% %*
+) ELSE (
+ ECHO.
+ ECHO ERROR: Could not find 'run' script in 'cordova' folder, aborting...>&2
+ EXIT /B 1
+)
\ No newline at end of file
diff --git a/platforms/android/cordova/version b/platforms/android/cordova/version
new file mode 100755
index 0000000..5d6e2b6
--- /dev/null
+++ b/platforms/android/cordova/version
@@ -0,0 +1,29 @@
+#!/usr/bin/env node
+
+/*
+ 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.
+*/
+
+// Coho updates this line:
+var VERSION = "7.1.0";
+
+module.exports.version = VERSION;
+
+if (!module.parent) {
+ console.log(VERSION);
+}
diff --git a/platforms/android/cordova/version.bat b/platforms/android/cordova/version.bat
new file mode 100644
index 0000000..3610c17
--- /dev/null
+++ b/platforms/android/cordova/version.bat
@@ -0,0 +1,26 @@
+:: 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.
+
+@ECHO OFF
+SET script_path="%~dp0version"
+IF EXIST %script_path% (
+ node %script_path% %*
+) ELSE (
+ ECHO.
+ ECHO ERROR: Could not find 'version' script in 'cordova' folder, aborting...>&2
+ EXIT /B 1
+)
diff --git a/platforms/android/platform_www/cordova-js-src/android/nativeapiprovider.js b/platforms/android/platform_www/cordova-js-src/android/nativeapiprovider.js
new file mode 100644
index 0000000..2e9aa67
--- /dev/null
+++ b/platforms/android/platform_www/cordova-js-src/android/nativeapiprovider.js
@@ -0,0 +1,36 @@
+/*
+ * 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.
+*/
+
+/**
+ * Exports the ExposedJsApi.java object if available, otherwise exports the PromptBasedNativeApi.
+ */
+
+var nativeApi = this._cordovaNative || require('cordova/android/promptbasednativeapi');
+var currentApi = nativeApi;
+
+module.exports = {
+ get: function() { return currentApi; },
+ setPreferPrompt: function(value) {
+ currentApi = value ? require('cordova/android/promptbasednativeapi') : nativeApi;
+ },
+ // Used only by tests.
+ set: function(value) {
+ currentApi = value;
+ }
+};
diff --git a/platforms/android/platform_www/cordova-js-src/android/promptbasednativeapi.js b/platforms/android/platform_www/cordova-js-src/android/promptbasednativeapi.js
new file mode 100644
index 0000000..f7fb6bc
--- /dev/null
+++ b/platforms/android/platform_www/cordova-js-src/android/promptbasednativeapi.js
@@ -0,0 +1,35 @@
+/*
+ * 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.
+*/
+
+/**
+ * Implements the API of ExposedJsApi.java, but uses prompt() to communicate.
+ * This is used pre-JellyBean, where addJavascriptInterface() is disabled.
+ */
+
+module.exports = {
+ exec: function(bridgeSecret, service, action, callbackId, argsJson) {
+ return prompt(argsJson, 'gap:'+JSON.stringify([bridgeSecret, service, action, callbackId]));
+ },
+ setNativeToJsBridgeMode: function(bridgeSecret, value) {
+ prompt(value, 'gap_bridge_mode:' + bridgeSecret);
+ },
+ retrieveJsMessages: function(bridgeSecret, fromOnlineEvent) {
+ return prompt(+fromOnlineEvent, 'gap_poll:' + bridgeSecret);
+ }
+};
diff --git a/platforms/android/platform_www/cordova-js-src/exec.js b/platforms/android/platform_www/cordova-js-src/exec.js
new file mode 100644
index 0000000..f73d87a
--- /dev/null
+++ b/platforms/android/platform_www/cordova-js-src/exec.js
@@ -0,0 +1,297 @@
+/*
+ *
+ * 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.
+ *
+*/
+
+/**
+ * Execute a cordova command. It is up to the native side whether this action
+ * is synchronous or asynchronous. The native side can return:
+ * Synchronous: PluginResult object as a JSON string
+ * Asynchronous: Empty string ""
+ * If async, the native side will cordova.callbackSuccess or cordova.callbackError,
+ * depending upon the result of the action.
+ *
+ * @param {Function} success The success callback
+ * @param {Function} fail The fail callback
+ * @param {String} service The name of the service to use
+ * @param {String} action Action to be run in cordova
+ * @param {String[]} [args] Zero or more arguments to pass to the method
+ */
+var cordova = require('cordova'),
+ nativeApiProvider = require('cordova/android/nativeapiprovider'),
+ utils = require('cordova/utils'),
+ base64 = require('cordova/base64'),
+ channel = require('cordova/channel'),
+ jsToNativeModes = {
+ PROMPT: 0,
+ JS_OBJECT: 1
+ },
+ nativeToJsModes = {
+ // Polls for messages using the JS->Native bridge.
+ POLLING: 0,
+ // For LOAD_URL to be viable, it would need to have a work-around for
+ // the bug where the soft-keyboard gets dismissed when a message is sent.
+ LOAD_URL: 1,
+ // For the ONLINE_EVENT to be viable, it would need to intercept all event
+ // listeners (both through addEventListener and window.ononline) as well
+ // as set the navigator property itself.
+ ONLINE_EVENT: 2,
+ EVAL_BRIDGE: 3
+ },
+ jsToNativeBridgeMode, // Set lazily.
+ nativeToJsBridgeMode = nativeToJsModes.EVAL_BRIDGE,
+ pollEnabled = false,
+ bridgeSecret = -1;
+
+var messagesFromNative = [];
+var isProcessing = false;
+var resolvedPromise = typeof Promise == 'undefined' ? null : Promise.resolve();
+var nextTick = resolvedPromise ? function(fn) { resolvedPromise.then(fn); } : function(fn) { setTimeout(fn); };
+
+function androidExec(success, fail, service, action, args) {
+ if (bridgeSecret < 0) {
+ // If we ever catch this firing, we'll need to queue up exec()s
+ // and fire them once we get a secret. For now, I don't think
+ // it's possible for exec() to be called since plugins are parsed but
+ // not run until until after onNativeReady.
+ throw new Error('exec() called without bridgeSecret');
+ }
+ // Set default bridge modes if they have not already been set.
+ // By default, we use the failsafe, since addJavascriptInterface breaks too often
+ if (jsToNativeBridgeMode === undefined) {
+ androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);
+ }
+
+ // If args is not provided, default to an empty array
+ args = args || [];
+
+ // Process any ArrayBuffers in the args into a string.
+ for (var i = 0; i < args.length; i++) {
+ if (utils.typeName(args[i]) == 'ArrayBuffer') {
+ args[i] = base64.fromArrayBuffer(args[i]);
+ }
+ }
+
+ var callbackId = service + cordova.callbackId++,
+ argsJson = JSON.stringify(args);
+ if (success || fail) {
+ cordova.callbacks[callbackId] = {success:success, fail:fail};
+ }
+
+ var msgs = nativeApiProvider.get().exec(bridgeSecret, service, action, callbackId, argsJson);
+ // If argsJson was received by Java as null, try again with the PROMPT bridge mode.
+ // This happens in rare circumstances, such as when certain Unicode characters are passed over the bridge on a Galaxy S2. See CB-2666.
+ if (jsToNativeBridgeMode == jsToNativeModes.JS_OBJECT && msgs === "@Null arguments.") {
+ androidExec.setJsToNativeBridgeMode(jsToNativeModes.PROMPT);
+ androidExec(success, fail, service, action, args);
+ androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);
+ } else if (msgs) {
+ messagesFromNative.push(msgs);
+ // Always process async to avoid exceptions messing up stack.
+ nextTick(processMessages);
+ }
+}
+
+androidExec.init = function() {
+ //CB-11828
+ //This failsafe checks the version of Android and if it's Jellybean, it switches it to
+ //using the Online Event bridge for communicating from Native to JS
+ //
+ //It's ugly, but it's necessary.
+ var check = navigator.userAgent.toLowerCase().match(/android\s[0-9].[0-9]/);
+ var version_code = check && check[0].match(/4.[0-3].*/);
+ if (version_code != null && nativeToJsBridgeMode == nativeToJsModes.EVAL_BRIDGE) {
+ nativeToJsBridgeMode = nativeToJsModes.ONLINE_EVENT;
+ }
+
+ bridgeSecret = +prompt('', 'gap_init:' + nativeToJsBridgeMode);
+ channel.onNativeReady.fire();
+};
+
+function pollOnceFromOnlineEvent() {
+ pollOnce(true);
+}
+
+function pollOnce(opt_fromOnlineEvent) {
+ if (bridgeSecret < 0) {
+ // This can happen when the NativeToJsMessageQueue resets the online state on page transitions.
+ // We know there's nothing to retrieve, so no need to poll.
+ return;
+ }
+ var msgs = nativeApiProvider.get().retrieveJsMessages(bridgeSecret, !!opt_fromOnlineEvent);
+ if (msgs) {
+ messagesFromNative.push(msgs);
+ // Process sync since we know we're already top-of-stack.
+ processMessages();
+ }
+}
+
+function pollingTimerFunc() {
+ if (pollEnabled) {
+ pollOnce();
+ setTimeout(pollingTimerFunc, 50);
+ }
+}
+
+function hookOnlineApis() {
+ function proxyEvent(e) {
+ cordova.fireWindowEvent(e.type);
+ }
+ // The network module takes care of firing online and offline events.
+ // It currently fires them only on document though, so we bridge them
+ // to window here (while first listening for exec()-releated online/offline
+ // events).
+ window.addEventListener('online', pollOnceFromOnlineEvent, false);
+ window.addEventListener('offline', pollOnceFromOnlineEvent, false);
+ cordova.addWindowEventHandler('online');
+ cordova.addWindowEventHandler('offline');
+ document.addEventListener('online', proxyEvent, false);
+ document.addEventListener('offline', proxyEvent, false);
+}
+
+hookOnlineApis();
+
+androidExec.jsToNativeModes = jsToNativeModes;
+androidExec.nativeToJsModes = nativeToJsModes;
+
+androidExec.setJsToNativeBridgeMode = function(mode) {
+ if (mode == jsToNativeModes.JS_OBJECT && !window._cordovaNative) {
+ mode = jsToNativeModes.PROMPT;
+ }
+ nativeApiProvider.setPreferPrompt(mode == jsToNativeModes.PROMPT);
+ jsToNativeBridgeMode = mode;
+};
+
+androidExec.setNativeToJsBridgeMode = function(mode) {
+ if (mode == nativeToJsBridgeMode) {
+ return;
+ }
+ if (nativeToJsBridgeMode == nativeToJsModes.POLLING) {
+ pollEnabled = false;
+ }
+
+ nativeToJsBridgeMode = mode;
+ // Tell the native side to switch modes.
+ // Otherwise, it will be set by androidExec.init()
+ if (bridgeSecret >= 0) {
+ nativeApiProvider.get().setNativeToJsBridgeMode(bridgeSecret, mode);
+ }
+
+ if (mode == nativeToJsModes.POLLING) {
+ pollEnabled = true;
+ setTimeout(pollingTimerFunc, 1);
+ }
+};
+
+function buildPayload(payload, message) {
+ var payloadKind = message.charAt(0);
+ if (payloadKind == 's') {
+ payload.push(message.slice(1));
+ } else if (payloadKind == 't') {
+ payload.push(true);
+ } else if (payloadKind == 'f') {
+ payload.push(false);
+ } else if (payloadKind == 'N') {
+ payload.push(null);
+ } else if (payloadKind == 'n') {
+ payload.push(+message.slice(1));
+ } else if (payloadKind == 'A') {
+ var data = message.slice(1);
+ payload.push(base64.toArrayBuffer(data));
+ } else if (payloadKind == 'S') {
+ payload.push(window.atob(message.slice(1)));
+ } else if (payloadKind == 'M') {
+ var multipartMessages = message.slice(1);
+ while (multipartMessages !== "") {
+ var spaceIdx = multipartMessages.indexOf(' ');
+ var msgLen = +multipartMessages.slice(0, spaceIdx);
+ var multipartMessage = multipartMessages.substr(spaceIdx + 1, msgLen);
+ multipartMessages = multipartMessages.slice(spaceIdx + msgLen + 1);
+ buildPayload(payload, multipartMessage);
+ }
+ } else {
+ payload.push(JSON.parse(message));
+ }
+}
+
+// Processes a single message, as encoded by NativeToJsMessageQueue.java.
+function processMessage(message) {
+ var firstChar = message.charAt(0);
+ if (firstChar == 'J') {
+ // This is deprecated on the .java side. It doesn't work with CSP enabled.
+ eval(message.slice(1));
+ } else if (firstChar == 'S' || firstChar == 'F') {
+ var success = firstChar == 'S';
+ var keepCallback = message.charAt(1) == '1';
+ var spaceIdx = message.indexOf(' ', 2);
+ var status = +message.slice(2, spaceIdx);
+ var nextSpaceIdx = message.indexOf(' ', spaceIdx + 1);
+ var callbackId = message.slice(spaceIdx + 1, nextSpaceIdx);
+ var payloadMessage = message.slice(nextSpaceIdx + 1);
+ var payload = [];
+ buildPayload(payload, payloadMessage);
+ cordova.callbackFromNative(callbackId, success, status, payload, keepCallback);
+ } else {
+ console.log("processMessage failed: invalid message: " + JSON.stringify(message));
+ }
+}
+
+function processMessages() {
+ // Check for the reentrant case.
+ if (isProcessing) {
+ return;
+ }
+ if (messagesFromNative.length === 0) {
+ return;
+ }
+ isProcessing = true;
+ try {
+ var msg = popMessageFromQueue();
+ // The Java side can send a * message to indicate that it
+ // still has messages waiting to be retrieved.
+ if (msg == '*' && messagesFromNative.length === 0) {
+ nextTick(pollOnce);
+ return;
+ }
+ processMessage(msg);
+ } finally {
+ isProcessing = false;
+ if (messagesFromNative.length > 0) {
+ nextTick(processMessages);
+ }
+ }
+}
+
+function popMessageFromQueue() {
+ var messageBatch = messagesFromNative.shift();
+ if (messageBatch == '*') {
+ return '*';
+ }
+
+ var spaceIdx = messageBatch.indexOf(' ');
+ var msgLen = +messageBatch.slice(0, spaceIdx);
+ var message = messageBatch.substr(spaceIdx + 1, msgLen);
+ messageBatch = messageBatch.slice(spaceIdx + msgLen + 1);
+ if (messageBatch) {
+ messagesFromNative.unshift(messageBatch);
+ }
+ return message;
+}
+
+module.exports = androidExec;
diff --git a/platforms/android/platform_www/cordova-js-src/platform.js b/platforms/android/platform_www/cordova-js-src/platform.js
new file mode 100644
index 0000000..2bfd024
--- /dev/null
+++ b/platforms/android/platform_www/cordova-js-src/platform.js
@@ -0,0 +1,125 @@
+/*
+ *
+ * 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.
+ *
+*/
+
+// The last resume event that was received that had the result of a plugin call.
+var lastResumeEvent = null;
+
+module.exports = {
+ id: 'android',
+ bootstrap: function() {
+ var channel = require('cordova/channel'),
+ cordova = require('cordova'),
+ exec = require('cordova/exec'),
+ modulemapper = require('cordova/modulemapper');
+
+ // Get the shared secret needed to use the bridge.
+ exec.init();
+
+ // TODO: Extract this as a proper plugin.
+ modulemapper.clobbers('cordova/plugin/android/app', 'navigator.app');
+
+ var APP_PLUGIN_NAME = Number(cordova.platformVersion.split('.')[0]) >= 4 ? 'CoreAndroid' : 'App';
+
+ // Inject a listener for the backbutton on the document.
+ var backButtonChannel = cordova.addDocumentEventHandler('backbutton');
+ backButtonChannel.onHasSubscribersChange = function() {
+ // If we just attached the first handler or detached the last handler,
+ // let native know we need to override the back button.
+ exec(null, null, APP_PLUGIN_NAME, "overrideBackbutton", [this.numHandlers == 1]);
+ };
+
+ // Add hardware MENU and SEARCH button handlers
+ cordova.addDocumentEventHandler('menubutton');
+ cordova.addDocumentEventHandler('searchbutton');
+
+ function bindButtonChannel(buttonName) {
+ // generic button bind used for volumeup/volumedown buttons
+ var volumeButtonChannel = cordova.addDocumentEventHandler(buttonName + 'button');
+ volumeButtonChannel.onHasSubscribersChange = function() {
+ exec(null, null, APP_PLUGIN_NAME, "overrideButton", [buttonName, this.numHandlers == 1]);
+ };
+ }
+ // Inject a listener for the volume buttons on the document.
+ bindButtonChannel('volumeup');
+ bindButtonChannel('volumedown');
+
+ // The resume event is not "sticky", but it is possible that the event
+ // will contain the result of a plugin call. We need to ensure that the
+ // plugin result is delivered even after the event is fired (CB-10498)
+ var cordovaAddEventListener = document.addEventListener;
+
+ document.addEventListener = function(evt, handler, capture) {
+ cordovaAddEventListener(evt, handler, capture);
+
+ if (evt === 'resume' && lastResumeEvent) {
+ handler(lastResumeEvent);
+ }
+ };
+
+ // Let native code know we are all done on the JS side.
+ // Native code will then un-hide the WebView.
+ channel.onCordovaReady.subscribe(function() {
+ exec(onMessageFromNative, null, APP_PLUGIN_NAME, 'messageChannel', []);
+ exec(null, null, APP_PLUGIN_NAME, "show", []);
+ });
+ }
+};
+
+function onMessageFromNative(msg) {
+ var cordova = require('cordova');
+ var action = msg.action;
+
+ switch (action)
+ {
+ // Button events
+ case 'backbutton':
+ case 'menubutton':
+ case 'searchbutton':
+ // App life cycle events
+ case 'pause':
+ // Volume events
+ case 'volumedownbutton':
+ case 'volumeupbutton':
+ cordova.fireDocumentEvent(action);
+ break;
+ case 'resume':
+ if(arguments.length > 1 && msg.pendingResult) {
+ if(arguments.length === 2) {
+ msg.pendingResult.result = arguments[1];
+ } else {
+ // The plugin returned a multipart message
+ var res = [];
+ for(var i = 1; i < arguments.length; i++) {
+ res.push(arguments[i]);
+ }
+ msg.pendingResult.result = res;
+ }
+
+ // Save the plugin result so that it can be delivered to the js
+ // even if they miss the initial firing of the event
+ lastResumeEvent = msg;
+ }
+ cordova.fireDocumentEvent(action, msg);
+ break;
+ default:
+ throw new Error('Unknown event action ' + action);
+ }
+}
diff --git a/platforms/android/platform_www/cordova-js-src/plugin/android/app.js b/platforms/android/platform_www/cordova-js-src/plugin/android/app.js
new file mode 100644
index 0000000..22cf96e
--- /dev/null
+++ b/platforms/android/platform_www/cordova-js-src/plugin/android/app.js
@@ -0,0 +1,108 @@
+/*
+ *
+ * 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.
+ *
+*/
+
+var exec = require('cordova/exec');
+var APP_PLUGIN_NAME = Number(require('cordova').platformVersion.split('.')[0]) >= 4 ? 'CoreAndroid' : 'App';
+
+module.exports = {
+ /**
+ * Clear the resource cache.
+ */
+ clearCache:function() {
+ exec(null, null, APP_PLUGIN_NAME, "clearCache", []);
+ },
+
+ /**
+ * Load the url into the webview or into new browser instance.
+ *
+ * @param url The URL to load
+ * @param props Properties that can be passed in to the activity:
+ * wait: int => wait msec before loading URL
+ * loadingDialog: "Title,Message" => display a native loading dialog
+ * loadUrlTimeoutValue: int => time in msec to wait before triggering a timeout error
+ * clearHistory: boolean => clear webview history (default=false)
+ * openExternal: boolean => open in a new browser (default=false)
+ *
+ * Example:
+ * navigator.app.loadUrl("http://server/myapp/index.html", {wait:2000, loadingDialog:"Wait,Loading App", loadUrlTimeoutValue: 60000});
+ */
+ loadUrl:function(url, props) {
+ exec(null, null, APP_PLUGIN_NAME, "loadUrl", [url, props]);
+ },
+
+ /**
+ * Cancel loadUrl that is waiting to be loaded.
+ */
+ cancelLoadUrl:function() {
+ exec(null, null, APP_PLUGIN_NAME, "cancelLoadUrl", []);
+ },
+
+ /**
+ * Clear web history in this web view.
+ * Instead of BACK button loading the previous web page, it will exit the app.
+ */
+ clearHistory:function() {
+ exec(null, null, APP_PLUGIN_NAME, "clearHistory", []);
+ },
+
+ /**
+ * Go to previous page displayed.
+ * This is the same as pressing the backbutton on Android device.
+ */
+ backHistory:function() {
+ exec(null, null, APP_PLUGIN_NAME, "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.
+ *
+ * Note: The user should not have to call this method. Instead, when the user
+ * registers for the "backbutton" event, this is automatically done.
+ *
+ * @param override T=override, F=cancel override
+ */
+ overrideBackbutton:function(override) {
+ exec(null, null, APP_PLUGIN_NAME, "overrideBackbutton", [override]);
+ },
+
+ /**
+ * Override the default behavior of the Android volume button.
+ * If overridden, when the volume button is pressed, the "volume[up|down]button"
+ * JavaScript event will be fired.
+ *
+ * Note: The user should not have to call this method. Instead, when the user
+ * registers for the "volume[up|down]button" event, this is automatically done.
+ *
+ * @param button volumeup, volumedown
+ * @param override T=override, F=cancel override
+ */
+ overrideButton:function(button, override) {
+ exec(null, null, APP_PLUGIN_NAME, "overrideButton", [button, override]);
+ },
+
+ /**
+ * Exit and terminate the application.
+ */
+ exitApp:function() {
+ return exec(null, null, APP_PLUGIN_NAME, "exitApp", []);
+ }
+};
diff --git a/platforms/android/platform_www/cordova.js b/platforms/android/platform_www/cordova.js
new file mode 100644
index 0000000..1f2cdee
--- /dev/null
+++ b/platforms/android/platform_www/cordova.js
@@ -0,0 +1,2188 @@
+// Platform: android
+// 4450a4cea50616e080a82e8ede9e3d6a1fe3c3ec
+/*
+ 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.
+*/
+;(function() {
+var PLATFORM_VERSION_BUILD_LABEL = '7.1.0';
+// file: src/scripts/require.js
+
+/* jshint -W079 */
+/* jshint -W020 */
+
+var require;
+var define;
+
+(function () {
+ var modules = {};
+ // Stack of moduleIds currently being built.
+ var requireStack = [];
+ // Map of module ID -> index into requireStack of modules currently being built.
+ var inProgressModules = {};
+ var SEPARATOR = '.';
+
+ function build (module) {
+ var factory = module.factory;
+ var localRequire = function (id) {
+ var resultantId = id;
+ // Its a relative path, so lop off the last portion and add the id (minus "./")
+ if (id.charAt(0) === '.') {
+ resultantId = module.id.slice(0, module.id.lastIndexOf(SEPARATOR)) + SEPARATOR + id.slice(2);
+ }
+ return require(resultantId);
+ };
+ module.exports = {};
+ delete module.factory;
+ factory(localRequire, module.exports, module);
+ return module.exports;
+ }
+
+ require = function (id) {
+ if (!modules[id]) {
+ throw 'module ' + id + ' not found';
+ } else if (id in inProgressModules) {
+ var cycle = requireStack.slice(inProgressModules[id]).join('->') + '->' + id;
+ throw 'Cycle in require graph: ' + cycle;
+ }
+ if (modules[id].factory) {
+ try {
+ inProgressModules[id] = requireStack.length;
+ requireStack.push(id);
+ return build(modules[id]);
+ } finally {
+ delete inProgressModules[id];
+ requireStack.pop();
+ }
+ }
+ return modules[id].exports;
+ };
+
+ define = function (id, factory) {
+ if (modules[id]) {
+ throw 'module ' + id + ' already defined';
+ }
+
+ modules[id] = {
+ id: id,
+ factory: factory
+ };
+ };
+
+ define.remove = function (id) {
+ delete modules[id];
+ };
+
+ define.moduleMap = modules;
+})();
+
+// Export for use in node
+if (typeof module === 'object' && typeof require === 'function') {
+ module.exports.require = require;
+ module.exports.define = define;
+}
+
+// file: src/cordova.js
+define("cordova", function(require, exports, module) {
+
+// Workaround for Windows 10 in hosted environment case
+// http://www.w3.org/html/wg/drafts/html/master/browsers.html#named-access-on-the-window-object
+if (window.cordova && !(window.cordova instanceof HTMLElement)) { // eslint-disable-line no-undef
+ throw new Error('cordova already defined');
+}
+
+var channel = require('cordova/channel');
+var platform = require('cordova/platform');
+
+/**
+ * Intercept calls to addEventListener + removeEventListener and handle deviceready,
+ * resume, and pause events.
+ */
+var m_document_addEventListener = document.addEventListener;
+var m_document_removeEventListener = document.removeEventListener;
+var m_window_addEventListener = window.addEventListener;
+var m_window_removeEventListener = window.removeEventListener;
+
+/**
+ * Houses custom event handlers to intercept on document + window event listeners.
+ */
+var documentEventHandlers = {};
+var windowEventHandlers = {};
+
+document.addEventListener = function (evt, handler, capture) {
+ var e = evt.toLowerCase();
+ if (typeof documentEventHandlers[e] !== 'undefined') {
+ documentEventHandlers[e].subscribe(handler);
+ } else {
+ m_document_addEventListener.call(document, evt, handler, capture);
+ }
+};
+
+window.addEventListener = function (evt, handler, capture) {
+ var e = evt.toLowerCase();
+ if (typeof windowEventHandlers[e] !== 'undefined') {
+ windowEventHandlers[e].subscribe(handler);
+ } else {
+ m_window_addEventListener.call(window, evt, handler, capture);
+ }
+};
+
+document.removeEventListener = function (evt, handler, capture) {
+ var e = evt.toLowerCase();
+ // If unsubscribing from an event that is handled by a plugin
+ if (typeof documentEventHandlers[e] !== 'undefined') {
+ documentEventHandlers[e].unsubscribe(handler);
+ } else {
+ m_document_removeEventListener.call(document, evt, handler, capture);
+ }
+};
+
+window.removeEventListener = function (evt, handler, capture) {
+ var e = evt.toLowerCase();
+ // If unsubscribing from an event that is handled by a plugin
+ if (typeof windowEventHandlers[e] !== 'undefined') {
+ windowEventHandlers[e].unsubscribe(handler);
+ } else {
+ m_window_removeEventListener.call(window, evt, handler, capture);
+ }
+};
+
+function createEvent (type, data) {
+ var event = document.createEvent('Events');
+ event.initEvent(type, false, false);
+ if (data) {
+ for (var i in data) {
+ if (data.hasOwnProperty(i)) {
+ event[i] = data[i];
+ }
+ }
+ }
+ return event;
+}
+
+/* eslint-disable no-undef */
+var cordova = {
+ define: define,
+ require: require,
+ version: PLATFORM_VERSION_BUILD_LABEL,
+ platformVersion: PLATFORM_VERSION_BUILD_LABEL,
+ platformId: platform.id,
+
+ /* eslint-enable no-undef */
+
+ /**
+ * Methods to add/remove your own addEventListener hijacking on document + window.
+ */
+ addWindowEventHandler: function (event) {
+ return (windowEventHandlers[event] = channel.create(event));
+ },
+ addStickyDocumentEventHandler: function (event) {
+ return (documentEventHandlers[event] = channel.createSticky(event));
+ },
+ addDocumentEventHandler: function (event) {
+ return (documentEventHandlers[event] = channel.create(event));
+ },
+ removeWindowEventHandler: function (event) {
+ delete windowEventHandlers[event];
+ },
+ removeDocumentEventHandler: function (event) {
+ delete documentEventHandlers[event];
+ },
+ /**
+ * Retrieve original event handlers that were replaced by Cordova
+ *
+ * @return object
+ */
+ getOriginalHandlers: function () {
+ return {'document': {'addEventListener': m_document_addEventListener, 'removeEventListener': m_document_removeEventListener},
+ 'window': {'addEventListener': m_window_addEventListener, 'removeEventListener': m_window_removeEventListener}};
+ },
+ /**
+ * Method to fire event from native code
+ * bNoDetach is required for events which cause an exception which needs to be caught in native code
+ */
+ fireDocumentEvent: function (type, data, bNoDetach) {
+ var evt = createEvent(type, data);
+ if (typeof documentEventHandlers[type] !== 'undefined') {
+ if (bNoDetach) {
+ documentEventHandlers[type].fire(evt);
+ } else {
+ setTimeout(function () {
+ // Fire deviceready on listeners that were registered before cordova.js was loaded.
+ if (type === 'deviceready') {
+ document.dispatchEvent(evt);
+ }
+ documentEventHandlers[type].fire(evt);
+ }, 0);
+ }
+ } else {
+ document.dispatchEvent(evt);
+ }
+ },
+ fireWindowEvent: function (type, data) {
+ var evt = createEvent(type, data);
+ if (typeof windowEventHandlers[type] !== 'undefined') {
+ setTimeout(function () {
+ windowEventHandlers[type].fire(evt);
+ }, 0);
+ } else {
+ window.dispatchEvent(evt);
+ }
+ },
+
+ /**
+ * Plugin callback mechanism.
+ */
+ // Randomize the starting callbackId to avoid collisions after refreshing or navigating.
+ // This way, it's very unlikely that any new callback would get the same callbackId as an old callback.
+ callbackId: Math.floor(Math.random() * 2000000000),
+ callbacks: {},
+ callbackStatus: {
+ NO_RESULT: 0,
+ OK: 1,
+ CLASS_NOT_FOUND_EXCEPTION: 2,
+ ILLEGAL_ACCESS_EXCEPTION: 3,
+ INSTANTIATION_EXCEPTION: 4,
+ MALFORMED_URL_EXCEPTION: 5,
+ IO_EXCEPTION: 6,
+ INVALID_ACTION: 7,
+ JSON_EXCEPTION: 8,
+ ERROR: 9
+ },
+
+ /**
+ * Called by native code when returning successful result from an action.
+ */
+ callbackSuccess: function (callbackId, args) {
+ cordova.callbackFromNative(callbackId, true, args.status, [args.message], args.keepCallback);
+ },
+
+ /**
+ * Called by native code when returning error result from an action.
+ */
+ callbackError: function (callbackId, args) {
+ // TODO: Deprecate callbackSuccess and callbackError in favour of callbackFromNative.
+ // Derive success from status.
+ cordova.callbackFromNative(callbackId, false, args.status, [args.message], args.keepCallback);
+ },
+
+ /**
+ * Called by native code when returning the result from an action.
+ */
+ callbackFromNative: function (callbackId, isSuccess, status, args, keepCallback) {
+ try {
+ var callback = cordova.callbacks[callbackId];
+ if (callback) {
+ if (isSuccess && status === cordova.callbackStatus.OK) {
+ callback.success && callback.success.apply(null, args);
+ } else if (!isSuccess) {
+ callback.fail && callback.fail.apply(null, args);
+ }
+ /*
+ else
+ Note, this case is intentionally not caught.
+ this can happen if isSuccess is true, but callbackStatus is NO_RESULT
+ which is used to remove a callback from the list without calling the callbacks
+ typically keepCallback is false in this case
+ */
+ // Clear callback if not expecting any more results
+ if (!keepCallback) {
+ delete cordova.callbacks[callbackId];
+ }
+ }
+ } catch (err) {
+ var msg = 'Error in ' + (isSuccess ? 'Success' : 'Error') + ' callbackId: ' + callbackId + ' : ' + err;
+ console && console.log && console.log(msg);
+ cordova.fireWindowEvent('cordovacallbackerror', { 'message': msg });
+ throw err;
+ }
+ },
+ addConstructor: function (func) {
+ channel.onCordovaReady.subscribe(function () {
+ try {
+ func();
+ } catch (e) {
+ console.log('Failed to run constructor: ' + e);
+ }
+ });
+ }
+};
+
+module.exports = cordova;
+
+});
+
+// file: /Users/steveng/repo/cordova/cordova-android/cordova-js-src/android/nativeapiprovider.js
+define("cordova/android/nativeapiprovider", function(require, exports, module) {
+
+/**
+ * Exports the ExposedJsApi.java object if available, otherwise exports the PromptBasedNativeApi.
+ */
+
+var nativeApi = this._cordovaNative || require('cordova/android/promptbasednativeapi');
+var currentApi = nativeApi;
+
+module.exports = {
+ get: function() { return currentApi; },
+ setPreferPrompt: function(value) {
+ currentApi = value ? require('cordova/android/promptbasednativeapi') : nativeApi;
+ },
+ // Used only by tests.
+ set: function(value) {
+ currentApi = value;
+ }
+};
+
+});
+
+// file: /Users/steveng/repo/cordova/cordova-android/cordova-js-src/android/promptbasednativeapi.js
+define("cordova/android/promptbasednativeapi", function(require, exports, module) {
+
+/**
+ * Implements the API of ExposedJsApi.java, but uses prompt() to communicate.
+ * This is used pre-JellyBean, where addJavascriptInterface() is disabled.
+ */
+
+module.exports = {
+ exec: function(bridgeSecret, service, action, callbackId, argsJson) {
+ return prompt(argsJson, 'gap:'+JSON.stringify([bridgeSecret, service, action, callbackId]));
+ },
+ setNativeToJsBridgeMode: function(bridgeSecret, value) {
+ prompt(value, 'gap_bridge_mode:' + bridgeSecret);
+ },
+ retrieveJsMessages: function(bridgeSecret, fromOnlineEvent) {
+ return prompt(+fromOnlineEvent, 'gap_poll:' + bridgeSecret);
+ }
+};
+
+});
+
+// file: src/common/argscheck.js
+define("cordova/argscheck", function(require, exports, module) {
+
+var utils = require('cordova/utils');
+
+var moduleExports = module.exports;
+
+var typeMap = {
+ 'A': 'Array',
+ 'D': 'Date',
+ 'N': 'Number',
+ 'S': 'String',
+ 'F': 'Function',
+ 'O': 'Object'
+};
+
+function extractParamName (callee, argIndex) {
+ return (/.*?\((.*?)\)/).exec(callee)[1].split(', ')[argIndex];
+}
+
+function checkArgs (spec, functionName, args, opt_callee) {
+ if (!moduleExports.enableChecks) {
+ return;
+ }
+ var errMsg = null;
+ var typeName;
+ for (var i = 0; i < spec.length; ++i) {
+ var c = spec.charAt(i);
+ var cUpper = c.toUpperCase();
+ var arg = args[i];
+ // Asterix means allow anything.
+ if (c === '*') {
+ continue;
+ }
+ typeName = utils.typeName(arg);
+ if ((arg === null || arg === undefined) && c === cUpper) {
+ continue;
+ }
+ if (typeName !== typeMap[cUpper]) {
+ errMsg = 'Expected ' + typeMap[cUpper];
+ break;
+ }
+ }
+ if (errMsg) {
+ errMsg += ', but got ' + typeName + '.';
+ errMsg = 'Wrong type for parameter "' + extractParamName(opt_callee || args.callee, i) + '" of ' + functionName + ': ' + errMsg;
+ // Don't log when running unit tests.
+ if (typeof jasmine === 'undefined') {
+ console.error(errMsg);
+ }
+ throw TypeError(errMsg);
+ }
+}
+
+function getValue (value, defaultValue) {
+ return value === undefined ? defaultValue : value;
+}
+
+moduleExports.checkArgs = checkArgs;
+moduleExports.getValue = getValue;
+moduleExports.enableChecks = true;
+
+});
+
+// file: src/common/base64.js
+define("cordova/base64", function(require, exports, module) {
+
+var base64 = exports;
+
+base64.fromArrayBuffer = function (arrayBuffer) {
+ var array = new Uint8Array(arrayBuffer);
+ return uint8ToBase64(array);
+};
+
+base64.toArrayBuffer = function (str) {
+ var decodedStr = typeof atob !== 'undefined' ? atob(str) : Buffer.from(str, 'base64').toString('binary'); // eslint-disable-line no-undef
+ var arrayBuffer = new ArrayBuffer(decodedStr.length);
+ var array = new Uint8Array(arrayBuffer);
+ for (var i = 0, len = decodedStr.length; i < len; i++) {
+ array[i] = decodedStr.charCodeAt(i);
+ }
+ return arrayBuffer;
+};
+
+// ------------------------------------------------------------------------------
+
+/* This code is based on the performance tests at http://jsperf.com/b64tests
+ * This 12-bit-at-a-time algorithm was the best performing version on all
+ * platforms tested.
+ */
+
+var b64_6bit = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+var b64_12bit;
+
+var b64_12bitTable = function () {
+ b64_12bit = [];
+ for (var i = 0; i < 64; i++) {
+ for (var j = 0; j < 64; j++) {
+ b64_12bit[i * 64 + j] = b64_6bit[i] + b64_6bit[j];
+ }
+ }
+ b64_12bitTable = function () { return b64_12bit; };
+ return b64_12bit;
+};
+
+function uint8ToBase64 (rawData) {
+ var numBytes = rawData.byteLength;
+ var output = '';
+ var segment;
+ var table = b64_12bitTable();
+ for (var i = 0; i < numBytes - 2; i += 3) {
+ segment = (rawData[i] << 16) + (rawData[i + 1] << 8) + rawData[i + 2];
+ output += table[segment >> 12];
+ output += table[segment & 0xfff];
+ }
+ if (numBytes - i === 2) {
+ segment = (rawData[i] << 16) + (rawData[i + 1] << 8);
+ output += table[segment >> 12];
+ output += b64_6bit[(segment & 0xfff) >> 6];
+ output += '=';
+ } else if (numBytes - i === 1) {
+ segment = (rawData[i] << 16);
+ output += table[segment >> 12];
+ output += '==';
+ }
+ return output;
+}
+
+});
+
+// file: src/common/builder.js
+define("cordova/builder", function(require, exports, module) {
+
+var utils = require('cordova/utils');
+
+function each (objects, func, context) {
+ for (var prop in objects) {
+ if (objects.hasOwnProperty(prop)) {
+ func.apply(context, [objects[prop], prop]);
+ }
+ }
+}
+
+function clobber (obj, key, value) {
+ exports.replaceHookForTesting(obj, key);
+ var needsProperty = false;
+ try {
+ obj[key] = value;
+ } catch (e) {
+ needsProperty = true;
+ }
+ // Getters can only be overridden by getters.
+ if (needsProperty || obj[key] !== value) {
+ utils.defineGetter(obj, key, function () {
+ return value;
+ });
+ }
+}
+
+function assignOrWrapInDeprecateGetter (obj, key, value, message) {
+ if (message) {
+ utils.defineGetter(obj, key, function () {
+ console.log(message);
+ delete obj[key];
+ clobber(obj, key, value);
+ return value;
+ });
+ } else {
+ clobber(obj, key, value);
+ }
+}
+
+function include (parent, objects, clobber, merge) {
+ each(objects, function (obj, key) {
+ try {
+ var result = obj.path ? require(obj.path) : {};
+
+ if (clobber) {
+ // Clobber if it doesn't exist.
+ if (typeof parent[key] === 'undefined') {
+ assignOrWrapInDeprecateGetter(parent, key, result, obj.deprecated);
+ } else if (typeof obj.path !== 'undefined') {
+ // If merging, merge properties onto parent, otherwise, clobber.
+ if (merge) {
+ recursiveMerge(parent[key], result);
+ } else {
+ assignOrWrapInDeprecateGetter(parent, key, result, obj.deprecated);
+ }
+ }
+ result = parent[key];
+ } else {
+ // Overwrite if not currently defined.
+ if (typeof parent[key] === 'undefined') {
+ assignOrWrapInDeprecateGetter(parent, key, result, obj.deprecated);
+ } else {
+ // Set result to what already exists, so we can build children into it if they exist.
+ result = parent[key];
+ }
+ }
+
+ if (obj.children) {
+ include(result, obj.children, clobber, merge);
+ }
+ } catch (e) {
+ utils.alert('Exception building Cordova JS globals: ' + e + ' for key "' + key + '"');
+ }
+ });
+}
+
+/**
+ * Merge properties from one object onto another recursively. Properties from
+ * the src object will overwrite existing target property.
+ *
+ * @param target Object to merge properties into.
+ * @param src Object to merge properties from.
+ */
+function recursiveMerge (target, src) {
+ for (var prop in src) {
+ if (src.hasOwnProperty(prop)) {
+ if (target.prototype && target.prototype.constructor === target) {
+ // If the target object is a constructor override off prototype.
+ clobber(target.prototype, prop, src[prop]);
+ } else {
+ if (typeof src[prop] === 'object' && typeof target[prop] === 'object') {
+ recursiveMerge(target[prop], src[prop]);
+ } else {
+ clobber(target, prop, src[prop]);
+ }
+ }
+ }
+ }
+}
+
+exports.buildIntoButDoNotClobber = function (objects, target) {
+ include(target, objects, false, false);
+};
+exports.buildIntoAndClobber = function (objects, target) {
+ include(target, objects, true, false);
+};
+exports.buildIntoAndMerge = function (objects, target) {
+ include(target, objects, true, true);
+};
+exports.recursiveMerge = recursiveMerge;
+exports.assignOrWrapInDeprecateGetter = assignOrWrapInDeprecateGetter;
+exports.replaceHookForTesting = function () {};
+
+});
+
+// file: src/common/channel.js
+define("cordova/channel", function(require, exports, module) {
+
+var utils = require('cordova/utils');
+var nextGuid = 1;
+
+/**
+ * Custom pub-sub "channel" that can have functions subscribed to it
+ * This object is used to define and control firing of events for
+ * cordova initialization, as well as for custom events thereafter.
+ *
+ * The order of events during page load and Cordova startup is as follows:
+ *
+ * onDOMContentLoaded* Internal event that is received when the web page is loaded and parsed.
+ * onNativeReady* Internal event that indicates the Cordova native side is ready.
+ * onCordovaReady* Internal event fired when all Cordova JavaScript objects have been created.
+ * onDeviceReady* User event fired to indicate that Cordova is ready
+ * onResume User event fired to indicate a start/resume lifecycle event
+ * onPause User event fired to indicate a pause lifecycle event
+ *
+ * The events marked with an * are sticky. Once they have fired, they will stay in the fired state.
+ * All listeners that subscribe after the event is fired will be executed right away.
+ *
+ * The only Cordova events that user code should register for are:
+ * deviceready Cordova native code is initialized and Cordova APIs can be called from JavaScript
+ * pause App has moved to background
+ * resume App has returned to foreground
+ *
+ * Listeners can be registered as:
+ * document.addEventListener("deviceready", myDeviceReadyListener, false);
+ * document.addEventListener("resume", myResumeListener, false);
+ * document.addEventListener("pause", myPauseListener, false);
+ *
+ * The DOM lifecycle events should be used for saving and restoring state
+ * window.onload
+ * window.onunload
+ *
+ */
+
+/**
+ * Channel
+ * @constructor
+ * @param type String the channel name
+ */
+var Channel = function (type, sticky) {
+ this.type = type;
+ // Map of guid -> function.
+ this.handlers = {};
+ // 0 = Non-sticky, 1 = Sticky non-fired, 2 = Sticky fired.
+ this.state = sticky ? 1 : 0;
+ // Used in sticky mode to remember args passed to fire().
+ this.fireArgs = null;
+ // Used by onHasSubscribersChange to know if there are any listeners.
+ this.numHandlers = 0;
+ // Function that is called when the first listener is subscribed, or when
+ // the last listener is unsubscribed.
+ this.onHasSubscribersChange = null;
+};
+var channel = {
+ /**
+ * Calls the provided function only after all of the channels specified
+ * have been fired. All channels must be sticky channels.
+ */
+ join: function (h, c) {
+ var len = c.length;
+ var i = len;
+ var f = function () {
+ if (!(--i)) h();
+ };
+ for (var j = 0; j < len; j++) {
+ if (c[j].state === 0) {
+ throw Error('Can only use join with sticky channels.');
+ }
+ c[j].subscribe(f);
+ }
+ if (!len) h();
+ },
+ /* eslint-disable no-return-assign */
+ create: function (type) {
+ return channel[type] = new Channel(type, false);
+ },
+ createSticky: function (type) {
+ return channel[type] = new Channel(type, true);
+ },
+ /* eslint-enable no-return-assign */
+ /**
+ * cordova Channels that must fire before "deviceready" is fired.
+ */
+ deviceReadyChannelsArray: [],
+ deviceReadyChannelsMap: {},
+
+ /**
+ * Indicate that a feature needs to be initialized before it is ready to be used.
+ * This holds up Cordova's "deviceready" event until the feature has been initialized
+ * and Cordova.initComplete(feature) is called.
+ *
+ * @param feature {String} The unique feature name
+ */
+ waitForInitialization: function (feature) {
+ if (feature) {
+ var c = channel[feature] || this.createSticky(feature);
+ this.deviceReadyChannelsMap[feature] = c;
+ this.deviceReadyChannelsArray.push(c);
+ }
+ },
+
+ /**
+ * Indicate that initialization code has completed and the feature is ready to be used.
+ *
+ * @param feature {String} The unique feature name
+ */
+ initializationComplete: function (feature) {
+ var c = this.deviceReadyChannelsMap[feature];
+ if (c) {
+ c.fire();
+ }
+ }
+};
+
+function checkSubscriptionArgument (argument) {
+ if (typeof argument !== 'function' && typeof argument.handleEvent !== 'function') {
+ throw new Error(
+ 'Must provide a function or an EventListener object ' +
+ 'implementing the handleEvent interface.'
+ );
+ }
+}
+
+/**
+ * Subscribes the given function to the channel. Any time that
+ * Channel.fire is called so too will the function.
+ * Optionally specify an execution context for the function
+ * and a guid that can be used to stop subscribing to the channel.
+ * Returns the guid.
+ */
+Channel.prototype.subscribe = function (eventListenerOrFunction, eventListener) {
+ checkSubscriptionArgument(eventListenerOrFunction);
+ var handleEvent, guid;
+
+ if (eventListenerOrFunction && typeof eventListenerOrFunction === 'object') {
+ // Received an EventListener object implementing the handleEvent interface
+ handleEvent = eventListenerOrFunction.handleEvent;
+ eventListener = eventListenerOrFunction;
+ } else {
+ // Received a function to handle event
+ handleEvent = eventListenerOrFunction;
+ }
+
+ if (this.state === 2) {
+ handleEvent.apply(eventListener || this, this.fireArgs);
+ return;
+ }
+
+ guid = eventListenerOrFunction.observer_guid;
+ if (typeof eventListener === 'object') {
+ handleEvent = utils.close(eventListener, handleEvent);
+ }
+
+ if (!guid) {
+ // First time any channel has seen this subscriber
+ guid = '' + nextGuid++;
+ }
+ handleEvent.observer_guid = guid;
+ eventListenerOrFunction.observer_guid = guid;
+
+ // Don't add the same handler more than once.
+ if (!this.handlers[guid]) {
+ this.handlers[guid] = handleEvent;
+ this.numHandlers++;
+ if (this.numHandlers === 1) {
+ this.onHasSubscribersChange && this.onHasSubscribersChange();
+ }
+ }
+};
+
+/**
+ * Unsubscribes the function with the given guid from the channel.
+ */
+Channel.prototype.unsubscribe = function (eventListenerOrFunction) {
+ checkSubscriptionArgument(eventListenerOrFunction);
+ var handleEvent, guid, handler;
+
+ if (eventListenerOrFunction && typeof eventListenerOrFunction === 'object') {
+ // Received an EventListener object implementing the handleEvent interface
+ handleEvent = eventListenerOrFunction.handleEvent;
+ } else {
+ // Received a function to handle event
+ handleEvent = eventListenerOrFunction;
+ }
+
+ guid = handleEvent.observer_guid;
+ handler = this.handlers[guid];
+ if (handler) {
+ delete this.handlers[guid];
+ this.numHandlers--;
+ if (this.numHandlers === 0) {
+ this.onHasSubscribersChange && this.onHasSubscribersChange();
+ }
+ }
+};
+
+/**
+ * Calls all functions subscribed to this channel.
+ */
+Channel.prototype.fire = function (e) {
+ var fail = false; // eslint-disable-line no-unused-vars
+ var fireArgs = Array.prototype.slice.call(arguments);
+ // Apply stickiness.
+ if (this.state === 1) {
+ this.state = 2;
+ this.fireArgs = fireArgs;
+ }
+ if (this.numHandlers) {
+ // Copy the values first so that it is safe to modify it from within
+ // callbacks.
+ var toCall = [];
+ for (var item in this.handlers) {
+ toCall.push(this.handlers[item]);
+ }
+ for (var i = 0; i < toCall.length; ++i) {
+ toCall[i].apply(this, fireArgs);
+ }
+ if (this.state === 2 && this.numHandlers) {
+ this.numHandlers = 0;
+ this.handlers = {};
+ this.onHasSubscribersChange && this.onHasSubscribersChange();
+ }
+ }
+};
+
+// defining them here so they are ready super fast!
+// DOM event that is received when the web page is loaded and parsed.
+channel.createSticky('onDOMContentLoaded');
+
+// Event to indicate the Cordova native side is ready.
+channel.createSticky('onNativeReady');
+
+// Event to indicate that all Cordova JavaScript objects have been created
+// and it's time to run plugin constructors.
+channel.createSticky('onCordovaReady');
+
+// Event to indicate that all automatically loaded JS plugins are loaded and ready.
+// FIXME remove this
+channel.createSticky('onPluginsReady');
+
+// Event to indicate that Cordova is ready
+channel.createSticky('onDeviceReady');
+
+// Event to indicate a resume lifecycle event
+channel.create('onResume');
+
+// Event to indicate a pause lifecycle event
+channel.create('onPause');
+
+// Channels that must fire before "deviceready" is fired.
+channel.waitForInitialization('onCordovaReady');
+channel.waitForInitialization('onDOMContentLoaded');
+
+module.exports = channel;
+
+});
+
+// file: /Users/steveng/repo/cordova/cordova-android/cordova-js-src/exec.js
+define("cordova/exec", function(require, exports, module) {
+
+/**
+ * Execute a cordova command. It is up to the native side whether this action
+ * is synchronous or asynchronous. The native side can return:
+ * Synchronous: PluginResult object as a JSON string
+ * Asynchronous: Empty string ""
+ * If async, the native side will cordova.callbackSuccess or cordova.callbackError,
+ * depending upon the result of the action.
+ *
+ * @param {Function} success The success callback
+ * @param {Function} fail The fail callback
+ * @param {String} service The name of the service to use
+ * @param {String} action Action to be run in cordova
+ * @param {String[]} [args] Zero or more arguments to pass to the method
+ */
+var cordova = require('cordova'),
+ nativeApiProvider = require('cordova/android/nativeapiprovider'),
+ utils = require('cordova/utils'),
+ base64 = require('cordova/base64'),
+ channel = require('cordova/channel'),
+ jsToNativeModes = {
+ PROMPT: 0,
+ JS_OBJECT: 1
+ },
+ nativeToJsModes = {
+ // Polls for messages using the JS->Native bridge.
+ POLLING: 0,
+ // For LOAD_URL to be viable, it would need to have a work-around for
+ // the bug where the soft-keyboard gets dismissed when a message is sent.
+ LOAD_URL: 1,
+ // For the ONLINE_EVENT to be viable, it would need to intercept all event
+ // listeners (both through addEventListener and window.ononline) as well
+ // as set the navigator property itself.
+ ONLINE_EVENT: 2,
+ EVAL_BRIDGE: 3
+ },
+ jsToNativeBridgeMode, // Set lazily.
+ nativeToJsBridgeMode = nativeToJsModes.EVAL_BRIDGE,
+ pollEnabled = false,
+ bridgeSecret = -1;
+
+var messagesFromNative = [];
+var isProcessing = false;
+var resolvedPromise = typeof Promise == 'undefined' ? null : Promise.resolve();
+var nextTick = resolvedPromise ? function(fn) { resolvedPromise.then(fn); } : function(fn) { setTimeout(fn); };
+
+function androidExec(success, fail, service, action, args) {
+ if (bridgeSecret < 0) {
+ // If we ever catch this firing, we'll need to queue up exec()s
+ // and fire them once we get a secret. For now, I don't think
+ // it's possible for exec() to be called since plugins are parsed but
+ // not run until until after onNativeReady.
+ throw new Error('exec() called without bridgeSecret');
+ }
+ // Set default bridge modes if they have not already been set.
+ // By default, we use the failsafe, since addJavascriptInterface breaks too often
+ if (jsToNativeBridgeMode === undefined) {
+ androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);
+ }
+
+ // If args is not provided, default to an empty array
+ args = args || [];
+
+ // Process any ArrayBuffers in the args into a string.
+ for (var i = 0; i < args.length; i++) {
+ if (utils.typeName(args[i]) == 'ArrayBuffer') {
+ args[i] = base64.fromArrayBuffer(args[i]);
+ }
+ }
+
+ var callbackId = service + cordova.callbackId++,
+ argsJson = JSON.stringify(args);
+ if (success || fail) {
+ cordova.callbacks[callbackId] = {success:success, fail:fail};
+ }
+
+ var msgs = nativeApiProvider.get().exec(bridgeSecret, service, action, callbackId, argsJson);
+ // If argsJson was received by Java as null, try again with the PROMPT bridge mode.
+ // This happens in rare circumstances, such as when certain Unicode characters are passed over the bridge on a Galaxy S2. See CB-2666.
+ if (jsToNativeBridgeMode == jsToNativeModes.JS_OBJECT && msgs === "@Null arguments.") {
+ androidExec.setJsToNativeBridgeMode(jsToNativeModes.PROMPT);
+ androidExec(success, fail, service, action, args);
+ androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);
+ } else if (msgs) {
+ messagesFromNative.push(msgs);
+ // Always process async to avoid exceptions messing up stack.
+ nextTick(processMessages);
+ }
+}
+
+androidExec.init = function() {
+ //CB-11828
+ //This failsafe checks the version of Android and if it's Jellybean, it switches it to
+ //using the Online Event bridge for communicating from Native to JS
+ //
+ //It's ugly, but it's necessary.
+ var check = navigator.userAgent.toLowerCase().match(/android\s[0-9].[0-9]/);
+ var version_code = check && check[0].match(/4.[0-3].*/);
+ if (version_code != null && nativeToJsBridgeMode == nativeToJsModes.EVAL_BRIDGE) {
+ nativeToJsBridgeMode = nativeToJsModes.ONLINE_EVENT;
+ }
+
+ bridgeSecret = +prompt('', 'gap_init:' + nativeToJsBridgeMode);
+ channel.onNativeReady.fire();
+};
+
+function pollOnceFromOnlineEvent() {
+ pollOnce(true);
+}
+
+function pollOnce(opt_fromOnlineEvent) {
+ if (bridgeSecret < 0) {
+ // This can happen when the NativeToJsMessageQueue resets the online state on page transitions.
+ // We know there's nothing to retrieve, so no need to poll.
+ return;
+ }
+ var msgs = nativeApiProvider.get().retrieveJsMessages(bridgeSecret, !!opt_fromOnlineEvent);
+ if (msgs) {
+ messagesFromNative.push(msgs);
+ // Process sync since we know we're already top-of-stack.
+ processMessages();
+ }
+}
+
+function pollingTimerFunc() {
+ if (pollEnabled) {
+ pollOnce();
+ setTimeout(pollingTimerFunc, 50);
+ }
+}
+
+function hookOnlineApis() {
+ function proxyEvent(e) {
+ cordova.fireWindowEvent(e.type);
+ }
+ // The network module takes care of firing online and offline events.
+ // It currently fires them only on document though, so we bridge them
+ // to window here (while first listening for exec()-releated online/offline
+ // events).
+ window.addEventListener('online', pollOnceFromOnlineEvent, false);
+ window.addEventListener('offline', pollOnceFromOnlineEvent, false);
+ cordova.addWindowEventHandler('online');
+ cordova.addWindowEventHandler('offline');
+ document.addEventListener('online', proxyEvent, false);
+ document.addEventListener('offline', proxyEvent, false);
+}
+
+hookOnlineApis();
+
+androidExec.jsToNativeModes = jsToNativeModes;
+androidExec.nativeToJsModes = nativeToJsModes;
+
+androidExec.setJsToNativeBridgeMode = function(mode) {
+ if (mode == jsToNativeModes.JS_OBJECT && !window._cordovaNative) {
+ mode = jsToNativeModes.PROMPT;
+ }
+ nativeApiProvider.setPreferPrompt(mode == jsToNativeModes.PROMPT);
+ jsToNativeBridgeMode = mode;
+};
+
+androidExec.setNativeToJsBridgeMode = function(mode) {
+ if (mode == nativeToJsBridgeMode) {
+ return;
+ }
+ if (nativeToJsBridgeMode == nativeToJsModes.POLLING) {
+ pollEnabled = false;
+ }
+
+ nativeToJsBridgeMode = mode;
+ // Tell the native side to switch modes.
+ // Otherwise, it will be set by androidExec.init()
+ if (bridgeSecret >= 0) {
+ nativeApiProvider.get().setNativeToJsBridgeMode(bridgeSecret, mode);
+ }
+
+ if (mode == nativeToJsModes.POLLING) {
+ pollEnabled = true;
+ setTimeout(pollingTimerFunc, 1);
+ }
+};
+
+function buildPayload(payload, message) {
+ var payloadKind = message.charAt(0);
+ if (payloadKind == 's') {
+ payload.push(message.slice(1));
+ } else if (payloadKind == 't') {
+ payload.push(true);
+ } else if (payloadKind == 'f') {
+ payload.push(false);
+ } else if (payloadKind == 'N') {
+ payload.push(null);
+ } else if (payloadKind == 'n') {
+ payload.push(+message.slice(1));
+ } else if (payloadKind == 'A') {
+ var data = message.slice(1);
+ payload.push(base64.toArrayBuffer(data));
+ } else if (payloadKind == 'S') {
+ payload.push(window.atob(message.slice(1)));
+ } else if (payloadKind == 'M') {
+ var multipartMessages = message.slice(1);
+ while (multipartMessages !== "") {
+ var spaceIdx = multipartMessages.indexOf(' ');
+ var msgLen = +multipartMessages.slice(0, spaceIdx);
+ var multipartMessage = multipartMessages.substr(spaceIdx + 1, msgLen);
+ multipartMessages = multipartMessages.slice(spaceIdx + msgLen + 1);
+ buildPayload(payload, multipartMessage);
+ }
+ } else {
+ payload.push(JSON.parse(message));
+ }
+}
+
+// Processes a single message, as encoded by NativeToJsMessageQueue.java.
+function processMessage(message) {
+ var firstChar = message.charAt(0);
+ if (firstChar == 'J') {
+ // This is deprecated on the .java side. It doesn't work with CSP enabled.
+ eval(message.slice(1));
+ } else if (firstChar == 'S' || firstChar == 'F') {
+ var success = firstChar == 'S';
+ var keepCallback = message.charAt(1) == '1';
+ var spaceIdx = message.indexOf(' ', 2);
+ var status = +message.slice(2, spaceIdx);
+ var nextSpaceIdx = message.indexOf(' ', spaceIdx + 1);
+ var callbackId = message.slice(spaceIdx + 1, nextSpaceIdx);
+ var payloadMessage = message.slice(nextSpaceIdx + 1);
+ var payload = [];
+ buildPayload(payload, payloadMessage);
+ cordova.callbackFromNative(callbackId, success, status, payload, keepCallback);
+ } else {
+ console.log("processMessage failed: invalid message: " + JSON.stringify(message));
+ }
+}
+
+function processMessages() {
+ // Check for the reentrant case.
+ if (isProcessing) {
+ return;
+ }
+ if (messagesFromNative.length === 0) {
+ return;
+ }
+ isProcessing = true;
+ try {
+ var msg = popMessageFromQueue();
+ // The Java side can send a * message to indicate that it
+ // still has messages waiting to be retrieved.
+ if (msg == '*' && messagesFromNative.length === 0) {
+ nextTick(pollOnce);
+ return;
+ }
+ processMessage(msg);
+ } finally {
+ isProcessing = false;
+ if (messagesFromNative.length > 0) {
+ nextTick(processMessages);
+ }
+ }
+}
+
+function popMessageFromQueue() {
+ var messageBatch = messagesFromNative.shift();
+ if (messageBatch == '*') {
+ return '*';
+ }
+
+ var spaceIdx = messageBatch.indexOf(' ');
+ var msgLen = +messageBatch.slice(0, spaceIdx);
+ var message = messageBatch.substr(spaceIdx + 1, msgLen);
+ messageBatch = messageBatch.slice(spaceIdx + msgLen + 1);
+ if (messageBatch) {
+ messagesFromNative.unshift(messageBatch);
+ }
+ return message;
+}
+
+module.exports = androidExec;
+
+});
+
+// file: src/common/exec/proxy.js
+define("cordova/exec/proxy", function(require, exports, module) {
+
+// internal map of proxy function
+var CommandProxyMap = {};
+
+module.exports = {
+
+ // example: cordova.commandProxy.add("Accelerometer",{getCurrentAcceleration: function(successCallback, errorCallback, options) {...},...);
+ add: function (id, proxyObj) {
+ console.log('adding proxy for ' + id);
+ CommandProxyMap[id] = proxyObj;
+ return proxyObj;
+ },
+
+ // cordova.commandProxy.remove("Accelerometer");
+ remove: function (id) {
+ var proxy = CommandProxyMap[id];
+ delete CommandProxyMap[id];
+ CommandProxyMap[id] = null;
+ return proxy;
+ },
+
+ get: function (service, action) {
+ return (CommandProxyMap[service] ? CommandProxyMap[service][action] : null);
+ }
+};
+
+});
+
+// file: src/common/init.js
+define("cordova/init", function(require, exports, module) {
+
+var channel = require('cordova/channel');
+var cordova = require('cordova');
+var modulemapper = require('cordova/modulemapper');
+var platform = require('cordova/platform');
+var pluginloader = require('cordova/pluginloader');
+var utils = require('cordova/utils');
+
+var platformInitChannelsArray = [channel.onNativeReady, channel.onPluginsReady];
+
+function logUnfiredChannels (arr) {
+ for (var i = 0; i < arr.length; ++i) {
+ if (arr[i].state !== 2) {
+ console.log('Channel not fired: ' + arr[i].type);
+ }
+ }
+}
+
+window.setTimeout(function () {
+ if (channel.onDeviceReady.state !== 2) {
+ console.log('deviceready has not fired after 5 seconds.');
+ logUnfiredChannels(platformInitChannelsArray);
+ logUnfiredChannels(channel.deviceReadyChannelsArray);
+ }
+}, 5000);
+
+// Replace navigator before any modules are required(), to ensure it happens as soon as possible.
+// We replace it so that properties that can't be clobbered can instead be overridden.
+function replaceNavigator (origNavigator) {
+ var CordovaNavigator = function () {};
+ CordovaNavigator.prototype = origNavigator;
+ var newNavigator = new CordovaNavigator();
+ // This work-around really only applies to new APIs that are newer than Function.bind.
+ // Without it, APIs such as getGamepads() break.
+ if (CordovaNavigator.bind) {
+ for (var key in origNavigator) {
+ if (typeof origNavigator[key] === 'function') {
+ newNavigator[key] = origNavigator[key].bind(origNavigator);
+ } else {
+ (function (k) {
+ utils.defineGetterSetter(newNavigator, key, function () {
+ return origNavigator[k];
+ });
+ })(key);
+ }
+ }
+ }
+ return newNavigator;
+}
+
+if (window.navigator) {
+ window.navigator = replaceNavigator(window.navigator);
+}
+
+if (!window.console) {
+ window.console = {
+ log: function () {}
+ };
+}
+if (!window.console.warn) {
+ window.console.warn = function (msg) {
+ this.log('warn: ' + msg);
+ };
+}
+
+// Register pause, resume and deviceready channels as events on document.
+channel.onPause = cordova.addDocumentEventHandler('pause');
+channel.onResume = cordova.addDocumentEventHandler('resume');
+channel.onActivated = cordova.addDocumentEventHandler('activated');
+channel.onDeviceReady = cordova.addStickyDocumentEventHandler('deviceready');
+
+// Listen for DOMContentLoaded and notify our channel subscribers.
+if (document.readyState === 'complete' || document.readyState === 'interactive') {
+ channel.onDOMContentLoaded.fire();
+} else {
+ document.addEventListener('DOMContentLoaded', function () {
+ channel.onDOMContentLoaded.fire();
+ }, false);
+}
+
+// _nativeReady is global variable that the native side can set
+// to signify that the native code is ready. It is a global since
+// it may be called before any cordova JS is ready.
+if (window._nativeReady) {
+ channel.onNativeReady.fire();
+}
+
+modulemapper.clobbers('cordova', 'cordova');
+modulemapper.clobbers('cordova/exec', 'cordova.exec');
+modulemapper.clobbers('cordova/exec', 'Cordova.exec');
+
+// Call the platform-specific initialization.
+platform.bootstrap && platform.bootstrap();
+
+// Wrap in a setTimeout to support the use-case of having plugin JS appended to cordova.js.
+// The delay allows the attached modules to be defined before the plugin loader looks for them.
+setTimeout(function () {
+ pluginloader.load(function () {
+ channel.onPluginsReady.fire();
+ });
+}, 0);
+
+/**
+ * Create all cordova objects once native side is ready.
+ */
+channel.join(function () {
+ modulemapper.mapModules(window);
+
+ platform.initialize && platform.initialize();
+
+ // Fire event to notify that all objects are created
+ channel.onCordovaReady.fire();
+
+ // Fire onDeviceReady event once page has fully loaded, all
+ // constructors have run and cordova info has been received from native
+ // side.
+ channel.join(function () {
+ require('cordova').fireDocumentEvent('deviceready');
+ }, channel.deviceReadyChannelsArray);
+
+}, platformInitChannelsArray);
+
+});
+
+// file: src/common/init_b.js
+define("cordova/init_b", function(require, exports, module) {
+
+var channel = require('cordova/channel');
+var cordova = require('cordova');
+var modulemapper = require('cordova/modulemapper');
+var platform = require('cordova/platform');
+var pluginloader = require('cordova/pluginloader');
+var utils = require('cordova/utils');
+
+var platformInitChannelsArray = [channel.onDOMContentLoaded, channel.onNativeReady, channel.onPluginsReady];
+
+// setting exec
+cordova.exec = require('cordova/exec');
+
+function logUnfiredChannels (arr) {
+ for (var i = 0; i < arr.length; ++i) {
+ if (arr[i].state !== 2) {
+ console.log('Channel not fired: ' + arr[i].type);
+ }
+ }
+}
+
+window.setTimeout(function () {
+ if (channel.onDeviceReady.state !== 2) {
+ console.log('deviceready has not fired after 5 seconds.');
+ logUnfiredChannels(platformInitChannelsArray);
+ logUnfiredChannels(channel.deviceReadyChannelsArray);
+ }
+}, 5000);
+
+// Replace navigator before any modules are required(), to ensure it happens as soon as possible.
+// We replace it so that properties that can't be clobbered can instead be overridden.
+function replaceNavigator (origNavigator) {
+ var CordovaNavigator = function () {};
+ CordovaNavigator.prototype = origNavigator;
+ var newNavigator = new CordovaNavigator();
+ // This work-around really only applies to new APIs that are newer than Function.bind.
+ // Without it, APIs such as getGamepads() break.
+ if (CordovaNavigator.bind) {
+ for (var key in origNavigator) {
+ if (typeof origNavigator[key] === 'function') {
+ newNavigator[key] = origNavigator[key].bind(origNavigator);
+ } else {
+ (function (k) {
+ utils.defineGetterSetter(newNavigator, key, function () {
+ return origNavigator[k];
+ });
+ })(key);
+ }
+ }
+ }
+ return newNavigator;
+}
+if (window.navigator) {
+ window.navigator = replaceNavigator(window.navigator);
+}
+
+if (!window.console) {
+ window.console = {
+ log: function () {}
+ };
+}
+if (!window.console.warn) {
+ window.console.warn = function (msg) {
+ this.log('warn: ' + msg);
+ };
+}
+
+// Register pause, resume and deviceready channels as events on document.
+channel.onPause = cordova.addDocumentEventHandler('pause');
+channel.onResume = cordova.addDocumentEventHandler('resume');
+channel.onActivated = cordova.addDocumentEventHandler('activated');
+channel.onDeviceReady = cordova.addStickyDocumentEventHandler('deviceready');
+
+// Listen for DOMContentLoaded and notify our channel subscribers.
+if (document.readyState === 'complete' || document.readyState === 'interactive') {
+ channel.onDOMContentLoaded.fire();
+} else {
+ document.addEventListener('DOMContentLoaded', function () {
+ channel.onDOMContentLoaded.fire();
+ }, false);
+}
+
+// _nativeReady is global variable that the native side can set
+// to signify that the native code is ready. It is a global since
+// it may be called before any cordova JS is ready.
+if (window._nativeReady) {
+ channel.onNativeReady.fire();
+}
+
+// Call the platform-specific initialization.
+platform.bootstrap && platform.bootstrap();
+
+// Wrap in a setTimeout to support the use-case of having plugin JS appended to cordova.js.
+// The delay allows the attached modules to be defined before the plugin loader looks for them.
+setTimeout(function () {
+ pluginloader.load(function () {
+ channel.onPluginsReady.fire();
+ });
+}, 0);
+
+/**
+ * Create all cordova objects once native side is ready.
+ */
+channel.join(function () {
+ modulemapper.mapModules(window);
+
+ platform.initialize && platform.initialize();
+
+ // Fire event to notify that all objects are created
+ channel.onCordovaReady.fire();
+
+ // Fire onDeviceReady event once page has fully loaded, all
+ // constructors have run and cordova info has been received from native
+ // side.
+ channel.join(function () {
+ require('cordova').fireDocumentEvent('deviceready');
+ }, channel.deviceReadyChannelsArray);
+
+}, platformInitChannelsArray);
+
+});
+
+// file: src/common/modulemapper.js
+define("cordova/modulemapper", function(require, exports, module) {
+
+var builder = require('cordova/builder');
+var moduleMap = define.moduleMap; // eslint-disable-line no-undef
+var symbolList;
+var deprecationMap;
+
+exports.reset = function () {
+ symbolList = [];
+ deprecationMap = {};
+};
+
+function addEntry (strategy, moduleName, symbolPath, opt_deprecationMessage) {
+ if (!(moduleName in moduleMap)) {
+ throw new Error('Module ' + moduleName + ' does not exist.');
+ }
+ symbolList.push(strategy, moduleName, symbolPath);
+ if (opt_deprecationMessage) {
+ deprecationMap[symbolPath] = opt_deprecationMessage;
+ }
+}
+
+// Note: Android 2.3 does have Function.bind().
+exports.clobbers = function (moduleName, symbolPath, opt_deprecationMessage) {
+ addEntry('c', moduleName, symbolPath, opt_deprecationMessage);
+};
+
+exports.merges = function (moduleName, symbolPath, opt_deprecationMessage) {
+ addEntry('m', moduleName, symbolPath, opt_deprecationMessage);
+};
+
+exports.defaults = function (moduleName, symbolPath, opt_deprecationMessage) {
+ addEntry('d', moduleName, symbolPath, opt_deprecationMessage);
+};
+
+exports.runs = function (moduleName) {
+ addEntry('r', moduleName, null);
+};
+
+function prepareNamespace (symbolPath, context) {
+ if (!symbolPath) {
+ return context;
+ }
+ var parts = symbolPath.split('.');
+ var cur = context;
+ for (var i = 0, part; part = parts[i]; ++i) { // eslint-disable-line no-cond-assign
+ cur = cur[part] = cur[part] || {};
+ }
+ return cur;
+}
+
+exports.mapModules = function (context) {
+ var origSymbols = {};
+ context.CDV_origSymbols = origSymbols;
+ for (var i = 0, len = symbolList.length; i < len; i += 3) {
+ var strategy = symbolList[i];
+ var moduleName = symbolList[i + 1];
+ var module = require(moduleName);
+ //
+ if (strategy === 'r') {
+ continue;
+ }
+ var symbolPath = symbolList[i + 2];
+ var lastDot = symbolPath.lastIndexOf('.');
+ var namespace = symbolPath.substr(0, lastDot);
+ var lastName = symbolPath.substr(lastDot + 1);
+
+ var deprecationMsg = symbolPath in deprecationMap ? 'Access made to deprecated symbol: ' + symbolPath + '. ' + deprecationMsg : null;
+ var parentObj = prepareNamespace(namespace, context);
+ var target = parentObj[lastName];
+
+ if (strategy === 'm' && target) {
+ builder.recursiveMerge(target, module);
+ } else if ((strategy === 'd' && !target) || (strategy !== 'd')) {
+ if (!(symbolPath in origSymbols)) {
+ origSymbols[symbolPath] = target;
+ }
+ builder.assignOrWrapInDeprecateGetter(parentObj, lastName, module, deprecationMsg);
+ }
+ }
+};
+
+exports.getOriginalSymbol = function (context, symbolPath) {
+ var origSymbols = context.CDV_origSymbols;
+ if (origSymbols && (symbolPath in origSymbols)) {
+ return origSymbols[symbolPath];
+ }
+ var parts = symbolPath.split('.');
+ var obj = context;
+ for (var i = 0; i < parts.length; ++i) {
+ obj = obj && obj[parts[i]];
+ }
+ return obj;
+};
+
+exports.reset();
+
+});
+
+// file: src/common/modulemapper_b.js
+define("cordova/modulemapper_b", function(require, exports, module) {
+
+var builder = require('cordova/builder');
+var symbolList = [];
+var deprecationMap;
+
+exports.reset = function () {
+ symbolList = [];
+ deprecationMap = {};
+};
+
+function addEntry (strategy, moduleName, symbolPath, opt_deprecationMessage) {
+ symbolList.push(strategy, moduleName, symbolPath);
+ if (opt_deprecationMessage) {
+ deprecationMap[symbolPath] = opt_deprecationMessage;
+ }
+}
+
+// Note: Android 2.3 does have Function.bind().
+exports.clobbers = function (moduleName, symbolPath, opt_deprecationMessage) {
+ addEntry('c', moduleName, symbolPath, opt_deprecationMessage);
+};
+
+exports.merges = function (moduleName, symbolPath, opt_deprecationMessage) {
+ addEntry('m', moduleName, symbolPath, opt_deprecationMessage);
+};
+
+exports.defaults = function (moduleName, symbolPath, opt_deprecationMessage) {
+ addEntry('d', moduleName, symbolPath, opt_deprecationMessage);
+};
+
+exports.runs = function (moduleName) {
+ addEntry('r', moduleName, null);
+};
+
+function prepareNamespace (symbolPath, context) {
+ if (!symbolPath) {
+ return context;
+ }
+ var parts = symbolPath.split('.');
+ var cur = context;
+ for (var i = 0, part; part = parts[i]; ++i) { // eslint-disable-line no-cond-assign
+ cur = cur[part] = cur[part] || {};
+ }
+ return cur;
+}
+
+exports.mapModules = function (context) {
+ var origSymbols = {};
+ context.CDV_origSymbols = origSymbols;
+ for (var i = 0, len = symbolList.length; i < len; i += 3) {
+ var strategy = symbolList[i];
+ var moduleName = symbolList[i + 1];
+ var module = require(moduleName);
+ //
+ if (strategy === 'r') {
+ continue;
+ }
+ var symbolPath = symbolList[i + 2];
+ var lastDot = symbolPath.lastIndexOf('.');
+ var namespace = symbolPath.substr(0, lastDot);
+ var lastName = symbolPath.substr(lastDot + 1);
+
+ var deprecationMsg = symbolPath in deprecationMap ? 'Access made to deprecated symbol: ' + symbolPath + '. ' + deprecationMsg : null;
+ var parentObj = prepareNamespace(namespace, context);
+ var target = parentObj[lastName];
+
+ if (strategy === 'm' && target) {
+ builder.recursiveMerge(target, module);
+ } else if ((strategy === 'd' && !target) || (strategy !== 'd')) {
+ if (!(symbolPath in origSymbols)) {
+ origSymbols[symbolPath] = target;
+ }
+ builder.assignOrWrapInDeprecateGetter(parentObj, lastName, module, deprecationMsg);
+ }
+ }
+};
+
+exports.getOriginalSymbol = function (context, symbolPath) {
+ var origSymbols = context.CDV_origSymbols;
+ if (origSymbols && (symbolPath in origSymbols)) {
+ return origSymbols[symbolPath];
+ }
+ var parts = symbolPath.split('.');
+ var obj = context;
+ for (var i = 0; i < parts.length; ++i) {
+ obj = obj && obj[parts[i]];
+ }
+ return obj;
+};
+
+exports.reset();
+
+});
+
+// file: /Users/steveng/repo/cordova/cordova-android/cordova-js-src/platform.js
+define("cordova/platform", function(require, exports, module) {
+
+// The last resume event that was received that had the result of a plugin call.
+var lastResumeEvent = null;
+
+module.exports = {
+ id: 'android',
+ bootstrap: function() {
+ var channel = require('cordova/channel'),
+ cordova = require('cordova'),
+ exec = require('cordova/exec'),
+ modulemapper = require('cordova/modulemapper');
+
+ // Get the shared secret needed to use the bridge.
+ exec.init();
+
+ // TODO: Extract this as a proper plugin.
+ modulemapper.clobbers('cordova/plugin/android/app', 'navigator.app');
+
+ var APP_PLUGIN_NAME = Number(cordova.platformVersion.split('.')[0]) >= 4 ? 'CoreAndroid' : 'App';
+
+ // Inject a listener for the backbutton on the document.
+ var backButtonChannel = cordova.addDocumentEventHandler('backbutton');
+ backButtonChannel.onHasSubscribersChange = function() {
+ // If we just attached the first handler or detached the last handler,
+ // let native know we need to override the back button.
+ exec(null, null, APP_PLUGIN_NAME, "overrideBackbutton", [this.numHandlers == 1]);
+ };
+
+ // Add hardware MENU and SEARCH button handlers
+ cordova.addDocumentEventHandler('menubutton');
+ cordova.addDocumentEventHandler('searchbutton');
+
+ function bindButtonChannel(buttonName) {
+ // generic button bind used for volumeup/volumedown buttons
+ var volumeButtonChannel = cordova.addDocumentEventHandler(buttonName + 'button');
+ volumeButtonChannel.onHasSubscribersChange = function() {
+ exec(null, null, APP_PLUGIN_NAME, "overrideButton", [buttonName, this.numHandlers == 1]);
+ };
+ }
+ // Inject a listener for the volume buttons on the document.
+ bindButtonChannel('volumeup');
+ bindButtonChannel('volumedown');
+
+ // The resume event is not "sticky", but it is possible that the event
+ // will contain the result of a plugin call. We need to ensure that the
+ // plugin result is delivered even after the event is fired (CB-10498)
+ var cordovaAddEventListener = document.addEventListener;
+
+ document.addEventListener = function(evt, handler, capture) {
+ cordovaAddEventListener(evt, handler, capture);
+
+ if (evt === 'resume' && lastResumeEvent) {
+ handler(lastResumeEvent);
+ }
+ };
+
+ // Let native code know we are all done on the JS side.
+ // Native code will then un-hide the WebView.
+ channel.onCordovaReady.subscribe(function() {
+ exec(onMessageFromNative, null, APP_PLUGIN_NAME, 'messageChannel', []);
+ exec(null, null, APP_PLUGIN_NAME, "show", []);
+ });
+ }
+};
+
+function onMessageFromNative(msg) {
+ var cordova = require('cordova');
+ var action = msg.action;
+
+ switch (action)
+ {
+ // Button events
+ case 'backbutton':
+ case 'menubutton':
+ case 'searchbutton':
+ // App life cycle events
+ case 'pause':
+ // Volume events
+ case 'volumedownbutton':
+ case 'volumeupbutton':
+ cordova.fireDocumentEvent(action);
+ break;
+ case 'resume':
+ if(arguments.length > 1 && msg.pendingResult) {
+ if(arguments.length === 2) {
+ msg.pendingResult.result = arguments[1];
+ } else {
+ // The plugin returned a multipart message
+ var res = [];
+ for(var i = 1; i < arguments.length; i++) {
+ res.push(arguments[i]);
+ }
+ msg.pendingResult.result = res;
+ }
+
+ // Save the plugin result so that it can be delivered to the js
+ // even if they miss the initial firing of the event
+ lastResumeEvent = msg;
+ }
+ cordova.fireDocumentEvent(action, msg);
+ break;
+ default:
+ throw new Error('Unknown event action ' + action);
+ }
+}
+
+});
+
+// file: /Users/steveng/repo/cordova/cordova-android/cordova-js-src/plugin/android/app.js
+define("cordova/plugin/android/app", function(require, exports, module) {
+
+var exec = require('cordova/exec');
+var APP_PLUGIN_NAME = Number(require('cordova').platformVersion.split('.')[0]) >= 4 ? 'CoreAndroid' : 'App';
+
+module.exports = {
+ /**
+ * Clear the resource cache.
+ */
+ clearCache:function() {
+ exec(null, null, APP_PLUGIN_NAME, "clearCache", []);
+ },
+
+ /**
+ * Load the url into the webview or into new browser instance.
+ *
+ * @param url The URL to load
+ * @param props Properties that can be passed in to the activity:
+ * wait: int => wait msec before loading URL
+ * loadingDialog: "Title,Message" => display a native loading dialog
+ * loadUrlTimeoutValue: int => time in msec to wait before triggering a timeout error
+ * clearHistory: boolean => clear webview history (default=false)
+ * openExternal: boolean => open in a new browser (default=false)
+ *
+ * Example:
+ * navigator.app.loadUrl("http://server/myapp/index.html", {wait:2000, loadingDialog:"Wait,Loading App", loadUrlTimeoutValue: 60000});
+ */
+ loadUrl:function(url, props) {
+ exec(null, null, APP_PLUGIN_NAME, "loadUrl", [url, props]);
+ },
+
+ /**
+ * Cancel loadUrl that is waiting to be loaded.
+ */
+ cancelLoadUrl:function() {
+ exec(null, null, APP_PLUGIN_NAME, "cancelLoadUrl", []);
+ },
+
+ /**
+ * Clear web history in this web view.
+ * Instead of BACK button loading the previous web page, it will exit the app.
+ */
+ clearHistory:function() {
+ exec(null, null, APP_PLUGIN_NAME, "clearHistory", []);
+ },
+
+ /**
+ * Go to previous page displayed.
+ * This is the same as pressing the backbutton on Android device.
+ */
+ backHistory:function() {
+ exec(null, null, APP_PLUGIN_NAME, "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.
+ *
+ * Note: The user should not have to call this method. Instead, when the user
+ * registers for the "backbutton" event, this is automatically done.
+ *
+ * @param override T=override, F=cancel override
+ */
+ overrideBackbutton:function(override) {
+ exec(null, null, APP_PLUGIN_NAME, "overrideBackbutton", [override]);
+ },
+
+ /**
+ * Override the default behavior of the Android volume button.
+ * If overridden, when the volume button is pressed, the "volume[up|down]button"
+ * JavaScript event will be fired.
+ *
+ * Note: The user should not have to call this method. Instead, when the user
+ * registers for the "volume[up|down]button" event, this is automatically done.
+ *
+ * @param button volumeup, volumedown
+ * @param override T=override, F=cancel override
+ */
+ overrideButton:function(button, override) {
+ exec(null, null, APP_PLUGIN_NAME, "overrideButton", [button, override]);
+ },
+
+ /**
+ * Exit and terminate the application.
+ */
+ exitApp:function() {
+ return exec(null, null, APP_PLUGIN_NAME, "exitApp", []);
+ }
+};
+
+});
+
+// file: src/common/pluginloader.js
+define("cordova/pluginloader", function(require, exports, module) {
+
+var modulemapper = require('cordova/modulemapper');
+
+// Helper function to inject a
+